create-pmos 0.133.0 → 0.135.0
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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +35 -2
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-audio.js +28 -9
- package/workspace/lib/notes-capture.js +136 -0
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-live-guard.js +117 -0
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +139 -16
- package/workspace/lib/notes.js +4 -2
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +4 -3
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +96 -13
- package/workspace/lib/routes-c3-strategy.js +4 -3
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +113 -15
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +3 -2
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +5 -4
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +26 -13
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +118 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +148 -17
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +53 -11
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +14 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +12 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* PMOS workspace — the Backlog, Board and Timeline tabs of a board on the console3 Initiatives page (mockup v5, 34-initiatives.js
|
|
3
|
+
* tabBacklog, tabBoard and tabTimeline), in demo mode (browser). Jira items and PMOS tasks are mixed: a PMOS task moves (drag or its
|
|
4
|
+
* menu) and reschedules (an arrow key on its bar, with Undo), a Jira item is locked and says so. The Initiative and Source filters
|
|
5
|
+
* are shared by the three tabs; a board with Build and deliver off has no Source filter. Backlog adds a PMOS task. The Timeline has
|
|
6
|
+
* zoom buttons, a Today line and release markers.
|
|
7
|
+
*
|
|
8
|
+
* The files it drives (named so `run-tests.js --changed` traces a change in any of them to this suite): public/pm/c3-ib-items.js,
|
|
9
|
+
* c3-ini-timeline.js, c3-ini-work.js, c3-ini-ui.js, c3-initiatives.js, c3-initiatives.css, c3-demo-boards.js, c3-demo.js, c3-toast.js
|
|
10
|
+
* and c3-ini-harness.js. Its list fragment is test-lists/c3-ib-items.json.
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
const { harness } = require('./c3-ini-harness');
|
|
14
|
+
|
|
15
|
+
(async () => {
|
|
16
|
+
const H = await harness('c3-ib-items');
|
|
17
|
+
const { page, ok, until, st, open, toast } = H;
|
|
18
|
+
const task = (id) => st((x) => { const t = window.__c3boards.tasks.find((k) => k.id === x); return t ? { state: t.state, start: t.start, due: t.due } : null; }, id);
|
|
19
|
+
const colOf = (uid) => st((u) => { const c = document.querySelector('[data-ini-card="' + u + '"]'); return c ? c.closest('[data-ini-col]').dataset.iniCol : ''; }, uid);
|
|
20
|
+
const waitToast = (re) => until(async () => { const t = await toast(); return re.test(t) && t; });
|
|
21
|
+
|
|
22
|
+
/* ── Board ── */
|
|
23
|
+
await open('board:idpf:board');
|
|
24
|
+
await until(() => st(() => document.querySelectorAll('[data-ini-col]').length === 4));
|
|
25
|
+
const cols = await st(() => [...document.querySelectorAll('[data-ini-col]')].map((c) => c.dataset.iniCol).join(','));
|
|
26
|
+
ok('K1 the Board has one column per item status', cols === 'To do,In progress,In review,Done', cols);
|
|
27
|
+
const mix = await st(() => { const s = [...document.querySelectorAll('[data-ini-card]')].map((c) => c.dataset.src); return s.includes('jira') && s.includes('pmos'); });
|
|
28
|
+
ok('K2 Jira items and PMOS tasks share the columns', mix);
|
|
29
|
+
ok('K3 only a PMOS task has a Move menu', await st(() => !!document.querySelector('[data-ini-card="t-idpf-2"] [data-wk-move]') && !document.querySelector('[data-ini-card="IDPF-1201"] [data-wk-move]')));
|
|
30
|
+
|
|
31
|
+
await page.click('[data-wk-move="t-idpf-2"]');
|
|
32
|
+
await until(() => st(() => !!document.querySelector('[data-wk-state="In review"]')));
|
|
33
|
+
await page.click('[data-wk-state="In review"]');
|
|
34
|
+
await until(async () => (await task('t-idpf-2')).state === 'In review');
|
|
35
|
+
const t4 = await waitToast(/moved to In review/);
|
|
36
|
+
ok('K4 the menu moves a PMOS task, the store records it and the toast says so', !!t4 && await until(async () => (await colOf('t-idpf-2')) === 'In review'), t4);
|
|
37
|
+
|
|
38
|
+
// a real HTML5 drag: the card is scrolled to the middle first (at the bottom edge of the view the press lands on its neighbour), and
|
|
39
|
+
// it is pressed on its own padding (its centre is the title link) and dropped on the column header
|
|
40
|
+
const drag = async (uid, col) => {
|
|
41
|
+
await st((u) => document.querySelector('[data-ini-card="' + u + '"]').scrollIntoView({ block: 'center' }), uid);
|
|
42
|
+
await page.dragAndDrop('[data-ini-card="' + uid + '"]', '[data-ini-col="' + col + '"]', { sourcePosition: { x: 4, y: 20 }, targetPosition: { x: 30, y: 20 } });
|
|
43
|
+
};
|
|
44
|
+
await drag('t-idpf-4', 'Done');
|
|
45
|
+
await until(async () => (await task('t-idpf-4')).state === 'Done');
|
|
46
|
+
ok('K5 a drag to another column moves a PMOS task', (await task('t-idpf-4')).state === 'Done' && await until(async () => (await colOf('t-idpf-4')) === 'Done'));
|
|
47
|
+
|
|
48
|
+
await drag('IDPF-1201', 'In review');
|
|
49
|
+
const t6 = await waitToast(/IDPF-1201 is a Jira item/);
|
|
50
|
+
ok('K6 a Jira item dropped on a column stays put and the toast says it changes in Jira', !!t6 && (await colOf('IDPF-1201')) === 'To do', t6);
|
|
51
|
+
|
|
52
|
+
/* ── shared filters ── */
|
|
53
|
+
await page.click('[data-wk-ini="IDPF-21"]');
|
|
54
|
+
await until(() => st(() => document.querySelector('[data-wk-ini="IDPF-21"]').getAttribute('aria-pressed') === 'true'));
|
|
55
|
+
const only21 = await st(() => [...document.querySelectorAll('[data-ini-card]')].map((c) => c.dataset.iniCard).join(','));
|
|
56
|
+
ok('F1 the Initiative filter keeps that initiative\'s items', only21 === 't-idpf-3' || /^(t-idpf-3,?)+$/.test(only21) || (only21.length > 0 && await st(() => [...document.querySelectorAll('[data-ini-card] [data-ini-open]')].every((n) => n.dataset.iniOpen === 'IDPF-21'))), only21);
|
|
57
|
+
await page.click('[data-wk-ini=""]');
|
|
58
|
+
await page.click('[data-wk-src="pmos"]');
|
|
59
|
+
await until(() => st(() => document.querySelector('[data-wk-src="pmos"]').getAttribute('aria-pressed') === 'true'));
|
|
60
|
+
ok('F2 the Source filter keeps PMOS tasks only', await st(() => { const s = [...document.querySelectorAll('[data-ini-card]')].map((c) => c.dataset.src); return s.length > 0 && s.every((x) => x === 'pmos'); }));
|
|
61
|
+
await page.click('[data-ini-tab="backlog"]');
|
|
62
|
+
await until(() => st(() => !!document.querySelector('[data-wk-tab="backlog"]')));
|
|
63
|
+
ok('F3 the filters stay on when the tab changes to Backlog', await st(() => document.querySelector('[data-wk-src="pmos"]').getAttribute('aria-pressed') === 'true' && [...document.querySelectorAll('[data-ini-item]')].every((n) => n.dataset.src === 'pmos')));
|
|
64
|
+
await page.click('[data-wk="clear"]');
|
|
65
|
+
await until(() => st(() => document.querySelector('[data-wk-src=""]').getAttribute('aria-pressed') === 'true'));
|
|
66
|
+
|
|
67
|
+
/* ── Backlog: add a PMOS task ── */
|
|
68
|
+
const groups = await st(() => [...document.querySelectorAll('[data-wk-group]')].map((g) => g.dataset.wkGroup).join(','));
|
|
69
|
+
ok('B1 Backlog groups the items by initiative', groups.split(',').length >= 2, groups);
|
|
70
|
+
await page.fill('[data-wk="add-title"]', 'Write the beta FAQ');
|
|
71
|
+
await page.selectOption('[data-wk="add-ini"]', 'IDPF-12');
|
|
72
|
+
await page.click('[data-wk="add-go"]');
|
|
73
|
+
await until(() => st(() => window.__c3boards.tasks.some((t) => t.title === 'Write the beta FAQ' && t.initiative === 'IDPF-12')));
|
|
74
|
+
ok('B2 Add stores a PMOS task on the initiative and shows it in its group', await until(() => st(() => [...document.querySelectorAll('[data-wk-group="IDPF-12"] [data-ini-item]')].some((n) => /Write the beta FAQ/.test(n.textContent)))));
|
|
75
|
+
|
|
76
|
+
/* ── Timeline ── */
|
|
77
|
+
await page.click('[data-ini-tab="timeline"]');
|
|
78
|
+
await until(() => st(() => !!document.querySelector('[data-wk-tab="timeline"] [data-tldrag="t-idpf-1"]')));
|
|
79
|
+
ok('T1 the Timeline draws a Today line and the release markers', await st(() => !!document.querySelector('.ini-tlnow') && document.querySelectorAll('.ini-tlm.release').length > 0));
|
|
80
|
+
ok('T2 PMOS bars can be dragged and Jira bars carry a lock', await st(() => document.querySelectorAll('[data-tldrag]').length > 0 && document.querySelectorAll('[data-tllock]').length > 0));
|
|
81
|
+
const before = await task('t-idpf-1');
|
|
82
|
+
await page.focus('[data-tldrag="t-idpf-1"]');
|
|
83
|
+
await page.keyboard.press('ArrowRight');
|
|
84
|
+
await until(async () => (await task('t-idpf-1')).start !== before.start);
|
|
85
|
+
const after = await task('t-idpf-1');
|
|
86
|
+
const t3 = await waitToast(/Rescheduled/);
|
|
87
|
+
ok('T3 ArrowRight on a PMOS bar moves the task one day and the toast offers Undo', after.start > before.start && after.due > before.due && !!t3 && /Undo/.test(t3), JSON.stringify([before, after, t3]));
|
|
88
|
+
await page.click('[data-c3-toast] button:has-text("Undo")').catch(() => {});
|
|
89
|
+
await until(async () => (await task('t-idpf-1')).start === before.start);
|
|
90
|
+
ok('T4 Undo puts the dates back', (await task('t-idpf-1')).start === before.start);
|
|
91
|
+
await until(() => st(() => !!document.querySelector('[data-tllock="IDPF-1190"]')));
|
|
92
|
+
await page.focus('[data-tllock="IDPF-1190"]');
|
|
93
|
+
await page.keyboard.press('ArrowRight');
|
|
94
|
+
const t5 = await waitToast(/IDPF-1190 is a Jira item/);
|
|
95
|
+
ok('T5 an arrow key on a Jira bar is refused with the locked toast', !!t5, t5);
|
|
96
|
+
await page.click('[data-tlzoom="week"]');
|
|
97
|
+
ok('T6 the zoom buttons change the day width', await until(() => st(() => document.querySelector('[data-tlzoom="week"]').getAttribute('aria-pressed') === 'true' && getComputedStyle(document.querySelector('.ini-tl')).getPropertyValue('--in-pd').trim() !== '6')));
|
|
98
|
+
|
|
99
|
+
/* ── a board with Build and deliver off ── */
|
|
100
|
+
await open('board:office:backlog');
|
|
101
|
+
await until(() => st(() => !!document.querySelector('[data-wk-tab="backlog"]')));
|
|
102
|
+
ok('O1 with Build and deliver off there is no Source filter and the note says why', await st(() => !document.querySelector('[data-wk="srcs"]') && /Build and deliver is off/.test(document.querySelector('[data-wk="no-jira"]').textContent)));
|
|
103
|
+
ok('O2 every item there is a PMOS task', await st(() => { const s = [...document.querySelectorAll('[data-ini-item]')].map((n) => n.dataset.src); return s.length > 0 && s.every((x) => x === 'pmos'); }));
|
|
104
|
+
await H.finish();
|
|
105
|
+
})();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* PMOS workspace — the Releases, Docs and Sprints tabs of a board, the six tabs of an initiative, and the stage records on an
|
|
3
|
+
* initiative's Overview (problem statement, RICE, launch checklist, kill conditions, learnings, sprint burndown) on the console3
|
|
4
|
+
* Initiatives page (mockup v5, 34-initiatives.js), in demo mode (browser). c3-demo-boards.js answers /api/boards and /api/stage-tools
|
|
5
|
+
* with the shapes of the stage-records route contract ("As built"); when the routes answer 404 (they are not on this server yet) the
|
|
6
|
+
* record cards say so quietly.
|
|
7
|
+
*
|
|
8
|
+
* The files it drives (named so `run-tests.js --changed` traces a change in any of them to this suite): public/pm/c3-ib-releases.js,
|
|
9
|
+
* c3-ib-docs.js, c3-ib-sprints.js, c3-ii-tabs.js, c3-ini-records.js, c3-ini-work.js, c3-ini-timeline.js, c3-initiatives.js,
|
|
10
|
+
* c3-initiatives.css, c3-demo-boards.js, c3-demo.js, c3-toast.js and c3-ini-harness.js. Its list fragment is test-lists/c3-ib-more.json.
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
const { harness } = require('./c3-ini-harness');
|
|
14
|
+
|
|
15
|
+
(async () => {
|
|
16
|
+
const H = await harness('c3-ib-more');
|
|
17
|
+
const { page, ok, until, st, open, toast } = H;
|
|
18
|
+
const rec = (id, tool) => st(([i, t]) => { const r = (window.__c3boards.records[i] || {})[t]; return r ? r.data : null; }, [id, tool]);
|
|
19
|
+
const waitToast = (re) => until(async () => { const t = await toast(); return re.test(t) && t; });
|
|
20
|
+
const text = (sel) => st((s) => { const n = document.querySelector(s); return n ? n.textContent : ''; }, sel);
|
|
21
|
+
|
|
22
|
+
/* ── Releases ── */
|
|
23
|
+
await open('board:idpf:releases');
|
|
24
|
+
await until(() => st(() => !!document.querySelector('[data-inb="release-next"]')));
|
|
25
|
+
const when = await st(() => [...document.querySelectorAll('[data-inb-release]')].map((n) => n.dataset.inbRelease + ':' + n.dataset.inbWhen).join(','));
|
|
26
|
+
ok('R1 Releases shows the next release first, then a later one and the past ones', /^rel-idpf-31:next,rel-idpf-32:later,rel-idpf-30:past$/.test(when), when);
|
|
27
|
+
ok('R2 each release lists its scope initiatives as links', await st(() => !!document.querySelector('[data-inb-release="rel-idpf-31"] [data-inb-scope="IDPF-12"]')));
|
|
28
|
+
await page.click('[data-inb-release="rel-idpf-31"] [data-inb-scope="IDPF-12"]').catch(async () => { await page.click('[data-inb-release="rel-idpf-31"] .tt'); await page.click('[data-inb-release="rel-idpf-31"] [data-inb-scope="IDPF-12"]'); });
|
|
29
|
+
ok('R3 a scope link opens the initiative', await until(() => st(() => /ini:idpf:IDPF-12/.test(decodeURIComponent(location.search)))));
|
|
30
|
+
|
|
31
|
+
/* ── Docs ── */
|
|
32
|
+
await open('board:idpf:docs');
|
|
33
|
+
await until(() => st(() => !!document.querySelector('[data-inb="docs-list"]')));
|
|
34
|
+
const grps = await st(() => [...document.querySelectorAll('[data-inb-docgrp]')].map((n) => n.dataset.inbDocgrp).join(','));
|
|
35
|
+
ok('D1 Docs groups the documents by initiative', grps === 'IDPF-12,IDPF-21', grps);
|
|
36
|
+
await page.fill('[data-inb="docs-search"]', 'rubric');
|
|
37
|
+
ok('D2 the search keeps the matching documents', await until(() => st(() => { const r = [...document.querySelectorAll('[data-inb-docrow]')].map((n) => n.dataset.inbDocrow); return r.length === 1 && r[0] === 'planning/ib/idpf-12-rubric.md'; })));
|
|
38
|
+
await page.fill('[data-inb="docs-search"]', 'zzz-nothing');
|
|
39
|
+
ok('D3 no match says so', await until(() => st(() => !!document.querySelector('[data-inb="docs-none"]'))));
|
|
40
|
+
|
|
41
|
+
/* ── Sprints, with the burndown card ── */
|
|
42
|
+
await open('board:idpf:sprints');
|
|
43
|
+
await until(() => st(() => !!document.querySelector('[data-inb="sprints-source"]')));
|
|
44
|
+
ok('SP1 Sprints says it is mirrored from Jira, read-only', /Mirrored from Jira, read-only/.test(await text('[data-inb="sprints-source"]')));
|
|
45
|
+
ok('SP2 the active sprint is drawn with its counts; future and closed sprints are listed', await st(() => !!document.querySelector('[data-inb-state="active"]') && !!document.querySelector('[data-inb-state="future"]') && !!document.querySelector('[data-inb-state="closed"]') && /Items12/.test(document.querySelector('[data-inb="sprint-counts"]').textContent)));
|
|
46
|
+
ok('SP3 the sprint burndown card draws the ideal and actual lines', await until(() => st(() => !!document.querySelector('[data-ini-tool-card="burndown"] svg.ini-burn path.ideal') && !!document.querySelector('[data-ini-tool-card="burndown"] svg.ini-burn path.act'))));
|
|
47
|
+
|
|
48
|
+
/* ── an initiative's six tabs ── */
|
|
49
|
+
await open('ini:idpf:IDPF-12:overview');
|
|
50
|
+
const itabs = await st(() => [...document.querySelectorAll('[data-ini="ini-tabs"] [data-ini-tab]')].map((n) => n.dataset.iniTab).join(','));
|
|
51
|
+
ok('IT1 an initiative has six tabs', itabs === 'overview,breakdown,docs,tickets,timeline,history', itabs);
|
|
52
|
+
const cards = await until(() => st(() => [...document.querySelectorAll('[data-ini-tool-card]')].map((n) => n.dataset.iniToolCard).join(',')));
|
|
53
|
+
ok('IT2 Overview of a Do initiative carries the launch checklist and the sprint burndown', /launch/.test(cards) && /burndown/.test(cards), cards);
|
|
54
|
+
ok('IT3 Overview links the board\'s Backlog, Board and Timeline', (await st(() => [...document.querySelectorAll('[data-ini-boardlink]')].map((n) => n.dataset.iniBoardlink).join(','))) === 'backlog,board,timeline');
|
|
55
|
+
await page.click('[data-ini="ini-tabs"] [data-ini-tab="breakdown"]');
|
|
56
|
+
ok('IT4 Breakdown lists the epics and items', await until(() => st(() => document.querySelectorAll('[data-ini="breakdown"] [data-ini-item]').length > 0)));
|
|
57
|
+
await page.fill('[data-wk="add-title"]', 'Draft the rollback runbook');
|
|
58
|
+
await page.click('[data-wk="add-go"]');
|
|
59
|
+
ok('IT5 Breakdown adds a PMOS task to the initiative', await until(() => st(() => window.__c3boards.tasks.some((t) => t.title === 'Draft the rollback runbook' && t.initiative === 'IDPF-12'))));
|
|
60
|
+
await page.click('[data-ini="ini-tabs"] [data-ini-tab="docs"]');
|
|
61
|
+
ok('IT6 Docs lists the initiative\'s documents', await until(() => st(() => document.querySelectorAll('[data-inb-docrow]').length === 3)));
|
|
62
|
+
await page.click('[data-ini="ini-tabs"] [data-ini-tab="tickets"]');
|
|
63
|
+
ok('IT7 Tickets lists the linked Jira items', await until(() => st(() => { const r = [...document.querySelectorAll('[data-ini="tickets"] [data-ini-item]')]; return r.length > 0 && r.every((n) => n.dataset.src === 'jira'); })));
|
|
64
|
+
await page.click('[data-ini="ini-tabs"] [data-ini-tab="timeline"]');
|
|
65
|
+
ok('IT8 Timeline draws the epic and item bars', await until(() => st(() => document.querySelectorAll('.ini-tl [data-tlb]').length > 0)));
|
|
66
|
+
await page.click('[data-ini="ini-tabs"] [data-ini-tab="history"]');
|
|
67
|
+
ok('IT9 History lists the recorded moves', await until(() => st(() => !!document.querySelector('[data-inb="hist-list"]'))));
|
|
68
|
+
await open('ini:idpf:IDPF-07:docs');
|
|
69
|
+
ok('IT10 an initiative with no documents says so', await until(() => st(() => !!document.querySelector('[data-inb="docs-empty"]'))));
|
|
70
|
+
await open('ini:idpf:IDPF-21:tickets');
|
|
71
|
+
ok('IT11 an initiative with no Jira items says so', await until(() => st(() => !!document.querySelector('[data-inb="tk-empty"]'))));
|
|
72
|
+
|
|
73
|
+
/* ── stage records ── */
|
|
74
|
+
// problem statement (Discover)
|
|
75
|
+
await open('ini:idpf:IDPF-21:overview');
|
|
76
|
+
await until(() => st(() => !!document.querySelector('[data-rec-field="problem"]')));
|
|
77
|
+
await page.fill('[data-rec-field="problem"]', 'Large API users cannot forecast spend under a flat fee.');
|
|
78
|
+
await page.click('[data-rec-save="problem"]');
|
|
79
|
+
await until(async () => ((await rec('IDPF-21', 'problem')) || {}).text === 'Large API users cannot forecast spend under a flat fee.');
|
|
80
|
+
ok('RC1 Save stores the problem statement and the toast says so', !!(await waitToast(/Problem statement saved/)));
|
|
81
|
+
|
|
82
|
+
// RICE (Plan): live score and rank among the board's scored initiatives
|
|
83
|
+
await open('ini:idpf:IDPF-07:overview');
|
|
84
|
+
await until(() => st(() => /Ranks/.test((document.querySelector('[data-rec="rice-rank"]') || {}).textContent || '')));
|
|
85
|
+
ok('RC2 RICE shows the score and its rank on the board', (await text('[data-rec="rice-score"]')) === '420' && /Ranks 2 of 2/.test(await text('[data-rec="rice-rank"]')), await text('[data-rec="rice-rank"]'));
|
|
86
|
+
await page.fill('[data-rec-rice="reach"]', '1200');
|
|
87
|
+
ok('RC3 the score follows the inputs as they are typed', await until(async () => (await text('[data-rec="rice-score"]')) === '840'), await text('[data-rec="rice-score"]'));
|
|
88
|
+
await page.click('[data-rec-save="rice"]');
|
|
89
|
+
await until(async () => ((await rec('IDPF-07', 'rice')) || {}).reach === 1200);
|
|
90
|
+
ok('RC4 Save stores RICE, and the rank is read again', !!(await waitToast(/RICE saved: score 840/)) && await until(async () => /Ranks 1 of 2/.test(await text('[data-rec="rice-rank"]'))), await text('[data-rec="rice-rank"]'));
|
|
91
|
+
|
|
92
|
+
// launch checklist (Do), kill conditions and learnings (picked from More tools)
|
|
93
|
+
await open('ini:idpf:IDPF-12:overview:kill');
|
|
94
|
+
await until(() => st(() => document.querySelectorAll('[data-rec-check]').length === 4));
|
|
95
|
+
ok('RC5 a picked tool from another stage is drawn first', (await st(() => document.querySelector('[data-ini-tool-card]').dataset.iniToolCard)) === 'kill');
|
|
96
|
+
await page.click('[data-rec-check="1"]');
|
|
97
|
+
ok('RC6 a launch check is stored', await until(async () => { const d = await rec('IDPF-12', 'launch'); return d && d.items[1].done === true; }));
|
|
98
|
+
await page.click('[data-rec-kill="0"] [data-rec-killst="tripped"]');
|
|
99
|
+
await until(async () => { const d = await rec('IDPF-12', 'kill'); return d && d.items[0].status === 'tripped'; });
|
|
100
|
+
ok('RC7 a kill condition set to Tripped is stored and the toast asks for a decision', !!(await waitToast(/Kill condition tripped/)) && await until(() => st(() => document.querySelector('[data-rec-kill="0"] [data-rec-killst="tripped"]').getAttribute('aria-pressed') === 'true')));
|
|
101
|
+
await open('ini:idpf:IDPF-12:overview:learnings');
|
|
102
|
+
await until(() => st(() => !!document.querySelector('[data-rec-add="learnings"]')));
|
|
103
|
+
await page.fill('[data-rec-add="learnings"]', 'Brief Support a week before a beta, not a day.');
|
|
104
|
+
await page.click('[data-rec-addgo="learnings"]');
|
|
105
|
+
ok('RC8 a learning is added and listed', await until(async () => { const d = await rec('IDPF-12', 'learnings'); return d && d.items.some((x) => /Brief Support a week/.test(x.text)); }) && await until(() => st(() => [...document.querySelectorAll('[data-rec-learning]')].some((n) => /Brief Support a week/.test(n.textContent)))));
|
|
106
|
+
|
|
107
|
+
// the routes are not on this server: 404 -> the cards say so quietly (no reload: the fetch wrapper lives in the page)
|
|
108
|
+
await st(() => { const prev = window.fetch; window.fetch = (input, init) => (/\/api\/stage-tools\//.test(String((input && input.url) || input)) ? Promise.resolve(new Response('{"ok":false,"error":"Not found"}', { status: 404, headers: { 'content-type': 'application/json' } })) : prev(input, init)); });
|
|
109
|
+
await page.click('[data-ini="ini-tabs"] [data-ini-tab="docs"]');
|
|
110
|
+
await until(() => st(() => !document.querySelector('[data-ini-tool-card]')));
|
|
111
|
+
await page.click('[data-ini="ini-tabs"] [data-ini-tab="overview"]');
|
|
112
|
+
await until(() => st(() => document.querySelectorAll('[data-rec="quiet"]').length >= 2));
|
|
113
|
+
const quiet = await st(() => [...document.querySelectorAll('[data-rec="quiet"]')].map((n) => n.textContent).join(' | '));
|
|
114
|
+
ok('RC9 on a 404 the record cards say "Nothing recorded yet" and "No sprint burndown yet", with no alert', /Nothing recorded yet/.test(quiet) && /No sprint burndown yet/.test(quiet) && await st(() => !document.querySelector('[data-ini-tool-card] [role="alert"]')), quiet);
|
|
115
|
+
|
|
116
|
+
// a project initiative on a board with Build and deliver off: the project workflow's records, and no sprint burndown
|
|
117
|
+
await open('ini:office:OFF-01:overview');
|
|
118
|
+
ok('RC10 a project initiative carries its launch checklist and learnings', await until(() => st(() => { const c = [...document.querySelectorAll('[data-ini-tool-card]')].map((n) => n.dataset.iniToolCard); return c.includes('launch') && !c.includes('burndown'); })));
|
|
119
|
+
await H.finish();
|
|
120
|
+
})();
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* PMOS workspace — the Triage and Initiatives tabs of a board on the console3 Initiatives page (mockup v5, 34-initiatives.js tabTriage and
|
|
3
|
+
* tabInitiatives), in demo mode (browser). Triage: add an ask and see its intake draft, Accept (the anchor is required; the initiative
|
|
4
|
+
* starts on the board's default workflow), Ask for more, Decline with a reason. Initiatives: the saved views, the category, status and
|
|
5
|
+
* owner filters (comma lists for "At risk or late"), sort with its direction, the numbered pager, the Move menu, and that every answer
|
|
6
|
+
* is the server's page of 25.
|
|
7
|
+
*
|
|
8
|
+
* The files it drives (named so `run-tests.js --changed` traces a change in any of them to this suite): public/pm/c3-ib-triage.js,
|
|
9
|
+
* c3-ib-initiatives.js, c3-initiatives.js, c3-initiatives.css, c3-ini-ui.js, c3-ini-work.js, c3-demo-boards.js, c3-demo.js, c3-sheet.js,
|
|
10
|
+
* c3-toast.js and c3-ini-harness.js. Its list fragment is test-lists/c3-ib-triage-backlog.json.
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
const { harness } = require('./c3-ini-harness');
|
|
14
|
+
|
|
15
|
+
(async () => {
|
|
16
|
+
const H = await harness('c3-ib-triage-backlog');
|
|
17
|
+
const { page, ok, until, st, open, toast } = H;
|
|
18
|
+
const store = (f, a) => st(f, a);
|
|
19
|
+
|
|
20
|
+
/* ── Triage ── */
|
|
21
|
+
await open('board:idpf:triage');
|
|
22
|
+
const asks = await until(() => st(() => [...document.querySelectorAll('[data-ibt="asks"] [data-ibt-ask]')].map((n) => n.dataset.ibtAsk).join(',')));
|
|
23
|
+
ok('T1 the open asks of the board, newest first', asks === 'ask-1,ask-3', asks);
|
|
24
|
+
ok('T2 an ask that was asked for more says so', await st(() => !!document.querySelector('[data-ibt-ask="ask-3"] [data-ibt="need-more"]')));
|
|
25
|
+
const intro = await st(() => document.querySelector('[data-ibt="intro"]').textContent);
|
|
26
|
+
ok('T3 the intro names the default workflow and its first stage', /Product, in Discover/.test(intro), intro);
|
|
27
|
+
await page.fill('[data-ibt="line"]', 'Audit log export to S3 for enterprise admins');
|
|
28
|
+
await page.click('[data-ibt="add"]');
|
|
29
|
+
const added = await until(() => st(() => { const r = document.querySelector('[data-ibt="asks"] [data-ibt-ask]'); return r && /Audit log export/.test(r.textContent) && r.dataset.ibtAsk; }));
|
|
30
|
+
ok('T4 Add puts the ask on top and opens it on its intake draft', !!added && await until(() => st((id) => { const r = document.querySelector('[data-ibt-ask="' + id + '"]'); return r && !!r.querySelector('.xp.open [data-ibt-accept]'); }, added)), String(added));
|
|
31
|
+
|
|
32
|
+
await page.click('[data-ibt-ask="ask-1"] .tt');
|
|
33
|
+
await until(() => st(() => !!document.querySelector('[data-ibt-ask="ask-1"] .xp.open [data-ibt="draft"]')));
|
|
34
|
+
const draft = await st(() => document.querySelector('[data-ibt-ask="ask-1"] [data-ibt="draft"]').textContent);
|
|
35
|
+
ok('T5 an ask opens in place on its draft: lane guess, anchor and a prior rejection', /feature, 72% sure/.test(draft) && /KR 2\.1/.test(draft) && /Rejected before: IDPF-5/.test(draft), draft);
|
|
36
|
+
await page.click('[data-ibt-more="ask-1"]');
|
|
37
|
+
await page.fill('[data-ibt="f-more"]', 'Which trials need SSO first?');
|
|
38
|
+
await page.click('[data-ibt="more-go"]');
|
|
39
|
+
await until(() => store(() => window.__c3boards.asks.find((a) => a.id === 'ask-1').needMore === true));
|
|
40
|
+
ok('T6 Ask for more records the question; the ask stays open', await store(() => { const a = window.__c3boards.asks.find((x) => x.id === 'ask-1'); return a.state === 'open' && a.moreNote === 'Which trials need SSO first?'; }) && /prompted for more detail/.test(await toast()));
|
|
41
|
+
|
|
42
|
+
await page.click('[data-ibt-ask="ask-1"] .tt').catch(() => {});
|
|
43
|
+
await until(() => st(() => !!document.querySelector('[data-ibt-ask="ask-1"] .xp.open [data-ibt-accept]')));
|
|
44
|
+
await page.click('[data-ibt-accept="ask-1"]');
|
|
45
|
+
await until(() => st(() => !!document.querySelector('[data-ibt="accept-form"]')));
|
|
46
|
+
await page.fill('[data-ibt="f-anchor"]', '');
|
|
47
|
+
ok('T7 Accept needs an anchor: with none the button is off and the sheet says why', await st(() => document.querySelector('[data-ibt="accept-go"]').disabled && /needs an anchor/.test(document.querySelector('[data-ibt="anchor-why"]').textContent)));
|
|
48
|
+
await page.fill('[data-ibt="f-anchor"]', 'KR 2.1 Activation to 45%');
|
|
49
|
+
await page.click('[data-ibt="accept-go"]');
|
|
50
|
+
await until(() => store(() => window.__c3boards.asks.find((a) => a.id === 'ask-1').state === 'accepted'));
|
|
51
|
+
const t8 = await until(async () => { const t = await toast(); return /Accepted as/.test(t) && t; });
|
|
52
|
+
ok('T8 Accept starts an initiative with the default workflow, and the toast opens it', /Accepted as \S+ in Discover, with the Product workflow/.test(t8 || ''), t8);
|
|
53
|
+
|
|
54
|
+
// no reload: the demo store lives in the page, and ask-1 must stay accepted
|
|
55
|
+
await until(() => st(() => !!document.querySelector('[data-ibt="closed"] [data-ibt-ask="ask-1"]') && !!document.querySelector('[data-ibt="asks"] [data-ibt-ask="ask-3"]')));
|
|
56
|
+
await page.click('[data-ibt-ask="ask-3"] .tt');
|
|
57
|
+
await until(() => st(() => !!document.querySelector('[data-ibt-ask="ask-3"] .xp.open [data-ibt-decline]')));
|
|
58
|
+
await page.click('[data-ibt-decline="ask-3"]');
|
|
59
|
+
ok('T9 Decline needs a reason', await until(() => st(() => document.querySelector('[data-ibt="decline-go"]') && document.querySelector('[data-ibt="decline-go"]').disabled)));
|
|
60
|
+
await page.fill('[data-ibt="f-why"]', 'Partner X moves to the new webhook service in Q1');
|
|
61
|
+
await page.click('[data-ibt="decline-go"]');
|
|
62
|
+
await until(() => store(() => window.__c3boards.asks.find((a) => a.id === 'ask-3').state === 'declined'));
|
|
63
|
+
await until(() => st(() => !!document.querySelector('[data-ibt="closed"] [data-ibt-ask="ask-3"]')));
|
|
64
|
+
ok('T10 the declined ask moves to Decided, with the accepted one', await st(() => !!document.querySelector('[data-ibt="closed"] [data-ibt-ask="ask-1"][data-state="accepted"]') && !!document.querySelector('[data-ibt="closed"] [data-ibt-ask="ask-3"][data-state="declined"]')));
|
|
65
|
+
|
|
66
|
+
/* ── Initiatives ── */
|
|
67
|
+
await open('board:idpf:initiatives');
|
|
68
|
+
const ready = () => until(() => st(() => document.querySelector('[data-ini-panel="initiatives"]') && document.querySelector('[data-ini-panel="initiatives"]').dataset.ibbState === 'ready'));
|
|
69
|
+
const rows = () => st(() => [...document.querySelectorAll('[data-ibb="rows"] [data-ibb-row]')].map((r) => r.dataset.ibbRow));
|
|
70
|
+
const seq = () => st(() => Number(document.querySelector('[data-ini-panel="initiatives"]').dataset.ibbSeq));
|
|
71
|
+
const after = async (s0) => { await until(async () => (await seq()) > s0 && (await st(() => document.querySelector('[data-ini-panel="initiatives"]').dataset.ibbState)) === 'ready'); };
|
|
72
|
+
await ready();
|
|
73
|
+
const count0 = await st(() => document.querySelector('[data-ibb="count"]').textContent);
|
|
74
|
+
ok('L1 the tab opens on Now and next, 25 rows a page', (await rows()).length === 25 && /Now and next/.test(count0) && await st(() => document.querySelector('[data-ibb-view="now"]').getAttribute('aria-pressed') === 'true'), count0);
|
|
75
|
+
const r0 = await st(() => document.querySelector('[data-ibb-row]').textContent);
|
|
76
|
+
ok('L2 a row shows its workflow template, its stage dots, status, owner, progress and dates', /Product|Quick fix|Research only/.test(r0) && await st(() => !!document.querySelector('[data-ibb-row] .ini-steps') && !!document.querySelector('[data-ibb-row] .ini-mini')), r0.slice(0, 200));
|
|
77
|
+
let s0 = await seq();
|
|
78
|
+
await page.selectOption('[data-ibb-filter="status"]', 'at risk,late');
|
|
79
|
+
await after(s0);
|
|
80
|
+
const st1 = await st(() => [...document.querySelectorAll('[data-ibb-row]')].every((r) => /At risk|Late/.test(r.textContent)));
|
|
81
|
+
ok('L3 "At risk or late" is a comma list the server reads as any of', st1 && (await rows()).length > 0);
|
|
82
|
+
s0 = await seq();
|
|
83
|
+
await page.click('[data-ibb-view="all"]');
|
|
84
|
+
await after(s0);
|
|
85
|
+
s0 = await seq();
|
|
86
|
+
await page.selectOption('[data-ibb-filter="cat"]', 'Learn');
|
|
87
|
+
await after(s0);
|
|
88
|
+
const learn = await st(() => [...document.querySelectorAll('[data-ibb-row]')].map((r) => r.dataset.cat));
|
|
89
|
+
ok('L4 the Category filter keeps one category, whatever each initiative\'s workflow calls its stage', learn.length > 0 && learn.every((c) => c === 'Learn'), learn.join(','));
|
|
90
|
+
s0 = await seq();
|
|
91
|
+
await page.click('[data-ibb="clear"]');
|
|
92
|
+
await after(s0);
|
|
93
|
+
const pager = await st(() => [...document.querySelectorAll('[data-ibb-page]')].map((b) => b.dataset.ibbPage).join(','));
|
|
94
|
+
ok('L5 All has the numbered pager (123 initiatives, 5 pages)', /^1,2,.*5$/.test(pager) && /123 initiatives/.test(await st(() => document.querySelector('[data-ibb="count"]').textContent)), pager);
|
|
95
|
+
s0 = await seq();
|
|
96
|
+
await page.click('[data-ibb-page="5"]');
|
|
97
|
+
await after(s0);
|
|
98
|
+
ok('L6 a page number reads that page from the server', (await rows()).length === 23 && /101–123 of 123/.test(await st(() => document.querySelector('[data-ibb="pager"]').textContent)));
|
|
99
|
+
s0 = await seq();
|
|
100
|
+
await page.selectOption('[data-ibb="sort"]', 'title');
|
|
101
|
+
await after(s0);
|
|
102
|
+
const t1 = await st(() => [...document.querySelectorAll('[data-ibb-row] .tt')].map((n) => n.textContent));
|
|
103
|
+
s0 = await seq();
|
|
104
|
+
await page.click('[data-ibb="dir"]');
|
|
105
|
+
await after(s0);
|
|
106
|
+
const t2 = await st(() => [...document.querySelectorAll('[data-ibb-row] .tt')].map((n) => n.textContent));
|
|
107
|
+
ok('L7 Sort by title, then the direction button flips it', t1[0].localeCompare(t1[1]) <= 0 && t2[0].localeCompare(t2[1]) >= 0 && t1[0] !== t2[0], t1[0] + ' / ' + t2[0]);
|
|
108
|
+
|
|
109
|
+
await page.fill('[data-ibb="q"]', 'IDPF-07');
|
|
110
|
+
await until(async () => (await rows()).join() === 'IDPF-07');
|
|
111
|
+
await page.click('[data-ibb-move="IDPF-07"]');
|
|
112
|
+
await until(() => st(() => !!document.querySelector('[data-ini="menu"] [data-ini-moveto]')));
|
|
113
|
+
const opts = await st(() => [...document.querySelectorAll('[data-ini-moveto]')].map((b) => b.dataset.iniMoveto).join(','));
|
|
114
|
+
ok('L8 Move lists the stages of the initiative\'s own workflow', opts === 'Discover,Plan,Build & Deliver,Monitor & Evaluate', opts);
|
|
115
|
+
await page.click('[data-ini-moveto="Discover"]');
|
|
116
|
+
await until(() => store(() => window.__c3boards.rows.find((r) => r.id === 'IDPF-07').stage === 'Discover'));
|
|
117
|
+
ok('L9 a pick moves it and the toast confirms', /IDPF-07 moved to Discover/.test(await toast()), await toast());
|
|
118
|
+
await H.finish();
|
|
119
|
+
})();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* c3-ini-harness.js — what the four console3 Initiatives browser suites share (c3-initiatives-test.js, c3-ib-triage-backlog-test.js,
|
|
2
|
+
* c3-ib-items-test.js, c3-ib-more-test.js): a workspace server on the suite's own port, a Chromium page in demo mode (?demo=1, so
|
|
3
|
+
* c3-demo-boards.js answers every /api/boards and /api/stage-tools call and nothing is written), and the small helpers the suites
|
|
4
|
+
* assert with. Not a suite: it asserts nothing itself.
|
|
5
|
+
*
|
|
6
|
+
* const H = await harness('name', { width }) -> { page, ok, until, st, open(loc), toast(), sheet(), clickText(sel, text), shot(name), finish() }
|
|
7
|
+
* open('board:idpf:board') goes to /c3.html?demo=1&tab=init&init=<loc> and waits for the page to draw.
|
|
8
|
+
* finish() asserts that no page error was thrown, closes everything and exits 1 when any assertion failed.
|
|
9
|
+
* A suite whose machine has no Chromium (or no playwright-core) prints that it was skipped and exits 0, as the other browser suites do. */
|
|
10
|
+
'use strict';
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { findBrowser } = require('./lib/find-browser');
|
|
14
|
+
|
|
15
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
async function until(fn, ms = 12000, step = 100) {
|
|
17
|
+
const t0 = Date.now();
|
|
18
|
+
for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function harness(name, opts = {}) {
|
|
22
|
+
const T = require('./test-run').configure({ name });
|
|
23
|
+
const BASE = `http://127.0.0.1:${T.PORT}`;
|
|
24
|
+
const results = [];
|
|
25
|
+
let srv = null; let browser = null;
|
|
26
|
+
const ok = (n, cond, extra = '') => { results.push({ n, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${n}${extra !== '' && !cond ? ' — ' + String(extra).slice(0, 400) : ''}`); };
|
|
27
|
+
const close = async (code) => {
|
|
28
|
+
if (browser) await browser.close().catch(() => {});
|
|
29
|
+
if (srv) srv.kill('SIGKILL');
|
|
30
|
+
T.killTmux();
|
|
31
|
+
const bad = results.filter((x) => !x.cond).length;
|
|
32
|
+
if (code === undefined) console.log(`\n${results.length - bad}/${results.length} ${name} assertions passed`);
|
|
33
|
+
process.exit(code !== undefined ? code : (bad ? 1 : 0));
|
|
34
|
+
};
|
|
35
|
+
process.on('unhandledRejection', (e) => { console.error(e); close(1); });
|
|
36
|
+
const exe = findBrowser();
|
|
37
|
+
if (!exe) { console.log('· no Chrome/Chromium on this machine — ' + name + ' suite skipped'); process.exit(0); }
|
|
38
|
+
let chromium;
|
|
39
|
+
try { ({ chromium } = require('playwright-core')); } catch { console.log('· playwright-core not installed — ' + name + ' suite skipped'); process.exit(0); }
|
|
40
|
+
|
|
41
|
+
srv = spawn(process.execPath, ['server.js'], { cwd: __dirname, env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
|
|
42
|
+
srv.stderr.on('data', () => {}); srv.stdout.on('data', () => {});
|
|
43
|
+
await T.waitForServer(BASE);
|
|
44
|
+
browser = await chromium.launch({ executablePath: exe });
|
|
45
|
+
const ctx = await browser.newContext({ viewport: { width: opts.width || 1280, height: 900 } });
|
|
46
|
+
const page = await ctx.newPage();
|
|
47
|
+
const errors = [];
|
|
48
|
+
page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
|
|
49
|
+
const st = (f, a) => page.evaluate(f, a);
|
|
50
|
+
const open = async (loc, extra) => {
|
|
51
|
+
await page.goto(BASE + '/c3.html?demo=1&tab=init' + (loc ? '&init=' + encodeURIComponent(loc) : '') + (extra || ''), { waitUntil: 'load' });
|
|
52
|
+
await until(() => st(() => { const p = document.querySelector('[data-ini="page"]'); return !!p && !p.querySelector('[data-ini="status"]') && p.children.length > 1; }));
|
|
53
|
+
await sleep(150);
|
|
54
|
+
};
|
|
55
|
+
const toast = () => st(() => [...document.querySelectorAll('[data-c3-toast]')].map((t) => t.textContent).join(' | '));
|
|
56
|
+
const sheet = () => page.$('[data-shell="sheet"] [role="dialog"]');
|
|
57
|
+
const clickText = async (sel, text) => {
|
|
58
|
+
const h = await page.evaluateHandle(([s, t]) => [...document.querySelectorAll(s)].find((n) => n.textContent.includes(t)) || null, [sel, text]);
|
|
59
|
+
const e = h.asElement(); if (!e) return false; await e.click(); return true;
|
|
60
|
+
};
|
|
61
|
+
const shot = async (n) => { if (process.env.C3_SHOTS) await page.screenshot({ path: path.join(process.env.C3_SHOTS, name + '-' + n + '.png'), fullPage: true }).catch(() => {}); };
|
|
62
|
+
const finish = async () => { ok('no page error was thrown', errors.length === 0, errors.join(' | ')); await close(); };
|
|
63
|
+
return { page, ok, until, sleep, st, open, toast, sheet, clickText, shot, finish, BASE, errors };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { harness, until, sleep };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* PMOS workspace — the console3 Initiatives page (/c3.html, mockup v5; PRD planning/prd/console3-initiatives-parity-prd.md), its shell:
|
|
3
|
+
* Portfolio, a board's frame and setup, the board settings, New board, New initiative, an initiative's header, stepper and workflow
|
|
4
|
+
* editor. DEMO mode (?demo=1): c3-demo-boards.js answers every /api/boards read and write from its in-memory fixture, so the server only
|
|
5
|
+
* serves the page. Nothing here writes a repository.
|
|
6
|
+
*
|
|
7
|
+
* The files it drives (named so `run-tests.js --changed` traces a change in any of them to this suite): public/pm/c3-initiatives.js,
|
|
8
|
+
* c3-initiatives.css, c3-ini-ui.js, c3-ini-work.js, c3-ini-timeline.js, c3-ini-records.js, c3-demo-boards.js, c3-demo.js, c3-shell.js,
|
|
9
|
+
* c3-sheet.js, c3-toast.js, c3-ib-initiatives.js, c3-ii-tabs.js and c3-ini-harness.js. Its list fragment is test-lists/c3-initiative-boards.json.
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
const { harness } = require('./c3-ini-harness');
|
|
13
|
+
|
|
14
|
+
(async () => {
|
|
15
|
+
const H = await harness('c3-initiatives');
|
|
16
|
+
const { page, ok, until, st, open, toast } = H;
|
|
17
|
+
const store = (f) => st(f);
|
|
18
|
+
|
|
19
|
+
/* ── Portfolio ── */
|
|
20
|
+
await open('');
|
|
21
|
+
const ids = await until(() => st(() => [...document.querySelectorAll('[data-ini="boards"] [data-ini-board]')].map((r) => r.dataset.iniBoard).join(',')));
|
|
22
|
+
ok('N1 the Portfolio lists the 5 boards in order', ids === 'idpf,pay,growth,office,hiring', ids);
|
|
23
|
+
const pf = await st(() => document.querySelector('[data-ini="portfolio"]').closest('[data-ini="page"]').textContent);
|
|
24
|
+
ok('N2 the header counts boards, initiatives and new asks; the sentence names the boards off track', /5 boards/.test(pf) && /312 initiatives/.test(pf) && /4 new asks/.test(pf) && /1 board is off track/.test(pf), pf.slice(0, 300));
|
|
25
|
+
const idpfRow = await st(() => document.querySelector('[data-ini-board="idpf"]').textContent);
|
|
26
|
+
ok('N3 a board row shows health, owner, Build and deliver with its Jira key, the four categories and the next release',
|
|
27
|
+
/Needs attention/.test(idpfRow) && /Build and deliver/.test(idpfRow) && /Jira · IDPF/.test(idpfRow) && /Discover\s*28/.test(idpfRow) && /Learn\s*31/.test(idpfRow) && /IDPF 3\.1/.test(idpfRow), idpfRow.slice(0, 300));
|
|
28
|
+
const offRow = await st(() => document.querySelector('[data-ini-board="office"]').textContent);
|
|
29
|
+
ok('N4 a board with Build and deliver off says PMOS tasks only', /PMOS tasks only/.test(offRow) && !/Jira/.test(offRow), offRow.slice(0, 200));
|
|
30
|
+
const tl = await until(() => st(() => { const t = document.querySelector('[data-ini="pf-timeline"] .ini-tl'); return t && t.querySelectorAll('[data-tlb]').length; }));
|
|
31
|
+
ok('N5 the timeline across boards draws board > initiative > epic bars with a Today line', tl > 10 && await st(() => !!document.querySelector('[data-ini="pf-timeline"] .ini-tlnow')), String(tl));
|
|
32
|
+
const asks = await st(() => [...document.querySelectorAll('[data-ini-asks]')].map((n) => n.dataset.iniAsks));
|
|
33
|
+
ok('N6 the asks card links each board with new asks to its Triage', asks.includes('idpf') && asks.includes('pay') && asks.includes('office'), asks.join(','));
|
|
34
|
+
|
|
35
|
+
/* ── a board ── */
|
|
36
|
+
await page.click('[data-ini-open-board="idpf"]');
|
|
37
|
+
await until(() => st(() => !!document.querySelector('[data-ini-board-page="idpf"] [data-ini="board-tabs"]')));
|
|
38
|
+
const tabs = await st(() => [...document.querySelectorAll('[data-ini="board-tabs"] [data-ini-tab]')].map((b) => b.dataset.iniTab).join(','));
|
|
39
|
+
ok('B1 a Build and deliver board has the nine tabs, Summary first', tabs === 'summary,initiatives,triage,backlog,board,timeline,releases,docs,sprints', tabs);
|
|
40
|
+
ok('B2 the URL keeps the place: init=board:idpf:summary', (await st(() => new URLSearchParams(location.search).get('init'))) === 'board:idpf:summary');
|
|
41
|
+
const setup = await st(() => document.querySelector('[data-ini="setup"]').textContent);
|
|
42
|
+
ok('B3 the setup chips say Build and deliver on, the Jira project and the default workflow', /Build and deliver on/.test(setup) && /IDPF/.test(setup) && /start with Product/.test(setup), setup);
|
|
43
|
+
const glance = await until(() => st(() => document.querySelectorAll('[data-ini="glance"] [data-ini-row]').length));
|
|
44
|
+
ok('B4 Summary lists initiatives at a glance with their own stage dots', glance > 0 && await st(() => !!document.querySelector('[data-ini="glance"] .ini-steps')), String(glance));
|
|
45
|
+
await page.click('[data-ini-catgo="Do"]');
|
|
46
|
+
await until(() => st(() => document.querySelector('[data-ibb-filter="cat"]') && document.querySelector('[data-ibb-filter="cat"]').value === 'Do'));
|
|
47
|
+
ok('B5 a category bar opens the Initiatives tab filtered to that category', /board:idpf:initiatives:/.test(await st(() => new URLSearchParams(location.search).get('init'))) && await st(() => document.querySelector('[data-ibb-filter="cat"]').value === 'Do'));
|
|
48
|
+
await page.reload({ waitUntil: 'load' });
|
|
49
|
+
await until(() => st(() => document.querySelector('[data-ibb-filter="cat"]') && document.querySelector('[data-ibb-filter="cat"]').value === 'Do'));
|
|
50
|
+
ok('B6 a reload comes back to the same tab and filter', await st(() => document.querySelector('[data-ini-tab="initiatives"]').getAttribute('aria-pressed') === 'true' && document.querySelector('[data-ibb-filter="cat"]').value === 'Do'));
|
|
51
|
+
|
|
52
|
+
await open('board:office:summary');
|
|
53
|
+
const offTabs = await st(() => [...document.querySelectorAll('[data-ini="board-tabs"] [data-ini-tab]')].map((b) => b.dataset.iniTab).join(','));
|
|
54
|
+
ok('B7 a board with Build and deliver off has no Releases and no Sprints tab', offTabs === 'summary,initiatives,triage,backlog,board,timeline,docs', offTabs);
|
|
55
|
+
await open('board:office:sprints');
|
|
56
|
+
ok('B8 a Sprints URL on such a board falls back to Summary', await st(() => document.querySelector('[data-ini-tab="summary"]').getAttribute('aria-pressed') === 'true'));
|
|
57
|
+
|
|
58
|
+
/* ── board settings: turn Build and deliver on for Office move ── */
|
|
59
|
+
await page.click('[data-ini="board-settings"]');
|
|
60
|
+
await until(() => st(() => !!document.querySelector('[data-ini="settings-form"]')));
|
|
61
|
+
ok('S1 the settings sheet offers no Build template while Build and deliver is off', await st(() => document.querySelector('[data-ini-offer="fix"]').disabled && document.querySelector('[data-ini-offer="product"]').disabled));
|
|
62
|
+
await page.click('[data-ini="build"]');
|
|
63
|
+
await until(() => st(() => !!document.querySelector('[data-ini="jira-key"]')));
|
|
64
|
+
await page.fill('[data-ini="jira-key"]', 'off');
|
|
65
|
+
ok('S2 turning it on offers the Build templates and asks for the Jira key', await st(() => !document.querySelector('[data-ini-offer="fix"]').disabled && document.querySelector('[data-ini-offer="fix"]').checked));
|
|
66
|
+
await page.click('[data-ini="settings-save"]');
|
|
67
|
+
await until(() => store(() => window.__c3boards.boards.find((b) => b.id === 'office').build === true));
|
|
68
|
+
const off = await store(() => { const b = window.__c3boards.boards.find((x) => x.id === 'office'); return { build: b.build, jira: b.jira, offer: b.offer }; });
|
|
69
|
+
ok('S3 Save posts buildDeliver: Office move mirrors Jira OFF and offers the Build templates', off.build === true && off.jira === 'OFF' && off.offer.includes('fix'), JSON.stringify(off));
|
|
70
|
+
const s4 = await until(async () => { const t = await toast(); return /Build and deliver on, mirroring Jira OFF/.test(t) && t; });
|
|
71
|
+
ok('S4 a toast says what changed', !!s4, await toast());
|
|
72
|
+
await until(() => st(() => !!document.querySelector('[data-ini-tab="sprints"]')));
|
|
73
|
+
ok('S5 the board now has Releases and Sprints', await st(() => !!document.querySelector('[data-ini-tab="releases"]') && !!document.querySelector('[data-ini-tab="sprints"]')));
|
|
74
|
+
|
|
75
|
+
/* ── New board ── */
|
|
76
|
+
await open('');
|
|
77
|
+
await page.click('[data-ini="new-board"]');
|
|
78
|
+
await page.fill('[data-ini="nb-name"]', 'Partner portal');
|
|
79
|
+
await page.click('[data-ini="nb-build"]');
|
|
80
|
+
ok('NB1 with Build and deliver off, the Build workflows cannot be the default', await st(() => document.querySelector('[data-ini="new-board-form"] [data-ini-tpl="product"]').disabled && !document.querySelector('[data-ini="new-board-form"] [data-ini-tpl="project"]').disabled));
|
|
81
|
+
await page.click('[data-ini="new-board-form"] [data-ini-tpl="simple"]');
|
|
82
|
+
await page.click('[data-ini="nb-create"]');
|
|
83
|
+
await until(() => st(() => !!document.querySelector('[data-ini-board-page="partner-portal"]')));
|
|
84
|
+
const nb = await store(() => { const b = window.__c3boards.boards.find((x) => x.id === 'partner-portal'); return b && { build: b.build, defTpl: b.defTpl }; });
|
|
85
|
+
ok('NB2 Create board adds the board (Build and deliver off, Simple by default) and opens its Triage', nb && nb.build === false && nb.defTpl === 'simple' && await st(() => document.querySelector('[data-ini-tab="triage"]').getAttribute('aria-pressed') === 'true'), JSON.stringify(nb));
|
|
86
|
+
|
|
87
|
+
/* ── New initiative, with its own workflow ── */
|
|
88
|
+
await open('board:idpf:initiatives');
|
|
89
|
+
await page.click('[data-ini="new-initiative"]');
|
|
90
|
+
await until(() => st(() => !!document.querySelector('[data-ini="new-ini-form"]')));
|
|
91
|
+
await page.fill('[data-ini="ni-title"]', 'Passwordless login with email OTP');
|
|
92
|
+
await page.click('[data-ini="ni-create"]');
|
|
93
|
+
ok('NI1 an initiative without an anchor is refused, and the sheet says why', /anchor/.test(await st(() => (document.querySelector('[data-ini="new-ini-form"] [data-ini="error"]') || {}).textContent || '')));
|
|
94
|
+
await page.fill('[data-ini="ni-anchor"]', 'KR 2.1 Activation to 45%');
|
|
95
|
+
await page.click('[data-ini="new-ini-form"] [data-ini-tpl="research"]');
|
|
96
|
+
await page.click('[data-ini="ni-create"]');
|
|
97
|
+
await until(() => st(() => !!document.querySelector('[data-ini-page]') && !!document.querySelector('[data-ini="stepper"]')));
|
|
98
|
+
const steps = await st(() => [...document.querySelectorAll('[data-ini="stepper"] [data-ini-step]')].map((b) => b.dataset.iniStep).join(','));
|
|
99
|
+
ok('NI2 Create opens the new initiative on its own workflow (Research only: Discover, Plan, Evaluate)', steps === 'Discover,Plan,Evaluate', steps);
|
|
100
|
+
|
|
101
|
+
/* ── an initiative: tools, stepper, workflow editor ── */
|
|
102
|
+
await open('ini:idpf:IDPF-07:overview');
|
|
103
|
+
const strip = await st(() => [...document.querySelectorAll('[data-ini="tools"] [data-ini-tool]')].map((b) => b.dataset.iniTool).join(','));
|
|
104
|
+
ok('I1 the tools strip shows the tools of the current stage (Plan: RICE, PRD, Done-criteria, ...)', /^rice,prd,rubric/.test(strip), strip);
|
|
105
|
+
await page.click('[data-ini="more-tools"]');
|
|
106
|
+
const more = await until(() => st(() => document.querySelectorAll('[data-ini="menu"] [data-ini-moretool]').length));
|
|
107
|
+
ok('I2 More tools lists the tools of the other stages, grouped by stage', more > 3 && /Discover/.test(await st(() => document.querySelector('[data-ini="menu"]').textContent)), String(more));
|
|
108
|
+
await page.keyboard.press('Escape');
|
|
109
|
+
ok('I3 Escape closes the menu', await until(() => st(() => !document.querySelector('[data-ini="menu"]'))));
|
|
110
|
+
await page.click('[data-ini-step="Build & Deliver"]');
|
|
111
|
+
await until(() => st(() => document.querySelector('[data-ini-step="Build & Deliver"]') && document.querySelector('[data-ini-step="Build & Deliver"]').getAttribute('aria-pressed') === 'true'));
|
|
112
|
+
const i4 = await until(async () => { const t = await toast(); return /moved to Build & Deliver/.test(t) && /No spec yet/.test(t) && t; });
|
|
113
|
+
ok('I4 a stepper press moves the stage; the order warning is shown, not a block', !!i4, await toast());
|
|
114
|
+
ok('I5 the move is written to the store', await store(() => window.__c3boards.rows.find((r) => r.id === 'IDPF-07').stage === 'Build & Deliver'));
|
|
115
|
+
|
|
116
|
+
await page.click('[data-ini="edit-workflow"]');
|
|
117
|
+
await until(() => st(() => !!document.querySelector('[data-ini="wf-editor"]')));
|
|
118
|
+
ok('W1 the editor offers only the templates the board offers', await st(() => !document.querySelector('[data-wf-tpl="fix"]').disabled && document.querySelector('[data-wf-tpl="project"]').disabled));
|
|
119
|
+
await page.focus('[data-wf-down="0"]'); await page.keyboard.press('Enter');
|
|
120
|
+
const order = await st(() => [...document.querySelectorAll('[data-wf-name]')].map((i) => i.value).join(','));
|
|
121
|
+
ok('W2 a stage moves down from the keyboard', order.startsWith('Plan,Discover'), order);
|
|
122
|
+
await page.click('[data-wf-tpl="research"]');
|
|
123
|
+
await page.click('[data-wf-save]');
|
|
124
|
+
await until(() => st(() => !!document.querySelector('[data-ini="wf-confirm"]')));
|
|
125
|
+
ok('W3 removing the current stage asks where it goes before saving', /Move IDPF-07 from Build & Deliver to Plan/.test(await st(() => document.querySelector('[data-ini="wf-confirm"]').textContent)));
|
|
126
|
+
await page.click('[data-wf-confirm]');
|
|
127
|
+
await until(() => store(() => window.__c3boards.rows.find((r) => r.id === 'IDPF-07').template === 'research'));
|
|
128
|
+
const r07 = await store(() => { const r = window.__c3boards.rows.find((x) => x.id === 'IDPF-07'); return { stage: r.stage, wf: (r.workflow || []).map((s) => s.name).join(',') }; });
|
|
129
|
+
ok('W4 the initiative now runs Research only and moved to Plan; other initiatives keep theirs', r07.stage === 'Plan' && r07.wf === 'Discover,Plan,Evaluate'
|
|
130
|
+
&& await store(() => !window.__c3boards.rows.find((r) => r.id === 'IDPF-12').template), JSON.stringify(r07));
|
|
131
|
+
|
|
132
|
+
/* ── phone width ── */
|
|
133
|
+
await page.setViewportSize({ width: 400, height: 860 });
|
|
134
|
+
for (const loc of ['', 'board:idpf:summary', 'ini:idpf:IDPF-12:overview']) {
|
|
135
|
+
await open(loc);
|
|
136
|
+
const sx = await st(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
137
|
+
ok('P1 at 400 px nothing scrolls sideways: ' + (loc || 'portfolio'), sx <= 0, String(sx));
|
|
138
|
+
}
|
|
139
|
+
await H.finish();
|
|
140
|
+
})();
|
|
@@ -43,7 +43,7 @@ function collect(tab) {
|
|
|
43
43
|
if (!pane) return null;
|
|
44
44
|
const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
45
45
|
const visible = (e) => !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length) && !e.closest('[hidden]');
|
|
46
|
-
const ITEM = '[data-row], [data-id], [data-due-row], [data-needs-row], [data-draft-row], li, article';
|
|
46
|
+
const ITEM = '[data-row], [data-id], [data-due-row], [data-needs-row], [data-draft-row], [data-ibb-row], [data-ibt-ask], [data-inb-board], li, article';
|
|
47
47
|
const out = new Map();
|
|
48
48
|
// A control is a button, an element with a press action (data-a), or a row that acts by itself: a swipe row or a focusable
|
|
49
49
|
// row (Work's rows have no buttons; a swipe or an arrow key is the action).
|
|
@@ -98,6 +98,19 @@ function collect(tab) {
|
|
|
98
98
|
last = n;
|
|
99
99
|
}
|
|
100
100
|
byTab[tab] = items || [];
|
|
101
|
+
// Initiatives opens on the Portfolio; the homes of initiatives and new asks are a board's Backlog and Triage, so open the
|
|
102
|
+
// first board and collect those two tabs into the same pane's list.
|
|
103
|
+
if (tab === 'init' && await page.$('[data-inb-board] [data-inb-open]')) {
|
|
104
|
+
await page.click('[data-inb-board] [data-inb-open]');
|
|
105
|
+
for (const sub of ['triage', 'backlog']) {
|
|
106
|
+
const at = await page.waitForSelector('[data-inb-tab="' + sub + '"]', { timeout: 10000 }).catch(() => null);
|
|
107
|
+
if (!at) continue;
|
|
108
|
+
await at.click();
|
|
109
|
+
let n0 = -1; let got = null;
|
|
110
|
+
for (let i = 0; i < 20; i++) { await sleep(250); got = await page.evaluate(collect, tab); const n = got ? got.length : -1; if (n === n0 && n > 0) break; n0 = n; }
|
|
111
|
+
for (const it of got || []) if (!byTab[tab].some((x) => x.title === it.title)) byTab[tab].push(it);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
101
114
|
// Reports (since the dashboards) draws widgets and charts, not items: it may have none, and then it cannot hold a second home.
|
|
102
115
|
if (tab === 'rep') ok('the rep tab holds no item with an action control, or only widgets', true);
|
|
103
116
|
else ok(`the ${tab} tab shows items with action controls`, byTab[tab].length > 0, JSON.stringify(items));
|