ultimate-jekyll-manager 1.1.10 โ 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 +22 -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 +15 -2
- 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 +8 -5
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* **SEO Optimized**: Ultimate Jekyll is fully SEO optimized.
|
|
29
29
|
* **Blazingly Fast**: Ultimate Jekyll is blazingly fast.
|
|
30
30
|
* **NPM & Gulp**: Ultimate Jekyll is fueled by an intuitive incorporation of npm and gulp.
|
|
31
|
+
* **Built-in test framework**: three layers (`build` / `page` / `boot`) โ plain Node, headless Chromium tab, headless Chromium against real `_site/` with SW registration verification.
|
|
31
32
|
|
|
32
33
|
## ๐ Getting started
|
|
33
34
|
1. [Create a repo](https://github.com/itw-creative-works/ultimate-jekyll/generate) from the **Ultimate Jekyll** template.
|
|
@@ -37,6 +38,49 @@
|
|
|
37
38
|
npm start
|
|
38
39
|
```
|
|
39
40
|
|
|
41
|
+
## ๐งช Testing
|
|
42
|
+
|
|
43
|
+
UJM ships a built-in three-layer test harness. Write tests under `test/<layer>/*.test.js` and run with:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx mgr test # all layers
|
|
47
|
+
npx mgr test --layer build # plain Node, fast
|
|
48
|
+
npx mgr test --layer page # headless Chromium tab against harness HTML
|
|
49
|
+
npx mgr test --layer boot # headless Chromium against built _site/
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Test files use Jest-compatible matchers:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
// test/build/config.test.js
|
|
56
|
+
const Manager = require('ultimate-jekyll-manager/build');
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
layer: 'build',
|
|
60
|
+
description: 'config has brand.id',
|
|
61
|
+
run: async (ctx) => {
|
|
62
|
+
const cfg = Manager.getConfig('project');
|
|
63
|
+
ctx.expect(cfg.brand.id).toBeTruthy();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Boot tests run against your actually-built `_site/` after `npm run build`:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
// test/boot/site.test.js
|
|
72
|
+
module.exports = {
|
|
73
|
+
layer: 'boot',
|
|
74
|
+
description: 'home renders + SW registers',
|
|
75
|
+
inspect: async ({ site, page, expect }) => {
|
|
76
|
+
await page.goto(site.baseUrl + '/');
|
|
77
|
+
expect((await page.title()).length).toBeGreaterThan(0);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Full guide: [docs/test-framework.md](docs/test-framework.md). Boot layer deep-dive: [docs/test-boot-layer.md](docs/test-boot-layer.md).
|
|
83
|
+
|
|
40
84
|
## ๐ฆ How to sync with the template
|
|
41
85
|
1. Simply run `npm start` in Terminal to get all the latest updates from the **Ultimate Jekyll template** and launch your website in the browser.
|
|
42
86
|
|
|
@@ -761,23 +805,8 @@ Add this to any js file to ONLY run in development mode (it will be excluded in
|
|
|
761
805
|
/* @dev-only:end */
|
|
762
806
|
```
|
|
763
807
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
โ seconds, while allowing further customization down the line. โ
|
|
770
|
-
โ Right now i am struggling on the theme portion of this โ
|
|
771
|
-
โ project. I want the user to be able to define the theme in โ
|
|
772
|
-
โ their _config.yml (which currently they do by setting โ
|
|
773
|
-
โ theme.id). I have some themes from the official bootstrap โ
|
|
774
|
-
โ team. usually a theme comes with a frontend, a backend/admin โ
|
|
775
|
-
โ dashboard, and docs. these 3 subparts of the theme have โ
|
|
776
|
-
โ different html structure and css and js requirements. so i โ
|
|
777
|
-
โ need a super easy system that allows me to make a file in โ
|
|
778
|
-
โ the consuming project, say its the index.html for example, โ
|
|
779
|
-
โ and i should easily be able to put which subseciton (or โ
|
|
780
|
-
โ target as i call it) of the theme to use. so for an agency โ
|
|
781
|
-
โ website i will probably use the frontend target, while for a โ
|
|
782
|
-
โ chat app i will probably use the backend target. however, i โ
|
|
783
|
-
โ need to be able to use
|
|
808
|
+
## ๐งฐ Sister projects
|
|
809
|
+
|
|
810
|
+
- [Electron Manager (EM)](https://github.com/itw-creative-works/electron-manager) โ same patterns, but for Electron desktop apps
|
|
811
|
+
- [Browser Extension Manager (BXM)](https://github.com/itw-creative-works/browser-extension-manager) โ same patterns, but for cross-browser MV3 extensions
|
|
812
|
+
- [Backend Manager (BEM)](https://github.com/itw-creative-works/backend-manager) โ Firebase Functions backend framework
|
package/dist/build.js
CHANGED
|
@@ -351,5 +351,8 @@ Manager.prototype.processBatches = Manager.processBatches;
|
|
|
351
351
|
// };
|
|
352
352
|
// };
|
|
353
353
|
|
|
354
|
+
// Mix in cross-context mode helpers (isDevelopment/isProduction/isTesting/getVersion).
|
|
355
|
+
require('./utils/mode-helpers.js').attachTo(Manager);
|
|
356
|
+
|
|
354
357
|
// Export
|
|
355
358
|
module.exports = Manager;
|
package/dist/cli.js
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Libraries
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const Manager = require('../build.js');
|
|
5
|
+
const mgr = new Manager();
|
|
6
|
+
const logger = mgr.logger('test');
|
|
7
|
+
const { run } = require('../test/runner.js');
|
|
8
|
+
|
|
9
|
+
module.exports = async function (options) {
|
|
10
|
+
const layer = options.layer || 'all';
|
|
11
|
+
const filter = options.filter || null;
|
|
12
|
+
const reporter = options.reporter || 'pretty';
|
|
13
|
+
const integration = options.integration === true || options.integration === 'true';
|
|
14
|
+
|
|
15
|
+
if (integration) {
|
|
16
|
+
process.env.UJ_TEST_INTEGRATION = '1';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Canonical signal โ every Manager picks this up via isTesting().
|
|
20
|
+
process.env.UJ_TEST_MODE = 'true';
|
|
21
|
+
|
|
22
|
+
// When UJM itself runs its own boot-layer tests (the cwd's package.json is
|
|
23
|
+
// UJM's package.json), there's no real consumer site to target. Point the
|
|
24
|
+
// boot runner at the fixture under dist/test/fixtures/consumer-site unless
|
|
25
|
+
// the caller has already set UJ_TEST_BOOT_PROJECT explicitly.
|
|
26
|
+
if (!process.env.UJ_TEST_BOOT_PROJECT) {
|
|
27
|
+
try {
|
|
28
|
+
const cwdPkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
29
|
+
if (cwdPkg.name === 'ultimate-jekyll-manager') {
|
|
30
|
+
process.env.UJ_TEST_BOOT_PROJECT = path.join(__dirname, '..', 'test', 'fixtures', 'consumer-site');
|
|
31
|
+
}
|
|
32
|
+
} catch (_) { /* no package.json โ leave unset */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (reporter !== 'json') {
|
|
36
|
+
logger.log(`Running tests (layer=${layer}${filter ? ` filter="${filter}"` : ''}${integration ? ' +integration' : ''})`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = await run({ layer, filter, reporter });
|
|
40
|
+
|
|
41
|
+
if (reporter === 'json') {
|
|
42
|
+
// Final machine-readable summary.
|
|
43
|
+
process.stdout.write(JSON.stringify({
|
|
44
|
+
event: 'summary',
|
|
45
|
+
passed: result.passed,
|
|
46
|
+
failed: result.failed,
|
|
47
|
+
skipped: result.skipped,
|
|
48
|
+
total: result.passed + result.failed + result.skipped,
|
|
49
|
+
}) + '\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (result.failed > 0) {
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
throw new Error(`${result.failed} test(s) failed`);
|
|
55
|
+
}
|
|
56
|
+
};
|
package/dist/defaults/CLAUDE.md
CHANGED
|
@@ -1,8 +1,74 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# ========== Default Values ==========
|
|
2
|
+
# Ultimate Jekyll Manager (UJM) โ consumer project
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
You should have a full understanding of Ultimate Jekyll before editing this project, which can be found at: node_modules/ultimate-jekyll-manager/CLAUDE.md
|
|
4
|
+
> **Auto-managed file.** Everything between `# ========== Default Values ==========` and `# ========== Custom Values ==========` is owned by `ultimate-jekyll-manager` and rewritten on every `npx mgr setup`. Put your own project-specific notes BELOW the `Custom Values` marker โ that section is preserved verbatim across setups.
|
|
6
5
|
|
|
7
|
-
##
|
|
8
|
-
|
|
6
|
+
## Framework
|
|
7
|
+
|
|
8
|
+
This project consumes **Ultimate Jekyll Manager** (UJM) โ a comprehensive framework for building modern Jekyll-powered static sites. UJM provides one-line bootstrap per context (build / frontend / service-worker), a multi-stage gulp pipeline (defaults / distribute / webpack / sass / imagemin / jekyll / audit / translation / minifyHtml / serve), default Jekyll layouts + themes, a frontend ES-module Manager with dynamic per-page module loading, a service worker with Firebase Messaging + cache management, and a built-in three-layer test framework.
|
|
9
|
+
|
|
10
|
+
**Framework's own docs** (read these for deep-dives; both paths point to the same files, the absolute path works regardless of working directory):
|
|
11
|
+
- Top-level overview: `/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/CLAUDE.md` (or `node_modules/ultimate-jekyll-manager/CLAUDE.md`)
|
|
12
|
+
- Subsystem references: `/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/docs/` (or `node_modules/ultimate-jekyll-manager/docs/`)
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm start # dev: clean โ setup โ bundle exec gulp serve (Jekyll + BrowserSync + livereload)
|
|
18
|
+
npm run build # production build (UJ_BUILD_MODE=true): clean โ setup โ full gulp pipeline โ _site/
|
|
19
|
+
npm run deploy # build โ `npu sync --message='Deploy'` (publishes _site/)
|
|
20
|
+
npx mgr test # run framework + project test suites (build / page / boot layers)
|
|
21
|
+
npx mgr audit # HTML validation + spellcheck + optional Lighthouse
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Where things live
|
|
25
|
+
|
|
26
|
+
- `src/_config.yml` โ Jekyll config: brand, theme, meta, web_manager (Firebase). `Manager.getConfig('project')` reads this. **`brand.id` + `theme.id` are required.**
|
|
27
|
+
- `config/ultimate-jekyll-manager.json` โ UJM-specific config (JSON5): purgecss safelist, webpack target, imagemin options, distribute glob patterns. `Manager.getUJMConfig()` reads this.
|
|
28
|
+
- `src/pages/<name>.html` โ your custom pages. May contain frontmatter only (and use a UJM `blueprint/*` layout) to customize a default page without writing HTML.
|
|
29
|
+
- `src/_layouts/`, `src/_includes/` โ custom layouts / includes that override UJM's defaults.
|
|
30
|
+
- `src/assets/css/main.scss` โ shared SCSS. Theme load paths resolve via `__theme__` webpack alias.
|
|
31
|
+
- `src/assets/css/pages/<page>/index.scss` โ page-specific styles (compile to `dist/assets/css/pages/<page>/...bundle.css`).
|
|
32
|
+
- `src/assets/js/main.js` โ main JS entry.
|
|
33
|
+
- `src/assets/js/pages/<page>/index.js` โ page-specific JS. UJM's frontend Manager loads these dynamically based on `data-page-path`.
|
|
34
|
+
- `src/_includes/frontend/sections/{nav,footer}.json` โ JSON-driven nav/footer config. UJM renders these into HTML at build time.
|
|
35
|
+
- `hooks/build/{pre,post}.js`, `hooks/middleware/request.js` โ optional lifecycle hooks.
|
|
36
|
+
- `_site/` โ Jekyll output (the deployable site). Not committed.
|
|
37
|
+
- `dist/` โ intermediate compile output (webpack bundles, sass, processed images) before Jekyll merges them into `_site/`.
|
|
38
|
+
- `test/**/*.js` โ your project test suites (framework auto-runs them alongside its own).
|
|
39
|
+
|
|
40
|
+
## Per-context imports
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// Frontend (browser ES module) โ every consumer page gets one
|
|
44
|
+
import Manager from 'ultimate-jekyll-manager';
|
|
45
|
+
new Manager().initialize();
|
|
46
|
+
|
|
47
|
+
// Service worker โ at the top of src/service-worker.js
|
|
48
|
+
importScripts('/build.js'); // exposes UJ_BUILD_JSON
|
|
49
|
+
// ...then construct your service-worker Manager
|
|
50
|
+
|
|
51
|
+
// Build-time / gulp / commands
|
|
52
|
+
const Manager = require('ultimate-jekyll-manager/build');
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Available APIs at runtime
|
|
56
|
+
|
|
57
|
+
After `new Manager().initialize()`, the frontend Manager exposes:
|
|
58
|
+
- `manager.webManager` โ Web Manager singleton (Firebase, auth, analytics, reactive `data-wm-bind` directives)
|
|
59
|
+
- `manager.isDevelopment()` / `isProduction()` / `isTesting()` / `getVersion()` โ cross-context helpers
|
|
60
|
+
|
|
61
|
+
At build time, `require('ultimate-jekyll-manager/build')` exposes:
|
|
62
|
+
- `Manager.getConfig(type)` โ read `_config.yml` (`'project'` or `'main'`)
|
|
63
|
+
- `Manager.getPackage(type)` โ read `package.json` (`'project'` or `'main'`)
|
|
64
|
+
- `Manager.getUJMConfig()` โ read `config/ultimate-jekyll-manager.json`
|
|
65
|
+
- `Manager.getEnvironment()` โ `'development'` or `'production'`
|
|
66
|
+
- `Manager.isBuildMode()` / `isQuickMode()` / `isServer()` / `actLikeProduction()` โ env-gated flags
|
|
67
|
+
- `Manager.logger(name)` โ timestamped logger instance
|
|
68
|
+
- `Manager.require(path)` โ escape hatch for UJM transitive deps (use sparingly)
|
|
69
|
+
|
|
70
|
+
# ========== Custom Values ==========
|
|
71
|
+
|
|
72
|
+
## Project-specific notes
|
|
73
|
+
|
|
74
|
+
Add anything specific to THIS project here. Edits below this line are preserved across `npx mgr setup` runs.
|
|
@@ -98,6 +98,15 @@ const FILE_MAP = {
|
|
|
98
98
|
mergeLines: true, // Merge line-by-line instead of overwriting
|
|
99
99
|
},
|
|
100
100
|
|
|
101
|
+
// Consumer CLAUDE.md uses the same marker-based merge as .env/.gitignore.
|
|
102
|
+
// Framework owns the Default section; consumer owns everything below the Custom marker.
|
|
103
|
+
// Must come AFTER `**/*.md` (which sets overwrite: false) โ `getFileOptions` does
|
|
104
|
+
// last-match-wins, so this rule's `mergeLines: true` activates the merge path even though
|
|
105
|
+
// the catch-all would otherwise skip.
|
|
106
|
+
'CLAUDE.md': {
|
|
107
|
+
mergeLines: true,
|
|
108
|
+
},
|
|
109
|
+
|
|
101
110
|
// Config file with smart merging
|
|
102
111
|
'config/ultimate-jekyll-manager.json': {
|
|
103
112
|
overwrite: true,
|
|
@@ -319,8 +328,12 @@ function mergeLineBasedFiles(existingContent, newContent, fileName) {
|
|
|
319
328
|
result.push(DEFAULT_SECTION_MARKER);
|
|
320
329
|
result.push(...mergedDefaultSection);
|
|
321
330
|
|
|
322
|
-
// Add custom section
|
|
323
|
-
|
|
331
|
+
// Add custom section. Insert a single blank line before the marker, but only if the
|
|
332
|
+
// merged default doesn't already end with one โ otherwise we'd accumulate an extra blank
|
|
333
|
+
// line on every merge, breaking idempotency after the first re-run.
|
|
334
|
+
if (mergedDefaultSection.length === 0 || mergedDefaultSection[mergedDefaultSection.length - 1].trim() !== '') {
|
|
335
|
+
result.push('');
|
|
336
|
+
}
|
|
324
337
|
result.push(CUSTOM_SECTION_MARKER);
|
|
325
338
|
|
|
326
339
|
// First add any user lines that were in default section (moved to custom)
|
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
|
+
};
|