@graphty/visual-review 0.1.2 → 0.1.3
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/README.md +12 -2
- package/capture/capture.mjs +8 -1
- package/package.json +1 -1
- package/templates/visual-seed.yml +32 -12
- package/trusted/page/index.html +1 -1
- package/trusted/page/review.css +4 -0
- package/trusted/page/review.js +12 -0
package/README.md
CHANGED
|
@@ -170,8 +170,10 @@ copy the `visual` job and the gate's steps into it, keep those names, and set `w
|
|
|
170
170
|
file.
|
|
171
171
|
|
|
172
172
|
**`visual-seed.yml`** is started by hand to capture an older commit with the default branch's
|
|
173
|
-
tool: `gh workflow run visual-seed.yml --ref main -f ref=<sha>`.
|
|
174
|
-
|
|
173
|
+
tool: `gh workflow run visual-seed.yml --ref main -f ref=<sha>`. It captures every project seeded
|
|
174
|
+
from the default branch; add `-f projects="web charts"` to capture only those. Each project is
|
|
175
|
+
built and captured on its own, so one whose Storybook does not build at that commit fails alone.
|
|
176
|
+
See [Seeding](#seeding-one-story-at-a-time).
|
|
175
177
|
|
|
176
178
|
Both need nothing but the default `GITHUB_TOKEN`: the capture job reads Actions artifacts
|
|
177
179
|
(`actions: read`); nothing in CI writes to the repository.
|
|
@@ -462,6 +464,14 @@ the pull request.
|
|
|
462
464
|
wider. It is never cropped to the content, so a small component sits in the full canvas and
|
|
463
465
|
every capture of a project has the same size unless its story overflows. results.json records the scale as `scale`, and each review record
|
|
464
466
|
copies it into its `subject`.
|
|
467
|
+
- **Why captures rasterize on the CPU.** Chromium runs with `--disable-gpu-rasterization`, so the
|
|
468
|
+
page's text and shapes are drawn by the CPU; WebGL still runs on SwiftShader. Drawn through
|
|
469
|
+
SwiftShader, a glyph that sat on a sub-pixel boundary landed on either side of it from one
|
|
470
|
+
render to the next (a quarter-pixel shift of one letter, in 1 to 7 of 48 renders of the same
|
|
471
|
+
story), so stories with nothing moving read `unstable`, a different few on each run. With the
|
|
472
|
+
switch, 48 of 48 renders matched. The repository owner chose this on 2026-09-30, knowing it
|
|
473
|
+
changes how text is drawn in every story of every project: a baseline captured before it can
|
|
474
|
+
read `changed` once, and is accepted again.
|
|
465
475
|
- **From GitHub Actions to the page.** Each `visual` job uploads `results.json` and the PNGs to
|
|
466
476
|
review as an artifact `visual-<project>-<attempt>`, kept 30 days. The server lists open pull
|
|
467
477
|
requests with `gh`, finds each one's newest run of the capturing workflow, and downloads those
|
package/capture/capture.mjs
CHANGED
|
@@ -54,10 +54,17 @@ const VIEWPORT = { width: 1200, height: 900 };
|
|
|
54
54
|
/** Device pixels per CSS pixel, as Chromatic captures; recorded in results.json as `scale`. */
|
|
55
55
|
const SCALE = 2;
|
|
56
56
|
const RENDER_TIMEOUT = 30_000;
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Chromium's switches for every capture. `--disable-gpu-rasterization` draws the page's text and
|
|
59
|
+
* shapes on the CPU: rasterized through SwiftShader, a glyph that sits on a sub-pixel boundary
|
|
60
|
+
* landed on either side of it from one render to the next, so a story with nothing moving read
|
|
61
|
+
* `unstable` (the README, "Why captures rasterize on the CPU"). WebGL still runs on SwiftShader.
|
|
62
|
+
*/
|
|
63
|
+
export const CHROMIUM_ARGS = [
|
|
58
64
|
"--use-gl=angle",
|
|
59
65
|
"--use-angle=swiftshader",
|
|
60
66
|
"--enable-unsafe-swiftshader",
|
|
67
|
+
"--disable-gpu-rasterization",
|
|
61
68
|
"--force-color-profile=srgb",
|
|
62
69
|
"--disable-lcd-text",
|
|
63
70
|
"--font-render-hinting=none",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphty/visual-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Self-hosted visual review of Storybook stories: capture in GitHub Actions, keep the baselines in git (Git LFS), accept or reject each change in a local page, and gate pull requests",
|
|
5
5
|
"author": "Adam Powers <apowers@ato.ms>",
|
|
6
6
|
"type": "module",
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
# baselines can be seeded from it: `visual-review serve --master-run <this run's id>` lists the run
|
|
6
6
|
# as the default branch, and Finish opens the seed pull request. Dispatch it from __BRANCH__:
|
|
7
7
|
# gh workflow run visual-seed.yml --ref __BRANCH__ -f ref=<sha>
|
|
8
|
-
# Only the projects whose "seedFromDefaultBranch" is not false are captured
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# Only the projects whose "seedFromDefaultBranch" is not false are captured, or, with
|
|
9
|
+
# `-f projects="a b"`, only those of them named there. Each project is built and captured on its
|
|
10
|
+
# own: one whose Storybook fails to build at that commit fails its own jobs and no other project's.
|
|
11
|
+
# The capture job repeats the "visual" job of the review workflow, except that it has no reference
|
|
12
|
+
# (every story without a baseline is "new") and results.json names the captured commit.
|
|
11
13
|
|
|
12
14
|
name: Visual seed capture
|
|
13
15
|
|
|
@@ -18,6 +20,11 @@ on:
|
|
|
18
20
|
description: "The commit (or branch or tag) whose Storybooks are captured"
|
|
19
21
|
required: true
|
|
20
22
|
type: string
|
|
23
|
+
projects:
|
|
24
|
+
description: "The projects to capture, separated by spaces or commas (default: every one seeded from the default branch)"
|
|
25
|
+
required: false
|
|
26
|
+
type: string
|
|
27
|
+
default: ""
|
|
21
28
|
|
|
22
29
|
permissions:
|
|
23
30
|
contents: read
|
|
@@ -34,13 +41,23 @@ jobs:
|
|
|
34
41
|
sparse-checkout: visual-review.config.json
|
|
35
42
|
sparse-checkout-cone-mode: false
|
|
36
43
|
|
|
44
|
+
# A name that is not a project seeded from the default branch stops the run here, rather
|
|
45
|
+
# than seeding fewer projects than were asked for.
|
|
37
46
|
- name: List the projects
|
|
38
47
|
id: plan
|
|
48
|
+
env:
|
|
49
|
+
WANT: ${{ inputs.projects }}
|
|
39
50
|
run: |
|
|
40
|
-
|
|
51
|
+
projects=$(jq -c --arg want "$WANT" '(.baselines // "visual-baselines") as $b
|
|
52
|
+
| [$want | splits("[ ,]+") | select(. != "")] as $w
|
|
41
53
|
| [.projects | to_entries[] | select(.value.seedFromDefaultBranch != false)
|
|
42
|
-
| {project: .key, storybook: .value.storybook, build: .value.build, baselines: $b}]
|
|
43
|
-
|
|
54
|
+
| {project: .key, storybook: .value.storybook, build: .value.build, baselines: $b}] as $all
|
|
55
|
+
| ($w - [$all[].project]) as $unknown
|
|
56
|
+
| if ($unknown | length) > 0
|
|
57
|
+
then error("not a project seeded from the default branch: \($unknown | join(", "))")
|
|
58
|
+
else [$all[] | select(($w | length) == 0 or (.project | IN($w[])))] end' \
|
|
59
|
+
visual-review.config.json)
|
|
60
|
+
echo "projects=$projects" >> "$GITHUB_OUTPUT"
|
|
44
61
|
|
|
45
62
|
storybook:
|
|
46
63
|
name: Build ${{ matrix.project }} at ${{ inputs.ref }}
|
|
@@ -71,9 +88,12 @@ jobs:
|
|
|
71
88
|
name: storybook-${{ matrix.project }}
|
|
72
89
|
path: ${{ matrix.storybook }}/
|
|
73
90
|
|
|
91
|
+
# Runs even when another project's build failed. A project whose own build failed has no
|
|
92
|
+
# Storybook artifact, so its job stops at the download, before installing anything.
|
|
74
93
|
visual:
|
|
75
94
|
name: visual (${{ matrix.project }})
|
|
76
95
|
needs: [plan, storybook]
|
|
96
|
+
if: ${{ !cancelled() && needs.plan.result == 'success' }}
|
|
77
97
|
runs-on: ubuntu-24.04
|
|
78
98
|
timeout-minutes: 45
|
|
79
99
|
strategy:
|
|
@@ -84,6 +104,12 @@ jobs:
|
|
|
84
104
|
# The default branch's checkout: the capture tool, the config and the baselines come from here.
|
|
85
105
|
- uses: actions/checkout@v4
|
|
86
106
|
|
|
107
|
+
- name: Download Storybook build
|
|
108
|
+
uses: actions/download-artifact@v4
|
|
109
|
+
with:
|
|
110
|
+
name: storybook-${{ matrix.project }}
|
|
111
|
+
path: ${{ matrix.storybook }}/
|
|
112
|
+
|
|
87
113
|
- name: Restore this project's baseline images
|
|
88
114
|
uses: actions/cache@v4
|
|
89
115
|
with:
|
|
@@ -99,12 +125,6 @@ jobs:
|
|
|
99
125
|
- name: Install Chromium
|
|
100
126
|
run: __CLI__ install-browser
|
|
101
127
|
|
|
102
|
-
- name: Download Storybook build
|
|
103
|
-
uses: actions/download-artifact@v4
|
|
104
|
-
with:
|
|
105
|
-
name: storybook-${{ matrix.project }}
|
|
106
|
-
path: ${{ matrix.storybook }}/
|
|
107
|
-
|
|
108
128
|
- name: Capture
|
|
109
129
|
run: __CLI__ capture --project ${{ matrix.project }} --out "$RUNNER_TEMP/visual"
|
|
110
130
|
|
package/trusted/page/index.html
CHANGED
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
<span id="status" role="status"></span>
|
|
15
15
|
<button type="button" id="copy-link" title="Copy a link to this screen">Copy link</button>
|
|
16
16
|
</header>
|
|
17
|
-
<main id="app"><p>Loading...</p></main>
|
|
17
|
+
<main id="app" tabindex="-1"><p>Loading...</p></main>
|
|
18
18
|
</body>
|
|
19
19
|
</html>
|
package/trusted/page/review.css
CHANGED
package/trusted/page/review.js
CHANGED
|
@@ -1759,6 +1759,18 @@ function toggleView(view) {
|
|
|
1759
1759
|
showStory();
|
|
1760
1760
|
}
|
|
1761
1761
|
|
|
1762
|
+
// Safari on an iPad sends a hardware keyboard's keys only to a focused element, and tapping an
|
|
1763
|
+
// image or a button focuses nothing, so the shortcuts never arrived. The page itself holds focus
|
|
1764
|
+
// whenever nothing else does.
|
|
1765
|
+
function keepKeys() {
|
|
1766
|
+
if (document.activeElement === null || document.activeElement === document.body) {
|
|
1767
|
+
app.focus({ preventScroll: true });
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
document.addEventListener("pointerup", () => setTimeout(keepKeys));
|
|
1771
|
+
document.addEventListener("focusout", () => setTimeout(keepKeys));
|
|
1772
|
+
keepKeys();
|
|
1773
|
+
|
|
1762
1774
|
document.addEventListener("keydown", (e) => {
|
|
1763
1775
|
if (!["story", "grid"].includes(state.screen) || e.ctrlKey || e.metaKey || e.altKey) {
|
|
1764
1776
|
return;
|