ultimate-jekyll-manager 1.1.9 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/CLAUDE.md +138 -1775
  3. package/README.md +49 -20
  4. package/dist/build.js +3 -0
  5. package/dist/cli.js +1 -0
  6. package/dist/commands/test.js +56 -0
  7. package/dist/defaults/CLAUDE.md +72 -6
  8. package/dist/gulp/tasks/defaults.js +20 -4
  9. package/dist/gulp/tasks/distribute.js +29 -26
  10. package/dist/gulp/tasks/jsonToHtml.js +86 -80
  11. package/dist/gulp/tasks/minifyHtml.js +55 -51
  12. package/dist/gulp/tasks/sass.js +7 -6
  13. package/dist/gulp/tasks/utils/template-transform.js +35 -35
  14. package/dist/index.js +4 -0
  15. package/dist/test/assert.js +120 -0
  16. package/dist/test/fixtures/consumer-site/_site/about.html +13 -0
  17. package/dist/test/fixtures/consumer-site/_site/assets/css/main.bundle.css +2 -0
  18. package/dist/test/fixtures/consumer-site/_site/assets/js/main.bundle.js +6 -0
  19. package/dist/test/fixtures/consumer-site/_site/build.json +11 -0
  20. package/dist/test/fixtures/consumer-site/_site/index.html +28 -0
  21. package/dist/test/fixtures/consumer-site/_site/service-worker.js +29 -0
  22. package/dist/test/fixtures/consumer-site/package.json +6 -0
  23. package/dist/test/harness/page/index.html +51 -0
  24. package/dist/test/index.js +63 -0
  25. package/dist/test/runner.js +402 -0
  26. package/dist/test/runners/boot.js +109 -0
  27. package/dist/test/runners/chromium.js +255 -0
  28. package/dist/test/server.js +127 -0
  29. package/dist/test/suites/boot/service-worker.test.js +84 -0
  30. package/dist/test/suites/boot/site-loads.test.js +65 -0
  31. package/dist/test/suites/build/cli.test.js +49 -0
  32. package/dist/test/suites/build/collect-text-nodes.test.js +37 -0
  33. package/dist/test/suites/build/dictionary.test.js +17 -0
  34. package/dist/test/suites/build/expect.test.js +59 -0
  35. package/dist/test/suites/build/exports.test.js +32 -0
  36. package/dist/test/suites/build/logger.test.js +62 -0
  37. package/dist/test/suites/build/manager.test.js +186 -0
  38. package/dist/test/suites/build/merge-jekyll-configs.test.js +95 -0
  39. package/dist/test/suites/build/mode-helpers.test.js +65 -0
  40. package/dist/test/suites/build/template-transform.test.js +94 -0
  41. package/dist/test/suites/build/templating-brackets.test.js +46 -0
  42. package/dist/test/suites/build/validate-yaml.test.js +60 -0
  43. package/dist/test/suites/page/dom-baseline.test.js +34 -0
  44. package/dist/test/suites/page/harness-globals.test.js +38 -0
  45. package/dist/test/suites/page/prerendered-icons.test.js +32 -0
  46. package/dist/utils/mode-helpers.js +84 -0
  47. package/docs/_legacy-claude-md.md +1832 -0
  48. package/docs/cross-context-helpers.md +75 -0
  49. package/docs/test-boot-layer.md +110 -0
  50. package/docs/test-framework.md +183 -0
  51. package/package.json +18 -16
@@ -4,7 +4,7 @@ const logger = Manager.logger('minifyHtml');
4
4
  const { src, dest, series } = require('gulp');
5
5
  const { minify: minifyRust } = require('@minify-html/node');
6
6
  const { minify: minifyJs } = require('terser');
7
- const through2 = require('through2');
7
+ const { Transform } = require('node:stream');
8
8
 
9
9
  // Load package
10
10
  const package = Manager.getPackage('main');
@@ -85,61 +85,65 @@ function minifyHtmlTask(complete) {
85
85
 
86
86
  // Process HTML files
87
87
  return src(input)
88
- .pipe(through2.obj(function(file, _enc, callback) {
89
- if (file.isBuffer()) {
90
- fileQueue.push({ file });
91
- callback();
92
- } else {
93
- callback(null, file);
94
- }
95
- }, async function(callback) {
96
- // This function is called when all files have been queued
97
- if (fileQueue.length === 0) {
98
- logger.log('No HTML files to minify');
99
- return callback();
100
- }
88
+ .pipe(new Transform({
89
+ objectMode: true,
90
+ transform(file, _enc, callback) {
91
+ if (file.isBuffer()) {
92
+ fileQueue.push({ file });
93
+ callback();
94
+ } else {
95
+ callback(null, file);
96
+ }
97
+ },
98
+ async flush(callback) {
99
+ // This function is called when all files have been queued
100
+ if (fileQueue.length === 0) {
101
+ logger.log('No HTML files to minify');
102
+ return callback();
103
+ }
101
104
 
102
- const totalFiles = fileQueue.length;
103
- logger.log(`Minifying ${totalFiles} HTML files...`);
104
-
105
- try {
106
- // Process files in batches
107
- for (let i = 0; i < fileQueue.length; i += CONCURRENCY_LIMIT) {
108
- const batch = fileQueue.slice(i, i + CONCURRENCY_LIMIT);
109
-
110
- // Process batch in parallel
111
- const processedFiles = await Promise.all(
112
- batch.map(async ({ file }) => {
113
- try {
114
- const htmlContent = file.contents.toString();
115
- const finalHtml = await minifyFileContent(htmlContent, options, file.path);
116
- file.contents = Buffer.from(finalHtml);
117
- processed.count++;
118
-
119
- // Log progress every 50 files or on last file
120
- if (processed.count % 50 === 0 || processed.count === totalFiles) {
121
- const percentage = ((processed.count / totalFiles) * 100).toFixed(1);
122
- logger.log(`Progress: ${processed.count}/${totalFiles} files (${percentage}%)`);
123
- Manager.logMemory(logger, `After ${processed.count} files`);
105
+ const totalFiles = fileQueue.length;
106
+ logger.log(`Minifying ${totalFiles} HTML files...`);
107
+
108
+ try {
109
+ // Process files in batches
110
+ for (let i = 0; i < fileQueue.length; i += CONCURRENCY_LIMIT) {
111
+ const batch = fileQueue.slice(i, i + CONCURRENCY_LIMIT);
112
+
113
+ // Process batch in parallel
114
+ const processedFiles = await Promise.all(
115
+ batch.map(async ({ file }) => {
116
+ try {
117
+ const htmlContent = file.contents.toString();
118
+ const finalHtml = await minifyFileContent(htmlContent, options, file.path);
119
+ file.contents = Buffer.from(finalHtml);
120
+ processed.count++;
121
+
122
+ // Log progress every 50 files or on last file
123
+ if (processed.count % 50 === 0 || processed.count === totalFiles) {
124
+ const percentage = ((processed.count / totalFiles) * 100).toFixed(1);
125
+ logger.log(`Progress: ${processed.count}/${totalFiles} files (${percentage}%)`);
126
+ Manager.logMemory(logger, `After ${processed.count} files`);
127
+ }
128
+
129
+ return file;
130
+ } catch (err) {
131
+ logger.error(`Error minifying ${file.path}: ${err.message}`);
132
+ return file;
124
133
  }
134
+ })
135
+ );
125
136
 
126
- return file;
127
- } catch (err) {
128
- logger.error(`Error minifying ${file.path}: ${err.message}`);
129
- return file;
130
- }
131
- })
132
- );
137
+ // Push processed files to the stream
138
+ processedFiles.forEach(file => this.push(file));
139
+ }
133
140
 
134
- // Push processed files to the stream
135
- processedFiles.forEach(file => this.push(file));
141
+ callback();
142
+ } catch (err) {
143
+ logger.error(`Batch processing error: ${err.message}`);
144
+ callback(err);
136
145
  }
137
-
138
- callback();
139
- } catch (err) {
140
- logger.error(`Batch processing error: ${err.message}`);
141
- callback(err);
142
- }
146
+ },
143
147
  }))
144
148
  .pipe(dest(output))
145
149
  .on('finish', () => {
@@ -13,7 +13,7 @@ const { template } = require('node-powertools');
13
13
  const yaml = require('js-yaml');
14
14
  const postcss = require('gulp-postcss');
15
15
  const purgeCss = require('@fullhuman/postcss-purgecss');
16
- const through2 = require('through2');
16
+ const { Transform } = require('node:stream');
17
17
 
18
18
  // Load package
19
19
  const package = Manager.getPackage('main');
@@ -324,19 +324,20 @@ function sass(complete) {
324
324
  let purgeCssLogged = false;
325
325
  stream = stream
326
326
  .pipe(postcss([purgeCssPlugin]))
327
- .pipe(through2.obj(
328
- function (file, enc, cb) {
327
+ .pipe(new Transform({
328
+ objectMode: true,
329
+ transform(file, enc, cb) {
329
330
  cb(null, file);
330
331
  },
331
- function (cb) {
332
+ flush(cb) {
332
333
  if (!purgeCssLogged) {
333
334
  purgeCssLogged = true;
334
335
  const purgeCssTime = ((performance.now() - purgeCssStartTime) / 1000).toFixed(2);
335
336
  logger.log(`PurgeCSS completed in ${purgeCssTime}s`);
336
337
  }
337
338
  cb();
338
- }
339
- ));
339
+ },
340
+ }));
340
341
  }
341
342
 
342
343
  // Process
@@ -1,49 +1,49 @@
1
1
  // Libraries
2
- const through2 = require('through2');
2
+ const { Transform } = require('node:stream');
3
3
  const { template } = require('node-powertools');
4
4
  const path = require('path');
5
5
 
6
6
  /**
7
- * Creates a through2 transform stream that processes template variables in files
7
+ * Creates a transform stream that processes template variables in files
8
8
  **/
9
9
  function createTemplateTransform(data) {
10
10
  const extensions = ['html', 'md', 'liquid', 'json']
11
11
 
12
- return through2.obj(function(file, encoding, callback) {
13
- // Skip directories
14
- if (file.isDirectory()) {
15
- return callback(null, file);
16
- }
17
-
18
- // Check if file extension matches
19
- const ext = path.extname(file.path).toLowerCase().slice(1);
20
- if (!extensions.includes(ext)) {
21
- return callback(null, file);
22
- }
23
-
24
- // Log
25
- // console.log(`Processing file: ${file.path}`);
26
-
27
- // Process the file contents
28
- try {
29
- const contents = file.contents.toString();
30
-
31
- // Process templates
32
- const templated = template(contents, data, {
33
- brackets: ['[', ']'],
34
- });
35
-
36
- // Update file contents if changed
37
- if (contents !== templated) {
38
- file.contents = Buffer.from(templated);
39
- const relativePath = file.relative || file.path;
12
+ return new Transform({
13
+ objectMode: true,
14
+ transform(file, encoding, callback) {
15
+ // Skip directories
16
+ if (file.isDirectory()) {
17
+ return callback(null, file);
40
18
  }
41
- } catch (error) {
42
- console.error(`Error processing templates in ${file.path}:`, error);
43
- }
44
19
 
45
- // Pass the file through
46
- callback(null, file);
20
+ // Check if file extension matches
21
+ const ext = path.extname(file.path).toLowerCase().slice(1);
22
+ if (!extensions.includes(ext)) {
23
+ return callback(null, file);
24
+ }
25
+
26
+ // Process the file contents
27
+ try {
28
+ const contents = file.contents.toString();
29
+
30
+ // Process templates
31
+ const templated = template(contents, data, {
32
+ brackets: ['[', ']'],
33
+ });
34
+
35
+ // Update file contents if changed
36
+ if (contents !== templated) {
37
+ file.contents = Buffer.from(templated);
38
+ const relativePath = file.relative || file.path;
39
+ }
40
+ } catch (error) {
41
+ console.error(`Error processing templates in ${file.path}:`, error);
42
+ }
43
+
44
+ // Pass the file through
45
+ callback(null, file);
46
+ },
47
47
  });
48
48
  }
49
49
 
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Libraries
2
2
  import webManager from 'web-manager';
3
+ import { attachTo as attachModeHelpers } from './utils/mode-helpers.js';
3
4
 
4
5
  // Manager Class
5
6
  class Manager {
@@ -133,5 +134,8 @@ class Manager {
133
134
  }
134
135
  }
135
136
 
137
+ // Mix in cross-context mode helpers (isDevelopment/isProduction/isTesting/getVersion).
138
+ attachModeHelpers(Manager);
139
+
136
140
  // Export
137
141
  export default Manager;
@@ -0,0 +1,120 @@
1
+ // Tiny expect() — Jest/Vitest-compatible subset.
2
+ // Each matcher throws on failure; the runner catches.
3
+
4
+ function deepEqual(a, b) {
5
+ if (a === b) return true;
6
+ if (typeof a !== typeof b) return false;
7
+ if (a === null || b === null) return a === b;
8
+ if (typeof a !== 'object') return false;
9
+
10
+ if (Array.isArray(a)) {
11
+ if (!Array.isArray(b) || a.length !== b.length) return false;
12
+ return a.every((x, i) => deepEqual(x, b[i]));
13
+ }
14
+
15
+ const ka = Object.keys(a);
16
+ const kb = Object.keys(b);
17
+ if (ka.length !== kb.length) return false;
18
+ return ka.every((k) => deepEqual(a[k], b[k]));
19
+ }
20
+
21
+ function fmt(v) {
22
+ if (typeof v === 'string') return JSON.stringify(v);
23
+ if (v === undefined) return 'undefined';
24
+ try {
25
+ return JSON.stringify(v);
26
+ } catch (e) {
27
+ return String(v);
28
+ }
29
+ }
30
+
31
+ function fail(message) {
32
+ const err = new Error(message);
33
+ err.name = 'AssertionError';
34
+ throw err;
35
+ }
36
+
37
+ function buildMatchers(actual, negated) {
38
+ const not = negated ? 'not ' : '';
39
+
40
+ function check(cond, message) {
41
+ if (negated) cond = !cond;
42
+ if (!cond) fail(message);
43
+ }
44
+
45
+ return {
46
+ toBe(expected) {
47
+ check(actual === expected, `expected ${fmt(actual)} ${not}to be ${fmt(expected)}`);
48
+ },
49
+ toEqual(expected) {
50
+ check(deepEqual(actual, expected), `expected ${fmt(actual)} ${not}to deeply equal ${fmt(expected)}`);
51
+ },
52
+ toBeTruthy() {
53
+ check(!!actual, `expected ${fmt(actual)} ${not}to be truthy`);
54
+ },
55
+ toBeFalsy() {
56
+ check(!actual, `expected ${fmt(actual)} ${not}to be falsy`);
57
+ },
58
+ toBeDefined() {
59
+ check(actual !== undefined, `expected ${fmt(actual)} ${not}to be defined`);
60
+ },
61
+ toBeUndefined() {
62
+ check(actual === undefined, `expected ${fmt(actual)} ${not}to be undefined`);
63
+ },
64
+ toBeNull() {
65
+ check(actual === null, `expected ${fmt(actual)} ${not}to be null`);
66
+ },
67
+ toContain(item) {
68
+ const has = Array.isArray(actual)
69
+ ? actual.includes(item)
70
+ : (typeof actual === 'string' && actual.includes(item));
71
+ check(has, `expected ${fmt(actual)} ${not}to contain ${fmt(item)}`);
72
+ },
73
+ toHaveProperty(key) {
74
+ const has = actual != null && Object.prototype.hasOwnProperty.call(actual, key);
75
+ check(has, `expected ${fmt(actual)} ${not}to have property "${key}"`);
76
+ },
77
+ toMatch(regex) {
78
+ check(regex.test(actual), `expected ${fmt(actual)} ${not}to match ${regex}`);
79
+ },
80
+ toBeInstanceOf(cls) {
81
+ check(actual instanceof cls, `expected value ${not}to be instance of ${cls.name}`);
82
+ },
83
+ toBeGreaterThan(n) {
84
+ check(actual > n, `expected ${fmt(actual)} ${not}to be > ${n}`);
85
+ },
86
+ toBeLessThan(n) {
87
+ check(actual < n, `expected ${fmt(actual)} ${not}to be < ${n}`);
88
+ },
89
+ async toThrow(matcher) {
90
+ let threw = false;
91
+ let thrown;
92
+ try {
93
+ if (typeof actual === 'function') {
94
+ await actual();
95
+ }
96
+ } catch (e) {
97
+ threw = true;
98
+ thrown = e;
99
+ }
100
+ if (!threw) {
101
+ return check(false, `expected function ${not}to throw`);
102
+ }
103
+ if (matcher instanceof RegExp) {
104
+ check(matcher.test(thrown.message), `expected thrown message ${not}to match ${matcher} (got: ${thrown.message})`);
105
+ } else if (typeof matcher === 'string') {
106
+ check(thrown.message.includes(matcher), `expected thrown message ${not}to contain "${matcher}" (got: ${thrown.message})`);
107
+ } else {
108
+ check(true, '');
109
+ }
110
+ },
111
+ };
112
+ }
113
+
114
+ function expect(actual) {
115
+ const m = buildMatchers(actual, false);
116
+ m.not = buildMatchers(actual, true);
117
+ return m;
118
+ }
119
+
120
+ module.exports = expect;
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en" data-page-path="/about">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>About — UJM Fixture Consumer</title>
6
+ </head>
7
+ <body>
8
+ <main id="main-content">
9
+ <h1>About</h1>
10
+ <p>Fixture about page.</p>
11
+ </main>
12
+ </body>
13
+ </html>
@@ -0,0 +1,2 @@
1
+ /* Fixture stylesheet — loaded by index.html to verify CSS MIME routing. */
2
+ body { font-family: system-ui, sans-serif; margin: 2rem; }
@@ -0,0 +1,6 @@
1
+ // Minimal fixture bundle. Real UJM bundles initialize a Manager + webManager;
2
+ // for boot tests we only need a script that loads without throwing so the
3
+ // fixture site can assert "no console errors".
4
+ (function () {
5
+ window.__ujmFixtureLoaded = true;
6
+ })();
@@ -0,0 +1,11 @@
1
+ {
2
+ "timestamp": "2024-01-01T00:00:00.000Z",
3
+ "repo": { "user": "itw-creative-works", "name": "ujm-fixture" },
4
+ "environment": "production",
5
+ "packages": { "ultimate-jekyll-manager": "fixture", "web-manager": "fixture" },
6
+ "config": {
7
+ "brand": { "id": "ujm-fixture", "name": "UJM Fixture Consumer" },
8
+ "theme": { "id": "classy", "target": "frontend" },
9
+ "uj": { "environment": "production", "cache_breaker": "fixture-1" }
10
+ }
11
+ }
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html lang="en" data-page-path="/">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>UJM Fixture Consumer</title>
6
+ <meta name="description" content="Fixture site for UJM boot tests.">
7
+ <link rel="stylesheet" href="/assets/css/main.bundle.css">
8
+ </head>
9
+ <body>
10
+ <main id="main-content">
11
+ <h1>UJM Fixture Consumer</h1>
12
+ <p>This is a minimal _site/ snapshot used by UJM's framework boot tests.</p>
13
+ </main>
14
+ <script>
15
+ window.Configuration = {
16
+ brand: { id: 'ujm-fixture', name: 'UJM Fixture Consumer' },
17
+ theme: { id: 'classy', target: 'frontend' },
18
+ uj: { environment: 'production', cache_breaker: 'fixture-1' },
19
+ };
20
+ </script>
21
+ <script src="/assets/js/main.bundle.js"></script>
22
+ <script>
23
+ if ('serviceWorker' in navigator) {
24
+ navigator.serviceWorker.register('/service-worker.js', { scope: '/' });
25
+ }
26
+ </script>
27
+ </body>
28
+ </html>
@@ -0,0 +1,29 @@
1
+ // Minimal fixture service worker. UJM's real SW imports Firebase + does cache
2
+ // management; for boot tests we only need to verify registration + activation
3
+ // + cache naming, so this is a trimmed-down stand-in.
4
+
5
+ const UJ_BUILD_JSON = {
6
+ config: {
7
+ brand: { id: 'ujm-fixture' },
8
+ uj: { environment: 'production', cache_breaker: 'fixture-1' },
9
+ },
10
+ };
11
+
12
+ const brand = UJ_BUILD_JSON.config.brand.id;
13
+ const cacheBreaker = UJ_BUILD_JSON.config.uj.cache_breaker;
14
+ const CACHE_NAME = `${brand}-${cacheBreaker}`;
15
+
16
+ self.addEventListener('install', (event) => {
17
+ self.skipWaiting();
18
+ });
19
+
20
+ self.addEventListener('activate', (event) => {
21
+ event.waitUntil(self.clients.claim());
22
+ });
23
+
24
+ self.addEventListener('message', (event) => {
25
+ const data = event.data || {};
26
+ if (data.command === 'get-cache-name') {
27
+ event.ports[0] && event.ports[0].postMessage({ name: CACHE_NAME });
28
+ }
29
+ });
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "ujm-fixture-consumer",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "Fixture consumer site used by UJM's framework boot tests. Not published."
6
+ }
@@ -0,0 +1,51 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>UJM Test Harness</title>
6
+ <!--
7
+ Page-layer test harness. Loaded by Puppeteer from a tiny embedded HTTP server.
8
+ Stubs `window.Configuration` (what consumer sites get from Jekyll-rendered
9
+ HTML) and the data-attributes Manager.initialize() reads. Page-layer suites
10
+ run their `run(ctx)` body inside this page via `page.evaluate(payload)` —
11
+ the payload bakes assert.js + each test's body as inline async functions.
12
+
13
+ Test results are emitted as console messages prefixed `__UJM_TEST__` and
14
+ parsed by the parent runner via `page.on('console')`.
15
+ -->
16
+ </head>
17
+ <body data-uj-test-harness>
18
+ <!--
19
+ Stub the same DOM signals the frontend Manager reads. The frontend
20
+ `src/index.js` reads `document.documentElement.dataset.pagePath` and
21
+ `dataset.assetPath` — set defaults here.
22
+ -->
23
+ <script>
24
+ document.documentElement.dataset.pagePath = '/';
25
+ document.documentElement.dataset.assetPath = '';
26
+
27
+ // Minimal stub of what Jekyll renders as `window.Configuration` —
28
+ // enough that webManager.initialize() doesn't throw, and enough for
29
+ // tests to assert against. Tests that need richer config can mutate
30
+ // this in their own setup.
31
+ window.Configuration = {
32
+ brand: { id: 'ujm-test', name: 'UJM Test' },
33
+ theme: { id: 'classy', target: 'frontend' },
34
+ meta: { title: 'UJM Test Harness' },
35
+ uj: { environment: 'development', cache_breaker: '0' },
36
+ web_manager: { firebase: { app: { config: { apiKey: 'test', projectId: 'ujm-test' } } } },
37
+ };
38
+
39
+ // Prerendered icons template — the harness exposes a single icon so
40
+ // `getPrerenderedIcon('test')` works without any consumer page setup.
41
+ const tmpl = document.createElement('template');
42
+ tmpl.id = 'prerendered-icons';
43
+ tmpl.innerHTML = '<svg data-icon="test" viewBox="0 0 1 1"><rect width="1" height="1"/></svg>';
44
+ document.body.appendChild(tmpl);
45
+
46
+ // Signal the canonical test-mode flag so cross-context helpers can pick
47
+ // it up even though `process.env` is unreachable from a tab.
48
+ globalThis.UJ_TEST_MODE = true;
49
+ </script>
50
+ </body>
51
+ </html>
@@ -0,0 +1,63 @@
1
+ // Public test API — what consumers see.
2
+ //
3
+ // Test files export a test definition. Three forms:
4
+ //
5
+ // Standalone:
6
+ // module.exports = {
7
+ // layer: 'build', // 'build' | 'page' | 'boot'
8
+ // description: 'config has brand.id',
9
+ // timeout: 5000,
10
+ // run: async (ctx) => {
11
+ // const cfg = Manager.getConfig('project');
12
+ // ctx.expect(cfg.brand.id).toBeTruthy();
13
+ // },
14
+ // cleanup: async (ctx) => { ... },
15
+ // };
16
+ //
17
+ // Boot layer — spawns Chromium with the consumer's actually-built `_site/` and
18
+ // serves it from an embedded local HTTP server, then runs `inspect` against the
19
+ // live site. Replaces shell-level smoke tests with deterministic, signal-driven
20
+ // pass/fail. Use this to verify the WHOLE integration: site builds, SW registers,
21
+ // pages render, no console errors.
22
+ //
23
+ // module.exports = {
24
+ // layer: 'boot',
25
+ // description: 'home renders + SW registers',
26
+ // timeout: 20000,
27
+ // inspect: async ({ site, page, expect, projectRoot }) => {
28
+ // await page.goto(site.baseUrl + '/');
29
+ // expect(await page.title()).toBeTruthy();
30
+ // },
31
+ // };
32
+ //
33
+ // Suite (sequential, shared state, stop on first failure):
34
+ // module.exports = {
35
+ // type: 'suite',
36
+ // layer: 'page',
37
+ // description: 'manager init',
38
+ // tests: [
39
+ // { name: 'step 1', run: async (ctx) => { ctx.state.mgr = new Manager(); } },
40
+ // { name: 'step 2', run: async (ctx) => { ctx.expect(ctx.state.mgr).toBeTruthy(); } },
41
+ // ],
42
+ // };
43
+ //
44
+ // Group (sequential, shared state, runs ALL tests even if some fail):
45
+ // module.exports = {
46
+ // type: 'group',
47
+ // layer: 'build',
48
+ // tests: [ ... ],
49
+ // };
50
+ //
51
+ // Array form (treated as group):
52
+ // module.exports = [ { name, run }, ... ];
53
+ //
54
+ // The ctx (context) provided to every run/cleanup includes:
55
+ // - ctx.expect — Jest-compatible assertion library
56
+ // - ctx.state — shared object across tests in a suite/group
57
+ // - ctx.skip(reason) — throw to skip the current test at runtime
58
+ // - ctx.layer — current layer name
59
+ // - ctx.page — Puppeteer Page (page layer only)
60
+
61
+ module.exports = {
62
+ expect: require('./assert.js'),
63
+ };