@bongos/core 1.20.49 → 1.20.50
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/.bongos-core.json +55 -35
- package/.claude/skills/new-project/SKILL.md +5 -0
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +3 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +314 -294
- package/docs/copy-registry.json +560 -378
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +5 -1
- package/docs/page-readings.json +538 -521
- package/modules/platform-identity/routes/sso.js +5 -0
- package/modules/provisioning/migrations/provisioning_033_terms_ack.sql +33 -0
- package/modules/provisioning/onboard-plan.js +5 -0
- package/modules/provisioning/provisioning.js +7 -4
- package/modules/provisioning/routes/provisioning.js +3 -3
- package/modules/provisioning/terms-ack.js +36 -0
- package/modules/public-landing/public/projects.html +226 -77
- package/modules/public-landing/public/projects.probes.json +8 -2
- package/modules/public-landing/public/projects.states.json +16 -12
- package/modules/ui-design/kit/serve.js +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +4 -2
- package/tests/projects_hub_app_status.mjs +1 -1
- package/tests/projects_hub_app_step.mjs +9 -9
- package/tests/projects_hub_module_picker.mjs +7 -7
- package/tests/projects_hub_pre_uat.mjs +4 -1
- package/tests/provisioning_terms_ack.mjs +216 -0
- package/tests/wizard_draft_resume.mjs +4 -4
- package/tests/wizard_front_door.mjs +271 -0
- package/tests/wizard_intent_resume.mjs +31 -19
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// tests/wizard_front_door.mjs — the create wizard's front door (task 1004417, BV2.PS05;
|
|
2
|
+
// goal 1000121, docs/design/project-startup-direction.md "Front door").
|
|
3
|
+
//
|
|
4
|
+
// Two screens now stand in front of the wizard's numbered steps:
|
|
5
|
+
// 1 · terms — "anything you want to make": one tick, recorded on the project against
|
|
6
|
+
// the terms version the SERVER stamps (tests/provisioning_terms_ack.mjs holds that half);
|
|
7
|
+
// 2 · fork — "you are now creating a digital planet": the world's door pill, two equal
|
|
8
|
+
// doors, try the demo / begin planet creation. The demo is its own task (BV2.PS09), so
|
|
9
|
+
// its door says the demo is not open yet rather than leading anywhere.
|
|
10
|
+
// Sign-in stays where it was: the gate (step 0) comes first.
|
|
11
|
+
//
|
|
12
|
+
// Most of this RUNS the page's own script, the vm-slice idiom of tests/wizard_draft_resume.mjs:
|
|
13
|
+
// validateStep(), the front door's wiring, and show()'s class toggles are sliced out of the
|
|
14
|
+
// inline script and executed under a minimal DOM stub. Every slice boundary is asserted, so a
|
|
15
|
+
// rename fails loudly instead of leaving this file quietly testing nothing.
|
|
16
|
+
//
|
|
17
|
+
// projects.html is CRLF on a Windows checkout; this file normalises at read time.
|
|
18
|
+
//
|
|
19
|
+
// Run: node --test tests/wizard_front_door.mjs
|
|
20
|
+
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import { test } from 'node:test';
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import vm from 'node:vm';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
30
|
+
const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
31
|
+
const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>'));
|
|
32
|
+
const STYLE = HUB.slice(HUB.indexOf('<style>'), HUB.indexOf('</style>'));
|
|
33
|
+
|
|
34
|
+
const panel = (id) => (HUB.match(new RegExp(`<section class="panel[^"]*" id="${id}"[\\s\\S]*?\\n {10}</section>`)) || [''])[0];
|
|
35
|
+
const TERMS = panel('panel-terms');
|
|
36
|
+
const FORK = panel('panel-fork');
|
|
37
|
+
const MAP = {};
|
|
38
|
+
for (const m of ((SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1]).matchAll(/(\d+):\s*'([a-z-]+)'/g)) MAP[m[2]] = Number(m[1]);
|
|
39
|
+
|
|
40
|
+
// ── the order ───────────────────────────────────────────────────────────────────
|
|
41
|
+
test('the front door stands between sign-in and the name step: gate 0, terms 1, fork 2, name 3', () => {
|
|
42
|
+
assert.equal(MAP['panel-gate'], 0, 'sign-in stays where it is today, first');
|
|
43
|
+
assert.equal(MAP['panel-terms'], 1);
|
|
44
|
+
assert.equal(MAP['panel-fork'], 2);
|
|
45
|
+
assert.equal(MAP['panel-name'], 3, 'and today\'s wizard continues unchanged after it');
|
|
46
|
+
assert.ok(HUB.indexOf('id="panel-gate"') < HUB.indexOf('id="panel-terms"')
|
|
47
|
+
&& HUB.indexOf('id="panel-terms"') < HUB.indexOf('id="panel-fork"')
|
|
48
|
+
&& HUB.indexOf('id="panel-fork"') < HUB.indexOf('id="panel-name"'), 'the markup reads in the same order');
|
|
49
|
+
assert.match(TERMS, /<form data-next="2">/, 'the terms go on to the fork');
|
|
50
|
+
assert.match(FORK, /<form data-next="3">/, 'the fork\'s primary door goes on to the name step');
|
|
51
|
+
assert.match(FORK, /data-back="1"/, 'and the way back is the terms');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the rail is the numbered steps\' — it steps aside on both front-door screens, statically too', () => {
|
|
55
|
+
const beats = [...HUB.matchAll(/<li[^>]*data-step="(\d+)"/g)].map((m) => Number(m[1]));
|
|
56
|
+
assert.deepEqual(beats, [3, 4, 5, 6, 7, 8, 9, 10], 'the rail numbers name…create, never the front door');
|
|
57
|
+
assert.match(HUB, /<div class="card wizFront">/, 'a no-script visitor on the static first screen sees no rail');
|
|
58
|
+
assert.match(STYLE, /#view-new \.card\.wizFront \.steps\{display:none;\}/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── screen 1 · the terms ────────────────────────────────────────────────────────
|
|
62
|
+
test('screen 1 is the canvas\'s "anything you want to make": two ledes, one tick, a read-the-terms link, one door', () => {
|
|
63
|
+
assert.ok(TERMS, '#panel-terms ships as real markup (C7)');
|
|
64
|
+
assert.match(TERMS, /^<section class="panel on" id="panel-terms"/, 'it carries the static `on` — step 1 for a no-script visitor');
|
|
65
|
+
assert.match(TERMS, /<h1 id="terms-h">Anything you want to make<\/h1>/);
|
|
66
|
+
assert.equal((TERMS.match(/<p class="lede">/g) || []).length, 2, 'two lede paragraphs');
|
|
67
|
+
assert.match(TERMS, /<input type="checkbox" id="termsAck" aria-describedby="h-terms">/, 'one checkbox, named by its hint');
|
|
68
|
+
assert.match(TERMS, /<label class="termsAck">\s*<input[^>]*>\s*<span>I can sign legally, or can get a signature, for the use of this platform, and I acknowledge the terms and conditions\.<\/span>/,
|
|
69
|
+
'the whole row is its label, so the target is the sentence, not a 20px box');
|
|
70
|
+
assert.match(TERMS, /<a class="termsRead" href="\/terms" target="_blank" rel="noopener">Read the terms<\/a>/,
|
|
71
|
+
'the platform\'s own /terms, in a new tab so the draft is not left behind');
|
|
72
|
+
assert.equal((TERMS.match(/type="submit"/g) || []).length, 1, 'one primary door');
|
|
73
|
+
assert.match(TERMS, /<span data-brand-product-inline>Cloud Bongos<\/span>/, 'the product name is the pack\'s, re-branded on a child');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('the page never names a terms version — the server stamps which text was acknowledged', () => {
|
|
77
|
+
// A version baked here would be a second copy of TERMS_VERSION that drifts, and worse, a
|
|
78
|
+
// value a caller could send. The create request carries only the tick.
|
|
79
|
+
assert.doesNotMatch(HUB, /terms_version|termsVersion|terms_ack_version/, 'no version travels from the page');
|
|
80
|
+
assert.doesNotMatch(TERMS, /\b20\d\d-\d\d-\d\d\b/, 'and none is printed on the screen');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('the resume offer moved with step 1 — it lives on the terms screen, once', () => {
|
|
84
|
+
assert.equal((HUB.match(/id="wizResume"/g) || []).length, 1);
|
|
85
|
+
assert.ok(TERMS.includes('id="wizResume"'), 'a deliberate "New project" lands here, so the offer does too');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ── the validator, executed ────────────────────────────────────────────────────
|
|
89
|
+
const VALIDATE_RE = /function validateStep\(step\) \{[\s\S]*?\n {4}\}/;
|
|
90
|
+
const validateSlice = (SCRIPT.match(VALIDATE_RE) || [null])[0];
|
|
91
|
+
|
|
92
|
+
function runValidate(step, termsAck) {
|
|
93
|
+
const els = {};
|
|
94
|
+
const $ = (id) => (els[id] = els[id] || { textContent: '', className: '', classList: { add() {} }, focus() {} });
|
|
95
|
+
const sandbox = { $, state: { termsAck, name: 'Mercury', projType: 'game' }, validateRender: () => true, repoAccess: true };
|
|
96
|
+
sandbox.globalThis = sandbox;
|
|
97
|
+
vm.createContext(sandbox);
|
|
98
|
+
vm.runInContext(`${validateSlice}\nvar ok = validateStep(${step});`, sandbox);
|
|
99
|
+
return { ok: sandbox.ok, hint: els['h-terms'] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
test('validateStep is still findable, with the terms clause inside it', () => {
|
|
103
|
+
assert.ok(validateSlice, 'validateStep() was not found in projects.html');
|
|
104
|
+
assert.match(validateSlice, /if \(step === 1\) \{\s*if \(!state\.termsAck\)/, 'the clause is part of the slice');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('Proceed is refused until the box is ticked, and the refusal says why in the hint', () => {
|
|
108
|
+
const refused = runValidate(1, false);
|
|
109
|
+
assert.equal(refused.ok, false, 'no tick, no fork');
|
|
110
|
+
assert.equal(refused.hint.className, 'hint err');
|
|
111
|
+
assert.match(refused.hint.textContent, /Tick the box to go on/);
|
|
112
|
+
const given = runValidate(1, true);
|
|
113
|
+
assert.equal(given.ok, true, 'a tick goes on');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('the fork has no clause of its own — its doors are the answer', () => {
|
|
117
|
+
assert.equal(runValidate(2, false).ok, true, 'the fork asks nothing a validator could refuse');
|
|
118
|
+
assert.ok(!validateSlice.includes('if (step === 2) {'), 'and no clause for step 2 exists');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── the wiring, executed ───────────────────────────────────────────────────────
|
|
122
|
+
const WIRE_START = " $('termsAck').checked = !!state.termsAck;";
|
|
123
|
+
const WIRE_END = ' /* ── field wiring ── */';
|
|
124
|
+
const wireSlice = SCRIPT.includes(WIRE_START) ? SCRIPT.slice(SCRIPT.indexOf(WIRE_START), SCRIPT.indexOf(WIRE_END)) : null;
|
|
125
|
+
|
|
126
|
+
function runWiring(initialAck) {
|
|
127
|
+
const handlers = {};
|
|
128
|
+
const mk = (id) => ({
|
|
129
|
+
id, checked: false, hidden: id === 'demoSoon', textContent: '', className: '', attrs: {},
|
|
130
|
+
addEventListener(type, fn) { handlers[id + ':' + type] = fn; },
|
|
131
|
+
setAttribute(k, v) { this.attrs[k] = v; },
|
|
132
|
+
});
|
|
133
|
+
const els = { termsAck: mk('termsAck'), wizDemo: mk('wizDemo'), demoSoon: mk('demoSoon'), 'h-terms': mk('h-terms') };
|
|
134
|
+
let saves = 0;
|
|
135
|
+
const sandbox = { $: (id) => els[id], state: { termsAck: initialAck }, saveDraft: () => { saves += 1; } };
|
|
136
|
+
sandbox.globalThis = sandbox;
|
|
137
|
+
vm.createContext(sandbox);
|
|
138
|
+
vm.runInContext(wireSlice, sandbox);
|
|
139
|
+
return { els, handlers, sandbox, saves: () => saves };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
test('the front door\'s wiring is still findable', () => {
|
|
143
|
+
assert.ok(wireSlice && wireSlice.length > 100, 'the wiring block was not found in projects.html');
|
|
144
|
+
assert.match(wireSlice, /\$\('wizDemo'\)\.addEventListener\('click'/, 'the demo door is part of the slice');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('the tick mirrors into the draft both ways, and a resumed draft paints its own answer', () => {
|
|
148
|
+
const { els, handlers, sandbox, saves } = runWiring(true);
|
|
149
|
+
assert.equal(els.termsAck.checked, true, 'a draft that ticked comes back ticked (the GitHub round-trip keeps it)');
|
|
150
|
+
els.termsAck.checked = false; handlers['termsAck:change'].call(els.termsAck);
|
|
151
|
+
assert.equal(sandbox.state.termsAck, false, 'unticking is an answer too');
|
|
152
|
+
els['h-terms'].className = 'hint err';
|
|
153
|
+
els.termsAck.checked = true; handlers['termsAck:change'].call(els.termsAck);
|
|
154
|
+
assert.equal(sandbox.state.termsAck, true);
|
|
155
|
+
assert.equal(els['h-terms'].className, 'hint', 'ticking clears the refusal');
|
|
156
|
+
assert.equal(saves(), 2, 'each change is saved');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('the demo door opens a line saying the demo is not open yet, and says so to a screen reader', () => {
|
|
160
|
+
const { els, handlers } = runWiring(false);
|
|
161
|
+
assert.equal(els.demoSoon.hidden, true, 'hidden at rest');
|
|
162
|
+
handlers['wizDemo:click'].call(els.wizDemo);
|
|
163
|
+
assert.equal(els.demoSoon.hidden, false, 'the placeholder opens');
|
|
164
|
+
assert.equal(els.wizDemo.attrs['aria-expanded'], 'true');
|
|
165
|
+
handlers['wizDemo:click'].call(els.wizDemo);
|
|
166
|
+
assert.equal(els.demoSoon.hidden, true, 'and closes again');
|
|
167
|
+
assert.equal(els.wizDemo.attrs['aria-expanded'], 'false');
|
|
168
|
+
assert.match(FORK, /id="wizDemo" aria-controls="demoSoon" aria-expanded="false"/, 'the door names what it opens');
|
|
169
|
+
assert.match(FORK, /<p class="demoSoon" id="demoSoon" hidden>The demo isn’t open yet\./, 'baked markup, never built by script');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ── screen 2 · the fork ─────────────────────────────────────────────────────────
|
|
173
|
+
test('screen 2 is the door pill: two equal doors in one object, the accent only on the one that goes on', () => {
|
|
174
|
+
assert.match(FORK, /<h1 id="fork-h">You are now creating a digital planet<\/h1>/);
|
|
175
|
+
const pill = (FORK.match(/<div class="doorPill" role="group" aria-label="How to begin">[\s\S]*?<\/div>/) || [''])[0];
|
|
176
|
+
assert.ok(pill, 'one grouped pill');
|
|
177
|
+
const doors = [...pill.matchAll(/<button type="(button|submit)" class="([^"]+)"[^>]*>([^<]+)<\/button>/g)];
|
|
178
|
+
assert.equal(doors.length, 2, 'exactly two doors');
|
|
179
|
+
assert.deepEqual(doors.map((d) => d[3]), ['Try the demo', 'Begin planet creation']);
|
|
180
|
+
assert.equal(doors[0][2], 'btn big ghost', 'the demo door is the secondary: transparent at rest');
|
|
181
|
+
assert.equal(doors[1][2], 'btn big', 'begin carries the accent');
|
|
182
|
+
assert.equal(doors[1][1], 'submit', 'and is the form\'s one submit, so Enter goes on');
|
|
183
|
+
assert.match(STYLE, /\.doorPill\{display:inline-grid;grid-template-columns:1fr 1fr;/, 'two EQUAL tracks — the two routes are equal ways in');
|
|
184
|
+
assert.match(STYLE, /\.doorPill\{[^}]*border-radius:999px;/, 'pill or nothing');
|
|
185
|
+
assert.match(STYLE, /@media \(max-width:520px\)\{\s*\.doorPill\{display:grid;grid-template-columns:1fr;/, 'at a phone width the two stack, still one object');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('the seed stands on the world\'s own field plate — nothing drawn but hairlines', () => {
|
|
189
|
+
assert.match(FORK, /<div class="forkStage" aria-hidden="true">/, 'decorative, so hidden from a screen reader');
|
|
190
|
+
assert.match(FORK, /<img class="forkSeed" src="\/assets\/world\/r2-chrome-blob-one-cut\.webp" alt="">/, 'a real plate, root-absolute so it is stamped');
|
|
191
|
+
assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', 'r2-chrome-blob-one-cut.webp')), 'and it exists on disk');
|
|
192
|
+
assert.match(STYLE, /\.forkStage\{[^}]*background:var\(--sky-bg\) var\(--field-plate\) center\/cover no-repeat;/, 'the field is the plate token, not a new image');
|
|
193
|
+
assert.match(STYLE, /\.forkOrbit\{[^}]*border:1px solid var\(--rule\);border-radius:50%;/, 'the orbits are hairline circles');
|
|
194
|
+
const at = STYLE.indexOf('the front door (task 1004417');
|
|
195
|
+
const block = STYLE.slice(at, STYLE.indexOf('/* a field:', at));
|
|
196
|
+
assert.ok(at > -1 && block.includes('.forkStage{'), 'the front door\'s CSS block is findable');
|
|
197
|
+
assert.doesNotMatch(block, /#[0-9a-f]{3,8}\b|box-shadow:(?!none)|linear-gradient/i, 'every colour a token, no shadow, no gradient');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('the two routes are described honestly for today: the demo says coming soon, creation promises no questions that do not exist yet', () => {
|
|
201
|
+
const routes = (FORK.match(/<div class="forkRoutes">[\s\S]*$/) || [''])[0];
|
|
202
|
+
assert.match(routes, /Route 1 · demo · coming soon/);
|
|
203
|
+
assert.match(routes, /Route 2 · planet creation/);
|
|
204
|
+
assert.doesNotMatch(routes, /seven questions/i, 'the seven planet questions are PS07\'s — the copy must not promise them before they ship');
|
|
205
|
+
assert.doesNotMatch(FORK, /\bBongos\b(?! pair)/, 'no product name baked where a child instance would ship it');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ── show(): the class toggles, executed ────────────────────────────────────────
|
|
209
|
+
const SHOW_RE = /function show\(step, moveFocus\) \{[\s\S]*?\n {4}\}/;
|
|
210
|
+
const showSlice = (SCRIPT.match(SHOW_RE) || [null])[0];
|
|
211
|
+
|
|
212
|
+
function runShow(step) {
|
|
213
|
+
const cls = (name) => { const set = new Set(); return { name, set, toggle(c, on) { if (on) set.add(c); else set.delete(c); } }; };
|
|
214
|
+
const card = cls('card'), split = cls('split');
|
|
215
|
+
const panels = {};
|
|
216
|
+
const $ = (id) => (panels[id] = panels[id] || { classList: cls(id), querySelector: () => null });
|
|
217
|
+
const sandbox = {
|
|
218
|
+
$, state: {}, panels: Object.fromEntries(Object.entries(MAP).map(([id, n]) => [n, id])),
|
|
219
|
+
root: { querySelector: (sel) => (sel === '.card' ? { classList: card } : { classList: split }) },
|
|
220
|
+
paintRail() {}, saveDraft() {}, authKnown: false, repoAccess: null,
|
|
221
|
+
renderModules() {}, syncHome() {}, ensureRepoAccess() {}, checkSlugAvailable() {}, renderAddrPanel() {},
|
|
222
|
+
syncAppStep() {}, syncRenderSetup() {}, renderReview() {},
|
|
223
|
+
};
|
|
224
|
+
sandbox.globalThis = sandbox;
|
|
225
|
+
vm.createContext(sandbox);
|
|
226
|
+
vm.runInContext(`${showSlice}\nshow(${step}, false);`, sandbox);
|
|
227
|
+
return { card: card.set, split: split.set, on: Object.keys(panels).filter((id) => panels[id].classList.set.has('on')) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
test('show() takes the rail away on the two front-door screens only, and gives the fork the whole card', () => {
|
|
231
|
+
assert.ok(showSlice, 'show() was not found in projects.html');
|
|
232
|
+
const terms = runShow(1), fork = runShow(2), name = runShow(3), done = runShow(MAP['panel-done']);
|
|
233
|
+
assert.deepEqual(terms.on, ['panel-terms']);
|
|
234
|
+
assert.ok(terms.card.has('wizFront') && !terms.split.has('wizFork'), 'terms: no rail, the bongo pair stays');
|
|
235
|
+
assert.ok(fork.card.has('wizFront') && fork.split.has('wizFork'), 'fork: no rail, and the seed takes the pair\'s place');
|
|
236
|
+
assert.ok(!name.card.has('wizFront') && !name.split.has('wizFork'), 'the name step has its rail back');
|
|
237
|
+
assert.ok(done.split.has('wizDone') && !done.card.has('wizFront'), 'the done panel is untouched by the front door');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── the create ──────────────────────────────────────────────────────────────────
|
|
241
|
+
test('the create carries the tick, and is never filed without it', () => {
|
|
242
|
+
const create = SCRIPT.slice(SCRIPT.indexOf("$('doCreate').addEventListener('click'"));
|
|
243
|
+
assert.match(create, /terms_acknowledged: state\.termsAck \? true : undefined,/,
|
|
244
|
+
'a real boolean when ticked, absent otherwise — the route refuses anything but a boolean');
|
|
245
|
+
const guard = create.indexOf('if (!state.termsAck) { show(1, true); validateStep(1); return; }');
|
|
246
|
+
assert.ok(guard > -1, 'an unticked draft is sent back to the terms');
|
|
247
|
+
assert.ok(guard < create.indexOf('ensureFreshRepo()'), 'BEFORE anything is created — the repo included');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('the tick is part of the draft and of the reset, and the draft key moved with the renumbering', () => {
|
|
251
|
+
assert.match(SCRIPT, /var state = \{ step: 0,[^}]*termsAck: false \};/, 'the initial state declares it');
|
|
252
|
+
assert.match(SCRIPT, /state = \{ step: 1,[^}]*termsAck: false \};/, 'and "start another project" clears it — each project records its own');
|
|
253
|
+
assert.match(SCRIPT, /var DRAFT_KEY = 'cb-create-draft-v6';/, 'a draft saved under the old numbering is ignored, not replayed onto the wrong screen');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ── the look-before-you-ship kit walks it ─────────────────────────────────────
|
|
257
|
+
test('the kit has a state for each front-door screen, and every walk past it ticks and chooses first', () => {
|
|
258
|
+
const states = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.states.json'), 'utf8')).states;
|
|
259
|
+
for (const s of ['in-new', 'in-new-terms-refused', 'in-new-fork', 'in-new-fork-demo', 'in-new-name']) assert.ok(states[s], `the ${s} state exists`);
|
|
260
|
+
assert.deepEqual(states['in-new'].expect.visible, ['#panel-terms', '#termsAck'], 'arriving at ?view=new pictures the terms');
|
|
261
|
+
const FRONT = ['#termsAck', '#panel-terms button[type=submit]', '#panel-fork button[type=submit]'];
|
|
262
|
+
const probes = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.probes.json'), 'utf8')).probes;
|
|
263
|
+
const walks = [...Object.entries(states).map(([k, v]) => [k, v.actions || []]), ...probes.map((p) => [p.name, p.steps])]
|
|
264
|
+
.filter(([, steps]) => steps.some((st) => st[1] === '#newName'));
|
|
265
|
+
assert.ok(walks.length >= 12, `every wizard walk is found (${walks.length})`);
|
|
266
|
+
for (const [name, steps] of walks) {
|
|
267
|
+
const clicks = steps.filter((st) => st[0] === 'click').map((st) => st[1]);
|
|
268
|
+
assert.deepEqual(clicks.slice(0, 3), FRONT, `${name}: the front door comes first`);
|
|
269
|
+
}
|
|
270
|
+
assert.ok(probes.some((p) => /the front door \(task 1004417\)/.test(p.name)), 'a probe drives the refusal, the demo door and the way on');
|
|
271
|
+
});
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
// ruled out the task's other option (drop the record at a terminal state).
|
|
16
16
|
//
|
|
17
17
|
// The reset fires only when there is a SECOND DRAFT to lose, and the draft's own step is
|
|
18
|
-
// what says so. A landed create clears DRAFT_KEY — but show(
|
|
18
|
+
// what says so. A landed create clears DRAFT_KEY — but show(11) then persists the done
|
|
19
19
|
// panel's position straight back into it, so a plain "is there a draft?" test is always
|
|
20
20
|
// yes and would skip the reset forever, leaving the finished project's name sitting in
|
|
21
21
|
// the fresh form. (That is exactly what the first cut of this change shipped into a
|
|
22
22
|
// render: "still watching Mercury" above a name field still reading "Mercury".) Only
|
|
23
|
-
// steps 1-
|
|
23
|
+
// steps 1-10 are wizard steps (1-8 before task 1004417's front door took 1 and 2);
|
|
24
|
+
// anything else is the done panel's own bookkeeping.
|
|
24
25
|
//
|
|
25
26
|
// Harness: there is no jsdom in this repo, so this follows tests/wizard_draft_resume.mjs
|
|
26
27
|
// and tests/project_door_ui.mjs — slice the real region out of the page's INLINE script
|
|
@@ -57,8 +58,8 @@ test('both slices are still findable (a rename must fail loudly, not silently pa
|
|
|
57
58
|
assert.ok(offerSlice, 'paintResumeOffer() was not found in projects.html');
|
|
58
59
|
assert.match(enterSlice, /if \(focus && !resumeIntent\) \{/, 'the intent branch is part of the slice');
|
|
59
60
|
assert.match(enterSlice, /startProgress\(created\.id\);/, 'so is the resume branch it guards');
|
|
60
|
-
assert.match(enterSlice, /draft\.step >= 1 && draft\.step <=
|
|
61
|
-
'the STORED draft\'s step decides, not state.step — state.step is
|
|
61
|
+
assert.match(enterSlice, /draft\.step >= 1 && draft\.step <= 10/,
|
|
62
|
+
'the STORED draft\'s step decides, not state.step — state.step is 11 for the whole life of a finished standup');
|
|
62
63
|
assert.match(SCRIPT, /if \(created && created\.id\) \{ enterCreated\(created, !!focus\); return; \}/,
|
|
63
64
|
'enter() still hands the created record straight to it');
|
|
64
65
|
});
|
|
@@ -72,7 +73,7 @@ function runEnter({ focus, resumeIntent = false, draft = null, record }) {
|
|
|
72
73
|
const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0 };
|
|
73
74
|
// state.step is the DONE step for the whole life of a finished standup — the value
|
|
74
75
|
// a draft-blind reading would have trusted
|
|
75
|
-
const state = { step: draft ? draft.step :
|
|
76
|
+
const state = { step: draft ? draft.step : 11, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
|
|
76
77
|
const sandbox = {
|
|
77
78
|
record, focus, resumeIntent, state,
|
|
78
79
|
DRAFT_KEY: 'draft',
|
|
@@ -98,7 +99,7 @@ const REC = { id: 'inst-1', name: 'Acme', slug: 'acme', domain: 'acme.example',
|
|
|
98
99
|
|
|
99
100
|
test('a reload / Back / identity settle (focus false) still resumes the done panel, watch and all', () => {
|
|
100
101
|
const { calls, state } = runEnter({ focus: false, record: REC });
|
|
101
|
-
assert.deepEqual(calls.show, [[
|
|
102
|
+
assert.deepEqual(calls.show, [[11, false]], 'the done panel, and no focus steal on a non-deliberate arrival');
|
|
102
103
|
assert.equal(calls.resets, 0, 'nothing is reset — this is the same standup continuing');
|
|
103
104
|
assert.equal(calls.done, 1, 'the checklist is re-rendered');
|
|
104
105
|
assert.deepEqual(calls.progress, ['inst-1'], 'the progress watch is restarted from the record');
|
|
@@ -113,7 +114,7 @@ test('a deliberate "New project" click after a finished standup opens a FRESH st
|
|
|
113
114
|
assert.equal(calls.progress.length, 0, 'and its watch is not restarted');
|
|
114
115
|
assert.equal(calls.resets, 1, 'with no second draft to lose, the wizard forgets the finished project');
|
|
115
116
|
assert.deepEqual(calls.show, [[1, false]], 'step 1, and step 1 does not take the heading focus');
|
|
116
|
-
assert.equal(calls.focused, 1, 'the
|
|
117
|
+
assert.equal(calls.focused, 1, 'its one control, the terms tick, does — the deliberate-nav rule enter() already applies');
|
|
117
118
|
assert.equal(state.name, '', 'the old name is gone from the form');
|
|
118
119
|
});
|
|
119
120
|
|
|
@@ -126,7 +127,7 @@ test('…and the standup stays one click away, named, on the resume link', () =>
|
|
|
126
127
|
|
|
127
128
|
test('the resume link gets the done panel back — the record was never cleared', () => {
|
|
128
129
|
const { calls, sandbox } = runEnter({ focus: true, resumeIntent: true, record: REC });
|
|
129
|
-
assert.deepEqual(calls.show, [[
|
|
130
|
+
assert.deepEqual(calls.show, [[11, true]], 'a deliberate resume moves focus to the done panel');
|
|
130
131
|
assert.deepEqual(calls.progress, ['inst-1'], 'the watch comes back with it');
|
|
131
132
|
assert.equal(sandbox.resumeIntent, false, 'the flag is ONE-SHOT — the next plain nav click must mean "new" again');
|
|
132
133
|
assert.deepEqual(calls.offer, [null], 'and the offer comes down behind it');
|
|
@@ -144,20 +145,20 @@ test('a second draft already under way is NOT wiped by navigating back to the wi
|
|
|
144
145
|
});
|
|
145
146
|
|
|
146
147
|
test('the done panel\'s own saved step is NOT a second draft — it must not block the reset', () => {
|
|
147
|
-
// Caught in a render, not in a unit test: show(
|
|
148
|
+
// Caught in a render, not in a unit test: show(11) calls saveDraft(), so DRAFT_KEY is
|
|
148
149
|
// non-empty again the moment a create lands. Reading "is there a draft?" left the
|
|
149
150
|
// finished project's name in the fresh form, under a line offering to resume it.
|
|
150
|
-
const { calls, state } = runEnter({ focus: true, record: REC, draft: { step:
|
|
151
|
+
const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 11, name: 'Mercury' } });
|
|
151
152
|
assert.equal(calls.resets, 1, 'the done step is bookkeeping, not a project being typed');
|
|
152
|
-
assert.deepEqual(calls.show, [[1, false]], 'out-of-range steps land on the
|
|
153
|
+
assert.deepEqual(calls.show, [[1, false]], 'out-of-range steps land on step 1, the front door');
|
|
153
154
|
assert.equal(state.name, '', 'and the finished project\'s name does not survive into it');
|
|
154
155
|
assert.equal(calls.done, 0, 'never back onto the done panel');
|
|
155
156
|
});
|
|
156
157
|
|
|
157
158
|
test('a draft on the app step or the review is live too — the last two numbered steps (task 1004351)', () => {
|
|
158
|
-
// The app step moved the review from 7 to 8
|
|
159
|
-
// project sitting on its review, silently wiping it.
|
|
160
|
-
for (const step of [
|
|
159
|
+
// The app step moved the review from 7 to 8, and the front door (task 1004417) to 10. A
|
|
160
|
+
// guard left behind would reset a second project sitting on its review, silently wiping it.
|
|
161
|
+
for (const step of [9, 10]) {
|
|
161
162
|
const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
|
|
162
163
|
assert.equal(calls.resets, 0, `step ${step} is a live draft`);
|
|
163
164
|
assert.deepEqual(calls.show, [[step, true]]);
|
|
@@ -165,8 +166,16 @@ test('a draft on the app step or the review is live too — the last two numbere
|
|
|
165
166
|
}
|
|
166
167
|
});
|
|
167
168
|
|
|
168
|
-
test('a draft
|
|
169
|
-
for (const
|
|
169
|
+
test('a draft on the front door is live too — a founder part-way through it keeps their place (task 1004417)', () => {
|
|
170
|
+
for (const step of [1, 2]) {
|
|
171
|
+
const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
|
|
172
|
+
assert.equal(calls.resets, 0, `step ${step} is a live draft`);
|
|
173
|
+
assert.equal(state.step, step);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('a draft whose step is missing or junk resets too — only 1-10 preserves anything', () => {
|
|
178
|
+
for (const draft of [{ step: 0 }, { step: 11 }, { name: 'half-typed' }]) {
|
|
170
179
|
const { calls } = runEnter({ focus: true, record: REC, draft });
|
|
171
180
|
assert.equal(calls.resets, 1, `step ${JSON.stringify(draft.step)} is not a wizard step`);
|
|
172
181
|
assert.deepEqual(calls.show, [[1, false]]);
|
|
@@ -191,11 +200,14 @@ test('paintResumeOffer names the project by textContent and hides on null (no li
|
|
|
191
200
|
});
|
|
192
201
|
|
|
193
202
|
test('the offer ships as real markup inside step 1, hidden until there is something to resume', () => {
|
|
194
|
-
|
|
203
|
+
// step 1 is the front door's terms screen since task 1004417 — the offer moved with it
|
|
204
|
+
const panel = HUB.slice(HUB.indexOf('id="panel-terms"'), HUB.indexOf('id="panel-fork"'));
|
|
195
205
|
assert.match(panel, /<p class="resumeOffer" id="wizResume" hidden>still watching <strong id="wizResumeName">your project<\/strong> — <a href="#" id="wizResumeGo">resume it<\/a><\/p>/,
|
|
196
206
|
'baked into the panel (C7), carrying its own hidden state');
|
|
197
|
-
assert.ok(panel.indexOf('id="wizResume"') < panel.indexOf('<form
|
|
198
|
-
'it sits above the
|
|
207
|
+
assert.ok(panel.indexOf('id="wizResume"') < panel.indexOf('<form data-next="2">'),
|
|
208
|
+
'it sits above the terms form, where a visitor reads it before anything else');
|
|
209
|
+
assert.doesNotMatch(HUB.slice(HUB.indexOf('id="panel-name"'), HUB.indexOf('id="panel-type"')), /id="wizResume"/,
|
|
210
|
+
'and only once — a second copy would be a duplicate id');
|
|
199
211
|
assert.match(SCRIPT, /\$\('wizResumeGo'\)\.addEventListener\('click', function \(ev\) \{\n\s*ev\.preventDefault\(\);\n\s*resumeIntent = true;\n\s*enter\(true\);/,
|
|
200
212
|
'the link raises the one-shot intent and re-enters, rather than href-navigating');
|
|
201
213
|
});
|