@bongos/core 1.21.4 → 1.21.6
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 +233 -108
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
- package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +37 -2
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +541 -658
- package/docs/copy-registry.json +1205 -2340
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
- package/docs/onboarding/diagrams/assertions.json +1 -1
- package/docs/page-inventory.json +47 -28
- package/docs/page-readings.json +1128 -1339
- package/modules/autonomy/config-idle.js +192 -0
- package/modules/autonomy/governor-docket.js +101 -0
- package/modules/autonomy/routes/autonomy.js +27 -0
- package/modules/autonomy/runner-health.js +22 -1
- package/modules/discord/board-broadcast.js +3 -1
- package/modules/discord/craft-broadcast.js +107 -0
- package/modules/discord/routes/discord.js +10 -0
- package/modules/discord/ship-broadcast.js +30 -2
- package/modules/economy/reward.js +3 -0
- package/modules/government/board.js +20 -5
- package/modules/government/docket-vocab.js +82 -0
- package/modules/government/docket.js +458 -0
- package/modules/government/routes/government.js +34 -0
- package/modules/hall-ui/public/city-draw.js +438 -0
- package/modules/hall-ui/public/city.css +110 -0
- package/modules/hall-ui/public/city.html +71 -0
- package/modules/hall-ui/public/city.js +177 -0
- package/modules/hall-ui/public/city.states.json +30 -0
- package/modules/hall-ui/public/gate.js +11 -4
- package/modules/hall-ui/public/genesis-home.js +9 -10
- package/modules/hall-ui/public/profile.css +4 -0
- package/modules/hall-ui/public/profile.js +25 -1
- package/modules/hall-ui/public/settings-autobongos.js +19 -1
- package/modules/hall-ui/public/settings.css +26 -0
- package/modules/hall-ui/public/settings.html +8 -0
- package/modules/hall-ui/public/settings.js +43 -3
- package/modules/ideas/governor-docket.js +93 -0
- package/modules/ideas/routes/blockers.js +5 -0
- package/modules/lifecycle/governor-docket.js +192 -0
- package/modules/lifecycle/routes/gate-approvals.js +78 -33
- package/modules/lifecycle/routes/lifecycle.js +3 -0
- package/modules/onboarding/routes/access-requests.js +68 -27
- package/modules/platform-identity/craft-rollup.js +72 -0
- package/modules/platform-identity/migrations/platform_identity_029_activity_crafts.sql +23 -0
- package/modules/platform-identity/platform-identity.js +12 -6
- package/modules/platform-identity/routes/sso.js +4 -0
- package/modules/platform-identity/tests/platform-identity.mjs +1 -1
- package/modules/provisioning/core-upgrade.js +34 -7
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/seams.js +2 -0
- package/modules/public-landing/public/assets/cosmos.css +6 -50
- package/modules/public-landing/public/projects.html +1170 -1659
- package/modules/public-landing/public/projects.probes.json +18 -18
- package/modules/public-landing/public/projects.states.json +24 -31
- package/modules/security/routes/reports.js +39 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +72 -0
- package/scripts/gds/autobongos-grade-cap.js +187 -0
- package/scripts/gds/autobongos-loop.js +4 -0
- package/scripts/gds/autobongos-run.js +154 -13
- package/scripts/gds/autobongos-verify.js +211 -14
- package/scripts/gds/provision-core-upgrade.js +14 -2
- package/scripts/gds/provision-repo.js +77 -13
- package/scripts/gds/ship-flow.js +5 -0
- package/scripts/gds/ship-preflight-steps.js +2 -1
- package/scripts/gds/ship.js +10 -0
- package/scripts/gds/update-sweep.js +24 -8
- package/scripts/gds/upgrade-outcome.js +1 -0
- package/src/bongos/auth-admission.js +95 -13
- package/src/bongos/db-kernel.js +1 -1
- package/src/bongos/module-rechecks.js +77 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/core-update.js +68 -2
- package/src/bongos/routes/modules.js +17 -0
- package/src/bongos/serve-internal.js +14 -0
- package/src/bongos/software-update.js +3 -2
- package/src/module-api.js +1 -1
- package/tests/activity_rollup_order_db.mjs +3 -1
- package/tests/activity_snapshots_db.mjs +2 -0
- package/tests/autobongos_grade_cap.mjs +125 -0
- package/tests/autobongos_loop.mjs +230 -1
- package/tests/autobongos_verify.mjs +239 -3
- package/tests/autonomy_config_idle.mjs +272 -0
- package/tests/core_update_banner.mjs +29 -0
- package/tests/core_upgrade_door.mjs +31 -1
- package/tests/core_upgrade_runner.mjs +20 -0
- package/tests/discord_craft_broadcast.mjs +162 -0
- package/tests/genesis_home.mjs +1 -1
- package/tests/government_routes.mjs +3 -1
- package/tests/governor_docket.mjs +552 -0
- package/tests/hall_audit.mjs +10 -1
- package/tests/hall_city.mjs +369 -0
- package/tests/hall_page_gate_map.mjs +5 -0
- package/tests/hub_craft_rollup.mjs +350 -0
- package/tests/onboard.mjs +10 -4
- package/tests/platform_boot.mjs +1 -1
- package/tests/profile_rollup_consent.mjs +2 -0
- package/tests/profile_route.mjs +16 -2
- package/tests/projects_hub.mjs +196 -89
- package/tests/projects_hub_app_status.mjs +3 -1
- package/tests/projects_hub_app_step.mjs +72 -48
- package/tests/projects_hub_dns_ready.mjs +10 -2
- package/tests/projects_hub_look.mjs +32 -22
- package/tests/projects_hub_module_picker.mjs +112 -62
- package/tests/projects_hub_pre_uat.mjs +48 -41
- package/tests/provision.mjs +78 -0
- package/tests/public_landing_projects.mjs +12 -6
- package/tests/settings_main_role.mjs +190 -0
- package/tests/software_update.mjs +56 -1
- package/tests/update_subscription_engine.mjs +55 -3
- package/tests/wizard_demo.mjs +37 -11
- package/tests/wizard_draft_resume.mjs +34 -22
- package/tests/wizard_front_door.mjs +173 -103
- package/tests/wizard_intent_resume.mjs +28 -25
- package/tests/wizard_physics.mjs +94 -76
- package/tests/wizard_physics_more.mjs +428 -277
- package/tests/wizard_six_screens.mjs +126 -0
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// tests/wizard_front_door.mjs — the create wizard's front door (task 1004417, BV2.PS05;
|
|
2
2
|
// goal 1000121, docs/design/project-startup-direction.md "Front door").
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Since the six blended screens (task 1004504, the design of record
|
|
5
|
+
// docs/design/mocks/combined-onboarding/) the front door is ONE screen:
|
|
6
|
+
// 1 · fork — "You’re about to start a digital planet.": the terms as its one tick, recorded
|
|
7
|
+
// on the project against the terms version the SERVER stamps
|
|
8
|
+
// (tests/provisioning_terms_ack.mjs holds that half), and the world's door pill, two
|
|
9
|
+
// equal doors, try the demo / start my project, both waiting for the tick. The demo is
|
|
10
|
+
// its own route (BV2.PS09). The separate terms screen is gone.
|
|
11
|
+
// The wizard carries no progress rail: an eyebrow names the screen, one Back walks back, and
|
|
12
|
+
// the night room (#wizRoom) stands behind every screen in place of the planet and bongo pair.
|
|
10
13
|
// Sign-in stays where it was: the gate (step 0) comes first.
|
|
11
14
|
//
|
|
12
15
|
// 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
|
|
16
|
+
// validateStep(), the front door's wiring, and show()'s screen bookkeeping are sliced out of the
|
|
14
17
|
// inline script and executed under a minimal DOM stub. Every slice boundary is asserted, so a
|
|
15
18
|
// rename fails loudly instead of leaving this file quietly testing nothing.
|
|
16
19
|
//
|
|
@@ -32,74 +35,73 @@ const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>
|
|
|
32
35
|
const STYLE = HUB.slice(HUB.indexOf('<style>'), HUB.indexOf('</style>'));
|
|
33
36
|
|
|
34
37
|
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
38
|
const FORK = panel('panel-fork');
|
|
39
|
+
const VIEW_NEW = (HUB.match(/<section id="view-new" hidden>[\s\S]*?\n<\/section>/) || [''])[0];
|
|
37
40
|
const MAP = {};
|
|
38
41
|
for (const m of ((SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1]).matchAll(/(\d+):\s*'([a-z-]+)'/g)) MAP[m[2]] = Number(m[1]);
|
|
39
42
|
|
|
40
43
|
// ── the order ───────────────────────────────────────────────────────────────────
|
|
41
|
-
test('the front door stands between sign-in and the
|
|
44
|
+
test('the front door stands between sign-in and the first question: gate 0, fork 1, what 2', () => {
|
|
42
45
|
assert.equal(MAP['panel-gate'], 0, 'sign-in stays where it is today, first');
|
|
43
|
-
assert.equal(MAP['panel-
|
|
44
|
-
assert.equal(MAP['panel-
|
|
45
|
-
|
|
46
|
-
assert.equal(MAP['panel-
|
|
47
|
-
assert.
|
|
48
|
-
assert.ok(HUB.indexOf('id="panel-gate"') < HUB.indexOf('id="panel-
|
|
49
|
-
&& HUB.indexOf('id="panel-
|
|
50
|
-
&& HUB.indexOf('id="panel-
|
|
51
|
-
|
|
52
|
-
assert.
|
|
53
|
-
assert.
|
|
54
|
-
assert.match(
|
|
46
|
+
assert.equal(MAP['panel-fork'], 1, 'the fork, with the terms as its tick, is the first screen after sign-in');
|
|
47
|
+
assert.equal(MAP['panel-what'], 2, 'the real-project door goes on to "What are you making?"');
|
|
48
|
+
assert.equal(MAP['panel-checks'], 3, 'then the quick checks (task 1004504)');
|
|
49
|
+
assert.equal(MAP['panel-terms'], undefined, 'the separate terms screen is gone — the tick lives on the fork');
|
|
50
|
+
assert.doesNotMatch(HUB, /id="panel-terms"/, 'and no markup is left for it');
|
|
51
|
+
assert.ok(HUB.indexOf('id="panel-gate"') < HUB.indexOf('id="panel-fork"')
|
|
52
|
+
&& HUB.indexOf('id="panel-fork"') < HUB.indexOf('id="panel-what"')
|
|
53
|
+
&& HUB.indexOf('id="panel-what"') < HUB.indexOf('id="panel-checks"'), 'the markup reads in the same order');
|
|
54
|
+
assert.match(FORK, /<form data-next="2">/, 'the fork\'s primary door goes on to the first question');
|
|
55
|
+
assert.ok(VIEW_NEW, 'the ?view=new section is findable');
|
|
56
|
+
assert.doesNotMatch(VIEW_NEW, /data-back=/, 'no per-panel Back buttons — one #wizBack walks the screens');
|
|
57
|
+
assert.equal((VIEW_NEW.match(/id="wizBack"/g) || []).length, 1, 'one Back, in the wizard\'s top row');
|
|
55
58
|
});
|
|
56
59
|
|
|
57
|
-
test('the rail is
|
|
60
|
+
test('the numbered rail is gone (task 1004504) — no steps list, no painter, no rule hiding it', () => {
|
|
58
61
|
const beats = [...HUB.matchAll(/<li[^>]*data-step="(\d+)"/g)].map((m) => Number(m[1]));
|
|
59
|
-
assert.deepEqual(beats, [
|
|
60
|
-
assert.
|
|
61
|
-
assert.
|
|
62
|
+
assert.deepEqual(beats, [], 'no rail beat is left anywhere on the page');
|
|
63
|
+
assert.doesNotMatch(HUB, /class="steps"/, 'no <ol class="steps"> in the wizard');
|
|
64
|
+
assert.doesNotMatch(SCRIPT, /paintRail\(/, 'no rail painter, and no call to one');
|
|
65
|
+
assert.doesNotMatch(STYLE, /\.wizFront \.steps|#view-new \.steps/, 'no rule left styling or hiding a rail');
|
|
62
66
|
});
|
|
63
67
|
|
|
64
|
-
// ── screen 1 · the terms
|
|
65
|
-
test('screen 1 is the
|
|
66
|
-
assert.ok(
|
|
67
|
-
assert.match(
|
|
68
|
-
assert.match(
|
|
69
|
-
assert.equal((
|
|
70
|
-
assert.match(
|
|
71
|
-
assert.match(
|
|
72
|
-
'the whole row is its label, so the target is the sentence, not a 20px box');
|
|
73
|
-
assert.match(
|
|
74
|
-
|
|
75
|
-
assert.equal((TERMS.match(/type="submit"/g) || []).length, 1, 'one primary door');
|
|
76
|
-
assert.match(TERMS, /<span data-brand-product-inline>Cloud Bongos<\/span>/, 'the product name is the pack\'s, re-branded on a child');
|
|
68
|
+
// ── screen 1 · the fork, with the terms as its one tick ─────────────────────────
|
|
69
|
+
test('screen 1 is the fork: one short lede, one tick that links the terms, one submit door', () => {
|
|
70
|
+
assert.ok(FORK, '#panel-fork ships as real markup (C7)');
|
|
71
|
+
assert.match(FORK, /^<section class="panel on" id="panel-fork"/, 'it carries the static `on` — step 1 for a no-script visitor');
|
|
72
|
+
assert.match(FORK, /<h1 id="fork-h">You’re about to start a digital planet\.<\/h1>/);
|
|
73
|
+
assert.equal((FORK.match(/<div id="forkIntro">[\s\S]*?<\/form>/)?.[0].match(/<p class="lede">/g) || []).length, 1, 'one short lede paragraph');
|
|
74
|
+
assert.match(FORK, /<input type="checkbox" id="termsAck" aria-describedby="h-terms">/, 'one checkbox, named by its hint');
|
|
75
|
+
assert.match(FORK, /<label class="termsAck">\s*<input[^>]*>\s*<span>I can sign for this project, and I accept the <a class="termsRead" href="\/terms" target="_blank" rel="noopener">terms<\/a>\.<\/span>/,
|
|
76
|
+
'the whole row is its label, so the target is the sentence, not a 20px box — and the terms are the platform\'s own /terms, in a new tab so the draft is not left behind');
|
|
77
|
+
assert.equal((FORK.match(/type="submit"/g) || []).length, 1, 'one primary door');
|
|
78
|
+
assert.doesNotMatch(FORK, /\bBongos\b/, 'no product name baked where a child instance would ship it');
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
test('the page never names a terms version — the server stamps which text was acknowledged', () => {
|
|
80
82
|
// A version baked here would be a second copy of TERMS_VERSION that drifts, and worse, a
|
|
81
83
|
// value a caller could send. The create request carries only the tick.
|
|
82
84
|
assert.doesNotMatch(HUB, /terms_version|termsVersion|terms_ack_version/, 'no version travels from the page');
|
|
83
|
-
assert.doesNotMatch(
|
|
85
|
+
assert.doesNotMatch(FORK, /\b20\d\d-\d\d-\d\d\b/, 'and none is printed on the screen');
|
|
84
86
|
});
|
|
85
87
|
|
|
86
|
-
test('the resume offer
|
|
88
|
+
test('the resume offer lives on step 1 — the fork, once', () => {
|
|
87
89
|
assert.equal((HUB.match(/id="wizResume"/g) || []).length, 1);
|
|
88
|
-
assert.ok(
|
|
90
|
+
assert.ok(FORK.includes('id="wizResume"'), 'a deliberate "New project" lands here, so the offer does too');
|
|
89
91
|
});
|
|
90
92
|
|
|
91
93
|
// ── the validator, executed ────────────────────────────────────────────────────
|
|
92
94
|
const VALIDATE_RE = /function validateStep\(step\) \{[\s\S]*?\n {4}\}/;
|
|
93
95
|
const validateSlice = (SCRIPT.match(VALIDATE_RE) || [null])[0];
|
|
94
96
|
|
|
95
|
-
function runValidate(step, termsAck) {
|
|
97
|
+
function runValidate(step, termsAck, extra = {}) {
|
|
96
98
|
const els = {};
|
|
97
99
|
const $ = (id) => (els[id] = els[id] || { textContent: '', className: '', classList: { add() {} }, focus() {} });
|
|
98
|
-
const sandbox = { $, state: { termsAck, name: 'Mercury', projType: 'game' }, validateRender: () => true, repoAccess: true };
|
|
100
|
+
const sandbox = { $, state: { termsAck, name: 'Mercury', projType: 'game', demo: null, confirmed: {}, ...extra }, validateRender: () => true, repoAccess: true };
|
|
99
101
|
sandbox.globalThis = sandbox;
|
|
100
102
|
vm.createContext(sandbox);
|
|
101
103
|
vm.runInContext(`${validateSlice}\nvar ok = validateStep(${step});`, sandbox);
|
|
102
|
-
return { ok: sandbox.ok, hint: els['h-terms'] };
|
|
104
|
+
return { ok: sandbox.ok, hint: els['h-terms'], state: sandbox.state };
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
test('validateStep is still findable, with the terms clause inside it', () => {
|
|
@@ -107,35 +109,44 @@ test('validateStep is still findable, with the terms clause inside it', () => {
|
|
|
107
109
|
assert.match(validateSlice, /if \(step === 1\) \{\s*if \(!state\.termsAck\)/, 'the clause is part of the slice');
|
|
108
110
|
});
|
|
109
111
|
|
|
110
|
-
test('
|
|
112
|
+
test('Start my project is refused until the box is ticked, and the refusal says why in the hint', () => {
|
|
111
113
|
const refused = runValidate(1, false);
|
|
112
|
-
assert.equal(refused.ok, false, 'no tick, no fork');
|
|
114
|
+
assert.equal(refused.ok, false, 'no tick, no way past the fork');
|
|
113
115
|
assert.equal(refused.hint.className, 'hint err');
|
|
114
116
|
assert.match(refused.hint.textContent, /Tick the box to go on/);
|
|
115
117
|
const given = runValidate(1, true);
|
|
116
118
|
assert.equal(given.ok, true, 'a tick goes on');
|
|
117
119
|
});
|
|
118
120
|
|
|
119
|
-
test('
|
|
120
|
-
|
|
121
|
-
// task 1004420: "Begin planet creation" is a real project, so a demo the founder
|
|
121
|
+
test('Start my project clears a demo started earlier — the submit door is a real project', () => {
|
|
122
|
+
// task 1004420: "Start my project" is a real project, so a demo the founder
|
|
122
123
|
// set up and then walked back from must not ride the create
|
|
123
|
-
|
|
124
|
+
const r = runValidate(1, true, { demo: { people: 3, hours_each: 4 }, confirmed: { team: true } });
|
|
125
|
+
assert.equal(r.ok, true);
|
|
126
|
+
assert.equal(r.state.demo, null, 'the walked-back demo does not ride the create');
|
|
127
|
+
assert.deepEqual(JSON.parse(JSON.stringify(r.state.confirmed)), {}, 'and its rows are asked again');
|
|
128
|
+
assert.equal(runValidate(1, false, { demo: { people: 3 } }).state.demo !== null, true, 'an unticked box changes nothing');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('"What are you making?" asks a name and a kind (task 1004504)', () => {
|
|
132
|
+
assert.equal(runValidate(2, true).ok, true, 'a name and a kind go on');
|
|
133
|
+
assert.equal(runValidate(2, true, { name: ' ' }).ok, false, 'no name, no way on');
|
|
134
|
+
assert.equal(runValidate(2, true, { projType: null }).ok, false, 'no kind, no way on — Not sure is a fine answer');
|
|
124
135
|
});
|
|
125
136
|
|
|
126
137
|
// ── the wiring, executed ───────────────────────────────────────────────────────
|
|
127
|
-
const WIRE_START =
|
|
138
|
+
const WIRE_START = ' function paintDoors() {';
|
|
128
139
|
const WIRE_END = ' /* ── the demo route (task 1004420, BV2.PS09) ──';
|
|
129
|
-
const wireSlice = SCRIPT.includes(WIRE_START) ? SCRIPT.slice(SCRIPT.indexOf(WIRE_START), SCRIPT.indexOf(WIRE_END)) : null;
|
|
140
|
+
const wireSlice = SCRIPT.includes(WIRE_START) && SCRIPT.includes(WIRE_END) ? SCRIPT.slice(SCRIPT.indexOf(WIRE_START), SCRIPT.indexOf(WIRE_END)) : null;
|
|
130
141
|
|
|
131
142
|
function runWiring(initialAck) {
|
|
132
143
|
const handlers = {};
|
|
133
144
|
const mk = (id) => ({
|
|
134
|
-
id, checked: false, hidden: false, textContent: '', className: '', attrs: {},
|
|
145
|
+
id, checked: false, hidden: false, disabled: true, textContent: '', className: '', attrs: {},
|
|
135
146
|
addEventListener(type, fn) { handlers[id + ':' + type] = fn; },
|
|
136
147
|
setAttribute(k, v) { this.attrs[k] = v; },
|
|
137
148
|
});
|
|
138
|
-
const els = { termsAck: mk('termsAck'), 'h-terms': mk('h-terms') };
|
|
149
|
+
const els = { termsAck: mk('termsAck'), 'h-terms': mk('h-terms'), wizDemo: mk('wizDemo'), wizStart: mk('wizStart') };
|
|
139
150
|
let saves = 0;
|
|
140
151
|
const sandbox = { $: (id) => els[id], state: { termsAck: initialAck }, saveDraft: () => { saves += 1; } };
|
|
141
152
|
sandbox.globalThis = sandbox;
|
|
@@ -147,6 +158,25 @@ function runWiring(initialAck) {
|
|
|
147
158
|
test('the front door\'s wiring is still findable', () => {
|
|
148
159
|
assert.ok(wireSlice && wireSlice.length > 100, 'the wiring block was not found in projects.html');
|
|
149
160
|
assert.match(wireSlice, /\$\('termsAck'\)\.addEventListener\('change'/, 'the tick is part of the slice');
|
|
161
|
+
assert.match(wireSlice, /\$\('wizStart'\)\.disabled = !state\.termsAck;/, 'and so is the door painter');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('both doors wait for the tick: disabled in the markup, and painted from the answer', () => {
|
|
165
|
+
assert.match(FORK, /<button type="button" class="btn big ghost" id="wizDemo"[^>]*\bdisabled>/, 'the demo door ships disabled');
|
|
166
|
+
assert.match(FORK, /<button type="submit" class="btn big" id="wizStart" disabled>/, 'and so does Start my project');
|
|
167
|
+
const fresh = runWiring(false);
|
|
168
|
+
assert.equal(fresh.els.wizDemo.disabled, true, 'an unticked draft keeps the demo door shut');
|
|
169
|
+
assert.equal(fresh.els.wizStart.disabled, true, 'and the real-project door');
|
|
170
|
+
fresh.els.termsAck.checked = true; fresh.handlers['termsAck:change'].call(fresh.els.termsAck);
|
|
171
|
+
assert.equal(fresh.els.wizDemo.disabled, false, 'ticking opens both doors');
|
|
172
|
+
assert.equal(fresh.els.wizStart.disabled, false);
|
|
173
|
+
fresh.els.termsAck.checked = false; fresh.handlers['termsAck:change'].call(fresh.els.termsAck);
|
|
174
|
+
assert.ok(fresh.els.wizDemo.disabled && fresh.els.wizStart.disabled, 'unticking shuts them again');
|
|
175
|
+
const resumed = runWiring(true);
|
|
176
|
+
assert.ok(!resumed.els.wizDemo.disabled && !resumed.els.wizStart.disabled, 'a draft that ticked comes back with its doors open');
|
|
177
|
+
assert.match(STYLE, /#view-new \.doorPill \.btn\[disabled\]\{opacity:\.35;cursor:default;\}/, 'a shut door reads as shut');
|
|
178
|
+
assert.match(SCRIPT, /\$\('wizDemo'\)\.addEventListener\('click', function \(\) \{ if \(state\.termsAck\) demoShow\('setup'\); \}\);/,
|
|
179
|
+
'and the demo door refuses an unticked box even if the attribute is stripped');
|
|
150
180
|
});
|
|
151
181
|
|
|
152
182
|
test('the tick mirrors into the draft both ways, and a resumed draft paints its own answer', () => {
|
|
@@ -167,14 +197,14 @@ test('the demo door names what it opens: the demo route\'s setup screen (task 10
|
|
|
167
197
|
assert.doesNotMatch(FORK, /demoSoon|isn’t open yet/, 'the placeholder that said the demo was not open is gone');
|
|
168
198
|
});
|
|
169
199
|
|
|
170
|
-
// ──
|
|
171
|
-
test('
|
|
172
|
-
assert.match(FORK, /<h1 id="fork-h">You
|
|
200
|
+
// ── the fork's door pill ────────────────────────────────────────────────────────
|
|
201
|
+
test('the fork is the door pill: two equal doors in one object, the accent only on the one that goes on', () => {
|
|
202
|
+
assert.match(FORK, /<h1 id="fork-h">You’re about to start a digital planet\.<\/h1>/);
|
|
173
203
|
const pill = (FORK.match(/<div class="doorPill" role="group" aria-label="How to begin">[\s\S]*?<\/div>/) || [''])[0];
|
|
174
204
|
assert.ok(pill, 'one grouped pill');
|
|
175
205
|
const doors = [...pill.matchAll(/<button type="(button|submit)" class="([^"]+)"[^>]*>([^<]+)<\/button>/g)];
|
|
176
206
|
assert.equal(doors.length, 2, 'exactly two doors');
|
|
177
|
-
assert.deepEqual(doors.map((d) => d[3]), ['Try the demo', '
|
|
207
|
+
assert.deepEqual(doors.map((d) => d[3]), ['Try the demo', 'Start my project']);
|
|
178
208
|
assert.equal(doors[0][2], 'btn big ghost', 'the demo door is the secondary: transparent at rest');
|
|
179
209
|
assert.equal(doors[1][2], 'btn big', 'begin carries the accent');
|
|
180
210
|
assert.equal(doors[1][1], 'submit', 'and is the form\'s one submit, so Enter goes on');
|
|
@@ -183,58 +213,94 @@ test('screen 2 is the door pill: two equal doors in one object, the accent only
|
|
|
183
213
|
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');
|
|
184
214
|
});
|
|
185
215
|
|
|
186
|
-
test('the
|
|
187
|
-
assert.match(
|
|
188
|
-
assert.match(
|
|
216
|
+
test('the night room stands behind every screen — the planet stage and the bongo pair are gone (task 1004504)', () => {
|
|
217
|
+
assert.match(VIEW_NEW, /<div class="wizRoom" id="wizRoom" data-screen="fork" aria-hidden="true">/, 'decorative, so hidden from a screen reader, and baked at the fork for a no-script visitor');
|
|
218
|
+
assert.match(VIEW_NEW, /<span class="rmSeed"><img src="\/assets\/world\/r2-chrome-blob-one-cut\.webp" alt=""><\/span>/, 'the seed is a real plate, root-absolute so it is stamped');
|
|
189
219
|
assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', 'r2-chrome-blob-one-cut.webp')), 'and it exists on disk');
|
|
190
|
-
assert.
|
|
191
|
-
assert.
|
|
192
|
-
const at = STYLE.indexOf('
|
|
193
|
-
const
|
|
194
|
-
assert.ok(at > -1 &&
|
|
195
|
-
|
|
220
|
+
assert.doesNotMatch(HUB, /id="forkStage"|class="forkSeed"|class="forkRoutes"|bongoCell|id="physStage"/, 'no fork stage, seed orbit, route list, bongo pair or forming planet left in the markup');
|
|
221
|
+
assert.doesNotMatch(STYLE, /\.forkStage\{|\.forkOrbit\{|\.bongoCell|\.physStage/, 'and no rule left for them');
|
|
222
|
+
const at = STYLE.indexOf('#view-new .wizRoom{');
|
|
223
|
+
const end = STYLE.indexOf('/* under 900 the window', at);
|
|
224
|
+
assert.ok(at > -1 && end > at, 'the night room\'s CSS block is findable');
|
|
225
|
+
const block = STYLE.slice(at, end);
|
|
226
|
+
const hexes = [...block.matchAll(/#[0-9a-f]{3,8}\b/gi)].map((m) => m[0].toLowerCase());
|
|
227
|
+
assert.ok(hexes.length > 0 && hexes.every((h) => h === '#000' || h === '#fff'), `every colour a mix of the fifteen with black or white (${[...new Set(hexes)]})`);
|
|
228
|
+
for (const m of block.matchAll(/--rm-[a-z0-9-]+:([^;]+);/g)) {
|
|
229
|
+
assert.match(m[1], /^color-mix\(in srgb,var\(--(sea|accent)\)|^rgba\(255,255,255,/, `a room token is derived, never a new literal: ${m[0]}`);
|
|
230
|
+
}
|
|
231
|
+
const motion = block.indexOf('@media (prefers-reduced-motion:no-preference){');
|
|
232
|
+
assert.ok(motion > -1, 'the room\'s motion sits inside the kill switch');
|
|
233
|
+
assert.doesNotMatch(block.slice(0, motion), /animation:|transition:/, 'and none outside it: the rest frame is the whole composition');
|
|
234
|
+
assert.doesNotMatch(block, /:root/, 'room tokens live on the room, never a page :root');
|
|
196
235
|
});
|
|
197
236
|
|
|
198
237
|
test('the two routes are described honestly for today: the demo is open (task 1004420), creation promises no questions that do not exist yet', () => {
|
|
199
|
-
const
|
|
200
|
-
assert.match(
|
|
201
|
-
assert.doesNotMatch(
|
|
202
|
-
assert.
|
|
203
|
-
assert.doesNotMatch(
|
|
204
|
-
assert.doesNotMatch(FORK, /\bBongos\b(?! pair)/, 'no product name baked where a child instance would ship it');
|
|
238
|
+
const intro = (FORK.match(/<div id="forkIntro">[\s\S]*?<\/form>/) || [''])[0];
|
|
239
|
+
assert.match(intro, /<p class="lede">Try a small demo first, or start your real project\.<\/p>/);
|
|
240
|
+
assert.doesNotMatch(FORK, /coming soon/, 'the demo is built, so nothing says it is coming');
|
|
241
|
+
assert.doesNotMatch(FORK, /seven questions/i, 'the copy promises no question count');
|
|
242
|
+
assert.doesNotMatch(FORK, /\bBongos\b/, 'no product name baked where a child instance would ship it');
|
|
205
243
|
});
|
|
206
244
|
|
|
207
|
-
// ── show(): the
|
|
245
|
+
// ── show(): the room, the eyebrow and the one Back, executed ───────────────────
|
|
246
|
+
// The slice runs from the SCREEN map through show(), so eyebrowFor() and prevStep() ride
|
|
247
|
+
// along; both ends are asserted.
|
|
248
|
+
const SHOW_START = ' var SCREEN = {';
|
|
208
249
|
const SHOW_RE = /function show\(step, moveFocus\) \{[\s\S]*?\n {4}\}/;
|
|
209
|
-
const
|
|
250
|
+
const showMatch = SCRIPT.match(SHOW_RE);
|
|
251
|
+
const showSlice = showMatch && SCRIPT.includes(SHOW_START) && SCRIPT.indexOf(SHOW_START) < showMatch.index
|
|
252
|
+
? SCRIPT.slice(SCRIPT.indexOf(SHOW_START), showMatch.index + showMatch[0].length) : null;
|
|
210
253
|
|
|
211
|
-
function runShow(step) {
|
|
254
|
+
function runShow(step, { demo = null, demoOpen = false } = {}) {
|
|
212
255
|
const cls = (name) => { const set = new Set(); return { name, set, toggle(c, on) { if (on) set.add(c); else set.delete(c); } }; };
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
256
|
+
const els = {};
|
|
257
|
+
const $ = (id) => (els[id] = els[id] || {
|
|
258
|
+
id, classList: cls(id), hidden: id === 'demoSetup' ? !demoOpen : false, textContent: '', attrs: {},
|
|
259
|
+
setAttribute(k, v) { this.attrs[k] = v; }, querySelector: () => null,
|
|
260
|
+
});
|
|
216
261
|
const sandbox = {
|
|
217
|
-
$, state: {}, panels: Object.fromEntries(Object.entries(MAP).map(([id, n]) => [n, id])),
|
|
218
|
-
|
|
219
|
-
|
|
262
|
+
$, state: { demo }, panels: Object.fromEntries(Object.entries(MAP).map(([id, n]) => [n, id])),
|
|
263
|
+
saveDraft() {}, authKnown: false, repoAccess: null,
|
|
264
|
+
paintDoors() {}, physLoadAccountOnce() {}, paintChecks() {}, physDefaults() {},
|
|
220
265
|
renderModules() {}, syncHome() {}, ensureRepoAccess() {}, checkSlugAvailable() {}, renderAddrPanel() {},
|
|
221
|
-
syncAppStep() {}, syncRenderSetup() {},
|
|
266
|
+
syncAppStep() {}, syncRenderSetup() {}, paintSetup() {}, renderLook() {}, renderRecap() {},
|
|
222
267
|
};
|
|
223
268
|
sandbox.globalThis = sandbox;
|
|
224
269
|
vm.createContext(sandbox);
|
|
225
270
|
vm.runInContext(`${showSlice}\nshow(${step}, false);`, sandbox);
|
|
226
|
-
return {
|
|
271
|
+
return {
|
|
272
|
+
on: Object.keys(els).filter((id) => els[id].classList.set.has('on')),
|
|
273
|
+
screen: els.wizRoom.attrs['data-screen'], eyebrow: els.wizEyebrow.textContent,
|
|
274
|
+
back: !els.wizBack.hidden, tag: !els.demoTag.hidden,
|
|
275
|
+
};
|
|
227
276
|
}
|
|
228
277
|
|
|
229
|
-
test('show()
|
|
230
|
-
assert.ok(showSlice, 'show() was not found in projects.html');
|
|
231
|
-
|
|
232
|
-
assert.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
278
|
+
test('show() turns one screen on, tells the room which screen it is, and names it in the eyebrow', () => {
|
|
279
|
+
assert.ok(showSlice, 'the SCREEN map through show() was not found in projects.html');
|
|
280
|
+
assert.match(showSlice, /function eyebrowFor\(step\)/, 'the eyebrow is part of the slice');
|
|
281
|
+
assert.doesNotMatch(showSlice, /paintRail|wizFront|wizFork|wizDone/, 'show() paints no rail and toggles no front-door or done layout (task 1004504)');
|
|
282
|
+
const screens = { 0: 'signin', 1: 'fork', 2: 'what', 3: 'checks', 4: 'review', 5: 'look', 6: 'done' };
|
|
283
|
+
for (const [n, screen] of Object.entries(screens)) {
|
|
284
|
+
const r = runShow(Number(n));
|
|
285
|
+
const id = Object.keys(MAP).find((k) => MAP[k] === Number(n));
|
|
286
|
+
assert.deepEqual(r.on, [id], `step ${n}: only ${id} is on`);
|
|
287
|
+
assert.equal(r.screen, screen, `step ${n}: the room grows to "${screen}"`);
|
|
288
|
+
}
|
|
289
|
+
assert.equal(runShow(1).eyebrow, 'New project');
|
|
290
|
+
assert.equal(runShow(1, { demoOpen: true }).eyebrow, 'Demo', 'the demo\'s own screen says so');
|
|
291
|
+
assert.equal(runShow(2, { demo: { people: 3 } }).eyebrow, 'New demo', 'and a demo past the fork stays marked');
|
|
292
|
+
assert.equal(runShow(6).eyebrow, 'Founding');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('one Back: hidden at sign-in, on the fork and on done; shown on every screen between', () => {
|
|
296
|
+
assert.equal(runShow(0).back, false, 'sign-in has nowhere to go back to');
|
|
297
|
+
assert.equal(runShow(1).back, false, 'the fork is the first screen');
|
|
298
|
+
assert.equal(runShow(1, { demoOpen: true }).back, true, 'the demo screen walks back to the fork');
|
|
299
|
+
for (const n of [2, 3, 4, 5]) assert.equal(runShow(n).back, true, `step ${n} walks back`);
|
|
300
|
+
assert.equal(runShow(6).back, false, 'a created project is not walked back into the wizard');
|
|
301
|
+
assert.equal(runShow(2).tag, false, 'no demo tag on a real project');
|
|
302
|
+
assert.equal(runShow(2, { demo: { people: 3 } }).tag, true, 'the demo tag rides every screen of a demo');
|
|
303
|
+
assert.match(SCRIPT, /function prevStep\(step\) \{\s*if \(step === 4 && state\.demo\) return 2;/, 'a demo, which skipped the checks, walks back past them too');
|
|
238
304
|
});
|
|
239
305
|
|
|
240
306
|
// ── the create ──────────────────────────────────────────────────────────────────
|
|
@@ -243,29 +309,33 @@ test('the create carries the tick, and is never filed without it', () => {
|
|
|
243
309
|
assert.match(create, /terms_acknowledged: state\.termsAck \? true : undefined,/,
|
|
244
310
|
'a real boolean when ticked, absent otherwise — the route refuses anything but a boolean');
|
|
245
311
|
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
|
|
312
|
+
assert.ok(guard > -1, 'an unticked draft is sent back to the fork\'s tick');
|
|
247
313
|
assert.ok(guard < create.indexOf('ensureFreshRepo()'), 'BEFORE anything is created — the repo included');
|
|
248
314
|
});
|
|
249
315
|
|
|
250
316
|
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,
|
|
317
|
+
assert.match(SCRIPT, /var state = \{ step: 0,[^}]*termsAck: false, confirmed: \{\} \};/, 'the initial state declares it');
|
|
318
|
+
assert.match(SCRIPT, /state = \{ step: 1,[^}]*termsAck: false, confirmed: \{\} \};/, 'and "start another project" clears it — each project records its own');
|
|
319
|
+
assert.match(SCRIPT, /\$\('termsAck'\)\.checked = false; paintDoors\(\);/, 'and the reset shuts the doors with it');
|
|
320
|
+
assert.match(SCRIPT, /var DRAFT_KEY = 'cb-create-draft-v9';/, 'a draft saved under the old numbering is ignored, not replayed onto the wrong screen');
|
|
254
321
|
});
|
|
255
322
|
|
|
256
323
|
// ── the look-before-you-ship kit walks it ─────────────────────────────────────
|
|
257
|
-
|
|
324
|
+
// Every walk past the fork opens the way a founder does: the tick, then a door — Start my
|
|
325
|
+
// project, or the demo door and the demo screen's Continue.
|
|
326
|
+
test('every kit walk past the fork ticks first, then takes a door', () => {
|
|
258
327
|
const states = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.states.json'), 'utf8')).states;
|
|
259
|
-
|
|
260
|
-
assert.
|
|
261
|
-
const
|
|
328
|
+
assert.ok(states['in-new'], 'the in-new state exists');
|
|
329
|
+
for (const sel of ['#panel-fork', '#termsAck']) assert.ok((states['in-new'].expect.visible || []).includes(sel), `arriving at ?view=new pictures the fork and its tick (${sel})`);
|
|
330
|
+
const REAL = ['#termsAck', '#wizStart'];
|
|
331
|
+
const DEMO = ['#termsAck', '#wizDemo', '#demoStart'];
|
|
262
332
|
const probes = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.probes.json'), 'utf8')).probes;
|
|
263
333
|
const walks = [...Object.entries(states).map(([k, v]) => [k, v.actions || []]), ...probes.map((p) => [p.name, p.steps])]
|
|
264
334
|
.filter(([, steps]) => steps.some((st) => st[1] === '#newName'));
|
|
265
335
|
assert.ok(walks.length >= 12, `every wizard walk is found (${walks.length})`);
|
|
266
336
|
for (const [name, steps] of walks) {
|
|
267
337
|
const clicks = steps.filter((st) => st[0] === 'click').map((st) => st[1]);
|
|
268
|
-
|
|
338
|
+
const door = clicks[1] === '#wizDemo' ? DEMO : REAL;
|
|
339
|
+
assert.deepEqual(clicks.slice(0, door.length), door, `${name}: the tick, then a door, come first`);
|
|
269
340
|
}
|
|
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
341
|
});
|
|
@@ -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(6) 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-5 are wizard steps since task 1004504's six blended screens (1-11 before them, 1-10
|
|
24
|
+
// before task 1004421's Look step, 1-8 before task 1004417's front door took 1 and 2);
|
|
24
25
|
// anything else is the done panel's own bookkeeping.
|
|
25
26
|
//
|
|
26
27
|
// Harness: there is no jsdom in this repo, so this follows tests/wizard_draft_resume.mjs
|
|
@@ -58,8 +59,8 @@ test('both slices are still findable (a rename must fail loudly, not silently pa
|
|
|
58
59
|
assert.ok(offerSlice, 'paintResumeOffer() was not found in projects.html');
|
|
59
60
|
assert.match(enterSlice, /if \(focus && !resumeIntent\) \{/, 'the intent branch is part of the slice');
|
|
60
61
|
assert.match(enterSlice, /startProgress\(created\.id\);/, 'so is the resume branch it guards');
|
|
61
|
-
assert.match(enterSlice, /draft\.step >= 1 && draft\.step <=
|
|
62
|
-
'the STORED draft\'s step decides, not state.step — state.step is
|
|
62
|
+
assert.match(enterSlice, /draft\.step >= 1 && draft\.step <= 5 \?/,
|
|
63
|
+
'the STORED draft\'s step decides, not state.step — state.step is 6 for the whole life of a finished standup');
|
|
63
64
|
assert.match(SCRIPT, /if \(created && created\.id\) \{ enterCreated\(created, !!focus\); return; \}/,
|
|
64
65
|
'enter() still hands the created record straight to it');
|
|
65
66
|
});
|
|
@@ -73,7 +74,7 @@ function runEnter({ focus, resumeIntent = false, draft = null, record }) {
|
|
|
73
74
|
const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0 };
|
|
74
75
|
// state.step is the DONE step for the whole life of a finished standup — the value
|
|
75
76
|
// a draft-blind reading would have trusted
|
|
76
|
-
const state = { step: draft ? draft.step :
|
|
77
|
+
const state = { step: draft ? draft.step : 6, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
|
|
77
78
|
const sandbox = {
|
|
78
79
|
record, focus, resumeIntent, state,
|
|
79
80
|
DRAFT_KEY: 'draft',
|
|
@@ -99,7 +100,7 @@ const REC = { id: 'inst-1', name: 'Acme', slug: 'acme', domain: 'acme.example',
|
|
|
99
100
|
|
|
100
101
|
test('a reload / Back / identity settle (focus false) still resumes the done panel, watch and all', () => {
|
|
101
102
|
const { calls, state } = runEnter({ focus: false, record: REC });
|
|
102
|
-
assert.deepEqual(calls.show, [[
|
|
103
|
+
assert.deepEqual(calls.show, [[6, false]], 'the done panel, and no focus steal on a non-deliberate arrival');
|
|
103
104
|
assert.equal(calls.resets, 0, 'nothing is reset — this is the same standup continuing');
|
|
104
105
|
assert.equal(calls.done, 1, 'the checklist is re-rendered');
|
|
105
106
|
assert.deepEqual(calls.progress, ['inst-1'], 'the progress watch is restarted from the record');
|
|
@@ -114,7 +115,7 @@ test('a deliberate "New project" click after a finished standup opens a FRESH st
|
|
|
114
115
|
assert.equal(calls.progress.length, 0, 'and its watch is not restarted');
|
|
115
116
|
assert.equal(calls.resets, 1, 'with no second draft to lose, the wizard forgets the finished project');
|
|
116
117
|
assert.deepEqual(calls.show, [[1, false]], 'step 1, and step 1 does not take the heading focus');
|
|
117
|
-
assert.equal(calls.focused, 1, 'its
|
|
118
|
+
assert.equal(calls.focused, 1, 'its first control, the fork\'s terms tick, does — the deliberate-nav rule enter() already applies');
|
|
118
119
|
assert.equal(state.name, '', 'the old name is gone from the form');
|
|
119
120
|
});
|
|
120
121
|
|
|
@@ -127,7 +128,7 @@ test('…and the standup stays one click away, named, on the resume link', () =>
|
|
|
127
128
|
|
|
128
129
|
test('the resume link gets the done panel back — the record was never cleared', () => {
|
|
129
130
|
const { calls, sandbox } = runEnter({ focus: true, resumeIntent: true, record: REC });
|
|
130
|
-
assert.deepEqual(calls.show, [[
|
|
131
|
+
assert.deepEqual(calls.show, [[6, true]], 'a deliberate resume moves focus to the done panel');
|
|
131
132
|
assert.deepEqual(calls.progress, ['inst-1'], 'the watch comes back with it');
|
|
132
133
|
assert.equal(sandbox.resumeIntent, false, 'the flag is ONE-SHOT — the next plain nav click must mean "new" again');
|
|
133
134
|
assert.deepEqual(calls.offer, [null], 'and the offer comes down behind it');
|
|
@@ -137,29 +138,30 @@ test('a second draft already under way is NOT wiped by navigating back to the wi
|
|
|
137
138
|
// The regression this guards: with the reset unconditional, typing a name, stepping
|
|
138
139
|
// away to "Your projects" and clicking "New project" again would blank the draft —
|
|
139
140
|
// trading the reported bug for a worse one.
|
|
140
|
-
const { calls, state } = runEnter({ focus: true, record: REC, draft: { step:
|
|
141
|
+
const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 3 } });
|
|
141
142
|
assert.equal(calls.resets, 0, 'a live draft is never reset');
|
|
142
|
-
assert.deepEqual(calls.show, [[
|
|
143
|
-
assert.equal(state.step,
|
|
143
|
+
assert.deepEqual(calls.show, [[3, true]], 'the second draft resumes where it was left, heading focused');
|
|
144
|
+
assert.equal(state.step, 3);
|
|
144
145
|
assert.deepEqual(calls.offer, ['Acme'], 'the offer stays up beside it');
|
|
145
146
|
});
|
|
146
147
|
|
|
147
148
|
test('the done panel\'s own saved step is NOT a second draft — it must not block the reset', () => {
|
|
148
|
-
// Caught in a render, not in a unit test: show(
|
|
149
|
+
// Caught in a render, not in a unit test: show(6) calls saveDraft(), so DRAFT_KEY is
|
|
149
150
|
// non-empty again the moment a create lands. Reading "is there a draft?" left the
|
|
150
151
|
// finished project's name in the fresh form, under a line offering to resume it.
|
|
151
|
-
const { calls, state } = runEnter({ focus: true, record: REC, draft: { step:
|
|
152
|
+
const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 6, name: 'Mercury' } });
|
|
152
153
|
assert.equal(calls.resets, 1, 'the done step is bookkeeping, not a project being typed');
|
|
153
154
|
assert.deepEqual(calls.show, [[1, false]], 'out-of-range steps land on step 1, the front door');
|
|
154
155
|
assert.equal(state.name, '', 'and the finished project\'s name does not survive into it');
|
|
155
156
|
assert.equal(calls.done, 0, 'never back onto the done panel');
|
|
156
157
|
});
|
|
157
158
|
|
|
158
|
-
test('a draft on the
|
|
159
|
-
//
|
|
160
|
-
// Look step (task 1004421)
|
|
161
|
-
//
|
|
162
|
-
|
|
159
|
+
test('a draft on the checks, the review or the look is live too — the last three screens (tasks 1004351, 1004504)', () => {
|
|
160
|
+
// Every renumbering has moved the review: 7 to 8 for the app step, 10 for the front door
|
|
161
|
+
// (task 1004417), 11 for the Look step (task 1004421), 4 for the six blended screens
|
|
162
|
+
// (task 1004504). A guard left behind would reset a second project sitting on its
|
|
163
|
+
// review, silently wiping it.
|
|
164
|
+
for (const step of [3, 4, 5]) {
|
|
163
165
|
const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
|
|
164
166
|
assert.equal(calls.resets, 0, `step ${step} is a live draft`);
|
|
165
167
|
assert.deepEqual(calls.show, [[step, true]]);
|
|
@@ -167,7 +169,7 @@ test('a draft on the app step, the Look step or the review is live too — the l
|
|
|
167
169
|
}
|
|
168
170
|
});
|
|
169
171
|
|
|
170
|
-
test('a draft on the
|
|
172
|
+
test('a draft on the fork or the first question is live too — a founder part-way through keeps their place (tasks 1004417, 1004504)', () => {
|
|
171
173
|
for (const step of [1, 2]) {
|
|
172
174
|
const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
|
|
173
175
|
assert.equal(calls.resets, 0, `step ${step} is a live draft`);
|
|
@@ -175,8 +177,8 @@ test('a draft on the front door is live too — a founder part-way through it ke
|
|
|
175
177
|
}
|
|
176
178
|
});
|
|
177
179
|
|
|
178
|
-
test('a draft whose step is missing or junk resets too — only 1-
|
|
179
|
-
for (const draft of [{ step: 0 }, { step:
|
|
180
|
+
test('a draft whose step is missing or junk resets too — only 1-5 preserves anything', () => {
|
|
181
|
+
for (const draft of [{ step: 0 }, { step: 6 }, { name: 'half-typed' }]) {
|
|
180
182
|
const { calls } = runEnter({ focus: true, record: REC, draft });
|
|
181
183
|
assert.equal(calls.resets, 1, `step ${JSON.stringify(draft.step)} is not a wizard step`);
|
|
182
184
|
assert.deepEqual(calls.show, [[1, false]]);
|
|
@@ -201,13 +203,14 @@ test('paintResumeOffer names the project by textContent and hides on null (no li
|
|
|
201
203
|
});
|
|
202
204
|
|
|
203
205
|
test('the offer ships as real markup inside step 1, hidden until there is something to resume', () => {
|
|
204
|
-
// step 1 is the
|
|
205
|
-
const panel = HUB.slice(HUB.indexOf('id="panel-
|
|
206
|
+
// step 1 is the fork since task 1004504 (the terms are its tick) — the offer lives there
|
|
207
|
+
const panel = HUB.slice(HUB.indexOf('id="panel-fork"'), HUB.indexOf('id="panel-what"'));
|
|
208
|
+
assert.ok(HUB.indexOf('id="panel-fork"') > -1 && HUB.indexOf('id="panel-what"') > HUB.indexOf('id="panel-fork"'), 'step 1 is findable');
|
|
206
209
|
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>/,
|
|
207
210
|
'baked into the panel (C7), carrying its own hidden state');
|
|
208
211
|
assert.ok(panel.indexOf('id="wizResume"') < panel.indexOf('<form data-next="2">'),
|
|
209
|
-
'it sits above the
|
|
210
|
-
assert.
|
|
212
|
+
'it sits above the fork\'s form, where a visitor reads it before anything else');
|
|
213
|
+
assert.equal((HUB.match(/id="wizResume"/g) || []).length, 1,
|
|
211
214
|
'and only once — a second copy would be a duplicate id');
|
|
212
215
|
assert.match(SCRIPT, /\$\('wizResumeGo'\)\.addEventListener\('click', function \(ev\) \{\n\s*ev\.preventDefault\(\);\n\s*resumeIntent = true;\n\s*enter\(true\);/,
|
|
213
216
|
'the link raises the one-shot intent and re-enters, rather than href-navigating');
|