@fungi.computer/bonsai 0.0.0-stage → 0.1.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/CHANGELOG.md +43 -0
- package/LICENSE +21 -0
- package/README.md +196 -2
- package/backend/background.ts +259 -0
- package/backend/change-copy.ts +99 -0
- package/backend/changes.ts +530 -0
- package/backend/diff.ts +228 -0
- package/backend/file-edits.ts +850 -0
- package/backend/files-copy.ts +361 -0
- package/backend/gills.ts +125 -0
- package/backend/git.ts +123 -0
- package/backend/graph-commit-copies.ts +321 -0
- package/backend/history.ts +178 -0
- package/backend/index.ts +144 -0
- package/backend/insights.ts +256 -0
- package/backend/merge.ts +999 -0
- package/backend/methods.ts +848 -0
- package/backend/policy.ts +160 -0
- package/backend/read-copy-page.ts +77 -0
- package/backend/repository-copy.ts +70 -0
- package/backend/reviews.ts +317 -0
- package/backend/service.ts +892 -0
- package/backend/state.ts +914 -0
- package/demo/koi-pond/README.md +17 -0
- package/demo/koi-pond/index.html +11 -0
- package/demo/koi-pond/package.json +13 -0
- package/demo/koi-pond/pnpm-lock.yaml +450 -0
- package/demo/koi-pond/src/fish.ts +32 -0
- package/demo/koi-pond/src/main.ts +24 -0
- package/demo/koi-pond/src/pond.ts +27 -0
- package/docs/media/bonsai-insights.png +0 -0
- package/docs/media/overview-phone.png +0 -0
- package/docs/media/overview.png +0 -0
- package/package.json +152 -3
- package/scripts/agent.sh +55 -0
- package/scripts/demo.sh +233 -0
- package/scripts/rebuild-copies.mjs +34 -0
- package/scripts/release.mjs +52 -0
- package/scripts/runtime-proof.mjs +299 -0
- package/scripts/seed-demo.mjs +724 -0
- package/scripts/seed-step.mjs +37 -0
- package/scripts/shed-terminal-proof.mjs +774 -0
- package/shared/contract.ts +404 -0
- package/shared/hooks.ts +208 -0
- package/shared/insights.ts +90 -0
- package/shared/picker-options.ts +23 -0
- package/shared/presentation.ts +21 -0
- package/shared/repository-deletion.ts +4 -0
- package/test/apps-build-install.test.ts +302 -0
- package/test/background-runtime.test.ts +206 -0
- package/test/browser/board.browser.mjs +52 -0
- package/test/browser/capture.browser.mjs +1264 -0
- package/test/browser/edit-in-inkcap.browser.mjs +83 -0
- package/test/browser/repository-settings-fixture.tsx +108 -0
- package/test/browser/repository-settings.browser.mjs +136 -0
- package/test/browser/repository-settings.html +10 -0
- package/test/browser/visual-fixture.tsx +667 -0
- package/test/browser/visual.html +10 -0
- package/test/browser/vite.config.mjs +9 -0
- package/test/change-page.test.ts +916 -0
- package/test/changes.test.ts +1063 -0
- package/test/checks-workflow-template.test.mjs +234 -0
- package/test/diff.test.ts +70 -0
- package/test/edit-in-inkcap.test.ts +227 -0
- package/test/file-edits.test.ts +2714 -0
- package/test/files-read-copy-calls.test.ts +374 -0
- package/test/fork-adoption.test.ts +111 -0
- package/test/gills.test.ts +125 -0
- package/test/git-binding.fixture.ts +30 -0
- package/test/git-push.config.jsonc +9 -0
- package/test/git-push.native.mjs +413 -0
- package/test/git-push.worker.ts +280 -0
- package/test/graph-commit-read-copy-calls.test.ts +401 -0
- package/test/home-page.test.ts +170 -0
- package/test/hooks.test.ts +250 -0
- package/test/insight-data.test.ts +51 -0
- package/test/insight-layout.test.mjs +230 -0
- package/test/insights.test.ts +154 -0
- package/test/merge-background.test.ts +756 -0
- package/test/merge-conflict.test.ts +337 -0
- package/test/merge-declaration.test.ts +46 -0
- package/test/merge-repro.test.ts +561 -0
- package/test/merge-template-build.test.mjs +234 -0
- package/test/network-layout.test.ts +200 -0
- package/test/outbox.test.ts +424 -0
- package/test/parts.test.ts +86 -0
- package/test/pkt-line.test.ts +54 -0
- package/test/policy.test.ts +172 -0
- package/test/present.test.ts +27 -0
- package/test/read-copy-calls.test.ts +691 -0
- package/test/read-copy-composer.test.ts +388 -0
- package/test/read-copy-lifetimes.test.ts +214 -0
- package/test/read-copy-rpc.config.jsonc +12 -0
- package/test/read-copy-rpc.native.mjs +47 -0
- package/test/read-copy-rpc.worker.ts +92 -0
- package/test/recent-activity.test.ts +62 -0
- package/test/repository-deletion.test.ts +500 -0
- package/test/seed-resume.test.mjs +91 -0
- package/test/storyboard-save-https-git.fixture.mjs +119 -0
- package/test/storyboard-save-template.test.mjs +545 -0
- package/test/ui-errors.test.ts +976 -0
- package/test/workflow-store.test.ts +115 -0
- package/tsconfig.json +36 -0
- package/turbo.jsonc +25 -0
- package/vite.config.ts +19 -0
- package/vitest.config.ts +47 -0
- package/web/api.ts +224 -0
- package/web/app-build-panel.tsx +171 -0
- package/web/board.tsx +259 -0
- package/web/bonsai.css +642 -0
- package/web/change-page.tsx +719 -0
- package/web/change-rows.tsx +135 -0
- package/web/code-tab.tsx +267 -0
- package/web/commit-tree.tsx +152 -0
- package/web/format.ts +54 -0
- package/web/home-page.tsx +274 -0
- package/web/index.html +17 -0
- package/web/insight-data.ts +76 -0
- package/web/insight-tiles.tsx +534 -0
- package/web/insights.tsx +250 -0
- package/web/issues-panel.tsx +305 -0
- package/web/main.tsx +72 -0
- package/web/network-graph.tsx +647 -0
- package/web/overview.tsx +339 -0
- package/web/pages.tsx +469 -0
- package/web/parts.tsx +381 -0
- package/web/public/favicon.svg +1 -0
- package/web/repo-navigation.tsx +256 -0
- package/web/repo-settings.tsx +145 -0
- package/web/router.tsx +151 -0
- package/web/transport.ts +91 -0
- package/web/views.tsx +151 -0
- package/web/working-now.tsx +98 -0
|
@@ -0,0 +1,1264 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import console from "node:console";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { chromium } from "playwright";
|
|
6
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
|
+
const phase = process.argv[2] ?? "before";
|
|
8
|
+
const output = new URL(
|
|
9
|
+
`../../../../.botanical/bonsai-shots/${phase}/`,
|
|
10
|
+
import.meta.url,
|
|
11
|
+
);
|
|
12
|
+
await mkdir(output, { recursive: true });
|
|
13
|
+
const browser = await chromium.launch({ headless: true });
|
|
14
|
+
try {
|
|
15
|
+
if (phase.startsWith("scene5-")) {
|
|
16
|
+
const origin = process.argv[3] ?? "http://127.0.0.1:5188";
|
|
17
|
+
const findings = [];
|
|
18
|
+
const railFindings = [];
|
|
19
|
+
const railMeasurements = [];
|
|
20
|
+
const measurements = [];
|
|
21
|
+
for (const width of [1440, 390]) {
|
|
22
|
+
const page = await browser.newPage({
|
|
23
|
+
viewport: { width, height: width === 390 ? 844 : 900 },
|
|
24
|
+
});
|
|
25
|
+
await page.clock.install();
|
|
26
|
+
await page.goto(`${origin}/test/browser/visual.html?tab=changes&scene5`);
|
|
27
|
+
await page.locator('[data-tile="changes"]').waitFor();
|
|
28
|
+
assert.equal(
|
|
29
|
+
await page.locator(".caps-copy-field code").first().textContent(),
|
|
30
|
+
"https://oyster.shiit.app/git/bonsai/koi-pond.git",
|
|
31
|
+
"capture serves this branch's scene5 fixture",
|
|
32
|
+
);
|
|
33
|
+
await page.evaluate(() => globalThis.document.fonts.ready);
|
|
34
|
+
await page.clock.runFor(500);
|
|
35
|
+
const measured = await page.evaluate(() => {
|
|
36
|
+
const nav = globalThis.document.querySelector(
|
|
37
|
+
'[aria-label="Repository sections"]',
|
|
38
|
+
);
|
|
39
|
+
const links = [...nav.querySelectorAll("a")];
|
|
40
|
+
const changes = links.find((link) =>
|
|
41
|
+
link.textContent.startsWith("Changes"),
|
|
42
|
+
);
|
|
43
|
+
const badge = changes.querySelector(".badge");
|
|
44
|
+
const label = globalThis.document.createRange();
|
|
45
|
+
label.selectNodeContents(changes.firstChild);
|
|
46
|
+
const overview = links.find((link) => link.textContent === "Overview");
|
|
47
|
+
const railBounds = nav.getBoundingClientRect();
|
|
48
|
+
const firstVisible = links.find(
|
|
49
|
+
(link) => link.getBoundingClientRect().right > railBounds.left + 1,
|
|
50
|
+
);
|
|
51
|
+
const clone = [
|
|
52
|
+
...globalThis.document.querySelectorAll(".caps-copy-field"),
|
|
53
|
+
].find((field) => field.getBoundingClientRect().width > 0);
|
|
54
|
+
const code = clone?.querySelector("code");
|
|
55
|
+
const statuses = [
|
|
56
|
+
...globalThis.document.querySelectorAll(".list-row .caps-status"),
|
|
57
|
+
];
|
|
58
|
+
const tile = globalThis.document.querySelector('[data-tile="changes"]');
|
|
59
|
+
const panel = tile.querySelector(".caps-screen") ?? tile;
|
|
60
|
+
const recent = globalThis.document.querySelector(
|
|
61
|
+
'[aria-label*="working now"], [aria-label="Recent repository pushes"]',
|
|
62
|
+
);
|
|
63
|
+
return {
|
|
64
|
+
badgeGap:
|
|
65
|
+
badge.getBoundingClientRect().left -
|
|
66
|
+
label.getBoundingClientRect().right,
|
|
67
|
+
overviewVisible:
|
|
68
|
+
overview.getBoundingClientRect().left >=
|
|
69
|
+
nav.getBoundingClientRect().left - 1,
|
|
70
|
+
leadingTabWhole:
|
|
71
|
+
firstVisible.getBoundingClientRect().left >= railBounds.left - 1,
|
|
72
|
+
cloneLines: code
|
|
73
|
+
? code.getBoundingClientRect().height /
|
|
74
|
+
Number.parseFloat(globalThis.getComputedStyle(code).lineHeight)
|
|
75
|
+
: null,
|
|
76
|
+
readyInsets: statuses.map((status) => {
|
|
77
|
+
const row = status.closest("li");
|
|
78
|
+
return (
|
|
79
|
+
row.getBoundingClientRect().right -
|
|
80
|
+
Number.parseFloat(globalThis.getComputedStyle(row).paddingRight) -
|
|
81
|
+
status.lastElementChild.getBoundingClientRect().right
|
|
82
|
+
);
|
|
83
|
+
}),
|
|
84
|
+
panelBottomBorder: panel
|
|
85
|
+
? Number.parseFloat(
|
|
86
|
+
globalThis.getComputedStyle(panel).borderBottomWidth,
|
|
87
|
+
)
|
|
88
|
+
: null,
|
|
89
|
+
recentText: recent?.textContent ?? null,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
measurements.push({ width, ...measured });
|
|
93
|
+
if (measured.badgeGap < 4)
|
|
94
|
+
findings.push(`${width}: Changes badge touches its label`);
|
|
95
|
+
if (!measured.leadingTabWhole)
|
|
96
|
+
findings.push(`${width}: leading tab is partially clipped`);
|
|
97
|
+
if (width === 1440 && measured.cloneLines > 1.1)
|
|
98
|
+
findings.push("1440: clone URL wraps mid-word");
|
|
99
|
+
if (measured.readyInsets.some((inset) => Math.abs(inset) > 2))
|
|
100
|
+
findings.push(`${width}: status is not a trailing column`);
|
|
101
|
+
if (measured.panelBottomBorder === 0)
|
|
102
|
+
findings.push(`${width}: Changes panel has no bottom rule`);
|
|
103
|
+
if (measured.recentText !== "3 pushed recently")
|
|
104
|
+
findings.push(`${width}: push activity is labelled as current work`);
|
|
105
|
+
if (width === 390) {
|
|
106
|
+
const clone = page.getByRole("button", { name: "Clone", exact: true });
|
|
107
|
+
if (await clone.locator(".caps-keycap-led").count())
|
|
108
|
+
findings.push("390: Clone has a stray status lamp");
|
|
109
|
+
}
|
|
110
|
+
await page.screenshot({
|
|
111
|
+
path: new URL(`${width}.png`, output).pathname,
|
|
112
|
+
animations: "disabled",
|
|
113
|
+
});
|
|
114
|
+
if (width === 390) {
|
|
115
|
+
const rail = page.getByRole("navigation", {
|
|
116
|
+
name: "Repository sections",
|
|
117
|
+
});
|
|
118
|
+
const viewport = rail.locator(".caps-navigation-rail-viewport");
|
|
119
|
+
const overflow = await viewport.evaluate(
|
|
120
|
+
(element) => element.scrollWidth > element.clientWidth + 1,
|
|
121
|
+
);
|
|
122
|
+
if (overflow) {
|
|
123
|
+
if (
|
|
124
|
+
!(await rail.getAttribute("data-overflow-start")) &&
|
|
125
|
+
!(await rail.getAttribute("data-overflow-end"))
|
|
126
|
+
)
|
|
127
|
+
railFindings.push(
|
|
128
|
+
"390: overflowing tabs have no scroll affordance",
|
|
129
|
+
);
|
|
130
|
+
for (const link of await rail.getByRole("link").all()) {
|
|
131
|
+
await link.focus();
|
|
132
|
+
await page.clock.runFor(500);
|
|
133
|
+
const readBounds = () =>
|
|
134
|
+
link.evaluate((element) => {
|
|
135
|
+
const bounds = element.getBoundingClientRect();
|
|
136
|
+
const scroller = element.closest(
|
|
137
|
+
".caps-navigation-rail-viewport",
|
|
138
|
+
);
|
|
139
|
+
const viewport = scroller.getBoundingClientRect();
|
|
140
|
+
return {
|
|
141
|
+
label: element.textContent,
|
|
142
|
+
left: bounds.left,
|
|
143
|
+
right: bounds.right,
|
|
144
|
+
viewportLeft: viewport.left,
|
|
145
|
+
viewportRight: viewport.right,
|
|
146
|
+
scrollLeft: scroller.scrollLeft,
|
|
147
|
+
scrollWidth: scroller.scrollWidth,
|
|
148
|
+
clientWidth: scroller.clientWidth,
|
|
149
|
+
reachable:
|
|
150
|
+
bounds.left >= viewport.left - 1 &&
|
|
151
|
+
bounds.right <= viewport.right + 1,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
let focused = await readBounds();
|
|
155
|
+
railMeasurements.push({ interaction: "focus", ...focused });
|
|
156
|
+
if (!focused.reachable) {
|
|
157
|
+
await viewport.hover();
|
|
158
|
+
await page.mouse.wheel(
|
|
159
|
+
focused.right > focused.viewportRight ? 180 : -180,
|
|
160
|
+
0,
|
|
161
|
+
);
|
|
162
|
+
await page.clock.runFor(500);
|
|
163
|
+
focused = await readBounds();
|
|
164
|
+
railMeasurements.push({
|
|
165
|
+
interaction: "horizontal wheel",
|
|
166
|
+
...focused,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (!focused.reachable)
|
|
170
|
+
railFindings.push(
|
|
171
|
+
`390: tab ${await link.innerText()} is unreachable`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (phase.endsWith("after")) {
|
|
177
|
+
await page.clock.fastForward(7 * 60_000 + 1);
|
|
178
|
+
await page.clock.runFor(100);
|
|
179
|
+
if (
|
|
180
|
+
await page
|
|
181
|
+
.getByLabel("Recent repository pushes", { exact: true })
|
|
182
|
+
.count()
|
|
183
|
+
)
|
|
184
|
+
findings.push(
|
|
185
|
+
`${width}: activity survives its window without new data`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
await page.close();
|
|
189
|
+
}
|
|
190
|
+
await writeFile(
|
|
191
|
+
new URL("measurements.json", output),
|
|
192
|
+
JSON.stringify(
|
|
193
|
+
{ measurements, findings, railFindings, railMeasurements },
|
|
194
|
+
null,
|
|
195
|
+
2,
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
assert.deepEqual(
|
|
199
|
+
railFindings,
|
|
200
|
+
[],
|
|
201
|
+
"phone tabs remain reachable with overflow affordance",
|
|
202
|
+
);
|
|
203
|
+
if (phase === "scene5-css") {
|
|
204
|
+
assert.ok(
|
|
205
|
+
measurements.find((item) => item.width === 1440).cloneLines <= 1.1,
|
|
206
|
+
"desktop clone URL remains on one line",
|
|
207
|
+
);
|
|
208
|
+
console.log(
|
|
209
|
+
`PASS clone URL + phone rail ONLY: 1440/390; ${output.pathname}`,
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
assert.deepEqual(findings, [], "scene 5 visual and activity regressions");
|
|
213
|
+
console.log(`PASS scene 5: 1440/390; ${output.pathname}`);
|
|
214
|
+
}
|
|
215
|
+
} else if (phase.startsWith("live-polish-")) {
|
|
216
|
+
const origin = process.argv[3] ?? "http://127.0.0.1:5188";
|
|
217
|
+
for (const scenario of ["overview", "home", "pending", "error"])
|
|
218
|
+
for (const width of [1440, 390])
|
|
219
|
+
for (const theme of ["light", "dark"]) {
|
|
220
|
+
const page = await browser.newPage({
|
|
221
|
+
viewport: { width, height: 1000 },
|
|
222
|
+
colorScheme: theme,
|
|
223
|
+
});
|
|
224
|
+
const errors = [];
|
|
225
|
+
page.on("pageerror", (error) => errors.push(error.message));
|
|
226
|
+
const tab = scenario === "home" ? "home" : "overview";
|
|
227
|
+
const loading = scenario === "pending" || scenario === "error";
|
|
228
|
+
await page.goto(
|
|
229
|
+
`${origin}/test/browser/visual.html?tab=${tab}${loading ? `&load=${scenario}` : ""}`,
|
|
230
|
+
);
|
|
231
|
+
await page.evaluate(
|
|
232
|
+
(t) =>
|
|
233
|
+
globalThis.document.documentElement.setAttribute("data-theme", t),
|
|
234
|
+
theme,
|
|
235
|
+
);
|
|
236
|
+
await page.locator('[data-tile="network"]').waitFor();
|
|
237
|
+
if (loading) {
|
|
238
|
+
assert.match(
|
|
239
|
+
await page.locator('[data-tile="readme"]').innerText(),
|
|
240
|
+
/Koi pond/u,
|
|
241
|
+
);
|
|
242
|
+
if (scenario === "pending")
|
|
243
|
+
assert.match(
|
|
244
|
+
await page.locator('[data-tile="network"]').innerText(),
|
|
245
|
+
/Loading network/u,
|
|
246
|
+
);
|
|
247
|
+
else
|
|
248
|
+
assert.equal(
|
|
249
|
+
await page
|
|
250
|
+
.locator('[data-tile="network"]')
|
|
251
|
+
.getByRole("button", { name: "Retry", exact: true })
|
|
252
|
+
.count(),
|
|
253
|
+
1,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (scenario === "overview") {
|
|
257
|
+
const gutter = await page
|
|
258
|
+
.locator(".bonsai-network-gutter")
|
|
259
|
+
.boundingBox();
|
|
260
|
+
const plot = await page
|
|
261
|
+
.locator(".bonsai-network-scroll")
|
|
262
|
+
.boundingBox();
|
|
263
|
+
assert.ok(
|
|
264
|
+
gutter && plot && gutter.x + gutter.width <= plot.x + 1,
|
|
265
|
+
"lane labels stay outside the plot",
|
|
266
|
+
);
|
|
267
|
+
const nav = page.getByRole("navigation", {
|
|
268
|
+
name: "Repository sections",
|
|
269
|
+
});
|
|
270
|
+
if (width === 390) {
|
|
271
|
+
const rail = await nav.boundingBox();
|
|
272
|
+
const arrange = await page
|
|
273
|
+
.getByRole("button", { name: "Arrange tiles" })
|
|
274
|
+
.boundingBox();
|
|
275
|
+
assert.ok(
|
|
276
|
+
rail && arrange && arrange.y >= rail.y + rail.height,
|
|
277
|
+
"Arrange sits below phone tabs",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
for (const name of [
|
|
281
|
+
"Overview",
|
|
282
|
+
"Code",
|
|
283
|
+
"History",
|
|
284
|
+
"Changes",
|
|
285
|
+
"Issues",
|
|
286
|
+
]) {
|
|
287
|
+
const link = nav.getByRole("link", {
|
|
288
|
+
name,
|
|
289
|
+
exact: name !== "Changes" && name !== "Issues",
|
|
290
|
+
});
|
|
291
|
+
await link.scrollIntoViewIfNeeded();
|
|
292
|
+
const box = await link.boundingBox();
|
|
293
|
+
assert.ok(
|
|
294
|
+
box && box.x >= 0 && box.x + box.width <= width,
|
|
295
|
+
`${name} reachable`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
await nav
|
|
299
|
+
.getByRole("link", { name: "Overview", exact: true })
|
|
300
|
+
.scrollIntoViewIfNeeded();
|
|
301
|
+
await page.evaluate(() => globalThis.scrollTo(0, 0));
|
|
302
|
+
}
|
|
303
|
+
assert.equal(
|
|
304
|
+
await page.evaluate(
|
|
305
|
+
() =>
|
|
306
|
+
globalThis.document.documentElement.scrollWidth <=
|
|
307
|
+
globalThis.innerWidth,
|
|
308
|
+
),
|
|
309
|
+
true,
|
|
310
|
+
"no viewport overflow",
|
|
311
|
+
);
|
|
312
|
+
assert.deepEqual(errors, []);
|
|
313
|
+
await page.screenshot({
|
|
314
|
+
path: new URL(`${String(width)}-${theme}-${scenario}.png`, output)
|
|
315
|
+
.pathname,
|
|
316
|
+
fullPage: true,
|
|
317
|
+
animations: "disabled",
|
|
318
|
+
});
|
|
319
|
+
await page.close();
|
|
320
|
+
}
|
|
321
|
+
console.log(
|
|
322
|
+
`PASS live polish: progressive loading, Retry, phone navigation and lane spacing; ${output.pathname}`,
|
|
323
|
+
);
|
|
324
|
+
} else if (/^redesign-[5-8]-/u.test(phase)) {
|
|
325
|
+
const slice = phase.split("-")[1];
|
|
326
|
+
const tab = { 5: "commits", 6: "code", 7: "home", 8: "issues" }[slice];
|
|
327
|
+
for (const width of [1440, 390])
|
|
328
|
+
for (const theme of ["light", "dark"]) {
|
|
329
|
+
const page = await browser.newPage({
|
|
330
|
+
viewport: { width, height: 1000 },
|
|
331
|
+
colorScheme: theme,
|
|
332
|
+
});
|
|
333
|
+
const errors = [];
|
|
334
|
+
page.on("pageerror", (error) => errors.push(error.message));
|
|
335
|
+
await page.goto(
|
|
336
|
+
`http://127.0.0.1:5188/test/browser/visual.html?tab=${tab}`,
|
|
337
|
+
);
|
|
338
|
+
await page.locator("main").waitFor();
|
|
339
|
+
await page.evaluate(
|
|
340
|
+
(t) =>
|
|
341
|
+
globalThis.document.documentElement.setAttribute("data-theme", t),
|
|
342
|
+
theme,
|
|
343
|
+
);
|
|
344
|
+
if (slice === "5" && phase.endsWith("after")) {
|
|
345
|
+
const rows = page.locator(".bonsai-history-commits > li");
|
|
346
|
+
await page.getByRole("tab", { name: "Agents", exact: true }).click();
|
|
347
|
+
assert.equal(await rows.count(), 30);
|
|
348
|
+
await page.getByRole("tab", { name: "People", exact: true }).click();
|
|
349
|
+
assert.equal(await rows.count(), 10);
|
|
350
|
+
await page.getByRole("tab", { name: "All", exact: true }).click();
|
|
351
|
+
assert.equal(await rows.count(), 40);
|
|
352
|
+
assert.equal(
|
|
353
|
+
await page
|
|
354
|
+
.locator(".bonsai-history")
|
|
355
|
+
.evaluate(
|
|
356
|
+
(element) => element.scrollHeight <= element.clientHeight,
|
|
357
|
+
),
|
|
358
|
+
true,
|
|
359
|
+
"history uses page scroll",
|
|
360
|
+
);
|
|
361
|
+
await page.evaluate(() => globalThis.document.activeElement?.blur());
|
|
362
|
+
await page.keyboard.press("j");
|
|
363
|
+
assert.match(
|
|
364
|
+
await page
|
|
365
|
+
.locator('[aria-current="true"]')
|
|
366
|
+
.locator("..")
|
|
367
|
+
.getAttribute("class"),
|
|
368
|
+
/stipe-surface-selected/u,
|
|
369
|
+
);
|
|
370
|
+
assert.match(
|
|
371
|
+
await page.locator('[aria-current="true"]').getAttribute("href"),
|
|
372
|
+
/00000027$/u,
|
|
373
|
+
);
|
|
374
|
+
if (width === 1440) {
|
|
375
|
+
await page
|
|
376
|
+
.locator('[data-tile="commit"] h1')
|
|
377
|
+
.filter({ hasText: "Keep fish inside the reeds" })
|
|
378
|
+
.waitFor();
|
|
379
|
+
} else {
|
|
380
|
+
assert.equal(await page.locator('[data-tile="commit"]').count(), 0);
|
|
381
|
+
await page.keyboard.press("Enter");
|
|
382
|
+
await page
|
|
383
|
+
.locator("h1")
|
|
384
|
+
.filter({ hasText: "Keep fish inside the reeds" })
|
|
385
|
+
.waitFor();
|
|
386
|
+
await page.goto(
|
|
387
|
+
`http://127.0.0.1:5188/test/browser/visual.html?tab=${tab}`,
|
|
388
|
+
);
|
|
389
|
+
await page.getByRole("tab", { name: "All", exact: true }).waitFor();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (slice === "6" && phase.endsWith("after")) {
|
|
393
|
+
await page
|
|
394
|
+
.locator('[data-tile="file"]')
|
|
395
|
+
.getByText("Koi pond", { exact: true })
|
|
396
|
+
.waitFor();
|
|
397
|
+
await page.getByText("Latest commit ·", { exact: false }).waitFor();
|
|
398
|
+
await page.getByRole("button", { name: "Raw", exact: true }).click();
|
|
399
|
+
await page
|
|
400
|
+
.locator('[data-tile="file"] pre')
|
|
401
|
+
.filter({ hasText: "# Koi pond" })
|
|
402
|
+
.waitFor();
|
|
403
|
+
await page.getByRole("button", { name: "Raw", exact: true }).click();
|
|
404
|
+
if (width === 390)
|
|
405
|
+
await page
|
|
406
|
+
.getByRole("button", { name: "Files · README.md", exact: true })
|
|
407
|
+
.click();
|
|
408
|
+
const fileRow = page.locator(
|
|
409
|
+
'[data-item-path="src/pond.ts"][role="treeitem"]',
|
|
410
|
+
);
|
|
411
|
+
if (!(await fileRow.isVisible()))
|
|
412
|
+
await page
|
|
413
|
+
.locator('[data-item-path="src"][role="treeitem"]')
|
|
414
|
+
.click();
|
|
415
|
+
await fileRow.click();
|
|
416
|
+
await page
|
|
417
|
+
.locator('[data-tile="file"]')
|
|
418
|
+
.getByText("src/pond.ts", { exact: true })
|
|
419
|
+
.waitFor();
|
|
420
|
+
await page.getByRole("button", { name: "Raw", exact: true }).click();
|
|
421
|
+
await page
|
|
422
|
+
.locator('[data-tile="file"] pre')
|
|
423
|
+
.filter({ hasText: "export const ripples = 5" })
|
|
424
|
+
.waitFor();
|
|
425
|
+
await page.getByRole("button", { name: "Raw", exact: true }).click();
|
|
426
|
+
if (width === 390)
|
|
427
|
+
assert.equal(
|
|
428
|
+
await page
|
|
429
|
+
.getByRole("button", {
|
|
430
|
+
name: "Files · src/pond.ts",
|
|
431
|
+
exact: true,
|
|
432
|
+
})
|
|
433
|
+
.getAttribute("aria-expanded"),
|
|
434
|
+
"false",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (slice === "7" && phase.endsWith("after")) {
|
|
438
|
+
await page
|
|
439
|
+
.locator('[data-tile="readme"]')
|
|
440
|
+
.getByRole("heading", { name: "Bonsai", exact: true })
|
|
441
|
+
.waitFor();
|
|
442
|
+
await page.locator('[data-tile="network"] svg').first().waitFor();
|
|
443
|
+
await page
|
|
444
|
+
.locator('[data-tile="repo:koi-pond"]')
|
|
445
|
+
.getByRole("link")
|
|
446
|
+
.waitFor();
|
|
447
|
+
const card = page.locator('[data-tile="repo:koi-pond"]');
|
|
448
|
+
assert.equal(
|
|
449
|
+
await card.getByRole("link").count(),
|
|
450
|
+
1,
|
|
451
|
+
"whole repository card is one link",
|
|
452
|
+
);
|
|
453
|
+
assert.equal(await card.getByText("Open repository ›").count(), 0);
|
|
454
|
+
if (width === 1440) {
|
|
455
|
+
await page.waitForFunction(() => {
|
|
456
|
+
const network = globalThis.document.querySelector(
|
|
457
|
+
'[data-tile="network"]',
|
|
458
|
+
);
|
|
459
|
+
const live =
|
|
460
|
+
globalThis.document.querySelector('[data-tile="live"]');
|
|
461
|
+
const card = globalThis.document.querySelector(
|
|
462
|
+
'[data-tile="repo:koi-pond"]',
|
|
463
|
+
);
|
|
464
|
+
return (
|
|
465
|
+
Math.abs(
|
|
466
|
+
network.getBoundingClientRect().bottom -
|
|
467
|
+
live.getBoundingClientRect().bottom,
|
|
468
|
+
) < 2 &&
|
|
469
|
+
Math.abs(
|
|
470
|
+
card.getBoundingClientRect().top -
|
|
471
|
+
network.getBoundingClientRect().bottom -
|
|
472
|
+
12,
|
|
473
|
+
) < 2
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
await page.waitForFunction(() =>
|
|
478
|
+
[
|
|
479
|
+
...globalThis.document.querySelectorAll(
|
|
480
|
+
'[data-tile="readme"] img',
|
|
481
|
+
),
|
|
482
|
+
].every((img) => img.complete && img.naturalWidth > 0),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (slice === "8" && phase.endsWith("after")) {
|
|
486
|
+
for (const name of ["Unblocked", "Blocked", "Done"])
|
|
487
|
+
await page.getByRole("heading", { name, exact: true }).waitFor();
|
|
488
|
+
assert.equal(
|
|
489
|
+
await page
|
|
490
|
+
.getByRole("heading", {
|
|
491
|
+
name: "Issue files need fixing",
|
|
492
|
+
exact: true,
|
|
493
|
+
})
|
|
494
|
+
.count(),
|
|
495
|
+
0,
|
|
496
|
+
);
|
|
497
|
+
const blocked = page
|
|
498
|
+
.getByRole("heading", { name: "Blocked", exact: true })
|
|
499
|
+
.locator("..");
|
|
500
|
+
await blocked
|
|
501
|
+
.getByRole("link", { name: "Plant reeds", exact: true })
|
|
502
|
+
.waitFor();
|
|
503
|
+
assert.equal(
|
|
504
|
+
await blocked.getByText("ready · agent", { exact: true }).count(),
|
|
505
|
+
0,
|
|
506
|
+
);
|
|
507
|
+
await blocked
|
|
508
|
+
.locator(".caps-status")
|
|
509
|
+
.filter({ hasText: "blocked by pond-01" })
|
|
510
|
+
.waitFor();
|
|
511
|
+
const done = page
|
|
512
|
+
.getByRole("heading", { name: "Done", exact: true })
|
|
513
|
+
.locator("..");
|
|
514
|
+
assert.equal(
|
|
515
|
+
await done.locator(".caps-status").getAttribute("data-glyph"),
|
|
516
|
+
"dot",
|
|
517
|
+
);
|
|
518
|
+
const titleBox = await blocked
|
|
519
|
+
.getByRole("link", { name: "Plant reeds", exact: true })
|
|
520
|
+
.boundingBox();
|
|
521
|
+
const statusBox = await blocked.locator(".caps-status").boundingBox();
|
|
522
|
+
assert.ok(
|
|
523
|
+
titleBox.x < statusBox.x,
|
|
524
|
+
"issue title precedes trailing status",
|
|
525
|
+
);
|
|
526
|
+
await blocked
|
|
527
|
+
.getByRole("link", { name: "pond-01", exact: true })
|
|
528
|
+
.click();
|
|
529
|
+
await page
|
|
530
|
+
.getByRole("heading", { name: "Add a lily pad", exact: true })
|
|
531
|
+
.waitFor();
|
|
532
|
+
await page
|
|
533
|
+
.getByRole("link", { name: "Issues", exact: true })
|
|
534
|
+
.last()
|
|
535
|
+
.click();
|
|
536
|
+
await page
|
|
537
|
+
.getByRole("heading", { name: "Blocked", exact: true })
|
|
538
|
+
.waitFor();
|
|
539
|
+
await page.waitForFunction(
|
|
540
|
+
() => globalThis.document.documentElement.scrollHeight <= 1200,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
await page.screenshot({
|
|
544
|
+
path: new URL(`${width}-${theme}-${tab}.png`, output).pathname,
|
|
545
|
+
fullPage: true,
|
|
546
|
+
animations: "disabled",
|
|
547
|
+
});
|
|
548
|
+
if (slice === "7" && phase.endsWith("after")) {
|
|
549
|
+
await page.screenshot({
|
|
550
|
+
path: new URL(`${width}-${theme}-${tab}-viewport.png`, output)
|
|
551
|
+
.pathname,
|
|
552
|
+
animations: "disabled",
|
|
553
|
+
});
|
|
554
|
+
await page
|
|
555
|
+
.locator('[data-tile="repo:koi-pond"]')
|
|
556
|
+
.getByRole("link")
|
|
557
|
+
.focus();
|
|
558
|
+
await page.keyboard.press("Enter");
|
|
559
|
+
assert.equal(
|
|
560
|
+
await page
|
|
561
|
+
.getByRole("link", { name: "Overview", exact: true })
|
|
562
|
+
.getAttribute("aria-current"),
|
|
563
|
+
"page",
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
assert.deepEqual(errors, []);
|
|
567
|
+
assert.equal(
|
|
568
|
+
await page.evaluate(
|
|
569
|
+
() =>
|
|
570
|
+
globalThis.document.documentElement.scrollWidth <=
|
|
571
|
+
globalThis.innerWidth,
|
|
572
|
+
),
|
|
573
|
+
true,
|
|
574
|
+
);
|
|
575
|
+
await page.close();
|
|
576
|
+
}
|
|
577
|
+
console.log(
|
|
578
|
+
`PASS ${phase}: 1440/390 light/dark screenshots, viewport fit, no page errors`,
|
|
579
|
+
);
|
|
580
|
+
} else if (phase.startsWith("redesign-4")) {
|
|
581
|
+
for (const width of [1440, 390])
|
|
582
|
+
for (const theme of phase.includes("-pr-")
|
|
583
|
+
? ["latte", "mocha"]
|
|
584
|
+
: ["light", "dark"])
|
|
585
|
+
for (const state of ["ready", "conflict"]) {
|
|
586
|
+
const page = await browser.newPage({
|
|
587
|
+
viewport: { width, height: 1000 },
|
|
588
|
+
colorScheme:
|
|
589
|
+
theme === "mocha" || theme === "dark" ? "dark" : "light",
|
|
590
|
+
});
|
|
591
|
+
page.on("pageerror", (error) => console.error(error.message));
|
|
592
|
+
await page.goto(
|
|
593
|
+
`http://127.0.0.1:5188/test/browser/visual.html?change=${state}`,
|
|
594
|
+
);
|
|
595
|
+
await page.locator("h1").waitFor();
|
|
596
|
+
await page.evaluate(
|
|
597
|
+
(t) =>
|
|
598
|
+
globalThis.document.documentElement.setAttribute("data-theme", t),
|
|
599
|
+
theme,
|
|
600
|
+
);
|
|
601
|
+
if (phase.endsWith("after")) {
|
|
602
|
+
assert.equal(await page.locator("[data-tile]").count(), 6);
|
|
603
|
+
assert.equal(
|
|
604
|
+
await page
|
|
605
|
+
.getByRole("button", { name: "Merge into main", exact: true })
|
|
606
|
+
.isDisabled(),
|
|
607
|
+
state === "conflict",
|
|
608
|
+
);
|
|
609
|
+
assert.equal(
|
|
610
|
+
await page.evaluate(
|
|
611
|
+
() =>
|
|
612
|
+
globalThis.document.documentElement.scrollWidth <=
|
|
613
|
+
globalThis.innerWidth,
|
|
614
|
+
),
|
|
615
|
+
true,
|
|
616
|
+
"Change fits viewport",
|
|
617
|
+
);
|
|
618
|
+
await page
|
|
619
|
+
.getByText("before the latest push", { exact: false })
|
|
620
|
+
.waitFor();
|
|
621
|
+
await page.getByText("1 file ·", { exact: false }).waitFor();
|
|
622
|
+
if (state === "conflict")
|
|
623
|
+
await page
|
|
624
|
+
.getByText("Update Luna/main from main", { exact: false })
|
|
625
|
+
.waitFor();
|
|
626
|
+
assert.match(
|
|
627
|
+
await page
|
|
628
|
+
.getByRole("button", { name: "Merge into main", exact: true })
|
|
629
|
+
.getAttribute("class"),
|
|
630
|
+
/btn-primary caps-button-filled/,
|
|
631
|
+
);
|
|
632
|
+
await page
|
|
633
|
+
.getByRole("heading", {
|
|
634
|
+
name: "#1 Add ripples around the koi",
|
|
635
|
+
exact: true,
|
|
636
|
+
})
|
|
637
|
+
.waitFor();
|
|
638
|
+
await page
|
|
639
|
+
.getByRole("button", { name: "Approve", exact: true })
|
|
640
|
+
.waitFor();
|
|
641
|
+
await page
|
|
642
|
+
.getByRole("button", { name: "Close", exact: true })
|
|
643
|
+
.waitFor();
|
|
644
|
+
assert.equal(
|
|
645
|
+
await page
|
|
646
|
+
.getByRole("button", { name: "Re-run checks", exact: true })
|
|
647
|
+
.count(),
|
|
648
|
+
0,
|
|
649
|
+
);
|
|
650
|
+
await page.getByText("1 of 1 passed", { exact: true }).waitFor();
|
|
651
|
+
const actionHeights = await Promise.all(
|
|
652
|
+
["Approve", "Close", "Merge into main"].map(async (name) => {
|
|
653
|
+
const box = await page
|
|
654
|
+
.getByRole("button", { name, exact: true })
|
|
655
|
+
.boundingBox();
|
|
656
|
+
assert.ok(box);
|
|
657
|
+
return box.height;
|
|
658
|
+
}),
|
|
659
|
+
);
|
|
660
|
+
assert.equal(
|
|
661
|
+
new Set(actionHeights).size,
|
|
662
|
+
1,
|
|
663
|
+
"actions share one Caps button height",
|
|
664
|
+
);
|
|
665
|
+
await page
|
|
666
|
+
.getByRole("link", { name: "Next change #2", exact: true })
|
|
667
|
+
.waitFor();
|
|
668
|
+
|
|
669
|
+
if (state === "conflict") {
|
|
670
|
+
const reason = page.locator(
|
|
671
|
+
'[data-tile="head"] span[tabindex="0"]',
|
|
672
|
+
);
|
|
673
|
+
await reason.focus();
|
|
674
|
+
await page
|
|
675
|
+
.getByRole("tooltip")
|
|
676
|
+
.filter({ hasText: "Resolve the conflicts" })
|
|
677
|
+
.waitFor();
|
|
678
|
+
await page
|
|
679
|
+
.getByRole("button", { name: "Close", exact: true })
|
|
680
|
+
.focus();
|
|
681
|
+
await page.getByRole("tooltip").waitFor({ state: "hidden" });
|
|
682
|
+
}
|
|
683
|
+
const labels = page.locator(
|
|
684
|
+
'[data-tile="lane"] .bonsai-network-lane',
|
|
685
|
+
);
|
|
686
|
+
await labels.locator(".bonsai-lane-owner").nth(1).waitFor();
|
|
687
|
+
assert.deepEqual(
|
|
688
|
+
await labels.locator(".bonsai-lane-owner").allTextContents(),
|
|
689
|
+
["main", "Luna"],
|
|
690
|
+
);
|
|
691
|
+
assert.equal(
|
|
692
|
+
await labels.last().getAttribute("title"),
|
|
693
|
+
"koi-pond--Luna/main",
|
|
694
|
+
);
|
|
695
|
+
for (const [above, below] of width === 1440
|
|
696
|
+
? [
|
|
697
|
+
["lane", "commits"],
|
|
698
|
+
["diff", "review"],
|
|
699
|
+
["commits", "files"],
|
|
700
|
+
]
|
|
701
|
+
: [
|
|
702
|
+
["review", "diff"],
|
|
703
|
+
["diff", "lane"],
|
|
704
|
+
["lane", "commits"],
|
|
705
|
+
["commits", "files"],
|
|
706
|
+
]) {
|
|
707
|
+
await page.waitForFunction(
|
|
708
|
+
([a, b]) => {
|
|
709
|
+
const first = globalThis.document
|
|
710
|
+
.querySelector(`[data-tile="${a}"] > :first-child`)
|
|
711
|
+
.getBoundingClientRect();
|
|
712
|
+
const second = globalThis.document
|
|
713
|
+
.querySelector(`[data-tile="${b}"]`)
|
|
714
|
+
.getBoundingClientRect();
|
|
715
|
+
const gap = second.y - first.bottom;
|
|
716
|
+
return gap >= 10 && gap <= 14;
|
|
717
|
+
},
|
|
718
|
+
[above, below],
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
await page.screenshot({
|
|
723
|
+
path: new URL(`${width}-${theme}-${state}.png`, output).pathname,
|
|
724
|
+
fullPage: true,
|
|
725
|
+
animations: "disabled",
|
|
726
|
+
timeout: 60_000,
|
|
727
|
+
});
|
|
728
|
+
await page.close();
|
|
729
|
+
}
|
|
730
|
+
if (phase.endsWith("after")) {
|
|
731
|
+
for (const result of [
|
|
732
|
+
"merged",
|
|
733
|
+
"moved",
|
|
734
|
+
"conflict",
|
|
735
|
+
"refused",
|
|
736
|
+
"error",
|
|
737
|
+
]) {
|
|
738
|
+
const page = await browser.newPage({
|
|
739
|
+
viewport: { width: 390, height: 1000 },
|
|
740
|
+
});
|
|
741
|
+
const errors = [];
|
|
742
|
+
page.on("pageerror", (error) => errors.push(error.message));
|
|
743
|
+
await page.goto(
|
|
744
|
+
`http://127.0.0.1:5188/test/browser/visual.html?change=ready&result=${result}`,
|
|
745
|
+
);
|
|
746
|
+
await page
|
|
747
|
+
.getByRole("button", { name: "Merge into main", exact: true })
|
|
748
|
+
.click();
|
|
749
|
+
const message = {
|
|
750
|
+
merged: "Merged as",
|
|
751
|
+
moved: "The reviewed head moved",
|
|
752
|
+
conflict: "Conflicts in",
|
|
753
|
+
refused: "Merge refused",
|
|
754
|
+
error: "Fixture merge unavailable",
|
|
755
|
+
}[result];
|
|
756
|
+
await page
|
|
757
|
+
.getByRole(result === "error" ? "alert" : "status")
|
|
758
|
+
.filter({ hasText: message })
|
|
759
|
+
.waitFor();
|
|
760
|
+
assert.deepEqual(
|
|
761
|
+
await page.evaluate(() =>
|
|
762
|
+
JSON.parse(globalThis.localStorage.getItem("fixture-merge")),
|
|
763
|
+
),
|
|
764
|
+
{
|
|
765
|
+
repo: "koi-pond",
|
|
766
|
+
number: 1,
|
|
767
|
+
head: "7c3c104600000000000000000000000000000026",
|
|
768
|
+
},
|
|
769
|
+
"merge uses numbered identity and exact displayed head",
|
|
770
|
+
);
|
|
771
|
+
if (result === "merged" || result === "conflict")
|
|
772
|
+
assert.equal(
|
|
773
|
+
await page
|
|
774
|
+
.getByRole("button", { name: "Merge into main", exact: true })
|
|
775
|
+
.isDisabled(),
|
|
776
|
+
true,
|
|
777
|
+
);
|
|
778
|
+
await page.keyboard.press("]");
|
|
779
|
+
await page
|
|
780
|
+
.getByRole("heading", {
|
|
781
|
+
name: "#2 Keep fish inside the reeds",
|
|
782
|
+
exact: true,
|
|
783
|
+
})
|
|
784
|
+
.waitFor();
|
|
785
|
+
await page.keyboard.press("[");
|
|
786
|
+
await page
|
|
787
|
+
.getByRole("heading", {
|
|
788
|
+
name: "#1 Add ripples around the koi",
|
|
789
|
+
exact: true,
|
|
790
|
+
})
|
|
791
|
+
.waitFor();
|
|
792
|
+
assert.deepEqual(errors, [], "no Change page errors");
|
|
793
|
+
await page.close();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (phase.includes("-pr-") && phase.endsWith("after")) {
|
|
797
|
+
const page = await browser.newPage({
|
|
798
|
+
viewport: { width: 390, height: 1000 },
|
|
799
|
+
});
|
|
800
|
+
const errors = [];
|
|
801
|
+
page.on("pageerror", (error) => errors.push(error.message));
|
|
802
|
+
await page.goto(
|
|
803
|
+
"http://127.0.0.1:5188/test/browser/visual.html?change=ready#change/koi-pond/1",
|
|
804
|
+
);
|
|
805
|
+
await page.getByRole("button", { name: "Approve", exact: true }).click();
|
|
806
|
+
await page
|
|
807
|
+
.getByRole("status")
|
|
808
|
+
.filter({ hasText: "Approved 7c3c104" })
|
|
809
|
+
.waitFor();
|
|
810
|
+
assert.deepEqual(
|
|
811
|
+
await page.evaluate(() =>
|
|
812
|
+
JSON.parse(globalThis.localStorage.getItem("fixture-approval")),
|
|
813
|
+
),
|
|
814
|
+
{
|
|
815
|
+
repo: "koi-pond",
|
|
816
|
+
number: 1,
|
|
817
|
+
head: "7c3c104600000000000000000000000000000026",
|
|
818
|
+
},
|
|
819
|
+
);
|
|
820
|
+
await page.getByRole("button", { name: "Close", exact: true }).click();
|
|
821
|
+
await page
|
|
822
|
+
.getByRole("status")
|
|
823
|
+
.filter({ hasText: "Change closed." })
|
|
824
|
+
.waitFor();
|
|
825
|
+
assert.deepEqual(
|
|
826
|
+
await page.evaluate(() =>
|
|
827
|
+
JSON.parse(globalThis.localStorage.getItem("fixture-close")),
|
|
828
|
+
),
|
|
829
|
+
{ repo: "koi-pond", number: 1 },
|
|
830
|
+
);
|
|
831
|
+
assert.equal(
|
|
832
|
+
await page
|
|
833
|
+
.getByRole("button", { name: "Approve", exact: true })
|
|
834
|
+
.count(),
|
|
835
|
+
0,
|
|
836
|
+
);
|
|
837
|
+
assert.equal(
|
|
838
|
+
await page.getByRole("button", { name: "Close", exact: true }).count(),
|
|
839
|
+
0,
|
|
840
|
+
);
|
|
841
|
+
assert.equal(
|
|
842
|
+
await page
|
|
843
|
+
.getByRole("button", { name: "Merge into main", exact: true })
|
|
844
|
+
.isDisabled(),
|
|
845
|
+
true,
|
|
846
|
+
);
|
|
847
|
+
assert.deepEqual(errors, []);
|
|
848
|
+
await page.close();
|
|
849
|
+
}
|
|
850
|
+
console.log(
|
|
851
|
+
`PASS ${phase}: Change ready/conflict shots 1440/390 ${phase.includes("-pr-") ? "Latte/Mocha; approve/close actions;" : "light/dark;"} numbered merge outcomes and keyboard neighbours`,
|
|
852
|
+
);
|
|
853
|
+
} else {
|
|
854
|
+
for (const width of [1440, 390])
|
|
855
|
+
for (const theme of ["light", "dark"]) {
|
|
856
|
+
const page = await browser.newPage({
|
|
857
|
+
viewport: { width, height: 1000 },
|
|
858
|
+
colorScheme: theme,
|
|
859
|
+
});
|
|
860
|
+
const errors = [];
|
|
861
|
+
page.on("pageerror", (error) => errors.push(error.message));
|
|
862
|
+
await page.goto(
|
|
863
|
+
"http://127.0.0.1:5188/" +
|
|
864
|
+
(phase === "before"
|
|
865
|
+
? ".botanical/before/visual.html"
|
|
866
|
+
: "test/browser/visual.html"),
|
|
867
|
+
);
|
|
868
|
+
await page.locator("h1").filter({ hasText: "koi-pond" }).waitFor();
|
|
869
|
+
await page.evaluate(
|
|
870
|
+
(t) =>
|
|
871
|
+
globalThis.document.documentElement.setAttribute("data-theme", t),
|
|
872
|
+
theme,
|
|
873
|
+
);
|
|
874
|
+
if (phase !== "before") {
|
|
875
|
+
assert.equal(
|
|
876
|
+
await page
|
|
877
|
+
.getByRole("button", { name: "G O", exact: true })
|
|
878
|
+
.count(),
|
|
879
|
+
0,
|
|
880
|
+
);
|
|
881
|
+
await page.locator(".bonsai-history-commits").evaluate((list) => {
|
|
882
|
+
for (const row of list.children) {
|
|
883
|
+
const title = row.querySelector("a > span:first-child");
|
|
884
|
+
const branch = row.querySelector(".bonsai-history-branch");
|
|
885
|
+
if (
|
|
886
|
+
!title ||
|
|
887
|
+
!branch ||
|
|
888
|
+
branch.getBoundingClientRect().left <
|
|
889
|
+
title.getBoundingClientRect().right
|
|
890
|
+
)
|
|
891
|
+
throw new Error("Branch label overlaps commit title");
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
await page.keyboard.press("g");
|
|
895
|
+
await page.keyboard.press("o");
|
|
896
|
+
await page
|
|
897
|
+
.locator('[aria-current="page"]')
|
|
898
|
+
.filter({ hasText: "Overview" })
|
|
899
|
+
.waitFor();
|
|
900
|
+
await page.keyboard.press("g");
|
|
901
|
+
await page.keyboard.press("h");
|
|
902
|
+
await page
|
|
903
|
+
.locator('[aria-current="page"]')
|
|
904
|
+
.filter({ hasText: "History" })
|
|
905
|
+
.waitFor();
|
|
906
|
+
if (width === 390) {
|
|
907
|
+
assert.ok(
|
|
908
|
+
(await page.locator("h1").boundingBox()).height < 60,
|
|
909
|
+
"repository name remains readable on phones",
|
|
910
|
+
);
|
|
911
|
+
await page
|
|
912
|
+
.getByRole("button", { name: "Clone", exact: true })
|
|
913
|
+
.click();
|
|
914
|
+
await page
|
|
915
|
+
.getByRole("region", { name: "Clone repository" })
|
|
916
|
+
.waitFor();
|
|
917
|
+
await page.keyboard.press("Escape");
|
|
918
|
+
assert.equal(
|
|
919
|
+
await page
|
|
920
|
+
.getByRole("region", { name: "Clone repository" })
|
|
921
|
+
.count(),
|
|
922
|
+
0,
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
await page
|
|
926
|
+
.context()
|
|
927
|
+
.grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
928
|
+
await page.keyboard.press("c");
|
|
929
|
+
await page.waitForFunction(
|
|
930
|
+
async () =>
|
|
931
|
+
(await globalThis.navigator.clipboard.readText()) ===
|
|
932
|
+
"https://example.test/git/koi-pond",
|
|
933
|
+
);
|
|
934
|
+
await page.keyboard.press("Escape");
|
|
935
|
+
}
|
|
936
|
+
await page.screenshot({
|
|
937
|
+
path: new URL(`${width}-${theme}-history.png`, output).pathname,
|
|
938
|
+
fullPage: true,
|
|
939
|
+
animations: "disabled",
|
|
940
|
+
timeout: 60_000,
|
|
941
|
+
});
|
|
942
|
+
if (phase !== "before") {
|
|
943
|
+
assert.equal(
|
|
944
|
+
await page.locator(".bonsai-history-commits li").count(),
|
|
945
|
+
40,
|
|
946
|
+
);
|
|
947
|
+
await page
|
|
948
|
+
.getByRole("link", { name: "Overview", exact: true })
|
|
949
|
+
.click();
|
|
950
|
+
await page.locator('[data-tile="contributors"]').waitFor();
|
|
951
|
+
assert.equal(await page.locator("[data-tile]").count(), 8);
|
|
952
|
+
await page.waitForFunction(() => {
|
|
953
|
+
const window = globalThis.document.querySelector(
|
|
954
|
+
".bonsai-network-scroll",
|
|
955
|
+
);
|
|
956
|
+
const newest = window?.querySelector(
|
|
957
|
+
".bonsai-bead:last-child circle",
|
|
958
|
+
);
|
|
959
|
+
if (!window || !newest) return false;
|
|
960
|
+
const frame = window.getBoundingClientRect();
|
|
961
|
+
const bead = newest.getBoundingClientRect();
|
|
962
|
+
return bead.left >= frame.left && bead.right <= frame.right;
|
|
963
|
+
});
|
|
964
|
+
assert.equal(
|
|
965
|
+
await page
|
|
966
|
+
.locator('[data-tile="ages"], [data-tile="churn"]')
|
|
967
|
+
.count(),
|
|
968
|
+
0,
|
|
969
|
+
);
|
|
970
|
+
for (const name of [".bonsai-contributor-chart"]) {
|
|
971
|
+
await page.waitForFunction((selector) => {
|
|
972
|
+
const chart = globalThis.document.querySelector(selector);
|
|
973
|
+
const axis = chart?.querySelector("[data-time-axis]");
|
|
974
|
+
return (
|
|
975
|
+
chart &&
|
|
976
|
+
axis &&
|
|
977
|
+
Math.abs(
|
|
978
|
+
chart.getBoundingClientRect().width -
|
|
979
|
+
axis.getBoundingClientRect().width,
|
|
980
|
+
) < 1
|
|
981
|
+
);
|
|
982
|
+
}, name);
|
|
983
|
+
}
|
|
984
|
+
assert.equal(
|
|
985
|
+
await page.evaluate(
|
|
986
|
+
() =>
|
|
987
|
+
globalThis.document.documentElement.scrollWidth <=
|
|
988
|
+
globalThis.innerWidth,
|
|
989
|
+
),
|
|
990
|
+
true,
|
|
991
|
+
"page fits viewport",
|
|
992
|
+
);
|
|
993
|
+
assert.equal(await page.locator('[data-tile="branches"]').count(), 0);
|
|
994
|
+
await page.waitForFunction(() => {
|
|
995
|
+
const chart = globalThis.document.querySelector(
|
|
996
|
+
".bonsai-contributor-chart",
|
|
997
|
+
);
|
|
998
|
+
const paths = [...chart.querySelectorAll("path")];
|
|
999
|
+
return paths.some(
|
|
1000
|
+
(path) =>
|
|
1001
|
+
path.getBBox().x < 1 &&
|
|
1002
|
+
path.getBBox().width >= chart.viewBox.baseVal.width - 1,
|
|
1003
|
+
);
|
|
1004
|
+
});
|
|
1005
|
+
const calendar = await page
|
|
1006
|
+
.locator('[data-tile="calendar"] > :first-child')
|
|
1007
|
+
.boundingBox();
|
|
1008
|
+
assert.ok(
|
|
1009
|
+
calendar && calendar.height < 330,
|
|
1010
|
+
"calendar fits its content",
|
|
1011
|
+
);
|
|
1012
|
+
if (phase === "redesign-9-after") {
|
|
1013
|
+
await page.locator(".bonsai-feed-item").evaluateAll((rows) => {
|
|
1014
|
+
for (const row of rows) {
|
|
1015
|
+
const lines = row.querySelectorAll("p");
|
|
1016
|
+
if (
|
|
1017
|
+
lines.length !== 2 ||
|
|
1018
|
+
row.getBoundingClientRect().height > 70
|
|
1019
|
+
)
|
|
1020
|
+
throw new Error("Live activity must fit two compact lines");
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
1024
|
+
assert.equal(
|
|
1025
|
+
await page
|
|
1026
|
+
.locator(".bonsai-recent-head")
|
|
1027
|
+
.evaluateAll((heads) =>
|
|
1028
|
+
heads.every(
|
|
1029
|
+
(head) =>
|
|
1030
|
+
globalThis.globalThis.getComputedStyle(head)
|
|
1031
|
+
.animationName === "none",
|
|
1032
|
+
),
|
|
1033
|
+
),
|
|
1034
|
+
true,
|
|
1035
|
+
);
|
|
1036
|
+
await page.emulateMedia({ reducedMotion: "no-preference" });
|
|
1037
|
+
if (width === 1440 && theme === "light") {
|
|
1038
|
+
await page.locator('[data-tile="network"]').screenshot({
|
|
1039
|
+
path: new URL(
|
|
1040
|
+
"../../../../packages/oyster/docs/media/bonsai-network.png",
|
|
1041
|
+
import.meta.url,
|
|
1042
|
+
).pathname,
|
|
1043
|
+
animations: "disabled",
|
|
1044
|
+
});
|
|
1045
|
+
await page.screenshot({
|
|
1046
|
+
path: new URL(
|
|
1047
|
+
"../../../../packages/oyster/docs/media/bonsai-repository.png",
|
|
1048
|
+
import.meta.url,
|
|
1049
|
+
).pathname,
|
|
1050
|
+
animations: "disabled",
|
|
1051
|
+
});
|
|
1052
|
+
await page.screenshot({
|
|
1053
|
+
path: new URL(
|
|
1054
|
+
"../../docs/media/bonsai-insights.png",
|
|
1055
|
+
import.meta.url,
|
|
1056
|
+
).pathname,
|
|
1057
|
+
fullPage: true,
|
|
1058
|
+
animations: "disabled",
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
await page.screenshot({
|
|
1063
|
+
path: new URL(`${width}-${theme}-insights.png`, output).pathname,
|
|
1064
|
+
fullPage: true,
|
|
1065
|
+
animations: "disabled",
|
|
1066
|
+
timeout: 60_000,
|
|
1067
|
+
});
|
|
1068
|
+
await page.getByRole("button", { name: "Arrange tiles" }).click();
|
|
1069
|
+
const contributor = page.locator('[data-tile="contributors"]');
|
|
1070
|
+
for (const title of ["README", "Contribution calendar"])
|
|
1071
|
+
await page
|
|
1072
|
+
.getByRole("button", { name: `Remove ${title}`, exact: true })
|
|
1073
|
+
.click();
|
|
1074
|
+
if (width === 1440) {
|
|
1075
|
+
await page
|
|
1076
|
+
.getByRole("button", { name: "Move Contributors", exact: true })
|
|
1077
|
+
.evaluate((element) =>
|
|
1078
|
+
element.scrollIntoView({ block: "center" }),
|
|
1079
|
+
);
|
|
1080
|
+
const handle = await page
|
|
1081
|
+
.getByRole("button", { name: "Move Contributors", exact: true })
|
|
1082
|
+
.boundingBox();
|
|
1083
|
+
assert.ok(handle);
|
|
1084
|
+
const original = await contributor.boundingBox();
|
|
1085
|
+
await page.mouse.move(
|
|
1086
|
+
handle.x + handle.width / 2,
|
|
1087
|
+
handle.y + handle.height / 2,
|
|
1088
|
+
);
|
|
1089
|
+
await page.mouse.down();
|
|
1090
|
+
await page.mouse.move(
|
|
1091
|
+
handle.x + handle.width / 2 + (original?.width ?? 0) + 12,
|
|
1092
|
+
handle.y + handle.height / 2,
|
|
1093
|
+
{ steps: 8 },
|
|
1094
|
+
);
|
|
1095
|
+
await page.mouse.up();
|
|
1096
|
+
assert.notEqual(
|
|
1097
|
+
(await contributor.boundingBox())?.x,
|
|
1098
|
+
original?.x,
|
|
1099
|
+
"pointer drag moves tile",
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
if (width === 1440) {
|
|
1103
|
+
await page
|
|
1104
|
+
.getByRole("button", { name: "Cancel", exact: true })
|
|
1105
|
+
.click();
|
|
1106
|
+
await page.getByRole("button", { name: "Arrange tiles" }).click();
|
|
1107
|
+
for (const title of ["README", "Contribution calendar"])
|
|
1108
|
+
await page
|
|
1109
|
+
.getByRole("button", { name: `Remove ${title}`, exact: true })
|
|
1110
|
+
.click();
|
|
1111
|
+
}
|
|
1112
|
+
const firstTop = await contributor.evaluate(
|
|
1113
|
+
(element) => element.offsetLeft + element.offsetTop,
|
|
1114
|
+
);
|
|
1115
|
+
await page
|
|
1116
|
+
.getByRole("button", { name: "Move Contributors", exact: true })
|
|
1117
|
+
.focus();
|
|
1118
|
+
await page.keyboard.press(width === 1440 ? "ArrowRight" : "ArrowUp");
|
|
1119
|
+
const moved = await contributor.boundingBox();
|
|
1120
|
+
assert.notEqual(
|
|
1121
|
+
await contributor.evaluate(
|
|
1122
|
+
(element) => element.offsetLeft + element.offsetTop,
|
|
1123
|
+
),
|
|
1124
|
+
firstTop,
|
|
1125
|
+
"keyboard movement moves tile",
|
|
1126
|
+
);
|
|
1127
|
+
await page
|
|
1128
|
+
.getByRole("button", { name: "Resize Contributors", exact: true })
|
|
1129
|
+
.focus();
|
|
1130
|
+
await page.keyboard.press("ArrowDown");
|
|
1131
|
+
await page.keyboard.press("ArrowDown");
|
|
1132
|
+
const resized = await contributor.boundingBox();
|
|
1133
|
+
assert.ok(
|
|
1134
|
+
(resized?.height ?? 0) > (moved?.height ?? 0),
|
|
1135
|
+
"keyboard resize grows tile",
|
|
1136
|
+
);
|
|
1137
|
+
assert.equal(await page.locator('[data-tile="ages"]').count(), 0);
|
|
1138
|
+
await page
|
|
1139
|
+
.getByRole("button", { name: "Save layout", exact: true })
|
|
1140
|
+
.click();
|
|
1141
|
+
await page.getByRole("button", { name: "Arrange tiles" }).waitFor();
|
|
1142
|
+
await page.reload();
|
|
1143
|
+
await page
|
|
1144
|
+
.getByRole("link", { name: "Overview", exact: true })
|
|
1145
|
+
.click();
|
|
1146
|
+
await contributor.waitFor();
|
|
1147
|
+
assert.equal(
|
|
1148
|
+
await page.locator('[data-tile="ages"]').count(),
|
|
1149
|
+
0,
|
|
1150
|
+
"removed tile stays removed after reload",
|
|
1151
|
+
);
|
|
1152
|
+
assert.equal(
|
|
1153
|
+
await page.evaluate(
|
|
1154
|
+
() =>
|
|
1155
|
+
JSON.parse(
|
|
1156
|
+
globalThis.localStorage.getItem("fixture-layout"),
|
|
1157
|
+
).tiles.placements.find((tile) => tile.id === "contributors").h,
|
|
1158
|
+
),
|
|
1159
|
+
3,
|
|
1160
|
+
"saved row span survives reload",
|
|
1161
|
+
);
|
|
1162
|
+
assert.ok(
|
|
1163
|
+
(await contributor.boundingBox())?.height >= 528,
|
|
1164
|
+
"reloaded tile retains at least its three-row height",
|
|
1165
|
+
);
|
|
1166
|
+
await page.getByRole("button", { name: "Arrange tiles" }).click();
|
|
1167
|
+
await page.getByLabel("Visualization to add").selectOption("ages");
|
|
1168
|
+
await page.getByRole("button", { name: "Add", exact: true }).click();
|
|
1169
|
+
await page.locator('[data-tile="ages"]').waitFor();
|
|
1170
|
+
await page.waitForFunction(() => {
|
|
1171
|
+
const chart =
|
|
1172
|
+
globalThis.document.querySelector(".bonsai-age-chart");
|
|
1173
|
+
const axis = chart?.querySelector("[data-time-axis]");
|
|
1174
|
+
return (
|
|
1175
|
+
chart &&
|
|
1176
|
+
axis &&
|
|
1177
|
+
Math.abs(
|
|
1178
|
+
chart.getBoundingClientRect().width -
|
|
1179
|
+
axis.getBoundingClientRect().width,
|
|
1180
|
+
) < 1
|
|
1181
|
+
);
|
|
1182
|
+
});
|
|
1183
|
+
await page
|
|
1184
|
+
.getByRole("button", { name: "Cancel", exact: true })
|
|
1185
|
+
.click();
|
|
1186
|
+
assert.equal(
|
|
1187
|
+
await page.locator('[data-tile="ages"]').count(),
|
|
1188
|
+
0,
|
|
1189
|
+
"cancel discards unsaved changes",
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
assert.deepEqual(errors, [], "no page errors");
|
|
1193
|
+
await page.close();
|
|
1194
|
+
}
|
|
1195
|
+
if (phase !== "before") {
|
|
1196
|
+
const page = await browser.newPage({
|
|
1197
|
+
viewport: { width: 390, height: 1000 },
|
|
1198
|
+
});
|
|
1199
|
+
await page.goto(
|
|
1200
|
+
"http://127.0.0.1:5188/test/browser/visual.html?ref=main",
|
|
1201
|
+
);
|
|
1202
|
+
await page.locator(".bonsai-history-commits li").first().waitFor();
|
|
1203
|
+
assert.equal(
|
|
1204
|
+
await page.locator(".bonsai-history-commits li").count(),
|
|
1205
|
+
10,
|
|
1206
|
+
"selected branch limits history",
|
|
1207
|
+
);
|
|
1208
|
+
await page.locator(".bonsai-history").evaluate((element) => {
|
|
1209
|
+
element.style.width = "280px";
|
|
1210
|
+
});
|
|
1211
|
+
assert.equal(
|
|
1212
|
+
await page
|
|
1213
|
+
.locator(".bonsai-history")
|
|
1214
|
+
.evaluate((element) => element.scrollWidth <= element.clientWidth),
|
|
1215
|
+
true,
|
|
1216
|
+
"history fits a sidebar",
|
|
1217
|
+
);
|
|
1218
|
+
await page.goto(
|
|
1219
|
+
"http://127.0.0.1:5188/test/browser/visual.html?tab=insights",
|
|
1220
|
+
);
|
|
1221
|
+
await page
|
|
1222
|
+
.locator('[aria-current="page"]')
|
|
1223
|
+
.filter({ hasText: "Overview" })
|
|
1224
|
+
.waitFor();
|
|
1225
|
+
assert.equal(
|
|
1226
|
+
await page.getByRole("link", { name: "Insights", exact: true }).count(),
|
|
1227
|
+
0,
|
|
1228
|
+
);
|
|
1229
|
+
await page.goto("http://127.0.0.1:5188/test/browser/visual.html?empty");
|
|
1230
|
+
await page.getByRole("link", { name: "Overview", exact: true }).click();
|
|
1231
|
+
await page
|
|
1232
|
+
.getByText("Push a first commit to start this repository.", {
|
|
1233
|
+
exact: false,
|
|
1234
|
+
})
|
|
1235
|
+
.waitFor();
|
|
1236
|
+
assert.equal(await page.locator("[data-tile]").count(), 0);
|
|
1237
|
+
await page.goto(
|
|
1238
|
+
"http://127.0.0.1:5188/test/browser/visual.html?save-error",
|
|
1239
|
+
);
|
|
1240
|
+
await page.getByRole("link", { name: "Overview", exact: true }).click();
|
|
1241
|
+
await page.getByRole("button", { name: "Arrange tiles" }).click();
|
|
1242
|
+
await page
|
|
1243
|
+
.getByRole("button", { name: "Remove Contributors", exact: true })
|
|
1244
|
+
.click();
|
|
1245
|
+
await page
|
|
1246
|
+
.getByRole("button", { name: "Save layout", exact: true })
|
|
1247
|
+
.click();
|
|
1248
|
+
await page.getByText("Fixture save unavailable").waitFor();
|
|
1249
|
+
assert.equal(
|
|
1250
|
+
await page.locator('[data-tile="contributors"]').count(),
|
|
1251
|
+
0,
|
|
1252
|
+
"failed saves retain the edit for retry",
|
|
1253
|
+
);
|
|
1254
|
+
await page.getByRole("button", { name: "Cancel", exact: true }).click();
|
|
1255
|
+
await page.locator('[data-tile="contributors"]').waitFor();
|
|
1256
|
+
await page.close();
|
|
1257
|
+
}
|
|
1258
|
+
console.log(
|
|
1259
|
+
`PASS ${phase}: 1440/390 light/dark screenshots${phase === "before" ? "" : ", pointer and keyboard move/resize, add/remove, cancel, save/reload, viewport fit, empty and save-error states"}`,
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
} finally {
|
|
1263
|
+
await browser.close();
|
|
1264
|
+
}
|