diff --git a/.github/actions/run-gates/action.yml b/.github/actions/run-gates/action.yml index 1fe28d66a..a039572bd 100644 --- a/.github/actions/run-gates/action.yml +++ b/.github/actions/run-gates/action.yml @@ -133,6 +133,13 @@ runs: - name: Unit-test that SVG screenshots are one line (test/svgshot.test.mjs) shell: bash run: node --test test/svgshot.test.mjs + # The screenshot review tool (scripts/shot_review.mjs): its session, what + # its pages show, its writes under a scratch docs folder, and its server, + # followed through a stand-in run, with its commit in a scratch repository. + # No IDE, no browser, no built tree; git. About 3 s. + - name: Unit-test the screenshot review tool (test/shot-review.test.mjs) + shell: bash + run: node --test test/shot-review.test.mjs # check_examples.mjs's probes: batching, crash isolation through a fake # lane, the canaries and the fence classifier. check_examples.mjs needs a # twinBASIC install, which CI has not; its probes need none. diff --git a/.gitignore b/.gitignore index 64c9b43cd..5d7d6fe54 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,7 @@ indexer/.packages/ # shoot_docs.mjs --svg: each picture's bundle (scripts/svgshot/bundle.mjs), what its SVG # is drawn from, for scripts/svgshot/replay.mjs. Bundles hold the Windows user name. /.svgshot-bundles/ + +# scripts/shot_review.mjs: the screenshot review's session and its runs' stages +# (scripts/lib/shot-session.mjs), which hold the Windows user name. +/.shot-review/ diff --git a/WIP.Harness.md b/WIP.Harness.md index 3f12fdcc6..fc6f01066 100644 --- a/WIP.Harness.md +++ b/WIP.Harness.md @@ -1206,7 +1206,26 @@ The recent lists are left alone: `test/ide/recent-projects-copies.test.mjs` need A probe of 50 launches hooked 49. The one miss was a page whose document began before the script was added. `attachIde` therefore checks `window.__tbDefaults`, the marker the script sets (it lists the keys answered, never their values), and reloads the page once when it is missing: the hook's session still holds -the script, the reload takes about 40 ms, and the UI and the command-line project come back. A marker still +the script, the reload takes about 40 ms, and the UI and the command-line project come back. The reload +waits for the hook to be installed (`hook.ready`): the hook connects on its own, and in a `shoot_docs +--jobs 10` run on BETA 1033 the help setup's page was reloaded before it had attached, loaded unhooked +again, and failed the setup. `ready` also waits for the pages being prepared: a page that existed +before the auto-attach is attached by `setAutoAttach` itself and prepared from its event, which `ready` +did not wait for, so in another `--jobs 10` run on BETA 1033 (2026-11-01) a debugging setup's page was +reloaded before its script was added, and failed the setup the same way. A page that could not be +prepared is named in the error. That was not the whole cause: the next `--jobs 10` run failed a +sample setup the same way with every page prepared, for a reason still unknown. So `checkDefaults` +also adds the script on its own connection to the page before the reload, which is open while the +page reloads whatever became of the hook's session, and a marker still missing after that reports +the page's address and state, the last check's error, the check's steps and the hook's (`trace`: +connected, each page attached and prepared, ready, a closed connection), each with its time. Those +named the cause in a `--jobs 16` retake on BETA 1033 (2026-11-02, settings-webview2), and likely the +sample setup's too: `attach` takes the target listed as `main.htm`, which it is while its navigation +there is still pending, so the check found `about:blank` with no marker, and its reload loaded +`about:blank` again and cancelled the navigation (the page "about:blank, complete" after the reload, +every page prepared). So `checkDefaults` first waits, a minute at most, for the page to commit +`main.htm`, whose document may then have run the hook after all, and fails with "its page never loaded +main.htm" rather than reload a page that has not. A marker still missing after that is a harness error, exit 2, since the run would have the user's settings. The hook's connection lives as long as the process, so a `tbbuild --keep` IDE is hooked until `tbbuild` ends. diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md index 1fe061ca2..0eb16981f 100644 --- a/WIP.HelpAddin.md +++ b/WIP.HelpAddin.md @@ -1186,7 +1186,12 @@ not: a restart saves a harness about a second against opening a new IDE. the add-in also removes `WEBVIEW2_USER_DATA_FOLDER` and `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS` from the compiler's environment (P19), and starts the control with the DevTools port in `TB_DOCS_HELP_WINDOW_PORT`, through which - the lane operates the window's page. The `%LOCALAPPDATA%` path is not run by any lane. + the lane operates the window's page, and with the further arguments in + `TB_DOCS_HELP_WINDOW_ARGS` beside it: `shoot_docs`'s help setup passes + `--force-device-scale-factor=1`, without which, on a display scaled to 150%, the page's + DOM snapshot is in the display's pixels and Help_Window's SVG is drawn 1.5 times too large + (the help lane passes none, so that it tests the window at the display's scale). The + `%LOCALAPPDATA%` path is not run by any lane. - **The lanes** read the add-in's test-switch lines: `window shown`, `window hidden`, `window state max|normal|min`, `window drag` and `window chrome custom|native style `. They check the bar's one row and the four buttons, no `WS_CAPTION` diff --git a/WIP.Screenshots.md b/WIP.Screenshots.md index b9c48b5ed..31151387b 100644 --- a/WIP.Screenshots.md +++ b/WIP.Screenshots.md @@ -3,6 +3,7 @@ The documentation's IDE screenshots go stale with every beta. `scripts/shoot_docs.mjs` retakes them from a live IDE: every picture of the IDE the documentation holds, annotations included, and the program windows and web pages that sit beside them. The owner's decisions behind it are marked (owner). +A run is reviewed, and its decisions overridden, with `node scripts/shot_review.mjs`: [WIP.ShotReview.md](WIP.ShotReview.md). ## What there is to retake @@ -149,7 +150,7 @@ Measured: `capture` stops at a capture equal to `HEAD`'s picture or the file's, and `keep` compares with `HEAD`'s first: within noise of it (or equal), the file becomes `HEAD`'s bytes, reported `restored as HEAD has it` when it held anything else, and its SVG goes back to `HEAD`'s, or - is removed when `HEAD` has none, so the pair stays matched (`restoreSvg`, the SVG written by + is removed when `HEAD` has none, so the pair stays matched (`restorePictures` in `scripts/lib/shot-apply.mjs`, the SVG written by `git restore --worktree`, so that its line endings are a checkout's; no SVG is drawn and the bundle stays as it is). Not near `HEAD`'s but within noise of the file: an earlier run of the retake wrote that change, unchanged. Else written. A picture `HEAD` lacks (a new shot, an @@ -177,7 +178,12 @@ Measured: `tabIndexRings`, a ring from 2 px inside the circle to 6 px beyond it, so the number is still compared): the shadow over the grid's dots is dithered in one of two versions, about 600 pixels apart, the same two bytes for bytes in every run. A shot without it compares byte for - byte. + byte. **Never use it for what differs between runs and shows**: the file keeps the old + pixels there, and an SVG written in a later run (a review's, or a stale bundle's) shows the + new ones beside them. Settle it instead. Settings_Close left its Project ID box out, the id the + lane gave the project for its slot, until a retake on 2026-11-02 wrote its light SVG with + another id than its PNG's; every project a setup opens now has one id, `DOCS_PROJECT_ID` + (Lane's `projectId`), and the box is compared. - **Settle by condition, not by sleep.** Fixed sleeps would be most of a run (a closed menu leaves a 2x8 px empty `#contextMenu`, so a wait for it to vanish always runs out its 2 s). A menu is closed when it holds no items; a dialog is still when a MutationObserver has seen no @@ -890,9 +896,16 @@ the PNG was just captured from, and the build shows it in place of the PNG as tB-Red are the IDE's own asset at its own size, and stay so (owner). - `fonts.mjs` gives each run's characters to the font files Chromium used for them, and `subset_font.py` cuts each file to the characters drawn, as WOFF2. -- `diff.mjs` renders the SVG as an `` at 2x and compares it with the PNG by luminance and - alpha, a shift of one device pixel allowed, so that the PNGs' ClearType fringes mostly cancel - out. +- `diff.mjs` renders the SVG as an `` at 2x and compares it with the PNG by luminance, a + shift of one device pixel allowed, so that the PNGs' ClearType fringes mostly cancel + out. **The luminance is each pixel's as seen over black and over white**, the larger of the + two distances: the colour of a pixel nearly transparent is rounding, and compared on its own + (with alpha beside it) it put a cut-out's shadow edge, alpha 1 at 255,255,255 in the PNG and + 0,0,0,0 in the render, 255 levels off. A replay of the 476 bundles (2026-11-01) lowered 58 + cut-outs' share strongly off and raised none, and accepted two SVGs it had refused for that + edge alone: Properties_SystemColours.light (0.442% to 0) and Properties_ColourPicker.light + (0.268% to 0.001%, the strip step 3 of WIP.ShotReview.md used to try `svg.ignore`, in a + manifest entry that was never committed). - **The SVG's size is the PNG's.** A clip half a CSS pixel past a whole one (a menu 168.5 wide) is captured a device pixel short of it, 168 wide, from the same origin; an SVG of the whole clip shows the half of the edge the PNG cuts off. That column alone put a small picture past @@ -947,7 +960,11 @@ the PNG was just captured from, and the build shows it in place of the PNG `svgs`) does not read a version 1 bundle, and replay lists every one as needing a retake. Reading version 1 would replay a split font with the one file it kept, and a picture with an inline `` without it, and nothing in a version 1 bundle - says which pictures have either. Answering misses offline in headless + says which pictures have either. Version 3 (2026-11-02) keeps every web font rule whose range + holds a character a run draws, as `webRulesFor` has chosen them since 4e05975c: a version 2 + bundle kept the last rule only, so a family the page declares twice (the IDE's `codicon`, its own + file and then Monaco's) was drawn from one file and nothing said so; 4e05975c should have raised + it. Answering misses offline in headless Chromium, and drawing bundles in `tbdocs`, are later steps, not done: the second needs the bundles committed, so cleaned of the user name, and complete, since CI has no Windows fonts to measure with. @@ -956,8 +973,10 @@ the PNG was just captured from, and the build shows it in place of the PNG 300 dpi bitmap, text included: the book would lose the text of 44 of its 110 SVGs that way. Every CSS filter function but `blur()` and `drop-shadow()` is an affine colour map, which commutes with antialiasing, so mapping each fill, stroke, gradient stop and text class under the group paints - the same picture on screen, to a channel's rounding; `opacity()` becomes the group's opacity. - A real filter stays only round an embedded image (a `` def wrapping it) and over a + the same picture on screen, to a channel's rounding; `opacity()` becomes the group's opacity, + and a filter of `opacity()` alone bakes nothing (`bakeMarkup`: the form designer's padlock, + `filter: opacity(0.5)` on a `::before`, was wrapped in a filter of no primitive, which draws + nothing, until 2026-11-01). A real filter stays only round an embedded image (a `` def wrapping it) and over a group that blends or already holds a real filter group. A `drop-shadow()` last in the list (the IDE's toolbar codicons glow with one) is split the same way: the shadow alone, from a copy of the group under a filter that blurs its alpha, then the group over it unfiltered, @@ -973,7 +992,8 @@ the PNG was just captured from, and the build shows it in place of the PNG user name (owner); `outputRefusal` keeps it out of `docs/` and out of any path git does not ignore, and an existing folder must hold an earlier sheet. What it shares with replay is `scripts/svgshot/redraw.mjs` (`redraw`, `writeJudged`, `runShards`/`inBrowser`, the - `judgedFiles` names: `.svg-diff.png` and `.svg` as `--diffs` names them). + `judgedFiles` names: `.svg-diff.png` and `.svg` as `--diffs` names them); its pages are + `scripts/svgshot/sheets.mjs`, made of the pieces in `scripts/lib/shot-views.mjs`. `shoot_docs.mjs --diffs` is to call `sheetHtml` over its diffs folder in a later commit. A sheet of the pictures with no SVG takes seconds; `--all`, over some 550 pictures, about a minute. @@ -990,21 +1010,43 @@ the PNG was just captured from, and the build shows it in place of the PNG cat-file --batch` (`readRevisionFiles`, `scripts/lib/shot-revision.mjs`). PNG against PNG, no browser: 337 pictures in about 30 s. On the BETA 1033 retake none was within noise. -- **The bar** (`SVG_FAITHFUL` in `shoot_docs.mjs`): an SVG with more than 0.25% of its pixels 96 +- **The bar** (`SVG_FAITHFUL` in `scripts/lib/shot-policy.mjs`): an SVG with more than 0.25% of its pixels 96 grey levels or more off is not written, and an older one is removed. Anything not drawn falls - to it. + to it. A picture's entry in `test/shots/manifest.json` can move it, to any level and share, + and leave rectangles out (`svgRule`): it is counted on `diffSvg`'s strength map, a byte per + pixel, so no level needs the picture drawn again. - **An SVG changes with its PNG** (owner). The SVG of a picture whose PNG is not rewritten - (unchanged, or within a capture's noise) stays as it is, with its bundle, when there is one: + (unchanged, or within a capture's noise) stays as it is, with its bundle, when there is one + (unless the SVG the run drew is the file's to the byte, line endings aside: its bundle then + replaces the cache's, which it keeps drawing the file's SVG and naming the file's PNG, and + answers what the converter has learnt to ask since; `bundleFollowsSvg`, owner, 2026-11-02): it is drawn from the whole page, layers the picture does not show included, and those differ from run to run (PackagePublishing_1, a floating panel over a form designer still drawing; CallStack_Frames.light), and one sitting on the bar (Properties_SystemColours.light at 0.251%, 94490c87) was written in one run and removed in the next. `replay.mjs` draws it - again after a change to the converter. + again after a change to the converter. **A review that accepts a run's SVG writes it all the + same** (owner, 2026-11-01; `svgFollowsPng`'s `accepted`, which `shoot_docs.mjs` and the review + tool's applier both ask): that is the way to replace an SVG drawn from a bundle the capture + recorded wrongly, which a replay redraws as wrongly. Seven were so on 2026-11-01, drawn from + bundles captured before 59b14ac0, whose cut-outs' styled scrollbars were recorded hidden: + Properties_ColourPicker and its light picture, 2b32ab8c-fabc-4f42-9e6b-06e85574eaf4 and its + light picture, ccGridButtonImage and its light picture, and Properties_SystemColours (1.30% of + its pixels 8 levels off its PNG, against 0.15% for the SVG run 5 of that day drew). **So does a + run whose picture's cached bundle cannot draw it again** (owner, 2026-11-02; `svgFollowsPng`'s + `stale`, from `redraw.mjs`'s `staleBundle`: no bundle, another format, another PNG, or a question + it has no answer for, what replay lists as needing a retake): the run's SVG is then what a + replay would write and cannot, so it is judged and written as with its PNG, and its bundle + cached. Without it the file kept an older converter's SVG for as long as its PNG did not change: + the retake of 2026-11-02 drew 53 SVGs faithful to 0.013% or better that differed from their + files only by the second `codicon` face of 4e05975c, which their version 2 bundles could not + draw, and every one stayed. The applier asks `staleBundle` itself, as the picture and the cache + are when it applies the verdict (up to 0.35 s for the largest bundle, Help_Pane's, one per turn + of the server's event loop). - **One line** (`oneLine`). Git checks a picture's SVG out with CRLF where `core.autocrlf` says so, since no `.gitattributes` rule can tell it from a diagram's SVG. An SVG icon embedded as text and the annotation layer's XML hold line breaks; so each is written as the space an XML parser makes of a break inside a tag, or ` ` in text, the same character. A checkout then - changes only the last line ending, which `keepSvg` ignores when it compares. + changes only the last line ending, which `writeSvg` (`scripts/lib/shot-apply.mjs`) ignores when it compares. **Scrollbars.** The IDE styles every scrollbar with `::-webkit-scrollbar` rules (`ide/styles.css`, coloured by theme variables), a second family under `.modalDialogContainer`. @@ -1014,7 +1056,10 @@ cannot be copied onto an element of the tool's own: Chromium drops a `var()` sho `background: var(...)`. So `scrollbarParts` gives each rule that styles this element's scrollbars, for a moment, one more selector, matching a probe element per part with the specificity of the selector it stands for; Chromium cascades and resolves the `var()`s, and the -rules are put back. The parts are laid out as Chromium's custom scrollbar theme does: a button's +rules are put back. The probes hang under the root element, not the body: a cut-out's `body +*{visibility:hidden !important}` matched them there, and every part of the scrollbar came out +hidden, undrawn (the Properties panel's in Properties_SystemColours and four other cut-outs; +`bench/scrollbars-cutout.html`). The parts are laid out as Chromium's custom scrollbar theme does: a button's length its `height` (vertical) or `width`, `auto` being the platform's scrollbar thickness, which the page measures; the thumb as long as its share of the content, never shorter than its own length, whose `auto` is 17 px (Windows' thumb length at 96 DPI; no page property shows it, and @@ -1045,11 +1090,47 @@ finds the ink by drawing the run 16 times as large on a canvas in the measuring overline, line-through, every style but solid, and `text-underline-position` other than auto. `test/svgshot.test.mjs` holds the fitted figures. +**An ellipsis cuts a line as Chromium's `NGLineTruncator` does** (`ellipsisLines`, `cutNeed`, +`cutPlan` in `snapshot-svg.mjs`; 2026-11-01, after the DIAGNOSTICS pane of IDE-FeatureMap drew +its messages whole, cut by the block's clip, where the PNG ends them in `...`). The snapshot gives +the whole text of an elided line, so the converter finds the lines itself: the block that holds +the line (the nearest box above the text that is not inline) clips its overflow, sets +`text-overflow: ellipsis`, and the line's text reaches past its content box. The fragments that +end before the line's end less the ellipsis are drawn whole, the first that does not is cut, the +rest are hidden, and the ellipsis, in the block's style, follows the last one shown. Where the +fragment is cut is the page's answer (`capture.mjs`'s `measure`, `cuts`): the longest start of the +run, at a grapheme's end, measured with a Range over the run as laid out, no wider than its room +less the ellipsis; the line's first fragment keeps one grapheme if none fits. Left out: right to +left, an atomic inline on the line, and a block scrolled sideways. `text-overflow` is in +`MISSABLE_STYLES`: a bundle taken before it was read is still drawn, and replay asks for a retake +only of a picture with a line its block clips (the miss `text-overflow of a line its block +clips`): 18 of the 480 bundles of 2026-11-01, some of which may clip with no ellipsis. + **A background image is sized by its bytes** (`naturalSize`), not by the type its URL names: the IDE calls a JPEG and a BMP `image/png`. The About dialog's EXCALIBUR logo is such a JPEG, drawn with `background-size: contain`; read as a PNG, its JFIF header gave 4,718,592 by about 4.3 billion pixels, and the logo would come out 0.054 px wide. +**A background stops at the borders that cover it** (`backgroundClip`, `bordersCover`): a box +with square corners whose sides are all opaque and fill their width (solid, inset, outset, +groove, ridge) has its background drawn in its padding box, not under its borders. Drawn under +them, it showed at their antialiased edges wherever the SVG is drawn at a scale other than 2x +(a seam of two antialiased edges): Menu_File_Project's separators, an inset border 2 px tall +over `rgb(89, 89, 89)`, came out 159 and 209 at 1.5x where the PNG, and the same borders drawn +as CSS, are 197 and 247; clipped, 196 and 247. At 2x the edges fall on whole pixels and nothing +changes (a replay of the 478 bundles of 2026-11-01: no verdict changed and no share strongly off +rose; seven rose by 0.02 to 0.07% at 8 levels, boxes at fractional positions), but 396 SVGs +come out with other bytes. A rounded box keeps its background under its borders: a curved inner +edge is antialiased at any scale, and the clip left a seam there at 2x (the designer's round +selection handles). + +**An `` of an image is drawn as an ``** (`embeddedImage`): Chromium stretches the +image to the element's box, as for an `` (checked in headless Chromium: a 10 by 10 PNG in a +100 by 60 `` filled it), and an ICO draws in an SVG ``. The resource preview shows +an `.ico` so (Resource_Preview: the tB icon, an `` of a `data:image/x-icon` URL, which was +missing and put 0.35% of its pixels strongly off; now 0%). Only a `data:` image is taken; the +snapshot's `currentSourceURL` is an ``'s alone, so the URL is the `src` attribute. + **Frames** (owner: cross-origin frames in general). A frame of the same site (the docked help pane: the documentation on another `localhost` port) is in the IDE's process, and `DOMSnapshot.captureSnapshot` returns its document beside the page's: the `