@bongos/core 1.19.971 → 1.19.972
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +55 -25
- package/docs/api/openapi.json +1 -1
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +151 -152
- package/docs/copy-registry.json +193 -212
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/goals-page.js +6 -0
- package/modules/hall-ui/public/goals-render.js +16 -0
- package/modules/hall-ui/public/mine-lens.js +172 -0
- package/modules/hall-ui/public/work-summary.js +90 -0
- package/modules/hall-ui/public/work.css +50 -0
- package/modules/hall-ui/public/work.html +24 -0
- package/modules/hall-ui/public/work.js +38 -51
- package/modules/lifecycle/db-goals.js +20 -3
- package/modules/lifecycle/routes/goals.js +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +3 -0
- package/scripts/gds/scope-roster-guard.js +118 -0
- package/src/bongos/module-scope-map.js +1 -0
- package/src/module-api.js +1 -1
- package/tests/fitness_scope_roster.mjs +117 -0
- package/tests/goal_mine_lens.mjs +233 -0
- package/tests/goal_tier.mjs +30 -4
- package/tests/hall_mine_lens.mjs +381 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// tests/hall_mine_lens.mjs — the hall's "Only mine" lens: goals you are added
|
|
2
|
+
// on, and tasks you have claimed.
|
|
3
|
+
//
|
|
4
|
+
// The lens is ONE checkbox that four tabs answer differently — Ready and Backlog
|
|
5
|
+
// by goal membership, In Progress by who holds the claim, Completed by who
|
|
6
|
+
// shipped it. That is exactly the shape a source-grep test cannot prove: a regex
|
|
7
|
+
// can see that `mineOn()` is called and still miss that a renderer filters the
|
|
8
|
+
// wrong list, or counts the unfiltered one, or silently empties every tab when
|
|
9
|
+
// the membership read fails. So work.js is BOOTED here, against the stub DOM
|
|
10
|
+
// tests/helpers.mjs already uses for the profile and idea pages, and the
|
|
11
|
+
// assertions read the HTML the page actually wrote.
|
|
12
|
+
//
|
|
13
|
+
// Each test below was verified by deleting the line it guards and watching it go
|
|
14
|
+
// red — the vacuous-guard check this suite has been bitten by before.
|
|
15
|
+
|
|
16
|
+
import { test } from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import vm from 'node:vm';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { settle } from './helpers.mjs';
|
|
23
|
+
|
|
24
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
26
|
+
const WORK_JS = fs.readFileSync(path.join(HALL, 'work.js'), 'utf8');
|
|
27
|
+
// The two files work.js was carved into. Run the REAL ones rather than stubbing
|
|
28
|
+
// them: mine-lens.js IS the subject here, and work-summary.js is the piece that
|
|
29
|
+
// left work.js to make room for it — a stub of either would let the carve rot.
|
|
30
|
+
const MINE_LENS_JS = fs.readFileSync(path.join(HALL, 'mine-lens.js'), 'utf8');
|
|
31
|
+
const WORK_SUMMARY_JS = fs.readFileSync(path.join(HALL, 'work-summary.js'), 'utf8');
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// The fixture. Two goals, and the viewer (builder 7, @percy) is on ONE of them.
|
|
35
|
+
// Every list below is built so that "mine" and "not mine" are both non-empty —
|
|
36
|
+
// a lens that returned everything and a lens that returned nothing would both
|
|
37
|
+
// pass against a one-sided fixture.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
const MINE_GOAL = 1000101;
|
|
40
|
+
const THEIRS_GOAL = 1000102;
|
|
41
|
+
|
|
42
|
+
const GOALS = [
|
|
43
|
+
{ id: MINE_GOAL, title: 'A goal I am on', viewer_is_member: true },
|
|
44
|
+
{ id: THEIRS_GOAL, title: 'A goal I am not on', viewer_is_member: false },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const CLAIMABLE = [
|
|
48
|
+
{ id: 501, title: 'ready in my goal', goal_id: MINE_GOAL, status: 'ready', version_id: 'V1' },
|
|
49
|
+
{ id: 502, title: 'ready in their goal', goal_id: THEIRS_GOAL, status: 'ready', version_id: 'V1' },
|
|
50
|
+
// A task with NO goal at all. It is nobody's by membership, and it exists in
|
|
51
|
+
// the fixture to pin that the lens excludes it rather than letting the
|
|
52
|
+
// ungoaled backlog ride along under the word "mine".
|
|
53
|
+
{ id: 503, title: 'ready with no goal', goal_id: null, status: 'ready', version_id: 'V1' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const BACKLOG = [
|
|
57
|
+
{ id: 601, title: 'backlog in my goal', goal_id: MINE_GOAL, status: 'backlog', version_id: 'V1' },
|
|
58
|
+
{ id: 602, title: 'backlog in their goal', goal_id: THEIRS_GOAL, status: 'backlog', version_id: 'V1' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const ACTIVE_CLAIMS = [
|
|
62
|
+
{ claim_id: 9001, task_id: 701, title: 'a claim of mine', builder_id: 7, github_login: 'percy', display_name: 'Percy', claimed_at: '2026-09-20T00:00:00Z' },
|
|
63
|
+
{ claim_id: 9002, task_id: 702, title: 'someone else holds this', builder_id: 8, github_login: 'annabeth', display_name: 'Annabeth', claimed_at: '2026-09-20T00:00:00Z' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const SHIPPED = [
|
|
67
|
+
{ id: 801, title: 'I shipped this', github_login: 'percy', shipped_at: '2026-09-21T00:00:00Z' },
|
|
68
|
+
{ id: 802, title: 'they shipped this', github_login: 'annabeth', shipped_at: '2026-09-21T00:00:00Z' },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// bootWorkBoard — run the REAL work.js in a vm.
|
|
73
|
+
//
|
|
74
|
+
// `goalsFail: true` makes GET /goals reject, which is the failure mode the lens
|
|
75
|
+
// has to survive honestly: with no membership evidence it must switch itself off
|
|
76
|
+
// rather than narrow both queue tabs to nothing.
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
function bootWorkBoard({ signedIn = true, goalsFail = false } = {}) {
|
|
79
|
+
const els = new Map();
|
|
80
|
+
const makeEl = (id = '') => ({
|
|
81
|
+
id,
|
|
82
|
+
dataset: {}, style: {}, textContent: '', hidden: false, disabled: false,
|
|
83
|
+
checked: false, value: '', title: '',
|
|
84
|
+
_html: '',
|
|
85
|
+
classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
|
|
86
|
+
setAttribute() {}, getAttribute() { return null; },
|
|
87
|
+
listeners: {},
|
|
88
|
+
addEventListener(type, fn) { (this.listeners[type] ||= []).push(fn); },
|
|
89
|
+
fire(type, ev = {}) { (this.listeners[type] || []).forEach((fn) => fn(ev)); },
|
|
90
|
+
querySelector() { return null; }, querySelectorAll() { return []; },
|
|
91
|
+
appendChild() {}, remove() {}, closest() { return null; },
|
|
92
|
+
set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
|
|
93
|
+
});
|
|
94
|
+
// Keyed by the selector text with the leading '#' stripped, so a compound
|
|
95
|
+
// selector like '#wip-table tbody' gets its own stable element — which is
|
|
96
|
+
// what work.js reads the in-flight rows out of.
|
|
97
|
+
const el = (key) => { if (!els.has(key)) els.set(key, makeEl(key)); return els.get(key); };
|
|
98
|
+
const documentObj = {
|
|
99
|
+
getElementById: (id) => el(id),
|
|
100
|
+
querySelector: (sel) => el(String(sel).replace(/^#/, '')),
|
|
101
|
+
querySelectorAll: () => [],
|
|
102
|
+
createElement: () => makeEl(),
|
|
103
|
+
addEventListener(type, fn) { (this.listeners[type] ||= []).push(fn); },
|
|
104
|
+
listeners: {},
|
|
105
|
+
body: { dataset: {} }, documentElement: { dataset: {} },
|
|
106
|
+
hidden: false,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const requests = [];
|
|
110
|
+
const client = {
|
|
111
|
+
request: async (method, url) => {
|
|
112
|
+
requests.push(`${method} ${url}`);
|
|
113
|
+
const ok = (data) => ({ status: 200, ok: true, data });
|
|
114
|
+
if (url.endsWith('/me')) {
|
|
115
|
+
// `signedIn: false` is a 200 that carries NO builder, not a 401: a 401
|
|
116
|
+
// bounces this page to sign-in before anything renders (and the server
|
|
117
|
+
// page gate bars an unauthenticated visitor from /work outright), so the
|
|
118
|
+
// identity-less state the lens actually has to survive is this one.
|
|
119
|
+
return signedIn
|
|
120
|
+
? ok({ builder: { id: 7, display_name: 'Percy', github_login: 'percy', rank: 'metic' }, task_kinds: ['build'] })
|
|
121
|
+
: ok({ builder: null });
|
|
122
|
+
}
|
|
123
|
+
if (url.endsWith('/goals')) {
|
|
124
|
+
return goalsFail ? { status: 500, ok: false, data: null } : ok({ goals: GOALS });
|
|
125
|
+
}
|
|
126
|
+
if (url.includes('/tasks/claimable')) return ok({ claimable: CLAIMABLE, active_claims: ACTIVE_CLAIMS });
|
|
127
|
+
if (url.includes('/tasks?status=backlog')) return ok({ tasks: BACKLOG });
|
|
128
|
+
if (url.includes('/public/recent-shipped')) return ok({ tasks: SHIPPED });
|
|
129
|
+
if (url.includes('/versions')) return ok({ versions: [{ version_id: 'V1' }] });
|
|
130
|
+
if (url.includes('/public/progress')) return ok({ progress: [{ version_id: 'V1', lifecycle_shipped_count: 42 }] });
|
|
131
|
+
if (url.includes('/public/cost-summary')) return ok({ total_value_credits: 1234 });
|
|
132
|
+
return ok({});
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const escapeHtml = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
137
|
+
|
|
138
|
+
let tabsOnChange = null;
|
|
139
|
+
const kit = {
|
|
140
|
+
makePager: (prefix) => ({ prefix, page: 1, perPage: 100 }),
|
|
141
|
+
paginate: (rows) => rows,
|
|
142
|
+
wirePager() {},
|
|
143
|
+
pagerBarHtml: () => '',
|
|
144
|
+
makeTabs: ({ tabs, onChange }) => {
|
|
145
|
+
let current = tabs[0].id;
|
|
146
|
+
tabsOnChange = onChange;
|
|
147
|
+
return { active: () => current, setCount() {}, activate(id) { current = id; onChange(id); } };
|
|
148
|
+
},
|
|
149
|
+
// The real bar is hall-kit's; here it only has to MAKE the controls readable,
|
|
150
|
+
// because what this suite asserts is what the renderers do with their values.
|
|
151
|
+
makeFilterBar: ({ search, primary = [], overflow = [] }) => {
|
|
152
|
+
[search, ...primary, ...overflow].filter(Boolean).forEach((c) => {
|
|
153
|
+
el(c.id).value = c.value ?? c.default ?? '';
|
|
154
|
+
});
|
|
155
|
+
return { values: () => ({}), get: () => '', set() {} };
|
|
156
|
+
},
|
|
157
|
+
ageChipHtml: () => '',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const windowObj = {
|
|
161
|
+
__BRANDING__: { currency: { label: 'credits' } },
|
|
162
|
+
BongosClient: { createClient: () => client },
|
|
163
|
+
OTBKit: kit,
|
|
164
|
+
OTBTaskDetail: { href: (id) => `/task/${id}`, open() {} },
|
|
165
|
+
OTBTaskLib: {
|
|
166
|
+
priorityRank: () => 0, touchesConflict: () => false, rankTagHtml: () => '',
|
|
167
|
+
creditsPerMinute: () => 0, buildPromptForTask: () => '', friendlyBatchReason: () => '',
|
|
168
|
+
taskActivityMs: () => 0, STALE_CHOICES: [{ value: '', label: '— any —' }], isStale: () => false,
|
|
169
|
+
},
|
|
170
|
+
OTB: {
|
|
171
|
+
escapeHtml,
|
|
172
|
+
$: (sel) => documentObj.querySelector(sel),
|
|
173
|
+
fmtNum: (n) => String(n ?? 0),
|
|
174
|
+
fmtDate: (d) => String(d ?? '').slice(0, 10),
|
|
175
|
+
errCode: () => null, errDetails: () => ({}),
|
|
176
|
+
toast() {},
|
|
177
|
+
categoryChip: () => '',
|
|
178
|
+
fillKindFilter() {},
|
|
179
|
+
kindIsUnset: () => true, KIND_UNSET: 'unclassified',
|
|
180
|
+
roleAxisLabel: () => 'Role',
|
|
181
|
+
roleOptions: () => [{ value: '', label: '— any —' }],
|
|
182
|
+
roleIsUnset: () => true, roleLabel: () => '', ROLE_UNSET: 'unclassified',
|
|
183
|
+
},
|
|
184
|
+
location: { hostname: 'builders.example.com', pathname: '/work', search: '', hash: '', href: 'https://builders.example.com/work' },
|
|
185
|
+
localStorage: { getItem: () => null, setItem() {}, removeItem() {} },
|
|
186
|
+
confirm: () => true,
|
|
187
|
+
addEventListener() {},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const sandbox = {
|
|
191
|
+
window: windowObj, document: documentObj, location: windowObj.location,
|
|
192
|
+
localStorage: windowObj.localStorage, console, setTimeout, clearTimeout,
|
|
193
|
+
setInterval: () => 0, clearInterval() {}, Date, URL, URLSearchParams, navigator: {},
|
|
194
|
+
history: { replaceState() {} },
|
|
195
|
+
};
|
|
196
|
+
sandbox.globalThis = sandbox;
|
|
197
|
+
vm.createContext(sandbox);
|
|
198
|
+
// Both are UMD modules that attach to the sandbox global rather than to
|
|
199
|
+
// `window` (there is no `module` here), so they are bridged across by hand.
|
|
200
|
+
vm.runInContext(MINE_LENS_JS, sandbox, { filename: 'mine-lens.js' });
|
|
201
|
+
vm.runInContext(WORK_SUMMARY_JS, sandbox, { filename: 'work-summary.js' });
|
|
202
|
+
windowObj.OTBMineLens = sandbox.OTBMineLens;
|
|
203
|
+
windowObj.OTBWorkSummary = sandbox.OTBWorkSummary;
|
|
204
|
+
vm.runInContext(WORK_JS, sandbox, { filename: 'work.js' });
|
|
205
|
+
(documentObj.listeners.DOMContentLoaded || []).forEach((fn) => fn());
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
el,
|
|
209
|
+
requests,
|
|
210
|
+
// Flip the lens the way a click does: set the box, then fire its listener.
|
|
211
|
+
setMine: (on) => { el('f-mine').checked = on; el('f-mine').fire('change'); },
|
|
212
|
+
switchTab: (id) => { if (tabsOnChange) tabsOnChange(id); },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// settle() twice: loadAll awaits /me, then the goals read, then the claimable
|
|
217
|
+
// read, and the history feed lands on its own promise chain.
|
|
218
|
+
const ready = async (board) => { for (let i = 0; i < 6; i += 1) await settle(); return board; };
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// 1. The queue tabs: "mine" is GOAL MEMBERSHIP
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
test('Ready narrows to tasks in a goal the viewer is a member of', async () => {
|
|
225
|
+
const b = await ready(bootWorkBoard());
|
|
226
|
+
const before = b.el('task-list').innerHTML;
|
|
227
|
+
assert.match(before, /ready in my goal/, 'the unfiltered board shows both goals');
|
|
228
|
+
assert.match(before, /ready in their goal/);
|
|
229
|
+
|
|
230
|
+
b.setMine(true);
|
|
231
|
+
const after = b.el('task-list').innerHTML;
|
|
232
|
+
assert.match(after, /ready in my goal/, 'the task in my goal survives');
|
|
233
|
+
assert.doesNotMatch(after, /ready in their goal/, 'the task in a goal I am not on is gone');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('a task with NO goal is not "mine" — the lens is membership, not leftovers', async () => {
|
|
237
|
+
const b = await ready(bootWorkBoard());
|
|
238
|
+
assert.match(b.el('task-list').innerHTML, /ready with no goal/, 'it is there unfiltered');
|
|
239
|
+
b.setMine(true);
|
|
240
|
+
assert.doesNotMatch(b.el('task-list').innerHTML, /ready with no goal/,
|
|
241
|
+
'an ungoaled task belongs to nobody, so it cannot arrive under "only mine"');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('Backlog narrows through the SAME predicate, so the lens cannot reach one tab only', async () => {
|
|
245
|
+
const b = await ready(bootWorkBoard());
|
|
246
|
+
assert.match(b.el('backlog-list').innerHTML, /backlog in their goal/);
|
|
247
|
+
b.setMine(true);
|
|
248
|
+
assert.match(b.el('backlog-list').innerHTML, /backlog in my goal/);
|
|
249
|
+
assert.doesNotMatch(b.el('backlog-list').innerHTML, /backlog in their goal/);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// 2. In Progress: "mine" is THE CLAIM
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
test('In Progress narrows to the claims the viewer is holding', async () => {
|
|
257
|
+
const b = await ready(bootWorkBoard());
|
|
258
|
+
const tbody = b.el('wip-table tbody');
|
|
259
|
+
assert.match(tbody.innerHTML, /someone else holds this/, 'unfiltered, the whole board is in flight');
|
|
260
|
+
|
|
261
|
+
b.setMine(true);
|
|
262
|
+
assert.match(tbody.innerHTML, /a claim of mine/);
|
|
263
|
+
assert.doesNotMatch(tbody.innerHTML, /someone else holds this/,
|
|
264
|
+
"another builder's live claim is not mine");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('the in-flight SECTION pill counts what is shown; the TAB pill keeps the whole board', async () => {
|
|
268
|
+
// The distinction is the point: narrowing to my own claims must not make the
|
|
269
|
+
// board look idle. The tab pill is the project's in-flight count either way.
|
|
270
|
+
const b = await ready(bootWorkBoard());
|
|
271
|
+
assert.equal(b.el('wip-count').textContent, '2');
|
|
272
|
+
b.setMine(true);
|
|
273
|
+
assert.equal(b.el('wip-count').textContent, '1', 'the section pill follows the narrowed list');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('holding nothing says so, instead of reading as a quiet board', async () => {
|
|
277
|
+
const b = await ready(bootWorkBoard());
|
|
278
|
+
// Hand the page an in-flight list with none of the viewer's own claims by
|
|
279
|
+
// flipping the lens on a fixture where builder 7 holds one — then assert the
|
|
280
|
+
// OTHER direction: a viewer who holds nothing gets the specific sentence.
|
|
281
|
+
ACTIVE_CLAIMS[0].builder_id = 8;
|
|
282
|
+
try {
|
|
283
|
+
b.setMine(true);
|
|
284
|
+
assert.match(b.el('wip-table tbody').innerHTML, /You are not holding any claims right now/,
|
|
285
|
+
'the empty state names the lens, not the project');
|
|
286
|
+
} finally {
|
|
287
|
+
ACTIVE_CLAIMS[0].builder_id = 7;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// 3. Completed: "mine" is WHO SHIPPED IT
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
test('Completed narrows to the viewer\'s own ships, matched on the login the public feed carries', async () => {
|
|
296
|
+
const b = await ready(bootWorkBoard());
|
|
297
|
+
assert.match(b.el('history-list').innerHTML, /they shipped this/);
|
|
298
|
+
b.setMine(true);
|
|
299
|
+
assert.match(b.el('history-list').innerHTML, /I shipped this/);
|
|
300
|
+
assert.doesNotMatch(b.el('history-list').innerHTML, /they shipped this/);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// 4. The two ways the lens must refuse rather than lie
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
test('with no identity the lens is disabled — never a checkbox that empties every tab', async () => {
|
|
308
|
+
const b = await ready(bootWorkBoard({ signedIn: false }));
|
|
309
|
+
assert.equal(b.el('f-mine').disabled, true);
|
|
310
|
+
assert.equal(b.el('f-mine').checked, false);
|
|
311
|
+
assert.match(b.el('f-mine').title, /Sign in/);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('a failed membership read disarms the lens instead of answering "you are on no goals"', async () => {
|
|
315
|
+
const b = await ready(bootWorkBoard({ goalsFail: true }));
|
|
316
|
+
assert.equal(b.el('f-mine').disabled, true,
|
|
317
|
+
'with no evidence the lens is off the table');
|
|
318
|
+
assert.match(b.el('f-mine').title, /could not be loaded/,
|
|
319
|
+
'and it says the read failed rather than implying an answer');
|
|
320
|
+
// The board itself is untouched — losing the lens must not lose the queue.
|
|
321
|
+
assert.match(b.el('task-list').innerHTML, /ready in their goal/);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// 5. The word means something different on each tab, so the hint is rewritten
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
test('the hint beside the checkbox is re-read from the tab, not left generic', async () => {
|
|
329
|
+
const b = await ready(bootWorkBoard());
|
|
330
|
+
const hint = b.el('work-mine-hint');
|
|
331
|
+
assert.equal(hint.textContent, 'tasks in goals you are on', 'Ready opens the board');
|
|
332
|
+
b.switchTab('in-progress');
|
|
333
|
+
assert.equal(hint.textContent, 'claims you are holding');
|
|
334
|
+
b.switchTab('completed');
|
|
335
|
+
assert.equal(hint.textContent, 'tasks you shipped');
|
|
336
|
+
b.switchTab('backlog');
|
|
337
|
+
assert.equal(hint.textContent, 'tasks in goals you are on');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// 6. It costs nothing to ask
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
test('the lens is answered from data the board already fetched — no read per goal', async () => {
|
|
345
|
+
const b = await ready(bootWorkBoard());
|
|
346
|
+
const before = b.requests.length;
|
|
347
|
+
b.setMine(true);
|
|
348
|
+
assert.equal(b.requests.length, before,
|
|
349
|
+
'flipping the lens makes no request: viewer_is_member rides the goal list');
|
|
350
|
+
assert.ok(b.requests.some((r) => r.endsWith('/goals')),
|
|
351
|
+
'and the goal list is the one read it depends on');
|
|
352
|
+
assert.ok(!b.requests.some((r) => /\/goals\/\d+\/members/.test(r)),
|
|
353
|
+
'never a membership read per goal');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// 7. The carve: work-summary.js
|
|
358
|
+
//
|
|
359
|
+
// The momentum card left work.js so the lens could fit under the size ceiling.
|
|
360
|
+
// Two things have to stay true across that move — it still paints, and it still
|
|
361
|
+
// reports the PROJECT, not the viewer. The second is the risk the carve created:
|
|
362
|
+
// the card sits directly above a list the lens narrows, and a card that quietly
|
|
363
|
+
// restated one builder's output as the project's totals would look right.
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
test('the momentum card still renders the project-wide figures after the carve', async () => {
|
|
367
|
+
const b = await ready(bootWorkBoard());
|
|
368
|
+
const card = b.el('works-summary');
|
|
369
|
+
assert.equal(card.hidden, false, 'the card is shown once the shipped counts land');
|
|
370
|
+
assert.match(card.innerHTML, /Works shipped/);
|
|
371
|
+
assert.match(card.innerHTML, /42/, 'the all-time shipped count');
|
|
372
|
+
assert.match(card.innerHTML, /1234/, 'and the credits awarded');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('"Only mine" does NOT narrow the project totals above the list it narrows', async () => {
|
|
376
|
+
const b = await ready(bootWorkBoard());
|
|
377
|
+
const before = b.el('works-summary').innerHTML;
|
|
378
|
+
b.setMine(true);
|
|
379
|
+
assert.equal(b.el('works-summary').innerHTML, before,
|
|
380
|
+
'the card reports the project; the lens reports the reader');
|
|
381
|
+
});
|