From ec7828ecafc3bd1f328934529370ac933c04a7fd Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 15:45:15 -0500 Subject: [PATCH 1/7] Move react-icon-browser's e2e suite and guard configs into place Verbatim moves out of .dev/imported/react-icon-browser: the Playwright suite, its baselines and harness pages to e2e/browser/, the dependency-cruiser config and the domain tsconfig to the root. The next commit rewires their paths. --- ...ependency-cruiser.cjs => .dependency-cruiser.cjs | 0 .../e2e => e2e/browser}/a11y.spec.ts | 0 .../e2e => e2e/browser}/browser.spec.ts | 0 .../e2e => e2e/browser}/consent-drag.spec.ts | 0 .../e2e => e2e/browser}/contrast.spec.ts | 0 .../e2e => e2e/browser}/credits.spec.ts | 0 .../browser}/embedded.html | 0 .../e2e => e2e/browser}/embedded.spec.ts | 0 .../e2e => e2e/browser}/exports.spec.ts | 0 .../e2e => e2e/browser}/first-run.spec.ts | 0 .../e2e => e2e/browser}/fixtures/overlap.js | 0 .../browser}/fixtures/react-landingpage-styles.css | 0 .../e2e => e2e/browser}/focus-return.spec.ts | 0 .../e2e => e2e/browser}/forms.spec.ts | 0 .../e2e => e2e/browser}/grid.spec.ts | 0 .../react-icon-browser => e2e/browser}/index.html | 0 .../e2e => e2e/browser}/mobile-header.spec.ts | 0 .../e2e => e2e/browser}/overlap.spec.ts | 0 .../browser}/playwright.config.ts | 0 .../browser}/playwright.visual.config.ts | 0 .../e2e => e2e/browser}/popover-caret.spec.ts | 0 .../e2e => e2e/browser}/scale.spec.ts | 0 .../e2e => e2e/browser}/selects.spec.ts | 0 .../e2e => e2e/browser}/settings.spec.ts | 0 .../e2e => e2e/browser}/shortcuts.spec.ts | 0 .../e2e => e2e/browser}/svg-baseline.spec.ts | 0 .../svg-icon-sets-flag-chromium-linux.png | Bin .../svg-icon-sets-tabler-chromium-linux.png | Bin .../e2e => e2e/browser}/theme-scope.spec.ts | 0 .../e2e => e2e/browser}/tooltip.spec.ts | 0 .../tsconfig.e2e.json => e2e/browser/tsconfig.json | 0 .../e2e => e2e/browser}/visual.spec.ts | 0 .../auth-create-chromium-linux.png | Bin .../auth-signin-chromium-linux.png | Bin .../browser-dark-chromium-linux.png | Bin .../browser-light-chromium-linux.png | Bin .../colour-picker-chromium-linux.png | Bin .../detail-dialog-chromium-linux.png | Bin .../embedded-host-chromium-linux.png | Bin .../sidebar-chromium-linux.png | Bin .../toolbar-chromium-linux.png | Bin .../tsconfig.domain.json => tsconfig.domain.json | 0 42 files changed, 0 insertions(+), 0 deletions(-) rename .dev/imported/react-icon-browser/.dependency-cruiser.cjs => .dependency-cruiser.cjs (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/a11y.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/browser.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/consent-drag.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/contrast.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/credits.spec.ts (100%) rename {.dev/imported/react-icon-browser => e2e/browser}/embedded.html (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/embedded.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/exports.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/first-run.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/fixtures/overlap.js (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/fixtures/react-landingpage-styles.css (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/focus-return.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/forms.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/grid.spec.ts (100%) rename {.dev/imported/react-icon-browser => e2e/browser}/index.html (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/mobile-header.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/overlap.spec.ts (100%) rename {.dev/imported/react-icon-browser => e2e/browser}/playwright.config.ts (100%) rename {.dev/imported/react-icon-browser => e2e/browser}/playwright.visual.config.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/popover-caret.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/scale.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/selects.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/settings.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/shortcuts.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/svg-baseline.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/svg-baseline.spec.ts-snapshots/svg-icon-sets-flag-chromium-linux.png (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/svg-baseline.spec.ts-snapshots/svg-icon-sets-tabler-chromium-linux.png (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/theme-scope.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/tooltip.spec.ts (100%) rename .dev/imported/react-icon-browser/tsconfig.e2e.json => e2e/browser/tsconfig.json (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/visual.spec.ts (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/visual.spec.ts-snapshots/auth-create-chromium-linux.png (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/visual.spec.ts-snapshots/auth-signin-chromium-linux.png (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/visual.spec.ts-snapshots/browser-dark-chromium-linux.png (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/visual.spec.ts-snapshots/browser-light-chromium-linux.png (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/visual.spec.ts-snapshots/colour-picker-chromium-linux.png (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/visual.spec.ts-snapshots/detail-dialog-chromium-linux.png (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/visual.spec.ts-snapshots/embedded-host-chromium-linux.png (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/visual.spec.ts-snapshots/sidebar-chromium-linux.png (100%) rename {.dev/imported/react-icon-browser/e2e => e2e/browser}/visual.spec.ts-snapshots/toolbar-chromium-linux.png (100%) rename .dev/imported/react-icon-browser/tsconfig.domain.json => tsconfig.domain.json (100%) diff --git a/.dev/imported/react-icon-browser/.dependency-cruiser.cjs b/.dependency-cruiser.cjs similarity index 100% rename from .dev/imported/react-icon-browser/.dependency-cruiser.cjs rename to .dependency-cruiser.cjs diff --git a/.dev/imported/react-icon-browser/e2e/a11y.spec.ts b/e2e/browser/a11y.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/a11y.spec.ts rename to e2e/browser/a11y.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/browser.spec.ts b/e2e/browser/browser.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/browser.spec.ts rename to e2e/browser/browser.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/consent-drag.spec.ts b/e2e/browser/consent-drag.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/consent-drag.spec.ts rename to e2e/browser/consent-drag.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/contrast.spec.ts b/e2e/browser/contrast.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/contrast.spec.ts rename to e2e/browser/contrast.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/credits.spec.ts b/e2e/browser/credits.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/credits.spec.ts rename to e2e/browser/credits.spec.ts diff --git a/.dev/imported/react-icon-browser/embedded.html b/e2e/browser/embedded.html similarity index 100% rename from .dev/imported/react-icon-browser/embedded.html rename to e2e/browser/embedded.html diff --git a/.dev/imported/react-icon-browser/e2e/embedded.spec.ts b/e2e/browser/embedded.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/embedded.spec.ts rename to e2e/browser/embedded.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/exports.spec.ts b/e2e/browser/exports.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/exports.spec.ts rename to e2e/browser/exports.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/first-run.spec.ts b/e2e/browser/first-run.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/first-run.spec.ts rename to e2e/browser/first-run.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/fixtures/overlap.js b/e2e/browser/fixtures/overlap.js similarity index 100% rename from .dev/imported/react-icon-browser/e2e/fixtures/overlap.js rename to e2e/browser/fixtures/overlap.js diff --git a/.dev/imported/react-icon-browser/e2e/fixtures/react-landingpage-styles.css b/e2e/browser/fixtures/react-landingpage-styles.css similarity index 100% rename from .dev/imported/react-icon-browser/e2e/fixtures/react-landingpage-styles.css rename to e2e/browser/fixtures/react-landingpage-styles.css diff --git a/.dev/imported/react-icon-browser/e2e/focus-return.spec.ts b/e2e/browser/focus-return.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/focus-return.spec.ts rename to e2e/browser/focus-return.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/forms.spec.ts b/e2e/browser/forms.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/forms.spec.ts rename to e2e/browser/forms.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/grid.spec.ts b/e2e/browser/grid.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/grid.spec.ts rename to e2e/browser/grid.spec.ts diff --git a/.dev/imported/react-icon-browser/index.html b/e2e/browser/index.html similarity index 100% rename from .dev/imported/react-icon-browser/index.html rename to e2e/browser/index.html diff --git a/.dev/imported/react-icon-browser/e2e/mobile-header.spec.ts b/e2e/browser/mobile-header.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/mobile-header.spec.ts rename to e2e/browser/mobile-header.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/overlap.spec.ts b/e2e/browser/overlap.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/overlap.spec.ts rename to e2e/browser/overlap.spec.ts diff --git a/.dev/imported/react-icon-browser/playwright.config.ts b/e2e/browser/playwright.config.ts similarity index 100% rename from .dev/imported/react-icon-browser/playwright.config.ts rename to e2e/browser/playwright.config.ts diff --git a/.dev/imported/react-icon-browser/playwright.visual.config.ts b/e2e/browser/playwright.visual.config.ts similarity index 100% rename from .dev/imported/react-icon-browser/playwright.visual.config.ts rename to e2e/browser/playwright.visual.config.ts diff --git a/.dev/imported/react-icon-browser/e2e/popover-caret.spec.ts b/e2e/browser/popover-caret.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/popover-caret.spec.ts rename to e2e/browser/popover-caret.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/scale.spec.ts b/e2e/browser/scale.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/scale.spec.ts rename to e2e/browser/scale.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/selects.spec.ts b/e2e/browser/selects.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/selects.spec.ts rename to e2e/browser/selects.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/settings.spec.ts b/e2e/browser/settings.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/settings.spec.ts rename to e2e/browser/settings.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/shortcuts.spec.ts b/e2e/browser/shortcuts.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/shortcuts.spec.ts rename to e2e/browser/shortcuts.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/svg-baseline.spec.ts b/e2e/browser/svg-baseline.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/svg-baseline.spec.ts rename to e2e/browser/svg-baseline.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/svg-baseline.spec.ts-snapshots/svg-icon-sets-flag-chromium-linux.png b/e2e/browser/svg-baseline.spec.ts-snapshots/svg-icon-sets-flag-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/svg-baseline.spec.ts-snapshots/svg-icon-sets-flag-chromium-linux.png rename to e2e/browser/svg-baseline.spec.ts-snapshots/svg-icon-sets-flag-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/e2e/svg-baseline.spec.ts-snapshots/svg-icon-sets-tabler-chromium-linux.png b/e2e/browser/svg-baseline.spec.ts-snapshots/svg-icon-sets-tabler-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/svg-baseline.spec.ts-snapshots/svg-icon-sets-tabler-chromium-linux.png rename to e2e/browser/svg-baseline.spec.ts-snapshots/svg-icon-sets-tabler-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/e2e/theme-scope.spec.ts b/e2e/browser/theme-scope.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/theme-scope.spec.ts rename to e2e/browser/theme-scope.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/tooltip.spec.ts b/e2e/browser/tooltip.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/tooltip.spec.ts rename to e2e/browser/tooltip.spec.ts diff --git a/.dev/imported/react-icon-browser/tsconfig.e2e.json b/e2e/browser/tsconfig.json similarity index 100% rename from .dev/imported/react-icon-browser/tsconfig.e2e.json rename to e2e/browser/tsconfig.json diff --git a/.dev/imported/react-icon-browser/e2e/visual.spec.ts b/e2e/browser/visual.spec.ts similarity index 100% rename from .dev/imported/react-icon-browser/e2e/visual.spec.ts rename to e2e/browser/visual.spec.ts diff --git a/.dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/auth-create-chromium-linux.png b/e2e/browser/visual.spec.ts-snapshots/auth-create-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/auth-create-chromium-linux.png rename to e2e/browser/visual.spec.ts-snapshots/auth-create-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/auth-signin-chromium-linux.png b/e2e/browser/visual.spec.ts-snapshots/auth-signin-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/auth-signin-chromium-linux.png rename to e2e/browser/visual.spec.ts-snapshots/auth-signin-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/browser-dark-chromium-linux.png b/e2e/browser/visual.spec.ts-snapshots/browser-dark-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/browser-dark-chromium-linux.png rename to e2e/browser/visual.spec.ts-snapshots/browser-dark-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/browser-light-chromium-linux.png b/e2e/browser/visual.spec.ts-snapshots/browser-light-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/browser-light-chromium-linux.png rename to e2e/browser/visual.spec.ts-snapshots/browser-light-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/colour-picker-chromium-linux.png b/e2e/browser/visual.spec.ts-snapshots/colour-picker-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/colour-picker-chromium-linux.png rename to e2e/browser/visual.spec.ts-snapshots/colour-picker-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/detail-dialog-chromium-linux.png b/e2e/browser/visual.spec.ts-snapshots/detail-dialog-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/detail-dialog-chromium-linux.png rename to e2e/browser/visual.spec.ts-snapshots/detail-dialog-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/embedded-host-chromium-linux.png b/e2e/browser/visual.spec.ts-snapshots/embedded-host-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/embedded-host-chromium-linux.png rename to e2e/browser/visual.spec.ts-snapshots/embedded-host-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/sidebar-chromium-linux.png b/e2e/browser/visual.spec.ts-snapshots/sidebar-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/sidebar-chromium-linux.png rename to e2e/browser/visual.spec.ts-snapshots/sidebar-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/toolbar-chromium-linux.png b/e2e/browser/visual.spec.ts-snapshots/toolbar-chromium-linux.png similarity index 100% rename from .dev/imported/react-icon-browser/e2e/visual.spec.ts-snapshots/toolbar-chromium-linux.png rename to e2e/browser/visual.spec.ts-snapshots/toolbar-chromium-linux.png diff --git a/.dev/imported/react-icon-browser/tsconfig.domain.json b/tsconfig.domain.json similarity index 100% rename from .dev/imported/react-icon-browser/tsconfig.domain.json rename to tsconfig.domain.json From 0ddea903dc060f35f248b5d72604787756f220f8 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 15:52:42 -0500 Subject: [PATCH 2/7] Wire react-icon-browser's e2e suite and guards to this layout e2e/browser/vite.config.ts builds the standalone harness pages from resources/js and serves them at the root, as react-icon-browser's preview did. The specs, Playwright configs, dependency-cruiser rules, domain tsconfig and the guard scripts in .dev/browser/ now name resources/js, public/assets/css and e2e/browser. The SVG rendering sample keeps the public packs only (tabler, flag): bundled and metronic are private and this repository is public. Its 18 files are react-icon-browser's own, so the committed tabler and flag baselines still apply. check-dead-links found a stale relative link in resources/assets/fonts/README.md; it is fixed. @playwright/test is ~1.65.0 to match the pinned image. --- .dependency-cruiser.cjs | 56 ++++++------ .dev/browser/check-boundary-rules.mjs | 13 +-- .dev/browser/check-dead-classes.mjs | 20 +++-- .dev/browser/check-playwright-image.mjs | 7 +- .dev/browser/linux.sh | 8 +- .dev/browser/sample-svg-archetypes.mjs | 17 +++- .dev/browser/schema-types.mjs | 9 +- .dev/browser/visual-linux.sh | 20 ++--- .gitignore | 5 ++ e2e/browser/a11y.spec.ts | 2 +- e2e/browser/browser.spec.ts | 2 +- e2e/browser/embedded.html | 4 +- e2e/browser/embedded.spec.ts | 2 +- e2e/browser/exports.spec.ts | 2 +- e2e/browser/index.html | 4 +- e2e/browser/mobile-header.spec.ts | 2 +- e2e/browser/playwright.config.ts | 13 ++- e2e/browser/playwright.visual.config.ts | 4 +- e2e/browser/svg-baseline.spec.ts | 15 ++-- e2e/browser/tsconfig.json | 4 +- e2e/browser/visual.spec.ts | 6 +- e2e/browser/vite.config.ts | 112 ++++++++++++++++++++++++ package.json | 20 ++++- resources/assets/fonts/README.md | 23 ++--- tsconfig.domain.json | 12 ++- 25 files changed, 282 insertions(+), 100 deletions(-) create mode 100644 e2e/browser/vite.config.ts diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs index 0fe4990..26cc908 100644 --- a/.dependency-cruiser.cjs +++ b/.dependency-cruiser.cjs @@ -27,9 +27,9 @@ * hand-written exception list did not know about the new file. */ // src/app/ is the composition root architecture.md 3.2 names (the browser() builder first). -const APP = '^src/(App|main|mount|embedded|IchavaBrowser)\\.tsx$|^src/app/'; +const APP = '^resources/js/(StandaloneApp|app|main|mount|embedded|IchavaBrowser)\\.tsx$|^resources/js/(app|pages)/'; /** The package barrel re-exports the public surface, entry points included. */ -const BARREL = '^src/index\\.ts$'; +const BARREL = '^resources/js/index\\.ts$'; module.exports = { forbidden: [ @@ -40,7 +40,7 @@ module.exports = { 'return type puts React into the public surface a Vue or Blade consumer inherits. ' + 'Use the plain StyleObject from core/types instead, and put the hook in hooks/.', severity: 'error', - from: { path: '^src/core/' }, + from: { path: '^resources/js/core/' }, // NOTE: `to.path` matches the RESOLVED path, not the import specifier, so this must // be written against node_modules. Written as '^react$' the rule silently never // fires -- which is how it shipped the first time, and why every rule here has a @@ -53,8 +53,12 @@ module.exports = { 'core/ must not depend on any other layer. Test fixtures are the one exception: ' + 'src/test/ is shared scaffolding, not a layer.', severity: 'error', - from: { path: '^src/core/', pathNot: '\\.test\\.tsx?$' }, - to: { path: `^src/(components|hooks|store\\.ts)|${APP}` }, + // Two exemptions, measured when these rules came over from react-icon-browser (react + // consolidation 3.3, 2026-11-13): the Inertia adapter's InertiaCatalog.ts and browserMount.ts + // predate the merge and import the query helpers in hooks/useFilterSync.ts. A third file + // that does the same fails. + from: { path: '^resources/js/core/', pathNot: ['\\.test\\.tsx?$', '^resources/js/core/(InertiaCatalog|browserMount)\\.ts$'] }, + to: { path: `^resources/js/(components|hooks|store\\.ts)|${APP}` }, }, { name: 'core-tests-stay-in-core', @@ -62,14 +66,14 @@ module.exports = { 'A test under core/ that imports a component is testing the wrong layer -- move the ' + 'test next to what it covers. src/test/ fixtures are allowed.', severity: 'error', - from: { path: '^src/core/.*\\.test\\.tsx?$' }, - to: { path: '^src/(components|hooks)/' }, + from: { path: '^resources/js/core/.*\\.test\\.tsx?$' }, + to: { path: '^resources/js/(components|hooks)/' }, }, { name: 'data-no-react', comment: 'The store and data layer must stay portable: no React imports.', severity: 'error', - from: { path: '^src/(store\\.ts|data/)' }, + from: { path: '^resources/js/(store\\.ts|data/)' }, // NOTE: `to.path` matches the RESOLVED path, not the import specifier, so this must // be written against node_modules. Written as '^react$' the rule silently never // fires -- which is how it shipped the first time, and why every rule here has a @@ -80,7 +84,7 @@ module.exports = { name: 'ui-not-into-app', comment: 'Components take props; the app wires them. ui/ must not reach into app/.', severity: 'error', - from: { path: '^src/(components|hooks)/' }, + from: { path: '^resources/js/(components|hooks)/' }, to: { path: APP }, }, { @@ -111,31 +115,31 @@ module.exports = { 'A module domain imports no framework: no React, zustand, react-aria or Untitled UI, ' + 'type-only imports included. The domain tsconfig drops DOM from lib for the same reason.', severity: 'error', - from: { path: '^src/modules/[^/]+/domain/' }, + from: { path: '^resources/js/modules/[^/]+/domain/' }, to: { path: '^node_modules/(react|react-dom|zustand|react-aria|react-aria-components|@react-aria|@react-stately|@react-types|@untitledui)(/|$)' }, }, { name: 'module-domain-is-innermost', comment: 'A domain depends on nothing outward: not its own application or infrastructure, not ui, state or the app.', severity: 'error', - from: { path: '^src/modules/[^/]+/domain/' }, - to: { path: `^src/(components|hooks|state|ui|app)/|^src/store\\.ts$|^src/modules/[^/]+/(application|infrastructure)/|${APP}` }, + from: { path: '^resources/js/modules/[^/]+/domain/' }, + to: { path: `^resources/js/(components|hooks|state|ui|app)/|^resources/js/store\\.ts$|^resources/js/modules/[^/]+/(application|infrastructure)/|${APP}` }, }, { name: 'module-application-not-outward', comment: 'Application services use the domain and ports; adapters, ui, state and React stay outside.', severity: 'error', - from: { path: '^src/modules/[^/]+/application/' }, + from: { path: '^resources/js/modules/[^/]+/application/' }, to: { - path: `^src/(components|hooks|state|ui|app)/|^src/store\\.ts$|^src/modules/[^/]+/infrastructure/|${APP}|^node_modules/(react|react-dom|zustand)(/|$)`, + path: `^resources/js/(components|hooks|state|ui|app)/|^resources/js/store\\.ts$|^resources/js/modules/[^/]+/infrastructure/|${APP}|^node_modules/(react|react-dom|zustand)(/|$)`, }, }, { name: 'module-infrastructure-not-into-ui', comment: 'Adapters implement ports; they never reach ui, state, the app or React.', severity: 'error', - from: { path: '^src/modules/[^/]+/infrastructure/' }, - to: { path: `^src/(components|hooks|state|ui|app)/|^src/store\\.ts$|${APP}|^node_modules/(react|react-dom)(/|$)` }, + from: { path: '^resources/js/modules/[^/]+/infrastructure/' }, + to: { path: `^resources/js/(components|hooks|state|ui|app)/|^resources/js/store\\.ts$|${APP}|^node_modules/(react|react-dom)(/|$)` }, }, { name: 'modules-meet-through-index', @@ -143,15 +147,15 @@ module.exports = { "One module reaches another only through that module's index.ts, so a module's " + 'internals can move without breaking its neighbours.', severity: 'error', - from: { path: '^src/modules/([^/]+)/' }, - to: { path: '^src/modules/[^/]+/', pathNot: ['^src/modules/$1/', '^src/modules/[^/]+/index\\.ts$'] }, + from: { path: '^resources/js/modules/([^/]+)/' }, + to: { path: '^resources/js/modules/[^/]+/', pathNot: ['^resources/js/modules/$1/', '^resources/js/modules/[^/]+/index\\.ts$'] }, }, { name: 'state-not-into-ui', comment: 'The store slices hold state and call domain and application code; they never import ui.', severity: 'error', - from: { path: '^src/state/' }, - to: { path: `^src/(components|hooks|ui)/|${APP}|^node_modules/(react|react-dom)(/|$)` }, + from: { path: '^resources/js/state/' }, + to: { path: `^resources/js/(components|hooks|ui)/|${APP}|^node_modules/(react|react-dom)(/|$)` }, }, { name: 'no-circular', @@ -172,7 +176,7 @@ module.exports = { '(^|/)\\.[^/]+\\.(cjs|mjs|js|ts)$', '\\.d\\.ts$', '(^|/)tsconfig\\.[^/]+\\.json$', - '^src/(main|mount|embedded)\\.tsx$', + '^resources/js/(main|mount|embedded|app)\\.tsx$', ], }, to: {}, @@ -184,10 +188,12 @@ module.exports = { 'no-orphans only sees modules with no edges at all, so a dead file that imported ' + 'clsx (src/lib/utils.ts) passed it. Tests, declaration files and configs are exempt.', severity: 'error', - from: { path: '^src/(main|mount|embedded)\\.tsx$|^src/index\\.ts$' }, + // app.tsx is the Inertia entry; it loads pages/ through import.meta.glob, which the + // cruiser cannot follow, so each page is an entry point too. + from: { path: '^resources/js/(main|mount|embedded|app)\\.tsx$|^resources/js/index\\.ts$|^resources/js/pages/' }, to: { - path: '^src/', - pathNot: ['\\.test\\.tsx?$', '^src/test/', '\\.d\\.ts$', '^src/(main|mount|embedded)\\.tsx$', '^src/index\\.ts$'], + path: '^resources/js/', + pathNot: ['\\.test\\.tsx?$', '^resources/js/test/', '\\.d\\.ts$', '^resources/js/(main|mount|embedded|app)\\.tsx$', '^resources/js/index\\.ts$', '^resources/js/pages/'], reachable: false, }, }, @@ -196,7 +202,7 @@ module.exports = { options: { doNotFollow: { path: 'node_modules' }, tsPreCompilationDeps: true, // so type-only imports are seen -- the whole point of core-no-react - tsConfig: { fileName: 'tsconfig.app.json' }, + tsConfig: { fileName: 'tsconfig.inertia.json' }, enhancedResolveOptions: { exportsFields: ['exports'], conditionNames: ['import', 'require', 'types'] }, reporterOptions: { text: { highlightFocused: true } }, }, diff --git a/.dev/browser/check-boundary-rules.mjs b/.dev/browser/check-boundary-rules.mjs index 2e76097..7ac9e4a 100644 --- a/.dev/browser/check-boundary-rules.mjs +++ b/.dev/browser/check-boundary-rules.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // Proves the module boundary rules in .dependency-cruiser.cjs can fail. // -// The rules for src/modules/ and src/state/ land before the code they govern +// The rules for resources/js/modules/ and resources/js/state/ land before the code they govern // (architecture.md 4.1), so on a tree with no modules they match nothing, and "no // violations" would say nothing about whether they work. `core-no-react` once shipped // matching nothing for exactly that reason. @@ -16,10 +16,11 @@ import { mkdirSync, rmdirSync, rmSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; -const root = join(dirname(fileURLToPath(import.meta.url)), '..'); -const A = 'src/modules/__probe_a__'; -const B = 'src/modules/__probe_b__'; -const STATE = 'src/state/__probe__.ts'; +// .dev/browser/ -> the repository root (react consolidation 3.3, 2026-11-13). +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const A = 'resources/js/modules/__probe_a__'; +const B = 'resources/js/modules/__probe_b__'; +const STATE = 'resources/js/state/__probe__.ts'; const PLANTED = [A, B, STATE]; /** file -> source. Each import names an existing module or another plant. */ @@ -48,7 +49,7 @@ const CONTROL = `${B}/domain/through-index.ts`; function cleanup() { for (const p of PLANTED) rmSync(join(root, p), { recursive: true, force: true }); // Remove the parent directories too when the plants were all they held. - for (const dir of ['src/modules', 'src/state']) { + for (const dir of ['resources/js/modules', 'resources/js/state']) { try { rmdirSync(join(root, dir)); } catch { diff --git a/.dev/browser/check-dead-classes.mjs b/.dev/browser/check-dead-classes.mjs index 20df0ef..91a1125 100644 --- a/.dev/browser/check-dead-classes.mjs +++ b/.dev/browser/check-dead-classes.mjs @@ -15,7 +15,10 @@ * source that was meant to produce it. Grepping the Tailwind config or the component * proves how the class was written, never what the compiler kept. * - * Run after `npm run build`, against `dist/`. + * Run after `npm run build:prod`, against the committed bundle's stylesheet in public/assets/css/, + * which is what a host receives. Moved from react-icon-browser's scripts/ (react consolidation 3.3, + * 2026-11-13): it read that repository's dist/assets and src/; the standalone harness pages it + * also scanned now live in e2e/browser/. * * WHAT IT DOES NOT COVER, so nobody reads a pass as more than it is: * @@ -32,9 +35,10 @@ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; import { join, extname, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; -const ROOT = fileURLToPath(new URL('..', import.meta.url)); -const CSS_DIR = join(ROOT, 'dist/assets'); -const SRC_DIR = join(ROOT, 'src'); +const ROOT = fileURLToPath(new URL('../..', import.meta.url)); +const CSS_DIR = join(ROOT, 'public/assets/css'); +const SRC_DIR = join(ROOT, 'resources/js'); +const HTML_DIR = join(ROOT, 'e2e/browser'); /** * Class names that are expected to emit nothing, each with the reason it is not a defect. @@ -84,9 +88,9 @@ function collectTokens() { }; walk(SRC_DIR).forEach(scan); - readdirSync(ROOT) + readdirSync(HTML_DIR) .filter((f) => f.endsWith('.html')) - .forEach((f) => scan(join(ROOT, f))); + .forEach((f) => scan(join(HTML_DIR, f))); return tokens; } @@ -98,11 +102,11 @@ function collectTokens() { */ function loadCss() { if (!existsSync(CSS_DIR)) { - fail(`No ${relative(ROOT, CSS_DIR)} directory. Run \`npm run build\` first.`); + fail(`No ${relative(ROOT, CSS_DIR)} directory. Run \`npm run build:prod\` first.`); } const files = readdirSync(CSS_DIR).filter((f) => f.endsWith('.css')); if (files.length === 0) { - fail(`No CSS in ${relative(ROOT, CSS_DIR)}. Run \`npm run build\` first.`); + fail(`No CSS in ${relative(ROOT, CSS_DIR)}. Run \`npm run build:prod\` first.`); } const css = files.map((f) => readFileSync(join(CSS_DIR, f), 'utf8')).join('\n'); return { css: css.replace(/\\/g, ''), files }; diff --git a/.dev/browser/check-playwright-image.mjs b/.dev/browser/check-playwright-image.mjs index 8bb027d..69967a0 100644 --- a/.dev/browser/check-playwright-image.mjs +++ b/.dev/browser/check-playwright-image.mjs @@ -22,13 +22,14 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; -const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +// .dev/browser/ -> the repository root (react consolidation 3.3, 2026-11-13). +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const IMAGE = /mcr\.microsoft\.com\/playwright:(v[\d.]+-\w+)/g; const sources = [ - ['.github/workflows/ci.yml', 'CI visual job container'], + ['.github/workflows/browser.yml', 'CI visual job container'], ['.github/workflows/visual-baselines.yml', 'seed workflow container'], - ['scripts/linux.sh', 'npm run visual:linux, visual:linux:check and e2e:linux'], + ['.dev/browser/linux.sh', 'npm run visual:linux, visual:linux:check and e2e:linux'], ]; const found = []; diff --git a/.dev/browser/linux.sh b/.dev/browser/linux.sh index 919fa63..82ea425 100644 --- a/.dev/browser/linux.sh +++ b/.dev/browser/linux.sh @@ -7,17 +7,17 @@ # npm run e2e:linux run the behavioural E2E suite # # Needs Docker. Arguments after the mode go to `playwright test` (a spec file, --grep ...). -# The image is pinned here once; check:playwright-image fails if it drifts from ci.yml's +# The image is pinned here once; check:playwright-image fails if it drifts from browser.yml's # visual job, the seed workflow or @playwright/test. set -eu IMAGE=mcr.microsoft.com/playwright:v1.65.0-noble -mode=${1:?usage: scripts/linux.sh visual-update|visual-check|e2e [playwright args]} +mode=${1:?usage: .dev/browser/linux.sh visual-update|visual-check|e2e [playwright args]} shift if ! command -v docker >/dev/null 2>&1; then - echo "scripts/linux.sh: Docker is not installed or not on PATH; CI's image needs it." >&2 + echo ".dev/browser/linux.sh: Docker is not installed or not on PATH; CI's image needs it." >&2 exit 127 fi -exec docker run --rm --ipc=host -v "$PWD":/src "$IMAGE" sh /src/scripts/visual-linux.sh "$mode" "$@" +exec docker run --rm --ipc=host -v "$PWD":/src "$IMAGE" sh /src/.dev/browser/visual-linux.sh "$mode" "$@" diff --git a/.dev/browser/sample-svg-archetypes.mjs b/.dev/browser/sample-svg-archetypes.mjs index 060f155..336d16a 100644 --- a/.dev/browser/sample-svg-archetypes.mjs +++ b/.dev/browser/sample-svg-archetypes.mjs @@ -9,7 +9,7 @@ * Deterministic: directory entries are sorted before sampling, so re-running picks the * same files and the baseline does not churn. * - * Writes to `public/baseline/` plus a manifest the spec reads. + * Writes to `.dev/browser/public/baseline/` plus a manifest the spec reads. */ import { readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join, basename, dirname } from 'node:path'; @@ -20,10 +20,19 @@ import { fileURLToPath } from 'node:url'; // list of directory names that the 2026-09-21 rebrand renamed -- three ways of // being wrong, none of which anything reported, because the script is run by // hand and is not wired to an npm script. -const APP = dirname(dirname(fileURLToPath(import.meta.url))); +// +// Moved from react-icon-browser's scripts/ to icon-browser's .dev/browser/ (react consolidation +// 3.1, 2026-11-13): the repository root is two levels up, the packs sit beside it in packages/, +// and the sample lands in the e2e harness's public directory. +// +// Public packs only. icon-browser is public, and icon-sets-bundled and icon-sets-metronic are +// private (Metronic "is never shown publicly"), so their samples and contact sheets stay in the +// retired react-icon-browser and are not reproduced here. The style_block and foreign archetypes +// came only from those two packs; e2e/browser/svg-baseline.spec.ts says so where it checks. +const APP = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); const ROOT = dirname(APP); -const OUT = join(APP, 'public', 'baseline'); -const PACKS = ['icon-sets-tabler', 'icon-sets-bundled', 'icon-sets-metronic', 'icon-sets-flag']; +const OUT = join(APP, '.dev', 'browser', 'public', 'baseline'); +const PACKS = ['icon-sets-tabler', 'icon-sets-flag']; const FEATURES = { style_block: (s) => /]/i.test(s), diff --git a/.dev/browser/schema-types.mjs b/.dev/browser/schema-types.mjs index 55b8719..58189fa 100644 --- a/.dev/browser/schema-types.mjs +++ b/.dev/browser/schema-types.mjs @@ -1,6 +1,13 @@ #!/usr/bin/env node // Types for the icon-browser payloads, generated from its JSON Schema. // +// Since react consolidation 3d (2026-11-12) the schema and the browser are in this one +// repository: the generator reads resources/schema/icon-browser.schema.json and writes +// resources/js/core/api/schema.gen.ts, and the record of the schema it was generated from is +// .dev/imported/react-icon-browser/schema/source.json (SOURCE_PATH). Run it with +// `npm run schema:types`, and check with `npm run check:schema-types`. The text below is +// react-icon-browser's, from when the schema was copied across repositories. +// // icon-browser owns the schema (resources/schema/icon-browser.schema.json); this repo // keeps a copy in schema/, with the commit it came from in schema/source.json, because // the two are separate repositories and CI cannot read across them. The generated file @@ -197,7 +204,7 @@ function main() { let current = ''; try { current = readFileSync(OUTPUT_PATH, 'utf8'); } catch { /* missing counts as drift */ } if (current !== text) { - console.error('src/core/api/schema.gen.ts does not match schema/icon-browser.schema.json. Run `npm run schema:types`.'); + console.error('resources/js/core/api/schema.gen.ts does not match resources/schema/icon-browser.schema.json. Run `npm run schema:types`.'); process.exit(1); } console.log(`schema types match (${Object.keys(schema.$defs ?? {}).length} definitions, schema ${schema.version}).`); diff --git a/.dev/browser/visual-linux.sh b/.dev/browser/visual-linux.sh index b940f6d..59f460e 100755 --- a/.dev/browser/visual-linux.sh +++ b/.dev/browser/visual-linux.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Runs inside the Playwright image for scripts/linux.sh, against a copy of the checkout. +# Runs inside the Playwright image for .dev/browser/linux.sh, against a copy of the checkout. # # Mounting the checkout and installing in place failed two ways. In a git worktree `.git` is a # pointer file to a directory outside the mount, so npm's git lookup for a git dependency died @@ -23,7 +23,7 @@ src=${VISUAL_SRC:-/src} work=${VISUAL_WORK:-$(mktemp -d)} tar -C "$src" \ - --exclude=./.git --exclude=./node_modules --exclude=./dist --exclude=./.claude \ + --exclude=./.git --exclude=./node_modules --exclude=./.cache --exclude=./vendor --exclude=./.claude \ --exclude=./test-results --exclude=./playwright-report \ -cf - . | tar -C "$work" -xf - @@ -33,29 +33,29 @@ npm install --no-audit --no-fund status=0 case "$mode" in e2e) - npx playwright test "$@" || status=$? + npx playwright test --config=e2e/browser/playwright.config.ts "$@" || status=$? ;; visual-check) - npx playwright test --config=playwright.visual.config.ts "$@" || status=$? + npx playwright test --config=e2e/browser/playwright.visual.config.ts "$@" || status=$? ;; visual-update) # Measure first, so the summary can say how far each baseline moved. - npx playwright test --config=playwright.visual.config.ts --reporter=json "$@" > "$work/.visual-drift.json" 2>/dev/null || true - npx playwright test --config=playwright.visual.config.ts --update-snapshots "$@" || status=$? + npx playwright test --config=e2e/browser/playwright.visual.config.ts --reporter=json "$@" > "$work/.visual-drift.json" 2>/dev/null || true + npx playwright test --config=e2e/browser/playwright.visual.config.ts --update-snapshots "$@" || status=$? changed="" - for f in e2e/*-snapshots/*.png; do + for f in e2e/browser/*-snapshots/*.png; do [ -f "$f" ] || continue if ! cmp -s "$f" "$src/$f"; then changed="$changed $f"; fi done - for dir in e2e/*-snapshots; do - if [ -d "$dir" ]; then cp -R "$dir" "$src/e2e/"; fi + for dir in e2e/browser/*-snapshots; do + if [ -d "$dir" ]; then cp -R "$dir" "$src/e2e/browser/"; fi done if [ -n "$changed" ]; then echo echo "Changed baselines (review them with git diff before committing):" # shellcheck disable=SC2086 # one path per word, by construction # The table is a courtesy: a missing report must not fail a run that regenerated fine. - node scripts/visual-drift.mjs "$work/.visual-drift.json" $changed || echo "(drift table unavailable)" + node .dev/browser/visual-drift.mjs "$work/.visual-drift.json" $changed || echo "(drift table unavailable)" else echo "Baselines already match; nothing changed." fi diff --git a/.gitignore b/.gitignore index dff012f..4e1510d 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,8 @@ package-lock.json /.dev/browser/public/baseline/ /.dev/imported/react-icon-browser/e2e/svg-baseline.spec.ts-snapshots/*bundled* /.dev/imported/react-icon-browser/e2e/svg-baseline.spec.ts-snapshots/*metronic* + +# The e2e harness build (e2e/browser/vite.config.ts) and Playwright output. +/.cache/ +/playwright-report/ +/test-results/ diff --git a/e2e/browser/a11y.spec.ts b/e2e/browser/a11y.spec.ts index 5d9e030..37fa282 100644 --- a/e2e/browser/a11y.spec.ts +++ b/e2e/browser/a11y.spec.ts @@ -8,7 +8,7 @@ import { expect, test, type Page } from '@playwright/test'; * * The theme is chosen through `prefers-color-scheme` with no stored preference, so this * also exercises L9: a first load follows the system scheme. The structural rules are - * pinned in jsdom too (src/IchavaBrowser.a11y.test.tsx); colour contrast needs a real + * pinned in jsdom too (resources/js/IchavaBrowser.a11y.test.tsx); colour contrast needs a real * renderer, which is why this suite exists. * * Moderate and minor findings are not gated here. Lower the bar only with a reason. diff --git a/e2e/browser/browser.spec.ts b/e2e/browser/browser.spec.ts index 6475a5c..77011bd 100644 --- a/e2e/browser/browser.spec.ts +++ b/e2e/browser/browser.spec.ts @@ -25,7 +25,7 @@ async function dismissFirstRun(page: Page) { // assets; the next regeneration made it 200 and every spec in this file failed on // a `beforeEach` that had nothing to do with what they test. const FIXTURE_ICON_COUNT = JSON.parse( - readFileSync(new URL('../public/data/icons.json', import.meta.url), 'utf8'), + readFileSync(new URL('../../.dev/browser/public/data/icons.json', import.meta.url), 'utf8'), ).icons.length; const ALL_FIXTURE_ICONS = new RegExp(`${FIXTURE_ICON_COUNT} icons`); diff --git a/e2e/browser/embedded.html b/e2e/browser/embedded.html index b1dce6c..9f8c018 100644 --- a/e2e/browser/embedded.html +++ b/e2e/browser/embedded.html @@ -10,7 +10,7 @@ browser used to overwrite the html class, the design tokens on , lang, dir, document.title, the description meta and the entire query string. - Not a product page. Referenced only by e2e/embedded.spec.ts. + Not a product page. Referenced only by e2e/browser/embedded.spec.ts. --> Host Application — Dashboard @@ -28,6 +28,6 @@ - + diff --git a/e2e/browser/embedded.spec.ts b/e2e/browser/embedded.spec.ts index 7565023..72dcbcd 100644 --- a/e2e/browser/embedded.spec.ts +++ b/e2e/browser/embedded.spec.ts @@ -11,7 +11,7 @@ import { test, expect, type Page } from '@playwright/test'; * first place, so the kill switch defeated itself. * * `embedded.html` is a host page with its own title, lang, dir, meta and chrome. - * `src/embedded.tsx` mounts through `mountIchavaBrowser` with the defaults. + * `resources/js/embedded.tsx` mounts through `mountIchavaBrowser` with the defaults. */ const HOST = { diff --git a/e2e/browser/exports.spec.ts b/e2e/browser/exports.spec.ts index 9a74ee3..c161bfd 100644 --- a/e2e/browser/exports.spec.ts +++ b/e2e/browser/exports.spec.ts @@ -12,7 +12,7 @@ async function dismissFirstRun(page: Page) { await page.getByRole('button', { name: 'Skip' }).click({ timeout: 10_000 }).catch(() => {}); } -const FIXTURE_ICON_COUNT = JSON.parse(readFileSync(new URL('../public/data/icons.json', import.meta.url), 'utf8')).icons.length; +const FIXTURE_ICON_COUNT = JSON.parse(readFileSync(new URL('../../.dev/browser/public/data/icons.json', import.meta.url), 'utf8')).icons.length; test.beforeEach(async ({ page }) => { await page.addInitScript(() => localStorage.clear()); diff --git a/e2e/browser/index.html b/e2e/browser/index.html index 67c0683..d68c054 100644 --- a/e2e/browser/index.html +++ b/e2e/browser/index.html @@ -25,7 +25,7 @@ [\s\S]*'); const doc = new DOMParser().parseFromString(out, 'text/html'); diff --git a/resources/js/core/visualLinux.test.ts b/resources/js/core/visualLinux.test.ts index 977464f..5b8ff4b 100644 --- a/resources/js/core/visualLinux.test.ts +++ b/resources/js/core/visualLinux.test.ts @@ -5,7 +5,7 @@ import { join, resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; /** - * The container side of `npm run visual:linux` (`scripts/visual-linux.sh`). + * The container side of `npm run visual:linux` (`.dev/browser/visual-linux.sh`). * * Mounting the checkout and installing in place failed from a git worktree, whose `.git` is a * pointer to a directory outside the mount, and replaced the host's node_modules with Linux @@ -15,26 +15,27 @@ import { describe, expect, it } from 'vitest'; */ const root = process.cwd(); // The standalone app's Playwright tooling moved to .dev/browser/ with react consolidation 3d -// (2026-11-12), and its npm scripts stay in the imported package.json until its e2e suite is -// ported; this checks them where they are. +// (2026-11-12), and its suite to e2e/browser/ with its npm scripts in package.json in 3.1 +// (2026-11-13). const script = resolve(root, '.dev/browser/visual-linux.sh'); -const pkg = JSON.parse(readFileSync(resolve(root, '.dev/imported/react-icon-browser/package.json'), 'utf8')) as { scripts: Record }; +const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { scripts: Record }; +const SNAPSHOTS = ['e2e', 'browser', 'visual.spec.ts-snapshots']; function fakeCheckout(playwrightExit: number, args: string[] = []) { const base = mkdtempSync(join(tmpdir(), 'visual-linux-')); const src = join(base, 'src'); const bin = join(base, 'bin'); - mkdirSync(join(src, 'e2e', 'visual.spec.ts-snapshots'), { recursive: true }); + mkdirSync(join(src, ...SNAPSHOTS), { recursive: true }); mkdirSync(join(src, 'node_modules'), { recursive: true }); mkdirSync(bin); writeFileSync(join(src, '.git'), 'gitdir: /nowhere/.git/worktrees/x\n'); writeFileSync(join(src, 'node_modules', 'host.txt'), 'darwin'); - writeFileSync(join(src, 'e2e', 'visual.spec.ts-snapshots', 'toolbar.png'), 'old'); + writeFileSync(join(src, ...SNAPSHOTS, 'toolbar.png'), 'old'); writeFileSync(join(src, 'package.json'), '{}'); // npm fails if it can see .git or the host's node_modules. writeFileSync(join(bin, 'npm'), '#!/bin/sh\n[ ! -e .git ] || exit 7\n[ ! -e node_modules/host.txt ] || exit 8\nmkdir -p node_modules && echo linux > node_modules/host.txt\n'); // Records its arguments, so a test can see which suite and options a mode ran. - writeFileSync(join(bin, 'npx'), `#!/bin/sh\necho "$*" >> ${join(base, 'npx.log')}\necho new > e2e/visual.spec.ts-snapshots/toolbar.png\nexit ${playwrightExit}\n`); + writeFileSync(join(bin, 'npx'), `#!/bin/sh\necho "$*" >> ${join(base, 'npx.log')}\necho new > e2e/browser/visual.spec.ts-snapshots/toolbar.png\nexit ${playwrightExit}\n`); chmodSync(join(bin, 'npm'), 0o755); chmodSync(join(bin, 'npx'), 0o755); const run = spawnSync('sh', [script, ...args], { @@ -49,45 +50,45 @@ describe('visual:linux', () => { it('installs and runs in a copy without .git, and hands back only the snapshots', () => { const { src, run } = fakeCheckout(0); expect(run.status, run.stderr).toBe(0); - expect(readFileSync(join(src, 'e2e', 'visual.spec.ts-snapshots', 'toolbar.png'), 'utf8').trim()).toBe('new'); + expect(readFileSync(join(src, ...SNAPSHOTS, 'toolbar.png'), 'utf8').trim()).toBe('new'); expect(readFileSync(join(src, 'node_modules', 'host.txt'), 'utf8')).toBe('darwin'); }); it('still hands back what a failing run wrote, and fails', () => { const { src, run } = fakeCheckout(1); expect(run.status).toBe(1); - expect(existsSync(join(src, 'e2e', 'visual.spec.ts-snapshots', 'toolbar.png'))).toBe(true); + expect(existsSync(join(src, ...SNAPSHOTS, 'toolbar.png'))).toBe(true); }); it('checks without touching the committed baselines', () => { const { src, run, calls } = fakeCheckout(0, ['visual-check']); expect(run.status, run.stderr).toBe(0); - expect(readFileSync(join(src, 'e2e', 'visual.spec.ts-snapshots', 'toolbar.png'), 'utf8')).toBe('old'); - expect(calls()).toEqual(['playwright test --config=playwright.visual.config.ts']); + expect(readFileSync(join(src, ...SNAPSHOTS, 'toolbar.png'), 'utf8')).toBe('old'); + expect(calls()).toEqual(['playwright test --config=e2e/browser/playwright.visual.config.ts']); }); it('runs the E2E suite with the arguments it was given', () => { - const { src, run, calls } = fakeCheckout(0, ['e2e', 'e2e/tooltip.spec.ts']); + const { src, run, calls } = fakeCheckout(0, ['e2e', 'e2e/browser/tooltip.spec.ts']); expect(run.status, run.stderr).toBe(0); - expect(calls()).toEqual(['playwright test e2e/tooltip.spec.ts']); - expect(readFileSync(join(src, 'e2e', 'visual.spec.ts-snapshots', 'toolbar.png'), 'utf8')).toBe('old'); + expect(calls()).toEqual(['playwright test --config=e2e/browser/playwright.config.ts e2e/browser/tooltip.spec.ts']); + expect(readFileSync(join(src, ...SNAPSHOTS, 'toolbar.png'), 'utf8')).toBe('old'); }); it('measures, then regenerates, when updating', () => { const { run, calls } = fakeCheckout(0, ['visual-update']); expect(run.status, run.stderr).toBe(0); expect(calls()).toEqual([ - 'playwright test --config=playwright.visual.config.ts --reporter=json', - 'playwright test --config=playwright.visual.config.ts --update-snapshots', + 'playwright test --config=e2e/browser/playwright.visual.config.ts --reporter=json', + 'playwright test --config=e2e/browser/playwright.visual.config.ts --update-snapshots', ]); }); it('wires each npm script to its mode through the one pinned image', () => { - expect(pkg.scripts['visual:linux']).toBe('sh scripts/linux.sh visual-update'); - expect(pkg.scripts['visual:linux:check']).toBe('sh scripts/linux.sh visual-check'); - expect(pkg.scripts['e2e:linux']).toBe('sh scripts/linux.sh e2e'); + expect(pkg.scripts['visual:linux']).toBe('sh .dev/browser/linux.sh visual-update'); + expect(pkg.scripts['visual:linux:check']).toBe('sh .dev/browser/linux.sh visual-check'); + expect(pkg.scripts['e2e:linux']).toBe('sh .dev/browser/linux.sh e2e'); const wrapper = readFileSync(resolve(root, '.dev/browser/linux.sh'), 'utf8'); expect(wrapper).toMatch(/-v "\$PWD":\/src /); - expect(wrapper).toMatch(/sh \/src\/scripts\/visual-linux\.sh "\$mode" "\$@"/); + expect(wrapper).toMatch(/sh \/src\/\.dev\/browser\/visual-linux\.sh "\$mode" "\$@"/); }); }); diff --git a/resources/js/noPrivateContent.test.ts b/resources/js/noPrivateContent.test.ts index a135e36..6e7d8ab 100644 --- a/resources/js/noPrivateContent.test.ts +++ b/resources/js/noPrivateContent.test.ts @@ -1,4 +1,6 @@ import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; // @ts-expect-error -- plain .mjs helper without declarations import { privateContent } from '../../.dev/tools/sync-vendored-library.mjs'; @@ -9,19 +11,39 @@ import { privateContent } from '../../.dev/tools/sync-vendored-library.mjs'; * The react-icon-browser import (2026-11-12) brought 34 bundled baseline SVGs and one bundled * snapshot in; they were removed and the folder is ignored. This reads what git tracks, so a file * on disk but ignored is fine, and a tracked one fails. + * + * Since react consolidation 3.1 (2026-11-13) the e2e harness's SVG rendering sample, + * .dev/browser/public/baseline/, is tracked again, holding the public packs only (tabler and flag, + * 18 files and their manifest). `privateContent()` refuses every baseline/ path, and it is a + * byte-identical copy of js-ui-kernel's, so the sample is checked here by name and by manifest + * instead, and every other tracked file still goes through it. */ // Vitest runs from the repository root; under jsdom import.meta.url is not a file: URL. const root = process.cwd(); const tracked = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' }).split('\n').filter(Boolean); const PRIVATE_RENDER = /(icon-sets-bundled|icon-sets-metronic|metronic)[^/]*\.(png|jpe?g|webp)$/i; +/** The public SVG rendering sample: these two packs only, and its manifest. */ +const SAMPLE_DIR = '.dev/browser/public/baseline/'; +const PUBLIC_SAMPLE = /^\.dev\/browser\/public\/baseline\/(icon-sets-(tabler|flag)__[a-z_]+__[a-z0-9-]+\.svg|manifest\.json)$/; +const notPublicSample = (paths: string[]) => paths.filter((f) => f.startsWith(SAMPLE_DIR) && !PUBLIC_SAMPLE.test(f)); +const sample = tracked.filter((f) => f.startsWith(SAMPLE_DIR)); + describe('no private pack content is tracked', () => { it('reads the tracked file list', () => { expect(tracked.length).toBeGreaterThan(500); }); - it('tracks no baseline path and no bundled or Metronic SVG', () => { - expect(privateContent(tracked)).toEqual([]); + it('tracks no baseline path and no bundled or Metronic SVG, apart from the public sample', () => { + expect(privateContent(tracked.filter((f) => !f.startsWith(SAMPLE_DIR)))).toEqual([]); + }); + + it('the rendering sample holds the public packs only', () => { + expect(sample.length, 'the 18 samples and the manifest').toBeGreaterThanOrEqual(19); + expect(notPublicSample(sample)).toEqual([]); + const manifest = JSON.parse(readFileSync(join(root, SAMPLE_DIR, 'manifest.json'), 'utf8')) as { pack: string; file: string }[]; + expect(new Set(manifest.map((m) => m.pack))).toEqual(new Set(['icon-sets-tabler', 'icon-sets-flag'])); + expect(manifest.map((m) => `${SAMPLE_DIR}${m.file}`).sort()).toEqual(sample.filter((f) => !f.endsWith('manifest.json')).sort()); }); it('tracks no rendering of a private pack', () => { @@ -31,5 +53,7 @@ describe('no private pack content is tracked', () => { it('can fail (positive control)', () => { expect(privateContent(['.dev/browser/public/baseline/icon-sets-bundled__x.svg'])).toHaveLength(1); expect(PRIVATE_RENDER.test('e2e/x-snapshots/svg-icon-sets-bundled-chromium-linux.png')).toBe(true); + expect(notPublicSample([`${SAMPLE_DIR}icon-sets-metronic__gradient__forest.svg`, `${SAMPLE_DIR}icon-sets-bundled__foreign__x.svg`])).toHaveLength(2); + expect(notPublicSample([`${SAMPLE_DIR}icon-sets-flag__use_ref__ad.svg`])).toEqual([]); }); }); diff --git a/resources/js/styleRules.test.ts b/resources/js/styleRules.test.ts index f32eb1b..723eeb7 100644 --- a/resources/js/styleRules.test.ts +++ b/resources/js/styleRules.test.ts @@ -19,10 +19,10 @@ import { describe, expect, it } from 'vitest'; */ // The repository root. The source is resources/js since react consolidation 3d (2026-11-12), and -// the standalone app's two pages wait with the other imported root files. +// the standalone app's two pages are the e2e harness since 3.1 (2026-11-13). const ROOT = join(__dirname, '..', '..'); const DIRS = ['resources/js']; -const EXTRA = ['.dev/imported/react-icon-browser/index.html', '.dev/imported/react-icon-browser/embedded.html']; +const EXTRA = ['e2e/browser/index.html', 'e2e/browser/embedded.html']; const EXT = /\.(tsx?|css|html)$/; const MIN_FILES = 150; const MAX_WEIGHT = 600; From 0be82da73034faec65f4fe38f78525c3015ece08 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 15:59:49 -0500 Subject: [PATCH 5/7] Port react-icon-browser's browser CI and visual seeding workflow browser.yml runs the browser guards and the Playwright suite on pull requests, and the visual suite weekly and by dispatch inside the pinned Playwright image. visual-baselines.yml is react-icon-browser's seeding workflow with its paths moved. actionlint.yaml carries the create-github-app-token v3 ignore for it, as for the two workflows that already use that action. --- .github/actionlint.yaml | 6 + .github/workflows/browser.yml | 193 +++++++++++++++++++++++++ .github/workflows/visual-baselines.yml | 156 ++++++++++++++++++++ 3 files changed, 355 insertions(+) create mode 100644 .github/workflows/browser.yml create mode 100644 .github/workflows/visual-baselines.yml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 8bdbd32..f1a22cc 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -16,3 +16,9 @@ paths: # The same create-github-app-token v3 false positive as release-please.yml above; delete with it. - 'missing input "app-id" which is required by action "actions/create-github-app-token@v3"' - 'input "client-id" is not defined in action "actions/create-github-app-token@v3"' + .github/workflows/visual-baselines.yml: + ignore: + # The same create-github-app-token v3 false positive as release-please.yml above; delete with it. + # Added with the workflow, ported from react-icon-browser (react consolidation 2.6, 2026-11-13). + - 'missing input "app-id" which is required by action "actions/create-github-app-token@v3"' + - 'input "client-id" is not defined in action "actions/create-github-app-token@v3"' diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml new file mode 100644 index 0000000..46f208f --- /dev/null +++ b/.github/workflows/browser.yml @@ -0,0 +1,193 @@ +name: Browser + +# The browser UI's own checks, ported from ichava/react-icon-browser's ci.yml when its source +# moved into resources/js (react consolidation 2.6, 2026-11-13). What that workflow ran and where +# it went: +# +# typecheck, build, lint, Vitest tests.yml's frontend job (typecheck:inertia covers the +# whole of resources/js, tests included; build:prod builds it) +# check:config-drift tests.yml's frontend job +# actionlint, changelog order code-quality.yml, changelog.yml +# build:lib, check:public-api, not ported: the library build is retired (Q2) and nothing +# consumer self-test vendors the browser any more (icon-browser#146) +# everything else below +# +# Pull requests run Linux only, drafts run nothing, superseded runs are cancelled and every job +# has a timeout. The visual suite runs weekly and by dispatch, not on every pull request. +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review] + schedule: + - cron: '0 6 * * 1' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + guards: + name: Browser guards + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: 24 + + # No lock file is committed (org rule), so this resolves fresh. + - name: Install dependencies + run: npm install --no-audit --no-fund + + # The payload types are generated from resources/schema/icon-browser.schema.json. A hand + # edit to the generated file fails here before anything type-checks against the wrong shapes. + - name: Payload types match the schema + run: npm run check:schema-types + + # The stricter overlay for resources/js/modules/*/domain and application: no DOM in lib, + # and exactOptionalPropertyTypes. Its own step, so a vendored component cannot fail it. + - name: Typecheck the domain layer + run: npm run typecheck:domain + + - name: Typecheck the e2e specs + run: npm run typecheck:e2e + + # Reads the stylesheet hosts receive (public/assets/css), which committed-build.yml proves + # equals a fresh build. A class Tailwind could not resolve is dropped with no warning, and + # nothing else notices: strings are invisible to tsc and ESLint. + - name: Dead CSS classes + run: npm run check:dead-classes + + # The documentation portal has no Ichava pages yet; this fails on a link to them, and on a + # relative Markdown link to a file that does not exist. + - name: Dead documentation links + run: npm run check:dead-links + + # The layering is what makes a later Vue or Blade consumer possible, and it only stays + # true if something checks. core/ leaked React because nothing did. + - name: Layer boundaries + run: npm run boundaries + + # The module rules match little on this tree, so this plants a violation of each and + # fails unless every rule fires. + - name: Boundary rules can fail + run: npm run check:boundary-rules + + # Cheap, and it fails on a config mistake that would otherwise surface as a screenshot + # diff, which reads as a rendering regression that is not one. + - name: Playwright image pinned consistently + run: npm run check:playwright-image + + e2e: + name: Browser E2E (Playwright) + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Install dependencies + run: npm install --no-audit --no-fund + + # The browser comes from Playwright's CDN; its system packages come from Ubuntu's apt + # mirrors, which went unreachable from the runner on 2026-11-11. The runner image already + # ships Chrome's libraries, so the extra packages are best effort: two minutes, then a + # warning instead of a red job. + - name: Install Playwright browsers + run: | + npx playwright install chromium + timeout 120 npx playwright install-deps chromium && exit 0 + echo "::warning title=Playwright system deps::apt mirrors unreachable; running with the runner's own libraries" + + # The config builds the harness (e2e/browser/vite.config.ts) and serves it itself. + - name: Playwright + run: npm run e2e:browser + + - name: Upload report on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: browser-e2e-report + path: test-results/ + retention-days: 7 + + visual: + name: Browser visual regression + runs-on: ubuntu-latest + # Weekly and by dispatch (react consolidation 3.1): the suite compares pixels in a container + # and is the slowest job here. + if: github.event_name != 'pull_request' + timeout-minutes: 20 + + # The gate runs INSIDE Playwright's own image, not bare on the runner. Font stacks differ + # between the two, so a baseline made in the container misses a bare runner by ~1% of + # pixels. Running here makes `npm run visual:linux` byte-identical to CI. + # + # `npm run check:playwright-image` asserts this tag, the seed workflow's and linux.sh's all + # agree, and that they match @playwright/test. Do not edit one alone. + container: + image: mcr.microsoft.com/playwright:v1.65.0-noble + options: --ipc=host + + # A containerised job does NOT inherit the runner's bash default: Actions runs `run:` steps + # under `sh -e`, where the bash arrays below expand to something else entirely. + defaults: + run: + shell: bash + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + # No setup-node and no `playwright install`: the image ships both. + - name: Install dependencies + run: npm install --no-audit --no-fund + + # Without committed Linux baselines Playwright WRITES them and fails, which reads as a + # mysterious diff. Fail here instead, with the fix in the message. + - name: Require Linux baselines + run: | + shopt -s nullglob + baselines=(e2e/browser/*.spec.ts-snapshots/*-linux.png) + if [ ${#baselines[@]} -eq 0 ]; then + echo "::error::No Linux screenshot baselines are committed, so the visual" + echo "::error::suite cannot compare anything." + echo "::error::Seed them with the same image this job runs in:" + echo "::error:: npm run visual:linux # locally, needs Docker" + echo "::error:: gh workflow run visual-baselines.yml --repo ichava/icon-browser --ref $GITHUB_REF_NAME" + echo "::error::Review the PNGs, then commit e2e/browser/*.spec.ts-snapshots/." + exit 1 + fi + echo "Found ${#baselines[@]} Linux baseline(s)." + + # Never --update-snapshots in CI. A self-updating baseline turns every regression into a + # committed one and the gate silently stops existing. + - name: Playwright (visual) + run: npm run visual + + - name: Upload diff report on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: browser-visual-report + path: | + playwright-report/ + test-results/ + retention-days: 7 diff --git a/.github/workflows/visual-baselines.yml b/.github/workflows/visual-baselines.yml new file mode 100644 index 0000000..615007d --- /dev/null +++ b/.github/workflows/visual-baselines.yml @@ -0,0 +1,156 @@ +# Seed the visual baselines from the environment that compares them. +# +# Ported from ichava/react-icon-browser with the suite (react consolidation 2.6, 2026-11-13). +# +# That environment is Playwright's own image, because CI's `visual` job runs inside +# it (see browser.yml). Font stacks differ between the image and a bare GitHub runner -- +# about 1% of pixels, invisible to look at and enough to fail -- so "Linux" alone was +# never specific enough. Pinning both to one image is what removed that gap; the same +# image is also what `npm run visual:linux` uses, so a local seed and this workflow +# produce identical pixels. +# +# Deliberately manual. A workflow that rewrites its own baselines on every run turns +# every regression into a committed one and the gate silently stops existing, so this +# only ever runs when someone dispatches it, and never writes to `main`. +# +# Two ways to run it (Sim, 2026-10-28: "automate" the download and the review): +# - commit: false (default): the new PNGs are uploaded as an artifact, as before. +# - commit: true, dispatched on a pull request's branch: the job first compares the +# current baselines and records how much each one changed, then regenerates them, +# commits only the changed PNGs to that branch with the Refresh Bot token (so CI runs +# again on the new head), and comments on the pull request with a table of what moved +# and by how much. The review is the pull request's own image diff ("Files changed" +# renders old and new side by side); merging stays a human decision. + +name: Seed visual baselines + +on: + workflow_dispatch: + inputs: + commit: + description: Commit the regenerated baselines to this branch and comment on its pull request + type: boolean + default: false + +permissions: + contents: read + +jobs: + seed: + name: Generate baselines in the Playwright image + runs-on: ubuntu-latest + timeout-minutes: 20 + + # Must match browser.yml's `visual` job exactly. If these two images ever differ, this + # workflow starts producing baselines the gate rejects, and the failure reads as a + # rendering regression rather than a configuration one. + container: + image: mcr.microsoft.com/playwright:v1.65.0-noble + options: --ipc=host + + # A containerised job does NOT inherit the runner's bash default -- Actions runs + # `run:` steps under `sh -e` here, and the switch is invisible until something breaks. + # It broke loudly once (`shopt: not found`, exit 127), but the quiet version is worse: + # `arr=(...)` and `${#arr[@]}` are bash arrays that sh expands to something else + # entirely, so a baseline check could have reported zero and "passed". + defaults: + run: + shell: bash + + steps: + # Refuse before doing any work: committing baselines straight onto the default + # branch would skip the review this workflow exists to keep. + - name: Refuse to commit to the default branch + if: inputs.commit + env: + REF_NAME: ${{ github.ref_name }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + if [ "${REF_NAME}" = "${DEFAULT_BRANCH}" ]; then + echo "::error::commit: true runs on a pull request's branch, never on ${DEFAULT_BRANCH}." + exit 1 + fi + + - name: Mint the Refresh Bot token + if: inputs.commit + id: app + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.REFRESH_APP_CLIENT_ID }} + private-key: ${{ secrets.REFRESH_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + + - uses: actions/checkout@v7 + with: + token: ${{ inputs.commit && steps.app.outputs.token || github.token }} + + # No setup-node, no `playwright install`: the image ships both. + - name: Install dependencies + run: npm install --no-audit --no-fund + + # Compare against the committed baselines first, so the comment can say how far + # each one moved. Failures are the point here, so this step never fails the job. + - name: Measure the drift + if: inputs.commit + continue-on-error: true + run: npx playwright test --config=e2e/browser/playwright.visual.config.ts --reporter=json > "${RUNNER_TEMP}/visual.json" + + - name: Regenerate baselines + run: npx playwright test --config=e2e/browser/playwright.visual.config.ts --update-snapshots + + - name: Commit the changed baselines and comment on the pull request + if: inputs.commit + env: + GH_TOKEN: ${{ steps.app.outputs.token }} + APP_SLUG: ${{ steps.app.outputs.app-slug }} + BRANCH: ${{ github.ref_name }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + git config --global --add safe.directory "$GITHUB_WORKSPACE" + mapfile -t CHANGED < <(git status --porcelain -- 'e2e/browser/*.spec.ts-snapshots/*.png' | awk '{print $2}') + echo "changed baselines: ${#CHANGED[@]}" + if [ "${#CHANGED[@]}" -eq 0 ]; then + echo "Baselines already match; nothing to commit." + exit 0 + fi + { + echo '### Visual baselines regenerated' + echo + echo "${#CHANGED[@]} baseline(s) changed and were committed to this branch by [the seeding run](${RUN_URL})." + echo 'Review them in **Files changed**, which shows the old and new image side by side. Merge only if every change is intended.' + echo + node .dev/browser/visual-drift.mjs "${RUNNER_TEMP}/visual.json" "${CHANGED[@]}" + } > "${RUNNER_TEMP}/comment.md" + # The Playwright image has no gh CLI, so the API calls go through Node's fetch. + api() { node -e ' + const [path, method = "GET", body] = process.argv.slice(1); + fetch("https://api.github.com" + path, { method, body, headers: { + authorization: "Bearer " + process.env.GH_TOKEN, accept: "application/vnd.github+json", + "content-type": "application/json", "x-github-api-version": "2022-11-28" } }) + .then(async (r) => { const j = await r.json(); if (!r.ok) { console.error(r.status, JSON.stringify(j)); process.exit(1); } console.log(JSON.stringify(j)); }); + ' "$@"; } + BOT_ID="$(api "/users/${APP_SLUG}%5Bbot%5D" | node -pe 'JSON.parse(require("fs").readFileSync(0)).id')" + git config user.name "${APP_SLUG}[bot]" + git config user.email "${BOT_ID}+${APP_SLUG}[bot]@users.noreply.github.com" + git add -- "${CHANGED[@]}" + git commit -m "Regenerate ${#CHANGED[@]} visual baseline(s)" -m "Seeded by ${RUN_URL}." + git push origin "HEAD:refs/heads/${BRANCH}" + PR="$(api "/repos/${GITHUB_REPOSITORY}/pulls?state=open&head=${GITHUB_REPOSITORY_OWNER}:${BRANCH}" | node -pe 'JSON.parse(require("fs").readFileSync(0))[0]?.number ?? ""')" + if [ -n "${PR}" ]; then + api "/repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" POST \ + "$(node -e 'console.log(JSON.stringify({ body: require("fs").readFileSync(process.argv[1], "utf8") }))' "${RUNNER_TEMP}/comment.md")" > /dev/null + echo "commented on #${PR}" + else + echo "::warning::No open pull request for ${BRANCH}; the baselines are committed but nobody was told." + fi + cat "${RUNNER_TEMP}/comment.md" >> "$GITHUB_STEP_SUMMARY" + + - name: Upload for review + uses: actions/upload-artifact@v7 + with: + name: visual-baselines + path: e2e/browser/*.spec.ts-snapshots/*.png + if-no-files-found: error + retention-days: 7 From a020f5f1676e3c2022571201f24c8c7f59eafc42 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 16:01:31 -0500 Subject: [PATCH 6/7] Record the ported browser suite and guards in the changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2100d80..51cdd5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to `ichava/icon-browser` follow [Keep a Changelog](https://k ## [Unreleased] +### Added + +- **The browser's Playwright suite and guards run here** (react consolidation 2.6, 3.1 and 3.3). react-icon-browser's 20 e2e specs and their harness pages moved to `e2e/browser/`, built from `resources/js` by `e2e/browser/vite.config.ts` and run by `npm run e2e:browser` (98 tests, as in react-icon-browser); `npm run visual` compares the 12 committed Linux baselines inside the pinned Playwright image. `.github/workflows/browser.yml` runs the guards and the e2e suite on pull requests and the visual suite weekly and by dispatch; `visual-baselines.yml` seeds baselines on a pull request's branch. New npm scripts: `typecheck:domain`, `typecheck:e2e`, `boundaries`, `check:boundary-rules`, `check:schema-types`, `schema:types`, `check:dead-classes`, `check:dead-links`, `check:playwright-image`, `visual`, `visual:update`, `visual:linux`, `visual:linux:check`, `e2e:linux`. +- **The SVG rendering sample is tracked, with the public packs only** (tabler and flag, react-icon-browser's own 18 files). The svg-baseline spec no longer renders the private bundled and Metronic packs, so it covers six archetypes, not eight; `noPrivateContent.test.ts` fails if the sample holds anything else. + +### Changed + +- `core/minifiedBundle.ts`, used only by its test, moved to `resources/js/test/minifiedBundle.ts`; dependency-cruiser's `no-unreachable` rule reported it. + +### Fixed + +- `resources/assets/fonts/README.md` linked `../../src/styles/theme.css`, which does not exist; `check:dead-links` found it. + ## [0.1.0] - 2026-11-13 First release of `ichava/icon-browser`. The repository is published as a single commit; the 0.1.x tags cut between 2026-10-23 and 2026-11-02 (v0.1.0 to v0.1.1, including the release-please v0.1.1 of 2026-11-02) are folded into this release, and their notes are kept below under *Archived history*. From 1e47837f9d8d70017d99bb63801d6504a728ceb5 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 16:19:06 -0500 Subject: [PATCH 7/7] Keep the moved suite and workflow comments out of the global scan ichava.scss is a second Tailwind root whose utilities are not scoped to the browser. It excluded .dev, where react-icon-browser's specs sat; moved to e2e/browser, their words became eight global utilities (.hidden, .ring, .grow ...) and committed-build failed. It now excludes e2e/browser and .github: a comment in the ported workflows added a global .invisible on its own. A fresh build equals the committed bundle again, and a word planted in a workflow no longer reaches it. --- .github/workflows/browser.yml | 2 +- .github/workflows/visual-baselines.yml | 4 ++-- resources/assets/styles/ichava.scss | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml index 46f208f..e29fb4d 100644 --- a/.github/workflows/browser.yml +++ b/.github/workflows/browser.yml @@ -64,7 +64,7 @@ jobs: # Reads the stylesheet hosts receive (public/assets/css), which committed-build.yml proves # equals a fresh build. A class Tailwind could not resolve is dropped with no warning, and - # nothing else notices: strings are invisible to tsc and ESLint. + # nothing else notices: tsc and ESLint do not read strings. - name: Dead CSS classes run: npm run check:dead-classes diff --git a/.github/workflows/visual-baselines.yml b/.github/workflows/visual-baselines.yml index 615007d..c86f574 100644 --- a/.github/workflows/visual-baselines.yml +++ b/.github/workflows/visual-baselines.yml @@ -4,7 +4,7 @@ # # That environment is Playwright's own image, because CI's `visual` job runs inside # it (see browser.yml). Font stacks differ between the image and a bare GitHub runner -- -# about 1% of pixels, invisible to look at and enough to fail -- so "Linux" alone was +# about 1% of pixels, too few to see and enough to fail -- so "Linux" alone was # never specific enough. Pinning both to one image is what removed that gap; the same # image is also what `npm run visual:linux` uses, so a local seed and this workflow # produce identical pixels. @@ -49,7 +49,7 @@ jobs: options: --ipc=host # A containerised job does NOT inherit the runner's bash default -- Actions runs - # `run:` steps under `sh -e` here, and the switch is invisible until something breaks. + # `run:` steps under `sh -e` here, and nothing shows the switch until something breaks. # It broke loudly once (`shopt: not found`, exit 127), but the quiet version is worse: # `arr=(...)` and `${#arr[@]}` are bash arrays that sh expands to something else # entirely, so a baseline check could have reported zero and "passed". diff --git a/resources/assets/styles/ichava.scss b/resources/assets/styles/ichava.scss index c138cea..7bcc051 100644 --- a/resources/assets/styles/ichava.scss +++ b/resources/assets/styles/ichava.scss @@ -28,6 +28,13 @@ .dev/imported), and the fonts' README, are not this package's markup. */ @source not "../../../.dev"; @source not "../fonts"; +/* The browser's Playwright suite (e2e/browser, react consolidation 3.1, 2026-11-13) came from + .dev/imported, which the line above excludes. Its words (`hidden`, `ring`, `grow` ...) would + otherwise become global utilities here. */ +@source not "../../../e2e/browser"; +/* Workflow comments are not markup either: a word in one (`invisible`) became a global utility + while the browser's CI was ported (react consolidation 2.6). */ +@source not "../../../.github"; @plugin "tailwindcss-animate";