From 3c54ef55c9a92af780e5ed47885a1f3ed12a17f8 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Wed, 1 Jul 2026 18:24:55 +0800 Subject: [PATCH] Cover Files inline create validation --- frontend/e2e/files-plugin.spec.js | 15 +++++++++++++++ frontend/src/lib/test/wails-mock.js | 30 +++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/frontend/e2e/files-plugin.spec.js b/frontend/e2e/files-plugin.spec.js index 8259337..a79a1ff 100644 --- a/frontend/e2e/files-plugin.spec.js +++ b/frontend/e2e/files-plugin.spec.js @@ -114,6 +114,21 @@ test.describe('G: Files Plugin', () => { await expect(page.locator('[data-file-name="bad"]')).toHaveCount(0); }); + test('files explorer shows inline create validation errors', async ({ page }) => { + await page.locator('.wt-label').filter({ hasText: 'Project' }).click(); + await openFilesTool(page); + await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 }); + + await page.locator('[data-files-action="new-folder"]').click(); + await page.locator('[data-files-create-input]').fill('bad/name'); + await page.locator('[data-files-create-confirm]').click(); + + await expect(page.locator('[data-files-create-error]')).toBeVisible(); + await expect(page.locator('[data-files-create-error]')).toContainText('Invalid characters'); + await expect(page.locator('[data-files-create-input]')).toBeVisible(); + await expect(page.locator('[data-file-name="bad"]')).toHaveCount(0); + }); + test('files explorer restores an item from trash metadata', async ({ page }) => { await page.locator('.wt-label').filter({ hasText: 'Project' }).click(); await openFilesTool(page); diff --git a/frontend/src/lib/test/wails-mock.js b/frontend/src/lib/test/wails-mock.js index cd6a3d3..07e8be8 100644 --- a/frontend/src/lib/test/wails-mock.js +++ b/frontend/src/lib/test/wails-mock.js @@ -770,10 +770,14 @@ var list = e('div', { className: 'files-list', 'data-files-list': '' }, []); c.appendChild(list); var createPanel = e('div', { className: 'files-panel', style: 'display:none' }, []); + var createField = e('div', { style: { display: 'flex', flex: '1', flexDirection: 'column', gap: '.25rem' } }, []); var createInput = e('input', { className: 'files-create-input', 'data-files-create-input': '' }, []); - createPanel.appendChild(createInput); + var createError = e('div', { 'data-files-create-error': '', role: 'alert', style: { display: 'none', color: '#ff8a8a', fontSize: '.72rem', lineHeight: '1.2' } }, []); + createField.appendChild(createInput); + createField.appendChild(createError); + createPanel.appendChild(createField); createPanel.appendChild(e('button', { className: 'files-toolbar-btn', 'data-files-create-confirm': '', onClick: confirmCreate }, ['Create'])); - createPanel.appendChild(e('button', { className: 'files-toolbar-btn', onClick: function () { createPanel.style.display = 'none'; } }, ['Cancel'])); + createPanel.appendChild(e('button', { className: 'files-toolbar-btn', onClick: cancelCreate }, ['Cancel'])); c.appendChild(createPanel); var renamePanel = e('div', { className: 'files-panel', style: 'display:none' }, []); var renameField = e('div', { style: { display: 'flex', flex: '1', flexDirection: 'column', gap: '.25rem' } }, []); @@ -935,15 +939,28 @@ if ((itemExt === '.md' || itemExt === '.markdown') && local(item.relativePath).split('/')[0] === 'Notes') { ctx.isInsideNotesFolder = true; ctx.notesMode = true; } api.workbench.openResource({ kind: 'vault-file', path: item.relativePath, mode: 'view', extension: itemExt, context: ctx }); } - function startCreate(mode) { createMode = mode; createInput.value = ''; createPanel.style.display = 'flex'; createInput.focus(); } + function startCreate(mode) { createMode = mode; createInput.value = ''; setCreateError(''); createPanel.style.display = 'flex'; createInput.focus(); } + function setCreateError(message) { + createError.textContent = message || ''; + createError.style.display = message ? 'block' : 'none'; + createInput.setAttribute('aria-invalid', message ? 'true' : 'false'); + } + function validateCreateName(name) { + if (!name) return 'Name is required'; + if (/[\\/:*?"<>|\x00-\x1f]/.test(name)) return 'Invalid characters in name'; + if (name === '.' || name === '..' || name[0] === ' ' || name[name.length - 1] === ' ' || name[name.length - 1] === '.') return 'Invalid name'; + return ''; + } + function cancelCreate() { createMode = ''; setCreateError(''); createPanel.style.display = 'none'; } function confirmCreate() { var name = createInput.value.trim(); var mode = createMode; - if (!name) return; + var validationError = validateCreateName(name); + if (validationError) { setCreateError(validationError); return; } if (mode === 'markdown' && !/\.(md|markdown)$/i.test(name)) name += '.md'; if (mode === 'text' && !/\.[^/.]+$/.test(name)) name += '.txt'; var path = scoped(current ? current + '/' + name : name); - (mode === 'folder' ? api.files.createFolder(path) : api.files.writeText(path, '', { createIfMissing: true, overwrite: false })).then(function () { createPanel.style.display = 'none'; load(); }); + (mode === 'folder' ? api.files.createFolder(path) : api.files.writeText(path, '', { createIfMissing: true, overwrite: false })).then(function () { cancelCreate(); load(); }).catch(function (err) { setCreateError('Error: ' + ((err && err.message) ? err.message : String(err))); }); } function setRenameError(message) { renameError.textContent = message || ''; @@ -1102,7 +1119,8 @@ } menu.style.display = 'block'; menu.style.left = x + 'px'; menu.style.top = y + 'px'; } - createInput.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') confirmCreate(); }); + createInput.addEventListener('input', function () { setCreateError(''); }); + createInput.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') confirmCreate(); if (ev.key === 'Escape') cancelCreate(); }); renameInput.addEventListener('input', function () { setRenameError(''); }); renameInput.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') confirmRename(); if (ev.key === 'Escape') cancelRename(); }); list.addEventListener('contextmenu', function (ev) { ev.preventDefault(); var row = ev.target.closest('.files-item'); showMenu(ev.clientX, ev.clientY, row ? entryByPath(row.getAttribute('data-file-path')) : null); });