ultimate-jekyll-manager 1.9.0 → 1.9.2

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.
@@ -0,0 +1,374 @@
1
+ // FormManager validation: HTML5 constraint validation (required, email, min,
2
+ // max, minlength, maxlength, pattern), honeypot detection, and file-accept
3
+ // matching. Logic inlined from form-manager.js.
4
+
5
+ module.exports = {
6
+ layer: 'page',
7
+ description: 'FormManager validation + honeypot + file-accept',
8
+ type: 'group',
9
+ tests: [
10
+ // ── Required validation ────────────────────────────────────────────
11
+ {
12
+ name: 'required text field fails when empty, passes when filled',
13
+ run: async (ctx) => {
14
+ var form = document.createElement('form');
15
+ form.innerHTML = '<input type="text" name="name" required>';
16
+ document.body.appendChild(form);
17
+
18
+ var errors = {};
19
+ var $f = form.querySelector('[name="name"]');
20
+
21
+ // Empty → error
22
+ if ($f.hasAttribute('required') && (!$f.value || !$f.value.trim())) {
23
+ errors['name'] = 'required';
24
+ }
25
+ ctx.expect(errors['name']).toBe('required');
26
+
27
+ // Filled → no error
28
+ errors = {};
29
+ $f.value = 'Ian';
30
+ if ($f.hasAttribute('required') && (!$f.value || !$f.value.trim())) {
31
+ errors['name'] = 'required';
32
+ }
33
+ ctx.expect(errors['name']).toBe(undefined);
34
+
35
+ form.remove();
36
+ },
37
+ },
38
+ {
39
+ name: 'required checkbox fails when unchecked',
40
+ run: async (ctx) => {
41
+ var form = document.createElement('form');
42
+ form.innerHTML = '<input type="checkbox" name="terms" required>';
43
+ document.body.appendChild(form);
44
+
45
+ var errors = {};
46
+ var $f = form.querySelector('[name="terms"]');
47
+
48
+ if ($f.hasAttribute('required') && $f.type === 'checkbox' && !$f.checked) {
49
+ errors['terms'] = 'required';
50
+ }
51
+ ctx.expect(errors['terms']).toBe('required');
52
+
53
+ $f.checked = true;
54
+ errors = {};
55
+ if ($f.hasAttribute('required') && $f.type === 'checkbox' && !$f.checked) {
56
+ errors['terms'] = 'required';
57
+ }
58
+ ctx.expect(errors['terms']).toBe(undefined);
59
+
60
+ form.remove();
61
+ },
62
+ },
63
+ {
64
+ name: 'required radio group fails when none checked',
65
+ run: async (ctx) => {
66
+ var form = document.createElement('form');
67
+ form.innerHTML = '<input type="radio" name="plan" value="a" required>'
68
+ + '<input type="radio" name="plan" value="b" required>';
69
+ document.body.appendChild(form);
70
+
71
+ var errors = {};
72
+ var $checked = form.querySelector('input[name="plan"]:checked');
73
+ if (!$checked) {
74
+ errors['plan'] = 'required';
75
+ }
76
+ ctx.expect(errors['plan']).toBe('required');
77
+
78
+ form.querySelector('[value="b"]').checked = true;
79
+ errors = {};
80
+ $checked = form.querySelector('input[name="plan"]:checked');
81
+ if (!$checked) {
82
+ errors['plan'] = 'required';
83
+ }
84
+ ctx.expect(errors['plan']).toBe(undefined);
85
+
86
+ form.remove();
87
+ },
88
+ },
89
+
90
+ // ── Email validation ───────────────────────────────────────────────
91
+ {
92
+ name: 'email validation rejects invalid formats and accepts valid ones',
93
+ run: async (ctx) => {
94
+ var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
95
+
96
+ ctx.expect(emailPattern.test('ian@example.com')).toBe(true);
97
+ ctx.expect(emailPattern.test('user+tag@sub.domain.co')).toBe(true);
98
+ ctx.expect(emailPattern.test('bad')).toBe(false);
99
+ ctx.expect(emailPattern.test('no@tld')).toBe(false);
100
+ ctx.expect(emailPattern.test('@missing.com')).toBe(false);
101
+ ctx.expect(emailPattern.test('spaces @test.com')).toBe(false);
102
+ },
103
+ },
104
+
105
+ // ── Min/max value validation ───────────────────────────────────────
106
+ {
107
+ name: 'number min/max validation catches out-of-range values',
108
+ run: async (ctx) => {
109
+ var form = document.createElement('form');
110
+ form.innerHTML = '<input type="number" name="age" min="18" max="120">';
111
+ document.body.appendChild(form);
112
+
113
+ var $f = form.querySelector('[name="age"]');
114
+
115
+ function validate(value) {
116
+ $f.value = value;
117
+ if ($f.hasAttribute('min') && parseFloat($f.value) < parseFloat($f.getAttribute('min'))) return 'too-low';
118
+ if ($f.hasAttribute('max') && parseFloat($f.value) > parseFloat($f.getAttribute('max'))) return 'too-high';
119
+ return 'ok';
120
+ }
121
+
122
+ ctx.expect(validate('17')).toBe('too-low');
123
+ ctx.expect(validate('18')).toBe('ok');
124
+ ctx.expect(validate('50')).toBe('ok');
125
+ ctx.expect(validate('120')).toBe('ok');
126
+ ctx.expect(validate('121')).toBe('too-high');
127
+
128
+ form.remove();
129
+ },
130
+ },
131
+
132
+ // ── Minlength / maxlength validation ───────────────────────────────
133
+ {
134
+ name: 'minlength and maxlength validation',
135
+ run: async (ctx) => {
136
+ var form = document.createElement('form');
137
+ form.innerHTML = '<input type="text" name="code" minlength="3" maxlength="10">';
138
+ document.body.appendChild(form);
139
+
140
+ var $f = form.querySelector('[name="code"]');
141
+
142
+ function validate(value) {
143
+ $f.value = value;
144
+ var minLen = parseInt($f.getAttribute('minlength'), 10);
145
+ var maxLen = parseInt($f.getAttribute('maxlength'), 10);
146
+ if ($f.value.length < minLen) return 'too-short';
147
+ if ($f.value.length > maxLen) return 'too-long';
148
+ return 'ok';
149
+ }
150
+
151
+ ctx.expect(validate('ab')).toBe('too-short');
152
+ ctx.expect(validate('abc')).toBe('ok');
153
+ ctx.expect(validate('abcdefghij')).toBe('ok');
154
+ ctx.expect(validate('abcdefghijk')).toBe('too-long');
155
+
156
+ form.remove();
157
+ },
158
+ },
159
+
160
+ // ── Pattern validation ─────────────────────────────────────────────
161
+ {
162
+ name: 'pattern attribute validation',
163
+ run: async (ctx) => {
164
+ var form = document.createElement('form');
165
+ form.innerHTML = '<input type="text" name="zip" pattern="[0-9]{5}" title="5-digit zip">';
166
+ document.body.appendChild(form);
167
+
168
+ var $f = form.querySelector('[name="zip"]');
169
+
170
+ function validate(value) {
171
+ $f.value = value;
172
+ var pattern = new RegExp('^' + $f.getAttribute('pattern') + '$');
173
+ return pattern.test($f.value);
174
+ }
175
+
176
+ ctx.expect(validate('12345')).toBe(true);
177
+ ctx.expect(validate('1234')).toBe(false);
178
+ ctx.expect(validate('123456')).toBe(false);
179
+ ctx.expect(validate('abcde')).toBe(false);
180
+
181
+ form.remove();
182
+ },
183
+ },
184
+
185
+ // ── Honeypot detection ─────────────────────────────────────────────
186
+ {
187
+ name: 'honeypot detection catches [data-honey] and [name="honey"] fields',
188
+ run: async (ctx) => {
189
+ var HONEYPOT_SELECTOR = '[data-honey], [name="honey"]';
190
+
191
+ // Form with empty honeypot → not filled
192
+ var form = document.createElement('form');
193
+ form.innerHTML = '<input type="text" name="email" value="real">'
194
+ + '<input type="text" name="honey" data-honey value="">';
195
+ document.body.appendChild(form);
196
+
197
+ function isHoneypotFilled(f) {
198
+ var pots = f.querySelectorAll(HONEYPOT_SELECTOR);
199
+ for (var i = 0; i < pots.length; i++) {
200
+ if (pots[i].value && pots[i].value.trim() !== '') return true;
201
+ }
202
+ return false;
203
+ }
204
+
205
+ ctx.expect(isHoneypotFilled(form)).toBe(false);
206
+
207
+ // Fill the honeypot → detected
208
+ form.querySelector('[name="honey"]').value = 'bot-spam';
209
+ ctx.expect(isHoneypotFilled(form)).toBe(true);
210
+
211
+ form.remove();
212
+ },
213
+ },
214
+ {
215
+ name: 'honeypot detects data-honey without name="honey"',
216
+ run: async (ctx) => {
217
+ var HONEYPOT_SELECTOR = '[data-honey], [name="honey"]';
218
+
219
+ var form = document.createElement('form');
220
+ form.innerHTML = '<input type="text" name="realfield" value="ok">'
221
+ + '<input type="text" name="decoy" data-honey value="">';
222
+ document.body.appendChild(form);
223
+
224
+ function isHoneypotFilled(f) {
225
+ var pots = f.querySelectorAll(HONEYPOT_SELECTOR);
226
+ for (var i = 0; i < pots.length; i++) {
227
+ if (pots[i].value && pots[i].value.trim() !== '') return true;
228
+ }
229
+ return false;
230
+ }
231
+
232
+ ctx.expect(isHoneypotFilled(form)).toBe(false);
233
+
234
+ form.querySelector('[data-honey]').value = 'filled';
235
+ ctx.expect(isHoneypotFilled(form)).toBe(true);
236
+
237
+ form.remove();
238
+ },
239
+ },
240
+
241
+ // ── _fileMatchesAccept ─────────────────────────────────────────────
242
+ {
243
+ name: 'file-accept matching: extension, wildcard MIME, exact MIME',
244
+ run: async (ctx) => {
245
+ function fileMatchesAccept(fileName, fileType, accept) {
246
+ var types = accept.split(',').map(function (t) { return t.trim().toLowerCase(); });
247
+ fileName = fileName.toLowerCase();
248
+ fileType = (fileType || '').toLowerCase();
249
+ var extToCategory = {
250
+ '.jpg': 'image/', '.jpeg': 'image/', '.png': 'image/', '.gif': 'image/',
251
+ '.webp': 'image/', '.svg': 'image/', '.pdf': 'application/pdf',
252
+ };
253
+
254
+ for (var i = 0; i < types.length; i++) {
255
+ var type = types[i];
256
+ if (type.startsWith('.')) {
257
+ if (fileName.endsWith(type)) return true;
258
+ continue;
259
+ }
260
+ if (type.endsWith('/*')) {
261
+ var prefix = type.slice(0, -2) + '/';
262
+ if (fileType && fileType.startsWith(prefix)) return true;
263
+ if (!fileType) {
264
+ var ext = '.' + fileName.split('.').pop();
265
+ var guessed = extToCategory[ext] || '';
266
+ if (guessed.startsWith(prefix)) return true;
267
+ }
268
+ continue;
269
+ }
270
+ if (fileType === type) return true;
271
+ }
272
+ return false;
273
+ }
274
+
275
+ // Extension matching
276
+ ctx.expect(fileMatchesAccept('doc.pdf', 'application/pdf', '.pdf')).toBe(true);
277
+ ctx.expect(fileMatchesAccept('doc.txt', 'text/plain', '.pdf')).toBe(false);
278
+
279
+ // Wildcard MIME
280
+ ctx.expect(fileMatchesAccept('photo.jpg', 'image/jpeg', 'image/*')).toBe(true);
281
+ ctx.expect(fileMatchesAccept('photo.png', 'image/png', 'image/*')).toBe(true);
282
+ ctx.expect(fileMatchesAccept('doc.pdf', 'application/pdf', 'image/*')).toBe(false);
283
+
284
+ // Exact MIME
285
+ ctx.expect(fileMatchesAccept('doc.pdf', 'application/pdf', 'application/pdf')).toBe(true);
286
+ ctx.expect(fileMatchesAccept('doc.pdf', 'application/pdf', 'text/plain')).toBe(false);
287
+
288
+ // Multi-accept
289
+ ctx.expect(fileMatchesAccept('photo.jpg', 'image/jpeg', '.pdf,image/*')).toBe(true);
290
+ ctx.expect(fileMatchesAccept('doc.pdf', 'application/pdf', '.pdf,image/*')).toBe(true);
291
+ ctx.expect(fileMatchesAccept('code.js', 'text/javascript', '.pdf,image/*')).toBe(false);
292
+
293
+ // Extension fallback when MIME type is empty
294
+ ctx.expect(fileMatchesAccept('photo.jpg', '', 'image/*')).toBe(true);
295
+ ctx.expect(fileMatchesAccept('photo.png', '', 'image/*')).toBe(true);
296
+ ctx.expect(fileMatchesAccept('unknown.xyz', '', 'image/*')).toBe(false);
297
+ },
298
+ },
299
+
300
+ // ── Field error display ────────────────────────────────────────────
301
+ {
302
+ name: 'field error display adds is-invalid class and feedback element',
303
+ run: async (ctx) => {
304
+ var form = document.createElement('form');
305
+ form.innerHTML = '<div class="mb-3"><input type="text" name="email" class="form-control"></div>';
306
+ document.body.appendChild(form);
307
+
308
+ var $field = form.querySelector('[name="email"]');
309
+
310
+ // Show error
311
+ $field.classList.add('is-invalid');
312
+ var $feedback = document.createElement('div');
313
+ $feedback.className = 'invalid-feedback';
314
+ $feedback.textContent = 'Email is required';
315
+ $feedback.style.display = 'block';
316
+ $field.parentElement.appendChild($feedback);
317
+
318
+ ctx.expect($field.classList.contains('is-invalid')).toBe(true);
319
+ var $fb = $field.parentElement.querySelector('.invalid-feedback');
320
+ ctx.expect($fb).toBeTruthy();
321
+ ctx.expect($fb.textContent).toBe('Email is required');
322
+
323
+ // Clear error
324
+ $field.classList.remove('is-invalid');
325
+ $fb.style.display = 'none';
326
+ ctx.expect($field.classList.contains('is-invalid')).toBe(false);
327
+
328
+ form.remove();
329
+ },
330
+ },
331
+
332
+ // ── Query param population ─────────────────────────────────────────
333
+ {
334
+ name: 'query param population skips UTM/tracking params',
335
+ run: async (ctx) => {
336
+ var skipPrefixes = ['utm_', 'itm_'];
337
+ var skipExact = ['cb', 'fbclid', 'gclid'];
338
+
339
+ function shouldSkip(key) {
340
+ for (var i = 0; i < skipPrefixes.length; i++) {
341
+ if (key.startsWith(skipPrefixes[i])) return true;
342
+ }
343
+ return skipExact.indexOf(key) !== -1;
344
+ }
345
+
346
+ ctx.expect(shouldSkip('utm_source')).toBe(true);
347
+ ctx.expect(shouldSkip('utm_campaign')).toBe(true);
348
+ ctx.expect(shouldSkip('itm_source')).toBe(true);
349
+ ctx.expect(shouldSkip('cb')).toBe(true);
350
+ ctx.expect(shouldSkip('fbclid')).toBe(true);
351
+ ctx.expect(shouldSkip('gclid')).toBe(true);
352
+ ctx.expect(shouldSkip('email')).toBe(false);
353
+ ctx.expect(shouldSkip('name')).toBe(false);
354
+ },
355
+ },
356
+
357
+ // ── State attribute ────────────────────────────────────────────────
358
+ {
359
+ name: 'data-form-state attribute reflects state transitions',
360
+ run: async (ctx) => {
361
+ var form = document.createElement('form');
362
+ document.body.appendChild(form);
363
+
364
+ var states = ['initializing', 'ready', 'submitting', 'ready', 'submitted'];
365
+ states.forEach(function (s) {
366
+ form.setAttribute('data-form-state', s);
367
+ ctx.expect(form.getAttribute('data-form-state')).toBe(s);
368
+ });
369
+
370
+ form.remove();
371
+ },
372
+ },
373
+ ],
374
+ };
@@ -387,12 +387,22 @@ Errors display with Bootstrap's `is-invalid` class and `.invalid-feedback` eleme
387
387
 
388
388
  When the form transitions to `ready` state, FormManager automatically focuses the field with the `autofocus` attribute (if present and not disabled).
389
389
 
390
- **Permanently-disabled fields (`data-fm-keep-disabled`):**
390
+ **Permanently-disabled fields (snapshot model):**
391
391
 
392
- FormManager blanket-toggles `disabled` on every control in the form while loading/submitting and re-enables them on `ready`/error. Fields that must STAY disabled (e.g. "coming soon" options rendered inside a managed form) opt out with the `data-fm-keep-disabled` attributethe toggle always forces them to `disabled = true` and never re-enables them:
392
+ FormManager snapshots every element that has `disabled` in HTML markup at init time (excluding `type="submit"` buttons, which are treated as loading guards). Snapshotted elements stay disabled through every state transition no data attributes needed. Just put `disabled` on the element in HTML:
393
393
 
394
394
  ```html
395
- <input type="radio" name="plan" value="enterprise" disabled data-fm-keep-disabled>
395
+ <input type="radio" name="plan" value="enterprise" disabled>
396
+ <select name="region" disabled>...</select>
397
+ ```
398
+
399
+ Submit buttons can still use `disabled` in HTML as a loading guard — FM always manages them regardless of initial state. For full pre-JS protection, add `data-form-state="initializing"` and `onsubmit="return false"` to the `<form>` tag, with CSS to block interaction:
400
+
401
+ ```css
402
+ form[data-form-state]:not([data-form-state="ready"]) {
403
+ pointer-events: none;
404
+ opacity: 0.6;
405
+ }
396
406
  ```
397
407
 
398
408
  **Methods:**
package/logs/test.log CHANGED
@@ -1,31 +1,31 @@
1
- # ujm log — 2026-06-17T22:01:06.463Z — pid=85523
2
- [15:01:06] 'test': Running tests (layer=all)
3
- [15:01:06] 'test': Test mode: normal (external APIs skipped)
1
+ # ujm log — 2026-06-18T02:44:23.186Z — pid=67215
2
+ [19:44:23] 'test': Running tests (layer=all)
3
+ [19:44:23] 'test': Test mode: normal (external APIs skipped)
4
4
 
5
5
  Ultimate Jekyll Manager Tests
6
6
 
7
7
  Framework Tests
8
8
  ⤷ attach-log-file — tee stdout/stderr to a file
9
9
  ✓ exports the expected surface (0ms)
10
- ✓ stripAnsi removes color escape codes (1ms)
10
+ ✓ stripAnsi removes color escape codes (0ms)
11
11
  hello world
12
12
  colored line
13
13
  ✓ attach + stdout.write + detach: file contains the writes (0ms)
14
14
  ✓ idempotent: attaching twice with same name returns same fd (0ms)
15
15
  ✓ attach with falsy name returns null and does nothing (0ms)
16
16
  ⤷ CLI alias resolution
17
- ✓ cli.js exports a Main class (1ms)
18
- ✓ all expected commands exist on disk (0ms)
19
- ✓ each command module exports an async function (8ms)
17
+ ✓ cli.js exports a Main class (0ms)
18
+ ✓ all expected commands exist on disk (1ms)
19
+ ✓ each command module exports an async function (3ms)
20
20
  ⤷ collectTextNodes (utils/collectTextNodes.js)
21
- ✓ extracts page title (134ms)
21
+ ✓ extracts page title (107ms)
22
22
  ✓ skips <script> and <style> (1ms)
23
- ✓ spellcheck dictionary (utils/dictionary.js) (0ms)
23
+ ✓ spellcheck dictionary (utils/dictionary.js) (1ms)
24
24
  ⤷ expect() matcher set
25
25
  ✓ toBe + toEqual basics (0ms)
26
26
  ✓ .not negates (0ms)
27
27
  ✓ toContain works on arrays and strings (0ms)
28
- ✓ toThrow catches sync + async throws (1ms)
28
+ ✓ toThrow catches sync + async throws (0ms)
29
29
  ✓ toBeGreaterThan / toBeLessThan (0ms)
30
30
  ✓ failing assertions throw AssertionError (0ms)
31
31
  ✓ package.json exports resolve to real files in dist/ (0ms)
@@ -44,16 +44,16 @@ colored line
44
44
  ✓ actLikeProduction is true when isBuildMode OR UJ_AUDIT_FORCE (0ms)
45
45
  ✓ getRootPath("package") points at UJM root (0ms)
46
46
  ✓ getMemoryUsage returns shape with MB-sized numbers (0ms)
47
- ✓ getArguments returns object with _ array + boolean defaults (1ms)
47
+ ✓ getArguments returns object with _ array + boolean defaults (0ms)
48
48
  ✓ logger returns object with log/error/warn/info methods (0ms)
49
49
  ✓ processBatches processes items in chunks and returns flat results (0ms)
50
50
  ⤷ mergeJekyllConfigs (utils/merge-jekyll-configs.js)
51
51
  ✓ merges collections from both configs (project additions win) (3ms)
52
52
  ✓ dedups defaults by scope key (project wins) (1ms)
53
- ✓ returns null when there is nothing to merge (0ms)
53
+ ✓ returns null when there is nothing to merge (1ms)
54
54
  ⤷ mode-helpers (isTesting / isDevelopment / isProduction / getVersion)
55
55
  ✓ helpers attach to Manager statically AND on prototype (0ms)
56
- ✓ isTesting reflects UJ_TEST_MODE env (1ms)
56
+ ✓ isTesting reflects UJ_TEST_MODE env (0ms)
57
57
  ✓ isDevelopment false / isProduction true when UJ_BUILD_MODE=true (and not testing) (0ms)
58
58
  ✓ environments are mutually exclusive — testing wins under UJ_TEST_MODE (0ms)
59
59
  ✓ invariant: is*() exactly matches getEnvironment() + mutually exclusive (every scenario) (0ms)
@@ -63,49 +63,82 @@ colored line
63
63
  ✓ leaves non-matching extensions untouched (e.g. .css) (0ms)
64
64
  ✓ passes directories through untouched (0ms)
65
65
  ⤷ node-powertools templating brackets ({} and [])
66
- ✓ default { } brackets resolve nested keys (0ms)
66
+ ✓ default { } brackets resolve nested keys (1ms)
67
67
  ✓ [ ] brackets resolve nested keys when explicitly configured (0ms)
68
68
  ✓ [ ] brackets leave Jekyll {{ }} placeholders alone (0ms)
69
69
  ⤷ theme contract (structure, swappability, cross-theme JS contracts)
70
70
  ✓ _template: entry files + config contract (0ms)
71
- ✓ classy: entry files + config contract (0ms)
72
- ✓ neobrutalism: entry files + config contract (1ms)
71
+ ✓ classy: entry files + config contract (1ms)
72
+ ✓ neobrutalism: entry files + config contract (0ms)
73
73
  ✓ newsflash: entry files + config contract (0ms)
74
- ✓ _template: layouts swappable, markup clean (1ms)
75
- ✓ classy: layouts swappable, markup clean (14ms)
76
- ✓ neobrutalism: layouts swappable, markup clean (1ms)
77
- ✓ newsflash: layouts swappable, markup clean (3ms)
74
+ ✓ _template: layouts swappable, markup clean (2ms)
75
+ ✓ classy: layouts swappable, markup clean (15ms)
76
+ ✓ neobrutalism: layouts swappable, markup clean (2ms)
77
+ ✓ newsflash: layouts swappable, markup clean (2ms)
78
78
  ✓ _template: cross-theme JS contracts (0ms)
79
- ✓ classy: cross-theme JS contracts (0ms)
80
- ✓ neobrutalism: cross-theme JS contracts (1ms)
81
- ✓ newsflash: cross-theme JS contracts (0ms)
82
- ✓ page asset files match a declared asset_path shape (4ms)
79
+ ✓ classy: cross-theme JS contracts (1ms)
80
+ ✓ neobrutalism: cross-theme JS contracts (0ms)
81
+ ✓ newsflash: cross-theme JS contracts (1ms)
82
+ ✓ page asset files match a declared asset_path shape (3ms)
83
83
  ⤷ validateYAMLFrontMatter (utils/_validate-yaml.js)
84
- ✓ returns { valid: true } for a file with valid frontmatter (1ms)
85
- ✓ returns { valid: true } when no frontmatter present (0ms)
84
+ ✓ returns { valid: true } for a file with valid frontmatter (0ms)
85
+ ✓ returns { valid: true } when no frontmatter present (1ms)
86
86
  ✓ flags malformed YAML frontmatter as invalid with error message (0ms)
87
87
  ⤷ page-layer baseline (DOM + fetch + storage)
88
- ✓ document is interactive or complete (0ms)
89
- ✓ fetch() works against the local harness server (2ms)
90
- ✓ localStorage is available (0ms)
88
+ ✓ document is interactive or complete (1ms)
89
+ ✓ fetch() works against the local harness server (11ms)
90
+ ✓ localStorage is available (2ms)
91
+ ⤷ FormManager getData / setData / input groups
92
+ ✓ _setNested builds nested objects from dot paths (1ms)
93
+ ✓ _setNested accumulates duplicate keys into arrays (1ms)
94
+ ✓ _getNested reads nested values and returns undefined for missing paths (0ms)
95
+ ✓ _flattenObject converts nested objects to dot paths (2ms)
96
+ ✓ getData collects text, select, radio, and textarea values with dot notation (8ms)
97
+ ✓ getData handles single checkbox (boolean) and checkbox groups (object) (1ms)
98
+ ✓ getData excludes honeypot fields (0ms)
99
+ ✓ input group filter includes matching + global fields, excludes others (0ms)
100
+ ✓ input group filter with multiple groups includes all matching (0ms)
101
+ ✓ setData populates text, select, textarea fields (1ms)
102
+ ✓ setData sets radio group to matching value (0ms)
103
+ ✓ setData sets single checkbox boolean and checkbox group values (0ms)
104
+ ⤷ FormManager disabled-state snapshot
105
+ ✓ snapshot captures disabled non-submit elements, ignores submit buttons (0ms)
106
+ ✓ setDisabled(true) disables everything (1ms)
107
+ ✓ setDisabled(false) re-enables managed elements but keeps snapshotted ones disabled (0ms)
108
+ ✓ survives multiple disable/enable cycles (0ms)
109
+ ✓ onsubmit="return false" blocks native submission before FM loads (20ms)
110
+ ⤷ FormManager validation + honeypot + file-accept
111
+ ✓ required text field fails when empty, passes when filled (0ms)
112
+ ✓ required checkbox fails when unchecked (0ms)
113
+ ✓ required radio group fails when none checked (0ms)
114
+ ✓ email validation rejects invalid formats and accepts valid ones (3ms)
115
+ ✓ number min/max validation catches out-of-range values (1ms)
116
+ ✓ minlength and maxlength validation (0ms)
117
+ ✓ pattern attribute validation (0ms)
118
+ ✓ honeypot detection catches [data-honey] and [name="honey"] fields (0ms)
119
+ ✓ honeypot detects data-honey without name="honey" (0ms)
120
+ ✓ file-accept matching: extension, wildcard MIME, exact MIME (1ms)
121
+ ✓ field error display adds is-invalid class and feedback element (0ms)
122
+ ✓ query param population skips UTM/tracking params (0ms)
123
+ ✓ data-form-state attribute reflects state transitions (0ms)
91
124
  ⤷ harness globals (window.Configuration + dataset)
92
125
  ✓ window.Configuration has brand + theme + web_manager (0ms)
93
126
  ✓ document.documentElement.dataset.pagePath is set (0ms)
94
127
  ✓ UJ_TEST_MODE is signalled on globalThis (0ms)
95
128
  ⤷ prerendered icons template lookup
96
- ✓ template#prerendered-icons exists and has the test icon (0ms)
129
+ ✓ template#prerendered-icons exists and has the test icon (1ms)
97
130
  ✓ looking up a missing icon returns null (0ms)
98
131
  ⤷ boot tests (consumer _site/)
99
- ✓ /service-worker.js served with javascript content type (94ms)
100
- ✓ index.html registers SW and reaches activated state (146ms)
101
- ✓ SW responds to get-cache-name message with brand-id pattern (76ms)
102
- ✓ home page renders with title + body content (171ms)
103
- ✓ /about resolves via Jekyll-style .html fallback (75ms)
104
- ✓ build.json is served with brand metadata (67ms)
105
- ✓ CSS bundle served with text/css content type (70ms)
132
+ ✓ /service-worker.js served with javascript content type (97ms)
133
+ ✓ index.html registers SW and reaches activated state (251ms)
134
+ ✓ SW responds to get-cache-name message with brand-id pattern (85ms)
135
+ ✓ home page renders with title + body content (184ms)
136
+ ✓ /about resolves via Jekyll-style .html fallback (71ms)
137
+ ✓ build.json is served with brand metadata (78ms)
138
+ ✓ CSS bundle served with text/css content type (73ms)
106
139
 
107
140
  Results
108
- 80 passing
141
+ 110 passing
109
142
 
110
- Total: 80 tests in 6044ms
143
+ Total: 110 tests in 3457ms
111
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {