Skip to content
Source
Sign inSign up

t3-mobile.ts

93 lines3.6 KB

1// `test` for iOS scenarios: e2e's mobile test plus `t3`, which seeds state like the web one and
2// opens the dev client paired with the scenario's server.
3import { test as base } from "@e2e-dev/mobile";
4import * as HostProcess from "@t3tools/shared/HostProcess";
5import { expect } from "e2e";
6import * as Effect from "effect/Effect";
7import * as Path from "effect/Path";
8import * as Schema from "effect/Schema";
9​
10import { APP_DIR, MOBILE, MOBILE_T3_HOME } from "../e2e.config.ts";
11import { run, ScenarioError } from "./shell.ts";
12import { makeState, runPromise, type T3State } from "./t3.ts";
13​
14const APP_ID = "com.t3tools.t3code.dev";
15const SCHEME = "t3code-dev";
16​
17export interface MobileT3 extends T3State {
18 /** Installs the dev client if the device lacks it, loads it from Metro, and pairs it. */
19 readonly open: () => Promise<void>;
21​
22const decodePrepared = Schema.decodeUnknownEffect(
23 Schema.fromJsonString(Schema.Struct({ fingerprint: Schema.String })),
24);
25​
26/** The dev client built for this checkout's native code, from the shared cache. */
27const devClientPath = Effect.gen(function* () {
28 const path = yield* Path.Path;
29 const prepared = yield* run(
30 yield* HostProcess.ExecutablePath,
31 ["scripts/mobile-native-client.ts", "ensure", "ios", "--prepare"],
32 { cwd: APP_DIR },
33 );
34 const { fingerprint } = yield* decodePrepared(prepared.stdout.trim().split("\n").at(-1) ?? "");
35 return path.join(
36 yield* HostProcess.HomeDirectory,
37 ".cache/t3code/native-clients/artifacts/ios/development",
38 fingerprint,
39 "T3CodeDev.app",
40 );
41});
42​
43// A leased device is wiped when it returns to the pool, so one install and pairing last the run.
44let installed = false;
45let paired = false;
46​
47export const test = base.extend<{ t3: MobileT3 }>({
48 t3: async ({ device, screen }, use) => {
49 const { state, cli, cleanup } = makeState(MOBILE_T3_HOME, {
50 T3CODE_PORT: String(MOBILE.serverPort),
51 });
52 const pairingUrl = Effect.gen(function* () {
53 const origin = `http://${MOBILE.host}:${MOBILE.serverPort}`;
54 const pairing = ["auth", "pairing", "create", "--ttl", "15m", "--label", "scenario"];
55 const created = yield* cli([...pairing, "--base-url", origin]);
56 const url = /Pair URL:\s*(\S+)/.exec(`${created.stdout}\n${created.stderr}`)?.[1];
57 return url ?? (yield* new ScenarioError({ message: "The server printed no pairing URL." }));
58 });
59 await use({
60 ...state,
61 open: async () => {
62 if (!installed) {
63 await device.installApp(await runPromise(devClientPath));
64 installed = true;
65 }
66 // iOS asks before a link opens another app.
67 const openLink = async (url: string) => {
68 await device.openLink(url, { app: APP_ID });
69 await device.alert("accept").catch(() => {});
70 };
71 const metro = encodeURIComponent(`http://${MOBILE.host}:${MOBILE.metroPort}`);
72 await openLink(
73 `${SCHEME}://expo-development-client/?url=${metro}&disableOnboarding=1&disableFab=1&disableAutoLaunch=1`,
74 );
75 // The first load bundles the app; the pairing link only works once it is on screen.
76 if (paired) return;
77 await expect(screen.getByRole("button", "Add environment")).toBeVisible({
78 timeout: 240_000,
79 });
80 const query = new URLSearchParams({
81 pairingUrl: await runPromise(pairingUrl),
82 autoConnect: "1",
83 });
84 await openLink(`${SCHEME}://connections/new?${query.toString()}`);
85 paired = true;
86 },
87 });
88 await runPromise(cleanup);
89 },
90});
91​
92/** Test option for an iOS scenario: the web target skips it, and no device is leased for it. */
93export const IOS = { platforms: ["ios"] } as const;