Files
buzz/desktop/tests/e2e/terminal-wheel.spec.ts
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

234 lines
7.3 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const TERM = 'section[aria-label="Buzz Term"]';
const NAMED = 0x0100_0000;
const FG = NAMED | 256;
const BG = NAMED | 257;
/**
* The shipped mock bridge throws on every `terminal_*` command, so Buzz Term is
* unreachable through `installMockBridge` alone. This pre-creates
* `__TAURI_INTERNALS__` and traps the `invoke` assignment `mockIPC` makes:
* terminal commands are answered here, everything else falls through to the
* real mock bridge.
*/
async function installTerminalBackend(page: Page) {
await page.addInitScript(() => {
const w = window as typeof window & {
isTauri?: boolean;
__TAURI_INTERNALS__?: Record<string, unknown>;
__SAMI_TERM__?: unknown;
};
w.isTauri = true;
const state = {
deliver: null as ((message: unknown) => void) | null,
scrolls: [] as number[],
inputs: [] as string[],
subscriptionId: "sami-sub-1",
columns: 80,
screenLines: 24,
sequence: 0,
};
w.__SAMI_TERM__ = state;
const internals: Record<string, unknown> = {};
let inner: ((cmd: string, args: unknown, opts: unknown) => unknown) | null =
null;
Object.defineProperty(internals, "invoke", {
configurable: true,
get:
() => (cmd: string, args: Record<string, unknown>, opts: unknown) => {
switch (cmd) {
case "terminal_attach": {
const channel = args.onFrame as {
onmessage: (m: unknown) => void;
};
state.deliver = (message) => channel.onmessage(message);
return Promise.resolve({
sessionId: `sami-session-${state.sequence}`,
subscriptionId: state.subscriptionId,
viewport: {
generation: 1,
columns: state.columns,
screenLines: state.screenLines,
},
});
}
case "terminal_resize":
state.columns = args.columns as number;
state.screenLines = args.rows as number;
return Promise.resolve({
generation: 1,
columns: state.columns,
screenLines: state.screenLines,
});
case "terminal_input":
state.inputs.push(args.data as string);
return Promise.resolve(null);
case "terminal_scroll":
state.scrolls.push(args.lines as number);
return Promise.resolve(null);
case "terminal_viewport_ready":
case "terminal_ack":
case "terminal_focus":
case "terminal_detach":
case "terminal_close":
return Promise.resolve(null);
default:
if (!inner) throw new Error(`no mock bridge for ${cmd}`);
return inner(cmd, args, opts);
}
},
set: (fn: (cmd: string, args: unknown, opts: unknown) => unknown) => {
inner = fn;
},
});
w.__TAURI_INTERNALS__ = internals;
});
}
async function pushFrame(
page: Page,
rows: { line: number; text: string }[],
cursor: { line: number; column: number },
) {
await page.evaluate(
({ rows, cursor, fg, bg }) => {
const state = (
window as typeof window & {
__SAMI_TERM__: {
deliver: ((m: unknown) => void) | null;
subscriptionId: string;
columns: number;
screenLines: number;
sequence: number;
};
}
).__SAMI_TERM__;
state.sequence += 1;
state.deliver?.({
type: "frame",
payload: {
subscriptionId: state.subscriptionId,
sequence: state.sequence,
bracketedPaste: false,
focusReporting: false,
full: true,
viewport: {
generation: 1,
columns: state.columns,
screenLines: state.screenLines,
},
cursor: { ...cursor, visible: true },
rows: rows.map(({ line, text }) => ({
line,
spans: [
{
style: { fg, bg, flags: 0 },
clusters: [...text].map((ch, index) => ({
column: index,
text: ch,
width: 1,
})),
},
],
})),
},
});
},
{ rows, cursor, fg: FG, bg: BG },
);
}
async function reveal(page: Page) {
await page.setViewportSize({ width: 1280, height: 800 });
await installTerminalBackend(page);
await installMockBridge(page);
await page.goto("/");
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
// Buzz Term needs a channel: TerminalBootstrap's context is null on Home, so
// no session spawns and the chord is inert.
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.keyboard.press("Meta+j");
await expect(page.locator(TERM)).toHaveAttribute(
"data-terminal-owner",
"terminal",
);
// The dock opens in the normal app surface now; unlike the removed
// full-screen takeover, it must not fade that surface away. Wait for the
// dock's own height transition before interacting with its viewport.
await expect(page.locator(TERM)).toHaveAttribute(
"data-terminal-mode",
"docked",
);
await expect(page.locator(TERM)).toBeVisible();
await expect
.poll(async () => page.locator(TERM).evaluate((el) => el.clientHeight))
.toBeGreaterThanOrEqual(180);
}
test("scrollback: wheel over Buzz Term reaches terminal_scroll", async ({
page,
}) => {
await reveal(page);
await pushFrame(page, [{ line: 0, text: "buzz@term:~$ " }], {
line: 0,
column: 13,
});
await page.mouse.move(640, 500);
await page.mouse.wheel(0, -17 * 5);
await page.mouse.wheel(0, 17 * 2);
const scrolls = await page.evaluate(
() =>
(window as typeof window & { __SAMI_TERM__: { scrolls: number[] } })
.__SAMI_TERM__.scrolls,
);
console.log("SCROLLS", JSON.stringify(scrolls));
expect(scrolls).toEqual([-5, 2]);
});
test("terminal viewport click retains keyboard input ownership", async ({
page,
}) => {
await reveal(page);
const input = page.getByLabel("Terminal input");
await expect(input).toBeFocused();
await page
.locator(".buzz-terminal-viewport")
.click({ position: { x: 40, y: 40 } });
await expect(input).toBeFocused();
await page.keyboard.type("FOCUS_KEYSTROKE");
await expect
.poll(async () =>
page.evaluate(() =>
(
window as typeof window & { __SAMI_TERM__: { inputs: string[] } }
).__SAMI_TERM__.inputs.join(""),
),
)
.toContain("FOCUS_KEYSTROKE");
});
test("concealed terminal viewport does not steal Buzz focus", async ({
page,
}) => {
await reveal(page);
await page.keyboard.press("Meta+j");
await expect(page.locator(TERM)).toHaveCount(0);
const input = page.getByLabel("Terminal input");
await expect(input).toHaveCount(0);
await page.getByTestId("chat-title").click();
await page.keyboard.type("BUZZ_KEYSTROKE");
const terminalInputs = await page.evaluate(
() =>
(window as typeof window & { __SAMI_TERM__: { inputs: string[] } })
.__SAMI_TERM__.inputs,
);
expect(terminalInputs.join("")).not.toContain("BUZZ_KEYSTROKE");
});