feat: restore global search in app header

This commit is contained in:
2026-06-06 02:39:29 +08:00
parent cf770262e5
commit 0cd8a79049
15 changed files with 513 additions and 65 deletions
+49 -2
View File
@@ -114,6 +114,20 @@ async function runReadyScenario(cdp, url) {
await assertText(cdp, 'Верстак', 'ready: brand visible')
await assertEval(cdp, `document.querySelectorAll('.nav-item').length >= 3`, 'ready: system navigation rendered')
await assertEval(cdp, `document.querySelectorAll('.tree-item').length >= 2`, 'ready: workspace tree rendered')
await waitForSelector(cdp, '.global-search-input')
await assertEval(cdp, `document.querySelector('.global-search-input')?.placeholder === 'Поиск по Верстаку...'`, 'header: global search is visible')
await dispatchShortcut(cdp, 'k', true)
await assertEval(cdp, `document.activeElement === document.querySelector('.global-search-input')`, 'header: Ctrl+K focuses global search')
await setInputValue(cdp, '.global-search-input', 'Smoke')
await waitForSelector(cdp, '.global-search-results')
await assertText(cdp, 'Проект · Верстак / Smoke Project', 'search: node result shows type and breadcrumbs')
await assertText(cdp, 'Заметка · Верстак / Smoke Project', 'search: note result shows type and breadcrumbs')
await assertText(cdp, 'Файл · Верстак / Smoke Project / Assets', 'search: file result shows type and breadcrumbs')
await assertText(cdp, 'Ссылка · Верстак / Smoke Project', 'search: link result shows type and breadcrumbs')
await clickText(cdp, '.global-search-result', 'Smoke note')
await assertText(cdp, 'Rendered by GUI smoke.', 'search: note result opens note editor')
await dispatchBodyKeydown(cdp, 'Backspace')
await waitForGone(cdp, '.note-editor')
await screenshot(cdp, 'ready-main.png')
await click(cdp, '.sidebar-settings-btn')
@@ -136,12 +150,13 @@ async function runReadyScenario(cdp, url) {
await clickText(cdp, '.nav-item', 'Неразобранное')
await assertText(cdp, 'Неразобранное', 'inbox: system view opens')
await assertEval(cdp, `!document.querySelector('.inbox-header h2')`, 'inbox: screen title is not duplicated inside body')
await assertText(cdp, 'Inbox Smoke Item', 'inbox: captured item visible')
await assertText(cdp, 'Текст', 'inbox: capture kind visible')
await assertText(cdp, 'Буфер обмена', 'inbox: capture source visible')
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('Manual Root Item')`, 'inbox: manual root is hidden')
await setClipboardText(cdp, 'https://example.test/from-clipboard')
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
await clickText(cdp, '.header-actions .btn', 'Вставить из буфера')
await assertText(cdp, 'example.test', 'inbox: clipboard URL captured')
await assertEval(cdp, `
(() => {
@@ -171,7 +186,7 @@ async function runReadyScenario(cdp, url) {
await waitForGone(cdp, '.modal-overlay')
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('example.test')`, 'inbox: assigned link leaves inbox')
await setClipboardText(cdp, 'mirv.top')
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
await clickText(cdp, '.header-actions .btn', 'Вставить из буфера')
await assertText(cdp, 'mirv.top', 'inbox: bare domain captured as URL')
await clickInboxItemButton(cdp, 'mirv.top', 'Открыть')
await assertEval(cdp, `window.__VERSTAK_GUI_SMOKE__.state.openedUrls.includes('https://mirv.top')`, 'inbox: bare domain opens normalized URL')
@@ -219,6 +234,7 @@ async function runReadyScenario(cdp, url) {
await clickText(cdp, '.note-editor-actions .btn', 'Закрыть')
await clickText(cdp, '.nav-item', 'Корзина')
await assertEval(cdp, `!document.querySelector('.trash-header h2')`, 'trash: screen title is not duplicated inside body')
await assertText(cdp, 'Trash Smoke Folder', 'trash: deleted node 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-icon')
@@ -324,10 +340,12 @@ async function runReadyScenario(cdp, url) {
await clickText(cdp, '.nav-item', 'Сегодня')
await assertText(cdp, 'Сегодня', 'today: system view opens')
await assertEval(cdp, `!document.querySelector('.today-header h2')`, 'today: screen title is not duplicated inside body')
await assertText(cdp, 'Smoke Project', 'today: dashboard data visible')
await clickText(cdp, '.nav-item', 'Журнал')
await waitForSelector(cdp, '.journal-screen')
await assertEval(cdp, `!document.querySelector('.journal-screen h2')`, 'journal: screen title is not duplicated inside body')
await clickText(cdp, '.journal-tab', 'Журнал работы')
await screenshot(cdp, 'journal.png')
await assertEval(cdp, `document.body.innerText.toLowerCase().includes('фильтры')`, 'journal: filter section visible')
@@ -335,6 +353,7 @@ async function runReadyScenario(cdp, url) {
await clickText(cdp, '.nav-item', 'Активность')
await assertText(cdp, 'Активность', 'activity feed: system view opens')
await assertEval(cdp, `!document.querySelector('.activity-feed-header h2')`, 'activity: screen title is not duplicated inside body')
await assertText(cdp, 'Smoke activity', 'activity feed: event visible')
await click(cdp, '.nav-add-btn')
@@ -478,6 +497,24 @@ async function dispatchBodyKeydown(cdp, key) {
await sleep(150)
}
async function dispatchShortcut(cdp, key, ctrlKey = false) {
await cdp.send('Runtime.evaluate', {
expression: `
(() => {
const event = new KeyboardEvent('keydown', {
key: ${JSON.stringify(key)},
ctrlKey: ${ctrlKey ? 'true' : 'false'},
bubbles: true,
cancelable: true,
});
window.dispatchEvent(event);
})()
`,
returnByValue: true,
})
await sleep(150)
}
async function dispatchMouseBack(cdp) {
await cdp.send('Runtime.evaluate', {
expression: `
@@ -891,6 +928,7 @@ function wailsMockSource() {
};
const fileNodeDetails = {
'note-1': { id: 'note-1', title: 'Smoke note', type: 'note', parent_id: 'node-project', createdAt: now, has_children: false },
'file-folder': { id: 'file-folder', title: 'Assets', type: 'folder', parent_id: 'node-project', createdAt: now, has_children: false, children: [] },
'file-brief': { id: 'file-brief', title: 'brief.md', type: 'file', parent_id: 'node-project', createdAt: now, has_children: false },
'file-nested': { id: 'file-nested', title: 'nested.txt', type: 'file', parent_id: 'file-folder', createdAt: now, has_children: false },
@@ -1164,6 +1202,15 @@ function wailsMockSource() {
SetTemplateEnabled: async () => true,
GetNodeDetail: async (id) => clone(findNode(id) || fileNodeDetails[id] || null),
GetNodeTitle: async (id) => findNode(id)?.title || '',
Search: async (query) => {
const q = String(query || '').toLowerCase();
return [
{ nodeId: 'node-project', title: 'Smoke Project', type: 'project', path: 'Верстак / Smoke Project', snippet: '' },
{ nodeId: 'note-1', title: 'Smoke note', type: 'note', path: 'Верстак / Smoke Project', snippet: 'Rendered by GUI smoke.' },
{ nodeId: 'file-brief', title: 'brief.md', type: 'file', path: 'Верстак / Smoke Project / Assets', snippet: '' },
{ nodeId: 'link-smoke', title: 'example.test', type: 'link', path: 'Верстак / Smoke Project', snippet: 'https://example.test' },
].filter((item) => (item.title + ' ' + item.snippet + ' ' + item.path).toLowerCase().includes(q));
},
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();