playthrough 0.2.0 → 0.3.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 +59 -71
- package/package.json +1 -1
- package/src/{read-C9bKWxxP.js → flows-Dt7fcPLt.js} +572 -1178
- package/src/playthrough.js +244 -84
- package/src/playwright.config.js +6 -10
- package/src/read-Bg9XVTtn.js +278 -0
- package/src/reporter-4C_7joIm.js +919 -0
- package/src/reporter.js +1 -1
- package/src/test.js +121 -16
- package/types/cli/src/app.d.ts +2 -0
- package/types/cli/src/changes.d.ts +1 -0
- package/types/cli/src/compare.d.ts +7 -2
- package/types/cli/src/flows.d.ts +8 -4
- package/types/cli/src/pictures.d.ts +1 -0
- package/types/cli/src/read.d.ts +3 -1
- package/types/cli/src/repo.d.ts +2 -0
- package/types/cli/src/reporter.d.ts +14 -4
- package/types/cli/src/setup.d.ts +4 -1
- package/types/cli/src/test.d.ts +1 -1
- package/types/page/src/reading.d.ts +11 -0
- package/types/review/src/rules.d.ts +2 -1
- package/types/review/src/store.d.ts +3 -0
- package/viewer/assets/index-Bbi9sjtm.js +193 -0
- package/viewer/assets/index-gTfnUrcK.css +2 -0
- package/viewer/index.html +2 -2
- package/src/reporter-CECNk7aW.js +0 -200
- package/viewer/assets/index-BriVL50M.js +0 -193
- package/viewer/assets/index-DaS2F2-s.css +0 -2
package/src/playthrough.js
CHANGED
|
@@ -1,44 +1,29 @@
|
|
|
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 time, C as conversation, D as roundInProgress, E as problemsFinishing, O as list, S as allNotes, T as notesToAnswer, _ as readingsOn, b as Refused, h as hashOf, k as quote, p as firstLine, w as lastSeen } from "./reporter-4C_7joIm.js";
|
|
3
|
+
import { D as pausedAt, E as repo, S as giveBackLeft, T as setupIn, _ as keepRunning, a as failedAt, c as makeFlowsFolder, d as walkFork, f as whereItStopped, g as shownAt, h as placeIn, i as called, l as runFlows, m as compareWithEarlier, o as howToWriteFlows, p as trySetup, s as isFlow, t as ALONGSIDE, v as serves, y as stopApp } from "./flows-Dt7fcPLt.js";
|
|
4
|
+
import { t as read } from "./read-Bg9XVTtn.js";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
|
-
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { copyFileSync, cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
7
9
|
import { constants, networkInterfaces, tmpdir, userInfo } from "node:os";
|
|
8
10
|
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
9
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { chromium, request } from "@playwright/test";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { createServer, request as request$1 } from "node:http";
|
|
13
14
|
import { connect } from "node:net";
|
|
15
|
+
// src/node.ts
|
|
16
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
17
|
+
if (major < 24) {
|
|
18
|
+
console.error(`Playthrough needs Node 24 or later, and this is Node ${process.versions.node}. Install Node 24, then run this again.`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
14
21
|
// src/before.ts
|
|
15
22
|
function keepBefore(repo, id) {
|
|
16
23
|
if (!attempt(() => git(repo, ["rev-parse", "--is-inside-work-tree"]))) return void 0;
|
|
17
24
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
"rev-parse",
|
|
21
|
-
"--verify",
|
|
22
|
-
"--quiet",
|
|
23
|
-
"HEAD"
|
|
24
|
-
]));
|
|
25
|
-
const commit = git(repo, [
|
|
26
|
-
"commit-tree",
|
|
27
|
-
"--no-gpg-sign",
|
|
28
|
-
...head ? ["-p", head] : [],
|
|
29
|
-
"-m",
|
|
30
|
-
`Before, of the review ${id}: the app as it was, with what wasn't committed`,
|
|
31
|
-
tree
|
|
32
|
-
], {
|
|
33
|
-
...process.env,
|
|
34
|
-
...PLAYTHROUGH
|
|
35
|
-
});
|
|
36
|
-
git(repo, [
|
|
37
|
-
"update-ref",
|
|
38
|
-
`refs/playthrough/${id}/before`,
|
|
39
|
-
commit
|
|
40
|
-
]);
|
|
41
|
-
return commit;
|
|
25
|
+
const head = headOf(repo);
|
|
26
|
+
return keep(repo, id, treeNow(repo), head ? [head] : []);
|
|
42
27
|
} catch (error) {
|
|
43
28
|
throw new Refused([
|
|
44
29
|
"Before can't be kept as a commit, because git says:",
|
|
@@ -47,29 +32,85 @@ function keepBefore(repo, id) {
|
|
|
47
32
|
]);
|
|
48
33
|
}
|
|
49
34
|
}
|
|
50
|
-
|
|
35
|
+
const keptWithOursNow = (repo, id, commit) => keep(repo, id, treeNow(repo, {
|
|
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) {
|
|
51
64
|
const folder = mkdtempSync(join(tmpdir(), "playthrough-"));
|
|
52
65
|
try {
|
|
53
66
|
const index = join(folder, "index");
|
|
54
|
-
attempt(() => copyFileSync(resolve(repo.root, git(repo, [
|
|
55
|
-
"rev-parse",
|
|
56
|
-
"--git-path",
|
|
57
|
-
"index"
|
|
58
|
-
])), index));
|
|
59
67
|
const env = {
|
|
60
68
|
...process.env,
|
|
61
69
|
GIT_INDEX_FILE: index
|
|
62
70
|
};
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
if (onto) {
|
|
72
|
+
git(repo, ["read-tree", onto.commit], env);
|
|
73
|
+
git(repo, [
|
|
74
|
+
"rm",
|
|
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
|
+
}
|
|
68
101
|
return git(repo, ["write-tree"], env);
|
|
69
102
|
} finally {
|
|
70
103
|
rmSync(folder, { recursive: true });
|
|
71
104
|
}
|
|
72
105
|
}
|
|
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;
|
|
73
114
|
const PLAYTHROUGH = {
|
|
74
115
|
GIT_AUTHOR_NAME: "Playthrough",
|
|
75
116
|
GIT_AUTHOR_EMAIL: "playthrough@localhost",
|
|
@@ -81,19 +122,65 @@ async function recordOnBefore(repo, id, number, flows) {
|
|
|
81
122
|
if (!before) return;
|
|
82
123
|
const later = flows.filter((name) => !before.flows.some((flow) => flow.name === name) && !before.cantStart?.includes(name));
|
|
83
124
|
if (!later.length) return;
|
|
84
|
-
const [are, they] = later.length > 1 ? [
|
|
85
|
-
|
|
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));
|
|
86
153
|
}
|
|
87
|
-
async function tryBeforeAgain(repo, id, flows) {
|
|
88
|
-
const [
|
|
89
|
-
|
|
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}.`);
|
|
90
166
|
}
|
|
91
|
-
async function
|
|
167
|
+
async function recordBeforeAgain(repo, id, command, again) {
|
|
168
|
+
const review = await repo.reviews.get(id);
|
|
169
|
+
const before = review.rounds.find((round) => round.number === 0);
|
|
170
|
+
if (!before?.commit || review.rounds.some((round) => round.number > 0 && round.finished) || !changedSince(repo, before.commit, ["playthrough"])) return false;
|
|
171
|
+
await trySetup(repo, command);
|
|
172
|
+
return onBefore(repo, id, [...before.flows.map((flow) => flow.name), ...before.cantStart ?? []], `playthrough/ has changed since Before was recorded, so Before is recorded again on it, in place of what it has`, "Before wasn't recorded again", again, (why) => `Before stays as it was recorded, on playthrough/ as it was then: ${why}.`, true);
|
|
173
|
+
}
|
|
174
|
+
async function onBefore(repo, id, flows, doing, unrecorded, again, cant, whole = false) {
|
|
92
175
|
const before = (await repo.reviews.get(id)).rounds.find((round) => round.number === 0);
|
|
93
|
-
const
|
|
94
|
-
|
|
176
|
+
const unchanged = !!before.commit && !changedSince(repo, before.commit, [":/", ":(exclude)playthrough"]);
|
|
177
|
+
const why = !before.commit ? "Before wasn't kept as a commit, so the app as it was can't be put back" : !unchanged && !headOf(repo) ? UNBORN : void 0;
|
|
178
|
+
if (why) {
|
|
179
|
+
console.log(`\n${cant(why)}`);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
95
182
|
const later = flows;
|
|
96
|
-
const taken =
|
|
183
|
+
const taken = await repo.reviews.files(id, 0);
|
|
97
184
|
const run = () => runFlows(repo, id, 0, {
|
|
98
185
|
later: {
|
|
99
186
|
flows: later,
|
|
@@ -103,37 +190,51 @@ async function onBefore(repo, id, flows, doing, unrecorded, again) {
|
|
|
103
190
|
}).catch((error) => {
|
|
104
191
|
if (error instanceof Refused) return {
|
|
105
192
|
recorded: [],
|
|
106
|
-
failed: later.map((flow) => ({ flow }))
|
|
193
|
+
failed: later.map((flow) => ({ flow })),
|
|
194
|
+
written: {}
|
|
107
195
|
};
|
|
108
196
|
throw error;
|
|
109
197
|
});
|
|
110
198
|
let ran;
|
|
111
|
-
if (
|
|
199
|
+
if (unchanged) {
|
|
112
200
|
console.log(`\n${doing}, on the app as it is: nothing has changed since Before.`);
|
|
113
201
|
ran = await run();
|
|
114
202
|
} else {
|
|
115
203
|
const { url } = await repo.app();
|
|
116
|
-
|
|
204
|
+
await stopApp(url);
|
|
205
|
+
const shared = await serves(url);
|
|
117
206
|
console.log(`\n${doing}. Your work is set aside with git stash meanwhile${shared ? `, and what serves ${url} serves the app as it was` : ""}.`);
|
|
118
207
|
ran = await asItWas(repo, unrecorded, before.commit, shared, async () => {
|
|
119
208
|
if (shared) await caughtUp(url);
|
|
120
|
-
|
|
209
|
+
try {
|
|
210
|
+
return await run();
|
|
211
|
+
} finally {
|
|
212
|
+
await stopApp(url);
|
|
213
|
+
}
|
|
121
214
|
});
|
|
215
|
+
if (shared) await caughtUp(url);
|
|
122
216
|
}
|
|
123
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));
|
|
124
220
|
await repo.reviews.addToBefore(id, {
|
|
125
221
|
flows: ran.recorded.map(({ flow }) => flow),
|
|
126
222
|
cantStart: cantStart(ran.recorded, ran.failed),
|
|
127
|
-
files: ran.recorded.flatMap(({ files }) => files)
|
|
223
|
+
files: ran.recorded.flatMap(({ files }) => files),
|
|
224
|
+
...commit && { commit },
|
|
225
|
+
gone
|
|
128
226
|
});
|
|
227
|
+
await noteRanOnBefore(repo, id, ran.written, gone);
|
|
228
|
+
if (gone.length) console.log(`\n${gone.length > 3 ? `${gone.length} of them` : list(gone.map(quote))} ${gone.length > 1 ? "aren't flows" : "isn't a flow"} any more, so Before lets ${gone.length > 1 ? "them" : "it"} go.`);
|
|
129
229
|
for (const line of howFar(repo, id, ran.recorded, ran.failed, "the app as it was")) console.log(line);
|
|
230
|
+
return true;
|
|
130
231
|
}
|
|
131
232
|
function refuseIfBroken(failed, unrecorded, again) {
|
|
132
233
|
const broken = failed.filter((failure) => failure.broken);
|
|
133
234
|
if (broken.length) throw new Refused([
|
|
134
|
-
|
|
235
|
+
`\n${unrecorded}, because the browser stopped ${broken.length > 1 ? "these flows" : "a flow"}, not the app:`,
|
|
135
236
|
...broken.map(failedAt),
|
|
136
|
-
|
|
237
|
+
again
|
|
137
238
|
]);
|
|
138
239
|
}
|
|
139
240
|
const cantStart = (recorded, failed) => failed.map(({ flow }) => flow).filter((flow) => !recorded.some((kept) => kept.flow.name === flow));
|
|
@@ -143,7 +244,7 @@ function howFar(repo, id, recorded, failed, app) {
|
|
|
143
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.`;
|
|
144
245
|
});
|
|
145
246
|
const where = existsSync(repo.failed(id)) ? ` ${whereItStopped(repo, id, failed)}` : "";
|
|
146
|
-
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
|
|
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}`);
|
|
147
248
|
return lines;
|
|
148
249
|
}
|
|
149
250
|
const SIGNALS = [
|
|
@@ -169,6 +270,9 @@ async function asItWas(repo, unrecorded, commit, shared, run) {
|
|
|
169
270
|
process.exit(128 + constants.signals[signal]);
|
|
170
271
|
};
|
|
171
272
|
for (const signal of SIGNALS) process.on(signal, stopped);
|
|
273
|
+
const ours = join(repo.root, "playthrough");
|
|
274
|
+
const today = existsSync(ours) ? mkdtempSync(join(tmpdir(), "playthrough-")) : void 0;
|
|
275
|
+
if (today) cpSync(ours, today, { recursive: true });
|
|
172
276
|
let moved = false;
|
|
173
277
|
try {
|
|
174
278
|
try {
|
|
@@ -178,7 +282,10 @@ async function asItWas(repo, unrecorded, commit, shared, run) {
|
|
|
178
282
|
"--include-untracked",
|
|
179
283
|
"--message",
|
|
180
284
|
aside.stash
|
|
181
|
-
]
|
|
285
|
+
], {
|
|
286
|
+
...process.env,
|
|
287
|
+
...PLAYTHROUGH
|
|
288
|
+
});
|
|
182
289
|
git(repo, [
|
|
183
290
|
"checkout",
|
|
184
291
|
"--quiet",
|
|
@@ -193,8 +300,19 @@ async function asItWas(repo, unrecorded, commit, shared, run) {
|
|
|
193
300
|
"Fix that, then try again."
|
|
194
301
|
]);
|
|
195
302
|
}
|
|
303
|
+
if (today) {
|
|
304
|
+
rmSync(ours, {
|
|
305
|
+
recursive: true,
|
|
306
|
+
force: true
|
|
307
|
+
});
|
|
308
|
+
cpSync(today, ours, { recursive: true });
|
|
309
|
+
}
|
|
196
310
|
return await run();
|
|
197
311
|
} finally {
|
|
312
|
+
if (today) rmSync(today, {
|
|
313
|
+
recursive: true,
|
|
314
|
+
force: true
|
|
315
|
+
});
|
|
198
316
|
for (const signal of SIGNALS) process.off(signal, stopped);
|
|
199
317
|
const problem = putWorkBack(repo, aside, true);
|
|
200
318
|
if (problem) throw stillAside(repo, aside, problem);
|
|
@@ -210,17 +328,34 @@ async function putBack(repo) {
|
|
|
210
328
|
if (!toDo) return;
|
|
211
329
|
const back = `Playthrough was stopped while your work was set aside for a flow to record on Before. It's back now, on ${aside.back}.`;
|
|
212
330
|
const url = (await repo.app().catch(() => void 0))?.url;
|
|
213
|
-
if (url && !aside.shared && await
|
|
331
|
+
if (url && !aside.shared && await serves(url)) throw new Refused([back, `Something is still serving ${url}, most likely the app as it was, left running: stop it, then run this again.`]);
|
|
214
332
|
console.log(`${back}\n`);
|
|
215
333
|
}
|
|
216
334
|
function putWorkBack(repo, aside, force) {
|
|
217
335
|
try {
|
|
218
|
-
if (here(repo) !== aside.back)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
336
|
+
if (here(repo) !== aside.back) {
|
|
337
|
+
attempt(() => git(repo, [
|
|
338
|
+
"checkout",
|
|
339
|
+
"--quiet",
|
|
340
|
+
"HEAD",
|
|
341
|
+
"--",
|
|
342
|
+
"playthrough"
|
|
343
|
+
]));
|
|
344
|
+
git(repo, [
|
|
345
|
+
"clean",
|
|
346
|
+
"--force",
|
|
347
|
+
"-d",
|
|
348
|
+
"--quiet",
|
|
349
|
+
"--",
|
|
350
|
+
"playthrough"
|
|
351
|
+
]);
|
|
352
|
+
git(repo, [
|
|
353
|
+
"checkout",
|
|
354
|
+
"--quiet",
|
|
355
|
+
...force ? ["--force"] : [],
|
|
356
|
+
aside.back
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
224
359
|
const stash = stashOf(repo, aside);
|
|
225
360
|
if (stash) git(repo, [
|
|
226
361
|
"stash",
|
|
@@ -242,6 +377,13 @@ function stillAside(repo, aside, problem) {
|
|
|
242
377
|
`Put it back with git checkout ${aside.back}${stash ? `, then git stash pop --index ${stash}` : ""}. Playthrough carries on once it's back.`
|
|
243
378
|
]);
|
|
244
379
|
}
|
|
380
|
+
const headOf = (repo) => attempt(() => git(repo, [
|
|
381
|
+
"rev-parse",
|
|
382
|
+
"--verify",
|
|
383
|
+
"--quiet",
|
|
384
|
+
"HEAD"
|
|
385
|
+
]));
|
|
386
|
+
const UNBORN = "this repo has no commits yet, and git can set your work aside to put the app as it was back only once it has one";
|
|
245
387
|
const here = (repo) => attempt(() => git(repo, [
|
|
246
388
|
"symbolic-ref",
|
|
247
389
|
"--quiet",
|
|
@@ -255,7 +397,6 @@ function stashOf(repo, aside) {
|
|
|
255
397
|
"--format=%gd %gs"
|
|
256
398
|
]).split("\n").find((stash) => stash.endsWith(aside.stash))?.split(" ")[0];
|
|
257
399
|
}
|
|
258
|
-
const serving = (url) => fetch(url, { signal: AbortSignal.timeout(2e3) }).then((response) => (response.body?.cancel(), true), () => false);
|
|
259
400
|
async function caughtUp(url) {
|
|
260
401
|
await new Promise((done) => setTimeout(done, 1e3));
|
|
261
402
|
await fetch(url, { signal: AbortSignal.timeout(6e4) }).then((response) => response.body?.cancel(), () => {});
|
|
@@ -391,7 +532,7 @@ async function start$1(repo, id, to, files, written) {
|
|
|
391
532
|
const { flows, setup } = await loadedOf(repo, id, files, written);
|
|
392
533
|
const kept = flows.get(to.flow);
|
|
393
534
|
if (!kept) throw new Refused([`${id} has no flow called ${quote(to.flow)} any more. Its flows: ${list([...flows.keys()].map(quote))}.`]);
|
|
394
|
-
const { context, page } = await pageFor(await browser(), app.url, kept.use);
|
|
535
|
+
const { context, page } = await pageFor(await browser(), app.url, kept.use, setup.use);
|
|
395
536
|
context.setDefaultTimeout(1e4);
|
|
396
537
|
context.setDefaultNavigationTimeout(0);
|
|
397
538
|
const request$2 = await request.newContext({ baseURL: app.url });
|
|
@@ -1378,9 +1519,13 @@ async function record(repo, id) {
|
|
|
1378
1519
|
const number = review.rounds.length ? roundInProgress(review) : 1;
|
|
1379
1520
|
const onBefore = !!review.rounds.find((round) => round.number === 0)?.commit;
|
|
1380
1521
|
const first = (flow) => !onBefore && !review.rounds.some((round) => round.number !== number && round.flows.some((kept) => kept.name === flow));
|
|
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
|
+
}
|
|
1381
1526
|
const { recorded, failed } = await runFlows(repo, id, number, { first });
|
|
1382
1527
|
if (failed.length) throw new Refused([
|
|
1383
|
-
|
|
1528
|
+
`\nRound ${number} wasn't recorded, because ${failed.length > 1 ? "these flows" : "a flow"} failed:`,
|
|
1384
1529
|
...failed.map(failedAt),
|
|
1385
1530
|
`Fix ${failed.length > 1 ? "them" : "it"} and record again, which runs ${failed.length > 1 ? "them" : "it"} first. ${whereItStopped(repo, id, failed)}`
|
|
1386
1531
|
]);
|
|
@@ -1416,20 +1561,19 @@ async function record(repo, id) {
|
|
|
1416
1561
|
const unsaid = flows.filter((flow) => !flow.line && !previous?.flows.some((kept) => kept.name === flow.name));
|
|
1417
1562
|
const file = repo.show(repo.round(id, number));
|
|
1418
1563
|
const left = problemsFinishing(await repo.reviews.get(id), number);
|
|
1419
|
-
console.log(`\nRecorded round ${number}: ${list(flows.map((flow) => `${quote(flow.name)} (${Math.round(flow.length)}s)`))}.`);
|
|
1564
|
+
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)`))}.`);
|
|
1420
1565
|
const since = comparisons.find((comparison) => comparison.since === number - 1);
|
|
1421
1566
|
if (since) console.log(`\n${changed(since.changes.length, number - 1)}`);
|
|
1422
1567
|
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.`);
|
|
1423
1568
|
if (walked.length) console.log(`\n${walked.map(saidOf).join("\n")}`);
|
|
1424
1569
|
if (left.length) console.log(`\nStill to do, in ${file}:\n${left.map((problem) => ` ${problem}`).join("\n")}`);
|
|
1425
|
-
if (
|
|
1570
|
+
if (number === 1 && !saved && since?.changes.length) console.log(`\nSay why each change is there in “accounts”, by its id: { "line": "<why, in your words>" }, { "note": "<the note it was made for>" }, or { "notReal": "<why nothing really changed>" }.`);
|
|
1426
1571
|
if (unsaid.length) {
|
|
1427
1572
|
const [are, each] = unsaid.length > 1 ? ["are", "each"] : ["is", "it"];
|
|
1428
|
-
console.log(`\n${list(unsaid.map((flow) => quote(flow.name)))} ${are} new to the
|
|
1573
|
+
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.`);
|
|
1429
1574
|
}
|
|
1430
1575
|
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.`);
|
|
1431
|
-
console.log(left.length ?
|
|
1432
|
-
console.log("It waits for the person's notes, which can take hours: run it in the background, so you're told when it ends.");
|
|
1576
|
+
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}`);
|
|
1433
1577
|
}
|
|
1434
1578
|
async function placed(repo, review, number, flows, reply, walked) {
|
|
1435
1579
|
const answer = {
|
|
@@ -1471,7 +1615,7 @@ async function start(repo, id, title, asked) {
|
|
|
1471
1615
|
await makeFlowsFolder(repo, id);
|
|
1472
1616
|
return console.log(`Started ${id}: “${title}”.
|
|
1473
1617
|
|
|
1474
|
-
Write its flows before you change anything. They show what was asked, so write them from the task, not from the code
|
|
1618
|
+
Write its flows before you change anything. They show what was asked, so write them from the task, not from the code.
|
|
1475
1619
|
|
|
1476
1620
|
${howToWriteFlows(repo, id)}
|
|
1477
1621
|
|
|
@@ -1480,13 +1624,14 @@ Then run playthrough start ${id} again. It records Before: each flow on the app
|
|
|
1480
1624
|
const review = await repo.reviews.get(id);
|
|
1481
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 record that one's Before: playthrough start ${id}`]);
|
|
1482
1626
|
const last = review.rounds.at(-1);
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1627
|
+
review.rounds.find((round) => round.number === 0);
|
|
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.");
|
|
1486
1631
|
if (last?.number === 0) throw new Refused([`Before is recorded already. Change the app, then record round 1: playthrough record ${id}`]);
|
|
1487
1632
|
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}`]);
|
|
1488
1633
|
const commit = keepBefore(repo, id);
|
|
1489
|
-
const { recorded, failed } = await runFlows(repo, id, 0, { first: () => true });
|
|
1634
|
+
const { recorded, failed, written } = await runFlows(repo, id, 0, { first: () => true });
|
|
1490
1635
|
refuseIfBroken(failed, "Before wasn't recorded", "Run this again.");
|
|
1491
1636
|
if (!recorded.length) return console.log(`\nNone of the flows can start on the app as it is, so there's no Before. If one should, fix it and run this again. Otherwise change the app, then record round 1: playthrough record ${id}`);
|
|
1492
1637
|
for (const { files } of recorded) for (const { name, bytes } of files) await repo.reviews.saveFile(id, 0, name, bytes);
|
|
@@ -1501,11 +1646,16 @@ Then run playthrough start ${id} again. It records Before: each flow on the app
|
|
|
1501
1646
|
...commit && { commit }
|
|
1502
1647
|
});
|
|
1503
1648
|
await repo.reviews.finish(id, 0);
|
|
1504
|
-
|
|
1649
|
+
await noteRanOnBefore(repo, id, written);
|
|
1650
|
+
console.log(`\nRecorded Before: ${called(flows.map((flow) => flow.name))}.`);
|
|
1505
1651
|
for (const line of howFar(repo, id, recorded, failed, "the app as it is")) console.log(line);
|
|
1506
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.`);
|
|
1507
1657
|
console.log(`
|
|
1508
|
-
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
|
|
1658
|
+
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}`);
|
|
1509
1659
|
}
|
|
1510
1660
|
// src/noted.ts
|
|
1511
1661
|
async function pictureNotes(repo, review, round, notes) {
|
|
@@ -1537,8 +1687,17 @@ async function whereSeen(repo, review, id, before) {
|
|
|
1537
1687
|
const seen = lastSeen(review, id, before);
|
|
1538
1688
|
const on = seen?.on;
|
|
1539
1689
|
const flow = seen && review.rounds.find((round) => round.number === seen.round)?.flows.find((flow) => flow.name === on.flow);
|
|
1540
|
-
if (!seen || !on ||
|
|
1690
|
+
if (!seen || !on || !flow) return void 0;
|
|
1541
1691
|
const readings = await readingsOn(repo.reviews, review.id, seen.round, flow);
|
|
1692
|
+
if (on.at === void 0) {
|
|
1693
|
+
const end = readings.at(-1);
|
|
1694
|
+
return end && {
|
|
1695
|
+
round: seen.round,
|
|
1696
|
+
picture: end.picture,
|
|
1697
|
+
width: end.screen.width,
|
|
1698
|
+
height: end.screen.height
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1542
1701
|
const at = on.at;
|
|
1543
1702
|
const shown = on.element ? shownAt(readings, on.element, at) : void 0;
|
|
1544
1703
|
const reading = shown?.reading ?? readings.reduce((best, next) => !best || Math.abs(next.at - at) < Math.abs(best.at - at) ? next : best, void 0);
|
|
@@ -1585,8 +1744,9 @@ async function wait(repo, id) {
|
|
|
1585
1744
|
`${error.problems.length > 1 ? "Fix these" : "Fix it"} in ${repo.show(repo.round(id, last.number))}, then hand it over again: playthrough wait ${id}`
|
|
1586
1745
|
]);
|
|
1587
1746
|
}
|
|
1588
|
-
console.log(`Round ${last.number} is ready for review.`);
|
|
1589
|
-
|
|
1747
|
+
console.log(`Round ${last.number} is ready for review. Tell the person it's theirs: npx playthrough open ${id} opens it.`);
|
|
1748
|
+
review = await reviews.get(id);
|
|
1749
|
+
} else console.log(`Round ${last.number} is handed over already.`);
|
|
1590
1750
|
console.log(`Waiting for notes on ${id}…`);
|
|
1591
1751
|
}
|
|
1592
1752
|
for (;;) {
|
|
@@ -1620,7 +1780,7 @@ async function wait(repo, id) {
|
|
|
1620
1780
|
}
|
|
1621
1781
|
if (asking.some((note) => notes.get(note)?.note.on?.app)) console.log(`\nWhat they did in the app is walked again on the new version when you record the round, and record says whether it still gets to their note. playthrough walk ${id} does that while you work.`);
|
|
1622
1782
|
const first = pictures.size ? "Look at what each note is on, change" : "Change";
|
|
1623
|
-
console.log(`\n${first} the app, then record round ${round}: playthrough record ${id}
|
|
1783
|
+
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.`);
|
|
1624
1784
|
return console.log(`New to this review? Run playthrough alone for how Playthrough works. Its flows are in ${repo.show(repo.flows(id))}.`);
|
|
1625
1785
|
}
|
|
1626
1786
|
}
|
package/src/playwright.config.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { u as waitsFor } from "./flows-Dt7fcPLt.js";
|
|
1
2
|
import { availableParallelism } from "node:os";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { defineConfig } from "@playwright/test";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
// src/playwright.config.ts
|
|
5
6
|
const run = JSON.parse(process.env.PLAYTHROUGH_RUN ?? "null");
|
|
6
7
|
var playwright_config_default = defineConfig({
|
|
7
8
|
testDir: run.flows,
|
|
8
9
|
testMatch: /\.[cm]?[jt]s$/,
|
|
9
10
|
outputDir: run.results ?? `${run.folder}/results`,
|
|
10
|
-
workers: Math.max(1, Math.floor(availableParallelism() / 4)),
|
|
11
|
-
reporter: [[
|
|
11
|
+
workers: run.app.oneAtATime ? 1 : Math.max(1, Math.floor(availableParallelism() / 4)),
|
|
12
|
+
reporter: [[fileURLToPath(new URL("./reporter.js", import.meta.url)), {
|
|
12
13
|
folder: run.folder,
|
|
14
|
+
flows: run.flows,
|
|
13
15
|
before: run.before,
|
|
14
16
|
taken: run.taken
|
|
15
17
|
}]],
|
|
@@ -19,15 +21,9 @@ var playwright_config_default = defineConfig({
|
|
|
19
21
|
width: 1280,
|
|
20
22
|
height: 800
|
|
21
23
|
},
|
|
22
|
-
actionTimeout:
|
|
24
|
+
actionTimeout: waitsFor(run),
|
|
23
25
|
screenshot: "only-on-failure"
|
|
24
26
|
},
|
|
25
|
-
webServer: {
|
|
26
|
-
command: run.app.start,
|
|
27
|
-
url: run.app.url,
|
|
28
|
-
cwd: run.root,
|
|
29
|
-
reuseExistingServer: true
|
|
30
|
-
},
|
|
31
27
|
...run.longest?.length && { projects: [{
|
|
32
28
|
name: "new",
|
|
33
29
|
grepInvert: run.longest.map(titled)
|