From 03dbede414abe0020b5593ea48ba44b4c0e2227f Mon Sep 17 00:00:00 2001 From: mirivlad Date: Wed, 1 Jul 2026 08:09:28 +0800 Subject: [PATCH] Cover Files duplicate context action --- frontend/e2e/files-plugin.spec.js | 16 ++++++++++++++++ frontend/src/lib/test/wails-mock.js | 25 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/frontend/e2e/files-plugin.spec.js b/frontend/e2e/files-plugin.spec.js index de619e6..c2c2026 100644 --- a/frontend/e2e/files-plugin.spec.js +++ b/frontend/e2e/files-plugin.spec.js @@ -156,6 +156,22 @@ test.describe('G: Files Plugin', () => { await expect(page.locator('[data-file-name="CutMe"]')).toHaveCount(0); }); + test('files explorer duplicates a file from the context menu', 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-file-name="project-only.txt"]').click({ button: 'right' }); + await page.locator('[data-files-menu-action="duplicate"]').click(); + + await expect(page.locator('[data-file-name="project-only.txt"]')).toBeVisible(); + await expect(page.locator('[data-file-name="project-only (copy).txt"]')).toBeVisible(); + + await page.locator('[data-file-name="project-only.txt"]').click({ button: 'right' }); + await page.locator('[data-files-menu-action="duplicate"]').click(); + await expect(page.locator('[data-file-name="project-only (copy 2).txt"]')).toBeVisible(); + }); + test('files explorer supports multiselect and internal drag/drop move', 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 1c8a33e..4fa4f43 100644 --- a/frontend/src/lib/test/wails-mock.js +++ b/frontend/src/lib/test/wails-mock.js @@ -897,6 +897,30 @@ function cutSelection() { setClipboard('cut', selectedEntries()); } function copySelection() { setClipboard('copy', selectedEntries().filter(function (item) { return item.type !== 'folder'; })); } function uniqueName(name, occupied) { if (!occupied[name]) return name; var dot = name.lastIndexOf('.'); var b = dot > 0 ? name.slice(0, dot) : name; var x = dot > 0 ? name.slice(dot) : ''; for (var i = 2; i < 100; i++) { var c = b + ' (' + i + ')' + x; if (!occupied[c]) return c; } return b + ' (' + Date.now() + ')' + x; } + function duplicate(item) { + if (!item || item.type === 'folder') return; + var dot = item.name.lastIndexOf('.'); + var stem = dot > 0 ? item.name.slice(0, dot) : item.name; + var extension = dot > 0 ? item.name.slice(dot) : ''; + var folder = parent(item.relativePath); + var maxAttempts = 100; + function candidatePath(n) { + var name = n === 1 ? stem + ' (copy)' + extension : stem + ' (copy ' + n + ')' + extension; + return folder ? folder + '/' + name : name; + } + function tryDuplicate(n) { + var target = candidatePath(n); + return api.files.metadata(target).then(function () { + if (n >= maxAttempts) return Promise.reject(new Error('all duplicate names are taken')); + return tryDuplicate(n + 1); + }, function () { + return api.files.readText(item.relativePath).then(function (text) { + return api.files.writeText(target, text, { createIfMissing: true, overwrite: false }); + }); + }); + } + tryDuplicate(1).then(load).catch(function (err) { console.error('[files] Duplicate failed:', err); }); + } function paste() { var clip = window.__filesClipboard; if (!clip || !clip.items || !clip.items.length) return; @@ -920,6 +944,7 @@ if (!selected[item.relativePath]) { selected = {}; selected[item.relativePath] = true; render(); } menu.appendChild(menuItem('Open', 'open', function () { open(item); })); menu.appendChild(menuItem('Rename', 'rename', function () { startRename(item); })); + if (item.type !== 'folder') menu.appendChild(menuItem('Duplicate', 'duplicate', function () { duplicate(item); })); menu.appendChild(menuItem('Cut', 'cut', cutSelection)); menu.appendChild(menuItem('Copy', 'copy', copySelection)); menu.appendChild(menuItem('Trash', 'trash', trashSelection));