Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions apps/desktop/e2e-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@
"electron": "SQLite admission crosses preload and main, survives both renderer reload and complete Electron restart, and cached history must render when the live IPC endpoint is unavailable"
},
"session-workbar.spec.ts": {
"tests": 6,
"electron": "Git changes re-read on window focus, terminal PTY ownership across Sessions, Side Chat's fork lifecycle, a first send that has to reach the Host, and per-Session collapse persisted across a renderer reload; the composer-usage test is renderer-only and rides along on those windows until app-shell.tsx's composer-to-workbar wiring has a story host"
"tests": 5,
"electron": "Git changes re-read on native window focus; terminal PTY ownership changes across Sessions; Side Chat owns a Host fork lifecycle; first send reaches the Host; the composer usage entry opens a workbar whose per-Session visibility survives renderer reload."
},
"settings.spec.ts": {
"tests": 5,
"electron": "Code Mode persistence crosses renderer/preload/main/Host settings IPC; the preload makaE2eLatch holds the settings chunk mid-load, and the rename it commits is a Host write; the workbar-chrome test rides along on that window and would move to a story the day app-shell.tsx's settings wiring has one"
"tests": 2,
"electron": "Code Mode reads back Host settings through IPC; opening Settings commits a pending title rename to the Host, with workbar hide/restore asserted in that same Session."
},
"sidebar-project-reload.spec.ts": {
"tests": 1,
Expand All @@ -66,8 +66,8 @@
"electron": "observation seeding, reconnect and settle are Host subscriptions surviving a renderer remount"
},
"transcript-scroll-cost.spec.ts": {
"tests": 4,
"electron": "the perf budget is measured from CDP wheel input and the browser's own render skipping, and reader displacement only exists against a real layout"
"tests": 1,
"electron": "One traversal exercises real Host transcript paging through preload/IPC, bounded retained ranges, reader displacement when pages install, and a return-to-tail Host read. Chromium containment and fixture motion run in the TranscriptRenderCost story."
},
"workhub-layout.spec.ts": {
"tests": 2,
Expand Down
72 changes: 72 additions & 0 deletions apps/desktop/e2e/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# Electron test admission

Before adding or extending a test here, choose the lowest tier that can expose
the actual defect. Apply this to each behavioral assertion, not just each file.
Existing tests and entries in `../e2e-budget.json` are not exemptions. See #4761,
#4877 and #4892 for the migration history.

1. Use existing unit, component or integration tests for state, event ordering,
request routing, storage and recovery. Desktop renderer tests already run
from `../src/main/__tests__` through `node --test`; React tests use the existing
fake DOM. The directory name does not restrict them to main-process code.
Shared UI tests live in `../../../packages/ui/src/__tests__`.
2. Use Storybook or a focused browser test when the assertion needs Chromium:
layout, scrolling, selection, focus, animation, viewport or theme behavior.
Reuse `../stories`, `../../../packages/ui/stories` and
[the fidelity convention](../stories/FIDELITY.md).
3. Use Electron only when a lower tier would miss a concrete Electron boundary:
native window/input behavior, Electron preload/main integration, or a
cross-process persistence or lifecycle failure requiring the actual app.

## Prove the boundary

- State beside each test which Electron-owned mechanism it verifies and what
defect a lower-tier test would miss. A call used only to prepare the scenario
is not that mechanism. Creating a Host Session does not make a focus test an
IPC test; reading a Host snapshot does not by itself prove Electron is needed.
- Real wheel input, CDP, geometry, localStorage, page reload, or a preload test
latch alone do not establish an Electron requirement. Trace the owner of the
behavior being asserted. Node integration tests can also exercise real Host
and storage boundaries.
- Do not justify a renderer-only assertion by another test in the same file,
an already-open window, or an unchanged test count. Keep Electron journeys
focused on their necessary boundaries; move independent renderer contracts.
- Search existing lower-tier coverage before adding a replacement. Extend the
actual component/controller/service seam; do not copy product logic into a
second shell or introduce a global render-completion protocol for tests.

## Migrate and verify

- Establish that the replacement detects the original defect, preferably by
reverting the relevant behavior or a targeted mutation. Verify the behavioral
failure, not merely an import/type error or the absence of a new helper.
- Delete replaced E2E cases, duplicate assertions and unreferenced fixture hooks
in the same PR. Preserve any independent native or cross-process protection.
- Do not raise timeouts, add retries, or weaken assertions to make a migration
pass. Diagnose a red replacement before deciding it is a harness problem.
- Update `../e2e-budget.json` with counts and concrete boundary reasons, then run
`npm run check:e2e-budget` from the repository root. This checks inventory
consistency; a green result is not proof of correct tier selection.
- Build before running compiled tests or Storybook smoke. Run Electron tests
from `apps/desktop` because the fixtures use that working directory. Report
transferred/lost protection and fixture launches or traversals removed,
separately from the number of test declarations.
22 changes: 4 additions & 18 deletions apps/desktop/e2e/session-workbar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,6 @@ async function createSession(page: Page, prompt: string) {
return { composer, sessionId: sessionId!, sidebar };
}

test('the composer usage action opens Task trace in the right workbar', async ({
accessibilityNarrativeWindow: page,
}) => {
const action = page.getByRole('button', { name: '打开用量追踪' });
await expect(action).toBeVisible();

await action.click();

const rightPanel = page.locator(
'.maka-session-workbar-panel[data-overlay][data-placement="right"]',
);
await expect(
rightPanel.locator('[data-maka-contract="session-inspector"]'),
).toBeVisible();
});

test('right workbar visibility belongs to each Session and survives reload', async ({
window: page,
}) => {
Expand All @@ -79,8 +63,10 @@ test('right workbar visibility belongs to each Session and survives reload', asy
.getByRole('list', { name: '打开工具' })
.getByRole('button', { name: /变更.*查看当前 Git 工作区变化/ })
.click();
await page.getByRole('button', { name: '打开或关闭工作栏的面' }).click();
await page.getByRole('menu').getByRole('menuitem', { name: '追踪', exact: true }).click();
await page.getByRole('button', { name: '打开用量追踪' }).click();
await expect(page.locator(
'.maka-session-workbar-panel[data-overlay][data-placement="right"] [data-maka-contract="session-inspector"]',
)).toBeVisible();
await expect(panel).toBeVisible();
await first.sidebar.getByRole('button', { name: '新任务', exact: true }).click();
const second = await createSession(page, 'second workbar owner');
Expand Down
161 changes: 13 additions & 148 deletions apps/desktop/e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,6 @@

import { awaitSendReady, COMPOSER_INPUT, ensureSidebarExpanded, expect, test } from './fixtures';

interface SettingsChunkLatchWindow extends Window {
makaE2eLatch?: {
arm(key: 'settings.chunk'): void;
release(key: 'settings.chunk'): void;
};
}

test('Code Mode persists as a global setting after reopening settings', async ({ window: page }, testInfo) => {
await ensureSidebarExpanded(page);
await page.getByRole('button', { name: '设置' }).click();
Expand All @@ -48,84 +41,12 @@ test('Code Mode persists as a global setting after reopening settings', async ({
await expect.poll(() => page.evaluate(async () => (await window.maka.settings.get()).chatDefaults.codeModeEnabled === true)).toBe(false);
});

test('Settings loading surface owns unmodified Escape', async ({ window: page }) => {
const latchInstalled = await page.evaluate(() => {
const e2eLatch = (window as unknown as SettingsChunkLatchWindow).makaE2eLatch;
e2eLatch?.arm('settings.chunk');
return e2eLatch !== undefined;
});
expect(latchInstalled, 'the preload E2E latch is installed').toBe(true);

try {
await ensureSidebarExpanded(page);
await page.getByRole('button', { name: '设置' }).click();

const loadingSurface = page.locator('.maka-lazy-fallback');
await expect(loadingSurface).toBeVisible();

for (const modifier of ['ctrlKey', 'metaKey', 'altKey'] as const) {
const wasNotPrevented = await page.evaluate((key) =>
window.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Escape',
[key]: true,
bubbles: true,
cancelable: true,
}),
), modifier);
expect(wasNotPrevented).toBe(true);
await expect(loadingSurface).toBeVisible();
}

const wasNotPrevented = await page.evaluate(() =>
window.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Escape',
bubbles: true,
cancelable: true,
}),
),
);
expect(wasNotPrevented).toBe(false);
await expect(page.locator('.settingsModal')).toHaveCount(0);
} finally {
await page.evaluate(() =>
(window as unknown as SettingsChunkLatchWindow).makaE2eLatch?.release(
'settings.chunk',
),
);
}
});

test('opening settings commits an active titlebar rename', async ({ window: page }) => {
const composer = page.locator(COMPOSER_INPUT);
await composer.fill('create a session for settings rename');
await awaitSendReady(page);
await composer.press('Enter');

const identity = page.locator('[data-maka-contract="titlebar-identity"]');
await expect(identity).toBeVisible();
await page.getByRole('button', { name: '展开侧边栏' }).click();
await identity.getByRole('button', { name: /重命名任务/ }).click();
await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');

// Programmatic activation preserves input focus, matching the macOS
// application-menu command that opens Settings before Chromium can blur it.
await page.getByRole('button', { name: '设置' }).evaluate((button) => button.click());
await expect(page.getByRole('main', { name: '设置内容' })).toBeVisible();
await page.keyboard.press('Escape');

await expect(identity).toContainText('renamed before settings');
});

test('settings hides expanded workbar chrome and restores it on close', async ({
window: page,
}) => {
const composer = page.locator(COMPOSER_INPUT);
await composer.fill('create a session with an expanded workbar');
await awaitSendReady(page);
await composer.press('Enter');

await page.getByRole('button', { name: '展开任务工作栏' }).click();
const workbar = page.locator('.maka-session-workbar[data-placement="right"]');
const workbarToolbar = workbar.getByRole('toolbar', { name: '任务工作栏标签' });
Expand All @@ -140,79 +61,23 @@ test('settings hides expanded workbar chrome and restores it on close', async ({
const openFaceTab = workbarToolbar.getByRole('tab', { name: '变更' });
await expect(openFaceTab).toBeVisible();

await ensureSidebarExpanded(page);
await page.getByRole('button', { name: '设置' }).click();
const identity = page.locator('[data-maka-contract="titlebar-identity"]');
await expect(identity).toBeVisible();
await page.getByRole('button', { name: '展开侧边栏' }).click();
await identity.getByRole('button', { name: /重命名任务/ }).click();
await page.getByRole('textbox', { name: '重命名任务' }).fill('renamed before settings');

// Programmatic activation preserves input focus, matching the macOS
// application-menu command that opens Settings before Chromium can blur it.
await page.getByRole('button', { name: '设置' }).evaluate((button) => button.click());
await expect(page.getByRole('main', { name: '设置内容' })).toBeVisible();
await expect(workbar).not.toBeVisible();

await page.keyboard.press('Escape');
await expect(workbarToolbar).toBeVisible();
await expect(openFaceTab).toBeVisible();
});

test('reopening settings keeps the last-ready General page stable while refreshing', async ({
window: page,
}) => {
await ensureSidebarExpanded(page);
await page.getByRole('button', { name: '设置' }).click();
const settings = page.locator('.settingsSurface');
await page.getByRole('button', { name: '通用', exact: true }).click();
await expect(settings.getByRole('textbox', { name: '助手语气偏好' })).toBeEnabled();
await expect(settings.getByRole('button', { name: '默认模型' })).toBeEnabled();

await page.keyboard.press('Escape');
await expect(settings).toHaveCount(0);
await expect(page.getByRole('button', { name: '设置' })).toBeVisible();

await page.evaluate(() => {
const state = {
sawLoadingWarning: false,
sawGeneralWithoutReadyHostControls: false,
};
const inspect = () => {
const surface = document.querySelector('.settingsSurface');
const main = surface?.querySelector('main, [role="main"]');
if (!surface || !main) return;
state.sawLoadingWarning ||= Array.from(
surface.querySelectorAll('[role="alert"]'),
).some((banner) => banner.textContent?.includes('正在加载设置') === true);
const defaultModelReady = Array.from(main.querySelectorAll('*')).some(
(element) =>
element.children.length === 0 &&
element.textContent?.trim() === '默认模型' &&
Boolean(element.closest('.astryx-item')?.querySelector('button')),
);
state.sawGeneralWithoutReadyHostControls ||=
main.querySelector('textarea') === null || !defaultModelReady;
};
const observer = new MutationObserver(inspect);
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
Object.assign(window, {
__makaSettingsReopenProbe: {
finish() {
inspect();
observer.disconnect();
return state;
},
},
});
});

await page.getByRole('button', { name: '设置' }).click();
await expect(settings.getByRole('textbox', { name: '助手语气偏好' })).toBeEnabled();
await expect(settings.getByRole('button', { name: '默认模型' })).toBeEnabled();

const probe = await page.evaluate(() => {
const target = window as typeof window & {
__makaSettingsReopenProbe: {
finish(): {
sawLoadingWarning: boolean;
sawGeneralWithoutReadyHostControls: boolean;
};
};
};
return target.__makaSettingsReopenProbe.finish();
});
expect(probe.sawLoadingWarning).toBe(false);
expect(probe.sawGeneralWithoutReadyHostControls).toBe(false);
await expect(identity).toContainText('renamed before settings');
await expect.poll(() => page.evaluate(async () =>
(await window.maka.sessions.list()).some((session) => session.name === 'renamed before settings'),
)).toBe(true);
});
Loading