fix: stabilize trash navigation and action icons

This commit is contained in:
2026-06-05 14:41:40 +08:00
parent 1fa009b1e2
commit c8aaf36533
11 changed files with 399 additions and 99 deletions
+74 -5
View File
@@ -143,7 +143,14 @@ async function runReadyScenario(cdp, url) {
await setClipboardText(cdp, 'https://example.test/from-clipboard')
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
await assertText(cdp, 'example.test', 'inbox: clipboard URL captured')
await assertText(cdp, 'Ссылка', 'inbox: clipboard URL kind visible')
await assertEval(cdp, `
(() => {
const row = [...document.querySelectorAll('.inbox-item')].find((el) => el.innerText.includes('example.test'));
if (!row) return false;
if (row.innerText.includes('Ссылка') || row.innerText.includes('URL') || row.innerText.includes('capture.kind.url')) return true;
throw new Error('URL row text: ' + row.innerText);
})()
`, 'inbox: clipboard URL kind visible')
await assertInboxItemButtonAbsent(cdp, 'example.test', 'Показать в проводнике', 'inbox: link has no explorer action')
await assertEval(cdp, `
(() => {
@@ -203,7 +210,18 @@ async function runReadyScenario(cdp, url) {
await clickText(cdp, '.nav-item', 'Корзина')
await assertText(cdp, 'Trash Smoke Folder', 'trash: deleted node visible')
await assertText(cdp, 'node-trash_Trash-Smoke-Folder', 'trash: physical entry visible')
await assertEval(cdp, `!document.body.innerText.includes('.verstak/trash') && !document.body.innerText.includes('node-trash_Trash-Smoke-Folder')`, 'trash: physical implementation entries are hidden')
await click(cdp, '.trash-row.folder .trash-row-actions .inbox-icon-btn[title="Открыть"]')
await assertText(cdp, 'trash-child.txt', 'trash: opening deleted folder shows children immediately')
await assertText(cdp, 'Корзина / Trash Smoke Folder', 'trash: current folder breadcrumb visible')
await dispatchBodyKeydown(cdp, 'Backspace')
await assertEval(cdp, `document.body.innerText.includes('Trash Smoke Folder') && !document.body.innerText.includes('trash-child.txt')`, 'trash: Backspace returns to trash root')
await click(cdp, '.trash-row.folder .trash-row-actions .inbox-icon-btn[title="Открыть"]')
await dispatchMouseBack(cdp)
await assertEval(cdp, `document.body.innerText.includes('Trash Smoke Folder') && !document.body.innerText.includes('trash-child.txt')`, 'trash: mouse Back returns to trash root')
await clickText(cdp, '.nav-item', 'Журнал')
await clickText(cdp, '.nav-item', 'Корзина')
await assertEval(cdp, `document.body.innerText.includes('Trash Smoke Folder') && !document.body.innerText.includes('trash-child.txt')`, 'trash: sidebar return uses stable root state')
await screenshot(cdp, 'trash.png')
await clickText(cdp, '.tree-label', 'Smoke Project')
@@ -223,6 +241,9 @@ async function runReadyScenario(cdp, url) {
await assertText(cdp, 'GUI smoke note', 'notes: created note appears')
await clickText(cdp, '.tab', 'Файлы')
await clickText(cdp, '.tab', 'Ссылки')
await dispatchBodyKeydown(cdp, 'Backspace')
await assertEval(cdp, `document.querySelector('.tab.active')?.innerText.includes('Файлы')`, 'navigation: Backspace returns to previous tab')
await waitForSelector(cdp, '.file-row')
await assertText(cdp, 'Assets', 'files: folder row visible')
await clickFolderOpenButton(cdp, 'Assets')
@@ -241,6 +262,8 @@ async function runReadyScenario(cdp, url) {
await waitForSelector(cdp, '.modal input[type="text"]')
await dispatchKeydown(cdp, '.modal input[type="text"]', ' ')
await assertEval(cdp, `!!document.querySelector('.modal input[type="text"]')`, 'actions: space in title input keeps modal open')
await dispatchKeydown(cdp, '.modal input[type="text"]', 'Backspace')
await assertEval(cdp, `!!document.querySelector('.modal input[type="text"]')`, 'navigation: Backspace in input does not navigate')
await setInputValue(cdp, '.modal input[type="text"]', 'GUI smoke action with spaces')
await setInputValue(cdp, '.modal input[placeholder="https://example.com"]', 'https://example.test/action')
await clickText(cdp, '.modal-actions .btn', 'Создать')
@@ -430,6 +453,33 @@ async function dispatchKeydown(cdp, selector, key) {
await sleep(100)
}
async function dispatchBodyKeydown(cdp, key) {
await cdp.send('Runtime.evaluate', {
expression: `
(() => {
document.body.focus();
const event = new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true, cancelable: true });
window.dispatchEvent(event);
})()
`,
returnByValue: true,
})
await sleep(150)
}
async function dispatchMouseBack(cdp) {
await cdp.send('Runtime.evaluate', {
expression: `
(() => {
const event = new MouseEvent('auxclick', { button: 3, bubbles: true, cancelable: true });
window.dispatchEvent(event);
})()
`,
returnByValue: true,
})
await sleep(150)
}
async function setClipboardText(cdp, value) {
await cdp.send('Runtime.evaluate', {
expression: `window.__VERSTAK_GUI_SMOKE_CLIPBOARD__ = ${JSON.stringify(value)}`,
@@ -537,7 +587,8 @@ async function evalValue(cdp, expression) {
userGesture: true,
})
if (result.exceptionDetails) {
throw new Error(result.exceptionDetails.text || 'Runtime.evaluate exception')
const detail = result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'Runtime.evaluate exception'
throw new Error(detail)
}
return result.result?.value
}
@@ -794,6 +845,11 @@ function wailsMockSource() {
links: {
'node-project': [],
},
trashNodes: [
{ id: 'node-trash', title: 'Trash Smoke Folder', type: 'folder', fsPath: 'Trash Smoke Folder', nodePath: 'Smoke / Trash Smoke Folder', deletedAt: now },
{ id: 'node-trash-child', parentId: 'node-trash', title: 'trash-child.txt', type: 'file', fsPath: 'Trash Smoke Folder/trash-child.txt', nodePath: 'Smoke / Trash Smoke Folder / trash-child.txt', deletedAt: now },
{ id: 'node-trash-nested', parentId: 'node-trash', title: 'Nested Trash Folder', type: 'folder', fsPath: 'Trash Smoke Folder/Nested Trash Folder', nodePath: 'Smoke / Trash Smoke Folder / Nested Trash Folder', deletedAt: now },
],
openedUrls: [],
};
@@ -856,7 +912,7 @@ function wailsMockSource() {
function normalizeURL(value) {
const raw = String(value || '').trim();
if (!raw || /[\s]/.test(raw) || raw.includes('@')) return '';
if (!raw || /[\\s]/.test(raw) || raw.includes('@')) return '';
try {
const parsed = new URL(raw);
return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.hostname ? raw : '';
@@ -1049,9 +1105,22 @@ function wailsMockSource() {
OpenURL: async (url) => { state.openedUrls.push(url); return true },
ListTrash: async () => clone({
trashPath: '/tmp/verstak-smoke-vault/.verstak/trash',
nodes: [{ id: 'node-trash', title: 'Trash Smoke Folder', type: 'folder', fsPath: 'Trash Smoke Folder', deletedAt: now }],
count: state.trashNodes.length,
nodes: state.trashNodes,
entries: [{ name: 'node-trash_Trash-Smoke-Folder', path: '/tmp/verstak-smoke-vault/.verstak/trash/node-trash_Trash-Smoke-Folder', isDir: true, size: 0, modifiedAt: now }],
}),
TrashCount: async () => state.trashNodes.length,
RestoreTrashNodesJSON: async (idsJSON) => {
const ids = JSON.parse(idsJSON || '[]');
state.trashNodes = state.trashNodes.filter((node) => !ids.includes(node.id));
return true;
},
PurgeTrashNodesJSON: async (idsJSON) => {
const ids = JSON.parse(idsJSON || '[]');
state.trashNodes = state.trashNodes.filter((node) => !ids.includes(node.id) && !ids.includes(node.parentId));
return true;
},
EmptyTrash: async () => { state.trashNodes = []; return true },
OpenTrashFolder: async () => true,
ListEnabledTemplates: async () => clone(templates),
AllTemplates: async () => clone(templates),