playthrough 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { f as waitsFor } from "./flows-DDB6AdQ-.js";
1
+ import { p as waitsFor } from "./flows-CbrwgoHB.js";
2
2
  import { availableParallelism } from "node:os";
3
3
  import { defineConfig } from "@playwright/test";
4
4
  import { fileURLToPath } from "node:url";
@@ -50,7 +50,11 @@ const Place = z.strictObject({
50
50
  }),
51
51
  did: z.array(Did).describe("What the person did there, in order"),
52
52
  recording: fileName("needs its recording: the name of a file in the round").describe("The flow up to where it was taken over, then the person's own steps"),
53
- length: z.number({ error: "needs its length, in seconds" }).positive({ error: "needs its length, in seconds" })
53
+ length: z.number({ error: "needs its length, in seconds" }).positive({ error: "needs its length, in seconds" }),
54
+ steps: z.array(z.strictObject({
55
+ title: text("needs its title"),
56
+ at: seconds$1("needs its start, in seconds")
57
+ })).optional().describe("Run again as the agent wrote it, a fork of its flow: its steps, each at its start")
54
58
  }).optional().describe("In the app: on the person's own recording, which branches off a flow where they took it over. Its flow names the branch")
55
59
  }).refine((place) => [
56
60
  place.element,
@@ -76,6 +80,8 @@ const Flow = z.strictObject({
76
80
  const Change = z.strictObject({
77
81
  id: text("needs an id").describe("Its thing and its kind of change, so it keeps its id in every round while it's the same change"),
78
82
  words: text("needs its words").describe("What changed, for the agent: “Download CSV and Cancel have swapped places”"),
83
+ reworded: text("needs the words that read differently").optional().describe("For words reworded, the words that read differently now, which the viewer marks where they are: “on their own”"),
84
+ gone: z.literal(true).optional().describe("Not on the newer round: where it was, on the round before, is where it's seen, with Compare"),
79
85
  seen: z.array(Place, { error: "needs where it's seen, a list" }).min(1, { error: "needs where it's seen: each flow, at its moment there" }).describe("Each flow it's seen in, at its moment there; a flow that's gone, with no moment"),
80
86
  pictureOnly: z.literal(true).optional().describe("Seen only in the pictures, with nothing read there changed: shown to the person, who can see what it is, and not put to the agent, who can't see more of it than Playthrough did")
81
87
  });
package/src/reporter.js CHANGED
@@ -1 +1 @@
1
- export { t as ACTIONS, n as ASSETS, r as DONE_ON, a as GLIMPSES, o as HANDS, s as HOLD, c as PERSON, l as PICTURE, u as READINGS, d as RECORDING, f as WROTE, i as default, p as firstLine, m as glimpsesOf, h as readingsOf, g as readingsOn, _ as seconds } from "./reporter-Dr7dWwSf.js";
1
+ export { t as ACTIONS, n as ASSETS, r as DONE_ON, a as GLIMPSES, o as HANDS, s as HOLD, c as PERSON, l as PICTURE, u as READINGS, d as RECORDING, f as WROTE, i as default, p as firstLine, m as glimpsesOf, h as readingsOf, g as readingsOn, _ as seconds } from "./reporter-DlS7qehW.js";
package/src/test.js CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as seconds, a as GLIMPSES, c as PERSON, d as RECORDING, f as WROTE, l as PICTURE, n as ASSETS, o as HANDS, p as firstLine, r as DONE_ON, s as HOLD, t as ACTIONS, u as READINGS, v as keepAssets } from "./reporter-Dr7dWwSf.js";
2
- import { A as repo, C as changes, D as personIn, E as make, O as setupIn, n as REACHES_OUTSIDE, r as WALKED, t as ALONGSIDE, w as firstOf } from "./flows-DDB6AdQ-.js";
1
+ import { _ as seconds, a as GLIMPSES, c as PERSON, d as RECORDING, f as WROTE, l as PICTURE, n as ASSETS, o as HANDS, p as firstLine, r as DONE_ON, s as HOLD, t as ACTIONS, u as READINGS, v as keepAssets } from "./reporter-DlS7qehW.js";
2
+ import { A as setupIn, E as firstOf, M as repo, O as make, T as changes, b as sameWords, k as personIn, n as FORK_RAN, r as REACHES_OUTSIDE, t as ALONGSIDE } from "./flows-CbrwgoHB.js";
3
3
  import { n as readPage, r as unhovered } from "./read-Bg9XVTtn.js";
4
4
  import { readFileSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
@@ -92,15 +92,20 @@ function onlyWaits(expect) {
92
92
  const recorder = (rrweb) => `;((Date, setTimeout, clearTimeout, requestAnimationFrame, cancelAnimationFrame) => {
93
93
  ${readFileSync(rrweb, "utf8")}
94
94
  })(Date, setTimeout, clearTimeout, requestAnimationFrame, cancelAnimationFrame);
95
- if (window.top === window) window.rrwebRecord.record({
96
- // Nothing is kept until the flow itself starts, after whatever the app's setup did on the page.
97
- emit: (event) => window.__playthrough?.(event),
98
- // every input, not just the last: with "last", rrweb only hears a field change when it loses focus,
99
- // so words typed into a search box that kept focus never reached the recording
100
- sampling: { mousemove: 20, scroll: 16, input: "all" },
101
- maskAllInputs: false,
102
- inlineStylesheet: true,
103
- });`;
95
+ // With each page taken whole, whether its browser was light or dark, still or moving, for it to be played back as it
96
+ // was: said once rrweb has the page, as said at its very start it went before anything was listening.
97
+ const said = () => window.rrwebRecord.record.addCustomEvent("browser", { dark: matchMedia("(prefers-color-scheme: dark)").matches, still: matchMedia("(prefers-reduced-motion: reduce)").matches });
98
+ if (window.top === window) {
99
+ window.rrwebRecord.record({
100
+ // Nothing is kept until the flow itself starts, after whatever the app's setup did on the page.
101
+ emit: (event) => (window.__playthrough?.(event), event.type === 2 && setTimeout(said)),
102
+ // every input, not just the last: with "last", rrweb only hears a field change when it loses focus,
103
+ // so words typed into a search box that kept focus never reached the recording
104
+ sampling: { mousemove: 20, scroll: 16, input: "all" },
105
+ maskAllInputs: false,
106
+ inlineStylesheet: true,
107
+ });
108
+ }`;
104
109
  const FRAME = 16;
105
110
  const REST = 140;
106
111
  const KEY = 60;
@@ -133,7 +138,7 @@ const recorded = test$1.extend({
133
138
  rest: async ({ page }, use) => use(run ? await watchRest(page) : async () => {}),
134
139
  setup: async ({ page, request, baseURL, person, rest: _ }, use, testInfo) => {
135
140
  if (run?.only && !run.only.includes(testInfo.title)) testInfo.skip();
136
- if (run?.walk && testInfo.title !== run.walk.flow) testInfo.skip();
141
+ if (run?.fork && testInfo.title !== run.fork.flow) testInfo.skip();
137
142
  const app = await setupOf();
138
143
  if (person) personIn(app, person);
139
144
  if (run) await readied(page, run);
@@ -171,7 +176,7 @@ const recorded = test$1.extend({
171
176
  });
172
177
  for (const context of opened) await context.close();
173
178
  },
174
- recorder: [async ({ page, setup, rest, person, handTo: _ }, use, testInfo) => {
179
+ recorder: [async ({ page, request, baseURL, setup, rest, person, handTo }, use, testInfo) => {
175
180
  if (!run) return use();
176
181
  testInfo.setTimeout(testInfo.timeout * 3);
177
182
  const flow = {
@@ -186,7 +191,15 @@ const recorded = test$1.extend({
186
191
  inStep: 0,
187
192
  hands: [],
188
193
  assets: [],
189
- doneOn: {}
194
+ doneOn: {},
195
+ given: {
196
+ page,
197
+ context: page.context(),
198
+ request,
199
+ baseURL,
200
+ setup,
201
+ handTo
202
+ }
190
203
  };
191
204
  await page.exposeFunction("__playthrough", (event) => void (flow.page === page && flow.events.push(event)));
192
205
  flow.assets.push(keepAssets(page, run.folder));
@@ -209,15 +222,20 @@ const recorded = test$1.extend({
209
222
  }
210
223
  const last = flow.page;
211
224
  const assets = async () => Object.assign({}, ...await Promise.all(flow.assets.map((kept) => kept())));
212
- if (run.walk) {
213
- if (!flow.walked) return;
225
+ if (run.fork) {
226
+ if (!flow.forked) return;
214
227
  await last.waitForTimeout(HOLD);
215
228
  await last.evaluate(() => window.rrwebRecord.record.addCustomEvent("end", {}));
216
229
  await last.waitForTimeout(100);
217
- return writeFileSync(join(run.folder, WALKED), JSON.stringify({
218
- ...flow.walked,
230
+ writeFileSync(join(run.folder, "end.png"), await last.screenshot({
231
+ animations: "disabled",
232
+ caret: "hide"
233
+ }));
234
+ return writeFileSync(join(run.folder, FORK_RAN), JSON.stringify({
235
+ ...flow.forked,
219
236
  events,
220
237
  readings,
238
+ glimpses,
221
239
  assets: await assets()
222
240
  }));
223
241
  }
@@ -382,12 +400,17 @@ function answers(dialog) {
382
400
  }
383
401
  }
384
402
  let collecting;
403
+ let collectingForks = [];
385
404
  let around = [];
386
405
  let loading = Promise.resolve();
387
406
  function flowsIn(file, written) {
388
407
  const flows = /* @__PURE__ */ new Map();
389
408
  const loaded = loading.then(async () => {
390
- [collecting, around] = [flows, [{ use: {} }]];
409
+ [collecting, around, collectingForks] = [
410
+ flows,
411
+ [{ use: {} }],
412
+ []
413
+ ];
391
414
  try {
392
415
  await import(`${pathToFileURL(file).href}?${written}`);
393
416
  } finally {
@@ -401,7 +424,7 @@ function flowsIn(file, written) {
401
424
  const taking = new AsyncLocalStorage();
402
425
  const holds = (flow) => !flow.wrote && flow.page.url() === flow.address;
403
426
  var Stopped = class extends Error {};
404
- async function runAlongside(page, { request, baseURL, started, setup: appSetup, borrowed, arrived, person }, body, orders) {
427
+ async function runAlongside(page, { request, baseURL, started, setup: appSetup, borrowed, arrived, person }, body, orders, forks = []) {
405
428
  const readied = async (page) => {
406
429
  const rest = await watchRest(page);
407
430
  await page.clock.setSystemTime(onReviewDay(started));
@@ -415,6 +438,7 @@ async function runAlongside(page, { request, baseURL, started, setup: appSetup,
415
438
  };
416
439
  const flow = {
417
440
  page,
441
+ forks,
418
442
  depth: 0,
419
443
  rest: async () => {},
420
444
  actions: 0,
@@ -463,14 +487,15 @@ async function runAlongside(page, { request, baseURL, started, setup: appSetup,
463
487
  person: who,
464
488
  setup: made.setup
465
489
  });
466
- await body({
490
+ flow.given = {
467
491
  page,
468
492
  context: page.context(),
469
493
  request,
470
494
  baseURL,
471
495
  setup: made.setup,
472
496
  handTo
473
- });
497
+ };
498
+ await body(flow.given);
474
499
  await reached(flow);
475
500
  return {
476
501
  handed: flow.handed,
@@ -500,7 +525,7 @@ async function reached(flow, step, actions) {
500
525
  if (stopped) throw new Stopped();
501
526
  const where = against(flow.passed, step, actions, to);
502
527
  if (where === "on") return;
503
- if (where === "here" && to.rests) await handOver(flow, to.did);
528
+ if (where === "here" && to.rests) await handOver(flow, to);
504
529
  if (where === "past" || to.rests) {
505
530
  if (step) throw new Stopped();
506
531
  return;
@@ -542,9 +567,13 @@ async function kept(page, rest) {
542
567
  clock: kept.clock - Date.now()
543
568
  };
544
569
  }
545
- async function handOver(flow, did) {
546
- const { page, rest, from, again } = flow;
547
- if (did?.length) {
570
+ async function handOver(flow, { notes, did }) {
571
+ const fork = notes && flow.forks.find((fork) => fork.notes.some((note) => notes.includes(note)));
572
+ if (fork) {
573
+ flow.forking = true;
574
+ await fork.body(flow.given);
575
+ } else if (did?.length) {
576
+ const { page, rest } = flow;
548
577
  await taking.exit(async () => {
549
578
  for (const one of did) {
550
579
  if (!await doAgain(page, one).then(() => true, () => false)) break;
@@ -554,6 +583,7 @@ async function handOver(flow, did) {
554
583
  flow.handed = await kept(page, rest);
555
584
  return;
556
585
  }
586
+ const { page, rest, from, again } = flow;
557
587
  flow.handed = from && again.length && holds(flow) ? {
558
588
  ...from,
559
589
  again
@@ -571,6 +601,7 @@ const pathTo = (target) => target.evaluate((element) => {
571
601
  const playwrightStep = test$1.step;
572
602
  const step = Object.assign(async (title, body, options) => {
573
603
  const taken = taking.getStore();
604
+ if (taken?.forking) return body(void 0);
574
605
  if (taken) {
575
606
  const own = !taken.depth;
576
607
  if (own) Object.assign(taken, {
@@ -597,6 +628,10 @@ const step = Object.assign(async (title, body, options) => {
597
628
  step: title,
598
629
  inStep: 0
599
630
  });
631
+ flow.forked?.steps.push({
632
+ title,
633
+ at: seconds(Date.now() - (flow.events[0]?.timestamp ?? Date.now()))
634
+ });
600
635
  flow.depth++;
601
636
  let done;
602
637
  try {
@@ -620,7 +655,7 @@ const step = Object.assign(async (title, body, options) => {
620
655
  caret: "hide"
621
656
  }));
622
657
  });
623
- if (run?.walk?.step === title && run.walk.actions === void 0 && !flow.walked) await walkOn(flow, run.walk);
658
+ if (run?.fork?.step === title && run.fork.actions === void 0 && !flow.forked) await forkOn(flow, run.fork);
624
659
  return done;
625
660
  }, options);
626
661
  }, playwrightStep);
@@ -637,8 +672,10 @@ const outsideIn = (details) => {
637
672
  };
638
673
  const keeping = Object.assign((title, ...rest) => {
639
674
  const scopes = [...around];
675
+ const forks = collectingForks;
640
676
  collecting?.set(title, {
641
677
  body: rest.at(-1),
678
+ forks,
642
679
  outside: (rest.length > 1 ? outsideIn(rest[0]) : void 0) ?? scopes.findLast((scope) => scope.outside)?.outside,
643
680
  get use() {
644
681
  return Object.assign({}, ...scopes.map((scope) => scope.use));
@@ -660,7 +697,17 @@ const keeping = Object.assign((title, ...rest) => {
660
697
  },
661
698
  extend: () => keeping
662
699
  });
663
- const test = globalThis[ALONGSIDE] ? keeping : withSteps(recorded);
700
+ const forks = [];
701
+ const fork = (name, { notes }, body) => void forks.push({
702
+ name,
703
+ notes: notes.map(String),
704
+ body
705
+ });
706
+ const test = globalThis[ALONGSIDE] ? Object.assign(keeping, { fork: (name, { notes }, body) => void collectingForks.push({
707
+ name,
708
+ notes: notes.map(String),
709
+ body
710
+ }) }) : Object.assign(withSteps(recorded), { fork });
664
711
  const actions = [
665
712
  "click",
666
713
  "dblclick",
@@ -703,6 +750,20 @@ function pace(page) {
703
750
  return result;
704
751
  };
705
752
  }
753
+ const mouse = Object.getPrototypeOf(page.mouse);
754
+ const wheel = mouse.wheel;
755
+ mouse.wheel = async function(deltaX, deltaY) {
756
+ if (!pointer) return wheel.call(this, deltaX, deltaY);
757
+ const moves = Math.max(10, Math.min(36, Math.round(Math.hypot(deltaX, deltaY) / 25)));
758
+ let done = 0;
759
+ for (let i = 1; i <= moves; i++) {
760
+ await wheel.call(this, deltaX * (ease(i / moves) - done), deltaY * (ease(i / moves) - done));
761
+ done = ease(i / moves);
762
+ await pointer.page.waitForTimeout(FRAME);
763
+ }
764
+ await pointer.page.waitForTimeout(650);
765
+ await glimpse();
766
+ };
706
767
  const keyboard = Object.getPrototypeOf(page.keyboard);
707
768
  const focused = async (page) => {
708
769
  const on = page.locator(":focus").first();
@@ -721,7 +782,7 @@ function pace(page) {
721
782
  }
722
783
  async function acted(does, target, act, given) {
723
784
  const taken = taking.getStore();
724
- const keeping = taken?.depth ? taken : void 0;
785
+ const keeping = taken?.depth || taken?.forking ? taken : void 0;
725
786
  let on, address;
726
787
  if (keeping) {
727
788
  if (!holds(keeping)) Object.assign(keeping, {
@@ -739,10 +800,10 @@ async function acted(does, target, act, given) {
739
800
  await dialog;
740
801
  });
741
802
  underway?.actions.push(Date.now());
742
- const walk = run?.walk;
743
- if (walk && underway?.depth && !underway.walked && walk.step === underway.step && walk.actions === ++underway.inStep) {
803
+ const fork = run?.fork;
804
+ if (fork && underway?.depth && !underway.forked && fork.step === underway.step && fork.actions === ++underway.inStep) {
744
805
  await underway.page.waitForTimeout(650);
745
- await walkOn(underway, walk);
806
+ await forkOn(underway, fork);
746
807
  }
747
808
  if (keeping) {
748
809
  await keeping.page.waitForTimeout(SAVING);
@@ -757,7 +818,7 @@ async function acted(does, target, act, given) {
757
818
  again: []
758
819
  });
759
820
  }
760
- if (keeping) await reached(keeping, keeping.step, ++keeping.actions);
821
+ if (keeping && !keeping.forking) await reached(keeping, keeping.step, ++keeping.actions);
761
822
  return result;
762
823
  }
763
824
  async function follow(flow, target) {
@@ -775,56 +836,32 @@ const doneOn = (target) => target.ariaSnapshot({ timeout: 1e3 }).then((snapshot)
775
836
  } : void 0;
776
837
  }, () => void 0);
777
838
  function alike(one, other) {
778
- if (one.role !== other.role) return false;
779
- const words = (name) => name.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
780
- const [few, many] = [words(one.name), words(other.name)].sort((a, b) => a.length - b.length);
781
- return !few.length || few.filter((word) => many.includes(word)).length * 2 >= few.length;
839
+ return one.role === other.role && sameWords(one.name, other.name);
782
840
  }
783
- async function walkOn(flow, walk) {
841
+ async function forkOn(flow, run) {
784
842
  await flow.rest();
785
843
  const since = () => seconds(Date.now() - (flow.events[0]?.timestamp ?? Date.now()));
786
- const walked = flow.walked = {
844
+ const written = forks.find((fork) => fork.notes.some((note) => run.notes.includes(note)));
845
+ const ran = flow.forked = {
787
846
  from: since(),
788
- done: [],
789
- found: []
847
+ steps: [],
848
+ ...written && { fork: written.name }
790
849
  };
791
- const look = async () => {
792
- for (const [i, { after, css }] of walk.look.entries()) {
793
- if (after !== walked.done.length) continue;
794
- walked.found[i] = css ? await flow.page.evaluate((css) => {
795
- const on = document.querySelector(css);
796
- const node = on ? window.rrwebRecord.record.mirror.getId(on) : -1;
797
- return node > 0 ? node : null;
798
- }, css).catch(() => null) : null;
799
- writeFileSync(join(run.folder, `look-${i}.png`), await flow.page.screenshot({
800
- animations: "disabled",
801
- caret: "hide"
802
- }));
803
- }
804
- };
805
- await look();
806
- for (const did of walk.did) {
850
+ if (written) {
851
+ const depth = flow.depth;
852
+ flow.depth = 0;
807
853
  try {
808
- await doAgain(flow.page, did);
809
- } catch {
810
- walked.stopped = did.what || did.on;
811
- break;
854
+ await written.body(flow.given);
855
+ } catch (error) {
856
+ ran.failed = {
857
+ ...ran.steps.length && { at: ran.steps.at(-1).title },
858
+ why: firstLine(String(error?.message ?? error))
859
+ };
860
+ } finally {
861
+ flow.depth = depth;
812
862
  }
813
- await flow.rest();
814
- walked.done.push(since());
815
- await unhovered(flow.page, async () => {
816
- const { screen, things, items } = await readPage(flow.page);
817
- flow.readings.push({
818
- step: "In the app",
819
- at: since(),
820
- screen,
821
- things,
822
- items
823
- });
824
- });
825
- await look();
826
863
  }
827
- test$1.info().skip(true, "Walked as far as the person's steps go");
864
+ test$1.info().skip(true, "Ran as far as the note's fork");
828
865
  }
829
866
  async function doAgain(page, { does, on, with: given }) {
830
867
  const target = page.locator(on).first();
@@ -843,7 +880,7 @@ async function doAgain(page, { does, on, with: given }) {
843
880
  }
844
881
  async function glimpse() {
845
882
  const flow = underway;
846
- if (!pointer || !flow || run?.walk) return;
883
+ if (!pointer || !flow || run?.fork && !flow.forked) return;
847
884
  const at = seconds(Date.now() - (flow.events[0]?.timestamp ?? Date.now()));
848
885
  const read = await readPage(flow.page).catch(() => void 0);
849
886
  if (read) flow.glimpses.push({
@@ -1,4 +1,6 @@
1
1
  import { type App } from "./repo.js";
2
- export declare function keepRunning(app: App, root: string): Promise<number | undefined>;
2
+ export declare function keepRunning(app: App, root: string, { ready }?: {
3
+ ready?: boolean | undefined;
4
+ }): Promise<number | undefined>;
3
5
  export declare function stopApp(url: string): Promise<void>;
4
6
  export declare const serves: (url: string) => Promise<boolean>;
@@ -7,6 +7,10 @@ export type Found = {
7
7
  alike?: Element[];
8
8
  box?: Box;
9
9
  pieces: string[];
10
+ reworded?: string;
11
+ gone?: true;
12
+ offScreen?: true;
13
+ top?: number;
10
14
  };
11
15
  export declare function changesAt(then: Reading, now: Reading, unsteady?: Set<string>, shown?: Shown): Found[];
12
16
  type Shown = {
@@ -0,0 +1,6 @@
1
+ export type Command = {
2
+ by: number;
3
+ boot?: string | number;
4
+ };
5
+ export declare const thisCommand: () => Command;
6
+ export declare const stillAt: (command: Command) => boolean;
@@ -1,3 +1,4 @@
1
+ import type { Glimpse, Reading } from "../../page/src/index.js";
1
2
  import { type Did, type Flow } from "../../review/src/index.js";
2
3
  import { type Failed, type Written } from "./reporter.js";
3
4
  import { type App, type Repo } from "./repo.js";
@@ -14,7 +15,7 @@ export type Run = {
14
15
  before: boolean;
15
16
  taken: string[];
16
17
  started: string;
17
- walk?: Walk;
18
+ fork?: ForkRun;
18
19
  doneInRound?: Record<string, DoneOn>;
19
20
  };
20
21
  export type On = {
@@ -22,29 +23,32 @@ export type On = {
22
23
  name: string;
23
24
  };
24
25
  export type DoneOn = Record<string, (On | null)[]>;
25
- export type Walk = {
26
+ export type ForkRun = {
26
27
  flow: string;
27
28
  step: string;
28
29
  actions?: number;
29
- did: Did[];
30
- look: {
31
- after: number;
32
- css?: string;
33
- }[];
30
+ notes: string[];
34
31
  };
35
- export type Walked = {
32
+ export type ForkRan = {
36
33
  from: number;
37
- done: number[];
38
- found: (number | null)[];
39
- stopped?: string;
34
+ fork?: string;
35
+ steps: {
36
+ title: string;
37
+ at: number;
38
+ }[];
39
+ failed?: {
40
+ at?: string;
41
+ why: string;
42
+ };
40
43
  };
41
- export declare const WALKED = "walked.json";
44
+ export declare const FORK_RAN = "fork.json";
42
45
  export type To = {
43
46
  flow: string;
44
47
  step?: string;
45
48
  actions?: number;
46
49
  rests?: boolean;
47
50
  did?: Did[];
51
+ notes?: string[];
48
52
  };
49
53
  export type Handed = {
50
54
  url: string;
@@ -110,17 +114,18 @@ export type Ran = {
110
114
  failed: Failed[];
111
115
  written: Record<string, Written>;
112
116
  };
113
- export type WalkedRun = Walked & {
117
+ export type ForkRecorded = ForkRan & {
114
118
  events: {
115
119
  timestamp: number;
116
120
  }[];
117
- readings: unknown[];
121
+ readings: Reading[];
122
+ glimpses: Glimpse[];
118
123
  assets: Record<string, string>;
119
124
  files: {
120
125
  name: string;
121
126
  bytes: Buffer;
122
127
  }[];
123
- pictures: (Buffer | undefined)[];
128
+ picture?: Buffer;
124
129
  };
125
- export declare function walkFork(repo: Repo, id: string, walk: Walk): Promise<WalkedRun | undefined>;
130
+ export declare function runFork(repo: Repo, id: string, fork: ForkRun): Promise<ForkRecorded | undefined>;
126
131
  export declare const failedAt: ({ flow, at, why }: Failed) => string;
@@ -14,6 +14,7 @@ export declare function repo(root?: string): {
14
14
  round: (review: string, round: number) => string;
15
15
  ranOnBefore: (review: string) => string;
16
16
  setAside: string;
17
+ turn: string;
17
18
  appFile: string;
18
19
  setup: string;
19
20
  borrowed: string;
@@ -8,6 +8,13 @@ export declare const tablet: Screen;
8
8
  export declare const expect: import("@playwright/test").Expect<{}>;
9
9
  export declare const onReviewDay: (started: string, now?: number) => Date;
10
10
  export type HandTo = (person: string, screen?: Screen) => Promise<Page>;
11
+ declare const recorded: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
12
+ person: string | undefined;
13
+ rest: () => Promise<void>;
14
+ setup: Record<string, any>;
15
+ handTo: HandTo;
16
+ recorder: void;
17
+ }, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
11
18
  export declare function pageFor(browser: Browser, baseURL: string, screen: Screen, use?: BrowserContextOptions): Promise<{
12
19
  context: BrowserContext;
13
20
  page: Page;
@@ -31,6 +38,7 @@ export type Kept = {
31
38
  use: BrowserContextOptions & {
32
39
  person?: string;
33
40
  };
41
+ forks: Forked[];
34
42
  };
35
43
  export declare function flowsIn(file: string, written: number): Promise<Map<string, Kept>>;
36
44
  export type Arrived = (step: string | undefined, actions: number | undefined, worked: number) => void;
@@ -42,15 +50,20 @@ export declare function runAlongside(page: Page, { request, baseURL, started, se
42
50
  borrowed: string;
43
51
  arrived?: Arrived;
44
52
  person?: string;
45
- }, body: Body, orders: Orders): Promise<{
53
+ }, body: Body, orders: Orders, forks?: Forked[]): Promise<{
46
54
  handed?: Handed;
47
55
  stopped?: Pick<Failed, "at" | "why">;
48
56
  made?: Made;
49
57
  }>;
50
- export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
51
- person: string | undefined;
52
- rest: () => Promise<void>;
53
- setup: Record<string, any>;
54
- handTo: HandTo;
55
- recorder: void;
56
- }, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
58
+ type Forked = {
59
+ name: string;
60
+ notes: string[];
61
+ body: Body;
62
+ };
63
+ declare const fork: (name: string, { notes }: {
64
+ notes: string[];
65
+ }, body: Body) => undefined;
66
+ export declare const test: typeof recorded & {
67
+ fork: typeof fork;
68
+ };
69
+ export {};
@@ -13,3 +13,8 @@ export declare const HOLDING: RegExp;
13
13
  export declare const nearTo: (read: Glimpse[], at: number, point: Point) => Thing | undefined;
14
14
  export declare function shownAt(readings: Reading[], element: Element, at: number): Shown | undefined;
15
15
  export declare const elementOf: (thing: Thing, name: string) => Element;
16
+ export declare function sameWords(one: string, other: string): boolean;
17
+ export declare function firstShown(element: Element, seen: Glimpse[]): {
18
+ at: number;
19
+ element: Element;
20
+ } | undefined;
@@ -2,3 +2,4 @@ export * from "./reading.js";
2
2
  export * from "./names.js";
3
3
  export * from "./differences.js";
4
4
  export * from "./found.js";
5
+ export * from "./scheme.js";
@@ -13,4 +13,5 @@ export declare const sentence: (words: string) => string;
13
13
  export declare const wayOf: (across: number, down: number) => string;
14
14
  export declare function together(all: Thing[], name?: (thing: Thing) => string): string;
15
15
  export declare function rewordedWords(a: Thing, b: Thing): string;
16
+ export declare function rewordedPart(a: string, b: string): string | undefined;
16
17
  export declare function lookWords(a: Thing, b: Thing): string;
@@ -0,0 +1,6 @@
1
+ export type Answered = {
2
+ dark: boolean;
3
+ still: boolean;
4
+ };
5
+ export declare function asRecorded(text: string, recorded: Answered): string;
6
+ export declare function answerAs(view: Window & typeof globalThis, recorded: Answered): void;