A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

at trunk 5.6 kB View raw
1/** 2 * Regression guard for the landing-page sky phase carrier (docs/specs/sp8-brand-first-light.md). 3 * 4 * `data-phase` lives on exactly one carrier: `<html>`, the only element the pre-paint inline head 5 * script can reach (it runs in `<head>`, before `.page` exists, so it sets `documentElement`). 6 * 7 * The catch: the per-phase rules — `[data-phase='x'] .sky` / `.masthead` / `.hero` — live in 8 * index.astro's *scoped* `<style>`. Astro appends index.astro's scope id to every part of a 9 * selector, including the ancestor, yielding `[cid][data-phase='x'] .sky[cid]`. But `<html>` is 10 * rendered by Base.astro and never carries index.astro's cid, so a bare (scoped) ancestor matches 11 * nothing and the sky renders unstyled. Wrapping the ancestor in `:global([data-phase='x'])` keeps 12 * the descendant (`.sky`/`.masthead`/`.hero`) scoped while letting the ancestor match `<html>`. 13 * 14 * These asserts pin the wiring at the source level — rendering the page through astro/container 15 * isn't viable here (the test runner is pinned to jsdom for the WordPress block suites, which 16 * breaks esbuild's init invariant). 17 */ 18import { readFileSync } from 'node:fs'; 19import { fileURLToPath } from 'node:url'; 20import { describe, expect, it } from 'vitest'; 21import { phaseForHour } from '../lib/landing/time-of-day'; 22 23const read = ( rel: string ) => 24 readFileSync( fileURLToPath( new URL( rel, import.meta.url ) ), 'utf8' ); 25 26/** Strip the `---` frontmatter and any <style>/<script> blocks, leaving only rendered markup. */ 27const markupOnly = ( astro: string ) => 28 astro 29 .replace( /^---[\s\S]*?\n---/, '' ) 30 .replace( /<style[\s\S]*?<\/style>/g, '' ) 31 .replace( /<script[\s\S]*?<\/script>/g, '' ); 32 33describe( 'landing page sky phase carrier', () => { 34 const index = read( './index.astro' ); 35 const base = read( '../layouts/Base.astro' ); 36 37 it( 'carries the phase on <html> (the no-JS / pre-paint default the head script overwrites)', () => { 38 const htmlTag = markupOnly( base ).match( /<html\b[^>]*>/ )?.[ 0 ]; 39 expect( htmlTag ).toBeDefined(); 40 expect( htmlTag ).toMatch( /data-phase=/ ); 41 } ); 42 43 it( 'passes the default phase from the landing page into the layout', () => { 44 expect( index ).toMatch( /phase=\{\s*DEFAULT_PHASE\s*\}/ ); 45 } ); 46 47 it( 'never puts data-phase on a body element that would shadow <html> in the cascade', () => { 48 // Every element in the page markup is a descendant of <html>; none may carry its own 49 // data-phase. (The head <script> assigns it via documentElement, never as markup.) 50 expect( markupOnly( index ) ).not.toMatch( /data-phase=/ ); 51 } ); 52 53 it( 'updates the phase before first paint via documentElement', () => { 54 expect( index ).toMatch( /document\.documentElement\.dataset\.phase\s*=/ ); 55 } ); 56 57 it( 'wraps every per-phase ancestor selector in :global() so it can match <html>', () => { 58 // The phase carrier is <html> (rendered by Base.astro), which never carries index.astro's 59 // scoped cid. A bare `[data-phase=...]` ancestor would be scoped to that cid and match 60 // nothing; only `:global([data-phase=...])` reaches <html>. Assert that EVERY data-phase 61 // selector is global-wrapped — a single scoped one silently blanks the sky. 62 const style = index.match( /<style>([\s\S]*?)<\/style>/ )?.[ 1 ] ?? ''; 63 const total = ( style.match( /\[data-phase=/g ) ?? [] ).length; 64 const global = ( style.match( /:global\(\s*\[data-phase=/g ) ?? [] ).length; 65 expect( total, 'expected per-phase selectors in the landing-page styles' ).toBeGreaterThan( 0 ); 66 expect( global, 'every [data-phase=...] selector must be :global()-wrapped' ).toBe( total ); 67 } ); 68 69 // At night the sky is the bare starfield: the sun-driven layers (.bloom glow, .halo ring, 70 // .horizon line) must be switched off so no sun gradient or halo bleeds into the dark sky. 71 it( 'hides every sun layer at night, leaving only the stars', () => { 72 const style = index.match( /<style>([\s\S]*?)<\/style>/ )?.[ 1 ] ?? ''; 73 for ( const layer of [ 'bloom', 'halo', 'horizon' ] ) { 74 const rule = new RegExp( 75 `:global\\(\\s*\\[data-phase='night'\\]\\s*\\)\\s*\\.${ layer }\\b` 76 ); 77 expect( style, `night must silence the .${ layer } sun layer` ).toMatch( rule ); 78 } 79 } ); 80 81 // The inline head script must run before any module loads (for a no-flash sky), so it 82 // can't import phaseForHour — it hand-mirrors the same hour->phase boundaries. This guard 83 // keeps that copy honest: extract its `var p = <ternary>` expression straight from source 84 // and assert it agrees with phaseForHour for every hour. (The source comment in 85 // time-of-day.ts promises exactly this test.) 86 it( 'mirrors the phaseForHour boundaries in the inline head script', () => { 87 const expr = index.match( /var p\s*=\s*([\s\S]*?);/ )?.[ 1 ]; 88 expect( expr, 'inline head script should assign `var p = <phase ternary>;`' ).toBeDefined(); 89 // Lock the expression to a phase ternary over `h` before evaluating it: only the variable 90 // `h`, integer literals, comparison/logical/ternary operators, and quoted phase words are 91 // allowed. This makes the `new Function` below incapable of running anything else, even if 92 // the source were ever changed to something unexpected. 93 expect( expr, 'unexpected tokens in inline phase expression' ).toMatch( 94 /^[\sa-z\d<>=|?:'"]+$/ 95 ); 96 // eslint-disable-next-line no-new-func -- expr is validated above to be a pure phase ternary. 97 const scriptPhase = new Function( 'h', `return ( ${ expr } );` ) as ( h: number ) => string; 98 for ( let h = 0; h < 24; h++ ) { 99 expect( scriptPhase( h ), `hour ${ h }` ).toBe( phaseForHour( h ) ); 100 } 101 } ); 102} );