@bongos/core 1.19.865 → 1.19.866
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 +31 -21
- package/docs/copy-inventory.md +94 -89
- package/docs/copy-registry.json +154 -108
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/builders.js +2 -2
- package/modules/hall-ui/public/hall-render.js +50 -4
- package/modules/hall-ui/public/people.css +9 -0
- package/modules/hall-ui/public/roster.js +11 -1
- package/modules/hall-ui/public/style.css +28 -0
- package/modules/lifecycle/routes/tasks.js +33 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/acclaim_feed_evidence_db.mjs +186 -0
- package/tests/hall_builders_page_pass.mjs +324 -0
- package/tests/joinability_invite_only.mjs +11 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// tests/hall_builders_page_pass.mjs — the three parts of task 1004004, each
|
|
2
|
+
// pinned by RUNNING the shipped renderer rather than reading it.
|
|
3
|
+
//
|
|
4
|
+
// The three were filed separately (1002851, 1002837, 1002835) and consolidated
|
|
5
|
+
// because their touches[] overlap serialised them anyway. What they share is one
|
|
6
|
+
// failure mode: a fact the hall already holds was rendered as a footnote, as
|
|
7
|
+
// nothing at all, or not rendered at all, so the reader had to go and look it up
|
|
8
|
+
// somewhere else.
|
|
9
|
+
//
|
|
10
|
+
// A (1002851) — the signed-out landing. Signing in was a footnote LINK, but
|
|
11
|
+
// only on the `apply` door; `open` and `invite_only` already render a real
|
|
12
|
+
// .landing__btn inside the primary door. So the button belongs on `apply`
|
|
13
|
+
// alone — adding one to all three would have made a third scattered path.
|
|
14
|
+
// These tests hold BOTH halves: the button appears on `apply`, and the two
|
|
15
|
+
// doors that already had one do NOT grow a second.
|
|
16
|
+
// B (1002837) — the roster's place cell. A builder's number is the id every
|
|
17
|
+
// task row and ship note cites, and the roster was the one place that knew
|
|
18
|
+
// who the builder was and didn't print it. Degrades to the place alone when
|
|
19
|
+
// the payload carries no id.
|
|
20
|
+
// C (1002835) — the peer-acclaim feed. A karma vote was cast on a title. The
|
|
21
|
+
// grade and the cost now ride along, and ABSENCE RENDERS AS ABSENCE: an
|
|
22
|
+
// ungraded ship says "not graded" and an unlogged cost says "no cost
|
|
23
|
+
// logged", because 0.0 and $0.00 are measurements that were never taken.
|
|
24
|
+
//
|
|
25
|
+
// Pure vm + file-read (no DB, no network) — runs in the DB-free unit gate.
|
|
26
|
+
import test from 'node:test';
|
|
27
|
+
import assert from 'node:assert/strict';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import vm from 'node:vm';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
|
|
33
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
|
+
const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
35
|
+
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
|
|
36
|
+
|
|
37
|
+
const escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => (
|
|
38
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
39
|
+
));
|
|
40
|
+
|
|
41
|
+
// Count real sign-in CONTROLS (a .landing__btn), as opposed to prose links.
|
|
42
|
+
const countButtons = (html) => (html.match(/class="[^"]*landing__btn[^"]*"/g) || []).length;
|
|
43
|
+
|
|
44
|
+
// =========================================================================
|
|
45
|
+
// PART A — the returning builder's door (task 1002851)
|
|
46
|
+
// =========================================================================
|
|
47
|
+
|
|
48
|
+
// Render the signed-out landing for one joinability mode and hand back the HTML
|
|
49
|
+
// the page actually wrote. Runs the shipped hall-render.js in a DOM stub thin
|
|
50
|
+
// enough that anything renderLanding touches beyond the landing root would
|
|
51
|
+
// throw rather than pass silently.
|
|
52
|
+
function hallRender(joinability) {
|
|
53
|
+
let written = '';
|
|
54
|
+
const el = (extra = {}) => ({
|
|
55
|
+
hidden: false,
|
|
56
|
+
textContent: '',
|
|
57
|
+
contains() { return false; },
|
|
58
|
+
addEventListener() {},
|
|
59
|
+
set innerHTML(v) { written = v; },
|
|
60
|
+
get innerHTML() { return written; },
|
|
61
|
+
...extra,
|
|
62
|
+
});
|
|
63
|
+
const nodes = { '#landing-root': el(), '#welcome-line': el() };
|
|
64
|
+
|
|
65
|
+
const sandbox = {
|
|
66
|
+
window: {
|
|
67
|
+
OTB: {
|
|
68
|
+
escapeHtml,
|
|
69
|
+
$: (sel) => nodes[sel] || null,
|
|
70
|
+
fmtNum: (n) => String(n),
|
|
71
|
+
moduleOn: () => true,
|
|
72
|
+
},
|
|
73
|
+
OTBShell: { applyAccess() {} },
|
|
74
|
+
// hall-render builds its pagers at factory time; the landing uses none.
|
|
75
|
+
OTBKit: { makePager: () => ({ page: 1 }), pagerBarHtml: () => '' },
|
|
76
|
+
__BRANDING__: joinability ? { project: { joinability } } : {},
|
|
77
|
+
},
|
|
78
|
+
document: { querySelectorAll: () => [] },
|
|
79
|
+
console,
|
|
80
|
+
};
|
|
81
|
+
vm.createContext(sandbox);
|
|
82
|
+
vm.runInContext(read('hall-render.js'), sandbox, { filename: 'hall-render.js' });
|
|
83
|
+
|
|
84
|
+
const hall = sandbox.window.OTBHallRender({
|
|
85
|
+
API: '/api/bongos',
|
|
86
|
+
RANK_ORDER: { xenos: 1, thetes: 2, metic: 3, archon: 4 },
|
|
87
|
+
api: { request: () => Promise.resolve({ status: 200, ok: true, data: {} }) },
|
|
88
|
+
brandWorld: () => 'Testworld',
|
|
89
|
+
curLabel: () => 'Credits',
|
|
90
|
+
emptyRow: () => '',
|
|
91
|
+
fmtDate: () => '',
|
|
92
|
+
mins: () => '',
|
|
93
|
+
normalizeRank: (r) => r,
|
|
94
|
+
pct: () => 0,
|
|
95
|
+
postJSON: () => Promise.resolve({}),
|
|
96
|
+
rankLabel: (r) => r,
|
|
97
|
+
rankWeight: () => 0,
|
|
98
|
+
signClasses: () => '',
|
|
99
|
+
toast: () => {},
|
|
100
|
+
usd: (n) => `$${n}`,
|
|
101
|
+
});
|
|
102
|
+
return { hall, html: () => written };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Render the signed-out landing for one joinability mode and hand back the
|
|
106
|
+
// HTML the page actually wrote.
|
|
107
|
+
function renderLandingHtml(joinability) {
|
|
108
|
+
const { hall, html } = hallRender(joinability);
|
|
109
|
+
hall.renderLanding();
|
|
110
|
+
return html();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
test('A: the apply door gives signing in a real button, not a footnote link', () => {
|
|
114
|
+
const html = renderLandingHtml('apply');
|
|
115
|
+
assert.match(html, /landing__returning/, 'the apply door renders a returning-builder block');
|
|
116
|
+
assert.match(
|
|
117
|
+
html,
|
|
118
|
+
/class="landing__btn landing__btn--quiet" href="\/api\/bongos\/auth\/web\/start"/,
|
|
119
|
+
'that block carries a real sign-in control in the quiet fill',
|
|
120
|
+
);
|
|
121
|
+
// The whole point of the change: the footnote link is GONE from this door, so
|
|
122
|
+
// there is exactly one sign-in path and it is the button.
|
|
123
|
+
assert.ok(
|
|
124
|
+
!/Already a member\? <a /.test(html),
|
|
125
|
+
'the apply door no longer repeats sign-in as a prose link under the form',
|
|
126
|
+
);
|
|
127
|
+
assert.match(html, /We store only your GitHub login/, 'the privacy sentence survives the note');
|
|
128
|
+
// Two controls total: request access, and sign in. Not three.
|
|
129
|
+
assert.equal(countButtons(html), 2, 'the apply door carries the request button and the sign-in button');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('A: the two doors that already had a sign-in button do not grow a second', () => {
|
|
133
|
+
for (const mode of ['open', 'invite_only']) {
|
|
134
|
+
const html = renderLandingHtml(mode);
|
|
135
|
+
assert.ok(
|
|
136
|
+
!/landing__returning/.test(html),
|
|
137
|
+
`the ${mode} door does not add a returning block — its primary door already signs you in`,
|
|
138
|
+
);
|
|
139
|
+
assert.equal(countButtons(html), 1, `the ${mode} door carries exactly one sign-in control`);
|
|
140
|
+
assert.match(html, /Already a member\? <a /, `the ${mode} door keeps its prose note for returning builders`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('A: an older branding pack with no joinability falls back to the apply door', () => {
|
|
145
|
+
// joinabilityMode() treats anything but open/invite_only as 'apply', so the
|
|
146
|
+
// button must follow that fallback — not sit behind a field an older pack
|
|
147
|
+
// never sent.
|
|
148
|
+
assert.match(renderLandingHtml(null), /landing__returning/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('A: the quiet fill is a defined variant, not an inline colour', () => {
|
|
152
|
+
const css = read('style.css');
|
|
153
|
+
const i = css.indexOf('\n.landing__btn--quiet {');
|
|
154
|
+
assert.ok(i > 0, 'style.css defines .landing__btn--quiet');
|
|
155
|
+
const block = css.slice(i, css.indexOf('}', i));
|
|
156
|
+
assert.deepEqual(block.match(/#[0-9a-f]{3,8}\b/gi) || [], [], 'the variant carries no literal colour');
|
|
157
|
+
// Same geometry as the request button means it inherits .landing__btn's
|
|
158
|
+
// min-height and padding rather than restating them.
|
|
159
|
+
assert.ok(!/min-height|padding/.test(block), 'the variant overrides fill only, never geometry');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// =========================================================================
|
|
163
|
+
// PART B — the builder's number on the roster (task 1002837)
|
|
164
|
+
// =========================================================================
|
|
165
|
+
|
|
166
|
+
// roster.js is an IIFE that fetches and mounts. Run it against a stubbed kit,
|
|
167
|
+
// capture the rowHtml it hands makeLedger, and call the real function.
|
|
168
|
+
async function rosterRowHtml(rows) {
|
|
169
|
+
let rowFn = null;
|
|
170
|
+
const elStub = { innerHTML: '', className: '', textContent: '', hidden: false };
|
|
171
|
+
const sandbox = {
|
|
172
|
+
window: {
|
|
173
|
+
OTB: { fmtInt: (n) => String(n == null ? 0 : n), rankLabel: (r) => r },
|
|
174
|
+
OTBBranding: { currencyLabel: () => 'Credits' },
|
|
175
|
+
OTBKit: {
|
|
176
|
+
esc: escapeHtml,
|
|
177
|
+
makePager: () => ({ page: 1 }),
|
|
178
|
+
makeFilterBar: () => {},
|
|
179
|
+
makeLedger: (cfg) => { rowFn = cfg.rowHtml; return { render() {} }; },
|
|
180
|
+
paginate: (xs) => xs,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
document: { getElementById: () => ({ ...elStub }) },
|
|
184
|
+
fetch: () => Promise.resolve({ ok: true, json: () => Promise.resolve({ leaderboard: rows }) }),
|
|
185
|
+
console,
|
|
186
|
+
};
|
|
187
|
+
vm.createContext(sandbox);
|
|
188
|
+
vm.runInContext(read('roster.js'), sandbox, { filename: 'roster.js' });
|
|
189
|
+
// Let the fetch promise chain drain.
|
|
190
|
+
await new Promise((r) => setImmediate(r));
|
|
191
|
+
assert.ok(rowFn, 'roster.js mounted a ledger and handed it a row renderer');
|
|
192
|
+
return rowFn;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const ROSTER_ROWS = [
|
|
196
|
+
{ id: '1000025', github_login: 'someone', display_name: 'Someone', rank: 'metic', total_credits: 900, karma: 5, tokens_used: 10 },
|
|
197
|
+
{ github_login: 'nameless', display_name: 'Nameless', rank: 'xenos', total_credits: 10, karma: 0, tokens_used: 1 },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
test('B: the roster prints the builder\'s number under their place', async () => {
|
|
201
|
+
const rowHtml = await rosterRowHtml(ROSTER_ROWS);
|
|
202
|
+
const html = rowHtml({ ...ROSTER_ROWS[0], __place: 1 });
|
|
203
|
+
assert.match(html, /<span class="ledger__id">#1000025<\/span>/, 'the id renders');
|
|
204
|
+
// Under the place, not beside the name — the place cell is what answers
|
|
205
|
+
// "which builder is this row".
|
|
206
|
+
const cell = html.slice(0, html.indexOf('</td>'));
|
|
207
|
+
assert.match(cell, /ledger__place/, 'the place cell is the one that carries it');
|
|
208
|
+
assert.match(cell, /1[\s\S]*ledger__id/, 'the place still comes first');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('B: a payload with no id degrades to the place alone', async () => {
|
|
212
|
+
const rowHtml = await rosterRowHtml(ROSTER_ROWS);
|
|
213
|
+
const html = rowHtml({ ...ROSTER_ROWS[1], __place: 2 });
|
|
214
|
+
assert.ok(!/ledger__id/.test(html), 'no empty "#" is printed for a row the server sent no id for');
|
|
215
|
+
assert.match(html, /<td class="ledger__num ledger__place">2<\/td>/, 'the place renders on its own');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('B: the id wears the same mono/faint voice as the login', () => {
|
|
219
|
+
const css = read('people.css');
|
|
220
|
+
const i = css.indexOf('.ledger__place .ledger__id {');
|
|
221
|
+
assert.ok(i > 0, 'people.css styles the roster id');
|
|
222
|
+
const block = css.slice(i, css.indexOf('}', i));
|
|
223
|
+
for (const token of ['var(--font-mono)', 'var(--ink-faint)']) {
|
|
224
|
+
assert.ok(block.includes(token), `the id spends ${token}, the voice .dir-login already uses`);
|
|
225
|
+
}
|
|
226
|
+
assert.match(block, /display: block/, 'it sits under the place rather than wrapping beside it');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// =========================================================================
|
|
230
|
+
// PART C — grade and cost on the peer-acclaim feed (task 1002835)
|
|
231
|
+
// =========================================================================
|
|
232
|
+
|
|
233
|
+
// acclaimEvidenceHtml is a view painter, so it lives in the hall-render carve
|
|
234
|
+
// (the same reason renderLanding does) and builders.js takes it from the
|
|
235
|
+
// factory. Read it off the same factory rather than re-deriving it.
|
|
236
|
+
const acclaimEvidence = () => hallRender().hall.acclaimEvidenceHtml;
|
|
237
|
+
test('C: a graded, costed ship carries both numbers', () => {
|
|
238
|
+
const html = acclaimEvidence()({ grade_score: '8.5', grade_passed: true, cost_usd: '12.3456' });
|
|
239
|
+
assert.match(html, /ledger__verdict--ok">8\.5 \/ 10</, 'a passing grade reads as passing');
|
|
240
|
+
assert.match(html, /acclaim__cost">\$12\.35</, 'the cost rounds to cents');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('C: a failing grade is not dressed as a passing one', () => {
|
|
244
|
+
const html = acclaimEvidence()({ grade_score: '3.0', grade_passed: false, cost_usd: '1' });
|
|
245
|
+
assert.match(html, /ledger__verdict--danger">3\.0 \/ 10</);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('C: absence renders as absence, never as zero', () => {
|
|
249
|
+
const ev = acclaimEvidence();
|
|
250
|
+
// The case that matters: a grader outage arrives as a NULL score (the route
|
|
251
|
+
// nulls it through gradeQualityWhere), and a voter must not read "0.0 / 10".
|
|
252
|
+
const ungraded = ev({ grade_score: null, grade_passed: null, cost_usd: '4' });
|
|
253
|
+
assert.match(ungraded, /ledger__verdict--none">not graded</);
|
|
254
|
+
assert.ok(!/0\.0 \/ 10/.test(ungraded), 'an ungraded ship is never scored 0.0');
|
|
255
|
+
|
|
256
|
+
// cost_usd PRESENT and null is the "logged nothing" case, distinct from the
|
|
257
|
+
// key being absent entirely (covered above) — both must not print $0.00.
|
|
258
|
+
const uncosted = ev({ grade_score: '9.0', grade_passed: true, cost_usd: null });
|
|
259
|
+
assert.match(uncosted, /ledger__verdict--none">no cost logged</);
|
|
260
|
+
assert.ok(!/\$0\.00/.test(uncosted), 'an unlogged cost is never printed as free');
|
|
261
|
+
|
|
262
|
+
// A genuine zero is still a measurement and must survive.
|
|
263
|
+
assert.match(ev({ grade_score: '0.0', grade_passed: false, cost_usd: '0' }), /0\.0 \/ 10/);
|
|
264
|
+
assert.match(ev({ grade_score: '0.0', grade_passed: false, cost_usd: '0' }), /\$0\.00/);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('C: the evidence line hangs under the work, in the feed row', () => {
|
|
268
|
+
const src = read('builders.js');
|
|
269
|
+
assert.match(
|
|
270
|
+
src,
|
|
271
|
+
/<td>\$\{title\}\$\{acclaimEvidenceHtml\(t\)\}<\/td>/,
|
|
272
|
+
'the row renders it beneath the title rather than adding a column',
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('C: a builder who may not vote is shown no cost figure at all', () => {
|
|
277
|
+
// The route OMITS cost_usd below Metic rather than nulling it, because a null
|
|
278
|
+
// means "nothing was logged" — telling a reader a figure is missing is a
|
|
279
|
+
// different (and false) statement from showing them no figure.
|
|
280
|
+
const html = acclaimEvidence()({ grade_score: '8.0', grade_passed: true });
|
|
281
|
+
assert.match(html, /8\.0 \/ 10/, 'the grade is a quality verdict and still shows');
|
|
282
|
+
assert.ok(!/no cost logged/.test(html), 'a withheld cost is never reported as an unlogged one');
|
|
283
|
+
assert.ok(!/acclaim__cost/.test(html), 'and no figure is printed');
|
|
284
|
+
assert.ok(!/ · /.test(html), 'the separator goes with the part it separated');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('C: the route hands the cost only to the ranks that can vote', () => {
|
|
288
|
+
const route = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/tasks.js'), 'utf8');
|
|
289
|
+
const i = route.indexOf("router.get('/tasks/peer-votes/feed'");
|
|
290
|
+
const handler = route.slice(i, route.indexOf('// POST /tasks/peer-votes/tally', i));
|
|
291
|
+
assert.match(
|
|
292
|
+
handler,
|
|
293
|
+
/const tasks = canVote \? rows : rows\.map\(\(\{ cost_usd, \.\.\.rest \}\) => rest\);/,
|
|
294
|
+
'the cost is stripped for a non-voter, by omission rather than by nulling',
|
|
295
|
+
);
|
|
296
|
+
assert.match(handler, /res\.json\(\{ tasks, can_vote: canVote \}\)/, 'and the gated rows are what is sent');
|
|
297
|
+
assert.ok(!/grade_score, \.\.\.rest/.test(handler), 'the grade is not spend and is not gated with it');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('C: the route supplies both fields, and nulls a non-verdict grade', () => {
|
|
301
|
+
const route = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/tasks.js'), 'utf8');
|
|
302
|
+
const i = route.indexOf("router.get('/tasks/peer-votes/feed'");
|
|
303
|
+
assert.ok(i > 0, 'the feed route is still here');
|
|
304
|
+
const handler = route.slice(i, route.indexOf('// POST /tasks/peer-votes/tally', i));
|
|
305
|
+
for (const field of ['grade_score', 'grade_passed', 'cost_usd']) {
|
|
306
|
+
assert.ok(handler.includes(`AS ${field}`), `the feed selects ${field}`);
|
|
307
|
+
}
|
|
308
|
+
// The honesty half is server-side: a grader outage carries score 0 in
|
|
309
|
+
// task_grades, and only gradeQualityWhere tells it apart from a real 0.
|
|
310
|
+
assert.match(handler, /gradeQualityWhere\('g'\)/, 'an outage row is nulled, not passed through as 0');
|
|
311
|
+
// SUM over no rows is NULL, which is the "no cost logged" the client renders.
|
|
312
|
+
// A COALESCE here would turn every uncosted ship into a free one.
|
|
313
|
+
assert.ok(
|
|
314
|
+
!/COALESCE\(SUM\(amount_usd\)/.test(handler),
|
|
315
|
+
'the cost sum is left NULL when nothing was logged',
|
|
316
|
+
);
|
|
317
|
+
// Both joins are indexed lookups. Pinned here so a future reader does not have
|
|
318
|
+
// to re-derive it: task_grades.task_id is UNIQUE and cost_log.task_id carries
|
|
319
|
+
// idx_cost_log_task. If either ever stops being true, this reds.
|
|
320
|
+
const g016 = fs.readFileSync(path.join(ROOT, 'migrations/016_pms_task_grades.sql'), 'utf8');
|
|
321
|
+
assert.match(g016, /task_id\s+BIGINT NOT NULL UNIQUE/, 'task_grades.task_id is unique, so indexed');
|
|
322
|
+
const m003 = fs.readFileSync(path.join(ROOT, 'migrations/003_pms.sql'), 'utf8');
|
|
323
|
+
assert.match(m003, /CREATE INDEX IF NOT EXISTS idx_cost_log_task ON cost_log \(task_id\)/, 'cost_log.task_id is indexed');
|
|
324
|
+
});
|
|
@@ -177,7 +177,17 @@ test('the hall landing renders one door per mode, and its form collapses on the
|
|
|
177
177
|
assert.match(door, /mode === 'invite_only'/);
|
|
178
178
|
assert.match(door, /there is nothing to apply for here/);
|
|
179
179
|
assert.match(door, /id="access-request-form"/, 'the apply door keeps the request form');
|
|
180
|
-
|
|
180
|
+
// This read `${landingDoorHtml(joinabilityMode())}` inline. Task 1004004 gave
|
|
181
|
+
// the apply door a real sign-in button, so renderLanding resolves the mode
|
|
182
|
+
// ONCE and hands the same value to both doors. The invariant is unchanged and
|
|
183
|
+
// now reaches further: BOTH doors are painted from the pack's own answer, so
|
|
184
|
+
// neither can drift onto a second reading of joinability.
|
|
185
|
+
const landing = (hall.match(/function renderLanding\(\) \{[\s\S]*?\n \}\n/) || [''])[0];
|
|
186
|
+
assert.ok(landing, 'renderLanding() must exist');
|
|
187
|
+
assert.match(landing, /const mode = joinabilityMode\(\);/, 'the pack names the mode, once');
|
|
188
|
+
assert.match(landing, /\$\{landingDoorHtml\(mode\)\}/, 'renderLanding paints the door the pack names');
|
|
189
|
+
assert.match(landing, /\$\{returningDoorHtml\(mode\)\}/, 'and the returning-builder door off the same answer');
|
|
190
|
+
assert.equal((landing.match(/joinabilityMode\(\)/g) || []).length, 1, 'joinability is read once, not per door');
|
|
181
191
|
assert.match(hall, /code === 'invite_only' \|\| code === 'open_enrollment'/, 'the refusal codes are handled, not shown red');
|
|
182
192
|
});
|
|
183
193
|
|