Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/skills/component-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ assert.ok(
assert.equal(render(props), render(props));
```

## Effectful visual components

`renderToStaticMarkup` does not run effects. For a component whose visible
behavior depends on scrolling, resizing, media queries, or browser observers:

1. Extract coordinate math and state transitions into a pure TypeScript module
and test that module with `node:test`.
2. Static-render the component shell to prove deterministic, accessible SSR
markup and verify browser globals are deferred to `useEffect`.
3. Run `npm run build:ci` to exercise Docusaurus server rendering and route
generation.
4. Start `just dev --port 3000` and use local Chromium for the behavior only a
browser can prove: transforms, overflow, responsive visibility, focus, media
preferences, console errors, and hydration.

Do not add a DOM test framework merely to simulate browser layout. Pure logic,
static SSR output, the production build, and one local-browser check are the
smallest complete test stack for this class of component.

## Common Rationalizations

| Rationalization | Reality |
Expand Down
111 changes: 111 additions & 0 deletions scripts/portal-parallax-model.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const ts = require("typescript");

function loadTsModule(file) {
const { outputText } = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: {
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.CommonJS,
},
});
const mod = { exports: {} };
new Function("require", "module", "exports", outputText)(
require,
mod,
mod.exports,
);
return mod.exports;
}

const modelPath = path.join(
__dirname,
"..",
"src",
"components",
"portal",
"portalModel.ts",
);

test("portal model preserves the website layer contract", () => {
const {
PORTAL_BREAKPOINT_PX,
PORTAL_LAYERS,
MOBILE_LAYER_SRC,
TRANSITION_SRC,
} = loadTsModule(modelPath);

assert.equal(PORTAL_BREAKPOINT_PX, 956);
assert.equal(PORTAL_LAYERS.length, 15);
assert.deepEqual(
PORTAL_LAYERS.map(({ key, top, rate }) => [key, top, rate]),
[
["sky", 0, 0],
["clouds-right", -60, 0],
["sun", -90, 0.05],
["clouds-left", 0, 0],
["mountains", 0, 0],
["fog-a", 0, 0],
["background-a", 165, 0],
["fog-b", 200, 0],
["background-b", 175, -0.01],
["midground-a", 210, -0.03],
["midground-b", 250, -0.05],
["midground-c", 300, -0.07],
["foreground-a", 320, -0.09],
["foreground-b", 340, -0.11],
["foreground-c", 360, -0.13],
],
);
assert.equal(PORTAL_LAYERS[1].drift, "right");
assert.equal(PORTAL_LAYERS[3].drift, "left");
assert.equal(
PORTAL_LAYERS[1].src,
PORTAL_LAYERS[3].src,
"byte-identical clouds must share one copied asset",
);
assert.equal(MOBILE_LAYER_SRC, "/img/portal/mobile-parallax.webp");
assert.equal(TRANSITION_SRC, "/img/portal/layer-transition.webp");

const uniqueSources = new Set(PORTAL_LAYERS.map(({ src }) => src));
assert.equal(uniqueSources.size, 14);
for (const src of [...uniqueSources, MOBILE_LAYER_SRC, TRANSITION_SRC]) {
assert.ok(
fs.existsSync(path.join(__dirname, "..", "static", src)),
`missing copied asset ${src}`,
);
}
for (const character of ["bluefin.webp", "karl.webp", "nest.webp"]) {
assert.ok(
fs.existsSync(
path.join(
__dirname,
"..",
"static",
"img",
"portal",
"characters",
character,
),
),
`missing copied character ${character}`,
);
}
});

test("portal motion math clamps overlay, preserves rates, and culls", () => {
const { overlayOpacity, layerTransform, isParallaxVisible } =
loadTsModule(modelPath);

assert.equal(overlayOpacity(0, 900), 0);
assert.equal(overlayOpacity(225, 900), 0);
assert.equal(overlayOpacity(550, 900), 0.5);
assert.equal(overlayOpacity(875, 900), 1);
assert.equal(overlayOpacity(2000, 900), 1);
assert.equal(layerTransform(100, -0.13), "translate3d(0, -13px, 0)");
assert.equal(layerTransform(100, 0.05), "translate3d(0, 5px, 0)");
assert.equal(isParallaxVisible(3000, 3000), true);
assert.equal(isParallaxVisible(3001, 3000), false);
});
77 changes: 77 additions & 0 deletions scripts/portal-parallax-render.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const ts = require("typescript");
const React = require("react");
const { renderToStaticMarkup } = require("react-dom/server");

function loadModule(file) {
const { outputText } = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: {
jsx: ts.JsxEmit.React,
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.CommonJS,
esModuleInterop: true,
},
});
const mod = { exports: {} };
new Function("require", "module", "exports", outputText)(
(id) => {
if (id.endsWith(".css")) return {};
if (id.startsWith(".")) {
const base = path.resolve(path.dirname(file), id);
for (const suffix of [".ts", ".tsx", "/index.ts", "/index.tsx"]) {
if (fs.existsSync(base + suffix)) return loadModule(base + suffix);
}
}
return require(id);
},
mod,
mod.exports,
);
return mod.exports;
}

const componentPath = path.join(
__dirname,
"..",
"src",
"components",
"portal",
"PortalParallax.tsx",
);

test("parallax statically renders fifteen decorative desktop planes", () => {
const PortalParallax = loadModule(componentPath).default;
const html = renderToStaticMarkup(React.createElement(PortalParallax));

assert.ok(html.includes('data-portal-parallax="true"'));
assert.equal(html.split('data-portal-mode="desktop"').length - 1, 15);
assert.equal(html.split('data-portal-mode="mobile"').length - 1, 1);
assert.equal(html.split('data-layer="').length - 1, 15);
assert.ok(html.includes('data-layer="sun"'));
assert.ok(html.includes('data-rate="0.05"'));
assert.ok(html.includes('data-rate="-0.13"'));
assert.ok(html.includes('aria-hidden="true"'));
assert.equal(html.split('alt=""').length - 1, 16);
});

test("browser globals are deferred to the effect", () => {
const source = fs.readFileSync(componentPath, "utf8");
const effect = source.indexOf("React.useEffect");

assert.notEqual(effect, -1);
for (const browserGlobal of [
"window.scrollY",
"window.innerHeight",
"window.matchMedia",
"document.getElementById",
"ResizeObserver",
]) {
assert.ok(
source.indexOf(browserGlobal) > effect,
`${browserGlobal} must only appear inside useEffect`,
);
}
});
116 changes: 116 additions & 0 deletions scripts/portal-prototype-page.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const ts = require("typescript");
const React = require("react");
const { renderToStaticMarkup } = require("react-dom/server");

function loadModule(file) {
const { outputText } = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: {
jsx: ts.JsxEmit.React,
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.CommonJS,
esModuleInterop: true,
},
});
const mod = { exports: {} };
new Function("require", "module", "exports", outputText)(
(id) => {
if (id.endsWith(".css")) return {};
if (id.startsWith(".")) {
const base = path.resolve(path.dirname(file), id);
for (const suffix of [".ts", ".tsx", "/index.ts", "/index.tsx"]) {
if (fs.existsSync(base + suffix)) return loadModule(base + suffix);
}
}
return require(id);
},
mod,
mod.exports,
);
return mod.exports;
}

const root = path.join(__dirname, "..");
const componentPath = path.join(
root,
"src",
"components",
"portal",
"PortalPrototype.tsx",
);
const pagePath = path.join(root, "src", "pages", "portal-prototype.tsx");
const cssPath = path.join(
root,
"src",
"components",
"portal",
"PortalPrototype.module.css",
);

test("prototype renders source-authored scenes in order", () => {
const PortalPrototype = loadModule(componentPath).default;
const html = renderToStaticMarkup(React.createElement(PortalPrototype));

const ids = [
'id="scene-landing"',
'id="scene-users"',
'id="scene-developers"',
'id="scene-mission"',
];
ids.reduce((previous, id) => {
const current = html.indexOf(id);
assert.ok(current > previous, `${id} must follow the previous scene`);
return current;
}, -1);

assert.ok(html.includes('id="portal-scenes"'));
assert.match(html, /<h1[^>]*>\s*<img[^>]*alt="Bluefin"[^>]*\/>\s*<\/h1>/);
assert.match(
html,
/<h1[^>]*>\s*<img[^>]*src="\/img\/bluefin-wordmark-light\.svg"[^>]*\/>\s*<\/h1>/,
);
assert.ok(
html.includes(
"The next generation Linux workstation, designed for reliability, performance, and sustainability.",
),
);
assert.ok(html.includes(">For<"));
assert.ok(html.includes(">You<"));
assert.ok(html.includes(">Developers<"));
assert.ok(html.includes(">Mission<"));
assert.ok(html.includes('href="#scene-users"'));
assert.match(html, /id="scene-users"[^>]*tabindex="-1"/);
assert.ok(html.includes('src="/img/portal/layer-transition.webp"'));
});

test("route stays temporary and does not replace the documentation root", () => {
const page = fs.readFileSync(pagePath, "utf8");
const index = fs.readFileSync(path.join(root, "docs", "index.md"), "utf8");

assert.ok(page.includes("PortalPrototype"));
assert.ok(page.includes("<Layout"));
assert.ok(page.includes("noFooter"));
assert.ok(index.includes("slug: /"));
assert.ok(index.includes("# Welcome to Bluefin"));
});

test("scoped CSS clips artwork and defines mobile and reduced-motion paths", () => {
const css = fs.readFileSync(cssPath, "utf8");

assert.match(css, /\.parallaxViewport\s*\{[^}]*overflow:\s*clip/s);
assert.match(css, /\.parallaxLayer\s*\{[^}]*position:\s*absolute/s);
assert.match(
css,
/\.contentScene\s*\{[^}]*scroll-margin-top:\s*var\(--ifm-navbar-height/s,
);
assert.match(css, /@media \(max-width:\s*956px\)/);
assert.match(css, /@media \(prefers-reduced-motion:\s*reduce\)/);
const sharedGridMatches = css.match(/\.landingGrid,\s*\.twoColumn/g);
assert.equal(sharedGridMatches?.length, 2);
assert.ok(!css.includes("scroll-behavior"));
assert.ok(!css.includes("html {"));
assert.ok(!css.includes(":root {"));
});
Loading
Loading