playthrough 0.3.0 → 0.3.1
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 +13 -11
- package/package.json +1 -1
- package/src/{flows-Dt7fcPLt.js → flows-BcISV1M-.js} +115 -10
- package/src/playthrough.js +8 -6
- package/src/playwright.config.js +1 -1
- package/src/{reporter-4C_7joIm.js → reporter-BEodHYRI.js} +16 -5
- package/src/reporter.js +1 -1
- package/src/test.js +6 -2
- package/types/cli/src/reporter.d.ts +4 -0
- package/types/page/src/found.d.ts +4 -2
- package/types/review/src/rules.d.ts +1 -0
- package/viewer/assets/index-D64VrgNj.js +193 -0
- package/viewer/index.html +1 -1
- package/viewer/assets/index-Bbi9sjtm.js +0 -193
package/INSTRUCTIONS.md
CHANGED
|
@@ -20,13 +20,14 @@ THE LOOP
|
|
|
20
20
|
name. Once the work is done, add a flow for each choice you made that the person can see and didn't ask for.
|
|
21
21
|
4. playthrough record <review> records the round and compares it with every round before it. In the round's round.json,
|
|
22
22
|
say why each change since the round before is there, under "accounts" by its id: { "line": "<why, in your words>" },
|
|
23
|
-
{ "note": "<the note it was made for>" }, or { "notReal": "<why nothing really changed>" }.
|
|
23
|
+
{ "note": "<the id of the note it was made for>" }, or { "notReal": "<why nothing really changed>" }.
|
|
24
24
|
5. playthrough wait <review> hands the round over and waits for the notes, which can take hours: run it in the
|
|
25
25
|
background, so you're told when it ends. Each note comes with a picture of what it's on, or what the person did in
|
|
26
|
-
the app. Change the app, record the next round, and answer each note in its round.json, under "replies"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
and
|
|
26
|
+
the app. Change the app, record the next round, and answer each note in its round.json, under "replies", in its
|
|
27
|
+
"words": its "on" is where Playthrough found the note's thing on this round, so leave it. Where the answer moves,
|
|
28
|
+
like an animation, add "watch": "<the title of the step it's seen in>". For a note made in the app, record walks the
|
|
29
|
+
person's steps again and puts your answer where they lead: answer it in words, not with a new flow, and check your
|
|
30
|
+
fix with playthrough walk <review>. Where their steps stop short, fix that, or say why they changed.
|
|
30
31
|
|
|
31
32
|
The person can only judge what the flows show, so keep them true to how a person uses the app.
|
|
32
33
|
|
|
@@ -91,15 +92,16 @@ tells tests by (the screen, language and time zone are Playthrough's):
|
|
|
91
92
|
|
|
92
93
|
export async function after({ request, setup }: { request: APIRequestContext; baseURL: string; setup: Record<string, any> }) {
|
|
93
94
|
const removed = await request.delete(`/api/test/accounts/${setup.account}`);
|
|
94
|
-
if (!removed.ok()) throw new Error(`Removing the account failed: ${removed.status()}`);
|
|
95
|
+
if (!removed.ok() && removed.status() !== 404) throw new Error(`Removing the account failed: ${removed.status()}`);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
Each run makes its own: what the app needs unique, like an email, of its own, and what the page shows, like a name, the
|
|
98
|
-
same every run. Check what each request answers, so one that fails says so.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
same every run. Check what each request answers, so one that fails says so. An after that fails is tried again whole by
|
|
100
|
+
the next command, so it takes what's gone already as given back. Avoid what can't be given back, like an email sent:
|
|
101
|
+
test keys, a mail catcher. Use what the project has (a test account, seed data, its own fixtures), make data through the
|
|
102
|
+
API or database rather than on screen, and stub what you can't control at the network. Sign in with page.request, not
|
|
103
|
+
request: it shares the page's cookies, so a person who takes the flow over is signed in too. Dates the server works out
|
|
104
|
+
don't follow the page's clock: fix them through the app's own test hook if it has one.
|
|
103
105
|
|
|
104
106
|
Whenever app.json or setup.ts changes, Playthrough tries the setup by itself first, twice for each of the app's people,
|
|
105
107
|
and shows a picture of where each starts. Fix what the pictures show and run the command again. It's the person's app:
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as Refused, M as list, N as quote, h as glimpsesOf, m as firstLine, w as Reviews, y as readingsOf } from "./reporter-BEodHYRI.js";
|
|
2
2
|
import * as modules from "node:module";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { existsSync, watch } from "node:fs";
|
|
@@ -268,7 +268,7 @@ async function giveBack(after, file, baseURL, setup) {
|
|
|
268
268
|
await rm(file, { force: true });
|
|
269
269
|
return true;
|
|
270
270
|
} catch (error) {
|
|
271
|
-
console.log(`playthrough/setup.ts's after couldn't give back what a run made: ${firstLine(String(error?.message ?? error))}.
|
|
271
|
+
console.log(`playthrough/setup.ts's after couldn't give back what a run made: ${firstLine(String(error?.message ?? error))}. The next command tries it again, whole, so it should take what's gone already as given back.`);
|
|
272
272
|
return false;
|
|
273
273
|
} finally {
|
|
274
274
|
giving.delete(file);
|
|
@@ -730,7 +730,7 @@ function placeIn(place, from, to) {
|
|
|
730
730
|
};
|
|
731
731
|
if (place.point) return {
|
|
732
732
|
...moment,
|
|
733
|
-
point: place.point
|
|
733
|
+
point: besideOn(place.point, place.at, from, to) ?? place.point
|
|
734
734
|
};
|
|
735
735
|
const there = (element) => {
|
|
736
736
|
const found = foundIn(from.readings, element, place.at, to.readings);
|
|
@@ -744,6 +744,36 @@ function placeIn(place, from, to) {
|
|
|
744
744
|
...alike?.length && { alike }
|
|
745
745
|
} : moment;
|
|
746
746
|
}
|
|
747
|
+
const HOLDING = /^(dialog|alertdialog|group|radiogroup|table|grid|treegrid|listbox|menu)$/;
|
|
748
|
+
function besideIn(read, at, point) {
|
|
749
|
+
const reading = read.reduce((best, next) => !best || Math.abs(next.at - at) < Math.abs(best.at - at) ? next : best, void 0);
|
|
750
|
+
if (!reading) return void 0;
|
|
751
|
+
const x = point.x * reading.screen.width;
|
|
752
|
+
const y = point.y * reading.screen.height;
|
|
753
|
+
const inside = (thing) => x >= thing.box.x && x <= thing.box.x + thing.box.width && y >= thing.box.y && y <= thing.box.y + thing.box.height;
|
|
754
|
+
const area = (thing) => thing.box.width * thing.box.height;
|
|
755
|
+
const room = reading.things.filter((thing) => (thing.role === "box" || HOLDING.test(thing.role) && !/^(table|grid|treegrid)$/.test(thing.role)) && inside(thing)).sort((a, b) => area(a) - area(b))[0];
|
|
756
|
+
const within = (thing) => {
|
|
757
|
+
for (let holder = thing.holder; holder !== void 0; holder = reading.things[holder]?.holder) if (reading.things[holder] === room) return true;
|
|
758
|
+
return false;
|
|
759
|
+
};
|
|
760
|
+
const gap = (thing) => Math.hypot(Math.max(thing.box.x - x, 0, x - thing.box.x - thing.box.width), Math.max(thing.box.y - y, 0, y - thing.box.y - thing.box.height));
|
|
761
|
+
const thing = reading.things.filter((thing) => thing.name && thing.role !== "box" && !HOLDING.test(thing.role) && (!room || within(thing)) && gap(thing) <= 80).sort((a, b) => gap(a) - gap(b) || area(a) - area(b))[0];
|
|
762
|
+
return thing && {
|
|
763
|
+
reading,
|
|
764
|
+
thing
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function besideOn(point, at, from, to) {
|
|
768
|
+
const beside = besideIn(from.readings, at, point);
|
|
769
|
+
const found = beside && sameIn(beside, from.readings, to.readings);
|
|
770
|
+
if (!beside || !found) return void 0;
|
|
771
|
+
const share = (pixels, of) => Math.min(1, Math.max(0, pixels / of));
|
|
772
|
+
return {
|
|
773
|
+
x: share(point.x * beside.reading.screen.width + found.thing.box.x - beside.thing.box.x, found.reading.screen.width),
|
|
774
|
+
y: share(point.y * beside.reading.screen.height + found.thing.box.y - beside.thing.box.y, found.reading.screen.height)
|
|
775
|
+
};
|
|
776
|
+
}
|
|
747
777
|
function foundIn(from, element, at, to) {
|
|
748
778
|
const shown = shownAt(from, element, at);
|
|
749
779
|
return shown && sameIn(shown, from, to);
|
|
@@ -791,11 +821,13 @@ function changesAt(then, now, unsteady = /* @__PURE__ */ new Set(), shown = {
|
|
|
791
821
|
const others = own.filter((difference) => !isMove(difference));
|
|
792
822
|
const caused = grownByWhatsInside(moment, own);
|
|
793
823
|
const along = /* @__PURE__ */ new Map();
|
|
794
|
-
const
|
|
824
|
+
const items = itemsReordered(moment, own.filter(isMove));
|
|
825
|
+
const { reordered, left } = rowsReordered(moment, items.left);
|
|
795
826
|
const { shifts, loose } = layoutShifts(left, carried);
|
|
796
827
|
moment.shifts = shifts.map((shift) => shift.by);
|
|
797
828
|
const widened = widenedTogether(others, caused, along);
|
|
798
829
|
const told = [
|
|
830
|
+
...items.reordered,
|
|
799
831
|
...reordered,
|
|
800
832
|
...swappedOrMoved(moment, loose),
|
|
801
833
|
...causesOfShifts(moment, shifts, widened, others, caused, along)
|
|
@@ -907,6 +939,76 @@ function grownByWhatsInside(moment, own) {
|
|
|
907
939
|
}
|
|
908
940
|
return caused;
|
|
909
941
|
}
|
|
942
|
+
function itemsReordered(moment, moves) {
|
|
943
|
+
const { then, now } = moment;
|
|
944
|
+
const pairs = pairItems(moment);
|
|
945
|
+
const reordered = [];
|
|
946
|
+
let left = moves;
|
|
947
|
+
for (const [, paired] of groupBy([...pairs], ([, b]) => now.items[b].list)) {
|
|
948
|
+
const byThen = [...paired].sort((p, q) => p[0] - q[0]);
|
|
949
|
+
const byNow = [...paired].sort((p, q) => p[1] - q[1]);
|
|
950
|
+
const placeThen = new Map(byThen.map(([a], i) => [a, i]));
|
|
951
|
+
const kept = longestRising(byNow.map(([a]) => placeThen.get(a)));
|
|
952
|
+
const way = (a, b) => {
|
|
953
|
+
const went = moveOf(asThing(then.items[a], a), asThing(now.items[b], b));
|
|
954
|
+
return Math.abs(went.y) >= Math.abs(went.x) ? went.y > 0 ? "down" : "up" : went.x > 0 ? "right" : "left";
|
|
955
|
+
};
|
|
956
|
+
const moved = byNow.flatMap(([a, b], i) => kept.has(i) ? [] : [{
|
|
957
|
+
thing: asThing(now.items[b], b),
|
|
958
|
+
way: way(a, b),
|
|
959
|
+
places: Math.abs(i - placeThen.get(a))
|
|
960
|
+
}]);
|
|
961
|
+
if (!moved.length) continue;
|
|
962
|
+
const changed = byNow.filter(([a], i) => placeThen.get(a) !== i);
|
|
963
|
+
const first = now.items[byNow[0][1]];
|
|
964
|
+
const of = `${first.role} of ${listOf(now, first)}`;
|
|
965
|
+
const holder = first.holder === void 0 ? void 0 : now.things[first.holder];
|
|
966
|
+
if (changed.length === 2) {
|
|
967
|
+
const both = changed.map(([, b]) => asThing(now.items[b], b));
|
|
968
|
+
reordered.push({
|
|
969
|
+
what: "swapped",
|
|
970
|
+
thing: both[0],
|
|
971
|
+
in: now,
|
|
972
|
+
words: `${together(both)} have swapped places`,
|
|
973
|
+
key: `${of}: ${both.map((thing) => thing.words).join(" / ")}`
|
|
974
|
+
});
|
|
975
|
+
} else if (moved.length <= 2) for (const { thing, way, places } of moved) reordered.push({
|
|
976
|
+
what: "moved",
|
|
977
|
+
thing,
|
|
978
|
+
in: now,
|
|
979
|
+
words: `${called$1(thing)} moved ${way} ${places} ${places > 1 ? "places" : "place"}`,
|
|
980
|
+
key: `${of}: ${thing.words}`
|
|
981
|
+
});
|
|
982
|
+
else {
|
|
983
|
+
const order = byNow.slice(0, 3).map(([, b]) => called$1(asThing(now.items[b], b)));
|
|
984
|
+
const words = first.role === "row" ? `Rows in ${holder ? the(holder) : "the table"} are in a new order` : `What's in ${holder ? the(holder) : "the list"} is in a new order: ${order.join(", ")}${byNow.length > 3 ? "…" : ""}`;
|
|
985
|
+
reordered.push({
|
|
986
|
+
what: "reordered",
|
|
987
|
+
thing: holder ?? moved[0].thing,
|
|
988
|
+
in: now,
|
|
989
|
+
words,
|
|
990
|
+
key: of
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
const inList = new Set(paired.map(([, b]) => b));
|
|
994
|
+
left = left.filter((move) => !(move.now.item !== void 0 && inList.has(move.now.item) && move.then.item !== void 0 && pairs.get(move.then.item) === move.now.item));
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
reordered,
|
|
998
|
+
left
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function longestRising(sequence) {
|
|
1002
|
+
const best = sequence.map(() => 1);
|
|
1003
|
+
const before = sequence.map(() => -1);
|
|
1004
|
+
for (let i = 0; i < sequence.length; i++) for (let j = 0; j < i; j++) if (sequence[j] < sequence[i] && best[j] + 1 > best[i]) {
|
|
1005
|
+
best[i] = best[j] + 1;
|
|
1006
|
+
before[i] = j;
|
|
1007
|
+
}
|
|
1008
|
+
const kept = /* @__PURE__ */ new Set();
|
|
1009
|
+
for (let i = best.indexOf(Math.max(...best)); i >= 0; i = before[i]) kept.add(i);
|
|
1010
|
+
return kept;
|
|
1011
|
+
}
|
|
910
1012
|
function rowsReordered(moment, moves) {
|
|
911
1013
|
const reordered = [];
|
|
912
1014
|
let left = moves;
|
|
@@ -1229,8 +1331,10 @@ function identityOf({ in: reading, thing, what }, moment) {
|
|
|
1229
1331
|
if (!item) return identity(reading, thing);
|
|
1230
1332
|
const kind = reading.things.filter((other) => other.item === thing.item && other.role === thing.role && other.column === thing.column);
|
|
1231
1333
|
const place = `${thing.role} ${kind.indexOf(thing) + 1}${thing.column ? ` under ${thing.column}` : ""}`;
|
|
1232
|
-
const was = what
|
|
1233
|
-
|
|
1334
|
+
const was = /^(reworded|moved)$/.test(what) ? moment.was.get(thing) : void 0;
|
|
1335
|
+
const went = was && what === "moved" && moveOf(was, thing);
|
|
1336
|
+
const how = went ? `, ${wayOf(near(went.x, 0) ? 0 : went.x, near(went.y, 0) ? 0 : went.y)}` : was ? `: ${shapeOf(was.words, thing.words)}` : "";
|
|
1337
|
+
return `${place} in a ${item.role} of ${listOf(reading, item)}${how}`;
|
|
1234
1338
|
}
|
|
1235
1339
|
const listOf = (reading, item) => item.holder === void 0 ? "the page" : identity(reading, reading.things[item.holder], 0);
|
|
1236
1340
|
function pairItems({ then, now, was }) {
|
|
@@ -1455,9 +1559,10 @@ async function picturesAt(page, then, now, pictures) {
|
|
|
1455
1559
|
if (patch.nudged && found.length) continue;
|
|
1456
1560
|
if (patch.moved && [...changedInside].some((holder) => contains(holder.box, patch, 8))) continue;
|
|
1457
1561
|
const on = whatsThere(now, patch);
|
|
1458
|
-
const
|
|
1459
|
-
const
|
|
1460
|
-
const
|
|
1562
|
+
const moved = on.identity && !on.identity.startsWith("near ") ? void 0 : patch.moved;
|
|
1563
|
+
const by = moved && Math.max(Math.abs(moved.x), Math.abs(moved.y));
|
|
1564
|
+
const words = moved ? `${on.called} moved ${wayOf(moved.x, moved.y)}, ${by}px` : `${on.called} looks different`;
|
|
1565
|
+
const id = idOf(on.identity ?? `place:${Math.round(patch.x / 32)},${Math.round(patch.y / 32)}`, moved ? "moved" : "looks different");
|
|
1461
1566
|
const same = told.get(id);
|
|
1462
1567
|
told.set(id, {
|
|
1463
1568
|
words,
|
|
@@ -1881,11 +1986,11 @@ async function trySetup(repo, again) {
|
|
|
1881
1986
|
for (const { who, did, took, at, refused, picture } of tried) console.log(line(who, `${did}${did === "nothing made" ? "" : ` in ${seconds(took)}`}, then at ${at}${refused ? `, which the app answered with ${refused}` : ""}: ${repo.show(picture)}`));
|
|
1882
1987
|
const missing = [];
|
|
1883
1988
|
if (!setup.after && tried.some(({ made }) => made)) missing.push("playthrough/setup.ts has no after, so what each run makes stays in the app: one for each flow recorded, two for a new one, and one each time the person pauses a recording, as it's made ready to take over.");
|
|
1884
|
-
else if (tried.some(({ givenBack }) => !givenBack)) missing.push("playthrough/setup.ts's after couldn't give back what it made, for the reason above.");
|
|
1885
1989
|
const slowest = Math.max(...tried.map(({ took }) => took));
|
|
1886
1990
|
if (slowest > SLOW) missing.push(`Setup took ${seconds(slowest)}: each flow waits that long before its first step, and so does each moment taken over.`);
|
|
1887
1991
|
console.log(`
|
|
1888
1992
|
Look at ${tried.length > 1 ? "each picture: it's where their flows start" : "the picture: it's where every flow starts"}. One that shows a sign-in page for someone who should be signed in, or anything a flow shouldn't start from, is for playthrough/setup.ts.`);
|
|
1993
|
+
if (tried.some(({ givenBack }) => !givenBack)) console.log(`\nplaythrough/setup.ts's after couldn't give back what it made, for the reason above: that's for setup.ts too.`);
|
|
1889
1994
|
if (missing.length) console.log(`
|
|
1890
1995
|
${missing.map((said) => `- ${said}`).join("\n")}
|
|
1891
1996
|
|
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
|
|
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-
|
|
2
|
+
import { A as problemsFinishing, C as Refused, D as inOrderJoined, E as conversation, M as list, N as quote, O as lastSeen, P as time, T as allNotes, _ as onPage, b as readingsOn, g as hashOf, j as roundInProgress, k as notesToAnswer, m as firstLine } from "./reporter-BEodHYRI.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-BcISV1M-.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";
|
|
@@ -239,9 +239,9 @@ function refuseIfBroken(failed, unrecorded, again) {
|
|
|
239
239
|
}
|
|
240
240
|
const cantStart = (recorded, failed) => failed.map(({ flow }) => flow).filter((flow) => !recorded.some((kept) => kept.flow.name === flow));
|
|
241
241
|
function howFar(repo, id, recorded, failed, app) {
|
|
242
|
-
const lines = failed.map(({ flow, at, why }) => {
|
|
242
|
+
const lines = failed.map(({ flow, at, why, on }) => {
|
|
243
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.`;
|
|
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)}${onPage(on)}${because}` : "stops after its last step"}, so its Before goes as far as that.`;
|
|
245
245
|
});
|
|
246
246
|
const where = existsSync(repo.failed(id)) ? ` ${whereItStopped(repo, id, failed)}` : "";
|
|
247
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}`);
|
|
@@ -1531,13 +1531,15 @@ async function record(repo, id) {
|
|
|
1531
1531
|
]);
|
|
1532
1532
|
await recordOnBefore(repo, id, number, recorded.map(({ flow }) => flow.name));
|
|
1533
1533
|
const saved = review.rounds.find((round) => round.number === number);
|
|
1534
|
+
const joined = inOrderJoined(review);
|
|
1535
|
+
const place = (name) => joined.includes(name) ? joined.indexOf(name) : joined.length;
|
|
1534
1536
|
const flows = recorded.map(({ flow }) => {
|
|
1535
1537
|
const line = saved?.flows.find((kept) => kept.name === flow.name)?.line;
|
|
1536
1538
|
return line ? {
|
|
1537
1539
|
...flow,
|
|
1538
1540
|
line
|
|
1539
1541
|
} : flow;
|
|
1540
|
-
});
|
|
1542
|
+
}).sort((a, b) => place(a.name) - place(b.name));
|
|
1541
1543
|
for (const { files } of recorded) for (const { name, bytes } of files) await repo.reviews.saveFile(id, number, name, bytes);
|
|
1542
1544
|
const written = saved?.replies ?? [];
|
|
1543
1545
|
const waiting = notesToAnswer(review, number).filter((note) => !written.some((reply) => reply.note === note)).map((note) => ({
|
|
@@ -1567,7 +1569,7 @@ async function record(repo, id) {
|
|
|
1567
1569
|
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
1570
|
if (walked.length) console.log(`\n${walked.map(saidOf).join("\n")}`);
|
|
1569
1571
|
if (left.length) console.log(`\nStill to do, in ${file}:\n${left.map((problem) => ` ${problem}`).join("\n")}`);
|
|
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>" }.`);
|
|
1572
|
+
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 id of the note it was made for>" }, or { "notReal": "<why nothing really changed>" }.`);
|
|
1571
1573
|
if (unsaid.length) {
|
|
1572
1574
|
const [are, each] = unsaid.length > 1 ? ["are", "each"] : ["is", "it"];
|
|
1573
1575
|
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.`);
|
package/src/playwright.config.js
CHANGED
|
@@ -84,7 +84,7 @@ const Comparison = z.strictObject({
|
|
|
84
84
|
});
|
|
85
85
|
const Account = z.union([
|
|
86
86
|
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"),
|
|
87
|
+
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
88
|
z.strictObject({ notReal: text("needs to say why nothing really changed") }).describe("Nothing really changed; nobody is shown it")
|
|
89
89
|
], { error: "says one of { line }, { note } or { notReal }" });
|
|
90
90
|
const Reply = z.strictObject({
|
|
@@ -153,6 +153,9 @@ function roundInProgress(review) {
|
|
|
153
153
|
if (!last) return 0;
|
|
154
154
|
return last.finished ? last.number + 1 : last.number;
|
|
155
155
|
}
|
|
156
|
+
function inOrderJoined(review) {
|
|
157
|
+
return [...new Set(review.rounds.flatMap((kept) => [...kept.flows.map((flow) => flow.name), ...kept.cantStart ?? []]))];
|
|
158
|
+
}
|
|
156
159
|
function allNotes(review) {
|
|
157
160
|
const notes = /* @__PURE__ */ new Map();
|
|
158
161
|
for (const send of review.sends) for (const note of send.notes) notes.set(note.id, {
|
|
@@ -335,7 +338,7 @@ function problemsFinishing(review, number) {
|
|
|
335
338
|
const comparisons = review.comparisons.filter((comparison) => comparison.round === number);
|
|
336
339
|
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
340
|
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)?.changes ?? []) if (!round.accounts[change.id]) problems.push(`${at}: say why ${change.id} is there (${change.words}${where(change)}): its line, the note it was made for, or why nothing really changed.`);
|
|
341
|
+
for (const change of comparisons.find((comparison) => comparison.since === number - 1)?.changes ?? []) 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
342
|
for (const [id, account] of Object.entries(round.accounts)) {
|
|
340
343
|
const change = changes.get(id);
|
|
341
344
|
if (!change) problems.push(`${at} says something about the change ${id}, which isn't one of this round's changes.`);
|
|
@@ -731,6 +734,7 @@ const HANDS = "playthrough hands";
|
|
|
731
734
|
const ACTIONS = "playthrough actions";
|
|
732
735
|
const WROTE = "playthrough wrote";
|
|
733
736
|
const UNMET = "playthrough unmet";
|
|
737
|
+
const STOPPED_ON = "playthrough stopped on";
|
|
734
738
|
const HOLD = 1500;
|
|
735
739
|
const readingsOf = (flow) => `readings/${flow.recording}`;
|
|
736
740
|
async function readingsOn(reviews, review, round, flow) {
|
|
@@ -748,6 +752,11 @@ function firstLine(said) {
|
|
|
748
752
|
if (found.length) line = `${line} ${found.slice(0, 2).join(", ")}${found.length > 2 ? ", …" : ""}`;
|
|
749
753
|
return line.length > 300 ? `${line.slice(0, 300)}…` : line;
|
|
750
754
|
}
|
|
755
|
+
const pathOf = (address) => {
|
|
756
|
+
const { pathname, search } = new URL(address);
|
|
757
|
+
return pathname + search;
|
|
758
|
+
};
|
|
759
|
+
const onPage = (on) => on ? `, on ${on}` : "";
|
|
751
760
|
const BROKEN = /Page crashed|Target crashed|Target page, context or browser has been closed|net::ERR_CONNECTION_REFUSED/;
|
|
752
761
|
const hashOf = (file) => {
|
|
753
762
|
try {
|
|
@@ -809,11 +818,13 @@ var Flows = class {
|
|
|
809
818
|
const said = (stop?.error ?? result.error)?.message ?? "";
|
|
810
819
|
const broken = BROKEN.test(said);
|
|
811
820
|
const why = firstLine(said);
|
|
821
|
+
const on = result.attachments.find((attachment) => attachment.name === STOPPED_ON)?.body?.toString();
|
|
812
822
|
const failed = {
|
|
813
823
|
flow: test.title,
|
|
814
824
|
...stop?.error && { at: stop.title },
|
|
815
825
|
...why && { why },
|
|
816
|
-
...broken && { broken }
|
|
826
|
+
...broken && { broken },
|
|
827
|
+
...stop?.error && on && { on }
|
|
817
828
|
};
|
|
818
829
|
if (!this.#before || broken || !recording || !stop || !kept.length) {
|
|
819
830
|
this.#ran.push({
|
|
@@ -830,7 +841,7 @@ var Flows = class {
|
|
|
830
841
|
failed,
|
|
831
842
|
recorded
|
|
832
843
|
});
|
|
833
|
-
went(`${stop.error ? `stops at ${quote(stop.title)}` : "stops after its last step"}${yet}`);
|
|
844
|
+
went(`${stop.error ? `stops at ${quote(stop.title)}${onPage(failed.on)}` : "stops after its last step"}${yet}`);
|
|
834
845
|
}
|
|
835
846
|
onError(error) {
|
|
836
847
|
console.log(`\n${error.message ?? error.value}`);
|
|
@@ -916,4 +927,4 @@ var Flows = class {
|
|
|
916
927
|
}
|
|
917
928
|
};
|
|
918
929
|
const seconds = (ms) => Math.round(Math.max(0, ms)) / 1e3;
|
|
919
|
-
export {
|
|
930
|
+
export { problemsFinishing as A, Refused as C, inOrderJoined as D, conversation as E, list as M, quote as N, lastSeen as O, time as P, keepAssets as S, allNotes as T, onPage as _, HANDS as a, readingsOn as b, PICTURE as c, STOPPED_ON as d, UNMET as f, hashOf as g, glimpsesOf as h, GLIMPSES as i, roundInProgress as j, notesToAnswer as k, READINGS as l, firstLine as m, ASSETS as n, HOLD as o, WROTE as p, Flows as r, PERSON as s, ACTIONS as t, RECORDING as u, pathOf as v, Reviews as w, seconds as x, readingsOf as y };
|
package/src/reporter.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as ACTIONS, n as ASSETS, i as GLIMPSES, a as HANDS, o as HOLD, s as PERSON, c as PICTURE, l as READINGS, u as RECORDING, d as
|
|
1
|
+
export { t as ACTIONS, n as ASSETS, i as GLIMPSES, a as HANDS, o as HOLD, s as PERSON, c as PICTURE, l as READINGS, u as RECORDING, d as STOPPED_ON, f as UNMET, p as WROTE, r as default, m as firstLine, h as glimpsesOf, g as hashOf, _ as onPage, v as pathOf, y as readingsOf, b as readingsOn, x as seconds } from "./reporter-BEodHYRI.js";
|
package/src/test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as HANDS, c as PICTURE, d as
|
|
2
|
-
import { C as make, E as repo, T as setupIn, b as changes, n as REACHES_OUTSIDE, r as WALKED, t as ALONGSIDE, w as personIn, x as firstOf } from "./flows-
|
|
1
|
+
import { S as keepAssets, a as HANDS, c as PICTURE, d as STOPPED_ON, f as UNMET, i as GLIMPSES, l as READINGS, m as firstLine, n as ASSETS, o as HOLD, p as WROTE, s as PERSON, t as ACTIONS, u as RECORDING, v as pathOf, x as seconds } from "./reporter-BEodHYRI.js";
|
|
2
|
+
import { C as make, E as repo, T as setupIn, b as changes, n as REACHES_OUTSIDE, r as WALKED, t as ALONGSIDE, w as personIn, x as firstOf } from "./flows-BcISV1M-.js";
|
|
3
3
|
import { n as readPage, r as unhovered } from "./read-Bg9XVTtn.js";
|
|
4
4
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
@@ -264,6 +264,10 @@ const recorded = test$1.extend({
|
|
|
264
264
|
body: String(flow.unmet),
|
|
265
265
|
contentType: "text/plain"
|
|
266
266
|
});
|
|
267
|
+
if (!ended && last.url() !== "about:blank") await testInfo.attach(STOPPED_ON, {
|
|
268
|
+
body: pathOf(last.url()),
|
|
269
|
+
contentType: "text/plain"
|
|
270
|
+
});
|
|
267
271
|
for (const picture of pictures) await testInfo.attach(PICTURE, {
|
|
268
272
|
body: picture,
|
|
269
273
|
contentType: "image/png"
|
|
@@ -11,6 +11,7 @@ export declare const HANDS = "playthrough hands";
|
|
|
11
11
|
export declare const ACTIONS = "playthrough actions";
|
|
12
12
|
export declare const WROTE = "playthrough wrote";
|
|
13
13
|
export declare const UNMET = "playthrough unmet";
|
|
14
|
+
export declare const STOPPED_ON = "playthrough stopped on";
|
|
14
15
|
export declare const HOLD = 1500;
|
|
15
16
|
export declare const readingsOf: (flow: Flow) => string;
|
|
16
17
|
export declare function readingsOn(reviews: Reviews, review: string, round: number, flow: Flow): Promise<Reading[]>;
|
|
@@ -21,7 +22,10 @@ export type Failed = {
|
|
|
21
22
|
at?: string;
|
|
22
23
|
why?: string;
|
|
23
24
|
broken?: true;
|
|
25
|
+
on?: string;
|
|
24
26
|
};
|
|
27
|
+
export declare const pathOf: (address: string) => string;
|
|
28
|
+
export declare const onPage: (on?: string) => string;
|
|
25
29
|
export type Recorded = {
|
|
26
30
|
flow: Flow;
|
|
27
31
|
files: string[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Element, type Flow, type Place } from "../../review/src/index.js";
|
|
2
|
-
import type { Reading, Thing } from "./reading.js";
|
|
1
|
+
import { type Element, type Flow, type Place, type Point } from "../../review/src/index.js";
|
|
2
|
+
import type { Glimpse, Reading, Thing } from "./reading.js";
|
|
3
3
|
export type Recorded = {
|
|
4
4
|
flow: Flow;
|
|
5
5
|
readings: Reading[];
|
|
@@ -9,5 +9,7 @@ export type Shown = {
|
|
|
9
9
|
thing: Thing;
|
|
10
10
|
};
|
|
11
11
|
export declare function placeIn(place: Place, from: Recorded, to: Recorded): Place;
|
|
12
|
+
export declare const HOLDING: RegExp;
|
|
13
|
+
export declare const nearTo: (read: Glimpse[], at: number, point: Point) => Thing | undefined;
|
|
12
14
|
export declare function shownAt(readings: Reading[], element: Element, at: number): Shown | undefined;
|
|
13
15
|
export declare const elementOf: (thing: Thing, name: string) => Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Comparison, Round, Send, type Account, type Flow, type Note, type Place, type Review, type Verdict } from "./model.js";
|
|
2
2
|
export declare function roundInProgress(review: Review): number;
|
|
3
|
+
export declare function inOrderJoined(review: Review): string[];
|
|
3
4
|
export declare function allNotes(review: Review): Map<string, {
|
|
4
5
|
note: Note;
|
|
5
6
|
round: number;
|