fix: stabilize trash navigation and action icons
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user