@bongos/core 1.23.14 → 1.23.15
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 +77 -42
- 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 +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +22 -1
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +924 -878
- package/docs/copy-registry.json +1796 -1325
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +8 -3
- package/docs/page-readings.json +575 -555
- package/modules/platform-identity/builder-of-the-week.js +180 -0
- package/modules/platform-identity/migrations/platform_identity_032_weekly_features.sql +53 -0
- package/modules/platform-identity/routes/community-leaderboard.js +25 -1
- package/modules/public-landing/public/account.html +2 -1
- package/modules/public-landing/public/community-week-none.states.json +16 -0
- package/modules/public-landing/public/community.html +142 -13
- package/modules/public-landing/public/community.states.json +6 -4
- package/modules/public-landing/public/connections.html +2 -1
- package/modules/public-landing/public/contact.html +2 -1
- package/modules/public-landing/public/guild.html +2 -1
- package/modules/public-landing/public/index.html +2 -1
- package/modules/public-landing/public/invitations.html +2 -1
- package/modules/public-landing/public/privacy.html +2 -1
- package/modules/public-landing/public/projects.html +2 -1
- package/modules/public-landing/public/terms.html +2 -1
- package/modules/ui-design/kit/fixtures/community-week-none.json +6 -0
- package/modules/ui-design/kit/fixtures/community-week.json +11 -0
- package/modules/ui-design/kit/serve.js +8 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +5 -5
- package/tests/builder_of_the_week.mjs +266 -0
- package/tests/builder_of_the_week_db.mjs +166 -0
- package/tests/hub_account_page.mjs +2 -2
- package/tests/hub_community_page.mjs +136 -25
- package/tests/landing_legal_pages.mjs +2 -2
- package/tests/privacy_matrix.mjs +53 -8
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// tests/builder_of_the_week_db.mjs — task 1002981 (community spec D5; real DB).
|
|
2
|
+
//
|
|
3
|
+
// Proves Builder of the Week against real SQL: which snapshots open and close the
|
|
4
|
+
// week, the shipped-work floor, the tie order, the leaderboard's eligibility, that
|
|
5
|
+
// the first decision is pinned (a later change, a hide, a race) and that a week
|
|
6
|
+
// nobody won is recorded empty. A fake pool can only prove the statements' shape
|
|
7
|
+
// (tests/builder_of_the_week.mjs).
|
|
8
|
+
//
|
|
9
|
+
// The crown is global (every eligible builder on the hub), so every fixture week is
|
|
10
|
+
// in 2099: a shared database's real builders have no snapshot there, their week's
|
|
11
|
+
// delta is zero, and the shipped-work floor keeps them out.
|
|
12
|
+
//
|
|
13
|
+
// Real-DB integration test: self-skips without a reachable Postgres; in the
|
|
14
|
+
// INTEGRATION set (scripts/bongos/run-unit-tests.js) so the DB-free lane never runs it.
|
|
15
|
+
//
|
|
16
|
+
// Run: DATABASE_URL=... node tests/builder_of_the_week_db.mjs
|
|
17
|
+
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
import { strict as assert } from 'node:assert';
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const { Pool } = require('pg');
|
|
25
|
+
const { readBuilderOfTheWeek, crownedWeek, clearCrownMemo } = require('../modules/platform-identity/builder-of-the-week.js');
|
|
26
|
+
|
|
27
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
|
|
29
|
+
const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
|
|
30
|
+
|
|
31
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
32
|
+
try {
|
|
33
|
+
await pool.query('SELECT 1');
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.log(`builder_of_the_week_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
36
|
+
await pool.end().catch(() => {});
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const A = 90298101, B = 90298102, C = 90298103, D = 90298104;
|
|
41
|
+
const HIDDEN = 90298105, PRIVATE = 90298106, E = 90298107;
|
|
42
|
+
const IDS = [A, B, C, D, E, HIDDEN, PRIVATE];
|
|
43
|
+
const P1 = 't1002981-p1', P2 = 't1002981-p2';
|
|
44
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
45
|
+
const NOW = new Date('2099-03-12T10:00:00.000Z');
|
|
46
|
+
const { start, end } = crownedWeek(NOW);
|
|
47
|
+
const at = (base, ms) => new Date(base.getTime() + ms);
|
|
48
|
+
const q = (sql, params) => pool.query(sql, params);
|
|
49
|
+
|
|
50
|
+
async function cleanup() {
|
|
51
|
+
await q(`DELETE FROM platform_identity_weekly_features WHERE week_start >= '2099-01-01'`).catch(() => {});
|
|
52
|
+
await q(`DELETE FROM platform_identity_activity_snapshots WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
|
|
53
|
+
await q(`DELETE FROM platform_identity_builder_project_activity WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
|
|
54
|
+
await q(`DELETE FROM platform_identity_accounts WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
|
|
55
|
+
}
|
|
56
|
+
async function account(id, handle, { visibility = 'public', hide = false } = {}) {
|
|
57
|
+
await q(
|
|
58
|
+
`INSERT INTO platform_identity_accounts
|
|
59
|
+
(github_id, github_login, display_name, handle, profile_state, account_visibility, hide_stats, terms_accepted_at)
|
|
60
|
+
VALUES ($1, $2, $2, $2, 'active', $3, $4, now())`,
|
|
61
|
+
[id, handle, visibility, hide],
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
// The live row (its numbers do not matter to a past week; first seen long before the
|
|
65
|
+
// week unless said) and a history of snapshots.
|
|
66
|
+
async function builder(id, client, history, firstSeen = at(start, -365 * DAY)) {
|
|
67
|
+
await q(`INSERT INTO platform_identity_builder_project_activity (client_id, github_id, credits, tasks_shipped, karma, first_seen_at) VALUES ($1, $2, 0, 0, 0, $3)`, [client, id, firstSeen]);
|
|
68
|
+
for (const [when, credits, ships, karma] of history) {
|
|
69
|
+
await q(
|
|
70
|
+
`INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at) VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
71
|
+
[client, id, credits, ships, karma, when],
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const recorded = async (week) => (await q(
|
|
76
|
+
`SELECT github_id::text AS github_id, credits_delta::int AS credits, ships_delta::int AS ships FROM platform_identity_weekly_features WHERE week_start = $1::date`,
|
|
77
|
+
[week],
|
|
78
|
+
)).rows;
|
|
79
|
+
const read = (now = NOW) => { clearCrownMemo(); return readBuilderOfTheWeek({ now, pool }); };
|
|
80
|
+
|
|
81
|
+
let pass = 0;
|
|
82
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
|
|
83
|
+
const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg); pass++; console.log(` ok - ${msg}`); };
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
for (const f of [
|
|
87
|
+
'platform_identity_001_tables.sql', 'platform_identity_005_builder_activity.sql',
|
|
88
|
+
'platform_identity_007_profile_consent.sql', 'platform_identity_008_handle.sql',
|
|
89
|
+
'platform_identity_012_account_privacy.sql', 'platform_identity_019_terms_acceptance.sql',
|
|
90
|
+
'platform_identity_022_activity_snapshots.sql', 'platform_identity_032_weekly_features.sql',
|
|
91
|
+
]) await q(migrationSql(f));
|
|
92
|
+
await cleanup();
|
|
93
|
+
|
|
94
|
+
await account(A, 't1002981-a');
|
|
95
|
+
await account(B, 't1002981-b');
|
|
96
|
+
await account(C, 't1002981-c');
|
|
97
|
+
await account(D, 't1002981-d');
|
|
98
|
+
await account(E, 't1002981-e');
|
|
99
|
+
await account(HIDDEN, 't1002981-hidden', { hide: true });
|
|
100
|
+
await account(PRIVATE, 't1002981-private', { visibility: 'private' });
|
|
101
|
+
|
|
102
|
+
// A: 350 credits, 3 works, no karma — across two projects. P1 has a snapshot before
|
|
103
|
+
// the week (the baseline); P2 has none but was first seen in the week (counts from
|
|
104
|
+
// zero). A snapshot a second AFTER the week closes is not the week's.
|
|
105
|
+
await builder(A, P1, [[at(start, -3 * DAY), 100, 5, 4], [at(start, 2 * DAY), 400, 7, 4], [at(end, 1000), 99999, 99, 4]]);
|
|
106
|
+
await builder(A, P2, [[at(start, 3 * DAY), 50, 1, 0]], at(start, 3 * DAY));
|
|
107
|
+
// E: a lifetime brought into the week — no history before it, and seen before it (or
|
|
108
|
+
// before first_seen_at existed, NULL). Counting from zero would crown E; neither counts.
|
|
109
|
+
await builder(E, P1, [[at(start, DAY), 90000, 40, 0]], null);
|
|
110
|
+
await builder(E, P2, [[at(start, DAY), 90000, 40, 0]]);
|
|
111
|
+
// C: the same credits and works as A, two more karma — C beats A on karma.
|
|
112
|
+
await builder(C, P1, [[at(start, -DAY), 0, 0, 0], [at(start, DAY), 360, 3, 2]]);
|
|
113
|
+
// D: the same credits, the most karma, but one work fewer — works come before karma.
|
|
114
|
+
await builder(D, P1, [[at(start, -DAY), 0, 0, 0], [at(start, DAY), 350, 2, 9]]);
|
|
115
|
+
// B: the most credits, but no work shipped in the week (the floor).
|
|
116
|
+
await builder(B, P1, [[at(start, -DAY), 0, 4, 0], [at(start, DAY), 900, 4, 0]]);
|
|
117
|
+
// HIDDEN and PRIVATE out-earn everyone, and the leaderboard may not show them.
|
|
118
|
+
await builder(HIDDEN, P1, [[at(start, -DAY), 0, 0, 0], [at(start, DAY), 5000, 5, 0]]);
|
|
119
|
+
await builder(PRIVATE, P1, [[at(start, -DAY), 0, 0, 0], [at(start, DAY), 5000, 5, 0]]);
|
|
120
|
+
// A snapshot exactly at the start belongs to the baseline: C's week is 360 - 10, not 360.
|
|
121
|
+
await q(`INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at) VALUES ($1, $2, 10, 0, 0, $3)`, [P1, C, start]);
|
|
122
|
+
|
|
123
|
+
// 1 — the crown, and the row that records it
|
|
124
|
+
const first = await read();
|
|
125
|
+
same(first.week, { start: start.toISOString().slice(0, 10), end: end.toISOString().slice(0, 10) }, 'the week is the last full Monday-to-Monday week');
|
|
126
|
+
same(first.winner && [first.winner.handle, first.winner.credits, first.winner.tasks_shipped], ['t1002981-c', 350, 3], 'ties go to works shipped, then karma; no work, hidden, private and a lifetime brought into the week never win');
|
|
127
|
+
ok(!('github_id' in first.winner), 'github_id never crosses');
|
|
128
|
+
same(await recorded(first.week.start), [{ github_id: String(C), credits: 350, ships: 3 }], 'the week is recorded on the first read');
|
|
129
|
+
|
|
130
|
+
// 2 — pinned: a later report into the past week, or a builder who goes public, changes nothing
|
|
131
|
+
await q(`INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at) VALUES ($1, $2, 99999, 50, 0, $3)`, [P1, A, at(start, 4 * DAY)]);
|
|
132
|
+
await q(`UPDATE platform_identity_accounts SET hide_stats = false WHERE github_id = $1`, [HIDDEN]);
|
|
133
|
+
same((await read()).winner.handle, 't1002981-c', 'the first decision stands');
|
|
134
|
+
await q(`UPDATE platform_identity_accounts SET hide_stats = true WHERE github_id = $1`, [HIDDEN]);
|
|
135
|
+
|
|
136
|
+
// 3 — a winner who hides is not shown, and their week goes to nobody else
|
|
137
|
+
await q(`UPDATE platform_identity_accounts SET hide_stats = true WHERE github_id = $1`, [C]);
|
|
138
|
+
same((await read()).winner, null, 'a hidden winner is not drawn');
|
|
139
|
+
same(await recorded(first.week.start), [{ github_id: String(C), credits: 350, ships: 3 }], 'and the row is never re-crowned');
|
|
140
|
+
|
|
141
|
+
// 4 — a later week nobody shipped in (two on, past A's late snapshot) is recorded empty;
|
|
142
|
+
// the crowned week is in its past list once its winner is shown again
|
|
143
|
+
const next = at(NOW, 14 * DAY);
|
|
144
|
+
const empty = await read(next);
|
|
145
|
+
same(empty.winner, null, 'a week nobody won has no crown');
|
|
146
|
+
same(await recorded(empty.week.start), [{ github_id: null, credits: 0, ships: 0 }], 'and is recorded empty, so it stays empty');
|
|
147
|
+
same(empty.past, [], 'the week before is left out while its winner hides');
|
|
148
|
+
await q(`UPDATE platform_identity_accounts SET hide_stats = false WHERE github_id = $1`, [C]);
|
|
149
|
+
same((await read(next)).past.map((p) => [p.week_start, p.handle]), [[first.week.start, 't1002981-c']], 'shown again, it is back in the past weeks');
|
|
150
|
+
|
|
151
|
+
// 5 — two hub processes crowning the same week at once: one row, no error
|
|
152
|
+
const third = at(NOW, 21 * DAY);
|
|
153
|
+
const other = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
154
|
+
try {
|
|
155
|
+
clearCrownMemo();
|
|
156
|
+
const both = await Promise.all([readBuilderOfTheWeek({ now: third, pool }), readBuilderOfTheWeek({ now: third, pool: other })]);
|
|
157
|
+
same(both.map((b) => b.winner), [null, null], 'both answer');
|
|
158
|
+
same((await recorded(both[0].week.start)).length, 1, 'one row: ON CONFLICT DO NOTHING');
|
|
159
|
+
} finally { await other.end().catch(() => {}); }
|
|
160
|
+
|
|
161
|
+
console.log(`\nbuilder_of_the_week_db.mjs: ${pass} passed`);
|
|
162
|
+
} finally {
|
|
163
|
+
clearCrownMemo();
|
|
164
|
+
await cleanup();
|
|
165
|
+
await pool.end().catch(() => {});
|
|
166
|
+
}
|
|
@@ -135,8 +135,8 @@ test('the bands say what the hub’s say and lead into /projects; Account is the
|
|
|
135
135
|
assert.deepEqual(bandLinks(bandOf(PAGE, 'menuProjects')), [
|
|
136
136
|
['Sky', '/projects?view=map'], ['Table', '/projects?view=map&sub=table'],
|
|
137
137
|
['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
|
|
138
|
-
// the Community menu's
|
|
139
|
-
['Leaderboard', '/community'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
138
|
+
// the Community menu's four sub-pages, folded into the band under 480px (tasks 1003363, 1002981)
|
|
139
|
+
['Leaderboard', '/community'], ['Builder of the week', '/community/week'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
140
140
|
['Builders you may know', '/connections#connSuggest'],
|
|
141
141
|
], 'this page has no views of its own, so every item names /projects in full');
|
|
142
142
|
assert.deepEqual(bandLinks(bandOf(PAGE, 'menuProfile')), [
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// tests/hub_community_page.mjs — the hub's community pages (task 1003363; ADR 0217 D1,
|
|
2
2
|
// ADR 0337 D1/D3/D4.1, docs/specs/bongos-v1-community-tab.md D1/D3/D4/D8).
|
|
3
3
|
//
|
|
4
|
-
// One file served as
|
|
5
|
-
// /community the leaderboard, /community/
|
|
6
|
-
// the name-only list of hidden
|
|
7
|
-
// platform-identity reads and add no
|
|
4
|
+
// One file served as four sub-pages of the bar's Community menu (owner, 2026-10-27):
|
|
5
|
+
// /community the leaderboard, /community/week the builder of the week (task 1002981),
|
|
6
|
+
// /community/find the builder search, /community/recruiters the name-only list of hidden
|
|
7
|
+
// builders open to recruiters. They render existing platform-identity reads and add no
|
|
8
|
+
// rule of their own. What this file pins:
|
|
8
9
|
// • the markup: the hub's bar with the Community menu as the current section, no top
|
|
9
10
|
// block, noindex, dark only, the routes' own windows and query limits
|
|
10
11
|
// • viewing is ungated: the leaderboard and the search read the same routes for every
|
|
@@ -13,11 +14,12 @@
|
|
|
13
14
|
// item marked current; the board in the server's order with its `place`, the window
|
|
14
15
|
// dropdown and the crafts, the empty, failed and rate-limited board; the search and
|
|
15
16
|
// its refusals; the recruiter list as a door signed out, a refusal on a 403, and rows
|
|
16
|
-
// only from a 200 — name, face and login, never the ranked half's numbers
|
|
17
|
+
// only from a 200 — name, face and login, never the ranked half's numbers; the builder
|
|
18
|
+
// of the week as one line on the board and as its own page, drawn as the server sent it
|
|
17
19
|
// • the Community menu on every page that wears the hub's bar (apex-only where the page
|
|
18
20
|
// also serves a child), folded into the Projects band on phones
|
|
19
|
-
// • the routes: the
|
|
20
|
-
// • the kit: the states files, and the stub answering as the
|
|
21
|
+
// • the routes: the four paths served on the hub's apex only
|
|
22
|
+
// • the kit: the states files, and the stub answering as the four reads do
|
|
21
23
|
//
|
|
22
24
|
// DB-free. Run: node --test tests/hub_community_page.mjs
|
|
23
25
|
|
|
@@ -44,7 +46,8 @@ const STYLE = PAGE.split('<style>')[1].split('</style>')[0].replace(/\/\*[\s\S]*
|
|
|
44
46
|
const MAIN = PAGE.split('<main class="acct" id="comm">')[1].split('</main>')[0];
|
|
45
47
|
const BOARD_JS = fs.readFileSync(path.join(PI, 'community-leaderboard.js'), 'utf8');
|
|
46
48
|
const SEARCH_JS = fs.readFileSync(path.join(PI, 'builder-search.js'), 'utf8');
|
|
47
|
-
const SUBPAGES = [['/community', 'Leaderboard'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
|
|
49
|
+
const SUBPAGES = [['/community', 'Leaderboard'], ['/community/week', 'Builder of the week'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
|
|
50
|
+
const WEEK_JS = fs.readFileSync(path.join(PI, 'builder-of-the-week.js'), 'utf8');
|
|
48
51
|
|
|
49
52
|
// ── 1. the markup ────────────────────────────────────────────────────────────
|
|
50
53
|
|
|
@@ -72,7 +75,7 @@ test('the bar and bands are /connections’ but for the sign-in return and the c
|
|
|
72
75
|
|
|
73
76
|
test('no top block: one visually hidden h1, then one section per sub-page, only the leaderboard shown in the markup', () => {
|
|
74
77
|
assert.match(MAIN, /^\s*<h1 class="vh">Community<\/h1>\s*(<!--[\s\S]*?-->\s*)?<div class="mineExtra">/, 'nothing is drawn above the first section (the /design floor)');
|
|
75
|
-
const order = ['<section id="board" data-view="leaderboard" aria-labelledby="boardH">', '<section id="find" data-view="find" aria-labelledby="findH" hidden>', '<section id="recruit" data-view="recruiters" aria-labelledby="recruitH" hidden>'].map((s) => MAIN.indexOf(s));
|
|
78
|
+
const order = ['<section id="board" data-view="leaderboard" aria-labelledby="boardH">', '<section id="week" data-view="week" aria-labelledby="weekH" hidden>', '<section id="find" data-view="find" aria-labelledby="findH" hidden>', '<section id="recruit" data-view="recruiters" aria-labelledby="recruitH" hidden>'].map((s) => MAIN.indexOf(s));
|
|
76
79
|
assert.ok(order.every((i) => i > 0), 'every sub-page is real markup the script only shows');
|
|
77
80
|
assert.deepEqual([...order].sort((a, b) => a - b), order);
|
|
78
81
|
assert.match(MAIN, /<ol class="commRows" id="boardList" data-unit="\{\{currency\}\}" aria-labelledby="boardH"><\/ol>/, 'the unit is the instance’s own word, stamped by the server');
|
|
@@ -82,6 +85,23 @@ test('no top block: one visually hidden h1, then one section per sub-page, only
|
|
|
82
85
|
assert.match(MAIN, /<h2 id="recruitH">Open to recruiters<\/h2>/, 'the hide flag is "hidden", never "private" (ADR 0337)');
|
|
83
86
|
assert.match(MAIN, /<a class="btn" id="recruitSignin" href="\/api\/bongos\/auth\/web\/start\?return=\/community\/recruiters&repos=1">/);
|
|
84
87
|
for (const id of ['recruitDoor', 'recruitClosed', 'recruitFailed', 'recruitEmpty']) assert.match(MAIN, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
|
|
88
|
+
// the builder of the week (task 1002981): its line on the board and its page wait for the read
|
|
89
|
+
assert.match(MAIN, /<h2 id="boardH">Leaderboard<\/h2>\n\s*<p class="commCrownLine" id="boardCrown" hidden><\/p>/, 'the crown line sits under the board’s heading, above the rows');
|
|
90
|
+
for (const id of ['weekCrown', 'weekNone', 'weekFailed', 'pastH']) assert.match(MAIN, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
|
|
91
|
+
assert.match(MAIN, /<ol class="commRows weekRows" id="weekPast" aria-labelledby="pastH"><\/ol>/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('the builder of the week states its rule in full, and the rule is the module’s (spec D5: the formula is public)', () => {
|
|
95
|
+
const rule = MAIN.split('<h3 class="commSub" id="weekRuleH">How it is chosen</h3>')[1].split('<h3 ')[0];
|
|
96
|
+
assert.match(rule, /the most \{\{currency\}\} across every project in one week, Monday to Sunday \(UTC\)/, 'the instance’s own unit, the week the module counts');
|
|
97
|
+
assert.match(rule, /shipped at least one work that week/);
|
|
98
|
+
assert.match(WEEK_JS, /HAVING sum\(tasks_shipped\) >= 1/, 'the floor the page states is the one the query applies');
|
|
99
|
+
assert.match(rule, /A tie goes to whoever shipped more works, then to whoever earned more karma\./);
|
|
100
|
+
assert.match(WEEK_JS, /ORDER BY sum\(credits\) DESC, sum\(tasks_shipped\) DESC, sum\(karma\) DESC,/, 'the tie order the page states is the query’s');
|
|
101
|
+
assert.match(rule, /Builders who hide their numbers or keep their account private are not counted\./);
|
|
102
|
+
assert.match(WEEK_JS, /rollupVisibleSql\('pa'\)/);
|
|
103
|
+
assert.match(rule, /Each week is decided once, the first time anyone opens this page after it ends, and it does not change after that\./);
|
|
104
|
+
assert.match(WEEK_JS, /ON CONFLICT \(week_start\) DO NOTHING/);
|
|
85
105
|
});
|
|
86
106
|
|
|
87
107
|
test('the window dropdown and the query limits are the routes’ own (boundary-forced copies)', () => {
|
|
@@ -101,12 +121,12 @@ test('the window dropdown and the query limits are the routes’ own (boundary-f
|
|
|
101
121
|
assert.match(MAIN, new RegExp(`<input id="findQ" type="search"[^>]* maxlength="${max}"`));
|
|
102
122
|
});
|
|
103
123
|
|
|
104
|
-
test('the script builds no markup from strings, reads only the
|
|
124
|
+
test('the script builds no markup from strings, reads only the five routes, and never re-ranks or reads the ranked tier', () => {
|
|
105
125
|
assert.doesNotMatch(SCRIPT, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/);
|
|
106
|
-
// every path literal in the code: the API base, the
|
|
126
|
+
// every path literal in the code: the API base, the five reads, the four sub-pages, the
|
|
107
127
|
// sign-in door and the hall's invite box the Recruit door opens (task 1002292)
|
|
108
128
|
const paths = [...new Set([...CODE.matchAll(/'(\/[^'?]*)/g)].map((m) => m[1]))];
|
|
109
|
-
assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/find', '/community/leaderboard', '/community/recruiters', '/community/search', '/my-projects', '/scouting']);
|
|
129
|
+
assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/builder-of-the-week', '/community/find', '/community/leaderboard', '/community/recruiters', '/community/search', '/community/week', '/my-projects', '/scouting']);
|
|
110
130
|
assert.doesNotMatch(CODE, /method:/, 'it only reads');
|
|
111
131
|
assert.doesNotMatch(CODE, /'\/me\b/, 'who is signed in is the bar’s question');
|
|
112
132
|
assert.doesNotMatch(CODE, /\.sort\(|\.count\b|_count\b/, 'the server ranks; every count is a list’s length');
|
|
@@ -147,7 +167,13 @@ const find = (n, cls) => {
|
|
|
147
167
|
};
|
|
148
168
|
|
|
149
169
|
const ROW = (place, handle, credits, extra = {}) => ({ place, handle, github_login: handle, display_name: handle.toUpperCase(), avatar_url: `https://avatars.example/${handle}`, credits, tasks_shipped: 2, karma: 1, disciplines: ['engineer'], ...extra });
|
|
170
|
+
const WEEK = () => ({ week: { start: '2026-10-19', end: '2026-10-26' },
|
|
171
|
+
winner: { week_start: '2026-10-19', handle: 'ana', github_login: 'ana', display_name: 'Ana <i>L</i>', avatar_url: 'https://avatars.example/ana', credits: 1240, tasks_shipped: 1 },
|
|
172
|
+
past: [{ week_start: '2026-10-12', handle: 'bo', github_login: 'bo', display_name: null, avatar_url: 'data:x', credits: 300, tasks_shipped: 2 },
|
|
173
|
+
{ week_start: '2026-09-28', handle: 'cy', github_login: 'cy', display_name: 'Cy', avatar_url: null, credits: 90, tasks_shipped: 1 },
|
|
174
|
+
{ week_start: '2025-12-29', handle: 'di', github_login: 'di', display_name: 'Di', avatar_url: null, credits: 10, tasks_shipped: 1 }] });
|
|
150
175
|
const READS = () => ({
|
|
176
|
+
'/community/builder-of-the-week': { status: 200, body: WEEK() },
|
|
151
177
|
'/community/leaderboard?window=all': { status: 200, body: { window: 'all', count: 99, results: [ROW(26, 'ana', 1200), ROW(27, 'bo', 1500, { tasks_shipped: 1, karma: 0, avatar_url: 'javascript:alert(1)', display_name: null })] } },
|
|
152
178
|
'/community/leaderboard?window=30d': { status: 200, body: { window: '30d', results: [ROW(1, 'cy', 40)] } },
|
|
153
179
|
'/community/leaderboard?window=all&discipline=artist': { status: 200, body: { window: 'all', results: [] } },
|
|
@@ -199,9 +225,9 @@ async function boot({ pathname = '/community', me = null, reads = READS() } = {}
|
|
|
199
225
|
}
|
|
200
226
|
|
|
201
227
|
test('each path shows its one section, marks its menu items current and names itself; any other shape is the leaderboard', async () => {
|
|
202
|
-
for (const [pathname, title, section] of [['/community', 'Leaderboard', 'board'], ['/community/', 'Leaderboard', 'board'], ['/community/find', 'Find a builder', 'find'], ['/community/recruiters/', 'Open to recruiters', 'recruit']]) {
|
|
228
|
+
for (const [pathname, title, section] of [['/community', 'Leaderboard', 'board'], ['/community/', 'Leaderboard', 'board'], ['/community/week', 'Builder of the week', 'week'], ['/community/find', 'Find a builder', 'find'], ['/community/recruiters/', 'Open to recruiters', 'recruit']]) {
|
|
203
229
|
const ui = await boot({ pathname });
|
|
204
|
-
assert.deepEqual(['board', 'find', 'recruit'].filter((s) => !ui.$(s).hidden), [section], `${pathname}: one section`);
|
|
230
|
+
assert.deepEqual(['board', 'week', 'find', 'recruit'].filter((s) => !ui.$(s).hidden), [section], `${pathname}: one section`);
|
|
205
231
|
const href = SUBPAGES.find(([, t]) => t === title)[0];
|
|
206
232
|
assert.deepEqual(ui.current(), [href, href], `${pathname}: its Community item and its folded twin in the Projects band`);
|
|
207
233
|
assert.equal(ui.document.title, `Cloud Bongos — Community · ${title}`);
|
|
@@ -211,7 +237,7 @@ test('each path shows its one section, marks its menu items current and names it
|
|
|
211
237
|
|
|
212
238
|
test('the leaderboard, signed out: the all-time board in the server’s order with the server’s places, and nothing asked about the viewer', async () => {
|
|
213
239
|
const ui = await boot();
|
|
214
|
-
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all']);
|
|
240
|
+
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week']);
|
|
215
241
|
assert.ok(ui.requests.every((r) => r.credentials === 'same-origin'));
|
|
216
242
|
assert.equal(ui.$('boardLoading').hidden, true);
|
|
217
243
|
assert.equal(ui.$('boardFailed').hidden, true);
|
|
@@ -229,7 +255,7 @@ test('the leaderboard, signed out: the all-time board in the server’s order wi
|
|
|
229
255
|
assert.equal(find(bo, 'mineRow__hint').textContent, '1 work shipped · 0 karma');
|
|
230
256
|
assert.equal(ui.$('boardNote').textContent, 'Credits earned across every project, since each builder started.');
|
|
231
257
|
const signedIn = await boot({ me: { github_login: 'someone' } });
|
|
232
|
-
assert.deepEqual(signedIn.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/my-projects'], 'signed in, the same board read: viewing is ungated, and the one more read is the Recruit door’s');
|
|
258
|
+
assert.deepEqual(signedIn.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week', '/api/bongos/my-projects'], 'signed in, the same board read: viewing is ungated, and the one more read is the Recruit door’s');
|
|
233
259
|
assert.equal(signedIn.$('boardList').textContent, ui.$('boardList').textContent, 'the same rows for every viewer');
|
|
234
260
|
});
|
|
235
261
|
|
|
@@ -325,6 +351,76 @@ test('recruiters: a door signed out, a refusal on a 403, rows only from a 200 as
|
|
|
325
351
|
assert.equal(failed.$('recruitFailed').textContent, 'This list could not be read. Reload the page to try again.');
|
|
326
352
|
});
|
|
327
353
|
|
|
354
|
+
// ── 2a. the builder of the week (task 1002981; spec D5) ──────────────────────
|
|
355
|
+
// The server decides each week and records it; the page draws what it sent, the same for
|
|
356
|
+
// every viewer: one line on the board, and its own page with the crown and the weeks before.
|
|
357
|
+
|
|
358
|
+
test('the board’s crown line: the winner, their credits and the week, linking to its page; nothing on no winner or a failed read', async () => {
|
|
359
|
+
const ui = await boot();
|
|
360
|
+
const line = ui.$('boardCrown');
|
|
361
|
+
assert.equal(line.hidden, false);
|
|
362
|
+
assert.equal(line.textContent, 'Builder of the week: Ana <i>L</i>, ' + (1240).toLocaleString() + ' credits, 19–25 Oct', 'a name is text, never markup');
|
|
363
|
+
const link = kids(line)[0];
|
|
364
|
+
assert.equal(link.tag, 'a');
|
|
365
|
+
assert.equal(link.attrs.href, '/community/week');
|
|
366
|
+
assert.equal(kids(line)[2].tag, 'strong', 'the name stands out of the line');
|
|
367
|
+
const signedIn = await boot({ me: { github_login: 'someone' } });
|
|
368
|
+
assert.equal(signedIn.$('boardCrown').textContent, line.textContent, 'the same crown for every viewer');
|
|
369
|
+
for (const answer of [{ status: 200, body: { ...WEEK(), winner: null } }, { status: 500, body: {} }, { status: 429, body: {} }, { reject: true }]) {
|
|
370
|
+
const reads = READS();
|
|
371
|
+
reads['/community/builder-of-the-week'] = answer;
|
|
372
|
+
const quiet = await boot({ reads });
|
|
373
|
+
assert.equal(quiet.$('boardCrown').hidden, true, 'no winner to show, or no answer: no line');
|
|
374
|
+
assert.equal(quiet.$('boardCrown').textContent, '');
|
|
375
|
+
assert.equal(quiet.$('boardFailed').hidden, true, 'and never the board’s failure');
|
|
376
|
+
assert.equal(kids(quiet.$('boardList')).length, 2);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('the week page: the crown, then the past weeks in the server’s order with their dates; nothing about projects asked', async () => {
|
|
381
|
+
const ui = await boot({ pathname: '/community/week', me: { github_login: 'owner' } });
|
|
382
|
+
assert.deepEqual(ui.gets(), ['/api/bongos/community/builder-of-the-week'], 'one read: no board, and no Recruit door here');
|
|
383
|
+
assert.equal(ui.$('weekLoading').hidden, true);
|
|
384
|
+
assert.equal(ui.$('weekFailed').hidden, true);
|
|
385
|
+
assert.equal(ui.$('weekNone').hidden, true);
|
|
386
|
+
const crown = ui.$('weekCrown');
|
|
387
|
+
assert.equal(crown.hidden, false);
|
|
388
|
+
assert.equal(find(crown, 'crownWhen').textContent, '19–25 Oct 2026');
|
|
389
|
+
assert.equal(find(crown, 'crownName').textContent, 'Ana <i>L</i>');
|
|
390
|
+
assert.equal(find(crown, 'mineRow__host').textContent, '@ana');
|
|
391
|
+
assert.equal(find(crown, 'connAv').src, 'https://avatars.example/ana');
|
|
392
|
+
assert.equal(find(crown, 'crownNums').textContent, (1240).toLocaleString() + 'credits1work shipped', 'one work, singular');
|
|
393
|
+
assert.equal(ui.$('pastH').hidden, false);
|
|
394
|
+
const rows = kids(ui.$('weekPast'));
|
|
395
|
+
assert.deepEqual(rows.map((r) => find(r, 'commPlace').textContent), ['12–18 Oct', '28 Sep – 4 Oct', '29 Dec – 4 Jan'], 'newest first, as sent; a week across a month says both');
|
|
396
|
+
assert.equal(find(rows[0], 'mineRow__name').textContent, 'bo', 'no display name: the handle stands in');
|
|
397
|
+
assert.equal(find(rows[0], 'connAv').tag, 'span', 'an avatar that is not http(s) is never loaded');
|
|
398
|
+
assert.equal(find(rows[0], 'mineRow__hint').textContent, '2 works shipped');
|
|
399
|
+
assert.equal(find(rows[0], 'commNum').textContent, '300');
|
|
400
|
+
assert.equal(find(rows[0], 'commUnit').textContent, 'credits');
|
|
401
|
+
const out = await boot({ pathname: '/community/week' });
|
|
402
|
+
assert.equal(out.$('weekCrown').textContent, crown.textContent, 'the same page signed out');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('the week page with no one to show, with no past weeks, and when the read fails', async () => {
|
|
406
|
+
const reads = READS();
|
|
407
|
+
reads['/community/builder-of-the-week'] = { status: 200, body: { week: { start: '2026-12-28', end: '2027-01-04' }, winner: null, past: [] } };
|
|
408
|
+
const none = await boot({ pathname: '/community/week', reads });
|
|
409
|
+
assert.equal(none.$('weekCrown').hidden, true);
|
|
410
|
+
assert.equal(none.$('weekNone').hidden, false);
|
|
411
|
+
assert.equal(none.$('weekNone').textContent, 'No builder of the week for 28 Dec 2026 – 3 Jan 2027.', 'a hidden winner and an empty week read the same (spec D8)');
|
|
412
|
+
assert.equal(none.$('pastH').hidden, true, 'no past weeks: no heading over nothing');
|
|
413
|
+
reads['/community/builder-of-the-week'] = { status: 200, body: { week: { start: 'soon' }, winner: null, past: [] } };
|
|
414
|
+
assert.equal((await boot({ pathname: '/community/week', reads })).$('weekNone').textContent, 'No builder of the week yet.', 'a date it cannot read is never drawn');
|
|
415
|
+
reads['/community/builder-of-the-week'] = { status: 500, body: {} };
|
|
416
|
+
const failed = await boot({ pathname: '/community/week', reads });
|
|
417
|
+
assert.equal(failed.$('weekFailed').hidden, false);
|
|
418
|
+
assert.equal(failed.$('weekFailed').textContent, 'The builder of the week could not be read. Reload the page to try again.');
|
|
419
|
+
assert.equal(failed.$('weekNone').hidden, true, 'a failure never reads as nobody this week');
|
|
420
|
+
reads['/community/builder-of-the-week'] = { status: 429, body: {} };
|
|
421
|
+
assert.match((await boot({ pathname: '/community/week', reads })).$('weekFailed').textContent, /^Too many requests/);
|
|
422
|
+
});
|
|
423
|
+
|
|
328
424
|
// ── 2b. the Recruit door (task 1002292; spec D6, owner 2026-10-27) ───────────
|
|
329
425
|
// A project OWNER gets a Recruit link on each row: the hub cannot read a project's
|
|
330
426
|
// `project.recruit` (ADR 0335 D1), so GET /my-projects' `owned` stands in. The link is the
|
|
@@ -342,7 +438,7 @@ test('recruit: an owner gets a Recruit link on every row but their own, opening
|
|
|
342
438
|
const reads = READS();
|
|
343
439
|
reads['/my-projects'] = OWNED(['mercury', 'https://mercury.example/x?y=1', 'Mercury']);
|
|
344
440
|
const ui = await boot({ me: { github_login: 'BO' }, reads });
|
|
345
|
-
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/my-projects']);
|
|
441
|
+
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week', '/api/bongos/my-projects']);
|
|
346
442
|
const [ana, bo] = kids(ui.$('boardList'));
|
|
347
443
|
const a = recruitOf(ana);
|
|
348
444
|
assert.equal(a.tag, 'a');
|
|
@@ -380,7 +476,7 @@ test('recruit: none for a builder who owns no project, signed out, on a failed r
|
|
|
380
476
|
assert.equal(member.$('boardFor').hidden, true);
|
|
381
477
|
assert.doesNotMatch(member.$('boardList').className, /canRecruit/);
|
|
382
478
|
const out = await boot({ reads });
|
|
383
|
-
assert.deepEqual(out.gets(), ['/api/bongos/community/leaderboard?window=all'], 'signed out, nobody is asked about projects');
|
|
479
|
+
assert.deepEqual(out.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week'], 'signed out, nobody is asked about projects');
|
|
384
480
|
for (const answer of [{ status: 500, body: {} }, { status: 401, body: {} }, { reject: true }, OWNED(['evil', 'javascript:alert(1)', 'Evil'], ['none', null, 'None'])]) {
|
|
385
481
|
reads['/my-projects'] = answer;
|
|
386
482
|
const ui = await boot({ me: { github_login: 'someone' }, reads });
|
|
@@ -440,6 +536,7 @@ const HUB_ONLY = ['account.html', 'connections.html', 'invitations.html', 'guild
|
|
|
440
536
|
const wrap = (apex, s) => (apex ? `<!--apex-only:start-->${s}<!--apex-only:end-->` : s);
|
|
441
537
|
const ITEMS = [
|
|
442
538
|
['/community', 'Leaderboard', 'who earned the most, across every project'],
|
|
539
|
+
['/community/week', 'Builder of the week', 'the top earner of each week, and how it is chosen'],
|
|
443
540
|
['/community/find', 'Find a builder', 'anyone building here, by name or role'],
|
|
444
541
|
['/community/recruiters', 'Open to recruiters', 'who is open to recruiting, for project owners'],
|
|
445
542
|
];
|
|
@@ -460,9 +557,9 @@ test('every page that wears the hub’s bar carries the Community menu before Pr
|
|
|
460
557
|
const at = html.indexOf(apex ? `<!--apex-only:start-->${trig}` : trig);
|
|
461
558
|
assert.ok(at > 0, `${f}: the Community trigger`);
|
|
462
559
|
assert.ok(html.indexOf('id="projTrig"') > at, `${f}: Community comes before Projects`);
|
|
463
|
-
assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its
|
|
560
|
+
assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its four sub-pages`);
|
|
464
561
|
assert.ok(html.indexOf('id="menuCommunity"') < html.indexOf('id="menuProjects"'), `${f}: the bands in the bar’s order`);
|
|
465
|
-
assert.ok(html.includes(wrap(apex, fold)), `${f}: the
|
|
562
|
+
assert.ok(html.includes(wrap(apex, fold)), `${f}: the four folded into the Projects band for phones`);
|
|
466
563
|
if (apex) assert.doesNotMatch(stripApexOnly(html, false).split('<body>')[1], /\/community|commTrig|menuCommunity|connSuggest/, `${f}: a child has no community pages, so no trace of the menu`);
|
|
467
564
|
}
|
|
468
565
|
});
|
|
@@ -520,7 +617,7 @@ function get(pathname, host) {
|
|
|
520
617
|
});
|
|
521
618
|
}
|
|
522
619
|
|
|
523
|
-
test('GET /community, /community/find and /community/recruiters: one stamped page on the hub’s apex; never on a child or another host', async () => {
|
|
620
|
+
test('GET /community, /community/week, /community/find and /community/recruiters: one stamped page on the hub’s apex; never on a child or another host', async () => {
|
|
524
621
|
modules.setModuleOverrides({ 'platform-identity': false });
|
|
525
622
|
for (const [p] of SUBPAGES) assert.equal((await get(p, 'game.test.local')).status, 404, `${p}: a child instance has none of the routes these pages read`);
|
|
526
623
|
modules.setModuleOverrides({ 'platform-identity': true });
|
|
@@ -533,7 +630,7 @@ test('GET /community, /community/find and /community/recruiters: one stamped pag
|
|
|
533
630
|
bodies.push(hub.body);
|
|
534
631
|
}
|
|
535
632
|
assert.ok(bodies.every((b) => b === bodies[0]), 'byte-identical for every sub-page: the script picks the view');
|
|
536
|
-
assert.doesNotMatch((await get('/community/everyone', 'game.test.local')).body, /— Community<\/title>/, 'only the
|
|
633
|
+
assert.doesNotMatch((await get('/community/everyone', 'game.test.local')).body, /— Community<\/title>/, 'only the four sub-pages');
|
|
537
634
|
for (const host of ['builders.test.local', 'status.test.local']) {
|
|
538
635
|
assert.doesNotMatch((await get('/community', host)).body, /— Community<\/title>/, `${host} must not serve /community`);
|
|
539
636
|
}
|
|
@@ -546,15 +643,18 @@ test('the kit renders each sub-page for everyone, the empty board, and a project
|
|
|
546
643
|
const states = json('community.states.json');
|
|
547
644
|
assert.equal(states.page, '/community');
|
|
548
645
|
assert.deepEqual(states.modes, ['dark']);
|
|
549
|
-
for (const s of ['out', 'out-30d', 'out-artists', 'out-find', 'out-find-search', 'out-recruiters', 'in-recruiters', 'in']) assert.ok(states.states[s], `kit state ${s}`);
|
|
646
|
+
for (const s of ['out', 'out-30d', 'out-artists', 'out-week', 'in-week', 'out-find', 'out-find-search', 'out-recruiters', 'in-recruiters', 'in']) assert.ok(states.states[s], `kit state ${s}`);
|
|
550
647
|
assert.equal(states.states['out-find'].url, '/community/find');
|
|
648
|
+
assert.equal(states.states['out-week'].url, '/community/week');
|
|
649
|
+
assert.ok(states.states.out.expect.visible.includes('#boardCrown'), 'the board carries the crown line');
|
|
551
650
|
assert.equal(states.states['in-recruiters'].url, '/community/recruiters');
|
|
552
651
|
assert.ok(states.states['in-recruiters'].expect.visible.includes('#recruitClosed'), 'signed in but not admitted: told, shown no one');
|
|
553
|
-
for (const [file, name, fixture] of [['community-empty.states.json', 'community-leaderboard', 'community-leaderboard-empty'], ['community-recruiter.states.json', 'scouting', 'scouting-recruiter'], ['community-owner.states.json', 'my-projects', 'my-projects-owner'], ['community-owner-two.states.json', 'my-projects', 'my-projects-owner-two']]) {
|
|
652
|
+
for (const [file, name, fixture] of [['community-empty.states.json', 'community-leaderboard', 'community-leaderboard-empty'], ['community-recruiter.states.json', 'scouting', 'scouting-recruiter'], ['community-owner.states.json', 'my-projects', 'my-projects-owner'], ['community-owner-two.states.json', 'my-projects', 'my-projects-owner-two'], ['community-week-none.states.json', 'community-week', 'community-week-none']]) {
|
|
554
653
|
assert.equal(json(file).stub.fixtures[name], fixture);
|
|
555
654
|
assert.ok(fs.existsSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', fixture + '.json')), `fixture ${fixture}`);
|
|
556
655
|
}
|
|
557
656
|
assert.equal(json('community-recruiter.states.json').states['in-recruiter'].url, '/community/recruiters');
|
|
657
|
+
assert.equal(json('community-week-none.states.json').states['out-week-none'].url, '/community/week');
|
|
558
658
|
// the Recruit door (task 1002292): an owner of one project on the board and the search, and of two with the choice
|
|
559
659
|
assert.ok(json('community-owner.states.json').states['in-owner'].expect.count.some(([sel, n]) => sel === '#boardList a.commRecruit' && n === 6));
|
|
560
660
|
assert.equal(json('community-owner.states.json').states['in-owner-find'].url, '/community/find');
|
|
@@ -585,9 +685,14 @@ async function call(origin, p) {
|
|
|
585
685
|
return { status: res.status, body: await res.json() };
|
|
586
686
|
}
|
|
587
687
|
|
|
588
|
-
test('the kit stub answers the board, the search and the recruiting door the way the routes do', async () => {
|
|
688
|
+
test('the kit stub answers the board, the builder of the week, the search and the recruiting door the way the routes do', async () => {
|
|
589
689
|
const s = await stub();
|
|
590
690
|
try {
|
|
691
|
+
const week = await call(s.origin, '/community/builder-of-the-week');
|
|
692
|
+
assert.deepEqual(Object.keys(week.body).sort(), ['past', 'week', 'winner'], 'the route’s three keys');
|
|
693
|
+
assert.ok(week.body.winner && week.body.past.length, 'a crown and the weeks before it');
|
|
694
|
+
assert.ok([week.body.winner, ...week.body.past].every((r) => !('github_id' in r) && Object.keys(r).sort().join() === 'avatar_url,credits,display_name,github_login,handle,tasks_shipped,week_start'), 'the module’s projection, no github_id');
|
|
695
|
+
assert.deepEqual(week.body.past.map((p) => p.week_start), [...week.body.past.map((p) => p.week_start)].sort().reverse(), 'newest first');
|
|
591
696
|
const all = await call(s.origin, '/community/leaderboard');
|
|
592
697
|
assert.equal(all.body.window, 'all');
|
|
593
698
|
assert.deepEqual(all.body.results.map((r) => r.place), all.body.results.map((_, i) => i + 1));
|
|
@@ -606,6 +711,12 @@ test('the kit stub answers the board, the search and the recruiting door the way
|
|
|
606
711
|
try {
|
|
607
712
|
assert.equal((await call(member.origin, '/scouting')).status, 403, 'signed in, most builders are refused');
|
|
608
713
|
} finally { member.stop(); }
|
|
714
|
+
const none = await stub({ FIXTURE_ALIAS: JSON.stringify({ 'community-week': 'community-week-none' }) });
|
|
715
|
+
try {
|
|
716
|
+
const r = await call(none.origin, '/community/builder-of-the-week');
|
|
717
|
+
assert.equal(r.body.winner, null);
|
|
718
|
+
assert.deepEqual(r.body.past, []);
|
|
719
|
+
} finally { none.stop(); }
|
|
609
720
|
const owner = await stub({ AUTH: '1', FIXTURE_ALIAS: JSON.stringify({ scouting: 'scouting-recruiter' }) });
|
|
610
721
|
try {
|
|
611
722
|
const r = await call(owner.origin, '/scouting');
|
|
@@ -193,8 +193,8 @@ for (const p of PAGES) {
|
|
|
193
193
|
assert.deepEqual(bandLinks(bandOf(html, 'menuProjects')), [
|
|
194
194
|
['Sky', '/projects?view=map'], ['Table', '/projects?view=map&sub=table'],
|
|
195
195
|
['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
|
|
196
|
-
// the Community menu's
|
|
197
|
-
['Leaderboard', '/community'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
196
|
+
// the Community menu's four sub-pages, folded into the band under 480px (tasks 1003363, 1002981); apex-only
|
|
197
|
+
['Leaderboard', '/community'], ['Builder of the week', '/community/week'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
198
198
|
['Builders you may know', '/connections#connSuggest'],
|
|
199
199
|
], 'this page has no views of its own, so every item names /projects in full');
|
|
200
200
|
assert.deepEqual(bandLinks(bandOf(stripApexOnly(html, false), 'menuProjects')).map((l) => l[0]), ['Sky', 'Table', 'Your projects', 'New project'], 'a child has no /community');
|