@bongos/core 1.19.402 → 1.19.404

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.
@@ -27,7 +27,8 @@ beforeEach(() => { stats._reset(); });
27
27
  function json(body) {
28
28
  return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(body) });
29
29
  }
30
- // route a fake instance: { leaderboard: [...], enabled: {...} }
30
+ // route a fake instance: { leaderboard: [...], enabled: {...}, tasks: [...],
31
+ // days: 30|undefined (whether the remote honoured ?days=), index: {...} }
31
32
  function stub(map) {
32
33
  globalThis.fetch = (url) => {
33
34
  const u = new URL(url);
@@ -39,9 +40,17 @@ function stub(map) {
39
40
  if (u.pathname.endsWith('/modules')) {
40
41
  return site.enabled ? json({ enabled: site.enabled }) : Promise.reject(new Error('down'));
41
42
  }
43
+ if (u.pathname.endsWith('/recent-shipped')) {
44
+ return site.tasks ? json({ tasks: site.tasks, days: site.days }) : Promise.reject(new Error('down'));
45
+ }
46
+ if (u.pathname === '/api/bongos') {
47
+ return site.index ? json(site.index) : Promise.reject(new Error('404'));
48
+ }
42
49
  return Promise.reject(new Error('404'));
43
50
  };
44
51
  }
52
+ const DAY = 24 * 60 * 60 * 1000;
53
+ function shipped(daysAgo) { return { shipped_at: new Date(Date.now() - daysAgo * DAY).toISOString() }; }
45
54
 
46
55
  test('system accounts are excluded — they are machines, not builders', async () => {
47
56
  // measured live: raw leaderboards read 13/3/3 against real 11/1/1
@@ -208,7 +217,7 @@ test('concurrent requests for the same origin collapse into one fetch', async ()
208
217
  stats.getProjectStats(['https://8.8.8.8']),
209
218
  stats.getProjectStats(['https://8.8.8.8']),
210
219
  ]);
211
- assert.equal(calls, 2, 'two endpoints for one origin, not six for three callers');
220
+ assert.equal(calls, 4, 'four feeds for one origin, not twelve for three callers');
212
221
  });
213
222
 
214
223
  test('a non-http origin is refused rather than fetched', async () => {
@@ -274,3 +283,120 @@ test('an origin named __proto__ cannot retarget the result prototype', async ()
274
283
  assert.equal(Object.getPrototypeOf(out), null, 'the result map is null-prototype');
275
284
  assert.equal(({}).builders, undefined, 'Object.prototype is untouched');
276
285
  });
286
+
287
+ /* ── The poster card's fields (task 1003301) ────────────────────────────────
288
+ Faces, shipped-in-30-days and the hall address ride the SAME allSettled as
289
+ builders and tags, degrade to null exactly like them, and share the one
290
+ frozen null shape — a key that went missing would ship as undefined, which
291
+ JSON.stringify drops, which silently changes the wire. ── */
292
+
293
+ const KEYS = ['builders', 'tags', 'owner', 'owner_avatar', 'faces', 'shipped_30d', 'shipped_30d_capped', 'hall_origin'];
294
+
295
+ test('one frozen null shape carries every key, and both fallbacks are that shape', async () => {
296
+ assert.ok(Object.isFrozen(stats.NULL_STATS));
297
+ assert.deepEqual(Object.keys(stats.NULL_STATS).sort(), KEYS.slice().sort());
298
+ const failed = await stats.getProjectStats(['file:///etc/passwd']); // the cached-failure literal
299
+ assert.equal(failed['file:///etc/passwd'], stats.NULL_STATS);
300
+ stub({});
301
+ const unreached = await stats.getProjectStats(['https://9.9.9.9']); // the read fallback
302
+ for (const k of KEYS) assert.equal(unreached['https://9.9.9.9'][k], null, `${k} is null, never undefined`);
303
+ });
304
+
305
+ test('faces are the first five people with an http avatar, in the feed\'s order; machines and bad URLs are skipped', async () => {
306
+ const av = (n) => `https://avatars.example.com/${n}.png`;
307
+ stub({
308
+ 'https://8.8.8.8': {
309
+ leaderboard: [
310
+ { github_login: 'a', avatar_url: av('a') }, { github_login: 'bfg-system', avatar_url: av('bot') },
311
+ { github_login: 'b', avatar_url: 'javascript:alert(1)' }, { github_login: 'c', avatar_url: av('c') },
312
+ { github_login: 'd', avatar_url: av('d') }, { github_login: 'e', avatar_url: av('e') },
313
+ { github_login: 'f', avatar_url: av('f') }, { github_login: 'g', avatar_url: av('g') },
314
+ ],
315
+ enabled: {},
316
+ },
317
+ });
318
+ const out = await stats.getProjectStats(['https://8.8.8.8']);
319
+ assert.deepEqual(out['https://8.8.8.8'].faces, ['a', 'c', 'd', 'e', 'f'].map(av));
320
+ assert.equal(out['https://8.8.8.8'].builders, 7, 'the +N the card shows is counted against builders');
321
+ });
322
+
323
+ test('a leaderboard with no avatars yields an empty faces list — asked, none — not null', async () => {
324
+ stub({ 'https://8.8.8.8': { leaderboard: [{ github_login: 'a' }], enabled: {} } });
325
+ const out = await stats.getProjectStats(['https://8.8.8.8']);
326
+ assert.deepEqual(out['https://8.8.8.8'].faces, []);
327
+ });
328
+
329
+ test('shipped_30d counts only rows inside the window, from shipped_at, whatever the remote returned', async () => {
330
+ stub({
331
+ 'https://8.8.8.8': {
332
+ leaderboard: [], enabled: {},
333
+ tasks: [shipped(1), shipped(29), shipped(31), shipped(400), { shipped_at: null }, {}],
334
+ },
335
+ });
336
+ const out = await stats.getProjectStats(['https://8.8.8.8']);
337
+ assert.equal(out['https://8.8.8.8'].shipped_30d, 2);
338
+ assert.equal(out['https://8.8.8.8'].shipped_30d_capped, false);
339
+ });
340
+
341
+ test('shipped_30d is capped ("N+") when the feed\'s row cap is full and every row is inside the window', async () => {
342
+ const full = (n) => Array.from({ length: n }, (_, i) => shipped(i % 20));
343
+ stub({
344
+ 'https://1.1.1.1': { leaderboard: [], enabled: {}, tasks: full(200), days: 30 }, // ?days honoured: cap 200
345
+ 'https://8.8.8.8': { leaderboard: [], enabled: {}, tasks: full(30) }, // older core, no days: cap 30
346
+ 'https://9.9.9.9': { leaderboard: [], enabled: {}, tasks: full(199), days: 30 }, // under the cap
347
+ });
348
+ const out = await stats.getProjectStats(['https://1.1.1.1', 'https://8.8.8.8', 'https://9.9.9.9']);
349
+ assert.deepEqual([out['https://1.1.1.1'].shipped_30d, out['https://1.1.1.1'].shipped_30d_capped], [200, true]);
350
+ assert.deepEqual([out['https://8.8.8.8'].shipped_30d, out['https://8.8.8.8'].shipped_30d_capped], [30, true]);
351
+ assert.deepEqual([out['https://9.9.9.9'].shipped_30d, out['https://9.9.9.9'].shipped_30d_capped], [199, false]);
352
+ });
353
+
354
+ test('the shipped feed is asked for the window explicitly', async () => {
355
+ const urls = [];
356
+ globalThis.fetch = (url) => { urls.push(String(url)); return Promise.reject(new Error('down')); };
357
+ await stats.getProjectStats(['https://8.8.8.8']);
358
+ assert.ok(urls.includes(`https://8.8.8.8/api/bongos/public/recent-shipped?days=${stats.SHIPPED_WINDOW_DAYS}`));
359
+ assert.ok(urls.includes('https://8.8.8.8/api/bongos'), 'the discovery index answers the hall address');
360
+ });
361
+
362
+ test('hall_origin is the discovery index\'s hall only where it is a different host from the site', async () => {
363
+ stub({
364
+ 'https://1.1.1.1': { leaderboard: [], enabled: {}, index: { hall: 'https://builders.example.com' } },
365
+ 'https://8.8.8.8': { leaderboard: [], enabled: {}, index: { hall: 'https://8.8.8.8' } }, // single host
366
+ 'https://9.9.9.9': { leaderboard: [], enabled: {}, index: { hall: 'javascript:alert(1)' } },
367
+ 'https://4.4.4.4': { leaderboard: [], enabled: {} }, // no index (older core)
368
+ });
369
+ const out = await stats.getProjectStats(['https://1.1.1.1', 'https://8.8.8.8', 'https://9.9.9.9', 'https://4.4.4.4']);
370
+ assert.equal(out['https://1.1.1.1'].hall_origin, 'https://builders.example.com');
371
+ assert.equal(out['https://8.8.8.8'].hall_origin, null, 'a hall at <site>/builders is the site\'s own door');
372
+ assert.equal(out['https://9.9.9.9'].hall_origin, null);
373
+ assert.equal(out['https://4.4.4.4'].hall_origin, null);
374
+ });
375
+
376
+ test('a dead shipped feed leaves the other quarters standing', async () => {
377
+ stub({ 'https://8.8.8.8': { leaderboard: [{ github_login: 'a' }], enabled: { game: true }, index: { hall: 'https://h.example.com' } } });
378
+ const out = await stats.getProjectStats(['https://8.8.8.8']);
379
+ assert.equal(out['https://8.8.8.8'].shipped_30d, null, 'the dead quarter is unknown');
380
+ assert.equal(out['https://8.8.8.8'].shipped_30d_capped, null);
381
+ assert.equal(out['https://8.8.8.8'].builders, 1);
382
+ assert.deepEqual(out['https://8.8.8.8'].tags, ['game']);
383
+ assert.equal(out['https://8.8.8.8'].hall_origin, 'https://h.example.com');
384
+ });
385
+
386
+ test('the origin\'s host is resolved once per sweep, not once per feed', async () => {
387
+ const src = await import('node:fs').then((fs) =>
388
+ fs.readFileSync(new URL('../modules/platform-identity/project-stats.js', import.meta.url), 'utf8'));
389
+ assert.match(src, /await assertPublicHost\(siteUrl\(origin\)\.hostname\);/, 'the guard runs in fetchStats, before the allSettled');
390
+ const getJson = (src.match(/async function getJson\(url\)\s*\{([\s\S]*?)\n\}/) || [, ''])[1];
391
+ assert.ok(getJson, 'getJson() must exist');
392
+ assert.doesNotMatch(getJson, /assertPublicHost/, 'no per-feed lookup');
393
+ });
394
+
395
+ test('httpUrl accepts only http(s) strings and caps them', () => {
396
+ assert.equal(stats.httpUrl('https://a.example/x.png'), 'https://a.example/x.png');
397
+ assert.equal(stats.httpUrl('http://a.example/x.png'), 'http://a.example/x.png');
398
+ assert.equal(stats.httpUrl('javascript:alert(1)'), null);
399
+ assert.equal(stats.httpUrl('//a.example/x.png'), null);
400
+ assert.equal(stats.httpUrl(42), null);
401
+ assert.equal(stats.httpUrl('https://a.example/' + 'x'.repeat(400)).length, 300);
402
+ });
@@ -483,13 +483,109 @@ test('the card claims only what the catalog and the project itself answer (C6)',
483
483
  for (const invented of [/\blive\b/i, /uptime/i, /\bCreated\b/]) {
484
484
  assert.doesNotMatch(card, invented, `the card must not invent ${invented}`);
485
485
  }
486
- for (const id of ['cardBuilders', 'cardRuns', 'cardOwner', 'cardListed']) {
487
- assert.match(card, new RegExp(`id="${id}"`), `the ${id} fact must be real markup (C7)`);
486
+ for (const id of ['cardBuilders', 'cardShipped', 'cardRuns', 'cardOwner', 'cardListed', 'cardArt', 'cardFaces', 'cardFigs']) {
487
+ assert.match(card, new RegExp(`id="${id}"`), `the ${id} region must be real markup (C7)`);
488
488
  }
489
489
  assert.match(card, />Listed</, 'the catalogue date wears its honest label');
490
490
  assert.doesNotMatch(card, /—/, 'an unanswerable card fact hides; it never dashes');
491
491
  });
492
492
 
493
+ /* ── The poster (task 1003301; the v2 pick, direction §7) ───────────────────
494
+ Art, faces, two figures on the art, the facts as a ledger, one accent door
495
+ and a member-only hall entry. The load-bearing rule is the DEGRADATION
496
+ LADDER: every region hides on null, down to the card that shipped before. ── */
497
+
498
+ test('every card region is a direct child of the card — the banner a bare img, the faces and figures paragraphs', () => {
499
+ const card = (HUB.match(/<div class="projCard"[\s\S]*?<div class="cardActions" id="cardActions"><\/div>/) || [''])[0];
500
+ assert.ok(card, 'the card must be findable');
501
+ assert.equal((card.match(/<div\b/g) || []).length, 2, 'the card and its actions strip are the only divs — a nested wrapper shortens the C6 slice');
502
+ assert.match(card, /<img class="cardArt" id="cardArt" alt="" hidden>/, 'the banner is a bare img, hidden until art arrives');
503
+ assert.match(card, /<p class="cardFigs" id="cardFigs" hidden>/, 'the figures ride a paragraph');
504
+ assert.match(card, /<p class="cardFaces" id="cardFaces" hidden>/, 'the faces ride a paragraph');
505
+ assert.match(card, /<span class="cardCall a" id="cardBuildersFig" hidden>[\s\S]*?<span class="fig" id="cardBuilders"><\/span><span class="cap">builders<\/span>/);
506
+ assert.match(card, /<span class="cardCall b" id="cardShippedFig" hidden>[\s\S]*?<span class="fig" id="cardShipped"><\/span><span class="cap">shipped in 30 days<\/span>/);
507
+ assert.equal((card.match(/<svg class="lead"/g) || []).length, 2, 'each callout carries its own leader');
508
+ assert.doesNotMatch(card, /id="cardBuildersL"/, 'Builders left the facts list for the figure band');
509
+ });
510
+
511
+ test('the ladder: each region hides on null and the other figure stands alone', () => {
512
+ const fig = (SCRIPT.match(/function cardFig\(id, value\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
513
+ assert.ok(fig, 'cardFig() must exist');
514
+ assert.match(fig, /\$\(id \+ 'Fig'\)\.hidden = !value/, 'a null figure hides its whole callout, leader and caption with it');
515
+ assert.match(SCRIPT, /\$\('cardFigs'\)\.hidden = !figs;/, 'both null hides the band');
516
+ const sf = (SCRIPT.match(/function shippedFigure\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
517
+ assert.match(sf, /p\.shipped_30d == null\) return ''/, 'null shipped hides; 0 renders');
518
+ assert.match(sf, /p\.shipped_30d_capped \? '\+' : ''/, 'at the feed cap the figure reads N+');
519
+ const faces = (SCRIPT.match(/function cardFaces\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
520
+ assert.ok(faces, 'cardFaces() must exist');
521
+ assert.match(faces, /\$\('cardFaces'\)\.hidden = !faces\.length/, 'no faces, no row — absent, not empty-looking');
522
+ assert.match(faces, /p\.faces\.map\(safeHttpUrl\)\.filter\(Boolean\)\.slice\(0, FACES_MAX\)/, 'five scheme-checked faces at most');
523
+ assert.match(faces, /p\.builders - faces\.length/, 'the +N counts the builders beyond the faces shown');
524
+ assert.match(SCRIPT, /var FACES_MAX = 5;/);
525
+ const art = (SCRIPT.match(/function cardArt\(url\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
526
+ assert.ok(art, 'cardArt() must exist');
527
+ assert.match(art, /classList\.toggle\('has-art', !!url\)/, 'art switches the whole layout');
528
+ assert.match(SCRIPT, /\$\('cardArt'\)\.addEventListener\('error', function \(\) \{ cardArt\(''\); \}\);/,
529
+ 'a broken image demotes to the no-art layout, never a broken glyph');
530
+ assert.match(SCRIPT, /cardArt\(p\.isSelf \? '' : safeHttpUrl\(p\.art_url\)\)/, 'the art URL is scheme-checked client-side too');
531
+ });
532
+
533
+ test('exactly one hall-URL derivation: the DOM for the hub, the feed for a project, safeHttpUrl for both, and only where the hall is elsewhere', () => {
534
+ assert.equal((SCRIPT.match(/function hallUrlOf\(/g) || []).length, 1);
535
+ const fn = (SCRIPT.match(/function hallUrlOf\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
536
+ assert.ok(fn, 'hallUrlOf() must exist');
537
+ assert.match(fn, /var a = \$\('selfHall'\);/, 'the hub\'s own hall is read from the stamped DOM anchor, never assembled in script');
538
+ assert.match(fn, /safeHttpUrl\(a\.getAttribute\('href'\)\)/);
539
+ assert.match(fn, /safeHttpUrl\(p\.hall_origin\)/, 'a project\'s hall is the feed field, scheme-checked');
540
+ assert.match(fn, /hostOf\(hall\) !== hostOf\(site\)/, 'address-gated: no door onto the room you are in');
541
+ assert.doesNotMatch(SCRIPT, /'[^'\n]*\{\{buildersOrigin\}\}/, 'no brand token is ever emitted into a script string');
542
+ assert.equal((SCRIPT.match(/isMember\(rel\) \? hallUrlOf\(p\) : ''/g) || []).length, 2, 'member-gated on the card AND on the row, one rule');
543
+ assert.match(SCRIPT, /'<a class="cardHall" id="cardHall" href="' \+ esc\(hall\)/, 'the card\'s hall door is escaped');
544
+ assert.match(SCRIPT, /'<a class="rowHall" href="' \+ esc\(hall\) \+ '" target="_blank" rel="noopener"/, 'the row\'s hall glyph is escaped and opens a new tab');
545
+ assert.match(SCRIPT, /: ''\) \+ hallGlyphHtml\(p, rel\) \+ '<\/td>'/, 'the glyph rides the existing door cell — no seventh column');
546
+ });
547
+
548
+ test('the platform row carries a stamped, hidden hall glyph the session unhides — same tab, no target', () => {
549
+ const selfRow = (HUB.match(/<tr id="selfRow">([\s\S]*?)<\/tr>/) || [, ''])[1];
550
+ assert.match(selfRow, /<a class="rowHall" id="selfHall" href="\{\{buildersOrigin\}\}"[^>]*hidden>/, 'server-stamped and hidden until the session says member');
551
+ assert.match(SCRIPT, /if \(sh\) sh\.hidden = !badge \|\| !hallUrlOf\(SELF_PROJECT\);/, 'member-gated and address-gated, and null-safe: the stamp drops the anchor whole when a pack names no hall');
552
+ assert.match(SCRIPT, /if \(p\.isSelf\) \{\r?\n\s*var a = \$\('selfHall'\);/);
553
+ });
554
+
555
+ test('the card\'s doors: one accent door, the hall entry beside it as text, the platform\'s door same-tab', () => {
556
+ assert.match(SCRIPT, /'<a class="cardOpen" id="cardOpen" href="' \+ esc\(href\) \+ '"' \+\r?\n\s*\(p\.isSelf \? '' : ' target="_blank" rel="noopener"'\)/);
557
+ assert.match(SCRIPT, /\(p\.isSelf \? 'Home →' : 'Open site →'\)/);
558
+ assert.match(SCRIPT, /HALL_GLYPH \+ 'Builders\\' hall<\/a>'/, 'the hall entry carries the glyph and its name');
559
+ assert.match(COSMOS, /\.projCard \.cardOpen\{[^}]*background:var\(--accent\);color:var\(--accent-on\)/, 'the one accent door');
560
+ assert.match(COSMOS, /\.projCard \.cardHall\{[^}]*color:var\(--accent-deep\)[^}]*text-decoration:underline/, 'the hall entry is the footer-link grammar');
561
+ });
562
+
563
+ test('the platform answers its own figures by the same rule: faces from its leaderboard, shipped from its own feed', () => {
564
+ assert.match(SCRIPT, /SELF_PROJECT\.faces = real\.map\(function \(b\) \{ return safeHttpUrl\(b\.avatar_url\); \}\)\.filter\(Boolean\)\.slice\(0, FACES_MAX\);/);
565
+ const fn = (SCRIPT.match(/function loadSelfShipped\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
566
+ assert.ok(fn, 'loadSelfShipped() must exist');
567
+ assert.match(fn, /\/public\/recent-shipped\?days=30/, 'the window is asked for explicitly');
568
+ assert.match(fn, /Date\.parse\(t && t\.shipped_at\) >= since/, 'counted from shipped_at, inside the window');
569
+ assert.match(fn, /d\.tasks\.length >= 200 && n === d\.tasks\.length/, 'N+ at the feed\'s row cap');
570
+ assert.match(SCRIPT, /\.then\(loadSelfStats, loadSelfStats\)\r?\n\s*\.then\(loadSelfShipped, loadSelfShipped\);/, 'runs once at boot beside the leaderboard read');
571
+ });
572
+
573
+ test('the poster is in the world: 560 wide, one hairline, no shadow, the disc surviving over the art, the facts a ledger', () => {
574
+ const block = (COSMOS.match(/\.projCard\{[^}]*\}/) || [''])[0];
575
+ assert.match(block, /width:min\(560px,100%\)/);
576
+ assert.match(block, /border:1px solid var\(--rule\)/);
577
+ assert.match(block, /box-shadow:none/);
578
+ assert.match(block, /container-type:inline-size/, 'the disc and the callouts measure the card, not the viewport');
579
+ assert.match(COSMOS, /\.projCard \.cardArt\{[^}]*aspect-ratio:5\/2;object-fit:cover/, 'a fixed 5:2 crop');
580
+ assert.match(COSMOS, /\.projCard\.has-art \.cardDot\{[^}]*top:calc\(100cqw \/ 2\.5 - 27px\)[^}]*border:4px solid var\(--bg-card\)/, 'half over the banner\'s edge, ringed in the card ground');
581
+ assert.match(COSMOS, /\.projCard\.has-art \.cardCall::before\{[^}]*mask-image/, 'each callout carries a feathered patch of the card ground — a mask, not a shadow');
582
+ assert.match(COSMOS, /\.projCard \.cardMeta dt\{[^}]*font-family:var\(--label-face\)[^}]*border-top:1px solid var\(--rule-soft\)/, 'mono label on a hairline');
583
+ assert.match(COSMOS, /\.projCard \.cardMeta dd\{[^}]*text-align:right[^}]*border-top:1px solid var\(--rule-soft\)/, 'the value right and on the same hairline');
584
+ assert.match(COSMOS, /\.projCard \.cardRel\{[^}]*font-family:var\(--label-face\)/, 'the relationship is a mono label, not an accent pill');
585
+ assert.doesNotMatch(COSMOS, /\.projCard \.cardMeta \.tags/, 'the card no longer renders the module list');
586
+ assert.match(COSMOS, /\.rowHall\{[^}]*width:28px;height:28px/, 'the row glyph clears the 24px floor');
587
+ });
588
+
493
589
  test('the row keeps its own Open link working underneath the card', () => {
494
590
  assert.match(SCRIPT, /if \(ev\.target\.closest && ev\.target\.closest\('a'\)\) return;/,
495
591
  'a click on the row\'s own anchor must not be swallowed by the card handler');
@@ -592,10 +688,15 @@ test('each card fact is wired to its data, not just present in the markup', () =
592
688
  assert.match(SCRIPT, /function cardFact/, 'the card fact writer must exist');
593
689
  assert.match(SCRIPT, /dd\.hidden = \$\(id \+ 'L'\)\.hidden = !value/,
594
690
  'a fact with no answer hides label and value together — never a dash');
595
- assert.match(SCRIPT, /cardFact\('cardBuilders', p\.builders == null \? '' : String\(p\.builders\)\)/,
691
+ // since task 1003301 Builders is a FIGURE (a callout on the art), not a fact,
692
+ // and Runs on the poster is the project's ADDRESS — the table keeps the module list
693
+ assert.match(SCRIPT, /cardFig\('cardBuilders', p\.builders == null \? '' : String\(p\.builders\)\)/,
596
694
  'builders: 0 is a real answer, null hides');
597
- assert.match(SCRIPT, /cardFact\('cardRuns', Array\.isArray\(p\.tags\) \? tagsHtml\(p\.tags\) : ''/,
598
- 'runs go through the one shared formatter');
695
+ assert.match(SCRIPT, /cardFig\('cardShipped', shippedFigure\(p\)\)/, 'the second figure is the 30-day count');
696
+ assert.match(SCRIPT, /cardFact\('cardRuns', runsOf\(p\)\)/, 'runs is the address on the poster');
697
+ const runs = (SCRIPT.match(/function runsOf\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
698
+ assert.match(runs, /hostOf\(p\.origin\)/, 'the address is the catalog origin\'s host');
699
+ assert.match(SCRIPT, /'<td class="colTags">' \+ tagsHtml\(p\.tags\)/, 'the table row keeps the module list');
599
700
  assert.match(SCRIPT, /cardFact\('cardOwner', rel === 'owner' \? '' : ownerHtml\(p\), true\)/,
600
701
  'owner comes from the enrichment and yields to the rel chip saying "yours"');
601
702
  const oh = (SCRIPT.match(/function ownerHtml\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];