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
14 changes: 13 additions & 1 deletion docs/operations/scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,19 @@ cd scripts && pnpm exec e2e run scenarios/replay-turn-streams.e2e.ts --headed

e2e starts the dev stack through [`start-app.ts`](../../scripts/scenarios/start-app.ts) on a free
port, runs the tests, and stops it. Results land in `scripts/.e2e`: `report.json`, `summary.md`,
and per test a `trace.md`, its screenshots, and a video. The first run downloads e2e's browser.
and per test a `trace.md`, its screenshots, and a video. `vp run scenarios --target web` runs one
target; flags go straight after the script name, without `--`.

The first run downloads e2e's browser (about 120 MB). To fetch it ahead of time, such as in a
machine image, run this from the repository root after `vp i`:

```sh
node scripts/node_modules/playwright-core/cli.js install --only-shell chromium ffmpeg
```

e2e opens the web app at `127.0.0.1`, and the start script tells the web dev server to bind that
address. It does not use `localhost`, which some machines resolve to `::1` first.

`e2e run --help` lists the rest: `--grep`, `--watch`, `--last-failed`, `--changed-since`.

Run one at a time per checkout: a run recreates the checkout's scenario T3 home and its `.e2e`
Expand Down
11 changes: 8 additions & 3 deletions scripts/e2e.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ export const T3_HOME = resolved.home;
export const MOBILE_T3_HOME = `${T3_HOME}-mobile`;
export const MOBILE = resolved.mobile;
const startApp = resolved.startApp;
/**
* The one address e2e opens the web app at and the app is told to bind. Never `localhost`: the
* runner and the app can resolve that name to different addresses.
*/
const LOOPBACK = "127.0.0.1";

/**
* Leases a device from the Simlock pool, whose gateway speaks agent-device's lease protocol.
Expand Down Expand Up @@ -131,12 +136,12 @@ export default {
name: "web",
engine: web({ viewport: { width: 1280, height: 800 } }),
app: {
url: "http://127.0.0.1:0",
url: `http://${LOOPBACK}:0`,
// Vite answers before the server does; this path is proxied to the server.
readyUrl: "http://127.0.0.1:{port}/.well-known/t3/environment",
readyUrl: `http://${LOOPBACK}:{port}/.well-known/t3/environment`,
command: {
executable: resolved.node,
args: [startApp, "web", T3_HOME, "{port}"],
args: [startApp, "web", T3_HOME, LOOPBACK, "{port}"],
cwd: APP_DIR,
startupTimeout: 300_000,
log: ".e2e/app.log",
Expand Down
49 changes: 40 additions & 9 deletions scripts/scenarios/start-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,17 +141,21 @@ const startApp = Effect.fn("startApp")(function* (input: {
];

if (input.surface === "web") {
const [webPort = ""] = input.ports;
const serverPort = yield* net.reserveLoopbackPort();
// e2e opens the app at this host, so the web server is told to bind exactly it. Left to its
// default, `localhost`, it binds ::1 alone on machines that resolve the name to IPv6 first.
const [host = "", webPort = ""] = input.ports;
const origin = `http://${host}:${webPort}`;
const serverPort = String(yield* net.reserveLoopbackPort());
const token = yield* (yield* SecureRandom.SecureRandom).uuidV4();
const runner = yield* start({
const ports = ["--port", serverPort, "--dev-url", origin];
const webEnv = { ...env, T3CODE_PORT_OFFSET: String(Number(webPort) - BASE_WEB_PORT) };
const server = yield* start({
cwd: checkout,
command: node,
args: devRunner("dev", ["--port", String(serverPort)]),
args: devRunner("dev:server", ports),
env: {
...env,
T3CODE_PORT_OFFSET: String(Number(webPort) - BASE_WEB_PORT),
T3CODE_DEV_ALLOWED_ORIGINS: `http://127.0.0.1:${webPort}`,
...webEnv,
T3CODE_DEV_ALLOWED_ORIGINS: origin,
// Makes the startup pairing link reusable, so every test can sign in with it.
T3CODE_DEV_AUTH_TOKEN: `${token}${token}`.replaceAll("-", ""),
},
Expand All @@ -165,7 +169,32 @@ const startApp = Effect.fn("startApp")(function* (input: {
.pipe(Effect.ignore);
},
});
return yield* runner.exitCode;
const web = yield* start({
cwd: checkout,
command: node,
args: devRunner("dev:web", [...ports, "--", "--host", host]),
env: webEnv,
onLine: Console.log,
});
// A web server that came up on another loopback address would leave e2e waiting out its
// whole startup timeout; say so at once instead.
const boundElsewhere = Effect.gen(function* () {
const elsewhere = host === "::1" ? "127.0.0.1" : "::1";
if (yield* net.hasListenerOnHost(Number(webPort), host)) return yield* Effect.never;
if (!(yield* net.hasListenerOnHost(Number(webPort), elsewhere))) {
return yield* new ScenarioError({ message: "The web server is not listening yet." });
}
yield* Console.error(
`The web server listens on ${elsewhere}:${webPort}, not on ${host}: it ignored --host.`,
);
return 1;
}).pipe(
Effect.retry({
schedule: Schedule.spaced("1 second"),
while: (error) => error._tag === "ScenarioError",
}),
);
return yield* Effect.raceAll([server.exitCode, web.exitCode, boundElsewhere]);
}

// The device is another machine: the server listens on every interface, and Metro serves the
Expand Down Expand Up @@ -212,7 +241,9 @@ const command = Command.make(
),
home: Argument.String("t3-home").pipe(Argument.withDescription("T3 home to recreate.")),
ports: Argument.String("port").pipe(
Argument.withDescription("web: <web-port>. mobile: <server-port> <metro-port> <host>."),
Argument.withDescription(
"web: <host> <web-port>. mobile: <server-port> <metro-port> <host>.",
),
Argument.variadic(),
),
},
Expand Down
Loading