playthrough 0.5.1 → 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.
- package/INSTRUCTIONS.md +27 -5
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/{flows-vi0amwSo.js → flows-CbrwgoHB.js} +43 -12
- package/src/playthrough.js +145 -84
- package/src/playwright.config.js +1 -1
- package/src/{reporter-S9fOk0za.js → reporter-DlS7qehW.js} +5 -1
- package/src/reporter.js +1 -1
- package/src/test.js +85 -67
- package/types/cli/src/flows.d.ts +21 -16
- package/types/cli/src/test.d.ts +21 -8
- package/types/page/src/found.d.ts +5 -0
- package/types/review/src/model.d.ts +40 -0
- package/types/review/src/rules.d.ts +4 -0
- package/viewer/assets/index-BbNKJRwK.css +2 -0
- package/viewer/assets/index-DwbSPQMk.js +194 -0
- package/viewer/index.html +2 -2
- package/viewer/assets/index-C4zcB6st.css +0 -2
- package/viewer/assets/index-D_6gdncQ.js +0 -194
package/src/playthrough.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON
|
|
2
|
-
import { A as quote, C as inOrderJoined, D as roundInProgress, E as problemsFinishing, O as toAccountFor, S as conversation, T as notesToAnswer, g as readingsOn, j as time, k as list, p as firstLine, w as lastSeen, x as allNotes, y as Refused } from "./reporter-
|
|
3
|
-
import { A as
|
|
2
|
+
import { A as quote, C as inOrderJoined, D as roundInProgress, E as problemsFinishing, O as toAccountFor, S as conversation, T as notesToAnswer, g as readingsOn, j as time, k as list, p as firstLine, w as lastSeen, x as allNotes, y as Refused } from "./reporter-DlS7qehW.js";
|
|
3
|
+
import { A as setupIn, C as serves, D as giveBackLeft, F as pausedAt, M as repo, N as stillAt, P as thisCommand, S as keepRunning, _ as compareWithEarlier, a as failedAt, c as isFlow, d as runFlows, f as runFork, g as writtenNow, h as trySetup, i as called, j as copies, l as makeFlowsFolder, m as whereItStopped, o as flowsOf, s as howToWriteFlows, t as ALONGSIDE, u as recorder, v as firstShown, w as stopApp, x as shownAt, y as placeIn } from "./flows-CbrwgoHB.js";
|
|
4
4
|
import { t as read } from "./read-Bg9XVTtn.js";
|
|
5
5
|
import { copyFileSync, cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
@@ -13,9 +13,9 @@ import { chromium, request } from "@playwright/test";
|
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { createServer as createServer$1, request as request$1 } from "node:http";
|
|
15
15
|
// src/node.ts
|
|
16
|
-
const [major] = process.versions.node.split(".").map(Number);
|
|
17
|
-
if (major <
|
|
18
|
-
console.error(`Playthrough needs Node
|
|
16
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
17
|
+
if (major < 22 || major === 22 && minor < 18) {
|
|
18
|
+
console.error(`Playthrough needs Node 22.18 or later, and this is Node ${process.versions.node}. Install Node 24, then run this again.`);
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
21
21
|
// ../page/src/scheme.ts
|
|
@@ -328,6 +328,7 @@ function putWorkBack(repo, aside, force) {
|
|
|
328
328
|
"--",
|
|
329
329
|
"playthrough"
|
|
330
330
|
]);
|
|
331
|
+
if (force) keepWhatChanged(repo);
|
|
331
332
|
git(repo, [
|
|
332
333
|
"checkout",
|
|
333
334
|
"--quiet",
|
|
@@ -348,6 +349,33 @@ function putWorkBack(repo, aside, force) {
|
|
|
348
349
|
return said(error);
|
|
349
350
|
}
|
|
350
351
|
}
|
|
352
|
+
function keepWhatChanged(repo) {
|
|
353
|
+
const changed = git(repo, [
|
|
354
|
+
"status",
|
|
355
|
+
"--porcelain",
|
|
356
|
+
"--untracked-files=all"
|
|
357
|
+
]).split("\n").filter(Boolean).map((line) => line.replace(/^\s*\S+\s+/, ""));
|
|
358
|
+
if (!changed.length) return;
|
|
359
|
+
const kept = `Playthrough: saved while flows recorded on Before, ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
360
|
+
git(repo, [
|
|
361
|
+
"stash",
|
|
362
|
+
"push",
|
|
363
|
+
"--include-untracked",
|
|
364
|
+
"--quiet",
|
|
365
|
+
"--message",
|
|
366
|
+
kept
|
|
367
|
+
], {
|
|
368
|
+
...process.env,
|
|
369
|
+
...PLAYTHROUGH
|
|
370
|
+
});
|
|
371
|
+
const ref = git(repo, [
|
|
372
|
+
"stash",
|
|
373
|
+
"list",
|
|
374
|
+
"--format=%gd %gs"
|
|
375
|
+
]).split("\n").find((stash) => stash.endsWith(kept))?.split(" ")[0];
|
|
376
|
+
const files = changed.length > 3 ? `${changed.slice(0, 3).join(", ")} and ${changed.length - 3} more` : changed.join(", ");
|
|
377
|
+
console.log(`\nWhile flows recorded on Before, ${files} changed, in the app as it was. That's kept, not lost: git stash pop ${ref} puts it over your work, if it's something you saved.`);
|
|
378
|
+
}
|
|
351
379
|
function stillAside(repo, aside, problem) {
|
|
352
380
|
const stash = stashOf(repo, aside);
|
|
353
381
|
return new Refused([
|
|
@@ -427,7 +455,7 @@ async function runTo(repo, id, to) {
|
|
|
427
455
|
order(one, resting);
|
|
428
456
|
const { handed, stopped, made } = await one.ran;
|
|
429
457
|
if (handed) {
|
|
430
|
-
if (one.passed.length > passed && !to.did?.length) timesOf(repo, id, one).handing.set(momentOf(to.step, to.actions), Date.now() - one.last.when);
|
|
458
|
+
if (one.passed.length > passed && !to.did?.length && !to.notes) timesOf(repo, id, one).handing.set(momentOf(to.step, to.actions), Date.now() - one.last.when);
|
|
431
459
|
return {
|
|
432
460
|
handed,
|
|
433
461
|
made
|
|
@@ -547,7 +575,7 @@ async function start$2(repo, id, to, files, written) {
|
|
|
547
575
|
borrowed: repo.borrowed,
|
|
548
576
|
arrived,
|
|
549
577
|
person: kept.use.person
|
|
550
|
-
}, kept.body, orders).catch((error) => ({ stopped: { why: firstLine(String(error?.message ?? error)) } }));
|
|
578
|
+
}, kept.body, orders, kept.forks).catch((error) => ({ stopped: { why: firstLine(String(error?.message ?? error)) } }));
|
|
551
579
|
one.ran.finally(() => (one.over = true, close()));
|
|
552
580
|
one.ran.then(({ handed, made }) => handed ? void 0 : made?.giveBack());
|
|
553
581
|
context.on("close", one.stop);
|
|
@@ -589,6 +617,7 @@ function loadedOf(repo, id, files, written) {
|
|
|
589
617
|
const kept = loaded.get(key(repo, id));
|
|
590
618
|
if (kept?.written === written) return kept.flows;
|
|
591
619
|
const load = (file, by) => by(file, written).catch((error) => {
|
|
620
|
+
if (error instanceof Refused) throw error;
|
|
592
621
|
throw new Refused([`${repo.show(file)} couldn't be loaded, because Node says “${firstLine(String(error?.message ?? error))}”, so no flow of ${id} can be taken over. Fix it, then take the flow over again.`]);
|
|
593
622
|
});
|
|
594
623
|
const flows = alongside().then(({ flowsIn }) => Promise.all([Promise.all(files.map((file) => load(file, flowsIn))), load(repo.setup, setupIn)])).then(([each, setup]) => {
|
|
@@ -634,7 +663,7 @@ async function takeOver$1(repo, id, flowName, stepName) {
|
|
|
634
663
|
Ready in ${seconds}s. Carry on from there in your browser: ${here}${away.length ? `\nOr on your phone, on the same network: ${list(away)}` : ""}
|
|
635
664
|
Each address hands over the same copy, and the app is passed on through Playthrough while this runs. Ctrl+C ends it.`);
|
|
636
665
|
}
|
|
637
|
-
async function getReady(repo, id, flowName, { step: stepName, actions, did, ahead, network } = {}) {
|
|
666
|
+
async function getReady(repo, id, flowName, { step: stepName, actions, did, notes, ahead, network } = {}) {
|
|
638
667
|
const round = await recorded(repo, id);
|
|
639
668
|
const flow = round.flows.find((flow) => flow.name === flowName);
|
|
640
669
|
if (!flow) throw new Refused([`${id} has no flow called ${quote(flowName)}. Its flows: ${list(round.flows.map((flow) => quote(flow.name)))}.`]);
|
|
@@ -651,7 +680,8 @@ async function getReady(repo, id, flowName, { step: stepName, actions, did, ahea
|
|
|
651
680
|
step: stepName,
|
|
652
681
|
...actions && { actions }
|
|
653
682
|
},
|
|
654
|
-
...did?.length && { did }
|
|
683
|
+
...did?.length && { did },
|
|
684
|
+
...notes?.length && { notes }
|
|
655
685
|
});
|
|
656
686
|
if (!passings.has(app.url)) {
|
|
657
687
|
const moments = /* @__PURE__ */ new Map();
|
|
@@ -867,7 +897,8 @@ const SAYS = `<script>(() => {
|
|
|
867
897
|
for (let at = target; at && at !== (pressed ?? document.body); at = at.parentElement) if (at.nodeType === 1 && words(at)) return at;
|
|
868
898
|
return pressed ?? target;
|
|
869
899
|
};
|
|
870
|
-
|
|
900
|
+
// The words you saw on it, as you'd name it; its label only when it shows none, as an icon's.
|
|
901
|
+
const words = (element) => (element.innerText?.trim() || element.getAttribute?.("aria-label") || element.value || element.placeholder || "").trim().replace(/\\s+/g, " ").slice(0, 80);
|
|
871
902
|
const roles = { A: "link", BUTTON: "button", SELECT: "combobox", TEXTAREA: "textbox", H1: "heading", H2: "heading", H3: "heading", H4: "heading", LI: "listitem", TD: "cell", TH: "columnheader", LABEL: "text", P: "text" };
|
|
872
903
|
const role = (element) => element.getAttribute("role") || (element.tagName === "INPUT" ? (["checkbox", "radio"].includes(element.type) ? element.type : "textbox") : roles[element.tagName]) || "text";
|
|
873
904
|
const path = (element) => {
|
|
@@ -938,11 +969,14 @@ const SAYS = `<script>(() => {
|
|
|
938
969
|
e.stopImmediatePropagation();
|
|
939
970
|
if (e.type === "pointerdown") {
|
|
940
971
|
const on = thing(e.target);
|
|
941
|
-
const name = words(on);
|
|
942
972
|
readNow();
|
|
943
973
|
send();
|
|
944
974
|
const node = window.rrwebRecord?.record?.mirror?.getId(on);
|
|
945
|
-
|
|
975
|
+
// Named as the page is read for every flow, so a later round's flow finds it by the same name: a reminder's
|
|
976
|
+
// message box is "Message" there, where its own words named it here.
|
|
977
|
+
const known = node > 0 && recording ? read().things.find((thing) => thing.node === node) : undefined;
|
|
978
|
+
const [kind, name] = known ? [known.role, known.name] : [role(on), words(on)];
|
|
979
|
+
parent.postMessage({ playthrough: "noted", ...(name ? { element: { role: kind, name, css: path(on), ...(node > 0 && { node }) } } : { point: { x: e.clientX / innerWidth, y: e.clientY / innerHeight } }), did, x: e.clientX, y: e.clientY }, "*");
|
|
946
980
|
noting = false;
|
|
947
981
|
swallowing = true;
|
|
948
982
|
ringed();
|
|
@@ -1644,11 +1678,12 @@ async function live(repo, id, request, response) {
|
|
|
1644
1678
|
response.end();
|
|
1645
1679
|
}
|
|
1646
1680
|
async function takeOver(repo, id, request, response) {
|
|
1647
|
-
const { flow, step, actions, did, ahead } = await body(request);
|
|
1681
|
+
const { flow, step, actions, did, notes, ahead } = await body(request);
|
|
1648
1682
|
const { port, moment } = await getReady(repo, id, flow, {
|
|
1649
1683
|
step,
|
|
1650
1684
|
actions,
|
|
1651
1685
|
did,
|
|
1686
|
+
notes,
|
|
1652
1687
|
ahead
|
|
1653
1688
|
});
|
|
1654
1689
|
json(response, 200, {
|
|
@@ -1809,9 +1844,9 @@ const known = [
|
|
|
1809
1844
|
function agent(written, shell = process.env) {
|
|
1810
1845
|
return known.find((agent) => agent.says.some((name) => shell[name]))?.name ?? written ?? "Agent";
|
|
1811
1846
|
}
|
|
1812
|
-
// src/
|
|
1813
|
-
const forkOf = (recording) => recording.match(/^(?:branches\/(.+)-\d+|
|
|
1814
|
-
async function
|
|
1847
|
+
// src/forks.ts
|
|
1848
|
+
const forkOf = (recording) => recording.match(/^(?:branches\/(.+)-\d+|forks\/(.+))\.json$/)?.slice(1).find(Boolean) ?? recording.replace(/\W+/g, "-");
|
|
1849
|
+
async function runForks(repo, review, round, flows) {
|
|
1815
1850
|
const everyNote = allNotes(review);
|
|
1816
1851
|
const open = notesToAnswer(review, round).flatMap((id) => {
|
|
1817
1852
|
const kept = everyNote.get(id);
|
|
@@ -1821,103 +1856,123 @@ async function walkForks(repo, review, round, flows) {
|
|
|
1821
1856
|
}] : [];
|
|
1822
1857
|
});
|
|
1823
1858
|
const forks = Map.groupBy(open, ({ note }) => forkOf(note.on.app.recording));
|
|
1824
|
-
const
|
|
1859
|
+
const ran = [];
|
|
1825
1860
|
for (const [fork, notes] of forks) {
|
|
1826
1861
|
const from = notes[0].note.on.app.from;
|
|
1827
1862
|
const then = review.rounds.find((kept) => kept.number === notes[0].round)?.flows.find((flow) => flow.name === from.flow);
|
|
1828
1863
|
const now = flows.find((flow) => flow.name === from.flow);
|
|
1829
1864
|
const paused = then && pausedAt(then, from.at);
|
|
1830
|
-
const
|
|
1865
|
+
const unrun = (failed) => notes.map(({ id }) => ({
|
|
1831
1866
|
note: id,
|
|
1832
1867
|
flow: from.flow,
|
|
1833
|
-
|
|
1834
|
-
|
|
1868
|
+
found: false,
|
|
1869
|
+
failed: { why: failed }
|
|
1835
1870
|
}));
|
|
1836
1871
|
if (!now || !paused) {
|
|
1837
|
-
|
|
1872
|
+
ran.push(...unrun(`${quote(from.flow)} isn't in round ${round}`));
|
|
1838
1873
|
continue;
|
|
1839
1874
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
const run = await walkFork(repo, review.id, {
|
|
1875
|
+
console.log(`\nRunning ${quote(from.flow)} to where ${list([...new Set(notes.map(({ note }) => quote(note.id)))])} took it over, then its fork…`);
|
|
1876
|
+
const run = await runFork(repo, review.id, {
|
|
1843
1877
|
flow: from.flow,
|
|
1844
1878
|
step: paused.step,
|
|
1845
1879
|
actions: paused.actions,
|
|
1846
|
-
|
|
1847
|
-
look: notes.map(({ note }) => ({
|
|
1848
|
-
after: note.on.app.did.length,
|
|
1849
|
-
css: note.on.element?.css
|
|
1850
|
-
}))
|
|
1880
|
+
notes: notes.map(({ id }) => id)
|
|
1851
1881
|
});
|
|
1852
1882
|
if (!run) {
|
|
1853
|
-
|
|
1883
|
+
ran.push(...unrun(`${quote(from.flow)} doesn't get to where they took it over, the step ${quote(paused.step)}`));
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
const step = paused.step;
|
|
1887
|
+
if (!run.fork || run.failed) {
|
|
1888
|
+
ran.push(...notes.map(({ id }) => ({
|
|
1889
|
+
note: id,
|
|
1890
|
+
flow: from.flow,
|
|
1891
|
+
step,
|
|
1892
|
+
found: false,
|
|
1893
|
+
...run.fork && { fork: run.fork },
|
|
1894
|
+
...run.failed && { failed: run.failed }
|
|
1895
|
+
})));
|
|
1854
1896
|
continue;
|
|
1855
1897
|
}
|
|
1856
|
-
const recording = `
|
|
1898
|
+
const recording = `forks/${fork}.json`;
|
|
1857
1899
|
const start = run.events[0].timestamp;
|
|
1858
|
-
const length =
|
|
1900
|
+
const length = Math.round(run.events.at(-1).timestamp - start) / 1e3;
|
|
1859
1901
|
await repo.reviews.saveFile(review.id, round, recording, Buffer.from(JSON.stringify(run.events)));
|
|
1860
1902
|
await repo.reviews.saveFile(review.id, round, `readings/${recording}`, Buffer.from(JSON.stringify(run.readings)));
|
|
1861
1903
|
await repo.reviews.saveFile(review.id, round, `assets/${recording}`, Buffer.from(JSON.stringify(run.assets)));
|
|
1862
1904
|
for (const { name, bytes } of run.files) await repo.reviews.saveFile(review.id, round, name, bytes);
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
const
|
|
1905
|
+
const picture = run.picture && await keepPicture(repo, review.id, round, `notes/${fork}-fork.png`, run.picture);
|
|
1906
|
+
const seen = [...run.readings, ...run.glimpses].filter((glimpse) => glimpse.at >= run.from);
|
|
1907
|
+
for (const { id, note } of notes) {
|
|
1908
|
+
const app = {
|
|
1909
|
+
from: {
|
|
1910
|
+
flow: from.flow,
|
|
1911
|
+
at: run.from
|
|
1912
|
+
},
|
|
1913
|
+
did: note.on.app.did,
|
|
1914
|
+
recording,
|
|
1915
|
+
length,
|
|
1916
|
+
steps: run.steps
|
|
1917
|
+
};
|
|
1918
|
+
const shown = note.on.element && firstShown(note.on.element, seen);
|
|
1877
1919
|
const on = {
|
|
1878
1920
|
flow: from.flow,
|
|
1879
|
-
at,
|
|
1880
|
-
...
|
|
1881
|
-
role: note.on.element.role,
|
|
1882
|
-
name: note.on.element.name,
|
|
1883
|
-
node
|
|
1884
|
-
} },
|
|
1921
|
+
at: shown ? shown.at : Math.max(run.from, length - .1),
|
|
1922
|
+
...shown && { element: shown.element },
|
|
1885
1923
|
app
|
|
1886
1924
|
};
|
|
1887
|
-
|
|
1888
|
-
walked.push({
|
|
1925
|
+
ran.push({
|
|
1889
1926
|
note: id,
|
|
1890
1927
|
flow: from.flow,
|
|
1928
|
+
step,
|
|
1929
|
+
fork: run.fork,
|
|
1891
1930
|
on,
|
|
1892
|
-
|
|
1893
|
-
...!reached && { stopped: run.stopped ?? "" },
|
|
1931
|
+
found: !!shown,
|
|
1894
1932
|
...picture && { picture }
|
|
1895
1933
|
});
|
|
1896
1934
|
}
|
|
1897
1935
|
}
|
|
1898
|
-
return
|
|
1936
|
+
return ran;
|
|
1899
1937
|
}
|
|
1900
1938
|
async function keepPicture(repo, id, round, name, bytes) {
|
|
1901
1939
|
await repo.reviews.saveFile(id, round, name, bytes);
|
|
1902
1940
|
return repo.show(join(dirname(repo.round(id, round)), "files", name));
|
|
1903
1941
|
}
|
|
1904
|
-
function
|
|
1905
|
-
const
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1942
|
+
function unfinished(ran) {
|
|
1943
|
+
const unwritten = ran.filter((one) => !one.fork && one.step);
|
|
1944
|
+
const lines = ran.filter((one) => one.fork && one.failed).map(({ note, fork, failed }) => `The fork ${quote(fork)} for note ${note} failed${failed.at ? ` at its step ${quote(failed.at)}` : ""}, where Playwright says “${failed.why}”. Fix it and record again.`);
|
|
1945
|
+
for (const [flow, notes] of Map.groupBy(unwritten, (one) => one.flow)) {
|
|
1946
|
+
const ids = notes.map((one) => one.note);
|
|
1947
|
+
lines.push(`${ids.length > 1 ? `Notes ${list(ids)} were` : `Note ${ids[0]} was`} made in the app, and ${ids.length > 1 ? "have" : "has"} no fork yet. Write what the person did there as a fork of ${quote(flow)}, after it in its file, finding things by role and name as its steps do:
|
|
1948
|
+
|
|
1949
|
+
test.fork("<what they do there>", { notes: [${ids.map((id) => `"${id}"`).join(", ")}] }, async ({ page }) => {
|
|
1950
|
+
await test.step("<a step of theirs>", async () => { … });
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
It runs from where they took the flow over, in its step ${quote(notes[0].step)}, every round until the note is settled, and your answer goes on what the note is on, where the fork first shows it. Then record again.`);
|
|
1954
|
+
}
|
|
1955
|
+
return lines;
|
|
1956
|
+
}
|
|
1957
|
+
function saidOf(ran) {
|
|
1958
|
+
if (!ran.fork) return `Couldn't run note ${ran.note}'s fork: ${ran.failed?.why}.`;
|
|
1959
|
+
const noted = ran.on?.element ? `the ${ran.on.element.role} ${quote(ran.on.element.name)}` : "what it's on";
|
|
1960
|
+
if (ran.found) return `Ran ${quote(ran.fork)}, note ${ran.note}'s fork: it shows ${noted}, and your answer is there.${ran.picture ? ` Where it ends: ${ran.picture}` : ""}`;
|
|
1961
|
+
return `Ran ${quote(ran.fork)}, note ${ran.note}'s fork: it never shows what the note is on, so your answer is at its end.${ran.picture ? ` Where it ends: ${ran.picture}` : ""}`;
|
|
1909
1962
|
}
|
|
1910
|
-
async function
|
|
1963
|
+
async function forks(repo, id) {
|
|
1911
1964
|
const review = await repo.reviews.get(id);
|
|
1912
|
-
if (review.accepted) throw new Refused(["This review is accepted, so there's
|
|
1965
|
+
if (review.accepted) throw new Refused(["This review is accepted, so there's no fork left to run."]);
|
|
1913
1966
|
const number = roundInProgress(review);
|
|
1914
1967
|
const saved = review.rounds.find((round) => round.number === number);
|
|
1915
|
-
const
|
|
1916
|
-
if (!
|
|
1968
|
+
const ran = await runForks(repo, review, number, saved?.flows ?? review.rounds.findLast((round) => round.number > 0)?.flows ?? []);
|
|
1969
|
+
if (!ran.length) return console.log(`Nothing to run: round ${number} answers no note made in the app.`);
|
|
1970
|
+
const left = unfinished(ran);
|
|
1971
|
+
if (left.length) throw new Refused(left);
|
|
1917
1972
|
const now = saved && (await repo.reviews.get(id)).rounds.find((round) => round.number === number);
|
|
1918
1973
|
if (now) {
|
|
1919
1974
|
const placed = (reply) => {
|
|
1920
|
-
const on =
|
|
1975
|
+
const on = ran.find((one) => one.note === reply.note)?.on;
|
|
1921
1976
|
return on ? {
|
|
1922
1977
|
...reply,
|
|
1923
1978
|
on
|
|
@@ -1928,7 +1983,7 @@ async function walk(repo, id) {
|
|
|
1928
1983
|
replies: now.replies.map(placed)
|
|
1929
1984
|
});
|
|
1930
1985
|
}
|
|
1931
|
-
console.log(`\n${
|
|
1986
|
+
console.log(`\n${ran.map(saidOf).join("\n")}`);
|
|
1932
1987
|
}
|
|
1933
1988
|
// src/record.ts
|
|
1934
1989
|
async function record(repo, id) {
|
|
@@ -1960,8 +2015,10 @@ async function record(repo, id) {
|
|
|
1960
2015
|
words: ""
|
|
1961
2016
|
}));
|
|
1962
2017
|
const by = agent(saved?.by);
|
|
1963
|
-
const
|
|
1964
|
-
const
|
|
2018
|
+
const forked = await runForks(repo, review, number, flows);
|
|
2019
|
+
const forkless = unfinished(forked);
|
|
2020
|
+
if (forkless.length) throw new Refused([`\nRound ${number} wasn't recorded:`, ...forkless]);
|
|
2021
|
+
const replies = await Promise.all([...written, ...waiting].map((reply) => placed(repo, review, number, flows, reply, forked)));
|
|
1965
2022
|
console.log(`\nComparing round ${number} with each round before it…`);
|
|
1966
2023
|
const comparisons = await compareWithEarlier(repo.reviews, id, number, flows);
|
|
1967
2024
|
await repo.reviews.saveRound(id, {
|
|
@@ -1996,7 +2053,7 @@ async function record(repo, id) {
|
|
|
1996
2053
|
console.log(`${parts} only ${look} different in the pictures, with nothing read there changed${pictured.length > 3 ? "" : `: ${list(pictured.map((change) => quote(change.words)))}`}. The person sees ${them} on the recording, and ${need} nothing from you, unless it's what you changed for a note: then say so in “accounts”, and it shows in your answer.`);
|
|
1997
2054
|
}
|
|
1998
2055
|
if (since && !since.changes.length && replies.some((reply) => reply.on && !reply.on.app && !reply.watch)) console.log(`Nothing at rest shows an answer that moves, like an animation: if yours does, add "watch": "<the title of the step it's seen in>" to its reply in ${file}, and the note plays that step.`);
|
|
1999
|
-
if (
|
|
2056
|
+
if (forked.length) console.log(`\n${forked.map(saidOf).join("\n")}`);
|
|
2000
2057
|
if (left.length) console.log(`\nStill to do, in ${file}:\n${left.map((problem) => ` ${problem}`).join("\n")}`);
|
|
2001
2058
|
if (number === 1 && !saved && named.length) console.log(`\nSay why each change is there in “accounts”, by its id: { "line": "<why, in your words>" }, { "note": "<the id of the note it was made for>" }, or { "notReal": "<why nothing really changed>" }.`);
|
|
2002
2059
|
const wrote = number === 1 && !saved && recorded.find((kept) => kept.wrote);
|
|
@@ -2009,21 +2066,21 @@ ${quote(wrote.flow.name)} sends the app ${wrote.wrote}, which changes what it ho
|
|
|
2009
2066
|
if (by === "Agent") console.log(`\nPlaythrough can't tell which agent you are. Write your name in ${file} as “by”, and each of your answers shows it.`);
|
|
2010
2067
|
console.log(`\n${left.length ? "Then hand it over" : "Hand it over when it's ready"}, in the background, so you're told when the person's notes come: playthrough wait ${id}`);
|
|
2011
2068
|
}
|
|
2012
|
-
async function placed(repo, review, number, flows, reply,
|
|
2069
|
+
async function placed(repo, review, number, flows, reply, forked) {
|
|
2013
2070
|
const answer = {
|
|
2014
2071
|
note: reply.note,
|
|
2015
2072
|
words: reply.words,
|
|
2016
2073
|
...reply.watch && { watch: reply.watch }
|
|
2017
2074
|
};
|
|
2018
|
-
const
|
|
2019
|
-
|
|
2020
|
-
if (walk) return {
|
|
2075
|
+
const fork = forked.find((one) => one.note === reply.note);
|
|
2076
|
+
if (fork) return fork.on ? {
|
|
2021
2077
|
...answer,
|
|
2022
|
-
on:
|
|
2023
|
-
};
|
|
2078
|
+
on: fork.on
|
|
2079
|
+
} : answer;
|
|
2080
|
+
const seen = lastSeen(review, reply.note, number);
|
|
2024
2081
|
const flow = seen && flows.find((flow) => flow.name === seen.on.flow);
|
|
2025
2082
|
const then = seen && review.rounds.find((round) => round.number === seen.round)?.flows.find((kept) => kept.name === seen.on.flow);
|
|
2026
|
-
if (!seen ||
|
|
2083
|
+
if (!seen || !flow || !then) return answer;
|
|
2027
2084
|
const from = {
|
|
2028
2085
|
flow: then,
|
|
2029
2086
|
readings: await readingsOn(repo.reviews, review.id, seen.round, then)
|
|
@@ -2204,21 +2261,25 @@ async function wait(repo, id) {
|
|
|
2204
2261
|
return ` ${turn.agent ? `${turn.by === you ? "You" : turn.by}, in round ${turn.round}` : turn.verdict ? `${turn.by}, ${turn.verdict}` : turn.by}${turn.words.trim() ? `: ${turn.words}` : ""}`;
|
|
2205
2262
|
});
|
|
2206
2263
|
const picture = pictures.get(note);
|
|
2207
|
-
|
|
2264
|
+
const kept = notes.get(note);
|
|
2265
|
+
const from = kept?.note.on?.app && review.rounds.find((one) => one.number === kept.round)?.flows.find((flow) => flow.name === kept.note.on.app.from.flow);
|
|
2266
|
+
console.log(`\n ${note} ${where(kept?.note.on, from)}${picture ? `\n As they saw it: ${picture}` : ""}\n${turns.join("\n")}`);
|
|
2208
2267
|
}
|
|
2209
|
-
if (asking.some((note) => notes.get(note)?.note.on?.app)) console.log(`\nWhat they did in the app is
|
|
2268
|
+
if (asking.some((note) => notes.get(note)?.note.on?.app)) console.log(`\nWhat they did in the app is yours to write, as a fork of the flow they took over, in its file: test.fork("<what they do there>", { notes: ["<the note's id>"] }, async ({ page }) => { … }), with steps as a flow's. Record runs the flow to where they took it over, then the fork, every round until the note is settled, and puts your answer on what the note is on, where the fork first shows it. playthrough forks ${id} runs it while you work.`);
|
|
2210
2269
|
const first = pictures.size ? "Look at what each note is on, change" : "Change";
|
|
2211
2270
|
console.log(`\n${first} the app, then record round ${round}: playthrough record ${id}. Then answer each note in that round's round.json, under “replies”: record says where.`);
|
|
2212
2271
|
return console.log(`New to this review? Run playthrough alone for how Playthrough works. Its flows are in ${repo.show(repo.flows(id))}.`);
|
|
2213
2272
|
}
|
|
2214
2273
|
}
|
|
2215
|
-
function where(place) {
|
|
2274
|
+
function where(place, from) {
|
|
2216
2275
|
if (!place) return "on the work in general";
|
|
2217
2276
|
const at = place.at === void 0 ? "" : ` at ${time(place.at)}${place.until === void 0 ? "" : ` to ${time(place.until)}`}`;
|
|
2218
2277
|
const on = place.element ? `, the ${place.element.role} “${place.element.name}”` : place.point ? `, ${share(place.point.x)} across and ${share(place.point.y)} down the screen` : place.box ? `, the part of the screen ${share(place.box.x)} across and ${share(place.box.y)} down` : "";
|
|
2219
2278
|
if (place.app) {
|
|
2220
2279
|
const did = place.app.did.length ? ` and carried on by hand (${list(place.app.did.map(done))})` : "";
|
|
2221
|
-
|
|
2280
|
+
const paused = from && pausedAt(from, place.app.from.at);
|
|
2281
|
+
const step = paused ? `, ${paused.actions ? `after ${paused.actions} of the actions in` : "at the end of"} its step “${paused.step}”` : "";
|
|
2282
|
+
return `on “${place.app.from.flow}”, in the app: they took the flow over at ${time(place.app.from.at)}${step}${did}${on}`;
|
|
2222
2283
|
}
|
|
2223
2284
|
return `on “${place.flow}”${at}${on}`;
|
|
2224
2285
|
}
|
|
@@ -2238,7 +2299,7 @@ const takes = {
|
|
|
2238
2299
|
start: [2, `playthrough start <review> "<title>" "<what was asked>"`],
|
|
2239
2300
|
record: [0, "playthrough record <review>"],
|
|
2240
2301
|
wait: [0, "playthrough wait <review>"],
|
|
2241
|
-
|
|
2302
|
+
forks: [0, "playthrough forks <review>"],
|
|
2242
2303
|
open: [0, "playthrough open [<review>]"],
|
|
2243
2304
|
"take-over": [2, `playthrough take-over <review> "<flow>" ["<step>"]`]
|
|
2244
2305
|
};
|
|
@@ -2255,7 +2316,7 @@ try {
|
|
|
2255
2316
|
else if (command === "start") await start(repo(), review, words[0], words[1]);
|
|
2256
2317
|
else if (command === "record") await record(repo(), review);
|
|
2257
2318
|
else if (command === "wait") await wait(repo(), review);
|
|
2258
|
-
else if (command === "
|
|
2319
|
+
else if (command === "forks") await forks(repo(), review);
|
|
2259
2320
|
else if (command === "take-over") await takeOver$1(repo(), review, words[0], words[1]);
|
|
2260
2321
|
}
|
|
2261
2322
|
} catch (error) {
|
package/src/playwright.config.js
CHANGED
|
@@ -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,
|
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-
|
|
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";
|