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.
- package/CHANGELOG.md +40 -0
- package/CLAUDE.md +138 -1775
- package/README.md +49 -20
- package/dist/build.js +3 -0
- package/dist/cli.js +1 -0
- package/dist/commands/test.js +56 -0
- package/dist/defaults/CLAUDE.md +72 -6
- package/dist/gulp/tasks/defaults.js +20 -4
- package/dist/gulp/tasks/distribute.js +29 -26
- package/dist/gulp/tasks/jsonToHtml.js +86 -80
- package/dist/gulp/tasks/minifyHtml.js +55 -51
- package/dist/gulp/tasks/sass.js +7 -6
- package/dist/gulp/tasks/utils/template-transform.js +35 -35
- package/dist/index.js +4 -0
- package/dist/test/assert.js +120 -0
- package/dist/test/fixtures/consumer-site/_site/about.html +13 -0
- package/dist/test/fixtures/consumer-site/_site/assets/css/main.bundle.css +2 -0
- package/dist/test/fixtures/consumer-site/_site/assets/js/main.bundle.js +6 -0
- package/dist/test/fixtures/consumer-site/_site/build.json +11 -0
- package/dist/test/fixtures/consumer-site/_site/index.html +28 -0
- package/dist/test/fixtures/consumer-site/_site/service-worker.js +29 -0
- package/dist/test/fixtures/consumer-site/package.json +6 -0
- package/dist/test/harness/page/index.html +51 -0
- package/dist/test/index.js +63 -0
- package/dist/test/runner.js +402 -0
- package/dist/test/runners/boot.js +109 -0
- package/dist/test/runners/chromium.js +255 -0
- package/dist/test/server.js +127 -0
- package/dist/test/suites/boot/service-worker.test.js +84 -0
- package/dist/test/suites/boot/site-loads.test.js +65 -0
- package/dist/test/suites/build/cli.test.js +49 -0
- package/dist/test/suites/build/collect-text-nodes.test.js +37 -0
- package/dist/test/suites/build/dictionary.test.js +17 -0
- package/dist/test/suites/build/expect.test.js +59 -0
- package/dist/test/suites/build/exports.test.js +32 -0
- package/dist/test/suites/build/logger.test.js +62 -0
- package/dist/test/suites/build/manager.test.js +186 -0
- package/dist/test/suites/build/merge-jekyll-configs.test.js +95 -0
- package/dist/test/suites/build/mode-helpers.test.js +65 -0
- package/dist/test/suites/build/template-transform.test.js +94 -0
- package/dist/test/suites/build/templating-brackets.test.js +46 -0
- package/dist/test/suites/build/validate-yaml.test.js +60 -0
- package/dist/test/suites/page/dom-baseline.test.js +34 -0
- package/dist/test/suites/page/harness-globals.test.js +38 -0
- package/dist/test/suites/page/prerendered-icons.test.js +32 -0
- package/dist/utils/mode-helpers.js +84 -0
- package/docs/_legacy-claude-md.md +1832 -0
- package/docs/cross-context-helpers.md +75 -0
- package/docs/test-boot-layer.md +110 -0
- package/docs/test-framework.md +183 -0
- 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
|
|
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(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return file;
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
);
|
|
137
|
+
// Push processed files to the stream
|
|
138
|
+
processedFiles.forEach(file => this.push(file));
|
|
139
|
+
}
|
|
133
140
|
|
|
134
|
-
|
|
135
|
-
|
|
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', () => {
|
package/dist/gulp/tasks/sass.js
CHANGED
|
@@ -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
|
|
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(
|
|
328
|
-
|
|
327
|
+
.pipe(new Transform({
|
|
328
|
+
objectMode: true,
|
|
329
|
+
transform(file, enc, cb) {
|
|
329
330
|
cb(null, file);
|
|
330
331
|
},
|
|
331
|
-
|
|
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
|
|
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
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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,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,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
|
+
};
|