feat: assign and delete inbox artifacts

This commit is contained in:
2026-06-05 02:15:27 +08:00
parent a96a316883
commit 6eaa4cda49
12 changed files with 324 additions and 8 deletions
+63 -1
View File
@@ -151,6 +151,18 @@ async function runReadyScenario(cdp, url) {
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
await assertText(cdp, 'pasted-smoke.png', 'inbox: clipboard image captured')
await assertText(cdp, 'Изображение', 'inbox: clipboard image kind visible')
await clickInboxItemButton(cdp, 'smoke-drop-folder', 'Разложить')
await waitForSelector(cdp, '.modal input[type="text"]')
await setInputValue(cdp, '.modal input[type="text"]', 'Smoke')
await waitForSelector(cdp, '.assign-search-result')
await assertEval(cdp, `document.querySelector('.assign-search-result')?.innerText.includes('Smoke Project')`, 'inbox: assign target search works')
await clickText(cdp, '.assign-search-result', 'Smoke Project')
await clickText(cdp, '.modal-actions .btn', 'Разложить')
await waitForGone(cdp, '.modal-overlay')
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('smoke-drop-folder')`, 'inbox: assigned item leaves inbox')
await clickInboxItemButton(cdp, 'pasted-smoke.png', 'Удалить')
await clickText(cdp, '.overlay .btn', 'Удалить')
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('pasted-smoke.png')`, 'inbox: deleted item leaves inbox')
await screenshot(cdp, 'inbox.png')
await clickText(cdp, '.inbox-item', 'Inbox Smoke Item')
await assertText(cdp, 'Inbox Smoke Item', 'inbox: item opens from list')
@@ -392,6 +404,24 @@ async function clickFolderOpenButton(cdp, name) {
await sleep(300)
}
async function clickInboxItemButton(cdp, title, buttonText) {
const ok = await evalValue(cdp, `
(() => {
const norm = (value) => (value || '').replace(/\\s+/g, ' ').trim();
const row = [...document.querySelectorAll('.inbox-item')]
.find((node) => norm(node.innerText || node.textContent).includes(${JSON.stringify(title)}));
if (!row) return false;
const btn = [...row.querySelectorAll('button')]
.find((node) => norm(node.innerText || node.textContent).includes(${JSON.stringify(buttonText)}));
if (!btn) return false;
btn.click();
return true;
})()
`)
if (!ok) throw new Error(`Inbox item button not found: ${title} -> ${buttonText}`)
await sleep(250)
}
async function evalValue(cdp, expression) {
const result = await cdp.send('Runtime.evaluate', {
expression,
@@ -688,6 +718,20 @@ function wailsMockSource() {
return node?.children || [];
}
function detachNode(id, items = state.nodes) {
const idx = items.findIndex((node) => node.id === id);
if (idx >= 0) {
const [node] = items.splice(idx, 1);
return node;
}
for (const item of items) {
if (!item.children) continue;
const found = detachNode(id, item.children);
if (found) return found;
}
return null;
}
function readyStatus() {
return {
status: 'ready',
@@ -762,6 +806,24 @@ function wailsMockSource() {
state.nodes.push(node);
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
},
AssignInboxNode: async (nodeId, targetParentId) => {
const node = detachNode(nodeId);
const parent = findNode(targetParentId);
if (!node || !parent) throw new Error('assign target not found');
node.captureInbox = false;
node.captureKind = '';
node.captureSource = '';
node.parent_id = targetParentId;
parent.children = parent.children || [];
parent.children.push(node);
parent.has_children = true;
return clone(node);
},
DeleteInboxNode: async (nodeId) => {
const node = detachNode(nodeId);
if (!node) throw new Error('inbox node not found');
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 }],
@@ -773,7 +835,7 @@ function wailsMockSource() {
SetTemplateEnabled: async () => true,
GetNodeDetail: async (id) => clone(findNode(id) || fileNodeDetails[id] || null),
GetNodeTitle: async (id) => findNode(id)?.title || '',
SearchNodes: async (query) => allNodes().filter((node) => node.title.toLowerCase().includes(String(query || '').toLowerCase())).map((node) => ({ id: node.id, title: node.title, path: '/Smoke/' + node.title })),
SearchNodes: async (query) => allNodes().filter((node) => node.title.toLowerCase().includes(String(query || '').toLowerCase())).map((node) => ({ id: node.id, title: node.title, path: '/Smoke/' + node.title, type: node.type })),
CreateNodeFromTemplate: async (parentId, title, templateId) => {
const id = 'node-created-' + Date.now();
const parent = parentId ? findNode(parentId) : null;