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.
- package/CHANGELOG.md +18 -0
- package/PROGRESS.md +24 -0
- package/dist/assets/js/libs/form-manager.js +15 -4
- package/dist/assets/js/pages/account/sections/api-keys.js +143 -38
- package/dist/assets/js/pages/test/libraries/form-manager/index.js +53 -0
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +210 -3
- package/dist/defaults/dist/pages/test/libraries/form-manager.html +72 -0
- package/dist/test/suites/page/form-manager-data.test.js +418 -0
- package/dist/test/suites/page/form-manager-disabled.test.js +170 -0
- package/dist/test/suites/page/form-manager-validation.test.js +374 -0
- package/docs/javascript-libraries.md +13 -3
- package/logs/test.log +72 -39
- package/package.json +1 -1
|
@@ -301,6 +301,78 @@ meta:
|
|
|
301
301
|
</div>
|
|
302
302
|
</div>
|
|
303
303
|
|
|
304
|
+
<!-- Test 7: Disabled-State Snapshot -->
|
|
305
|
+
<div class="col-lg-8">
|
|
306
|
+
<div class="card">
|
|
307
|
+
<div class="card-header">
|
|
308
|
+
<h5 class="mb-0">Test 7: Disabled-State Snapshot</h5>
|
|
309
|
+
<small class="text-muted">Elements disabled in HTML stay disabled through every FM state transition. Submit buttons are loading guards — FM takes them over.</small>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="card-body">
|
|
312
|
+
<form id="test-form-snapshot" data-form-state="initializing" onsubmit="return false">
|
|
313
|
+
<div class="row">
|
|
314
|
+
<div class="col-md-6">
|
|
315
|
+
<h6 class="text-muted mb-3">FM-managed <span class="badge bg-success">enabled on ready</span></h6>
|
|
316
|
+
<div class="mb-3">
|
|
317
|
+
<label for="snapshot-name" class="form-label">Name</label>
|
|
318
|
+
<input type="text" class="form-control" id="snapshot-name" name="name" value="Ian">
|
|
319
|
+
</div>
|
|
320
|
+
<div class="mb-3">
|
|
321
|
+
<label for="snapshot-email" class="form-label">Email</label>
|
|
322
|
+
<input type="email" class="form-control" id="snapshot-email" name="email" value="ian@example.com">
|
|
323
|
+
</div>
|
|
324
|
+
<div class="mb-3">
|
|
325
|
+
<label class="form-label d-block">Plan</label>
|
|
326
|
+
<div class="form-check">
|
|
327
|
+
<input type="radio" class="form-check-input" id="snapshot-plan-free" name="plan" value="free" checked>
|
|
328
|
+
<label class="form-check-label" for="snapshot-plan-free">Free</label>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="form-check">
|
|
331
|
+
<input type="radio" class="form-check-input" id="snapshot-plan-pro" name="plan" value="pro">
|
|
332
|
+
<label class="form-check-label" for="snapshot-plan-pro">Pro</label>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="form-check">
|
|
335
|
+
<input type="radio" class="form-check-input" id="snapshot-plan-enterprise" name="plan" value="enterprise" disabled>
|
|
336
|
+
<label class="form-check-label" for="snapshot-plan-enterprise">Enterprise <span class="badge bg-secondary">Coming soon</span></label>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="col-md-6">
|
|
341
|
+
<h6 class="text-muted mb-3">Permanently disabled <span class="badge bg-danger">stays disabled</span></h6>
|
|
342
|
+
<div class="mb-3">
|
|
343
|
+
<label for="snapshot-region" class="form-label">Region <span class="badge bg-secondary">Coming soon</span></label>
|
|
344
|
+
<select class="form-select" id="snapshot-region" name="region" disabled>
|
|
345
|
+
<option>US (coming soon)</option>
|
|
346
|
+
<option>EU (coming soon)</option>
|
|
347
|
+
</select>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="mb-3">
|
|
350
|
+
<label for="snapshot-notes" class="form-label">Internal notes <span class="badge bg-secondary">Admin only</span></label>
|
|
351
|
+
<textarea class="form-control" id="snapshot-notes" name="notes" rows="2" disabled>Restricted field</textarea>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="alert alert-info small mb-0">
|
|
354
|
+
Enterprise radio, Region select, and Notes textarea have <code>disabled</code> in the HTML. FM snapshots them at init and never re-enables them. The other fields start without <code>disabled</code> — the form-level <code>data-form-state</code> + CSS blocks interaction until FM is ready.
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
<div class="d-flex gap-2">
|
|
359
|
+
<button type="submit" class="btn btn-primary" disabled>
|
|
360
|
+
<span class="button-text">Submit (watch disabled states)</span>
|
|
361
|
+
</button>
|
|
362
|
+
<button type="button" id="snapshot-cycle" class="btn btn-outline-warning">Rapid cycle (5x)</button>
|
|
363
|
+
</div>
|
|
364
|
+
</form>
|
|
365
|
+
<div class="mt-3 d-flex gap-3">
|
|
366
|
+
<small class="text-muted" id="snapshot-status">Status: initializing</small>
|
|
367
|
+
<small class="text-muted" id="snapshot-cycle-count">Cycles: 0</small>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="mt-2">
|
|
370
|
+
<pre id="snapshot-output" class="bg-body-secondary p-2 rounded small" style="max-height: 160px; overflow: auto;">Submit to see disabled states toggle. Enterprise radio + Region select should NEVER re-enable.</pre>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
304
376
|
</div>
|
|
305
377
|
</div>
|
|
306
378
|
</section>
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// FormManager data collection and population: getData() with dot notation,
|
|
2
|
+
// radio/checkbox groups, honeypot exclusion, input-group filtering, and
|
|
3
|
+
// setData() reverse population. Logic inlined from form-manager.js since
|
|
4
|
+
// page-layer tests can't import ESM modules.
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
layer: 'page',
|
|
8
|
+
description: 'FormManager getData / setData / input groups',
|
|
9
|
+
type: 'group',
|
|
10
|
+
tests: [
|
|
11
|
+
// ── _setNested: dot-notation path → nested object ──────────────────
|
|
12
|
+
{
|
|
13
|
+
name: '_setNested builds nested objects from dot paths',
|
|
14
|
+
run: async (ctx) => {
|
|
15
|
+
function setNested(obj, path, value) {
|
|
16
|
+
var keys = path.split('.');
|
|
17
|
+
var lastKey = keys.pop();
|
|
18
|
+
var current = obj;
|
|
19
|
+
for (var i = 0; i < keys.length; i++) {
|
|
20
|
+
if (!current[keys[i]] || typeof current[keys[i]] !== 'object') {
|
|
21
|
+
current[keys[i]] = {};
|
|
22
|
+
}
|
|
23
|
+
current = current[keys[i]];
|
|
24
|
+
}
|
|
25
|
+
if (current[lastKey] !== undefined) {
|
|
26
|
+
if (!Array.isArray(current[lastKey])) {
|
|
27
|
+
current[lastKey] = [current[lastKey]];
|
|
28
|
+
}
|
|
29
|
+
current[lastKey].push(value);
|
|
30
|
+
} else {
|
|
31
|
+
current[lastKey] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var obj = {};
|
|
36
|
+
setNested(obj, 'user.name', 'Ian');
|
|
37
|
+
setNested(obj, 'user.address.city', 'NYC');
|
|
38
|
+
setNested(obj, 'plan', 'pro');
|
|
39
|
+
|
|
40
|
+
ctx.expect(obj.user.name).toBe('Ian');
|
|
41
|
+
ctx.expect(obj.user.address.city).toBe('NYC');
|
|
42
|
+
ctx.expect(obj.plan).toBe('pro');
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: '_setNested accumulates duplicate keys into arrays',
|
|
47
|
+
run: async (ctx) => {
|
|
48
|
+
function setNested(obj, path, value) {
|
|
49
|
+
var keys = path.split('.');
|
|
50
|
+
var lastKey = keys.pop();
|
|
51
|
+
var current = obj;
|
|
52
|
+
for (var i = 0; i < keys.length; i++) {
|
|
53
|
+
if (!current[keys[i]] || typeof current[keys[i]] !== 'object') {
|
|
54
|
+
current[keys[i]] = {};
|
|
55
|
+
}
|
|
56
|
+
current = current[keys[i]];
|
|
57
|
+
}
|
|
58
|
+
if (current[lastKey] !== undefined) {
|
|
59
|
+
if (!Array.isArray(current[lastKey])) {
|
|
60
|
+
current[lastKey] = [current[lastKey]];
|
|
61
|
+
}
|
|
62
|
+
current[lastKey].push(value);
|
|
63
|
+
} else {
|
|
64
|
+
current[lastKey] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var obj = {};
|
|
69
|
+
setNested(obj, 'tags', 'a');
|
|
70
|
+
setNested(obj, 'tags', 'b');
|
|
71
|
+
setNested(obj, 'tags', 'c');
|
|
72
|
+
|
|
73
|
+
ctx.expect(Array.isArray(obj.tags)).toBe(true);
|
|
74
|
+
ctx.expect(obj.tags.length).toBe(3);
|
|
75
|
+
ctx.expect(obj.tags).toEqual(['a', 'b', 'c']);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// ── _getNested: read from nested object via dot path ────────────────
|
|
80
|
+
{
|
|
81
|
+
name: '_getNested reads nested values and returns undefined for missing paths',
|
|
82
|
+
run: async (ctx) => {
|
|
83
|
+
function getNested(obj, path) {
|
|
84
|
+
return path.split('.').reduce(function (current, key) {
|
|
85
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
86
|
+
}, obj);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var obj = { user: { address: { city: 'NYC' } }, plan: 'pro' };
|
|
90
|
+
|
|
91
|
+
ctx.expect(getNested(obj, 'user.address.city')).toBe('NYC');
|
|
92
|
+
ctx.expect(getNested(obj, 'plan')).toBe('pro');
|
|
93
|
+
ctx.expect(getNested(obj, 'user.phone')).toBe(undefined);
|
|
94
|
+
ctx.expect(getNested(obj, 'nonexistent.deep.path')).toBe(undefined);
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// ── _flattenObject: nested → dot-notation flat ─────────────────────
|
|
99
|
+
{
|
|
100
|
+
name: '_flattenObject converts nested objects to dot paths',
|
|
101
|
+
run: async (ctx) => {
|
|
102
|
+
function flattenObject(obj, prefix) {
|
|
103
|
+
prefix = prefix || '';
|
|
104
|
+
var result = {};
|
|
105
|
+
for (var key in obj) {
|
|
106
|
+
var value = obj[key];
|
|
107
|
+
var path = prefix ? prefix + '.' + key : key;
|
|
108
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
109
|
+
var isCheckboxGroup = Object.values(value).every(function (v) { return typeof v === 'boolean'; });
|
|
110
|
+
if (isCheckboxGroup) {
|
|
111
|
+
result[path] = value;
|
|
112
|
+
} else {
|
|
113
|
+
Object.assign(result, flattenObject(value, path));
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
result[path] = value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
var nested = {
|
|
123
|
+
user: { name: 'Ian', address: { city: 'NYC' } },
|
|
124
|
+
plan: 'pro',
|
|
125
|
+
features: { dark: true, beta: false },
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
var flat = flattenObject(nested);
|
|
129
|
+
|
|
130
|
+
ctx.expect(flat['user.name']).toBe('Ian');
|
|
131
|
+
ctx.expect(flat['user.address.city']).toBe('NYC');
|
|
132
|
+
ctx.expect(flat['plan']).toBe('pro');
|
|
133
|
+
// Boolean-valued objects treated as checkbox groups — kept as objects
|
|
134
|
+
ctx.expect(flat['features'].dark).toBe(true);
|
|
135
|
+
ctx.expect(flat['features'].beta).toBe(false);
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// ── getData: full integration with real form ───────────────────────
|
|
140
|
+
{
|
|
141
|
+
name: 'getData collects text, select, radio, and textarea values with dot notation',
|
|
142
|
+
run: async (ctx) => {
|
|
143
|
+
var form = document.createElement('form');
|
|
144
|
+
form.innerHTML = '<input type="text" name="user.name" value="Ian">'
|
|
145
|
+
+ '<input type="email" name="user.email" value="ian@test.com">'
|
|
146
|
+
+ '<select name="settings.theme"><option value="light">L</option><option value="dark" selected>D</option></select>'
|
|
147
|
+
+ '<textarea name="notes">Hello</textarea>'
|
|
148
|
+
+ '<input type="radio" name="pref" value="a">'
|
|
149
|
+
+ '<input type="radio" name="pref" value="b" checked>';
|
|
150
|
+
document.body.appendChild(form);
|
|
151
|
+
|
|
152
|
+
// Inline getData logic (simplified — no group filter, no honeypot)
|
|
153
|
+
function setNested(obj, path, value) {
|
|
154
|
+
var keys = path.split('.');
|
|
155
|
+
var lastKey = keys.pop();
|
|
156
|
+
var current = obj;
|
|
157
|
+
for (var i = 0; i < keys.length; i++) {
|
|
158
|
+
if (!current[keys[i]] || typeof current[keys[i]] !== 'object') current[keys[i]] = {};
|
|
159
|
+
current = current[keys[i]];
|
|
160
|
+
}
|
|
161
|
+
if (current[lastKey] !== undefined) {
|
|
162
|
+
if (!Array.isArray(current[lastKey])) current[lastKey] = [current[lastKey]];
|
|
163
|
+
current[lastKey].push(value);
|
|
164
|
+
} else {
|
|
165
|
+
current[lastKey] = value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
var data = {};
|
|
170
|
+
form.querySelectorAll('input, select, textarea').forEach(function ($f) {
|
|
171
|
+
if (!$f.name) return;
|
|
172
|
+
if ($f.type === 'checkbox') return;
|
|
173
|
+
if ($f.type === 'radio' && !$f.checked) return;
|
|
174
|
+
setNested(data, $f.name, $f.value);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
ctx.expect(data.user.name).toBe('Ian');
|
|
178
|
+
ctx.expect(data.user.email).toBe('ian@test.com');
|
|
179
|
+
ctx.expect(data.settings.theme).toBe('dark');
|
|
180
|
+
ctx.expect(data.notes).toBe('Hello');
|
|
181
|
+
ctx.expect(data.pref).toBe('b');
|
|
182
|
+
|
|
183
|
+
form.remove();
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'getData handles single checkbox (boolean) and checkbox groups (object)',
|
|
188
|
+
run: async (ctx) => {
|
|
189
|
+
var form = document.createElement('form');
|
|
190
|
+
form.innerHTML = '<input type="checkbox" name="subscribe" checked>'
|
|
191
|
+
+ '<input type="checkbox" name="features" value="dark" checked>'
|
|
192
|
+
+ '<input type="checkbox" name="features" value="beta">'
|
|
193
|
+
+ '<input type="checkbox" name="features" value="analytics" checked>';
|
|
194
|
+
document.body.appendChild(form);
|
|
195
|
+
|
|
196
|
+
var data = {};
|
|
197
|
+
var fields = form.querySelectorAll('input');
|
|
198
|
+
var checkboxCounts = {};
|
|
199
|
+
fields.forEach(function ($f) {
|
|
200
|
+
if ($f.type === 'checkbox') checkboxCounts[$f.name] = (checkboxCounts[$f.name] || 0) + 1;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Single checkboxes
|
|
204
|
+
fields.forEach(function ($f) {
|
|
205
|
+
if ($f.type !== 'checkbox') return;
|
|
206
|
+
if (checkboxCounts[$f.name] === 1) {
|
|
207
|
+
data[$f.name] = $f.checked;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Checkbox groups
|
|
212
|
+
var processed = {};
|
|
213
|
+
fields.forEach(function ($f) {
|
|
214
|
+
if ($f.type !== 'checkbox') return;
|
|
215
|
+
if (checkboxCounts[$f.name] === 1) return;
|
|
216
|
+
if (processed[$f.name]) return;
|
|
217
|
+
processed[$f.name] = true;
|
|
218
|
+
var values = {};
|
|
219
|
+
form.querySelectorAll('input[type="checkbox"][name="' + $f.name + '"]').forEach(function ($cb) {
|
|
220
|
+
values[$cb.value] = $cb.checked;
|
|
221
|
+
});
|
|
222
|
+
data[$f.name] = values;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Single checkbox → boolean
|
|
226
|
+
ctx.expect(data.subscribe).toBe(true);
|
|
227
|
+
|
|
228
|
+
// Checkbox group → object with value: boolean
|
|
229
|
+
ctx.expect(data.features.dark).toBe(true);
|
|
230
|
+
ctx.expect(data.features.beta).toBe(false);
|
|
231
|
+
ctx.expect(data.features.analytics).toBe(true);
|
|
232
|
+
|
|
233
|
+
form.remove();
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'getData excludes honeypot fields',
|
|
238
|
+
run: async (ctx) => {
|
|
239
|
+
var form = document.createElement('form');
|
|
240
|
+
form.innerHTML = '<input type="text" name="email" value="real@test.com">'
|
|
241
|
+
+ '<input type="text" name="honey" data-honey value="bot-filled">'
|
|
242
|
+
+ '<input type="text" name="trap" value="legit">';
|
|
243
|
+
document.body.appendChild(form);
|
|
244
|
+
|
|
245
|
+
var HONEYPOT_SELECTOR = '[data-honey], [name="honey"]';
|
|
246
|
+
var data = {};
|
|
247
|
+
form.querySelectorAll('input').forEach(function ($f) {
|
|
248
|
+
if (!$f.name) return;
|
|
249
|
+
if ($f.matches(HONEYPOT_SELECTOR)) return;
|
|
250
|
+
data[$f.name] = $f.value;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
ctx.expect(data.email).toBe('real@test.com');
|
|
254
|
+
ctx.expect(data.honey).toBe(undefined);
|
|
255
|
+
ctx.expect(data.trap).toBe('legit');
|
|
256
|
+
|
|
257
|
+
form.remove();
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// ── Input group filtering ──────────────────────────────────────────
|
|
262
|
+
{
|
|
263
|
+
name: 'input group filter includes matching + global fields, excludes others',
|
|
264
|
+
run: async (ctx) => {
|
|
265
|
+
var form = document.createElement('form');
|
|
266
|
+
form.innerHTML = '<input type="text" name="global_name" value="always">'
|
|
267
|
+
+ '<input type="text" name="group_a_field" data-input-group="a" value="from-a">'
|
|
268
|
+
+ '<input type="text" name="group_b_field" data-input-group="b" value="from-b">';
|
|
269
|
+
document.body.appendChild(form);
|
|
270
|
+
|
|
271
|
+
var allowedGroups = ['a'];
|
|
272
|
+
|
|
273
|
+
function isFieldInGroup($field) {
|
|
274
|
+
if (!allowedGroups) return true;
|
|
275
|
+
var fieldGroup = $field.getAttribute('data-input-group');
|
|
276
|
+
if (!fieldGroup || fieldGroup.trim() === '') return true;
|
|
277
|
+
return allowedGroups.indexOf(fieldGroup.toLowerCase()) !== -1;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
var data = {};
|
|
281
|
+
form.querySelectorAll('input').forEach(function ($f) {
|
|
282
|
+
if (!$f.name || !isFieldInGroup($f)) return;
|
|
283
|
+
data[$f.name] = $f.value;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
ctx.expect(data.global_name).toBe('always');
|
|
287
|
+
ctx.expect(data.group_a_field).toBe('from-a');
|
|
288
|
+
ctx.expect(data.group_b_field).toBe(undefined);
|
|
289
|
+
|
|
290
|
+
form.remove();
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'input group filter with multiple groups includes all matching',
|
|
295
|
+
run: async (ctx) => {
|
|
296
|
+
var form = document.createElement('form');
|
|
297
|
+
form.innerHTML = '<input type="text" name="a_field" data-input-group="a" value="A">'
|
|
298
|
+
+ '<input type="text" name="b_field" data-input-group="b" value="B">'
|
|
299
|
+
+ '<input type="text" name="c_field" data-input-group="c" value="C">';
|
|
300
|
+
document.body.appendChild(form);
|
|
301
|
+
|
|
302
|
+
var allowedGroups = ['a', 'b'];
|
|
303
|
+
|
|
304
|
+
function isFieldInGroup($field) {
|
|
305
|
+
if (!allowedGroups) return true;
|
|
306
|
+
var fieldGroup = $field.getAttribute('data-input-group');
|
|
307
|
+
if (!fieldGroup || fieldGroup.trim() === '') return true;
|
|
308
|
+
return allowedGroups.indexOf(fieldGroup.toLowerCase()) !== -1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
var data = {};
|
|
312
|
+
form.querySelectorAll('input').forEach(function ($f) {
|
|
313
|
+
if (!$f.name || !isFieldInGroup($f)) return;
|
|
314
|
+
data[$f.name] = $f.value;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
ctx.expect(data.a_field).toBe('A');
|
|
318
|
+
ctx.expect(data.b_field).toBe('B');
|
|
319
|
+
ctx.expect(data.c_field).toBe(undefined);
|
|
320
|
+
|
|
321
|
+
form.remove();
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
// ── setData: populate form from object ─────────────────────────────
|
|
326
|
+
{
|
|
327
|
+
name: 'setData populates text, select, textarea fields',
|
|
328
|
+
run: async (ctx) => {
|
|
329
|
+
var form = document.createElement('form');
|
|
330
|
+
form.innerHTML = '<input type="text" name="user.name">'
|
|
331
|
+
+ '<input type="email" name="user.email">'
|
|
332
|
+
+ '<select name="theme"><option value="light">L</option><option value="dark">D</option></select>'
|
|
333
|
+
+ '<textarea name="notes"></textarea>';
|
|
334
|
+
document.body.appendChild(form);
|
|
335
|
+
|
|
336
|
+
function setFieldValue(name, value) {
|
|
337
|
+
var fields = form.querySelectorAll('[name="' + name + '"]');
|
|
338
|
+
if (fields.length === 0) return;
|
|
339
|
+
fields[0].value = value;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function flattenObject(obj, prefix) {
|
|
343
|
+
prefix = prefix || '';
|
|
344
|
+
var result = {};
|
|
345
|
+
for (var key in obj) {
|
|
346
|
+
var value = obj[key];
|
|
347
|
+
var path = prefix ? prefix + '.' + key : key;
|
|
348
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
349
|
+
var isCbGroup = Object.values(value).every(function (v) { return typeof v === 'boolean'; });
|
|
350
|
+
if (isCbGroup) { result[path] = value; } else { Object.assign(result, flattenObject(value, path)); }
|
|
351
|
+
} else {
|
|
352
|
+
result[path] = value;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
var flat = flattenObject({ user: { name: 'John', email: 'j@test.com' }, theme: 'dark', notes: 'Hi' });
|
|
359
|
+
for (var path in flat) {
|
|
360
|
+
setFieldValue(path, flat[path]);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
ctx.expect(form.querySelector('[name="user.name"]').value).toBe('John');
|
|
364
|
+
ctx.expect(form.querySelector('[name="user.email"]').value).toBe('j@test.com');
|
|
365
|
+
ctx.expect(form.querySelector('[name="theme"]').value).toBe('dark');
|
|
366
|
+
ctx.expect(form.querySelector('[name="notes"]').value).toBe('Hi');
|
|
367
|
+
|
|
368
|
+
form.remove();
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'setData sets radio group to matching value',
|
|
373
|
+
run: async (ctx) => {
|
|
374
|
+
var form = document.createElement('form');
|
|
375
|
+
form.innerHTML = '<input type="radio" name="plan" value="free" checked>'
|
|
376
|
+
+ '<input type="radio" name="plan" value="pro">'
|
|
377
|
+
+ '<input type="radio" name="plan" value="enterprise">';
|
|
378
|
+
document.body.appendChild(form);
|
|
379
|
+
|
|
380
|
+
// Set radio group
|
|
381
|
+
form.querySelectorAll('[name="plan"]').forEach(function ($r) {
|
|
382
|
+
$r.checked = ($r.value === 'pro');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
ctx.expect(form.querySelector('[value="free"]').checked).toBe(false);
|
|
386
|
+
ctx.expect(form.querySelector('[value="pro"]').checked).toBe(true);
|
|
387
|
+
ctx.expect(form.querySelector('[value="enterprise"]').checked).toBe(false);
|
|
388
|
+
|
|
389
|
+
form.remove();
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: 'setData sets single checkbox boolean and checkbox group values',
|
|
394
|
+
run: async (ctx) => {
|
|
395
|
+
var form = document.createElement('form');
|
|
396
|
+
form.innerHTML = '<input type="checkbox" name="subscribe">'
|
|
397
|
+
+ '<input type="checkbox" name="features" value="dark">'
|
|
398
|
+
+ '<input type="checkbox" name="features" value="beta">';
|
|
399
|
+
document.body.appendChild(form);
|
|
400
|
+
|
|
401
|
+
// Single checkbox
|
|
402
|
+
form.querySelector('[name="subscribe"]').checked = true;
|
|
403
|
+
|
|
404
|
+
// Checkbox group
|
|
405
|
+
var groupValues = { dark: true, beta: false };
|
|
406
|
+
form.querySelectorAll('[name="features"]').forEach(function ($cb) {
|
|
407
|
+
$cb.checked = !!groupValues[$cb.value];
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
ctx.expect(form.querySelector('[name="subscribe"]').checked).toBe(true);
|
|
411
|
+
ctx.expect(form.querySelector('[value="dark"]').checked).toBe(true);
|
|
412
|
+
ctx.expect(form.querySelector('[value="beta"]').checked).toBe(false);
|
|
413
|
+
|
|
414
|
+
form.remove();
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// FormManager disabled-state snapshot: elements disabled in HTML markup stay
|
|
2
|
+
// disabled through every FM state transition. Submit buttons (loading guards)
|
|
3
|
+
// are always FM-managed regardless of initial HTML state.
|
|
4
|
+
//
|
|
5
|
+
// Can't import FormManager (ESM + web-manager dep), so we inline the snapshot
|
|
6
|
+
// + _setDisabled logic and test against a real DOM form.
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
layer: 'page',
|
|
10
|
+
description: 'FormManager disabled-state snapshot',
|
|
11
|
+
type: 'group',
|
|
12
|
+
tests: [
|
|
13
|
+
{
|
|
14
|
+
name: 'snapshot captures disabled non-submit elements, ignores submit buttons',
|
|
15
|
+
run: async (ctx) => {
|
|
16
|
+
var form = document.createElement('form');
|
|
17
|
+
form.innerHTML = '<input type="text" name="email">'
|
|
18
|
+
+ '<input type="radio" name="plan" value="free">'
|
|
19
|
+
+ '<input type="radio" name="plan" value="enterprise" disabled>'
|
|
20
|
+
+ '<select name="region" disabled><option>US</option></select>'
|
|
21
|
+
+ '<textarea name="notes"></textarea>'
|
|
22
|
+
+ '<button type="submit" disabled>Submit</button>'
|
|
23
|
+
+ '<button type="button">Cancel</button>';
|
|
24
|
+
document.body.appendChild(form);
|
|
25
|
+
|
|
26
|
+
var permanently = new Set();
|
|
27
|
+
form.querySelectorAll('button, input, select, textarea').forEach(function ($el) {
|
|
28
|
+
if ($el.disabled && $el.type !== 'submit') {
|
|
29
|
+
permanently.add($el);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
ctx.expect(permanently.size).toBe(2);
|
|
34
|
+
ctx.expect(permanently.has(form.querySelector('[value="enterprise"]'))).toBe(true);
|
|
35
|
+
ctx.expect(permanently.has(form.querySelector('[name="region"]'))).toBe(true);
|
|
36
|
+
ctx.expect(permanently.has(form.querySelector('[type="submit"]'))).toBe(false);
|
|
37
|
+
|
|
38
|
+
form.remove();
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'setDisabled(true) disables everything',
|
|
43
|
+
run: async (ctx) => {
|
|
44
|
+
var form = document.createElement('form');
|
|
45
|
+
form.innerHTML = '<input type="text" name="email">'
|
|
46
|
+
+ '<input type="radio" name="plan" value="enterprise" disabled>'
|
|
47
|
+
+ '<button type="submit" disabled>Submit</button>';
|
|
48
|
+
document.body.appendChild(form);
|
|
49
|
+
|
|
50
|
+
var permanently = new Set();
|
|
51
|
+
form.querySelectorAll('button, input, select, textarea').forEach(function ($el) {
|
|
52
|
+
if ($el.disabled && $el.type !== 'submit') {
|
|
53
|
+
permanently.add($el);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
form.querySelectorAll('button, input, select, textarea').forEach(function ($el) {
|
|
58
|
+
if (permanently.has($el)) { $el.disabled = true; return; }
|
|
59
|
+
$el.disabled = true;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
form.querySelectorAll('button, input, select, textarea').forEach(function ($el) {
|
|
63
|
+
ctx.expect($el.disabled).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
form.remove();
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'setDisabled(false) re-enables managed elements but keeps snapshotted ones disabled',
|
|
71
|
+
run: async (ctx) => {
|
|
72
|
+
var form = document.createElement('form');
|
|
73
|
+
form.innerHTML = '<input type="text" name="email">'
|
|
74
|
+
+ '<input type="radio" name="plan" value="free">'
|
|
75
|
+
+ '<input type="radio" name="plan" value="pro">'
|
|
76
|
+
+ '<input type="radio" name="plan" value="enterprise" disabled>'
|
|
77
|
+
+ '<select name="region" disabled><option>US</option></select>'
|
|
78
|
+
+ '<textarea name="notes"></textarea>'
|
|
79
|
+
+ '<button type="submit" disabled>Submit</button>'
|
|
80
|
+
+ '<button type="button">Cancel</button>';
|
|
81
|
+
document.body.appendChild(form);
|
|
82
|
+
|
|
83
|
+
var permanently = new Set();
|
|
84
|
+
form.querySelectorAll('button, input, select, textarea').forEach(function ($el) {
|
|
85
|
+
if ($el.disabled && $el.type !== 'submit') {
|
|
86
|
+
permanently.add($el);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function setDisabled(disabled) {
|
|
91
|
+
form.querySelectorAll('button, input, select, textarea').forEach(function ($el) {
|
|
92
|
+
if (permanently.has($el)) { $el.disabled = true; return; }
|
|
93
|
+
$el.disabled = disabled;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setDisabled(true);
|
|
98
|
+
setDisabled(false);
|
|
99
|
+
|
|
100
|
+
ctx.expect(form.querySelector('[name="email"]').disabled).toBe(false);
|
|
101
|
+
ctx.expect(form.querySelector('[value="free"]').disabled).toBe(false);
|
|
102
|
+
ctx.expect(form.querySelector('[value="pro"]').disabled).toBe(false);
|
|
103
|
+
ctx.expect(form.querySelector('[type="submit"]').disabled).toBe(false);
|
|
104
|
+
ctx.expect(form.querySelector('[type="button"]').disabled).toBe(false);
|
|
105
|
+
ctx.expect(form.querySelector('textarea').disabled).toBe(false);
|
|
106
|
+
|
|
107
|
+
ctx.expect(form.querySelector('[value="enterprise"]').disabled).toBe(true);
|
|
108
|
+
ctx.expect(form.querySelector('[name="region"]').disabled).toBe(true);
|
|
109
|
+
|
|
110
|
+
form.remove();
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'survives multiple disable/enable cycles',
|
|
115
|
+
run: async (ctx) => {
|
|
116
|
+
var form = document.createElement('form');
|
|
117
|
+
form.innerHTML = '<input type="text" name="email">'
|
|
118
|
+
+ '<input type="radio" name="plan" value="enterprise" disabled>'
|
|
119
|
+
+ '<select name="region" disabled><option>US</option></select>'
|
|
120
|
+
+ '<button type="submit" disabled>Submit</button>';
|
|
121
|
+
document.body.appendChild(form);
|
|
122
|
+
|
|
123
|
+
var permanently = new Set();
|
|
124
|
+
form.querySelectorAll('button, input, select, textarea').forEach(function ($el) {
|
|
125
|
+
if ($el.disabled && $el.type !== 'submit') {
|
|
126
|
+
permanently.add($el);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function setDisabled(disabled) {
|
|
131
|
+
form.querySelectorAll('button, input, select, textarea').forEach(function ($el) {
|
|
132
|
+
if (permanently.has($el)) { $el.disabled = true; return; }
|
|
133
|
+
$el.disabled = disabled;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (var i = 0; i < 5; i++) {
|
|
138
|
+
setDisabled(true);
|
|
139
|
+
setDisabled(false);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ctx.expect(form.querySelector('[name="email"]').disabled).toBe(false);
|
|
143
|
+
ctx.expect(form.querySelector('[type="submit"]').disabled).toBe(false);
|
|
144
|
+
ctx.expect(form.querySelector('[value="enterprise"]').disabled).toBe(true);
|
|
145
|
+
ctx.expect(form.querySelector('[name="region"]').disabled).toBe(true);
|
|
146
|
+
|
|
147
|
+
form.remove();
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'onsubmit="return false" blocks native submission before FM loads',
|
|
152
|
+
run: async (ctx) => {
|
|
153
|
+
var form = document.createElement('form');
|
|
154
|
+
form.setAttribute('onsubmit', 'return false');
|
|
155
|
+
form.innerHTML = '<input type="text" name="x"><button type="submit">Go</button>';
|
|
156
|
+
document.body.appendChild(form);
|
|
157
|
+
|
|
158
|
+
var navigated = false;
|
|
159
|
+
form.addEventListener('submit', function (e) {
|
|
160
|
+
navigated = !e.defaultPrevented;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
form.querySelector('button').click();
|
|
164
|
+
ctx.expect(navigated).toBe(false);
|
|
165
|
+
|
|
166
|
+
form.remove();
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|