playthrough 0.3.0 → 0.4.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 +23 -20
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/{flows-Dt7fcPLt.js → flows-BkG0TgHA.js} +156 -44
- package/src/playthrough.js +124 -211
- package/src/playwright.config.js +1 -1
- package/src/{reporter-4C_7joIm.js → reporter-DE8aueAO.js} +18 -16
- package/src/reporter.js +1 -1
- package/src/test.js +29 -8
- package/types/cli/src/flows.d.ts +13 -5
- package/types/cli/src/reporter.d.ts +3 -1
- package/types/cli/src/try-setup.d.ts +1 -0
- package/types/page/src/found.d.ts +4 -2
- package/types/review/src/model.d.ts +3 -0
- package/types/review/src/rules.d.ts +47 -0
- package/types/review/src/store.d.ts +0 -1
- package/viewer/assets/index-Ds2iot3O.js +193 -0
- package/viewer/index.html +1 -1
- package/viewer/assets/index-Bbi9sjtm.js +0 -193
package/src/playthrough.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON
|
|
2
|
-
import { A as
|
|
3
|
-
import { D as
|
|
2
|
+
import { A as list, C as conversation, D as problemsFinishing, E as notesToAnswer, M as time, O as roundInProgress, S as allNotes, T as lastSeen, _ as readingsOn, b as Refused, j as quote, k as toAccountFor, p as firstLine, w as inOrderJoined } from "./reporter-DE8aueAO.js";
|
|
3
|
+
import { D as setupIn, O as repo, _ as placeIn, a as failedAt, b as serves, c as isFlow, f as walkFork, g as compareWithEarlier, h as writtenNow, i as called, k as pausedAt, l as makeFlowsFolder, m as trySetup, o as flowsOf, p as whereItStopped, s as howToWriteFlows, t as ALONGSIDE, u as runFlows, v as shownAt, w as giveBackLeft, x as stopApp, y as keepRunning } from "./flows-BkG0TgHA.js";
|
|
4
4
|
import { t as read } from "./read-Bg9XVTtn.js";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { copyFileSync, cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import { constants, networkInterfaces, tmpdir, userInfo } from "node:os";
|
|
10
11
|
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
11
12
|
import { chromium, request } from "@playwright/test";
|
|
@@ -21,9 +22,27 @@ if (major < 24) {
|
|
|
21
22
|
// src/before.ts
|
|
22
23
|
function keepBefore(repo, id) {
|
|
23
24
|
if (!attempt(() => git(repo, ["rev-parse", "--is-inside-work-tree"]))) return void 0;
|
|
25
|
+
const head = headOf(repo);
|
|
26
|
+
if (!head) return void 0;
|
|
24
27
|
try {
|
|
25
|
-
const
|
|
26
|
-
|
|
28
|
+
const commit = git(repo, [
|
|
29
|
+
"commit-tree",
|
|
30
|
+
"--no-gpg-sign",
|
|
31
|
+
"-p",
|
|
32
|
+
head,
|
|
33
|
+
"-m",
|
|
34
|
+
`Before, of the review ${id}: the app as it was, with what wasn't committed`,
|
|
35
|
+
treeNow(repo)
|
|
36
|
+
], {
|
|
37
|
+
...process.env,
|
|
38
|
+
...PLAYTHROUGH
|
|
39
|
+
});
|
|
40
|
+
git(repo, [
|
|
41
|
+
"update-ref",
|
|
42
|
+
`refs/playthrough/${id}/before`,
|
|
43
|
+
commit
|
|
44
|
+
]);
|
|
45
|
+
return commit;
|
|
27
46
|
} catch (error) {
|
|
28
47
|
throw new Refused([
|
|
29
48
|
"Before can't be kept as a commit, because git says:",
|
|
@@ -32,179 +51,82 @@ function keepBefore(repo, id) {
|
|
|
32
51
|
]);
|
|
33
52
|
}
|
|
34
53
|
}
|
|
35
|
-
|
|
36
|
-
commit,
|
|
37
|
-
paths: ["playthrough"]
|
|
38
|
-
}), git(repo, [
|
|
39
|
-
"rev-list",
|
|
40
|
-
"--parents",
|
|
41
|
-
"--max-count=1",
|
|
42
|
-
commit
|
|
43
|
-
]).split(" ").slice(1));
|
|
44
|
-
function keep(repo, id, tree, parents) {
|
|
45
|
-
const commit = git(repo, [
|
|
46
|
-
"commit-tree",
|
|
47
|
-
"--no-gpg-sign",
|
|
48
|
-
...parents.flatMap((parent) => ["-p", parent]),
|
|
49
|
-
"-m",
|
|
50
|
-
`Before, of the review ${id}: the app as it was, with what wasn't committed`,
|
|
51
|
-
tree
|
|
52
|
-
], {
|
|
53
|
-
...process.env,
|
|
54
|
-
...PLAYTHROUGH
|
|
55
|
-
});
|
|
56
|
-
git(repo, [
|
|
57
|
-
"update-ref",
|
|
58
|
-
`refs/playthrough/${id}/before`,
|
|
59
|
-
commit
|
|
60
|
-
]);
|
|
61
|
-
return commit;
|
|
62
|
-
}
|
|
63
|
-
function treeNow(repo, onto) {
|
|
54
|
+
function treeNow(repo) {
|
|
64
55
|
const folder = mkdtempSync(join(tmpdir(), "playthrough-"));
|
|
65
56
|
try {
|
|
66
57
|
const index = join(folder, "index");
|
|
67
|
-
|
|
58
|
+
attempt(() => copyFileSync(resolve(repo.root, git(repo, [
|
|
59
|
+
"rev-parse",
|
|
60
|
+
"--git-path",
|
|
61
|
+
"index"
|
|
62
|
+
])), index));
|
|
63
|
+
git(repo, [
|
|
64
|
+
"add",
|
|
65
|
+
"--all",
|
|
66
|
+
":/"
|
|
67
|
+
], {
|
|
68
68
|
...process.env,
|
|
69
69
|
GIT_INDEX_FILE: index
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"-r",
|
|
76
|
-
"--cached",
|
|
77
|
-
"--force",
|
|
78
|
-
"--quiet",
|
|
79
|
-
"--ignore-unmatch",
|
|
80
|
-
"--",
|
|
81
|
-
...onto.paths
|
|
82
|
-
], env);
|
|
83
|
-
git(repo, [
|
|
84
|
-
"add",
|
|
85
|
-
"--all",
|
|
86
|
-
"--",
|
|
87
|
-
...onto.paths
|
|
88
|
-
], env);
|
|
89
|
-
} else {
|
|
90
|
-
attempt(() => copyFileSync(resolve(repo.root, git(repo, [
|
|
91
|
-
"rev-parse",
|
|
92
|
-
"--git-path",
|
|
93
|
-
"index"
|
|
94
|
-
])), index));
|
|
95
|
-
git(repo, [
|
|
96
|
-
"add",
|
|
97
|
-
"--all",
|
|
98
|
-
":/"
|
|
99
|
-
], env);
|
|
100
|
-
}
|
|
101
|
-
return git(repo, ["write-tree"], env);
|
|
70
|
+
});
|
|
71
|
+
return git(repo, ["write-tree"], {
|
|
72
|
+
...process.env,
|
|
73
|
+
GIT_INDEX_FILE: index
|
|
74
|
+
});
|
|
102
75
|
} finally {
|
|
103
76
|
rmSync(folder, { recursive: true });
|
|
104
77
|
}
|
|
105
78
|
}
|
|
106
|
-
const changedSince = (repo, commit, paths) => attempt(() => git(repo, [
|
|
107
|
-
"diff",
|
|
108
|
-
"--quiet",
|
|
109
|
-
`${commit}^{tree}`,
|
|
110
|
-
treeNow(repo),
|
|
111
|
-
"--",
|
|
112
|
-
...paths
|
|
113
|
-
])) === void 0;
|
|
114
79
|
const PLAYTHROUGH = {
|
|
115
80
|
GIT_AUTHOR_NAME: "Playthrough",
|
|
116
81
|
GIT_AUTHOR_EMAIL: "playthrough@localhost",
|
|
117
82
|
GIT_COMMITTER_NAME: "Playthrough",
|
|
118
83
|
GIT_COMMITTER_EMAIL: "playthrough@localhost"
|
|
119
84
|
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (!before) return;
|
|
123
|
-
const later = flows.filter((name) => !before.flows.some((flow) => flow.name === name) && !before.cantStart?.includes(name));
|
|
124
|
-
if (!later.length) return;
|
|
125
|
-
const [are, they, have] = later.length > 1 ? [
|
|
126
|
-
"are",
|
|
127
|
-
"they're",
|
|
128
|
-
"have"
|
|
129
|
-
] : [
|
|
130
|
-
"is",
|
|
131
|
-
"it's",
|
|
132
|
-
"has"
|
|
133
|
-
];
|
|
134
|
-
const named = called(later);
|
|
135
|
-
await onBefore(repo, id, later, `${named} ${are} new, so ${they} recorded on Before too`, `Round ${number} wasn't recorded`, "Record again.", (why) => `${named} ${have} no Before: ${why}.`);
|
|
136
|
-
}
|
|
137
|
-
async function changedOnBefore(repo, review) {
|
|
138
|
-
const before = review.rounds.find((round) => round.number === 0);
|
|
139
|
-
if (!before) return [];
|
|
140
|
-
const handedOver = review.rounds.some((round) => round.number > 0 && round.finished);
|
|
141
|
-
const flows = [...before.flows.filter((flow) => !handedOver || flow.stopped).map((flow) => flow.name), ...before.cantStart ?? []];
|
|
142
|
-
const ran = await ranOnBefore(repo, review.id);
|
|
143
|
-
return flows.filter((name) => !ran[name] || hashOf(join(repo.flows(review.id), ran[name].file)) !== ran[name].hash);
|
|
144
|
-
}
|
|
145
|
-
const ranOnBefore = (repo, id) => readFile(repo.ranOnBefore(id), "utf8").then(JSON.parse, () => ({}));
|
|
146
|
-
async function noteRanOnBefore(repo, id, written, gone = []) {
|
|
147
|
-
const ran = {
|
|
148
|
-
...await ranOnBefore(repo, id),
|
|
149
|
-
...written
|
|
150
|
-
};
|
|
151
|
-
for (const name of gone) delete ran[name];
|
|
152
|
-
await writeFile(repo.ranOnBefore(id), JSON.stringify(ran));
|
|
153
|
-
}
|
|
154
|
-
async function tryBeforeAgain(repo, id, flows, again) {
|
|
155
|
-
const [has, they, them] = flows.length > 1 ? [
|
|
156
|
-
"have",
|
|
157
|
-
"they're",
|
|
158
|
-
"them"
|
|
159
|
-
] : [
|
|
160
|
-
"has",
|
|
161
|
-
"it's",
|
|
162
|
-
"it"
|
|
163
|
-
];
|
|
164
|
-
const named = called(flows);
|
|
165
|
-
await onBefore(repo, id, flows, `${named} ${has} changed since ${flows.length > 1 ? "they" : "it"} last ran on Before, so ${they} recorded on it again, in place of what it has of ${them}`, "Before wasn't recorded again", again, (why) => `Before can't record ${named} again: ${why}.`);
|
|
166
|
-
}
|
|
167
|
-
async function recordBeforeAgain(repo, id, command, again) {
|
|
85
|
+
const handedOver = (review, flow) => review.rounds.some((round) => round.number > 0 && round.finished && round.flows.some((kept) => kept.name === flow));
|
|
86
|
+
async function recordOnBefore(repo, id, number, { recorded, written }) {
|
|
168
87
|
const review = await repo.reviews.get(id);
|
|
169
88
|
const before = review.rounds.find((round) => round.number === 0);
|
|
170
|
-
if (!before
|
|
171
|
-
await
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
89
|
+
if (!before) return;
|
|
90
|
+
const ran = await readFile(repo.ranOnBefore(id), "utf8").then(JSON.parse, () => ({}));
|
|
91
|
+
const setup = await writtenNow(repo);
|
|
92
|
+
const follows = Object.fromEntries(recorded.map(({ flow, doneOn }) => [flow.name, createHash("sha256").update(JSON.stringify([
|
|
93
|
+
written[flow.name]?.hash,
|
|
94
|
+
setup,
|
|
95
|
+
doneOn ?? {}
|
|
96
|
+
])).digest("hex")]));
|
|
97
|
+
const on = [...before.flows.map((flow) => flow.name), ...before.cantStart ?? []];
|
|
98
|
+
const flows = recorded.filter(({ flow }) => !on.includes(flow.name) || !handedOver(review, flow.name) && ran[flow.name] !== follows[flow.name]);
|
|
99
|
+
const gone = on.filter((name) => !handedOver(review, name) && !recorded.some(({ flow }) => flow.name === name));
|
|
100
|
+
if (!flows.length && !gone.length) return;
|
|
101
|
+
const names = flows.map(({ flow }) => flow.name);
|
|
102
|
+
const unrecorded = `Round ${number} wasn't recorded`;
|
|
103
|
+
const why = !before.commit ? "Before wasn't kept as a commit, so the app as it was can't be put back" : !headOf(repo) ? UNBORN : void 0;
|
|
104
|
+
if (why) return void (names.length && console.log(`\n${called(names)} ${names.length > 1 ? "have" : "has"} no Before: ${why}.`));
|
|
183
105
|
const taken = await repo.reviews.files(id, 0);
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}).catch((error) => {
|
|
106
|
+
const doneInRound = Object.fromEntries(flows.map(({ flow, doneOn }) => [flow.name, doneOn ?? {}]));
|
|
107
|
+
const run = () => runFlows(repo, id, 0, { before: {
|
|
108
|
+
flows: names,
|
|
109
|
+
taken,
|
|
110
|
+
doneInRound
|
|
111
|
+
} }).catch((error) => {
|
|
191
112
|
if (error instanceof Refused) return {
|
|
192
113
|
recorded: [],
|
|
193
|
-
failed:
|
|
114
|
+
failed: names.map((flow) => ({ flow })),
|
|
194
115
|
written: {}
|
|
195
116
|
};
|
|
196
117
|
throw error;
|
|
197
118
|
});
|
|
198
|
-
let
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
119
|
+
let kept = {
|
|
120
|
+
recorded: [],
|
|
121
|
+
failed: [],
|
|
122
|
+
written: {}
|
|
123
|
+
};
|
|
124
|
+
if (names.length) {
|
|
203
125
|
const { url } = await repo.app();
|
|
204
126
|
await stopApp(url);
|
|
205
127
|
const shared = await serves(url);
|
|
206
|
-
console.log(`\
|
|
207
|
-
|
|
128
|
+
console.log(`\nYour work is set aside with git stash while ${names.length > 1 ? "they record" : "it records"} on Before${shared ? `, and what serves ${url} serves the app as it was` : ""}.`);
|
|
129
|
+
kept = await asItWas(repo, unrecorded, before.commit, shared, async () => {
|
|
208
130
|
if (shared) await caughtUp(url);
|
|
209
131
|
try {
|
|
210
132
|
return await run();
|
|
@@ -213,21 +135,17 @@ async function onBefore(repo, id, flows, doing, unrecorded, again, cant, whole =
|
|
|
213
135
|
}
|
|
214
136
|
});
|
|
215
137
|
if (shared) await caughtUp(url);
|
|
138
|
+
refuseIfBroken(kept.failed, unrecorded, "Record again.");
|
|
216
139
|
}
|
|
217
|
-
refuseIfBroken(ran.failed, unrecorded, again);
|
|
218
|
-
const commit = whole ? keptWithOursNow(repo, id, before.commit) : void 0;
|
|
219
|
-
const gone = later.filter((name) => !ran.recorded.some(({ flow }) => flow.name === name) && !ran.failed.some(({ flow }) => flow === name));
|
|
220
140
|
await repo.reviews.addToBefore(id, {
|
|
221
|
-
flows:
|
|
222
|
-
cantStart: cantStart(
|
|
223
|
-
files:
|
|
224
|
-
...commit && { commit },
|
|
141
|
+
flows: kept.recorded.map(({ flow }) => flow),
|
|
142
|
+
cantStart: cantStart(kept.recorded, kept.failed),
|
|
143
|
+
files: kept.recorded.flatMap(({ files }) => files),
|
|
225
144
|
gone
|
|
226
145
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return true;
|
|
146
|
+
for (const name of names) ran[name] = follows[name];
|
|
147
|
+
for (const name of gone) delete ran[name];
|
|
148
|
+
await writeFile(repo.ranOnBefore(id), JSON.stringify(ran));
|
|
231
149
|
}
|
|
232
150
|
function refuseIfBroken(failed, unrecorded, again) {
|
|
233
151
|
const broken = failed.filter((failure) => failure.broken);
|
|
@@ -238,15 +156,6 @@ function refuseIfBroken(failed, unrecorded, again) {
|
|
|
238
156
|
]);
|
|
239
157
|
}
|
|
240
158
|
const cantStart = (recorded, failed) => failed.map(({ flow }) => flow).filter((flow) => !recorded.some((kept) => kept.flow.name === flow));
|
|
241
|
-
function howFar(repo, id, recorded, failed, app) {
|
|
242
|
-
const lines = failed.map(({ flow, at, why }) => {
|
|
243
|
-
const because = why ? `, where Playwright says “${why}”` : "";
|
|
244
|
-
return !recorded.some((kept) => kept.flow.name === flow) ? `${quote(flow)} can't start on ${app}${because}, so it has no Before.` : `${quote(flow)} ${at ? `stops at ${quote(at)}${because}` : "stops after its last step"}, so its Before goes as far as that.`;
|
|
245
|
-
});
|
|
246
|
-
const where = existsSync(repo.failed(id)) ? ` ${whereItStopped(repo, id, failed)}` : "";
|
|
247
|
-
if (lines.length) lines.push(`If that's the app as it was, that's Before. If it's the flow's own mistake, fix the flow and run playthrough start ${id} again: it records a flow changed since on Before again.${where}`);
|
|
248
|
-
return lines;
|
|
249
|
-
}
|
|
250
159
|
const SIGNALS = [
|
|
251
160
|
"SIGINT",
|
|
252
161
|
"SIGTERM",
|
|
@@ -1517,27 +1426,24 @@ async function record(repo, id) {
|
|
|
1517
1426
|
const review = await repo.reviews.get(id);
|
|
1518
1427
|
if (review.accepted) throw new Refused(["This review is accepted, so nothing more can be recorded."]);
|
|
1519
1428
|
const number = review.rounds.length ? roundInProgress(review) : 1;
|
|
1520
|
-
const
|
|
1521
|
-
const
|
|
1522
|
-
if (number === 1 && !await recordBeforeAgain(repo, id, `playthrough record ${id}`, "Record again.")) {
|
|
1523
|
-
const again = await changedOnBefore(repo, review);
|
|
1524
|
-
if (again.length) await tryBeforeAgain(repo, id, again, "Record again.");
|
|
1525
|
-
}
|
|
1526
|
-
const { recorded, failed } = await runFlows(repo, id, number, { first });
|
|
1429
|
+
const ran = await runFlows(repo, id, number, { first: (flow) => !handedOver(review, flow) });
|
|
1430
|
+
const { recorded, failed } = ran;
|
|
1527
1431
|
if (failed.length) throw new Refused([
|
|
1528
1432
|
`\nRound ${number} wasn't recorded, because ${failed.length > 1 ? "these flows" : "a flow"} failed:`,
|
|
1529
1433
|
...failed.map(failedAt),
|
|
1530
1434
|
`Fix ${failed.length > 1 ? "them" : "it"} and record again, which runs ${failed.length > 1 ? "them" : "it"} first. ${whereItStopped(repo, id, failed)}`
|
|
1531
1435
|
]);
|
|
1532
|
-
await recordOnBefore(repo, id, number,
|
|
1436
|
+
await recordOnBefore(repo, id, number, ran);
|
|
1533
1437
|
const saved = review.rounds.find((round) => round.number === number);
|
|
1438
|
+
const joined = inOrderJoined(review);
|
|
1439
|
+
const place = (name) => joined.includes(name) ? joined.indexOf(name) : joined.length;
|
|
1534
1440
|
const flows = recorded.map(({ flow }) => {
|
|
1535
1441
|
const line = saved?.flows.find((kept) => kept.name === flow.name)?.line;
|
|
1536
1442
|
return line ? {
|
|
1537
1443
|
...flow,
|
|
1538
1444
|
line
|
|
1539
1445
|
} : flow;
|
|
1540
|
-
});
|
|
1446
|
+
}).sort((a, b) => place(a.name) - place(b.name));
|
|
1541
1447
|
for (const { files } of recorded) for (const { name, bytes } of files) await repo.reviews.saveFile(id, number, name, bytes);
|
|
1542
1448
|
const written = saved?.replies ?? [];
|
|
1543
1449
|
const waiting = notesToAnswer(review, number).filter((note) => !written.some((reply) => reply.note === note)).map((note) => ({
|
|
@@ -1563,11 +1469,30 @@ async function record(repo, id) {
|
|
|
1563
1469
|
const left = problemsFinishing(await repo.reviews.get(id), number);
|
|
1564
1470
|
console.log(`\nRecorded round ${number}: ${flows.length > 3 ? called(flows.map((flow) => flow.name)) : list(flows.map((flow) => `${quote(flow.name)} (${Math.round(flow.length)}s)`))}.`);
|
|
1565
1471
|
const since = comparisons.find((comparison) => comparison.since === number - 1);
|
|
1566
|
-
|
|
1472
|
+
const named = toAccountFor(since);
|
|
1473
|
+
if (since) console.log(`\n${changed(named.length, number - 1)}`);
|
|
1474
|
+
const pictured = since?.changes.filter((change) => change.pictureOnly) ?? [];
|
|
1475
|
+
if (pictured.length) {
|
|
1476
|
+
const [parts, look, them, need] = pictured.length > 1 ? [
|
|
1477
|
+
`${pictured.length} parts of the page`,
|
|
1478
|
+
"look",
|
|
1479
|
+
"them",
|
|
1480
|
+
"they need"
|
|
1481
|
+
] : [
|
|
1482
|
+
"One part of the page",
|
|
1483
|
+
"looks",
|
|
1484
|
+
"it",
|
|
1485
|
+
"it needs"
|
|
1486
|
+
];
|
|
1487
|
+
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.`);
|
|
1488
|
+
}
|
|
1567
1489
|
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.`);
|
|
1568
1490
|
if (walked.length) console.log(`\n${walked.map(saidOf).join("\n")}`);
|
|
1569
1491
|
if (left.length) console.log(`\nStill to do, in ${file}:\n${left.map((problem) => ` ${problem}`).join("\n")}`);
|
|
1570
|
-
if (number === 1 && !saved &&
|
|
1492
|
+
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>" }.`);
|
|
1493
|
+
const wrote = number === 1 && !saved && recorded.find((kept) => kept.wrote);
|
|
1494
|
+
if (wrote && !(await setupIn(repo.setup)).after) console.log(`
|
|
1495
|
+
${quote(wrote.flow.name)} sends the app ${wrote.wrote}, which changes what it holds, and ${existsSync(repo.setup) ? "playthrough/setup.ts has no after, so what each flow makes stays in the app" : "there's no playthrough/setup.ts, so each flow starts on what the ones before it left, and what they make stays in the app"}: one for each flow recorded, three for a new one. Tell the person, with the step that would fix it; run playthrough alone for how setup.ts makes what each flow starts from, and gives it back.`);
|
|
1571
1496
|
if (unsaid.length) {
|
|
1572
1497
|
const [are, each] = unsaid.length > 1 ? ["are", "each"] : ["is", "it"];
|
|
1573
1498
|
console.log(`\n${list(unsaid.map((flow) => quote(flow.name)))} ${are} new to the person. In ${file}, ${each} can have a line at its start, as “line” in its flow under “flows”: what it starts from, or the choice it shows, in a sentence of up to 140 characters.`);
|
|
@@ -1619,42 +1544,30 @@ Write its flows before you change anything. They show what was asked, so write t
|
|
|
1619
1544
|
|
|
1620
1545
|
${howToWriteFlows(repo, id)}
|
|
1621
1546
|
|
|
1622
|
-
Then run playthrough start ${id} again. It
|
|
1547
|
+
Then run playthrough start ${id} again. It tries the app's setup, and keeps the app as it is as Before, to record each flow on with round 1, so the person can compare. If the app is new, there's no Before: go straight to the work.`);
|
|
1623
1548
|
}
|
|
1624
1549
|
const review = await repo.reviews.get(id);
|
|
1625
|
-
if (title && title !== review.title) throw new Refused([`There's already a review called “${id}”, of “${review.title}”. Give this one another name, or
|
|
1550
|
+
if (title && title !== review.title) throw new Refused([`There's already a review called “${id}”, of “${review.title}”. Give this one another name, or carry that one on: playthrough start ${id}`]);
|
|
1626
1551
|
const last = review.rounds.at(-1);
|
|
1627
|
-
|
|
1628
|
-
if (last?.number === 0 && await recordBeforeAgain(repo, id, `playthrough start ${id}`, "Run this again.")) return;
|
|
1629
|
-
const changed = await changedOnBefore(repo, review);
|
|
1630
|
-
if (changed.length) return tryBeforeAgain(repo, id, changed, "Run this again.");
|
|
1631
|
-
if (last?.number === 0) throw new Refused([`Before is recorded already. Change the app, then record round 1: playthrough record ${id}`]);
|
|
1552
|
+
if (last?.number === 0) throw new Refused([`Before is kept already. Change the app, then record round 1: playthrough record ${id}`]);
|
|
1632
1553
|
if (last) throw new Refused([`Before is how the app was before the work, and ${id} has round ${last.number} already. Record the round being worked on: playthrough record ${id}`]);
|
|
1554
|
+
await flowsOf(repo, id);
|
|
1555
|
+
await trySetup(repo, `playthrough start ${id}`);
|
|
1633
1556
|
const commit = keepBefore(repo, id);
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
cantStart: cantStart(recorded, failed),
|
|
1646
|
-
...commit && { commit }
|
|
1647
|
-
});
|
|
1648
|
-
await repo.reviews.finish(id, 0);
|
|
1649
|
-
await noteRanOnBefore(repo, id, written);
|
|
1650
|
-
console.log(`\nRecorded Before: ${called(flows.map((flow) => flow.name))}.`);
|
|
1651
|
-
for (const line of howFar(repo, id, recorded, failed, "the app as it is")) console.log(line);
|
|
1652
|
-
if (!commit) console.log(`This folder isn't in git, so Before isn't kept as a commit, and a flow added later can't be recorded on it.`);
|
|
1653
|
-
const wrote = recorded.find((kept) => kept.wrote);
|
|
1654
|
-
const { after } = await setupIn(repo.setup);
|
|
1655
|
-
if (wrote && !after) console.log(`
|
|
1656
|
-
${quote(wrote.flow.name)} sends the app ${wrote.wrote}, which changes what it holds, and ${existsSync(repo.setup) ? "playthrough/setup.ts has no after, so what each flow makes stays in the app" : "there's no playthrough/setup.ts, so each flow starts on what the ones before it left, and what they make stays in the app"}: one for each flow recorded, two for a new one. Tell the person, with the step that would fix it; run playthrough alone for how setup.ts makes what each flow starts from, and gives it back.`);
|
|
1557
|
+
if (commit) {
|
|
1558
|
+
await repo.reviews.saveRound(id, {
|
|
1559
|
+
number: 0,
|
|
1560
|
+
by: agent(),
|
|
1561
|
+
flows: [],
|
|
1562
|
+
replies: [],
|
|
1563
|
+
accounts: {},
|
|
1564
|
+
commit
|
|
1565
|
+
});
|
|
1566
|
+
await repo.reviews.finish(id, 0);
|
|
1567
|
+
}
|
|
1657
1568
|
console.log(`
|
|
1569
|
+
${commit ? "Kept Before: the app as it is now. Each flow is recorded on it with round 1, as far as the app as it was takes it, for the person to compare with." : "This folder isn't in git, or has no commits yet, so there's no Before: the app as it is can't be put back once you've changed it."}
|
|
1570
|
+
|
|
1658
1571
|
Now change the app. Keep each flow's name, and each step's while it does the same job: they're matched across rounds by name. Once the work is done, add a flow for each choice you made that the person can see and didn't ask for. Then record round 1: playthrough record ${id}`);
|
|
1659
1572
|
}
|
|
1660
1573
|
// src/noted.ts
|
package/src/playwright.config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { join, relative } from "node:path";
|
|
4
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
5
|
// ../review/src/model.ts
|
|
6
6
|
const text = (error) => z.string({ error }).refine((value) => value.trim() !== "", { error });
|
|
7
7
|
const seconds$1 = (error) => z.number({ error }).min(0, { error });
|
|
@@ -68,14 +68,15 @@ const Flow = z.strictObject({
|
|
|
68
68
|
steps: z.array(Step),
|
|
69
69
|
recording: fileName("needs its recording: the name of a file in the round"),
|
|
70
70
|
line: text("needs words in its line, or no line").optional().describe("The agent's, at the flow's start: what it starts from, or the choice it shows"),
|
|
71
|
-
stopped: text("needs the step it stopped at").optional().describe("On Before, the step the flow couldn't take there: it's kept as far as that
|
|
71
|
+
stopped: text("needs the step it stopped at").optional().describe("On Before, the step the flow couldn't take there, or that the app as it was had something else for: it's kept as far as that"),
|
|
72
72
|
person: text("needs the name of who it acts as, or no person").optional().describe("Who the flow is done as, as its file or the app's setup names them: the name on the recorded pointer, handed to you when you take over"),
|
|
73
73
|
unsteady: z.array(text("needs the ids of the changes it makes by itself")).optional().describe("Changes found between two recordings of the same app, like a time the server stamps: left out of every comparison, in every flow")
|
|
74
74
|
});
|
|
75
75
|
const Change = z.strictObject({
|
|
76
76
|
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"),
|
|
77
77
|
words: text("needs its words").describe("What changed, for the agent: “Download CSV and Cancel have swapped places”"),
|
|
78
|
-
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")
|
|
78
|
+
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"),
|
|
79
|
+
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")
|
|
79
80
|
});
|
|
80
81
|
const Comparison = z.strictObject({
|
|
81
82
|
round: z.number({ error: "needs its round" }).int().min(1, { error: "needs its round, after Before" }),
|
|
@@ -84,7 +85,7 @@ const Comparison = z.strictObject({
|
|
|
84
85
|
});
|
|
85
86
|
const Account = z.union([
|
|
86
87
|
z.strictObject({ line: text("needs words in its line") }).describe("Why it's there, in the agent's voice: what it meant or chose"),
|
|
87
|
-
z.strictObject({ note: text("needs the note it was made for") }).describe("Made for that note: the reply on the note says why"),
|
|
88
|
+
z.strictObject({ note: text("needs the id of the note it was made for") }).describe("Made for that note: the reply on the note says why"),
|
|
88
89
|
z.strictObject({ notReal: text("needs to say why nothing really changed") }).describe("Nothing really changed; nobody is shown it")
|
|
89
90
|
], { error: "says one of { line }, { note } or { notReal }" });
|
|
90
91
|
const Reply = z.strictObject({
|
|
@@ -153,6 +154,9 @@ function roundInProgress(review) {
|
|
|
153
154
|
if (!last) return 0;
|
|
154
155
|
return last.finished ? last.number + 1 : last.number;
|
|
155
156
|
}
|
|
157
|
+
function inOrderJoined(review) {
|
|
158
|
+
return [...new Set(review.rounds.flatMap((kept) => [...kept.flows.map((flow) => flow.name), ...kept.cantStart ?? []]))];
|
|
159
|
+
}
|
|
156
160
|
function allNotes(review) {
|
|
157
161
|
const notes = /* @__PURE__ */ new Map();
|
|
158
162
|
for (const send of review.sends) for (const note of send.notes) notes.set(note.id, {
|
|
@@ -315,6 +319,7 @@ function problemsWithComparison(review, comparison) {
|
|
|
315
319
|
}
|
|
316
320
|
return problems;
|
|
317
321
|
}
|
|
322
|
+
const toAccountFor = (comparison) => (comparison?.changes ?? []).filter((change) => !change.pictureOnly);
|
|
318
323
|
function problemsFinishing(review, number) {
|
|
319
324
|
if (review.accepted) return ["This review is accepted, so nothing more can be recorded."];
|
|
320
325
|
const round = review.rounds.find((round) => round.number === number);
|
|
@@ -322,7 +327,7 @@ function problemsFinishing(review, number) {
|
|
|
322
327
|
const at = `Round ${number}`;
|
|
323
328
|
const shape = problemsWithShape(Round, round, at);
|
|
324
329
|
if (shape.length) return shape;
|
|
325
|
-
if (!round.flows.length) return [`${at} has no flows yet. Record them first.`];
|
|
330
|
+
if (number > 0 && !round.flows.length) return [`${at} has no flows yet. Record them first.`];
|
|
326
331
|
const problems = [];
|
|
327
332
|
const notes = allNotes(review);
|
|
328
333
|
for (const id of notesToAnswer(review, number)) {
|
|
@@ -335,7 +340,7 @@ function problemsFinishing(review, number) {
|
|
|
335
340
|
const comparisons = review.comparisons.filter((comparison) => comparison.round === number);
|
|
336
341
|
if (comparisons.length) for (const earlier of review.rounds.filter((earlier) => earlier.number < number && !comparisons.some((comparison) => comparison.since === earlier.number))) problems.push(`${at} isn't compared with ${roundName(earlier.number)} yet, so it can't be handed over. Record it again.`);
|
|
337
342
|
const changes = new Map(comparisons.flatMap((comparison) => comparison.changes.map((change) => [change.id, change])));
|
|
338
|
-
for (const change of comparisons.find((comparison) => comparison.since === number - 1)
|
|
343
|
+
for (const change of toAccountFor(comparisons.find((comparison) => comparison.since === number - 1))) if (!round.accounts[change.id]) problems.push(`${at}: say why ${change.id} is there (${change.words}${where(change)}): its line, the id of the note it was made for, or why nothing really changed.`);
|
|
339
344
|
for (const [id, account] of Object.entries(round.accounts)) {
|
|
340
345
|
const change = changes.get(id);
|
|
341
346
|
if (!change) problems.push(`${at} says something about the change ${id}, which isn't one of this round's changes.`);
|
|
@@ -588,10 +593,7 @@ var Reviews = class {
|
|
|
588
593
|
if (problems.length) throw new Refused(problems);
|
|
589
594
|
const before = review.rounds.find((round) => round.number === 0);
|
|
590
595
|
for (const { name, bytes } of added.files) await this.storage.write(`${id}/rounds/0/files/${name}`, bytes);
|
|
591
|
-
await this.#writeJson(`${id}/rounds/0/round.json`,
|
|
592
|
-
...withInBefore(before, added.flows, added.cantStart, added.gone),
|
|
593
|
-
...added.commit && { commit: added.commit }
|
|
594
|
-
});
|
|
596
|
+
await this.#writeJson(`${id}/rounds/0/round.json`, withInBefore(before, added.flows, added.cantStart, added.gone));
|
|
595
597
|
});
|
|
596
598
|
}
|
|
597
599
|
async saveComparison(id, comparison) {
|
|
@@ -730,7 +732,7 @@ const PERSON = "playthrough person";
|
|
|
730
732
|
const HANDS = "playthrough hands";
|
|
731
733
|
const ACTIONS = "playthrough actions";
|
|
732
734
|
const WROTE = "playthrough wrote";
|
|
733
|
-
const
|
|
735
|
+
const DONE_ON = "playthrough done on";
|
|
734
736
|
const HOLD = 1500;
|
|
735
737
|
const readingsOf = (flow) => `readings/${flow.recording}`;
|
|
736
738
|
async function readingsOn(reviews, review, round, flow) {
|
|
@@ -786,14 +788,14 @@ var Flows = class {
|
|
|
786
788
|
const steps = result.steps.filter((step) => step.category === "test.step");
|
|
787
789
|
const person = result.attachments.find((attachment) => attachment.name === PERSON)?.body?.toString();
|
|
788
790
|
const wrote = result.attachments.find((attachment) => attachment.name === WROTE)?.body?.toString();
|
|
791
|
+
const doneOn = result.attachments.find((attachment) => attachment.name === DONE_ON)?.body?.toString();
|
|
789
792
|
const as = (made) => {
|
|
790
793
|
if (person) made.flow.person = person;
|
|
791
794
|
for (const step of made.flow.steps) if (step.person === made.flow.person) delete step.person;
|
|
792
795
|
if (wrote) made.wrote = wrote;
|
|
796
|
+
if (doneOn) made.doneOn = JSON.parse(doneOn);
|
|
793
797
|
return made;
|
|
794
798
|
};
|
|
795
|
-
const unmet = Number(result.attachments.find((attachment) => attachment.name === "playthrough unmet")?.body?.toString() ?? 0);
|
|
796
|
-
const yet = unmet ? `, though ${unmet > 1 ? `${unmet} things it expects aren't` : "1 thing it expects isn't"} so yet` : "";
|
|
797
799
|
const went = (how) => console.log(` ${quote(test.title)}: ${how}`);
|
|
798
800
|
if (result.status === "passed") {
|
|
799
801
|
if (!recording) return;
|
|
@@ -802,7 +804,7 @@ var Flows = class {
|
|
|
802
804
|
...where,
|
|
803
805
|
recorded
|
|
804
806
|
});
|
|
805
|
-
return went(`to its end, ${Math.round(recorded.flow.length)}s
|
|
807
|
+
return went(`to its end, ${Math.round(recorded.flow.length)}s`);
|
|
806
808
|
}
|
|
807
809
|
const stop = steps.at(-1);
|
|
808
810
|
const kept = stop?.error ? steps.slice(0, -1) : steps;
|
|
@@ -830,7 +832,7 @@ var Flows = class {
|
|
|
830
832
|
failed,
|
|
831
833
|
recorded
|
|
832
834
|
});
|
|
833
|
-
went(
|
|
835
|
+
went(stop.error ? `stops at ${quote(stop.title)}` : "stops after its last step");
|
|
834
836
|
}
|
|
835
837
|
onError(error) {
|
|
836
838
|
console.log(`\n${error.message ?? error.value}`);
|
|
@@ -916,4 +918,4 @@ var Flows = class {
|
|
|
916
918
|
}
|
|
917
919
|
};
|
|
918
920
|
const seconds = (ms) => Math.round(Math.max(0, ms)) / 1e3;
|
|
919
|
-
export {
|
|
921
|
+
export { list as A, conversation as C, problemsFinishing as D, notesToAnswer as E, time as M, roundInProgress as O, allNotes as S, lastSeen as T, readingsOn as _, GLIMPSES as a, Refused as b, PERSON as c, RECORDING as d, WROTE as f, readingsOf as g, hashOf as h, Flows as i, quote as j, toAccountFor as k, PICTURE as l, glimpsesOf as m, ASSETS as n, HANDS as o, firstLine as p, DONE_ON as r, HOLD as s, ACTIONS as t, READINGS as u, seconds as v, inOrderJoined as w, Reviews as x, keepAssets as y };
|
package/src/reporter.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as ACTIONS, n as ASSETS,
|
|
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 hashOf, g as readingsOf, _ as readingsOn, v as seconds } from "./reporter-DE8aueAO.js";
|