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
49 changes: 43 additions & 6 deletions e2e/multi-tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
await page.click('form.todo-form button[type="submit"]');
}

test.describe('TabMesh — multi-tab harness', () => {

Check failure on line 56 in e2e/multi-tab.spec.ts

View workflow job for this annotation

GitHub Actions / Test

e2e/multi-tab.spec.ts

Error: Playwright Test did not expect test.describe() to be called here. Most common reasons include: - You are calling test.describe() in a configuration file. - You are calling test.describe() in a file that is imported by the configuration file. - You have two different versions of @playwright/test. This usually happens when one of the dependencies in your package.json depends on @playwright/test. ❯ TestTypeImpl._currentSuite node_modules/.pnpm/playwright@1.59.1/node_modules/playwright/lib/common/testType.js:75:13 ❯ TestTypeImpl._describe node_modules/.pnpm/playwright@1.59.1/node_modules/playwright/lib/common/testType.js:115:24 ❯ Function.describe node_modules/.pnpm/playwright@1.59.1/node_modules/playwright/lib/transform/transform.js:275:12 ❯ e2e/multi-tab.spec.ts:56:6
test.describe.configure({ mode: 'serial' });

test('single WS connection across two tabs (#1)', async ({ context }) => {
Expand Down Expand Up @@ -247,12 +247,49 @@
await b.close();
});

test.fixme('worker-side observation of lifecycle messages (#6)', async () => {
// SharedWorker console output is not visible to Playwright's page-
// attached console listener. Either:
// (a) route worker logs over a `port.ping`-style introspection that
// returns the recorded visibility state, or
// (b) use chrome://inspect/#workers via CDP to attach to the SW.
test('worker records lifecycle messages and replies via pong (#6)', async ({ context }) => {
// Open the playground, drive a visibilitychange so the tab posts a
// `lifecycle` message to the worker, then probe the worker by opening
// a fresh SharedWorker port (same name) and sending a `ping` with the
// playground tab's id. The pong now carries `visibilityState`, which
// proves the lifecycle message reached the worker registry.
const a = await newPlaygroundTab(context);
await waitForTransportConnected(a);

const playgroundTabId = await a.evaluate(() =>
sessionStorage.getItem('tabmesh:tabId:playground-todos')
);
expect(playgroundTabId).toBeTruthy();

await a.evaluate(() => {
Object.defineProperty(document, 'visibilityState', {
value: 'hidden',
configurable: true,
});
document.dispatchEvent(new Event('visibilitychange'));
});

// Give the lifecycle message a tick to land in the worker.
await a.waitForTimeout(150);

const visibility = await a.evaluate((targetTabId) => {
return new Promise<string | undefined>((resolve) => {
const w = new SharedWorker('/tabmesh-worker.js', {
name: 'tabmesh:playground-todos',
});
w.port.onmessage = (e) => {
const msg = e.data as { kind?: string; visibilityState?: string };
if (msg?.kind === 'pong') resolve(msg.visibilityState);
};
w.port.start();
w.port.postMessage({ kind: 'ping', tabId: targetTabId });
setTimeout(() => resolve(undefined), 1500);
});
}, playgroundTabId);

expect(visibility).toBe('hidden');

await a.close();
});

test.fixme('Service Worker Background Sync drains pending events (#26 / #27)', async () => {
Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,17 @@ export type HubMessage =
| { kind: 'clear-outbox-ack' }
| { kind: 'broadcast-event'; event: TabMeshEvent }
| { kind: 'ping'; tabId: string }
| { kind: 'pong'; tabId: string }
| {
kind: 'pong';
tabId: string;
/**
* Worker's recorded visibility state for `tabId`, or undefined if
* the tab is not in the registry. Populated by the SharedWorker
* hub; useful for tests verifying that lifecycle messages reached
* the worker.
*/
visibilityState?: TabVisibilityState;
}
| { kind: 'lifecycle'; tabId: string; state: TabVisibilityState }
| { kind: 'leader-elected'; tabId: string; term: number }
| { kind: 'system-event'; event: TabMeshEvent }
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/worker/tabmesh-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,11 @@ function handlePing(port: MessagePort, msg: Extract<HubMessage, { kind: 'ping' }
if (entry) {
entry.lastSeenAt = Date.now();
}
const pong: HubMessage = { kind: 'pong', tabId: msg.tabId };
const pong: HubMessage = {
kind: 'pong',
tabId: msg.tabId,
visibilityState: entry?.visibilityState,
};
port.postMessage(pong);
}

Expand Down
6 changes: 5 additions & 1 deletion packages/playground/public/tabmesh-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,11 @@
if (entry) {
entry.lastSeenAt = Date.now();
}
const pong = { kind: "pong", tabId: msg.tabId };
const pong = {
kind: "pong",
tabId: msg.tabId,
visibilityState: entry?.visibilityState
};
port.postMessage(pong);
}
function handleLifecycle(msg) {
Expand Down
Loading