@bongos/core 1.19.281 → 1.19.283

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.
@@ -168,6 +168,8 @@
168
168
  active: { label: 'recently active', cmp: (a, b) => goalActivityMs(b) - goalActivityMs(a), stamp: (g) => ({ label: 'last work', ms: goalActivityMs(g) }) },
169
169
  };
170
170
  const GOAL_SORT_PREF = 'otb.goals.sort';
171
+ // task 1003136 — the list|cards view choice, persisted like the sort above.
172
+ const GOAL_VIEW_PREF = 'otb.goals.view';
171
173
  const currentGoalSort = () => GOAL_SORTS[$('#f-goal-order')?.value] || GOAL_SORTS.priority;
172
174
 
173
175
  // Stable: filteredGoals already returns the server's priority order, and
@@ -262,11 +264,10 @@
262
264
  }
263
265
  }
264
266
 
265
- // task 1760 — priority is a WITHIN-version ordering, so the drag affordance
266
- // only makes sense once the grid is narrowed to exactly one version (the
267
- // "any" filter mixes versions and dragging across them would be meaningless).
268
- // Priority is always visible in that state; only an Archon gets the handle —
269
- // the server-side rank gate on POST /goals/reorder is the real enforcement.
267
+ // Archon check for the detail view's manage/visibility affordances (the
268
+ // server-side rank gates are the real enforcement). The task-1760 drag-reorder
269
+ // affordance this once also served is retired — see the note where
270
+ // wireDragReorder used to live (task 1003136).
270
271
  const isArchon = () => (state.me?.builder?.rank || '').toLowerCase() === 'archon';
271
272
 
272
273
  // task 1002164 — confirming a criterion is Metic+ (POST /done-when/:id/satisfy,
@@ -321,13 +322,16 @@
321
322
  return ` · ${escapeHtml(label)} ${escapeHtml(ms ? fmtDate(new Date(ms).toISOString()) : '—')}`;
322
323
  }
323
324
 
324
- function goalRowHtml(g) {
325
+ // The facts a goal row/card renders, computed ONCE so the two views (task
326
+ // 1003136) can't drift: ring, meta line, roll line, and the single
327
+ // next-action ladder (achieved → criteria-met → claim-next → drill → quiet).
328
+ function goalRenderFacts(g, ringSize) {
325
329
  const d = state.details.get(String(g.id));
326
330
  const crit = d ? (d.criteria || []) : [];
327
331
  const sat = crit.filter((c) => c.satisfied).length;
328
332
  const r = rollupOf(g.id);
329
333
  const achieved = g.status === 'achieved';
330
- const ringHtml = crit.length ? ring(sat, crit.length, achieved, 44)
334
+ const ringHtml = crit.length ? ring(sat, crit.length, achieved, ringSize)
331
335
  : `<span class="gr-crit-none" aria-hidden="true">—</span>`;
332
336
  const goalHref = `#/goal/${escapeHtml(String(g.id))}`;
333
337
  let right;
@@ -343,6 +347,12 @@
343
347
  } else {
344
348
  right = `<span class="gr-quiet">No tasks yet</span>`;
345
349
  }
350
+ const metaHtml = `#${escapeHtml(String(g.id))} · ${escapeHtml(g.version_id || '')}${(achieved || g.status === 'open') ? '' : ' · ' + escapeHtml(g.status)}${goalStampHtml(g)}`;
351
+ return { r, achieved, ringHtml, goalHref, right, metaHtml };
352
+ }
353
+
354
+ function goalRowHtml(g) {
355
+ const { r, achieved, ringHtml, goalHref, right, metaHtml } = goalRenderFacts(g, 44);
346
356
  // NOTE: a rollup row nests real links (title → goal, Claim → task overlay,
347
357
  // drill → goal), so the row itself is a <div>, not an <a> — nested anchors
348
358
  // are invalid. Only the explicit links navigate.
@@ -350,13 +360,31 @@
350
360
  <div class="gr-ringcol">${ringHtml}</div>
351
361
  <div class="gr-main">
352
362
  <a class="gr-title" href="${goalHref}">${escapeHtml(g.title || 'Untitled goal')}</a>
353
- <div class="gr-meta">#${escapeHtml(String(g.id))} · ${escapeHtml(g.version_id || '')}${(achieved || g.status === 'open') ? '' : ' · ' + escapeHtml(g.status)}${goalStampHtml(g)}</div>
363
+ <div class="gr-meta">${metaHtml}</div>
354
364
  ${achieved ? '' : goalRollLine(r)}
355
365
  </div>
356
366
  <div class="gr-right">${right}</div>
357
367
  </div>`;
358
368
  }
359
369
 
370
+ // task 1003136 — the same goal as a CARD: identical facts as the row via
371
+ // goalRenderFacts, so nothing is lost switching views. Reuses the surviving
372
+ // .goal-card* CSS (goals.css — task 1002439 neutralised it, never deleted it).
373
+ // Deliberately emits NO draggable attribute: drag-to-reorder is retired, see
374
+ // the note where wireDragReorder used to live.
375
+ function goalCardHtml(g) {
376
+ const { r, achieved, ringHtml, goalHref, right, metaHtml } = goalRenderFacts(g, 36);
377
+ return `<div class="goal-card" data-goal-id="${escapeHtml(String(g.id))}">
378
+ <div class="goal-card__head">
379
+ <div class="gr-ringcol">${ringHtml}</div>
380
+ <a class="goal-card__title gr-title" href="${goalHref}">${escapeHtml(g.title || 'Untitled goal')}</a>
381
+ </div>
382
+ <div class="goal-card__meta">${metaHtml}</div>
383
+ ${achieved ? '' : goalRollLine(r)}
384
+ <div class="goal-card__foot">${right}</div>
385
+ </div>`;
386
+ }
387
+
360
388
  // task 1002439 — the Goals page, reorganised around momentum: a task-readiness
361
389
  // summary, then goals grouped Active (has ready/in-progress work) · Blocked
362
390
  // (only waiting-on-upstream tasks) · Achieved. "Blocked" here is a DERIVED
@@ -399,7 +427,15 @@
399
427
  </div>`;
400
428
 
401
429
  const sec = (label, n) => `<div class="gr-sec"><b>${label}</b><span class="gr-sec__n">${n}</span></div>`;
402
- const listOf = (arr) => `<div class="gr-list">${arr.map(goalRowHtml).join('')}</div>`;
430
+ // task 1003136 — the list|cards toggle swaps how the open groups render;
431
+ // grouping, filters, and sort run above the fork so both views share them.
432
+ // Achieved/archived pills stay pills (already the compact form). The inner
433
+ // div is class-only .goal-grid on purpose: task 1002439's block override
434
+ // targets #goal-grid.goal-grid, so the original card-grid CSS applies here.
435
+ const cardsView = ($('#f-goal-view')?.value || 'list') === 'cards';
436
+ const listOf = cardsView
437
+ ? (arr) => `<div class="goal-grid">${arr.map(goalCardHtml).join('')}</div>`
438
+ : (arr) => `<div class="gr-list">${arr.map(goalRowHtml).join('')}</div>`;
403
439
  // task 1002965 — an achieved/archived goal is a compact pill, so its sort
404
440
  // date rides as its own muted span (the row's `gr-meta` equivalent). The
405
441
  // pills row already wraps, so the extra text can't widen the page.
@@ -418,63 +454,14 @@
418
454
  + (archived.length ? sec('Archived', archived.length) + `<div class="gr-pills gr-pills--muted">${archived.map(pill).join('')}</div>` : '');
419
455
  }
420
456
 
421
- // ---- drag-to-reorder (task 1760, Archon-only, one version at a time) ------
422
- let dragEl = null;
423
-
424
- function wireDragReorder() {
425
- const grid = $('#goal-grid');
426
- if (!grid) return;
427
- grid.addEventListener('dragstart', (e) => {
428
- const el = e.target.closest('.goal-card');
429
- if (!el || !el.hasAttribute('draggable')) return;
430
- dragEl = el;
431
- el.classList.add('goal-card--dragging');
432
- e.dataTransfer.effectAllowed = 'move';
433
- });
434
- grid.addEventListener('dragover', (e) => {
435
- if (!dragEl) return;
436
- const over = e.target.closest('.goal-card');
437
- if (!over || over === dragEl || !grid.contains(over)) return;
438
- e.preventDefault();
439
- const rect = over.getBoundingClientRect();
440
- const after = (e.clientX - rect.left) > rect.width / 2;
441
- grid.insertBefore(dragEl, after ? over.nextSibling : over);
442
- });
443
- grid.addEventListener('drop', (e) => { if (dragEl) e.preventDefault(); });
444
- grid.addEventListener('dragend', async () => {
445
- if (!dragEl) return;
446
- const el = dragEl;
447
- el.classList.remove('goal-card--dragging');
448
- dragEl = null;
449
- const ver = $('#f-goal-version')?.value || '';
450
- if (!ver) return;
451
- // task 1932 — with pagination the grid shows only the current PAGE, so the
452
- // reorder must submit the FULL version order: the goals before this page +
453
- // the page's cards in their new DOM order + the goals after it. Without
454
- // this, POSTing only the visible page's ids would corrupt the priorities
455
- // of the goals on other pages. Priority numbers reflect the global
456
- // position (start + i + 1).
457
- const filtered = filteredGoals();
458
- const start = (pagers.list.page - 1) * pagers.list.perPage;
459
- const cards = [...grid.querySelectorAll('.goal-card')];
460
- cards.forEach((card, i) => {
461
- const num = card.querySelector('.goal-card__priority');
462
- if (num) num.textContent = String(start + i + 1);
463
- });
464
- const pageIds = cards.map((card) => Number(card.dataset.goalId));
465
- const beforeIds = filtered.slice(0, start).map((g) => Number(g.id));
466
- const afterIds = filtered.slice(start + pageIds.length).map((g) => Number(g.id));
467
- const orderedIds = [...beforeIds, ...pageIds, ...afterIds];
468
- try {
469
- await postJSON(`${API}/goals/reorder`, { version_id: ver, ordered_ids: orderedIds });
470
- toast('Priority order saved.');
471
- } catch (err) {
472
- toast((err && err.data && err.data.message) || 'Could not save the new order — reloading.');
473
- state.goals = (await getJSON(`${API}/goals`).catch(() => ({ goals: state.goals }))).goals || state.goals;
474
- renderList();
475
- }
476
- });
477
- }
457
+ // ---- drag-to-reorder: RETIRED (task 1003136; was task 1760) ---------------
458
+ // wireDragReorder() lived here, dead since task 1002439's grouped rollup (its
459
+ // .goal-card selector matched nothing). The card view revives .goal-card
460
+ // markup, so the decision is now DELIBERATE: the listener is DELETED — both
461
+ // views render grouped, sorted, filtered sets, so DOM order no longer
462
+ // expresses the version priority the old dragend POSTed to /goals/reorder;
463
+ // re-enabling it would submit a corrupted order. Cards emit no draggable
464
+ // attribute. POST /goals/reorder itself is untouched (Archon, pinned).
478
465
 
479
466
  /* ---------------- detail view ---------------- */
480
467
 
@@ -1822,7 +1809,12 @@
1822
1809
  // reads it, so keeping the pref off the URL leaves deep links shareable and
1823
1810
  // unchanged. Private-mode writes throw — swallowed, the pref is a nicety.
1824
1811
  const onListFilterChange = () => {
1825
- try { window.localStorage.setItem(GOAL_SORT_PREF, $('#f-goal-order')?.value || 'priority'); } catch (_) { /* private mode */ }
1812
+ try {
1813
+ window.localStorage.setItem(GOAL_SORT_PREF, $('#f-goal-order')?.value || 'priority');
1814
+ // task 1003136 — the list|cards view choice survives reload, same
1815
+ // precedent (localStorage, never the URL).
1816
+ window.localStorage.setItem(GOAL_VIEW_PREF, $('#f-goal-view')?.value || 'list');
1817
+ } catch (_) { /* private mode */ }
1826
1818
  pagers.list.page = 1;
1827
1819
  renderList();
1828
1820
  };
@@ -1833,7 +1825,11 @@
1833
1825
  // task 1002965 adds Sort (f-goal-order — NOT f-goal-sort, which is already
1834
1826
  // taken by the goal-detail To-do bar further down this file).
1835
1827
  let savedGoalSort = null;
1836
- try { savedGoalSort = window.localStorage.getItem(GOAL_SORT_PREF); } catch (_) { /* private mode */ }
1828
+ let savedGoalView = null;
1829
+ try {
1830
+ savedGoalSort = window.localStorage.getItem(GOAL_SORT_PREF);
1831
+ savedGoalView = window.localStorage.getItem(GOAL_VIEW_PREF);
1832
+ } catch (_) { /* private mode */ }
1837
1833
  // task 1003135 — Search declared on the kit spec (live 'input' listener comes
1838
1834
  // free, hall-kit.js:273; no debounce needed, the match is pure client-side over
1839
1835
  // the already-loaded state.goals). Id is f-goals-search — f-goal-search is
@@ -1853,6 +1849,12 @@
1853
1849
  label: 'Sort',
1854
1850
  value: GOAL_SORTS[savedGoalSort] ? savedGoalSort : undefined,
1855
1851
  options: Object.entries(GOAL_SORTS).map(([value, { label }]) => ({ value, label })) },
1852
+ // task 1003136 — list|cards toggle; renderList forks its group renderer
1853
+ // on this. Persisted via GOAL_VIEW_PREF, never the URL.
1854
+ { id: 'f-goal-view',
1855
+ label: 'View',
1856
+ value: savedGoalView === 'cards' ? 'cards' : undefined,
1857
+ options: [{ value: 'list', label: 'list' }, { value: 'cards', label: 'cards' }] },
1856
1858
  ],
1857
1859
  onChange: onListFilterChange,
1858
1860
  });
@@ -1981,7 +1983,6 @@
1981
1983
  if (denyBtn) { respondToJoinRequest(state.current, denyBtn.dataset.denyReq, 'deny', denyBtn); return; }
1982
1984
  });
1983
1985
  window.addEventListener('hashchange', route);
1984
- wireDragReorder();
1985
1986
 
1986
1987
  boot();
1987
1988
  })();
@@ -6,10 +6,12 @@
6
6
  are in style.css's hall-kit section. Everything derives from the shared
7
7
  design tokens so instance packs reskin this page along with the rest. */
8
8
 
9
- /* ---- goal-card grid (list view) ----
9
+ /* ---- goal-card grid (the CARDS view) ----
10
10
  task 2171 density pass: minmax 300→230 (four across at desktop width),
11
- tighter gaps/padding/type — the grid holds ~26 goals, so card height is
12
- what sets the page height. */
11
+ tighter gaps/padding/type. Retired by task 1002439's grouped rollup, REVIVED
12
+ by task 1003136 as the list|cards toggle's card view: renderList nests
13
+ id-less .goal-grid wrappers per group, which this rule styles — the
14
+ #goal-grid override further down only flattens the OUTER container. */
13
15
  .goal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
14
16
  .goal-card {
15
17
  display: flex; flex-direction: column; gap: 7px;
@@ -19,16 +21,9 @@
19
21
  }
20
22
  a.goal-card:hover { text-decoration: none; border-color: var(--border-strong); background: var(--surface-2); }
21
23
  .goal-card__head { display: flex; align-items: flex-start; gap: 9px; }
22
- /* task 1760 — priority (visible once narrowed to one version) + Archon
23
- drag-to-reorder handle (server enforces the write; this is the affordance). */
24
- .goal-card__handle { flex: none; cursor: grab; color: var(--ink-faint); font-size: 14px; line-height: 1.2; touch-action: none; }
25
- .goal-card__handle:active { cursor: grabbing; }
26
- .goal-card__priority {
27
- flex: none; min-width: 1.4em; text-align: center;
28
- font-size: 11.5px; font-weight: 600; color: var(--ink-faint);
29
- font-variant-numeric: tabular-nums;
30
- }
31
- .goal-card--dragging { opacity: 0.45; }
24
+ /* task 1003136: the task-1760 drag handle/priority/dragging rules that lived
25
+ here are deleted with wireDragReorder (see goals-page.js) — the revived
26
+ cards deliberately carry no drag affordance. */
32
27
  /* task 1761: a goal blocked by an unachieved prerequisite is dimmed + hue-shifted. */
33
28
  .goal-card--blocked { opacity: 0.62; border-style: dashed; }
34
29
  .goal-card--blocked:hover { opacity: 0.85; }
@@ -209,8 +204,10 @@ a.goal-card:hover { text-decoration: none; border-color: var(--border-strong); b
209
204
  tasks up into ready/in-progress/blocked; the task→task dependency
210
205
  graph itself lives on the Tasks page. Uses the shared style.css tokens.
211
206
  ============================================================ */
212
- /* The list container used to be an auto-fill CARD grid; the rollup renders
213
- grouped full-width blocks into it, so flip it to normal block flow. */
207
+ /* The OUTER container renders grouped full-width blocks (summary + section
208
+ heads), so flip it to normal block flow. Id-selected on purpose: the cards
209
+ view (task 1003136) nests id-less .goal-grid wrappers per group, which keep
210
+ the real card grid above. */
214
211
  #goal-grid.goal-grid { display: block; gap: 0; }
215
212
  .gr-summary { display: flex; flex-wrap: wrap; gap: 22px; margin: 6px 0 20px; }
216
213
  .gr-sm { display: flex; align-items: baseline; gap: 8px; font-size: 13px; color: var(--ink-soft); }
@@ -199,6 +199,6 @@
199
199
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
200
200
  <script src="/builders/brand-apply.js?v=2026-06-27-r75"></script>
201
201
  <script src="/builders/goals-lib.js?v=2026-08-21-goalslib1"></script>
202
- <script src="/builders/goals-page.js?v=2026-08-21-goals6"></script>
202
+ <script src="/builders/goals-page.js?v=2026-08-21-goals7"></script>
203
203
  </body>
204
204
  </html>
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.281",
3
+ "version": "1.19.283",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.281",
9
+ "version": "1.19.283",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.281",
3
+ "version": "1.19.283",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -989,8 +989,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
989
989
  // landed since 1.19.279 with no explicit bump. run 32523185119. (task 1002620)
990
990
  // 1.19.281 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
991
991
  // landed since 1.19.280 with no explicit bump. run 32525071763. (task 1002620)
992
+ // 1.19.282 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
993
+ // landed since 1.19.281 with no explicit bump. run 32526392955. (task 1002620)
994
+ // 1.19.283 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
995
+ // landed since 1.19.282 with no explicit bump. run 32527313879. (task 1002620)
992
996
  // ---------------------------------------------------------------------------
993
- const CORE_VERSION = '1.19.281'; // CI auto-patch carrier (ADR 0161); see changelog above
997
+ const CORE_VERSION = '1.19.283'; // CI auto-patch carrier (ADR 0161); see changelog above
994
998
 
995
999
  // A namespaced logger so a module's log lines are attributable + consistent.
996
1000
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,61 @@
1
+ // tests/goals_card_view.mjs — the Goals page list|cards toggle (task 1003136).
2
+ //
3
+ // Source-contract pins over goals-page.js: the DONE-WHEN properties that a DOM
4
+ // harness can't cheaply hold are exactly the ones that rot silently — the two
5
+ // views sharing one fact source, the drag landmine staying DELIBERATELY dead,
6
+ // and the view choice persisting off-URL. The interactive render itself is
7
+ // exercised by tests/goal_criterion_confirm_ui.mjs, which runs the real page.
8
+ //
9
+ // Run: node tests/goals_card_view.mjs
10
+
11
+ import { strict as assert } from 'node:assert';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { makeRunner } from './helpers.mjs';
16
+
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+ const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-page.js'), 'utf8');
19
+ const CSS = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals.css'), 'utf8');
20
+ const { test, summary } = makeRunner();
21
+
22
+ await test('both views render from ONE fact source (goalRenderFacts) so they cannot drift', () => {
23
+ assert.match(SRC, /function goalRenderFacts\(/, 'the shared fact fn must exist');
24
+ const rowBody = SRC.slice(SRC.indexOf('function goalRowHtml'), SRC.indexOf('function goalCardHtml'));
25
+ assert.match(rowBody, /goalRenderFacts\(g/, 'the row must consume the shared facts');
26
+ const cardBody = SRC.slice(SRC.indexOf('function goalCardHtml'));
27
+ assert.match(cardBody.slice(0, 1200), /goalRenderFacts\(g/, 'the card must consume the shared facts');
28
+ });
29
+
30
+ await test('renderList forks on the f-goal-view toggle AFTER grouping/filter/sort (both views share them)', () => {
31
+ const body = SRC.slice(SRC.indexOf('function renderList'), SRC.indexOf('drag-to-reorder: RETIRED'));
32
+ assert.match(body, /#f-goal-view/, 'the fork reads the toggle');
33
+ const sortIdx = body.indexOf('sortGoals(');
34
+ const forkIdx = body.indexOf('#f-goal-view');
35
+ assert.ok(sortIdx >= 0 && forkIdx > sortIdx, 'grouping/sort must run before the view fork');
36
+ assert.match(body, /goalCardHtml/, 'cards view renders goalCardHtml');
37
+ assert.match(body, /goalRowHtml/, 'list view still renders goalRowHtml');
38
+ assert.match(body, /<div class="goal-grid">/, 'cards nest an id-less .goal-grid so the block override does not flatten them');
39
+ });
40
+
41
+ await test('the view choice persists via localStorage (otb.goals.view), never the URL', () => {
42
+ assert.match(SRC, /GOAL_VIEW_PREF = 'otb\.goals\.view'/);
43
+ assert.match(SRC, /setItem\(GOAL_VIEW_PREF/, 'onChange writes the pref');
44
+ assert.match(SRC, /getItem\(GOAL_VIEW_PREF\)/, 'boot reads the pref back');
45
+ });
46
+
47
+ await test('drag-to-reorder is deliberately retired: no listener, no draggable cards, decision stated', () => {
48
+ assert.ok(!/function wireDragReorder/.test(SRC), 'wireDragReorder must stay deleted');
49
+ assert.ok(!/addEventListener\('drag/.test(SRC), 'no drag listeners anywhere on the page');
50
+ const cardBody = SRC.slice(SRC.indexOf('function goalCardHtml'), SRC.indexOf('function renderList'));
51
+ assert.ok(!/draggable/.test(cardBody), 'goalCardHtml must not emit a draggable attribute');
52
+ assert.match(SRC, /RETIRED \(task 1003136/, 'the retirement decision is stated where the code lived');
53
+ });
54
+
55
+ await test('goals.css: the block override stays id-scoped so inner .goal-grid card grids survive', () => {
56
+ assert.match(CSS, /#goal-grid\.goal-grid \{ display: block/, 'the outer flatten must stay id-selected');
57
+ assert.match(CSS, /\.goal-grid \{ display: grid; grid-template-columns: repeat\(auto-fill/, 'the card grid rule must survive');
58
+ assert.ok(!/goal-card__handle|goal-card--dragging/.test(CSS), 'the drag affordance CSS is gone with the listener');
59
+ });
60
+
61
+ summary();