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
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// Test runner — discovers + runs suites, reports UJM-style.
|
|
2
|
+
//
|
|
3
|
+
// Discovery: globs for `test/**/*.js` (recursively, excluding directories starting with `_`)
|
|
4
|
+
// in two locations:
|
|
5
|
+
// 1. The framework itself (ultimate-jekyll-manager/dist/test/suites/...) — default suites.
|
|
6
|
+
// 2. The consumer project's CWD (./test/...) — consumer suites.
|
|
7
|
+
//
|
|
8
|
+
// Each test file is a CommonJS module that exports a test definition (see ./index.js).
|
|
9
|
+
// Three forms supported:
|
|
10
|
+
//
|
|
11
|
+
// - Standalone: module.exports = { layer, description, run, cleanup, timeout, skip };
|
|
12
|
+
// - Suite: module.exports = { type: 'suite', layer, description, tests: [...], cleanup, stopOnFailure };
|
|
13
|
+
// - Group: module.exports = { type: 'group', layer, description, tests: [...], cleanup };
|
|
14
|
+
// - Array form: module.exports = [ {name, run}, ... ]; // implicit group
|
|
15
|
+
//
|
|
16
|
+
// `tests[]` items are { name, run(ctx), cleanup?, skip?, timeout? }.
|
|
17
|
+
//
|
|
18
|
+
// Suites stop on first failure (sequential, share state). Groups run all tests regardless.
|
|
19
|
+
//
|
|
20
|
+
// Layers:
|
|
21
|
+
// - 'build' runs in plain Node (this file).
|
|
22
|
+
// - 'page' spawns Chromium via runners/chromium.js, runs in a tab loading the harness HTML.
|
|
23
|
+
// - 'boot' spawns Chromium loading the consumer's actually-built `_site/` from a tiny
|
|
24
|
+
// embedded HTTP server.
|
|
25
|
+
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const glob = require('glob').globSync;
|
|
28
|
+
const jetpack = require('fs-jetpack');
|
|
29
|
+
const chalk = require('chalk').default;
|
|
30
|
+
|
|
31
|
+
const expect = require('./assert.js');
|
|
32
|
+
|
|
33
|
+
// Chromium / boot runners are lazy-loaded so a missing puppeteer doesn't prevent
|
|
34
|
+
// build-layer tests from running. The dispatch points below require() them only when
|
|
35
|
+
// those layers exist.
|
|
36
|
+
|
|
37
|
+
class SkipError extends Error {
|
|
38
|
+
constructor(reason) { super(reason); this.name = 'SkipError'; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function run(options = {}) {
|
|
42
|
+
options.layer = options.layer || 'all';
|
|
43
|
+
options.filter = options.filter || null;
|
|
44
|
+
options.reporter = options.reporter || 'pretty';
|
|
45
|
+
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
|
|
48
|
+
const sources = discoverTestFiles();
|
|
49
|
+
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(chalk.bold(' Ultimate Jekyll Manager Tests'));
|
|
52
|
+
|
|
53
|
+
const results = { passed: 0, failed: 0, skipped: 0, tests: [] };
|
|
54
|
+
|
|
55
|
+
if (sources.framework.length > 0) {
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(chalk.bold(' Framework Tests'));
|
|
58
|
+
await runSource(sources.framework, 'framework', options, results);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (sources.project.length > 0) {
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(chalk.bold(' Project Tests'));
|
|
64
|
+
await runSource(sources.project, 'project', options, results);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (sources.framework.length === 0 && sources.project.length === 0) {
|
|
68
|
+
console.log(chalk.gray(' No test files found.'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
reportResults(results, Date.now() - startTime);
|
|
72
|
+
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function runSource(files, source, options, results) {
|
|
77
|
+
// Partition by layer (peek at module.exports without invoking run functions).
|
|
78
|
+
const byLayer = { build: [], page: [], boot: [] };
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const layer = peekLayer(file) || 'build';
|
|
81
|
+
if (byLayer[layer]) byLayer[layer].push(file);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build layer — run inline.
|
|
85
|
+
if ((options.layer === 'all' || options.layer === 'build') && byLayer.build.length > 0) {
|
|
86
|
+
for (const file of byLayer.build) {
|
|
87
|
+
await runBuildFile(file, source, options, results);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Page layer — share one Chromium instance for all page-layer suites.
|
|
92
|
+
const wantsPage = (options.layer === 'all' || options.layer === 'page') && byLayer.page.length > 0;
|
|
93
|
+
|
|
94
|
+
if (wantsPage) {
|
|
95
|
+
let runChromiumTests;
|
|
96
|
+
try {
|
|
97
|
+
({ runChromiumTests } = require('./runners/chromium.js'));
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.log(chalk.yellow(` ○ page tests skipped (chromium runner not available: ${e.message})`));
|
|
100
|
+
results.skipped += byLayer.page.length;
|
|
101
|
+
}
|
|
102
|
+
if (runChromiumTests) {
|
|
103
|
+
const projectRoot = process.cwd();
|
|
104
|
+
const counts = await runChromiumTests({
|
|
105
|
+
pageSuiteFiles: byLayer.page,
|
|
106
|
+
filter: options.filter,
|
|
107
|
+
projectRoot,
|
|
108
|
+
ujmDistRoot: path.resolve(__dirname, '..'),
|
|
109
|
+
});
|
|
110
|
+
results.passed += counts.passed;
|
|
111
|
+
results.failed += counts.failed;
|
|
112
|
+
results.skipped += counts.skipped;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Boot layer — spawn Chromium pointed at consumer's built `_site/`.
|
|
117
|
+
if ((options.layer === 'all' || options.layer === 'boot') && byLayer.boot.length > 0) {
|
|
118
|
+
await runBootLayer(byLayer.boot, source, options, results);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function runBootLayer(files, source, options, results) {
|
|
123
|
+
// Aggregate every boot test (whether standalone or inside a suite) into one flat list.
|
|
124
|
+
// The boot harness runs them sequentially in a single Chromium process to keep startup
|
|
125
|
+
// cost amortized. State doesn't carry across boot tests — each runs against a single
|
|
126
|
+
// shared `site` (the consumer's built `_site/`).
|
|
127
|
+
const tests = [];
|
|
128
|
+
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
let mod;
|
|
131
|
+
try {
|
|
132
|
+
delete require.cache[require.resolve(file)];
|
|
133
|
+
mod = require(file);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
const rel = relativizePath(file, source);
|
|
136
|
+
console.log(chalk.red(` ✗ ${rel}`));
|
|
137
|
+
console.log(chalk.red(` Failed to load: ${e.message}`));
|
|
138
|
+
results.failed += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (Array.isArray(mod)) mod = { type: 'group', tests: mod };
|
|
143
|
+
if (Array.isArray(mod.tests)) {/* multi-test */ }
|
|
144
|
+
else if (typeof mod.inspect === 'function') mod = { tests: [mod] };
|
|
145
|
+
|
|
146
|
+
const baseDescription = mod.description || relativizePath(file, source);
|
|
147
|
+
|
|
148
|
+
for (const t of (mod.tests || [])) {
|
|
149
|
+
if (typeof t.inspect !== 'function') continue;
|
|
150
|
+
if (options.filter && !(t.description || baseDescription).includes(options.filter)) continue;
|
|
151
|
+
tests.push({
|
|
152
|
+
description: t.description || baseDescription,
|
|
153
|
+
timeout: t.timeout || mod.timeout || 20000,
|
|
154
|
+
inspect: t.inspect,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (tests.length === 0) return;
|
|
160
|
+
|
|
161
|
+
let runBootTests;
|
|
162
|
+
try {
|
|
163
|
+
({ runBootTests } = require('./runners/boot.js'));
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.log(chalk.yellow(` ○ boot tests skipped (boot runner not available: ${e.message})`));
|
|
166
|
+
results.skipped += tests.length;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(chalk.cyan(' ⤷ boot tests (consumer _site/)'));
|
|
171
|
+
|
|
172
|
+
const projectRoot = process.cwd();
|
|
173
|
+
const counts = await runBootTests({
|
|
174
|
+
tests,
|
|
175
|
+
projectRoot,
|
|
176
|
+
ujmDistRoot: path.resolve(__dirname, '..'),
|
|
177
|
+
});
|
|
178
|
+
results.passed += counts.passed;
|
|
179
|
+
results.failed += counts.failed;
|
|
180
|
+
results.skipped += counts.skipped;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function peekLayer(file) {
|
|
184
|
+
try {
|
|
185
|
+
delete require.cache[require.resolve(file)];
|
|
186
|
+
const mod = require(file);
|
|
187
|
+
if (Array.isArray(mod)) return 'build';
|
|
188
|
+
return mod.layer || 'build';
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function runBuildFile(file, source, options, results) {
|
|
195
|
+
let mod;
|
|
196
|
+
try {
|
|
197
|
+
delete require.cache[require.resolve(file)];
|
|
198
|
+
mod = require(file);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
const rel = relativizePath(file, source);
|
|
201
|
+
console.log(chalk.red(` ✗ ${rel}`));
|
|
202
|
+
console.log(chalk.red(` Failed to load: ${e.message}`));
|
|
203
|
+
results.failed += 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (Array.isArray(mod)) {
|
|
208
|
+
mod = { type: 'group', tests: mod };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const rel = relativizePath(file, source);
|
|
212
|
+
|
|
213
|
+
if (mod.skip) {
|
|
214
|
+
const reason = typeof mod.skip === 'string' ? mod.skip : '';
|
|
215
|
+
console.log(chalk.yellow(` ○ ${mod.description || rel}`) + chalk.gray(` (skipped${reason ? ': ' + reason : ''})`));
|
|
216
|
+
const count = Array.isArray(mod.tests) ? mod.tests.length : 1;
|
|
217
|
+
results.skipped += count;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (mod.type === 'suite' || mod.type === 'group' || Array.isArray(mod.tests)) {
|
|
222
|
+
await runSuite(mod, rel, options, results);
|
|
223
|
+
} else {
|
|
224
|
+
await runStandalone(mod, rel, options, results);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function runSuite(suite, rel, options, results) {
|
|
229
|
+
const description = suite.description || rel;
|
|
230
|
+
const isGroup = suite.type === 'group';
|
|
231
|
+
const stopOnFailure = !isGroup && suite.stopOnFailure !== false;
|
|
232
|
+
const tests = suite.tests || [];
|
|
233
|
+
|
|
234
|
+
console.log(chalk.cyan(` ⤷ ${description}`));
|
|
235
|
+
|
|
236
|
+
const state = {};
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < tests.length; i += 1) {
|
|
239
|
+
const t = tests[i];
|
|
240
|
+
const name = t.name || `step-${i + 1}`;
|
|
241
|
+
|
|
242
|
+
if (options.filter && !name.includes(options.filter) && !description.includes(options.filter)) continue;
|
|
243
|
+
|
|
244
|
+
if (t.skip) {
|
|
245
|
+
const reason = typeof t.skip === 'string' ? t.skip : '';
|
|
246
|
+
console.log(chalk.yellow(` ○ ${name}`) + chalk.gray(` (skipped${reason ? ': ' + reason : ''})`));
|
|
247
|
+
results.skipped += 1;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const ctx = createContext({ state, layer: suite.layer || 'build' });
|
|
252
|
+
const timeout = t.timeout || suite.timeout || 30000;
|
|
253
|
+
|
|
254
|
+
const start = Date.now();
|
|
255
|
+
try {
|
|
256
|
+
await Promise.race([
|
|
257
|
+
Promise.resolve(t.run(ctx)),
|
|
258
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Test timeout')), timeout)),
|
|
259
|
+
]);
|
|
260
|
+
const duration = Date.now() - start;
|
|
261
|
+
console.log(chalk.green(` ✓ ${name}`) + chalk.gray(` (${duration}ms)`));
|
|
262
|
+
results.passed += 1;
|
|
263
|
+
|
|
264
|
+
if (t.cleanup) {
|
|
265
|
+
try { await t.cleanup(ctx); } catch (e) {
|
|
266
|
+
console.log(chalk.yellow(` ⚠ Cleanup failed: ${e.message}`));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
const duration = Date.now() - start;
|
|
271
|
+
if (e.name === 'SkipError') {
|
|
272
|
+
console.log(chalk.yellow(` ○ ${name}`) + chalk.gray(` (skipped: ${e.message})`));
|
|
273
|
+
results.skipped += 1;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
console.log(chalk.red(` ✗ ${name}`) + chalk.gray(` (${duration}ms)`));
|
|
277
|
+
console.log(chalk.red(` ${e.message || e}`));
|
|
278
|
+
results.failed += 1;
|
|
279
|
+
|
|
280
|
+
if (stopOnFailure) {
|
|
281
|
+
const remaining = tests.length - i - 1;
|
|
282
|
+
if (remaining > 0) {
|
|
283
|
+
console.log(chalk.yellow(` Skipping ${remaining} remaining test(s) in suite`));
|
|
284
|
+
results.skipped += remaining;
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (suite.cleanup) {
|
|
292
|
+
try {
|
|
293
|
+
const ctx = createContext({ state, layer: suite.layer || 'build' });
|
|
294
|
+
await suite.cleanup(ctx);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.log(chalk.yellow(` ⚠ Suite cleanup failed: ${e.message}`));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function runStandalone(mod, rel, options, results) {
|
|
302
|
+
const description = mod.description || rel;
|
|
303
|
+
if (options.filter && !description.includes(options.filter)) return;
|
|
304
|
+
|
|
305
|
+
const ctx = createContext({ state: {}, layer: mod.layer || 'build' });
|
|
306
|
+
const timeout = mod.timeout || 30000;
|
|
307
|
+
|
|
308
|
+
const start = Date.now();
|
|
309
|
+
try {
|
|
310
|
+
await Promise.race([
|
|
311
|
+
Promise.resolve(mod.run(ctx)),
|
|
312
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Test timeout')), timeout)),
|
|
313
|
+
]);
|
|
314
|
+
const duration = Date.now() - start;
|
|
315
|
+
console.log(chalk.green(` ✓ ${description}`) + chalk.gray(` (${duration}ms)`));
|
|
316
|
+
results.passed += 1;
|
|
317
|
+
|
|
318
|
+
if (mod.cleanup) {
|
|
319
|
+
try { await mod.cleanup(ctx); } catch (e) {
|
|
320
|
+
console.log(chalk.yellow(` ⚠ Cleanup failed: ${e.message}`));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (e) {
|
|
324
|
+
const duration = Date.now() - start;
|
|
325
|
+
if (e.name === 'SkipError') {
|
|
326
|
+
console.log(chalk.yellow(` ○ ${description}`) + chalk.gray(` (skipped: ${e.message})`));
|
|
327
|
+
results.skipped += 1;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
console.log(chalk.red(` ✗ ${description}`) + chalk.gray(` (${duration}ms)`));
|
|
331
|
+
console.log(chalk.red(` ${e.message || e}`));
|
|
332
|
+
results.failed += 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function createContext({ state, layer }) {
|
|
337
|
+
return {
|
|
338
|
+
expect,
|
|
339
|
+
state,
|
|
340
|
+
layer,
|
|
341
|
+
skip(reason) { throw new SkipError(reason || 'skipped at runtime'); },
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function reportResults(results, durationMs) {
|
|
346
|
+
const total = results.passed + results.failed + results.skipped;
|
|
347
|
+
console.log('');
|
|
348
|
+
console.log(' ' + chalk.bold('Results'));
|
|
349
|
+
console.log(` ${chalk.green(`${results.passed} passing`)}`);
|
|
350
|
+
if (results.failed > 0) console.log(` ${chalk.red(`${results.failed} failing`)}`);
|
|
351
|
+
if (results.skipped > 0) console.log(` ${chalk.yellow(`${results.skipped} skipped`)}`);
|
|
352
|
+
console.log(chalk.gray(`\n Total: ${total} tests in ${durationMs}ms\n`));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function discoverTestFiles() {
|
|
356
|
+
const framework = [];
|
|
357
|
+
const project = [];
|
|
358
|
+
|
|
359
|
+
// Detect whether we're running UJM's own framework self-tests, vs a consumer
|
|
360
|
+
// who installed UJM and is running their own tests. Used below to filter the
|
|
361
|
+
// boot/ layer of framework suites — those target UJM's internal fixture
|
|
362
|
+
// _site/, so they only make sense when UJM tests itself.
|
|
363
|
+
const isFrameworkSelfTest = (() => {
|
|
364
|
+
try {
|
|
365
|
+
const cwdPkg = require(path.join(process.cwd(), 'package.json'));
|
|
366
|
+
return cwdPkg.name === 'ultimate-jekyll-manager';
|
|
367
|
+
} catch (_) { return false; }
|
|
368
|
+
})();
|
|
369
|
+
|
|
370
|
+
// Framework default suites (relative to this file: dist/test/runner.js).
|
|
371
|
+
// For consumers, we exclude boot/ — those suites assert on UJM's own fixture
|
|
372
|
+
// site and would fail noisily when run against a real consumer's _site/.
|
|
373
|
+
// Consumers write their own boot tests under <cwd>/test/boot/.
|
|
374
|
+
const frameworkSuitesDir = path.join(__dirname, 'suites');
|
|
375
|
+
if (jetpack.exists(frameworkSuitesDir)) {
|
|
376
|
+
const ignore = ['_**'];
|
|
377
|
+
if (!isFrameworkSelfTest) ignore.push('boot/**');
|
|
378
|
+
glob('**/*.js', { cwd: frameworkSuitesDir, ignore }).sort().forEach((rel) => {
|
|
379
|
+
framework.push(path.join(frameworkSuitesDir, rel));
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Consumer project suites — CWD/test/**/*.js. Skip when running from inside the
|
|
384
|
+
// framework's own dist tree (where consumer-tests-dir === framework-tests-parent).
|
|
385
|
+
const projectTestsDir = path.join(process.cwd(), 'test');
|
|
386
|
+
if (jetpack.exists(projectTestsDir) && projectTestsDir !== path.dirname(frameworkSuitesDir)) {
|
|
387
|
+
glob('**/*.js', { cwd: projectTestsDir, ignore: ['_**'] }).sort().forEach((rel) => {
|
|
388
|
+
project.push(path.join(projectTestsDir, rel));
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return { framework, project };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function relativizePath(file, source) {
|
|
396
|
+
if (source === 'framework') {
|
|
397
|
+
return path.relative(path.join(__dirname, 'suites'), file);
|
|
398
|
+
}
|
|
399
|
+
return path.relative(path.join(process.cwd(), 'test'), file);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
module.exports = { run, SkipError };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Boot-runner — spawns Chromium pointed at the consumer's actually-built
|
|
2
|
+
// `_site/` via a tiny embedded HTTP server, runs `inspect` functions against
|
|
3
|
+
// the live site, then closes cleanly.
|
|
4
|
+
//
|
|
5
|
+
// Why a server (not file://)? Service workers don't register from file:// URLs
|
|
6
|
+
// — the whole point of boot tests is to catch SW registration / cache-name /
|
|
7
|
+
// activation regressions, so we serve over real http.
|
|
8
|
+
//
|
|
9
|
+
// `inspect` functions receive { site, page, expect, projectRoot } where:
|
|
10
|
+
// site.baseUrl — http://127.0.0.1:<port> root
|
|
11
|
+
// site.port — port the local server bound to
|
|
12
|
+
// site.root — absolute path to the served _site/ directory
|
|
13
|
+
// page — Puppeteer Page (fresh per test)
|
|
14
|
+
// projectRoot — absolute path to the consumer project root
|
|
15
|
+
// expect — same Jest-compatible expect() as build/page
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const chalk = require('chalk').default;
|
|
20
|
+
|
|
21
|
+
const { startServer } = require('../server.js');
|
|
22
|
+
|
|
23
|
+
async function runBootTests({ tests, projectRoot, ujmDistRoot }) {
|
|
24
|
+
if (tests.length === 0) return { passed: 0, failed: 0, skipped: 0 };
|
|
25
|
+
|
|
26
|
+
let puppeteer;
|
|
27
|
+
try {
|
|
28
|
+
puppeteer = require('puppeteer');
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.log(chalk.yellow(` ○ boot tests skipped (puppeteer not installed)`));
|
|
31
|
+
return { passed: 0, failed: 0, skipped: tests.length };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Locate the consumer's built `_site/`. Discovery order:
|
|
35
|
+
// 1. UJ_TEST_BOOT_DIR (explicit absolute path) — full override
|
|
36
|
+
// 2. UJ_TEST_BOOT_PROJECT/_site — UJM self-test fixture path
|
|
37
|
+
// 3. <projectRoot>/_site — default for UJM consumers
|
|
38
|
+
const candidates = [];
|
|
39
|
+
if (process.env.UJ_TEST_BOOT_DIR) candidates.push(path.resolve(process.env.UJ_TEST_BOOT_DIR));
|
|
40
|
+
if (process.env.UJ_TEST_BOOT_PROJECT) candidates.push(path.join(path.resolve(process.env.UJ_TEST_BOOT_PROJECT), '_site'));
|
|
41
|
+
candidates.push(path.join(projectRoot, '_site'));
|
|
42
|
+
|
|
43
|
+
let siteRoot = null;
|
|
44
|
+
for (const dir of candidates) {
|
|
45
|
+
if (fs.existsSync(path.join(dir, 'index.html'))) {
|
|
46
|
+
siteRoot = dir;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!siteRoot) {
|
|
51
|
+
console.log(chalk.yellow(` ○ boot tests skipped (no _site/index.html found in any of:`));
|
|
52
|
+
for (const c of candidates) console.log(chalk.yellow(` ${c}`));
|
|
53
|
+
console.log(chalk.yellow(` — run \`npm run build\` first to produce _site/)`));
|
|
54
|
+
return { passed: 0, failed: 0, skipped: tests.length };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (process.env.UJ_TEST_DEBUG) {
|
|
58
|
+
console.log(chalk.gray(` [boot] serving _site/ from ${siteRoot}`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const expect = require('../assert.js');
|
|
62
|
+
const counts = { passed: 0, failed: 0, skipped: 0 };
|
|
63
|
+
|
|
64
|
+
const server = await startServer({ root: siteRoot });
|
|
65
|
+
|
|
66
|
+
const browser = await puppeteer.launch({
|
|
67
|
+
headless: 'new',
|
|
68
|
+
args: [
|
|
69
|
+
'--no-sandbox',
|
|
70
|
+
'--disable-dev-shm-usage',
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const site = {
|
|
76
|
+
baseUrl: server.baseUrl,
|
|
77
|
+
port: server.port,
|
|
78
|
+
root: siteRoot,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (const t of tests) {
|
|
82
|
+
const start = Date.now();
|
|
83
|
+
const page = await browser.newPage();
|
|
84
|
+
try {
|
|
85
|
+
await Promise.race([
|
|
86
|
+
t.inspect({ site, page, expect, projectRoot }),
|
|
87
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Boot test timeout')), t.timeout || 20000)),
|
|
88
|
+
]);
|
|
89
|
+
const duration = Date.now() - start;
|
|
90
|
+
console.log(chalk.green(` ✓ ${t.description}`) + chalk.gray(` (${duration}ms)`));
|
|
91
|
+
counts.passed += 1;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
const duration = Date.now() - start;
|
|
94
|
+
console.log(chalk.red(` ✗ ${t.description}`) + chalk.gray(` (${duration}ms)`));
|
|
95
|
+
console.log(chalk.red(` ${(e && e.message) || String(e)}`));
|
|
96
|
+
counts.failed += 1;
|
|
97
|
+
} finally {
|
|
98
|
+
try { await page.close(); } catch (_) { /* ignore */ }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
try { await browser.close(); } catch (_) { /* ignore */ }
|
|
103
|
+
try { await server.close(); } catch (_) { /* ignore */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return counts;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { runBootTests };
|