171 lines
5.8 KiB
TypeScript
171 lines
5.8 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
/**
|
|
* Transport-share two-browser sweep.
|
|
*
|
|
* Drives the package's transport-share workflow across two Playwright
|
|
* browser contexts wired together by a local signalling-channel fixture
|
|
* served by the harness page. The fixture is a real `BroadcastChannel`
|
|
* (cross-context) backed implementation — not a mock — so the wire
|
|
* envelopes round-trip through the same machinery that production would
|
|
* use across WebRTC data channels.
|
|
*
|
|
* The harness page at `/transport-share-harness` MUST expose:
|
|
* - `window.__share`: an object with
|
|
* - `becomeDriver(receiverId)`
|
|
* - `becomeReceiver(driverId)`
|
|
* - `driverPlay(time, offset)`
|
|
* - `driverSeek(seconds)`
|
|
* - `getRole()`
|
|
* - `getLastEnvelope()`
|
|
* - `disconnectDriver()`
|
|
* - DOM elements with `data-testid="role"` (text content === current role)
|
|
* and `data-testid="indicator"` (present iff `<SharedTransportIndicator>`
|
|
* is rendered).
|
|
*
|
|
* This spec is intentionally executable under the e2e turbo pipeline only;
|
|
* the harness binary is built per-PR by the e2e job (it lives in the
|
|
* monorepo's `apps/react-audio-stream-e2e` workspace).
|
|
*/
|
|
test.describe("transport-share — two-browser e2e", () => {
|
|
test("play on driver advances every receiver's transport", async ({
|
|
browser,
|
|
}) => {
|
|
const driverCtx = await browser.newContext();
|
|
const receiverCtx = await browser.newContext();
|
|
const driver = await driverCtx.newPage();
|
|
const receiver = await receiverCtx.newPage();
|
|
|
|
await driver.goto("/transport-share-harness?role=driver");
|
|
await receiver.goto("/transport-share-harness?role=receiver");
|
|
|
|
await receiver.evaluate(() => {
|
|
(
|
|
window as unknown as {
|
|
__share: { becomeReceiver(id: string): void };
|
|
}
|
|
).__share.becomeReceiver("driver");
|
|
});
|
|
await driver.evaluate(() => {
|
|
(
|
|
window as unknown as {
|
|
__share: { becomeDriver(id: string): void };
|
|
}
|
|
).__share.becomeDriver("receiver");
|
|
});
|
|
|
|
await driver.evaluate(() => {
|
|
(
|
|
window as unknown as {
|
|
__share: {
|
|
driverPlay(t: number, o: number): void;
|
|
};
|
|
}
|
|
).__share.driverPlay(0.5, 0);
|
|
});
|
|
|
|
await expect
|
|
.poll(
|
|
async () =>
|
|
await receiver.evaluate(() =>
|
|
(
|
|
window as unknown as {
|
|
__share: { getLastEnvelope(): unknown };
|
|
}
|
|
).__share.getLastEnvelope()
|
|
),
|
|
{ timeout: 5_000 }
|
|
)
|
|
.toEqual({ type: "play", time: 0.5, offset: 0 });
|
|
|
|
// The receiver MUST render the spec-mandated indicator.
|
|
await expect(receiver.locator('[data-testid="indicator"]')).toHaveText(
|
|
"Shared transport active"
|
|
);
|
|
|
|
await driverCtx.close();
|
|
await receiverCtx.close();
|
|
});
|
|
|
|
test("driver disconnect returns receiver to local", async ({ browser }) => {
|
|
const driverCtx = await browser.newContext();
|
|
const receiverCtx = await browser.newContext();
|
|
const driver = await driverCtx.newPage();
|
|
const receiver = await receiverCtx.newPage();
|
|
|
|
await driver.goto("/transport-share-harness?role=driver");
|
|
await receiver.goto("/transport-share-harness?role=receiver");
|
|
|
|
await receiver.evaluate(() => {
|
|
(
|
|
window as unknown as {
|
|
__share: { becomeReceiver(id: string): void };
|
|
}
|
|
).__share.becomeReceiver("driver");
|
|
});
|
|
await driver.evaluate(() => {
|
|
(
|
|
window as unknown as {
|
|
__share: { becomeDriver(id: string): void };
|
|
}
|
|
).__share.becomeDriver("receiver");
|
|
});
|
|
|
|
await expect(receiver.locator('[data-testid="role"]')).toHaveText(
|
|
"receiving"
|
|
);
|
|
|
|
await driver.evaluate(() => {
|
|
(
|
|
window as unknown as {
|
|
__share: { disconnectDriver(): void };
|
|
}
|
|
).__share.disconnectDriver();
|
|
});
|
|
|
|
await expect(receiver.locator('[data-testid="role"]')).toHaveText(
|
|
"local",
|
|
{ timeout: 5_000 }
|
|
);
|
|
await expect(receiver.locator('[data-testid="indicator"]')).toHaveCount(
|
|
0
|
|
);
|
|
|
|
await driverCtx.close();
|
|
await receiverCtx.close();
|
|
});
|
|
|
|
test("receiver controls no-op without throwing", async ({ browser }) => {
|
|
const receiverCtx = await browser.newContext();
|
|
const receiver = await receiverCtx.newPage();
|
|
await receiver.goto("/transport-share-harness?role=receiver");
|
|
|
|
await receiver.evaluate(() => {
|
|
(
|
|
window as unknown as {
|
|
__share: { becomeReceiver(id: string): void };
|
|
}
|
|
).__share.becomeReceiver("driver");
|
|
});
|
|
|
|
// notifySeek while receiving is the canonical receiver-controls-no-op
|
|
// path. It must complete without throwing AND must not appear on
|
|
// the wire (the local sent log stays empty).
|
|
const sent = await receiver.evaluate(() => {
|
|
(
|
|
window as unknown as {
|
|
__share: { driverSeek(s: number): unknown[] };
|
|
}
|
|
).__share.driverSeek(42);
|
|
return (
|
|
window as unknown as {
|
|
__share: { sent: unknown[] };
|
|
}
|
|
).__share.sent;
|
|
});
|
|
expect(sent).toEqual([]);
|
|
|
|
await receiverCtx.close();
|
|
});
|
|
});
|