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.
Files changed (46) hide show
  1. package/CHANGELOG.md +22 -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 +15 -2
  9. package/dist/index.js +4 -0
  10. package/dist/test/assert.js +120 -0
  11. package/dist/test/fixtures/consumer-site/_site/about.html +13 -0
  12. package/dist/test/fixtures/consumer-site/_site/assets/css/main.bundle.css +2 -0
  13. package/dist/test/fixtures/consumer-site/_site/assets/js/main.bundle.js +6 -0
  14. package/dist/test/fixtures/consumer-site/_site/build.json +11 -0
  15. package/dist/test/fixtures/consumer-site/_site/index.html +28 -0
  16. package/dist/test/fixtures/consumer-site/_site/service-worker.js +29 -0
  17. package/dist/test/fixtures/consumer-site/package.json +6 -0
  18. package/dist/test/harness/page/index.html +51 -0
  19. package/dist/test/index.js +63 -0
  20. package/dist/test/runner.js +402 -0
  21. package/dist/test/runners/boot.js +109 -0
  22. package/dist/test/runners/chromium.js +255 -0
  23. package/dist/test/server.js +127 -0
  24. package/dist/test/suites/boot/service-worker.test.js +84 -0
  25. package/dist/test/suites/boot/site-loads.test.js +65 -0
  26. package/dist/test/suites/build/cli.test.js +49 -0
  27. package/dist/test/suites/build/collect-text-nodes.test.js +37 -0
  28. package/dist/test/suites/build/dictionary.test.js +17 -0
  29. package/dist/test/suites/build/expect.test.js +59 -0
  30. package/dist/test/suites/build/exports.test.js +32 -0
  31. package/dist/test/suites/build/logger.test.js +62 -0
  32. package/dist/test/suites/build/manager.test.js +186 -0
  33. package/dist/test/suites/build/merge-jekyll-configs.test.js +95 -0
  34. package/dist/test/suites/build/mode-helpers.test.js +65 -0
  35. package/dist/test/suites/build/template-transform.test.js +94 -0
  36. package/dist/test/suites/build/templating-brackets.test.js +46 -0
  37. package/dist/test/suites/build/validate-yaml.test.js +60 -0
  38. package/dist/test/suites/page/dom-baseline.test.js +34 -0
  39. package/dist/test/suites/page/harness-globals.test.js +38 -0
  40. package/dist/test/suites/page/prerendered-icons.test.js +32 -0
  41. package/dist/utils/mode-helpers.js +84 -0
  42. package/docs/_legacy-claude-md.md +1832 -0
  43. package/docs/cross-context-helpers.md +75 -0
  44. package/docs/test-boot-layer.md +110 -0
  45. package/docs/test-framework.md +183 -0
  46. 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
- > This project is "ultimate-jekyll", an NPM module that helps โ”‚
765
- โ”‚ streamline development of Jekyll websites. A "consuming โ”‚
766
- โ”‚ project" will require this NPM module to take advantage of โ”‚
767
- โ”‚ its features like automatic folder structure setup, themes, โ”‚
768
- โ”‚ and default pages to get a website up and running in โ”‚
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
@@ -17,6 +17,7 @@ const ALIASES = {
17
17
  // Tasks
18
18
  audit: ['-a', '--audit'],
19
19
  translation: ['-t', '--translation'],
20
+ test: ['--test'],
20
21
  };
21
22
 
22
23
  // Function to resolve command name from aliases
@@ -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
+ };
@@ -1,8 +1,74 @@
1
- # Identity
2
- This is a jekyll site that is "consuming" a jekyll template project called Ultimate Jekyll--a collection of components that can be used to build a Jekyll site quickly and efficiently.
1
+ # ========== Default Values ==========
2
+ # Ultimate Jekyll Manager (UJM) โ€” consumer project
3
3
 
4
- ## UJ Documentation
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
- ## Project-specific Notes
8
- Add any notes specific to this project here.
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
- result.push('');
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,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
+ };