diff --git a/frontend/e2e/files-plugin.spec.js b/frontend/e2e/files-plugin.spec.js index a79a1ff..f8c5182 100644 --- a/frontend/e2e/files-plugin.spec.js +++ b/frontend/e2e/files-plugin.spec.js @@ -129,6 +129,34 @@ test.describe('G: Files Plugin', () => { await expect(page.locator('[data-file-name="bad"]')).toHaveCount(0); }); + test('files explorer uses rich file icons by file type', async ({ page }) => { + await page.locator('.wt-label').filter({ hasText: 'Project' }).click(); + await openFilesTool(page); + await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 }); + + async function createFile(action, name) { + await page.locator(`[data-files-action="${action}"]`).click(); + await page.locator('[data-files-create-input]').fill(name); + await page.locator('[data-files-create-confirm]').click(); + await expect(page.locator(`[data-file-name="${name}"]`)).toBeVisible(); + } + + await createFile('new-markdown', 'IconNote.md'); + await createFile('new-text', 'Photo.png'); + await createFile('new-text', 'Manual.pdf'); + await createFile('new-text', 'Script.js'); + + const iconFor = (name) => page.locator(`[data-file-name="${name}"] .files-item-icon`); + await expect(iconFor('Notes')).toHaveAttribute('data-file-icon', 'folder'); + await expect(iconFor('project-only.txt')).toHaveAttribute('data-file-icon', 'text'); + await expect(iconFor('IconNote.md')).toHaveAttribute('data-file-icon', 'markdown'); + await expect(iconFor('Photo.png')).toHaveAttribute('data-file-icon', 'image'); + await expect(iconFor('Manual.pdf')).toHaveAttribute('data-file-icon', 'pdf'); + await expect(iconFor('Script.js')).toHaveAttribute('data-file-icon', 'code'); + await expect(iconFor('Manual.pdf')).toHaveAttribute('aria-label', 'PDF file'); + await expect(iconFor('Photo.png')).toHaveAttribute('title', 'Image file'); + }); + 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 07e8be8..cad8b4e 100644 --- a/frontend/src/lib/test/wails-mock.js +++ b/frontend/src/lib/test/wails-mock.js @@ -679,6 +679,41 @@ return '(' + function () { var SVG = ''; var FOLDER_SVG = ''; + var FILE_SVGS = { + folder: FOLDER_SVG, + generic: SVG, + markdown: '', + text: '', + image: '', + pdf: '', + code: '' + }; + var ICON_EXTENSIONS = { + md: 'markdown', markdown: 'markdown', + txt: 'text', text: 'text', log: 'text', rtf: 'text', + jpg: 'image', jpeg: 'image', png: 'image', gif: 'image', webp: 'image', svg: 'image', + pdf: 'pdf', + js: 'code', jsx: 'code', mjs: 'code', cjs: 'code', ts: 'code', tsx: 'code', + py: 'code', go: 'code', rs: 'code', css: 'code', html: 'code', json: 'code' + }; + function fileIconCategory(item) { + if (item.type === 'folder') return 'folder'; + return ICON_EXTENSIONS[(item.extension || ext(item.name)).toLowerCase()] || 'generic'; + } + function fileIconLabel(category) { + return { + folder: 'Folder', + markdown: 'Markdown file', + text: 'Text file', + image: 'Image file', + pdf: 'PDF file', + code: 'Code file', + generic: 'File' + }[category] || 'File'; + } + function fileIcon(item) { + return FILE_SVGS[fileIconCategory(item)] || FILE_SVGS.generic; + } function e(tag, attrs, children) { var node = document.createElement(tag); attrs = attrs || {}; @@ -838,6 +873,8 @@ list.appendChild(e('div', { className: 'files-header' }, [e('span', {}, ['Name']), e('span', {}, ['Type']), e('span', {}, ['Size']), e('span', {}, ['Modified']), e('span', {}, ['Actions'])])); var shown = visible(); shown.forEach(function (item) { + var iconCategory = fileIconCategory(item); + var iconLabel = fileIconLabel(iconCategory); var row = e('div', { className: 'files-item' + (selected[item.relativePath] ? ' selected' : ''), 'data-file-name': item.name, @@ -852,7 +889,7 @@ ev.dataTransfer.effectAllowed = 'move'; } }, []); - row.appendChild(e('span', { className: 'files-namecell' }, [e('span', { className: 'files-item-icon', innerHTML: item.type === 'folder' ? FOLDER_SVG : SVG }, []), e('span', { className: 'files-item-name' }, [item.name])])); + row.appendChild(e('span', { className: 'files-namecell' }, [e('span', { className: 'files-item-icon', 'data-file-icon': iconCategory, title: iconLabel, 'aria-label': iconLabel, innerHTML: fileIcon(item) }, []), e('span', { className: 'files-item-name' }, [item.name])])); row.appendChild(e('span', { className: 'files-item-meta' }, [item.type === 'folder' ? 'folder' : (item.extension || ext(item.name) || 'file')])); row.appendChild(e('span', { className: 'files-item-meta' }, [item.size ? String(item.size) : ''])); row.appendChild(e('span', { className: 'files-item-meta' }, [formatDate(item.modifiedAt)]));