Files
paseo/packages/app/e2e/browser/new-workspace-dictation-submit.spec.ts
T
Mohamed BoudraandGitHub 5a30affd7f Skip unrelated required checks without losing their names (#2542)
* fix(ci): skip unrelated required checks cleanly

Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable.

* fix(ci): gate changes to install patching script

* fix(ci): route cross-package test contracts narrowly

* fix(ci): keep routed contracts merge-blocking

* fix(server): exclude nested test utilities from builds

* fix(ci): cover app-owned native and browser domains

* refactor(ci): align tests with runtime domains

Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check.

* fix(ci): keep routing and desktop suites isolated

The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs.

* fix(ci): isolate desktop e2e runtime

* perf(test): isolate Playwright workers

* perf(test): parallelize Playwright within specs

* fix(test): keep Playwright file scheduling

* fix(test): share dynamic Metro fixture

* fix(test): preserve own-host restart fixtures

* fix(ci): preserve routed contracts after rebase

* Stabilize Playwright wheel-scroll assertions
2026-08-02 00:56:05 +08:00

199 lines
6.4 KiB
TypeScript

import { expect, test, type Page } from "../support/fixtures";
import { gotoAppShell } from "../support/helpers/app";
import { daemonWsRoutePattern } from "../support/helpers/daemon-port";
import {
openNewWorkspaceComposer,
selectWorkspaceIsolation,
} from "../support/helpers/new-workspace";
import { seedWorkspace } from "../support/helpers/seed-client";
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
type WebSocketMessage = string | Buffer;
const TRANSCRIPT = "Keep this spoken prompt visible while creating the workspace";
const CREATE_FAILURE = "Synthetic workspace creation failure";
function parseEnvelope(message: WebSocketMessage): {
type?: unknown;
message?: Record<string, unknown>;
} | null {
const raw = typeof message === "string" ? message : message.toString("utf8");
try {
return JSON.parse(raw) as { type?: unknown; message?: Record<string, unknown> };
} catch {
return null;
}
}
function sessionMessage(message: WebSocketMessage): Record<string, unknown> | null {
const envelope = parseEnvelope(message);
return envelope?.type === "session" && envelope.message ? envelope.message : null;
}
function sendSessionMessage(
ws: { send(message: string): void },
message: Record<string, unknown>,
): void {
ws.send(JSON.stringify({ type: "session", message }));
}
function enableDictationCapability(message: WebSocketMessage): WebSocketMessage {
const envelope = parseEnvelope(message);
const payload = envelope?.message?.payload;
if (
envelope?.message?.type !== "status" ||
!payload ||
typeof payload !== "object" ||
(payload as { status?: unknown }).status !== "server_info"
) {
return message;
}
(payload as Record<string, unknown>).capabilities = {
voice: {
dictation: { enabled: true, reason: "" },
voice: { enabled: false, reason: "Realtime voice is disabled in this test." },
},
};
return JSON.stringify(envelope);
}
async function installSyntheticMicrophone(page: Page): Promise<void> {
await page.addInitScript(() => {
const mediaDevices = navigator.mediaDevices;
Object.defineProperty(mediaDevices, "getUserMedia", {
configurable: true,
value: async () => {
const context = new AudioContext();
const oscillator = context.createOscillator();
const destination = context.createMediaStreamDestination();
oscillator.connect(destination);
oscillator.start();
return destination.stream;
},
});
});
}
async function installDictationFailureHarness(page: Page) {
let resolveAudioChunk!: () => void;
let resolveCreateRequest!: () => void;
const audioChunk = new Promise<void>((resolve) => {
resolveAudioChunk = resolve;
});
const createRequest = new Promise<void>((resolve) => {
resolveCreateRequest = resolve;
});
let failCreate: (() => void) | null = null;
await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
const server = ws.connectToServer();
ws.onMessage((message) => {
const request = sessionMessage(message);
const type = request?.type;
const dictationId = typeof request?.dictationId === "string" ? request.dictationId : null;
if (type === "dictation_stream_start" && dictationId) {
sendSessionMessage(ws, {
type: "dictation_stream_ack",
payload: { dictationId, ackSeq: -1 },
});
return;
}
if (type === "dictation_stream_chunk" && dictationId) {
const seq = typeof request?.seq === "number" ? request.seq : 0;
sendSessionMessage(ws, {
type: "dictation_stream_ack",
payload: { dictationId, ackSeq: seq },
});
resolveAudioChunk();
return;
}
if (type === "dictation_stream_finish" && dictationId) {
sendSessionMessage(ws, {
type: "dictation_stream_finish_accepted",
payload: { dictationId, timeoutMs: 5_000 },
});
sendSessionMessage(ws, {
type: "dictation_stream_final",
payload: { dictationId, text: TRANSCRIPT },
});
return;
}
if (request && type === "workspace.create.request" && typeof request.requestId === "string") {
const requestId = request.requestId;
failCreate = () => {
sendSessionMessage(ws, {
type: "workspace.create.response",
payload: {
requestId,
workspace: null,
setupTerminalId: null,
error: CREATE_FAILURE,
},
});
};
resolveCreateRequest();
return;
}
server.send(message);
});
server.onMessage((message) => ws.send(enableDictationCapability(message)));
});
return {
waitForAudio: () => audioChunk,
waitForCreateRequest: () => createRequest,
failWorkspaceCreation: () => {
if (!failCreate) {
throw new Error("Workspace creation request has not been received");
}
failCreate();
},
};
}
async function dictateAndSend(page: Page, waitForAudio: () => Promise<void>): Promise<void> {
await page.getByRole("button", { name: "Start dictation" }).click();
await waitForAudio();
await page.getByRole("button", { name: "Insert transcription and send" }).click();
}
test.describe("New Workspace dictation submit", () => {
test.describe.configure({ timeout: 240_000 });
test("keeps the spoken prompt visible while pending and after failure", async ({ page }) => {
const seeded = await seedWorkspace({ repoPrefix: "dictation-submit-" });
await installSyntheticMicrophone(page);
const harness = await installDictationFailureHarness(page);
try {
await gotoAppShell(page);
await waitForSidebarHydration(page);
await openNewWorkspaceComposer(page, {
projectKey: seeded.projectKey,
projectDisplayName: seeded.projectDisplayName,
});
await selectWorkspaceIsolation(page, "local");
await dictateAndSend(page, harness.waitForAudio);
await harness.waitForCreateRequest();
const composer = page.getByRole("textbox", { name: "Message agent..." });
await expect(composer).toHaveValue(TRANSCRIPT);
await expect(composer).not.toBeEditable();
harness.failWorkspaceCreation();
await expect(composer).toBeEditable();
await expect(composer).toHaveValue(TRANSCRIPT);
await expect(page.getByText(CREATE_FAILURE).first()).toBeVisible();
} finally {
await seeded.cleanup();
}
});
});