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
2,107 changes: 2,107 additions & 0 deletions docs/plans/2026-04-29-event-based-waits-pr2-peer-wrapper.md

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,76 @@ the same commit rather than fixing the selector.
- `just test-e2e-full` — full setup + teardown + run, good for CI.
- `PLAYWRIGHT_WORKERS=N npx playwright test ...` — override worker count.
- `PLAYWRIGHT_FULLY_PARALLEL=0 npx playwright test ...` — disable intra-file parallelism.

## Helpers layout

The legacy 703-LOC `helpers.ts` has been split into focused modules. New
specs should import directly from the focused module they need.

```
e2e/
├── helpers/
│ ├── peers.ts -- freshStart, createServer, getPeerId, generateInvite,
│ │ joinViaInvite, setupTwoPeers, openServerSettings, waitForApp
│ ├── ui.ts -- visibleShell, isMobile, sendMessage, waitForMessage,
│ │ switchChannel, openSidebar, openMemberList, createChannel,
│ │ messageAction, editMessage, deleteMessage, reactToMessage,
│ │ trustPeer, untrustPeer, kickPeer, openCompareFingerprints, …
│ └── touch.ts -- longPress, longPressAvatar, swipeLeft, swipeRight
├── helpers.ts -- re-export barrel; un-migrated specs continue to import
│ from './helpers' with zero diff
├── test-hooks.ts -- Peer wrapper + `peer` fixture (see "Event-based waits" below)
└── *.spec.ts
```

## Event-based waits (Peer wrapper)

The web crate exposes `window.__willow` and a `__willowEvent` push stream
when built with `--features test-hooks`. The `Peer` class in
`e2e/test-hooks.ts` wraps both:

- **Pull**: `peer.snapshot()`, `peer.heads()`, `peer.eventCount()`,
`peer.lastEvent()` — each round-trips through `window.__willow.*`.
- **Push**: `peer.nextEvent(predicate, { timeout? })` — drains the next
event matching `predicate` from the per-page event queue.
- **Convergence**: `peer.waitUntilHeadsEqual(otherPeer)` and
`peer.waitUntilAllHeadsEqual([otherPeers])` — `expect.poll`-based
CRDT convergence checks. Failure messages include a structured
per-author-key diff so missing-author hangs are debuggable without a
manual `console.log`.

Specs that need the wrapper import the typed `test` + `expect` from
`./test-hooks` instead of `@playwright/test`:

```ts
import { test, expect } from './test-hooks';

test('peer B converges with peer A', async ({ peer, browser }) => {
const { ctx1, ctx2, page1, page2 } = await setupTwoPeers(browser);
const a = await peer(page1, 'Alice');
const b = await peer(page2, 'Bob');
await b.waitUntilHeadsEqual(a); // gossip-side wait
await expect(page2.locator('.channel-item', { hasText: 'general' }))
.toBeVisible(); // default 5s — DOM-only after convergence
});
```

`peer(page, label)` is async and idempotently wires `__willowEvent`
bindings on the page's `BrowserContext` on first call per context, so
contexts created via `browser.newContext()` or `setupTwoPeers(browser)`
work without per-spec setup. Call `peer()` before the page's first
`goto()` when possible — `addInitScript` only takes effect on
subsequent loads.

The full design is in
[`docs/specs/2026-04-27-event-based-waits-design.md`](../docs/specs/2026-04-27-event-based-waits-design.md).
Migration progress for the remaining 7 specs is tracked in
[#458](https://github.com/intendednull/willow/issues/458).

## Anti-patterns blocked by ESLint

`page.waitForTimeout(ms)` is blocked by `no-restricted-syntax` in
`eslint.config.js`. Specs migrated off the timeout pattern remove their
file-top `eslint-disable` header in the same PR. Each remaining
disabled file references issue #458; the rule sunsets on 2026-09-30
(per spec §"Sunset").
88 changes: 88 additions & 0 deletions e2e/helpers.barrel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// e2e/helpers.barrel.test.ts
//
// Build-time coverage of the helpers.ts barrel. Asserts every name imported
// by any un-migrated spec is still re-exported. If you remove a name from
// helpers/{peers,ui,touch}.ts, tsc fails here with TS2305 before any
// Playwright test runs.
//
// This is NOT a Playwright spec (filename uses .test.ts so Playwright's
// default `testMatch: '*.spec.ts'` skips it). It executes only as part of
// `npx tsc --noEmit` / `npx eslint`.

import {
// peers
freshStart,
createServer,
getPeerId,
generateInvite,
joinViaInvite,
setupTwoPeers,
waitForApp,
openServerSettings,
// ui
sendMessage,
waitForMessage,
switchChannel,
createChannel,
openSidebar,
closeSidebar,
openMemberList,
closeMemberList,
visibleShell,
isMobile,
messageAction,
editMessage,
deleteMessage,
reactToMessage,
trustPeer,
untrustPeer,
kickPeer,
openCompareFingerprints,
markFingerprintsMatch,
markFingerprintsMismatch,
getMessages,
switchTab,
// touch
longPress,
longPressAvatar,
swipeLeft,
swipeRight,
} from './helpers';

// One reference per name so TS can't tree-shake the imports away. The
// `void` operator silences `@typescript-eslint/no-unused-expressions`
// without needing an eslint-disable comment.
void freshStart;
void createServer;
void getPeerId;
void generateInvite;
void joinViaInvite;
void setupTwoPeers;
void waitForApp;
void openServerSettings;
void sendMessage;
void waitForMessage;
void switchChannel;
void createChannel;
void openSidebar;
void closeSidebar;
void openMemberList;
void closeMemberList;
void visibleShell;
void isMobile;
void messageAction;
void editMessage;
void deleteMessage;
void reactToMessage;
void trustPeer;
void untrustPeer;
void kickPeer;
void openCompareFingerprints;
void markFingerprintsMatch;
void markFingerprintsMismatch;
void getMessages;
void switchTab;
void longPress;
void longPressAvatar;
void swipeLeft;
void swipeRight;
Loading