- commit
- e86770c361a7be928ca7eeec361bb105ff8bd646
- parent
- 9bf849c7c2becd5b19a9b70b113d37d1e9a1d428
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 98s, signed by cat
- kind
- fix risk score 1.2 of 3, Jev's estimate
- review
- #26 A shelf link followed on its owner page opens that shelf
ci attestation
forge-attestation 1 type ci commit e86770c361a7be928ca7eeec361bb105ff8bd646 tree 7c22ecfa65a1d84c72a887df62c90e3813740515 script .forge/ci 91d640bd19a231e82d21cc0a10d907253d9b0f41 result pass exit 0 started 2026-10-19T05:30:07Z duration 98s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:4c30364a294b838727618c6b6ef73582104af4a944b2329781b2493f240c7aa6 289 lines |pushed wip/i18n |afe7ee5 running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |afe7ee5 pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |de25128 running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |de25128 fail in 0s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |506024e running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |506024e pass in 0s, signed |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |warning: You appear to have cloned an empty repository. |site home: deployed e80a319 -> /sites/ana/dotfiles/home/ |created ana/notes |remote: review #1: /ana/notes/reviews/1/ |pushed ana/todo |landing #1 A todo list: ana/todo -> main |af8a24d running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |af8a24d pass in 0s, signed |pushed main |9521fcb running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |9521fcb pass in 0s, signed |remote: review #7: /dami/greet/reviews/7/ |pushed dami/regions |created dami/forge |warning: You appear to have cloned an empty repository. |site guide: mkdir -p guide/fonts && cp site/assets/fonts/*.woff2 guide/fonts/ |site guide: deployed 13ccf03 -> /sites/dami/forge/guide/ |(node:49670) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/dotfiles in 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.1DUlIV/wt/demo/root/.forge/www |(node:49852) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/notes in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.1DUlIV/wt/demo/root/.forge/www |(node:50155) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/forge in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.1DUlIV/wt/demo/root/.forge/www |(node:50411) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/greet in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.1DUlIV/wt/demo/root/.forge/www |(node:51128) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built - in 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.1DUlIV/wt/demo/root/.forge/www |235 of 235 pages render the same without 11ty |guide: 8 files, links ok |ok
Signature: good, cat
| M | site/ | 15 | ++++++++++++--- |
| M | test/ | 25 | ++++++++++++++++--------- |
| M | test/ | 9 | +++++++-- |
| M | test/ | 2 | +- |
@@ -139,9 +139,18 @@
139139 else b.setAttribute('aria-pressed', String(b.getAttribute('aria-pressed') !== 'true'));140140 });141141 142- // A link to one shelf (/cat/#games, from the home page's topics) opens on it.143- const at = decodeURIComponent(location.hash.slice(1));144- if (at && document.querySelector(`[data-shelves] a[data-shelf="${CSS.escape(at)}"]`)) pick.shelf = at;142+ // A link to one shelf (/cat/#games, from the home page's topics) opens on143+ // it, and so does following one, or going back, on the page itself: the144+ // address changes but the page doesn't load again. Any other address is "all".145+ const atHash = (): void => {146+ const at = decodeURIComponent(location.hash.slice(1));147+ pick.shelf = at && document.querySelector(`[data-shelves] a[data-shelf="${CSS.escape(at)}"]`) ? at : '';148+ };149+ atHash();150+ window.addEventListener('hashchange', () => {151+ atHash();152+ shelves();153+ });145154 146155 const content = (): void => {147156 wrap(wrapped());@@ -4,10 +4,10 @@
44 // hidden, text, moving children, and selectors made of tags, classes and55 // attributes, descendants and lists.66 //7-// node test/page.ts shelves <page.html> [#fragment] [click <selector>]...7+// node test/page.ts shelves <page.html> [#fragment] [click <selector> | go <#fragment>]...88 //9-// After loading the page at the fragment and clicking each selector in10-// turn, prints the shelf panels that show, a line each:9+// After loading the page at the fragment, then clicking each selector or10+// going to each fragment on the page (hashchange) in turn, prints the shelf panels that show, a line each:1111 // `<head strip>: <repo> <repo>...`, the repos as their rows show them.1212 import fs from 'node:fs';1313 import vm from 'node:vm';@@ -197,9 +197,11 @@ function load(file: string, hash: string) {
197197 },198198 });199199 const noop = (): void => {};200+ const location = { hash };200201 const page = vm.createContext({201202 document: doc,202- location: { hash },203+ window: { addEventListener: (type: string, f: (e: object) => void) => (handlers['window ' + type] ??= []).push(f) },204+ location,203205 localStorage: { getItem: () => null, setItem: noop },204206 matchMedia: () => ({ matches: false }),205207 setInterval: noop,@@ -221,7 +223,11 @@ function load(file: string, hash: string) {
221223 if (!target) throw new Error(`test/page.ts: nothing on the page matches ${selector}`);222224 for (const f of handlers.click ?? []) f({ type: 'click', target, preventDefault: noop });223225 };224- return { doc, click };226+ const go = (to: string): void => {227+ location.hash = to;228+ for (const f of handlers['window hashchange'] ?? []) f({ type: 'hashchange' });229+ };230+ return { doc, click, go };225231 }226232 227233 const shown = (e: El): boolean => !e.closest('[hidden]');@@ -229,14 +235,15 @@ const words = (s: string): string => s.replace(/\s+/g, ' ').trim();
229235 230236 const [mode, file, ...rest] = process.argv.slice(2);231237 if (mode !== 'shelves' || !file) {232- console.error('usage: node test/page.ts shelves <page.html> [#fragment] [click <selector>]...');238+ console.error('usage: node test/page.ts shelves <page.html> [#fragment] [click <selector> | go <#fragment>]...');233239 process.exit(2);234240 }235241 const hash = rest[0]?.startsWith('#') ? rest.shift()! : '';236-const { doc, click } = load(file, hash);242+const { doc, click, go } = load(file, hash);237243 for (let i = 0; i < rest.length; i += 2) {238- if (rest[i] !== 'click' || !rest[i + 1]) throw new Error(`test/page.ts: expected click <selector>, got ${rest.slice(i).join(' ')}`);239- click(rest[i + 1]);244+ if (rest[i] === 'click' && rest[i + 1]) click(rest[i + 1]);245+ else if (rest[i] === 'go' && rest[i + 1]) go(rest[i + 1]);246+ else throw new Error(`test/page.ts: expected click <selector> or go <#fragment>, got ${rest.slice(i).join(' ')}`);240247 }241248 for (const sec of doc.querySelectorAll('section.shelf').filter(shown)) {242249 const names = sec.querySelectorAll('.card').filter(shown).map((c) => words(c.querySelector('.card-name')!.textContent).replace(' / ', '/'));@@ -1,6 +1,6 @@
11 # A link to one shelf (/dami/#games, from the home page's topics) opens the2-# owner's page on that shelf's tab; an address naming no shelf opens on3-# "all". Without JS the address picks the panel too (:target, in the2+# owner's page on that shelf's tab, also when followed on the page itself;3+# an address naming no shelf opens on "all". Without JS the address picks the panel too (:target, in the44 # shelves macro's CSS).55 . "$(dirname "$0")/../lib.sh"66 user dami@@ -22,6 +22,11 @@ has "$FORGE_WWW/index.html" '<a href="/dami/#games">games'
2222 eq "$(page '#games')" "games 2: go chess"2323 eq "$(page '#tools')" "tools 1: notes"2424 eq "$(page '#nothing')" "all 4: blog notes go chess"25+# Following such a link on the page itself, or going back: the address26+# changes, the page stays.27+eq "$(page go '#games')" "games 2: go chess"28+eq "$(page click '[data-shelves] a[data-shelf="tools"]' go '#games')" "games 2: go chess"29+eq "$(page '#games' go '#')" "all 4: blog notes go chess"2530 has "$i" '<section class="panel shelf" data-shelf="games" id="games">'2631 css=$FORGE_WWW$(grep -o '/assets/[A-Za-z0-9_-]*\.css' "$i" | while read -r f; do grep -q shelves-js "$FORGE_WWW$f" && echo "$f"; done | head -n 1)2732 has "$css" '\.shelves:not\(\.shelves-js\) > \.shelf\[data-shelf\]:not\(\[data-shelf=""\]\):not\(:target\) \{ display: none; \}'@@ -13,7 +13,7 @@ got=$(cd "$T_HOME" && TZ=UTC LC_ALL=en_US.UTF-8 node -e '
1313 for (const [name, iso] of cases) for (const ago of [false, true]) els.push({ name: name + (ago ? ", in a sentence" : ""), dateTime: iso, textContent: "", title: "", hasAttribute: (a) => ago && a === "data-ago" });1414 const noop = () => {};1515 const document = { addEventListener: noop, querySelector: () => null, querySelectorAll: (s) => (s === "time[datetime]" ? els : []), body: { classList: { toggle: noop } } };16- const page = vm.createContext({ document, localStorage: { getItem: () => null, setItem: noop }, matchMedia: () => ({ matches: false }), location: { hash: "" }, setInterval: noop, setTimeout: noop, navigator: {} });16+ const page = vm.createContext({ document, window: { addEventListener: noop }, localStorage: { getItem: () => null, setItem: noop }, matchMedia: () => ({ matches: false }), location: { hash: "" }, setInterval: noop, setTimeout: noop, navigator: {} });1717 vm.runInContext("Date.now = () => " + now, page);1818 vm.runInContext(stripTypeScriptTypes(fs.readFileSync("site/assets/ui.ts", "utf8")), page);1919 for (const e of els) console.log(e.name + ": " + e.textContent);