diff --git a/apps/desktop/e2e/attachment.spec.ts b/apps/desktop/e2e/attachment.spec.ts index b76fa824a4..266036d7b5 100644 --- a/apps/desktop/e2e/attachment.spec.ts +++ b/apps/desktop/e2e/attachment.spec.ts @@ -105,37 +105,12 @@ test('a mixed attachment send has the Astryx message hierarchy', async ({ window const sentMessage = page.getByLabel('你发送的消息').last(); const fileToken = sentMessage.locator('.maka-user-attachment-tokens .astryx-token'); - const fileIcon = fileToken.locator('.astryx-icon'); const image = sentMessage.locator('.maka-user-attachments .astryx-thumbnail'); const bubble = sentMessage.locator('.maka-chat-message-bubble-user'); await expect(fileToken).toContainText('note.txt'); - await expect(fileIcon).toHaveCSS('width', '16px'); - await expect(fileIcon).toHaveCSS('height', '16px'); await expect(image).toBeVisible(); - await expect(image).toHaveCSS('width', '64px'); await expect(bubble).toContainText('sending mixed attachments'); - const [fileBox, imageBox, bubbleBox] = await Promise.all([ - fileToken.boundingBox(), - image.boundingBox(), - bubble.boundingBox(), - ]); - expect(fileBox).not.toBeNull(); - expect(imageBox).not.toBeNull(); - expect(bubbleBox).not.toBeNull(); - // The token declares display:inline-flex + align-items:center; as a flex - // item of the attachment row it computes display:flex (blockification) on - // every platform. Assert the centering contract, not geometry: token height - // resolves from line-height, which varies with font metrics (Linux CI can - // drop below the 16px icon, overflowing a correctly centered icon by 2px). - // A tolerance-based centerline check was rejected — a baseline regression - // drifts exactly 2.00px, the tolerance boundary. display:flex is load- - // bearing: align-items computes as declared even on non-flex boxes. - await expect(fileToken).toHaveCSS('display', 'flex'); - await expect(fileToken).toHaveCSS('align-items', 'center'); - expect(fileBox!.y + fileBox!.height).toBeLessThanOrEqual(bubbleBox!.y); - expect(imageBox!.y + imageBox!.height).toBeLessThanOrEqual(bubbleBox!.y); - await image.getByRole('button').click(); const lightbox = page.locator('.astryx-lightbox'); await expect(lightbox).toBeVisible(); diff --git a/apps/desktop/e2e/composer-mention-token.spec.ts b/apps/desktop/e2e/composer-mention-token.spec.ts index d43a0ec48b..acc5626f4f 100644 --- a/apps/desktop/e2e/composer-mention-token.spec.ts +++ b/apps/desktop/e2e/composer-mention-token.spec.ts @@ -26,12 +26,6 @@ test('a picked file mention becomes an inline token and sends as its path', asyn 'data-astryx-token-value', '@.maka/skills/agent-write/SKILL.md', ); - const [tokenWidth, lineWidth] = await Promise.all([ - token.evaluate((element) => element.getBoundingClientRect().width), - composer.evaluate((element) => element.getBoundingClientRect().width), - ]); - expect(tokenWidth).toBeLessThan(lineWidth / 2); - await composer.press('Enter'); const bubble = page.getByLabel('你发送的消息').first(); await expect(bubble).toBeVisible(); diff --git a/apps/desktop/e2e/default-thinking-level.spec.ts b/apps/desktop/e2e/default-thinking-level.spec.ts deleted file mode 100644 index bee3445f8c..0000000000 --- a/apps/desktop/e2e/default-thinking-level.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { expect, test, COMPOSER_INPUT } from './fixtures.js'; - -/** - * Settings → 通用 → 默认思考级别 reaching the next new chat. - * - * This journey is e2e rather than unit because the bug it guards lives in the - * seam between two async sources: the setting arrives from a settings fetch - * that resolves AFTER the composer hook first mounts. Seeding it through a - * useState initializer type-checks, renders, and persists correctly — and the - * setting still never takes effect, because an initializer runs once, before - * the value exists. Only a real round-trip through the settings page and back - * catches that. - */ -test('a configured default thinking level reaches the next new chat, and the composer can still overrule it', async ({ - modelPickerLongWindow: page, -}) => { - const composerChip = () => - page.locator('.maka-composer-left-controls').getByRole('button', { name: /思考级别/ }); - - // Negative control: with nothing configured the chip is the model's own - // default, so a later 关 cannot be mistaken for a value that was always there. - await expect(composerChip()).toContainText('模型默认'); - - await page.getByRole('button', { name: /设置|Settings/ }).click(); - await page - .getByRole('navigation', { name: /设置分组|Settings sections/ }) - .getByRole('button', { name: /通用|General/, exact: true }) - .click(); - - const selector = page.getByRole('combobox', { name: '默认思考级别' }); - await selector.scrollIntoViewIfNeeded(); - await expect(selector).toContainText('跟随模型默认'); - await selector.click(); - await page.getByRole('option', { name: '关', exact: true }).click(); - - await expect - .poll(() => - page.evaluate(() => window.maka.settings.get().then((value) => value.chatDefaults.thinkingLevel)), - ) - .toBe('off'); - - await page.getByRole('button', { name: /返回应用|Back to app/ }).click(); - // Scope to the composer before reading the chip: the settings Selector also - // answers to /思考级别/ and reads 关 once set, so an unscoped lookup passes - // whether or not the chip ever changed. - await expect(page.locator(COMPOSER_INPUT)).toBeVisible(); - await expect(composerChip()).toHaveCount(1); - await expect(composerChip()).toContainText('关'); - - // The per-chat picker must still overrule the configured default — including - // back to 模型默认, which is why an untouched picker and an explicit - // 模型默认 pick cannot share one representation. - await composerChip().click(); - await page.getByRole('menuitem', { name: '模型默认', exact: true }).click(); - await expect(composerChip()).toContainText('模型默认'); -}); diff --git a/apps/desktop/e2e/keyboard-help.spec.ts b/apps/desktop/e2e/keyboard-help.spec.ts index ef5dbdeef5..9c085eb3b2 100644 --- a/apps/desktop/e2e/keyboard-help.spec.ts +++ b/apps/desktop/e2e/keyboard-help.spec.ts @@ -22,24 +22,6 @@ test('the help modal opens from its entry points and keeps its styled layout', a await page.keyboard.press(MOD_SLASH); await expect(body).toBeVisible(); - // Description/keys resolve into a two-column grid… - const rows = page.locator('.maka-help-section dl').first(); - await expect(rows).toHaveCSS('display', 'grid'); - const columns = await rows.evaluate((el) => getComputedStyle(el).gridTemplateColumns.split(' ').length); - expect(columns).toBe(2); - // …headings carry the caption typography, not default heading bulk… - const heading = page.locator('.maka-help-section h3').first(); - await expect(heading).toHaveCSS('font-size', '12px'); - await expect(heading).toHaveCSS('font-weight', '600'); - // …multi-key combos keep their distinct separator… - const plus = page.locator('.maka-help-plus').first(); - await expect(plus).toBeVisible(); - await expect(plus).toHaveText('+'); - // …and Kbd chips carry real keycap chrome. - const keycap = page.locator('.maka-help-section .astryx-kbd kbd').first(); - await expect(keycap).not.toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); - await expect(keycap).not.toHaveCSS('border-radius', '0px'); - // The platform's own modifier is what the sheet documents — ⌘ on macOS, // Ctrl elsewhere. (This harness boots with healthy UA-CH; the blank-UA-CH // environment is pinned by the dedicated test below.) diff --git a/apps/desktop/e2e/mcp.spec.ts b/apps/desktop/e2e/mcp.spec.ts index cc0cda7cb7..821a7ba8d9 100644 --- a/apps/desktop/e2e/mcp.spec.ts +++ b/apps/desktop/e2e/mcp.spec.ts @@ -6,155 +6,6 @@ const fixtureServer = path.resolve( '../../packages/mcp/dist/__fixtures__/stdio-server.js', ); -test('module navigation removes the hidden chat surface from layout and hit testing', async ({ window: page }) => { - await page.getByRole('button', { name: '展开侧边栏' }).click(); - await page.getByRole('navigation', { name: '对话列表' }).getByRole('button', { name: '扩展', exact: true }).click(); - await expect(page.getByRole('main', { name: '扩展' })).toBeVisible(); - - const hiddenChat = page.locator('.maka-chat-layout[hidden]'); - await expect(hiddenChat).toHaveCount(1); - await expect(hiddenChat).toHaveCSS('display', 'none'); - expect(await hiddenChat.boundingBox()).toBeNull(); - expect( - await page.evaluate(() => { - const target = document.elementFromPoint(window.innerWidth * 0.75, window.innerHeight - 100); - return Boolean(target?.closest('.maka-chat-layout')); - }), - ).toBe(false); -}); - -test('MCP module page keeps one centred column without horizontal overflow', async ({ window: page }) => { - await page.getByRole('button', { name: '展开侧边栏' }).click(); - await page.getByRole('navigation', { name: '对话列表' }).getByRole('button', { name: '扩展', exact: true }).click(); - await page.locator('.maka-module-hub-selector').getByRole('button', { name: 'MCP' }).click(); - await expect(page.getByRole('toolbar', { name: 'MCP 浏览操作' })).toBeVisible(); - - for (const width of [1440, 1280, 861, 860, 761]) { - await page.setViewportSize({ width, height: 700 }); - await expect.poll(() => page.evaluate(() => window.innerWidth)).toBe(width); - - const geometry = await page.evaluate(() => { - const main = document.querySelector('.maka-module-main'); - const content = main?.querySelector('.astryx-layout-content'); - if (!main || !content) throw new Error('Expected the MCP module layout'); - const rows = content.querySelector('.maka-module-page-rows') - ?? content.querySelector('.maka-module-page-panel'); - if (!rows) throw new Error('Expected the MCP module content'); - const rowsRect = rows.getBoundingClientRect(); - const contentRect = content.getBoundingClientRect(); - return { - mainOverflow: main.scrollWidth - main.clientWidth, - contentOverflow: content.scrollWidth - content.clientWidth, - // Centring is measured against the content scroller's inner box, not - // the page: a classic (non-overlay) vertical scrollbar takes width - // out of the scroller, and the column centres in what remains. - centerDelta: Math.abs( - rowsRect.left + rowsRect.width / 2 - - (contentRect.left + content.clientWidth / 2), - ), - rowsWidth: rowsRect.width, - }; - }); - - expect(geometry.mainOverflow, `${width}px: ${JSON.stringify(geometry)}`).toBe(0); - expect(geometry.contentOverflow, `${width}px: ${JSON.stringify(geometry)}`).toBe(0); - // Centred and capped at every width, not just wide ones: below the clamp - // the column fills the plate, which is centring too. - expect(geometry.rowsWidth, `${width}px: ${JSON.stringify(geometry)}`).toBeLessThanOrEqual(900); - expect(geometry.centerDelta, `${width}px: ${JSON.stringify(geometry)}`).toBeLessThanOrEqual(1); - } -}); - -test('MCP server descriptions truncate with an ellipsis at 700px and 500px', async ({ window: page }) => { - await page.getByRole('button', { name: '展开侧边栏' }).click(); - await page.getByRole('navigation', { name: '对话列表' }).getByRole('button', { name: '扩展', exact: true }).click(); - await page.locator('.maka-module-hub-selector').getByRole('button', { name: 'MCP' }).click(); - - const endpoint = `https://example.com/${'narrow-description-segment/'.repeat(12)}mcp`; - await page.evaluate(async (url) => { - await window.maka.mcp.upsert('long-endpoint', { - enabled: false, - url, - transport: 'streamable-http', - }); - }, endpoint); - await page.getByRole('button', { name: '刷新', exact: true }).click(); - await page.getByRole('radio', { name: '已安装' }).click(); - - const row = page.getByRole('listitem').filter({ hasText: 'long-endpoint' }); - const description = row.locator('[data-maka-contract="mcp-server-description"]'); - await expect(description).toBeVisible(); - await expect(description.getByTitle(endpoint)).toHaveText(endpoint); - - for (const width of [700, 500]) { - await page.setViewportSize({ width, height: 700 }); - await expect.poll(() => page.evaluate(() => window.innerWidth)).toBe(width); - - const geometry = await description.evaluate((content) => { - const slot = content.parentElement; - const main = content.closest('.maka-module-main'); - const item = content.closest('li'); - if (!slot || !main || !item) throw new Error('Expected the MCP server description layout'); - const contentStyle = getComputedStyle(content); - const slotRect = slot.getBoundingClientRect(); - const itemRect = item.getBoundingClientRect(); - return { - mainOverflow: main.scrollWidth - main.clientWidth, - contentOverflow: content.scrollWidth - content.clientWidth, - slotRightOverflow: slotRect.right - itemRect.right, - contentOverflowStyle: contentStyle.overflow, - contentTextOverflow: contentStyle.textOverflow, - contentWhiteSpace: contentStyle.whiteSpace, - }; - }); - - expect(geometry.mainOverflow, `${width}px: ${JSON.stringify(geometry)}`).toBe(0); - expect(geometry.contentOverflow, `${width}px: ${JSON.stringify(geometry)}`).toBeGreaterThan(0); - expect(geometry.slotRightOverflow, `${width}px: ${JSON.stringify(geometry)}`).toBeLessThanOrEqual(1); - expect(geometry.contentOverflowStyle).toBe('hidden'); - expect(geometry.contentTextOverflow).toBe('ellipsis'); - expect(geometry.contentWhiteSpace).toBe('nowrap'); - } -}); - -test('credentialed MCP editor fits a compact desktop viewport without incidental scrolling', async ({ window: page }) => { - await page.setViewportSize({ width: 1164, height: 700 }); - await page.getByRole('button', { name: '展开侧边栏' }).click(); - await page.getByRole('navigation', { name: '对话列表' }).getByRole('button', { name: '扩展', exact: true }).click(); - await page.locator('.maka-module-hub-selector').getByRole('button', { name: 'MCP' }).click(); - - const slackRow = page.locator('[data-maka-contract="mcp-market-row"]').filter({ hasText: 'Slack' }); - await slackRow.getByRole('button', { name: '安装 Slack' }).click(); - await slackRow.getByRole('button', { name: '管理' }).click(); - - const editor = page.getByRole('dialog', { name: '编辑 slack' }); - await expect(editor).toBeVisible(); - await expect.poll(() => editor.evaluate((element) => ( - element.getAnimations().every((animation) => animation.playState === 'finished') - ))).toBe(true); - const overflow = await editor.evaluate((dialog) => { - const fields = dialog.querySelector('.maka-mcp-form-fields'); - if (!fields) throw new Error('Expected MCP editor fields'); - return { - dialog: dialog.scrollHeight - dialog.clientHeight, - fields: fields.scrollHeight - fields.clientHeight, - }; - }); - - expect(overflow.dialog, JSON.stringify(overflow)).toBeLessThanOrEqual(1); - expect(overflow.fields, JSON.stringify(overflow)).toBeLessThanOrEqual(1); - - const selectedTransportSpacing = await editor.getByRole('radio', { name: '本地 stdio' }).evaluate((radio) => { - const radioWrapper = radio.parentElement; - const icon = radioWrapper?.nextElementSibling; - if (!(radioWrapper instanceof HTMLElement) || !(icon instanceof SVGElement)) { - throw new Error('Expected selected transport radio and icon'); - } - return icon.getBoundingClientRect().left - radioWrapper.getBoundingClientRect().right; - }); - expect(selectedTransportSpacing).toBeGreaterThanOrEqual(4); -}); - test('MCP module completes stdio add, discovery, disable, JSON import, and delete', async ({ window: page }) => { await page.getByRole('button', { name: '展开侧边栏' }).click(); const sidebar = page.getByRole('navigation', { name: '对话列表' }); diff --git a/apps/desktop/e2e/onboarding.spec.ts b/apps/desktop/e2e/onboarding.spec.ts index 697a4140df..b22b106b75 100644 --- a/apps/desktop/e2e/onboarding.spec.ts +++ b/apps/desktop/e2e/onboarding.spec.ts @@ -6,16 +6,6 @@ test('first run connects a provider and starts the first task without workspace await expect(onboarding.getByRole('heading', { name: '接入一个 AI,开始第一项任务。' })).toBeVisible(); await expect(onboarding.locator('.maka-onboarding-provider-row')).toHaveCount(4); - await page.setViewportSize({ width: 480, height: 900 }); - const cardRect = await onboarding.boundingBox(); - const layoutRect = await page.locator('[data-chat-scroll-container="true"]').boundingBox(); - expect(cardRect).not.toBeNull(); - expect(layoutRect).not.toBeNull(); - expect(cardRect?.x).toBeGreaterThanOrEqual(layoutRect?.x ?? 0); - expect((cardRect?.x ?? 0) + (cardRect?.width ?? 0)).toBeLessThanOrEqual( - (layoutRect?.x ?? 0) + (layoutRect?.width ?? 0), - ); - await onboarding.locator('.maka-onboarding-provider-row[data-provider="opencode-free"]').click(); await expect(page.locator('[data-maka-contract="provider-setup"]')).toBeVisible(); diff --git a/apps/desktop/e2e/providers.spec.ts b/apps/desktop/e2e/providers.spec.ts index 9818f1ee0d..6e35c14a4b 100644 --- a/apps/desktop/e2e/providers.spec.ts +++ b/apps/desktop/e2e/providers.spec.ts @@ -44,16 +44,12 @@ async function openCatalog(page: Page, options: { category: string; search: stri const search = catalog.getByPlaceholder('搜索服务商'); const category = catalog.getByRole('combobox', { name: '分类', exact: true }); await expect(search).toBeFocused(); - const searchIcon = search.locator('xpath=..').locator('svg').first(); - await expect(searchIcon).toHaveCSS('width', '16px'); - await expect(searchIcon).toHaveCSS('height', '16px'); // These are independent filters, not one composite toolbar: ordinary Tab // order must move between them in both directions. await page.keyboard.press('Tab'); await expect(category).toBeFocused(); await page.keyboard.press('Shift+Tab'); await expect(search).toBeFocused(); - await expect(catalog.getByRole('toolbar')).toHaveCount(0); await category.click(); await page.getByRole('option', { name: options.category, exact: true }).click(); await catalog.getByPlaceholder('搜索服务商').fill(options.search); @@ -75,45 +71,6 @@ async function expectNoDialog(page: Page) { await expect(page.getByRole('dialog')).toHaveCount(0); } -test('Models collection header matches its collection', async ({ window: page }) => { - await openModelsPage(page); - const panel = page.locator('[data-maka-contract="providers-panel"]'); - await expect(panel.locator('ul')).toBeVisible(); - - const geometry = await panel.evaluate((element) => { - const heading = element.querySelector('h3')?.getBoundingClientRect(); - const add = element.querySelector('[data-maka-contract="add-connection"]')?.getBoundingClientRect(); - const list = element.querySelector('ul')?.getBoundingClientRect(); - if (!heading || !add || !list) return null; - return { - headingLeft: heading.left, - addRight: add.right, - listLeft: list.left, - listRight: list.right, - headerBottom: Math.max(heading.bottom, add.bottom), - listTop: list.top, - }; - }); - - expect(geometry).not.toBeNull(); - if (!geometry) return; - expect(Math.abs(geometry.headingLeft - geometry.listLeft)).toBeLessThanOrEqual(0.5); - expect(Math.abs(geometry.addRight - geometry.listRight)).toBeLessThanOrEqual(0.5); - expect(geometry.listTop - geometry.headerBottom).toBeGreaterThanOrEqual(7.5); - expect(geometry.listTop - geometry.headerBottom).toBeLessThanOrEqual(8.5); - - await expect(panel.getByRole('heading', { name: '模型连接', exact: true })).toBeVisible(); - await expect(panel.getByText('· 1', { exact: true })).toBeVisible(); - - await page.evaluate(async () => { - await window.maka.connections.delete('e2e'); - }); - await page.reload(); - await page.getByRole('button', { name: '设置' }).click(); - await page.locator('[aria-label="设置分组"]').getByText('模型', { exact: true }).click(); - await expect(panel.getByText(/^· \d+$/)).toHaveCount(0); -}); - // Canonical API-key add journey. Cerebras is the concrete stand-in only because // it is the strongest exercise of the color-asset render contract (a real // upstream mark that must stay untouched in BOTH light and dark themes); @@ -159,20 +116,6 @@ test('adds a catalog provider through the canonical API-key setup page', async ( await expect(setup.getByLabel('服务地址', { exact: true })).toHaveCount(0); await expect(setup.getByLabel('默认模型', { exact: true })).toHaveCount(0); - // A 300-character key scrolls inside the field instead of growing it. The - // before/after values are the oracle; the field's width is a design token, - // not this contract. - const inputBox = await keyInput.boundingBox(); - await keyInput.fill(`sk-${'a'.repeat(300)}`); - const longKeyLayout = await keyInput.evaluate((input) => ({ - clientWidth: input.clientWidth, - scrollWidth: input.scrollWidth, - clientHeight: input.clientHeight, - scrollHeight: input.scrollHeight, - })); - expect(longKeyLayout.scrollWidth).toBeGreaterThan(longKeyLayout.clientWidth); - expect(longKeyLayout.scrollHeight).toBe(longKeyLayout.clientHeight); - expect((await keyInput.boundingBox())?.height).toBe(inputBox?.height); }); await test.step('saving creates the connection and lands on its detail level', async () => { diff --git a/apps/desktop/e2e/send-message.spec.ts b/apps/desktop/e2e/send-message.spec.ts index 10f483a161..91217ca93d 100644 --- a/apps/desktop/e2e/send-message.spec.ts +++ b/apps/desktop/e2e/send-message.spec.ts @@ -7,19 +7,6 @@ import { FAKE_MERMAID_HOSTILE_PROMPT, FAKE_MERMAID_PROMPT } from '@maka/runtime' * fixture's MAKA_E2E=1 forces sessions:create onto the fake backend, and the * seeded 'e2e' connection clears onboarding so the composer is usable. */ -test('send a message and see the fake backend stream a reply', async ({ window: page }) => { - const composer = page.locator(COMPOSER_INPUT); - // #1433: the deleted first-run panel had its own input, and the spec that - // covered the handoff between the two asserted this accessible name. With - // one composer left, the name is what a screen-reader user has to find the - // send target by — assert it on the path that exercises it. - await expect(composer).toHaveAttribute('aria-label', '消息输入框'); - await composer.fill('hello e2e'); - await composer.press('Enter'); - - await expect(page.getByText(/Fake backend received: hello e2e/)).toBeVisible(); -}); - /** * Enter commits a candidate in a CJK IME; nothing else may act on it. Both the * composer's send and ChatComposerInput's trigger menu read Enter, and the diff --git a/apps/desktop/e2e/session-workbar.spec.ts b/apps/desktop/e2e/session-workbar.spec.ts index 495495aac5..16f8c5f682 100644 --- a/apps/desktop/e2e/session-workbar.spec.ts +++ b/apps/desktop/e2e/session-workbar.spec.ts @@ -35,34 +35,6 @@ test('session tools share one user-controlled workbar', async ({ sessionWorkbarW await expect(resize).toHaveAttribute('aria-valuenow', '490'); await expect(workbar).toHaveCSS('width', '490px'); - // The seam is the canvas between two plates, and it is asserted as a GAP, - // not as a colour. Comparing the workbar's tone to another surface is what - // this used to do, and it passed while the column was invisible: it was - // compared to the sidebar, which paints the same value the conversation does, - // so "the workbar matches" and "the workbar has no boundary" were the same - // assertion. Every default palette but darwin dark resolves all three to - // `oklch(1 0 0)`. - const conversation = page.locator('.mainColumn'); - const workbarBox = (await workbar.boundingBox())!; - const conversationBox = (await conversation.boundingBox())!; - const gutter = workbarBox.x - (conversationBox.x + conversationBox.width); - expect(Math.round(gutter)).toBe(4); - // And it is really canvas showing through, not a third surface: the frame - // holding both plates paints nothing. - await expect(page.locator('.maka-panel-detail')).toHaveCSS( - 'background-color', - 'rgba(0, 0, 0, 0)', - ); - await expect(workbar).toHaveCSS('border-left-width', '0px'); - - // Two plates, one line. Each keeps the titlebar clearance as its own padding, - // so their top edges agree without either reaching outside its box for it — - // the previous formulation hung the column above the frame's padding box, - // which made the frame scroll the overhang into view on the first focus. - // Measured after the handle above took focus, which is what caught that. - expect(Math.round(workbarBox.y)).toBe(Math.round(conversationBox.y)); - expect(Math.round(workbarBox.height)).toBe(Math.round(conversationBox.height)); - // Pointer drag, grabbed near the bottom of the divider: Astryx's default // side-placed grab zone lifts itself half its height off the handle, so a // low grab is what proves `pillPlacement="center"` is still holding the hit @@ -149,22 +121,3 @@ test('session tools share one user-controlled workbar', async ({ sessionWorkbarW await expect(workbar).toBeHidden(); await expect(page.getByRole('main', { name: '扩展' })).toBeVisible(); }); - -test('the right workbar stays mounted across repeated collapse', async ({ - sessionWorkbarWindow: page, -}) => { - const workbar = page.locator('[data-maka-contract="session-workbar-right"]'); - await expect(workbar).toBeVisible(); - const collapseOnce = async () => { - await page.getByRole('button', { name: '收起会话工作栏' }).click(); - await expect(workbar).toHaveCount(1); - await expect(workbar).toBeHidden(); - await expect(workbar).toHaveAttribute('data-collapsed', 'true'); - }; - - await collapseOnce(); - await page.getByRole('button', { name: '展开会话工作栏' }).click(); - await expect(workbar).toBeVisible(); - await expect(workbar).toHaveCSS('width', '480px'); - await collapseOnce(); -}); diff --git a/apps/desktop/e2e/settings.spec.ts b/apps/desktop/e2e/settings.spec.ts index fa961dcf5a..efff2d268e 100644 --- a/apps/desktop/e2e/settings.spec.ts +++ b/apps/desktop/e2e/settings.spec.ts @@ -28,21 +28,6 @@ test('changing the theme in settings applies to the UI', async ({ window: page } ).toBe(true); }); -test('settings back icon shares the navigation icon rail', async ({ window: page }) => { - await page.getByRole('button', { name: '展开侧边栏' }).click(); - await page.getByRole('button', { name: '设置' }).click(); - - const iconCenterX = async (locator: ReturnType) => { - const box = await locator.boundingBox(); - expect(box).not.toBeNull(); - return box ? box.x + box.width / 2 : Number.NaN; - }; - const backIcon = page.getByRole('button', { name: '返回应用', exact: true }).locator('svg').first(); - const navIcon = settingsNavigation(page).getByRole('button', { name: '通用', exact: true }).locator('svg').first(); - - expect(Math.abs(await iconCenterX(backIcon) - await iconCenterX(navIcon))).toBeLessThanOrEqual(0.5); -}); - test('subagent presets can be reviewed and edited in desktop settings', async ({ window: page }) => { await page.evaluate(async () => { const connections = await window.maka.connections.list(); diff --git a/apps/desktop/src/main/__tests__/app-update-activity.test.ts b/apps/desktop/src/main/__tests__/app-update-activity.test.ts deleted file mode 100644 index c3b9ca6db0..0000000000 --- a/apps/desktop/src/main/__tests__/app-update-activity.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, test } from 'node:test'; -import { hasInterruptibleUpdateWork } from '../app-update-activity.js'; - -describe('App update activity', () => { - test('guards every kind of work that app shutdown interrupts', () => { - const cases = [ - { name: 'idle', session: false, automation: false, shells: 0, expected: false }, - { name: 'session turn', session: true, automation: false, shells: 0, expected: true }, - { name: 'Automation fire', session: false, automation: true, shells: 0, expected: true }, - { name: 'background shell', session: false, automation: false, shells: 1, expected: true }, - ]; - - for (const input of cases) { - assert.equal( - hasInterruptibleUpdateWork({ - sessionActivities: { hasActive: () => input.session }, - automationScheduler: { hasInFlight: () => input.automation }, - shellRuns: { liveCount: () => input.shells }, - }), - input.expected, - input.name, - ); - } - }); -}); diff --git a/apps/desktop/src/main/__tests__/artifact-list-keyboard.test.ts b/apps/desktop/src/main/__tests__/artifact-list-keyboard.test.ts deleted file mode 100644 index 91f916f32b..0000000000 --- a/apps/desktop/src/main/__tests__/artifact-list-keyboard.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Tests for the ArtifactPane list keyboard helper (PR108i, @kenji a11y gate #1). - * - * The pure helper has to handle five concerns simultaneously: arrow-key - * selection wrapping, Home/End jumping, Enter/Space activation, Escape - * dismissal, and "no nav key" passthrough. We lock the matrix down so a - * future change can't accidentally start swallowing Esc (which would - * break the global Command Palette) or stop wrapping at the bottom. - */ - -import { strict as assert } from 'node:assert'; -import { describe, it } from 'node:test'; -import { - nextArtifactListAction, - type ArtifactListAction, -} from '../../renderer/artifact-list-keyboard.js'; - -function expectAction(actual: ArtifactListAction, expected: ArtifactListAction) { - assert.deepEqual(actual, expected); -} - -const IDS = ['a', 'b', 'c'] as const; - -describe('nextArtifactListAction', () => { - describe('empty list', () => { - it('returns noop regardless of key', () => { - for (const key of ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter', ' ', 'Escape', 'q']) { - expectAction( - nextArtifactListAction({ currentSelectedId: undefined, visibleIds: [], key }), - { kind: 'noop' }, - ); - } - }); - }); - - describe('arrow key selection', () => { - it('ArrowDown moves selection to next item', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'ArrowDown' }), - { kind: 'select', targetId: 'b' }, - ); - }); - - it('ArrowDown from last wraps to first', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'c', visibleIds: IDS, key: 'ArrowDown' }), - { kind: 'select', targetId: 'a' }, - ); - }); - - it('ArrowUp moves selection to previous item', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'b', visibleIds: IDS, key: 'ArrowUp' }), - { kind: 'select', targetId: 'a' }, - ); - }); - - it('ArrowUp from first wraps to last', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'ArrowUp' }), - { kind: 'select', targetId: 'c' }, - ); - }); - - it('ArrowDown with no current selection starts at first', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: undefined, visibleIds: IDS, key: 'ArrowDown' }), - { kind: 'select', targetId: 'a' }, - ); - }); - - it('ArrowUp with no current selection starts at last', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: undefined, visibleIds: IDS, key: 'ArrowUp' }), - { kind: 'select', targetId: 'c' }, - ); - }); - }); - - describe('Home / End jumps', () => { - it('Home jumps to first', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'b', visibleIds: IDS, key: 'Home' }), - { kind: 'select', targetId: 'a' }, - ); - }); - - it('End jumps to last', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'End' }), - { kind: 'select', targetId: 'c' }, - ); - }); - }); - - describe('Enter / Space activation', () => { - it('Enter activates current selection', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'b', visibleIds: IDS, key: 'Enter' }), - { kind: 'activate', targetId: 'b' }, - ); - }); - - it('Space activates current selection', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'b', visibleIds: IDS, key: ' ' }), - { kind: 'activate', targetId: 'b' }, - ); - }); - - it('Enter with no selection activates first item', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: undefined, visibleIds: IDS, key: 'Enter' }), - { kind: 'activate', targetId: 'a' }, - ); - }); - - it('Enter on a stale selection (no longer in list) falls back to first', () => { - // The list churns; the selected id might be deleted between renders. - expectAction( - nextArtifactListAction({ currentSelectedId: 'gone', visibleIds: IDS, key: 'Enter' }), - { kind: 'activate', targetId: 'a' }, - ); - }); - }); - - describe('Escape dismissal (does NOT swallow if list empty)', () => { - it('Escape on non-empty list returns dismiss', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'Escape' }), - { kind: 'dismiss' }, - ); - }); - - it('Escape on empty list returns noop (does not steal Esc from Command Palette)', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: undefined, visibleIds: [], key: 'Escape' }), - { kind: 'noop' }, - ); - }); - }); - - describe('unrelated keys', () => { - it('letter keys return noop', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'q' }), - { kind: 'noop' }, - ); - }); - - it('Tab returns noop (focus moves via browser default, not list helper)', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'Tab' }), - { kind: 'noop' }, - ); - }); - - it('Shift+Tab returns noop', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'Tab' }), - { kind: 'noop' }, - ); - }); - - it('ArrowLeft / ArrowRight return noop (this is a vertical listbox)', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'ArrowLeft' }), - { kind: 'noop' }, - ); - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'ArrowRight' }), - { kind: 'noop' }, - ); - }); - }); - - describe('priority order', () => { - // We lock the order so future edits don't surprise reviewers. - // 1. empty list → noop (regardless of key) - // 2. Escape → dismiss - // 3. Enter / Space → activate - // 4. ArrowDown/Up/Home/End → select - // 5. anything else → noop - it('empty list dominates Escape', () => { - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: [], key: 'Escape' }), - { kind: 'noop' }, - ); - }); - - it('Escape dominates ArrowDown when both could apply (Escape is the chord)', () => { - // (Synthetic: keys are single, but verifies the precedence in the switch) - expectAction( - nextArtifactListAction({ currentSelectedId: 'a', visibleIds: IDS, key: 'Escape' }), - { kind: 'dismiss' }, - ); - }); - }); -}); diff --git a/apps/desktop/src/main/__tests__/attachment-chat-render.test.ts b/apps/desktop/src/main/__tests__/attachment-chat-render.test.ts deleted file mode 100644 index ccc99157b6..0000000000 --- a/apps/desktop/src/main/__tests__/attachment-chat-render.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { createElement, type ReactNode } from 'react'; -import { renderToStaticMarkup } from 'react-dom/server'; -import type { AttachmentRef, SessionSummary, StoredMessage } from '@maka/core'; -import { ChatSurfaceLayout, ChatView, LocaleProvider } from '@maka/ui'; - -function renderWithLocale(child: ReactNode): string { - return renderToStaticMarkup( - createElement(LocaleProvider, { - locale: 'zh', - children: createElement(ChatSurfaceLayout, { composer: null, children: child }), - }), - ); -} - -const activeSession: SessionSummary = { - id: 's1', - name: 'Sent reference check', - isFlagged: false, - isArchived: false, - labels: [], - hasUnread: false, - status: 'active', - backend: 'ai-sdk', - llmConnectionSlug: 'fixture', - connectionLocked: false, - model: 'fixture-model', - permissionMode: 'ask', -}; - -describe('sent reference frontend', () => { - it('keeps sent references outside the authored-text bubble', () => { - const attachments: AttachmentRef[] = [ - { - kind: 'pdf', - name: 'design-spec.pdf', - mimeType: 'application/pdf', - bytes: 512_000, - ref: { kind: 'session_file', sessionId: 's1', relativePath: 'artifact-1' }, - }, - { - kind: 'image', - name: 'layout.png', - mimeType: 'image/png', - bytes: 4, - ref: { kind: 'session_file', sessionId: 's1', relativePath: 'artifact-2' }, - }, - ]; - const messages: StoredMessage[] = [{ - type: 'user', - id: 'u1', - turnId: 't1', - ts: 1, - text: '请用 /skill:writer 检查', - attachments, - quotes: [{ text: 'reference', sourceTurnId: 't0' }], - }]; - const markup = renderWithLocale(createElement(ChatView, { - messages, - activeSession, - onNew: () => {}, - } satisfies Parameters[0])); - - const bubbleIndex = markup.indexOf('astryx-chat-message-bubble'); - assert.ok(markup.indexOf('astryx-token') < bubbleIndex); - assert.ok(markup.indexOf('maka-user-quotes') < bubbleIndex); - assert.ok(markup.indexOf('maka-user-attachments') < bubbleIndex); - assert.ok(markup.indexOf('astryx-badge') > bubbleIndex); - }); - - it('renders sent Skill invocations as neutral tokens without guessing @ text', () => { - const messages: StoredMessage[] = [ - { - type: 'user', - id: 'u1', - turnId: 't1', - ts: 1, - text: '请用 /skill:writer 看 @notes', - }, - ]; - const markup = renderWithLocale(createElement(ChatView, { - messages, - activeSession, - onNew: () => {}, - } satisfies Parameters[0])); - - assert.match(markup, /class="astryx-badge neutral[^"]*"[^>]*>[^<]*\/skill:writer<\/span>/); - assert.doesNotMatch(markup, /class="astryx-badge neutral[^"]*"[^>]*>[^<]*@notes<\/span>/); - }); - - it('keeps sent file tokens compact while exposing size accessibly', () => { - const attachment: AttachmentRef = { - kind: 'pdf', - name: 'design-spec.pdf', - mimeType: 'application/pdf', - bytes: 512_000, - ref: { kind: 'session_file', sessionId: 's1', relativePath: 'artifact-1' }, - }; - const messages: StoredMessage[] = [ - { type: 'user', id: 'u1', turnId: 't1', ts: 1, text: '看下附件', attachments: [attachment] }, - ]; - const markup = renderWithLocale(createElement(ChatView, { - messages, - activeSession, - onNew: () => {}, - } satisfies Parameters[0])); - - const tokenIndex = markup.indexOf('astryx-token'); - const bubbleIndex = markup.indexOf('astryx-chat-message-bubble'); - assert.notEqual(tokenIndex, -1); - assert.ok(tokenIndex < bubbleIndex); - assert.match(markup, /lucide-file-text/); - assert.match(markup, /class="[^"]*astryx-icon[^"]*"[^>]*data-size="sm"/); - assert.match(markup, /design-spec\.pdf/); - assert.match(markup, /aria-description="500\.0 KB"/); - assert.doesNotMatch(markup, />500\.0 KB { - const attachment: AttachmentRef = { - kind: 'image', - name: 'clipboard.png', - mimeType: 'image/png', - bytes: 4, - ref: { kind: 'session_file', sessionId: 's1', relativePath: 'artifact-1' }, - }; - const messages: StoredMessage[] = [ - { type: 'user', id: 'u1', turnId: 't1', ts: 1, text: '看这张图', attachments: [attachment] }, - ]; - const markup = renderWithLocale(createElement(ChatView, { - messages, - activeSession, - onNew: () => {}, - } satisfies Parameters[0])); - - assert.match(markup, /maka-user-attachments/); - assert.match(markup, /astryx-thumbnail/); - assert.doesNotMatch(markup, /maka-user-attachment-thumb(?:-pending|-image)?(?:\s|")/); - }); -}); diff --git a/apps/desktop/src/main/__tests__/attachment-resize.test.ts b/apps/desktop/src/main/__tests__/attachment-resize.test.ts deleted file mode 100644 index e5a695dd4d..0000000000 --- a/apps/desktop/src/main/__tests__/attachment-resize.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, test } from 'node:test'; -import { computeResizeDimensions } from '../attachment-resize.js'; - -describe('computeResizeDimensions', () => { - test('scales the longest edge down to the cap, preserving aspect ratio', () => { - assert.deepEqual(computeResizeDimensions(3000, 2000, 2000), { width: 2000, height: 1333 }); - }); - - test('returns null when the image already fits the cap', () => { - assert.equal(computeResizeDimensions(2000, 1000, 2000), null); - assert.equal(computeResizeDimensions(1000, 2000, 2000), null); - }); - - test('returns null for a zero-dimension image (cannot scale)', () => { - assert.equal(computeResizeDimensions(0, 0, 2000), null); - }); -}); diff --git a/apps/desktop/src/main/__tests__/branch-banner.test.ts b/apps/desktop/src/main/__tests__/branch-banner.test.ts deleted file mode 100644 index 21ffcb58e1..0000000000 --- a/apps/desktop/src/main/__tests__/branch-banner.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Tests for branch banner derivation (PR109f). - * - * Locks the contract: - * - banner only renders for sessions with parentSessionId set - * - banner requires the parent session to be visible in the list - * - banner copy uses the parent's display name - * - fromAbortedTurn is caller-supplied; helper never guesses - */ - -import { strict as assert } from 'node:assert'; -import { describe, it } from 'node:test'; -import { - deriveBranchBanner, - type BranchBannerSessionInput, -} from '../../renderer/branch-banner.js'; - -function session(partial: Partial & { id: string; name: string }): BranchBannerSessionInput { - return { ...partial }; -} - -describe('deriveBranchBanner', () => { - it('returns undefined for non-branched sessions', () => { - const active = session({ id: 's1', name: '原会话' }); - const result = deriveBranchBanner(active, [active]); - assert.equal(result, undefined); - }); - - it('returns undefined when activeSession is undefined', () => { - const result = deriveBranchBanner(undefined, []); - assert.equal(result, undefined); - }); - - it('returns undefined when parentSessionId references a hidden session', () => { - // Parent archived / filtered out of the visible list — better to - // show no banner than render a banner that clicks into nothing. - const active = session({ id: 's2', name: '分支会话', parentSessionId: 'parent-not-here' }); - const result = deriveBranchBanner(active, [active]); - assert.equal(result, undefined); - }); - - it('returns banner with parent name when parent is visible', () => { - const parent = session({ id: 'p1', name: '父会话' }); - const active = session({ id: 's3', name: '分支会话', parentSessionId: 'p1' }); - const result = deriveBranchBanner(active, [parent, active]); - assert.deepEqual(result, { - parentSessionId: 'p1', - parentSessionName: '父会话', - }); - }); - - it('passes through fromAbortedTurn when caller supplies it', () => { - const parent = session({ id: 'p2', name: '父会话' }); - const active = session({ id: 's4', name: '分支会话', parentSessionId: 'p2' }); - const result = deriveBranchBanner(active, [parent, active], true); - assert.deepEqual(result, { - parentSessionId: 'p2', - parentSessionName: '父会话', - fromAbortedTurn: true, - }); - }); - - it('omits fromAbortedTurn when caller passes false / undefined', () => { - const parent = session({ id: 'p3', name: '父会话' }); - const active = session({ id: 's5', name: '分支会话', parentSessionId: 'p3' }); - const resultFalse = deriveBranchBanner(active, [parent, active], false); - const resultUndef = deriveBranchBanner(active, [parent, active], undefined); - assert.equal(resultFalse?.fromAbortedTurn, undefined); - assert.equal(resultUndef?.fromAbortedTurn, undefined); - }); - - it('does not mutate the input sessions list', () => { - const parent = session({ id: 'p4', name: '父会话' }); - const active = session({ id: 's6', name: '分支会话', parentSessionId: 'p4' }); - const sessions = [parent, active]; - const before = JSON.stringify(sessions); - deriveBranchBanner(active, sessions, true); - assert.equal(JSON.stringify(sessions), before); - }); - - it('uses the parent name verbatim (no truncation, no fallback)', () => { - // If parent rename is involved later, the banner must reflect the - // current name so the user sees the same label as in the sidebar. - const parent = session({ id: 'p5', name: '一个非常长的父会话名称用于测试不截断' }); - const active = session({ id: 's7', name: '分支会话', parentSessionId: 'p5' }); - const result = deriveBranchBanner(active, [parent, active]); - assert.equal(result?.parentSessionName, '一个非常长的父会话名称用于测试不截断'); - }); -}); diff --git a/apps/desktop/src/main/__tests__/build-info.test.ts b/apps/desktop/src/main/__tests__/build-info.test.ts deleted file mode 100644 index 4ce31a088d..0000000000 --- a/apps/desktop/src/main/__tests__/build-info.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * PR-BUILD-HYGIENE-0: cover the dev/packaged + commit-resolution - * branches of `resolveBuildInfo()` so the About-page badge cannot - * silently regress. - */ - -import { strict as assert } from 'node:assert'; -import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { describe, it } from 'node:test'; - -import { resolveBuildInfo } from '../build-info.js'; - -function makeTempRepo(setup: (gitDir: string) => void): string { - const root = mkdtempSync(join(tmpdir(), 'maka-build-info-')); - const gitDir = join(root, '.git'); - mkdirSync(gitDir, { recursive: true }); - setup(gitDir); - return root; -} - -describe('resolveBuildInfo', () => { - it('returns mode=packaged with no commit when app is packaged', () => { - const info = resolveBuildInfo(true, '/anywhere'); - assert.equal(info.mode, 'packaged'); - assert.equal(info.commit, null); - }); - - it('returns mode=dev with null commit when no .git is found', () => { - const root = mkdtempSync(join(tmpdir(), 'maka-build-info-nogit-')); - try { - const info = resolveBuildInfo(false, root); - assert.equal(info.mode, 'dev'); - assert.equal(info.commit, null); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it('reads loose ref when HEAD points to a branch with a loose ref file', () => { - const fullSha = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0'; - const root = makeTempRepo((gitDir) => { - writeFileSync(join(gitDir, 'HEAD'), 'ref: refs/heads/main\n'); - mkdirSync(join(gitDir, 'refs', 'heads'), { recursive: true }); - writeFileSync(join(gitDir, 'refs', 'heads', 'main'), `${fullSha}\n`); - }); - try { - const info = resolveBuildInfo(false, root); - assert.equal(info.mode, 'dev'); - assert.equal(info.commit, fullSha.slice(0, 7)); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it('falls back to packed-refs when the loose ref file is missing', () => { - const fullSha = 'feedfacedeadbeef0011223344556677889900aa'; - const root = makeTempRepo((gitDir) => { - writeFileSync(join(gitDir, 'HEAD'), 'ref: refs/heads/main\n'); - writeFileSync( - join(gitDir, 'packed-refs'), - `# pack-refs with: peeled fully-peeled sorted\n${fullSha} refs/heads/main\n`, - ); - }); - try { - const info = resolveBuildInfo(false, root); - assert.equal(info.commit, fullSha.slice(0, 7)); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - - it('handles detached HEAD (HEAD contains a sha directly)', () => { - const fullSha = '0123456789abcdef0123456789abcdef01234567'; - const root = makeTempRepo((gitDir) => { - writeFileSync(join(gitDir, 'HEAD'), `${fullSha}\n`); - }); - try { - const info = resolveBuildInfo(false, root); - assert.equal(info.commit, fullSha.slice(0, 7)); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); -}); diff --git a/apps/desktop/src/main/__tests__/computer-use-pip-appearance.test.ts b/apps/desktop/src/main/__tests__/computer-use-pip-appearance.test.ts deleted file mode 100644 index dea3145c49..0000000000 --- a/apps/desktop/src/main/__tests__/computer-use-pip-appearance.test.ts +++ /dev/null @@ -1,381 +0,0 @@ -/** - * What the Computer Use mirror looks like. - * - * Reported from a screenshot: a stray white L in one corner, two buttons - * crowding the picture, and a bare blue dot standing in for the cursor. The - * values behind those, and the ones recovered from Codex that replace them, - * are asserted here as values — not as strings that happen to appear in the - * file — so that reverting any single one of them fails. - */ - -import { strict as assert } from 'node:assert'; -import { readFile } from 'node:fs/promises'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { describe, it, test } from 'node:test'; -import { CODEX_CURSOR_GLYPH } from '../../renderer/computer-use-overlay/engine/cursor-engine.js'; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const DESKTOP = resolve(HERE, '..', '..', '..'); -const PIP_HTML = resolve(DESKTOP, 'src', 'overlay', 'pip.html'); -/** Built by `build:overlay`, which `build:test` runs before `test:dist`. */ -const PIP_BUNDLE = resolve(DESKTOP, 'dist', 'overlay', 'pip.js'); - -/** The page's stylesheet, comments removed. */ -async function styleSheet(): Promise { - const html = await readFile(PIP_HTML, 'utf8'); - const style = /