create-pmos 0.196.0 → 0.198.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/c3-ib-items-test.js +16 -8
- package/workspace/c3-ib-more-test.js +3 -2
- package/workspace/c3-ib-triage-backlog-test.js +1 -1
- package/workspace/c3-quiet-test.js +4 -3
- package/workspace/c3-report-design-real-test.js +225 -0
- package/workspace/c3-report-design-test.js +403 -0
- package/workspace/c3-report-setup-test.js +3 -2
- package/workspace/c3-work-test.js +1 -1
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +9 -5
- package/workspace/guide/report-design.md +104 -0
- package/workspace/guide/report-slides.md +3 -1
- package/workspace/guide/reports-set-up.md +2 -1
- package/workspace/lib/headless.js +24 -6
- package/workspace/lib/report-design.js +741 -0
- package/workspace/lib/report-draft.js +8 -3
- package/workspace/lib/report-setups.js +1 -1
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-doc-sheet.js +8 -1
- package/workspace/lib/routes-report-design.js +481 -0
- package/workspace/lib/routes-reports.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-rd.js +426 -0
- package/workspace/public/pm/c3-demo-rep2.js +2 -0
- package/workspace/public/pm/c3-demo-rs.js +5 -1
- package/workspace/public/pm/c3-design-studio.js +308 -0
- package/workspace/public/pm/c3-doc-formats.js +3 -2
- package/workspace/public/pm/c3-doc-sheet.js +30 -5
- package/workspace/public/pm/c3-ib-items.js +26 -17
- package/workspace/public/pm/c3-ib-releases.js +4 -7
- package/workspace/public/pm/c3-ib-sprints.js +5 -8
- package/workspace/public/pm/c3-ib-triage.js +4 -9
- package/workspace/public/pm/c3-ini-life.css +2 -0
- package/workspace/public/pm/c3-initiatives.js +2 -1
- package/workspace/public/pm/c3-quiet.css +1 -1
- package/workspace/public/pm/c3-quiet.js +4 -5
- package/workspace/public/pm/c3-report-deck.css +139 -0
- package/workspace/public/pm/c3-report-deck.js +558 -0
- package/workspace/public/pm/c3-report-frame.css +20 -0
- package/workspace/public/pm/c3-report-setup.js +18 -6
- package/workspace/server.js +17 -5
- package/workspace/skills/report-design.skill +61 -0
- package/workspace/test-lists/report-design-backend.json +5 -0
- package/workspace/test-lists/report-design-studio-ui.json +5 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/* c3-report-design-test.js — the report design studio and the deck of a cycle, UI slice (initiative report-design-studio; mockup 42-reportdeck).
|
|
4
|
+
* DEMO mode: a real server and a real Chromium drive /c3.html?demo=1&tab=rep; c3-demo-rd.js answers the design and notes routes.
|
|
5
|
+
*
|
|
6
|
+
* studio Design how it looks on a set-up row; the conversation (samples with a thumbnail and what PMOS took, turns, version links);
|
|
7
|
+
* add a sample by upload, link and past document; a describe then a turn on a report with no design; the running state
|
|
8
|
+
* while the job runs; an error line; point and say (markers go with the turn); Compare two with What changed; Go back;
|
|
9
|
+
* the Deck / Document switch; This is it; the Design brief (#design, #design-point, #design-compare, #design-doc, #design-saved)
|
|
10
|
+
* deck the Read tab of a designed cycle: every frame is sandbox="allow-same-origin" with no allow-scripts; the strip and prev/next;
|
|
11
|
+
* data-value, data-src, data-more, data-initiative and data-from wired from the page; MORE THAN SLIDES; the copy notice;
|
|
12
|
+
* Differs from the design with Keep for this cycle, Make it part of the design, Match the design and Show the slide; Matches the design
|
|
13
|
+
* notes speaker notes save and carry forward (Keep, Clear); pins: yours blue, PMOS dashed purple, Keep the note, a new pin, the Edit log,
|
|
14
|
+
* Edit this opens the Edit tab at its section
|
|
15
|
+
* present the presenter view (slide, note, next, timer, n / N, Jump to) and the audience view: arrows, Page keys, Space, Home, End,
|
|
16
|
+
* swipe, Escape
|
|
17
|
+
* print Print / PDF writes the slides only: no speaker note and no pin; Download .pptx reads the existing export
|
|
18
|
+
* document a report designed as a document reads as an interactive document (pmos-sec) with its notes keyed by section
|
|
19
|
+
* output the output choice reads Slide deck; a Slides report says its Google Slides copy is optional
|
|
20
|
+
* phone at 400 wide the deck, the studio and the presenter view do not scroll sideways
|
|
21
|
+
* Set RD_SHOTS=<dir> to save screenshots. No script error may be raised. */
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { spawn } = require('child_process');
|
|
25
|
+
const { findBrowser } = require('./lib/find-browser');
|
|
26
|
+
const T = require('./test-run').configure({ name: 'c3-report-design' });
|
|
27
|
+
|
|
28
|
+
const BASE = T.BASE;
|
|
29
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
30
|
+
const until = async (fn, ms = 10000, step = 100) => { const t0 = Date.now(); for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); } };
|
|
31
|
+
const results = [];
|
|
32
|
+
const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + String(extra).slice(0, 400) : ''}`); };
|
|
33
|
+
let srv = null; let browser = null;
|
|
34
|
+
const finish = async (code) => {
|
|
35
|
+
if (browser) await browser.close().catch(() => {});
|
|
36
|
+
if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
|
|
37
|
+
T.killTmux();
|
|
38
|
+
const bad = results.filter((x) => !x.cond).length;
|
|
39
|
+
console.log(`\n${results.length - bad}/${results.length} c3-report-design assertions passed`);
|
|
40
|
+
process.exit(code !== undefined ? code : (bad ? 1 : 0));
|
|
41
|
+
};
|
|
42
|
+
process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
43
|
+
|
|
44
|
+
const DOC = 'planning/reports/rs1/2026-10-21.md';
|
|
45
|
+
(async () => {
|
|
46
|
+
const exe = findBrowser();
|
|
47
|
+
if (!exe) { console.log('· no Chrome/Chromium on this machine, c3-report-design suite skipped'); process.exit(0); }
|
|
48
|
+
let chromium;
|
|
49
|
+
try { ({ chromium } = require('playwright-core')); } catch { console.log('· playwright-core not installed, c3-report-design suite skipped'); process.exit(0); }
|
|
50
|
+
srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
|
|
51
|
+
await T.waitForServer(BASE);
|
|
52
|
+
browser = await chromium.launch({ executablePath: exe });
|
|
53
|
+
require('./lib/c3-full-console').fullConsole(browser); // the desktop layout at phone width (c3-phone-companion)
|
|
54
|
+
const shots = process.env.RD_SHOTS || ''; if (shots) fs.mkdirSync(shots, { recursive: true });
|
|
55
|
+
|
|
56
|
+
const errors = [];
|
|
57
|
+
/* two reads of other initiatives (Horizon scan, Settings) have no demo answer: a 404 on them is not this suite's */
|
|
58
|
+
const watch = (pg) => {
|
|
59
|
+
pg.on('pageerror', (e) => errors.push('pageerror: ' + String((e && e.message) || e)));
|
|
60
|
+
pg.on('response', (r) => { if (r.status() >= 400 && !/\/api\/(scan\/(?:today|portfolio)|settings)\b/.test(r.url())) errors.push('http ' + r.status() + ' ' + r.url()); });
|
|
61
|
+
pg.on('dialog', (d) => d.dismiss().catch(() => {}));
|
|
62
|
+
};
|
|
63
|
+
const posts = []; const gets = [];
|
|
64
|
+
/* demo answers never reach the network, so the posts are counted by a wrapper over the page's fetch (installed after the demo's) */
|
|
65
|
+
const spy = (pg) => pg.evaluate(() => { if (window.__rdspy) return; window.__rdspy = 1; const f = window.fetch; window.fetch = (i, init) => { const u = new URL(typeof i === 'string' ? i : i.url, location.origin); if (String((init && init.method) || 'GET').toUpperCase() === 'POST') window.__rdPost(u.pathname, String((init && init.body) || '{}')); else window.__rdGet(u.pathname + u.search); return f(i, init); }; });
|
|
66
|
+
const sent = (p) => posts.filter((x) => x.path === p);
|
|
67
|
+
const newPage = async (ctx) => {
|
|
68
|
+
const pg = await ctx.newPage(); watch(pg);
|
|
69
|
+
await pg.exposeFunction('__rdPost', (p, b) => { let j = null; try { j = JSON.parse(b || '{}'); } catch { j = null; } posts.push({ path: p, body: j }); });
|
|
70
|
+
await pg.exposeFunction('__rdGet', (p) => { gets.push(p); });
|
|
71
|
+
return pg;
|
|
72
|
+
};
|
|
73
|
+
/* the Reports tab with rs1 (a saved deck design, D3) and rs3 (a document design, not saved) set up from their findings */
|
|
74
|
+
const goReports = async (pg) => {
|
|
75
|
+
await pg.goto(BASE + '/c3.html?demo=1&tab=rep'); await pg.waitForSelector('[data-rp3]'); await pg.click('[data-rp3-tab="reports"]'); await pg.waitForSelector('[data-rs-row]'); await spy(pg);
|
|
76
|
+
await pg.click('[data-rs-row="rs1"] [data-rs-act="setup"]'); await pg.waitForSelector('[data-rs-reg="rs1"]');
|
|
77
|
+
await pg.click('[data-rs-row="rs3"] [data-rs-act="setup"]'); await pg.waitForSelector('[data-rs-reg="rs3"]');
|
|
78
|
+
};
|
|
79
|
+
const openDoc = async (pg, p) => { await pg.click('[data-rs-doc="' + p + '"] [data-rs-act="opendoc"] >> nth=0'); await pg.waitForSelector('[data-rd-deck] [data-rd-big] iframe, [data-rd-deck] .rd-docf iframe'); await sleep(300); };
|
|
80
|
+
const closeAll = async (pg) => { for (let i = 0; i < 3 && await pg.$('.sheet'); i++) { await pg.keyboard.press('Escape'); await sleep(250); } };
|
|
81
|
+
const shot = async (pg, n) => { if (shots) await pg.screenshot({ path: path.join(shots, n + '.png') }); };
|
|
82
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
|
83
|
+
const page = await newPage(ctx);
|
|
84
|
+
const text = (sel, pg = page) => pg.textContent(sel).catch(() => '');
|
|
85
|
+
const bigFrame = async (pg = page) => { const h = await pg.$('[data-rd-big] iframe'); return h ? h.contentFrame() : null; };
|
|
86
|
+
const clickAt = async (sel, fx, fy, pg = page) => { await pg.$eval(sel, (n) => n.scrollIntoView({ block: 'center' })); await sleep(150); const b = await (await pg.$(sel)).boundingBox(); await pg.mouse.click(b.x + b.width * fx, b.y + b.height * fy); };
|
|
87
|
+
/* a click inside a frame lands only once the frame has loaded and been wired: under load, click again until the answer shows */
|
|
88
|
+
const clickUntil = async (sel, fx, fy, want, pg = page) => { for (let i = 0; i < 6; i++) { await clickAt(sel, fx, fy, pg); if (await until(() => pg.$(want), 1500)) return true; } return false; };
|
|
89
|
+
|
|
90
|
+
/* ── output choice and the entry points ── */
|
|
91
|
+
await page.goto(BASE + '/c3.html?demo=1&tab=rep'); await page.waitForSelector('[data-rp3]'); await page.click('[data-rp3-tab="reports"]'); await page.waitForSelector('[data-rs-row]'); await spy(page);
|
|
92
|
+
await page.click('[data-rs-row="rs1"] .tt');
|
|
93
|
+
await page.waitForSelector('[data-rs-sheet]');
|
|
94
|
+
ok('the output choice says Slide deck where it said Google Slides', /Slide deck/.test(await text('[data-rs-sheet]')) && !(await page.$$eval('[data-rs-sheet] .seg button', (a) => a.some((b) => /^\s*Google Slides\s*$/.test(b.textContent)))));
|
|
95
|
+
await closeAll(page);
|
|
96
|
+
await page.click('[data-rs-row="rs1"] [data-rs-act="setup"]'); await page.waitForSelector('[data-rs-reg="rs1"]');
|
|
97
|
+
await page.click('[data-rs-row="rs3"] [data-rs-act="setup"]'); await page.waitForSelector('[data-rs-reg="rs3"]');
|
|
98
|
+
ok('a set-up row built from your work has Design how it looks', !!(await page.$('[data-rs-reg="rs1"] [data-rs-act="design"]')) && /Design how it looks/.test(await text('[data-rs-reg="rs1"] [data-rs-act="design"]')));
|
|
99
|
+
|
|
100
|
+
/* ── studio: a saved deck design (#design) ── */
|
|
101
|
+
await page.click('[data-rs-reg="rs1"] [data-rs-act="design"]');
|
|
102
|
+
await page.waitForSelector('[data-rd-studio] [data-rd-v]');
|
|
103
|
+
ok('the studio opens on the saved design: Deck selected, Saved design: D3, This is it and Design brief',
|
|
104
|
+
await page.getAttribute('[data-rd-fmt="deck"]', 'aria-pressed') === 'true' && /Saved design: D3/.test(await text('[data-rd-saved]')) && !!(await page.$('[data-rd-act="accept"]')) && !!(await page.$('[data-rd-act="brief"]')));
|
|
105
|
+
ok('the versions D1 to D3, with D3 current', (await page.$$('[data-rd-v]')).length === 3 && await page.getAttribute('[data-rd-v="3"]', 'aria-pressed') === 'true');
|
|
106
|
+
const logKinds = await page.$$eval('[data-rd-log]', (a) => a.map((b) => b.getAttribute('data-rd-log')));
|
|
107
|
+
ok('the conversation holds your description, the samples, your turns and PMOS versions', ['describe', 'sample', 'turn', 'version'].every((k) => logKinds.includes(k)), logKinds.join(','));
|
|
108
|
+
ok('a sample chip shows a thumbnail and what PMOS took', !!(await page.$('[data-rd-chat] .rd-dks-smp .rd-dks-thumb')) && /PMOS took:/.test(await text('[data-rd-chat]')));
|
|
109
|
+
ok('a version line links to its version', (await page.$$('[data-rd-chat] [data-rd-ver]')).length >= 2);
|
|
110
|
+
await page.click('[data-rd-chat] [data-rd-ver="1"] >> nth=0');
|
|
111
|
+
ok('a version link shows that version in the preview', !!(await until(async () => await page.getAttribute('[data-rd-v="1"]', 'aria-pressed') === 'true')));
|
|
112
|
+
await page.click('[data-rd-v="3"]');
|
|
113
|
+
const sb = await until(() => page.$('[data-rd-sbig] iframe'));
|
|
114
|
+
ok('the preview is a sandboxed frame with no scripts', !!sb && await sb.getAttribute('sandbox') === 'allow-same-origin');
|
|
115
|
+
await shot(page, 'design-1440');
|
|
116
|
+
|
|
117
|
+
/* add samples: upload, link and a past PMOS document */
|
|
118
|
+
await page.click('[data-rd-act="panel"]');
|
|
119
|
+
await page.waitForSelector('[data-rd-panel]');
|
|
120
|
+
ok('Add a sample offers an upload, a link and a past PMOS document', !!(await page.$('[data-rd-smp="file"]')) && !!(await page.$('[data-rd-smp="link"]')) && !!(await page.$('[data-rd-smp="doc"]')));
|
|
121
|
+
await page.setInputFiles('[data-rd-file]', { name: 'Biweekly update, 9 Apr.pdf', mimeType: 'application/pdf', buffer: Buffer.from('%PDF-1.4 a small sample') });
|
|
122
|
+
const up = await until(() => sent('/api/report-design/sample')[0]);
|
|
123
|
+
ok('an upload posts the file as base64 with its name and kind', !!up && up.body.name === 'Biweekly update, 9 Apr.pdf' && up.body.kind === 'pdf' && Buffer.from(up.body.data || '', 'base64').toString().startsWith('%PDF'), JSON.stringify(up && { ...up.body, data: String(up.body.data).slice(0, 12) }));
|
|
124
|
+
ok('the uploaded sample shows in the conversation', !!(await until(async () => /9 Apr\.pdf/.test(await text('[data-rd-chat]')))));
|
|
125
|
+
await page.click('[data-rd-act="panel"]'); await page.waitForSelector('[data-rd-link]');
|
|
126
|
+
await page.fill('[data-rd-link]', 'https://acme.atlassian.net/wiki/spaces/IDPF/pages/decisions');
|
|
127
|
+
await page.click('[data-rd-smp="link"]');
|
|
128
|
+
ok('a link posts {link}', !!(await until(() => sent('/api/report-design/sample').length === 2)) && /decisions$/.test(sent('/api/report-design/sample')[1].body.link || ''));
|
|
129
|
+
await page.click('[data-rd-act="panel"]'); await page.waitForSelector('[data-rd-smp="doc"]');
|
|
130
|
+
await page.click('[data-rd-smp="doc"] >> nth=0');
|
|
131
|
+
ok('a past PMOS document posts {doc} with its path', !!(await until(() => sent('/api/report-design/sample').length === 3)) && /^planning\/reports\//.test(sent('/api/report-design/sample')[2].body.doc || ''));
|
|
132
|
+
|
|
133
|
+
/* point and say (#design-point), then a turn with the marker, the running state, the new version */
|
|
134
|
+
await until(() => page.$('[data-rd-sbig] iframe'));
|
|
135
|
+
await clickUntil('[data-rd-sbig]', 0.5, 0.45, '[data-rd-mkdraft]');
|
|
136
|
+
ok('clicking the preview places a marker and asks what to change there', !!(await until(() => page.$('[data-rd-mkdraft]'))));
|
|
137
|
+
await page.fill('[data-rd-mkdraft]', 'Make this title smaller');
|
|
138
|
+
await page.click('[data-rd-act="mksave"]');
|
|
139
|
+
ok('the marker is numbered on the preview and listed under Point and say', !!(await until(() => page.$('[data-rd-mk]'))) && (await page.$$('[data-rd-mkrow]')).length === 1 && /Make this title smaller/.test(await text('[data-rd-point]')));
|
|
140
|
+
await shot(page, 'design-point-1440');
|
|
141
|
+
await page.fill('[data-rd-in]', 'Make it dense');
|
|
142
|
+
await page.click('[data-rd-act="send"]');
|
|
143
|
+
const turn = await until(() => sent('/api/report-design/turn')[0]);
|
|
144
|
+
ok('a turn posts the words and the markers as points', !!turn && turn.body.text === 'Make it dense' && Array.isArray(turn.body.points) && turn.body.points.length === 1 && turn.body.points[0].text === 'Make this title smaller' && turn.body.points[0].n === 1 && typeof turn.body.points[0].slide === 'string', JSON.stringify(turn && turn.body));
|
|
145
|
+
ok('while the job runs the conversation shows PMOS working', !!(await until(() => page.$('[data-rd-running]'), 3000)));
|
|
146
|
+
ok('the job lands D4 as the current version, with its PMOS line', !!(await until(async () => await page.getAttribute('[data-rd-v="4"]', 'aria-pressed') === 'true', 8000)) && /D4:/.test(await text('[data-rd-chat]')));
|
|
147
|
+
ok('the markers are cleared after the turn', (await page.$$('[data-rd-mkrow]')).length === 0);
|
|
148
|
+
await page.fill('[data-rd-in]', 'This turn should fail');
|
|
149
|
+
await page.click('[data-rd-act="send"]');
|
|
150
|
+
ok('a failed job shows an error line and makes no version', !!(await until(() => page.$('[data-rd-log="error"]'), 8000)) && (await page.$$('[data-rd-v]')).length === 4);
|
|
151
|
+
|
|
152
|
+
/* compare (#design-compare) */
|
|
153
|
+
await page.click('[data-rd-act="cmp"]');
|
|
154
|
+
ok('Compare two shows two versions side by side, the one before by default', !!(await until(() => page.$('[data-rd-compare]'))) && await page.getAttribute('[data-rd-a="3"]', 'aria-pressed') === 'true'
|
|
155
|
+
&& (await until(async () => (await page.$$('[data-rd-compare] iframe')).length >= 2)));
|
|
156
|
+
ok('What changed lists the change between them', /What changed from D3 to D4/i.test(await text('[data-rd-changed]')));
|
|
157
|
+
await page.click('[data-rd-a="1"]');
|
|
158
|
+
ok('another A version can be picked', !!(await until(async () => /What changed from D1 to D4/i.test(await text('[data-rd-changed]')))));
|
|
159
|
+
await shot(page, 'design-compare-1440');
|
|
160
|
+
await page.click('[data-rd-act="cmp"]');
|
|
161
|
+
|
|
162
|
+
/* revert */
|
|
163
|
+
await page.click('[data-rd-v="2"]');
|
|
164
|
+
await until(() => page.$('[data-rd-act="revert"]'));
|
|
165
|
+
await page.click('[data-rd-act="revert"]');
|
|
166
|
+
ok('Go back posts the version and the next turn builds on it', !!(await until(() => sent('/api/report-design/revert').length === 1)) && sent('/api/report-design/revert')[0].body.n === 2
|
|
167
|
+
&& !!(await until(async () => /Back to D2/.test(await text('[data-rd-chat]')))));
|
|
168
|
+
|
|
169
|
+
/* the design brief (#design-saved) */
|
|
170
|
+
await page.click('[data-rd-act="brief"]');
|
|
171
|
+
await page.waitForSelector('[data-rd-brief]');
|
|
172
|
+
ok('the Design brief shows the design, your description, your samples and the reference version', /Design D3/.test(await text('[data-rd-design]')) && /progress session/.test(await text('[data-rd-desc]')) && (await page.$$('[data-rd-sample]')).length >= 2 && !!(await page.$('[data-rd-ref] iframe')));
|
|
173
|
+
ok('What PMOS learned: the same each cycle, changes each cycle, learned from your turns', !!(await page.$('[data-rd-rule="same"]')) && !!(await page.$('[data-rd-rule="changes"]')) && !!(await page.$('[data-rd-rule="learned"]')));
|
|
174
|
+
ok('the brief lists the cycles drawn with the design', !!(await page.$('[data-rd-briefcycles]')));
|
|
175
|
+
await shot(page, 'design-saved-1440');
|
|
176
|
+
ok('the studio posted no accept on its own', sent('/api/report-design/accept').length === 0);
|
|
177
|
+
await closeAll(page);
|
|
178
|
+
|
|
179
|
+
/* a report with no design: describe, then the first turn */
|
|
180
|
+
await page.click('[data-rs-reg="rg0"] [data-rs-act="design"]');
|
|
181
|
+
await page.waitForSelector('[data-rd-studio]');
|
|
182
|
+
ok('a report with no design has no version yet and no brief', !!(await until(() => page.$('[data-rd-noversion]'))) && !(await page.$('[data-rd-act="brief"]')));
|
|
183
|
+
await page.click('[data-rd-fmt="deck"]');
|
|
184
|
+
await page.fill('[data-rd-in]', 'The PMOS biweekly for the team, decisions first');
|
|
185
|
+
await page.click('[data-rd-act="send"]');
|
|
186
|
+
const desc = await until(() => sent('/api/report-design/describe')[0]);
|
|
187
|
+
ok('the first words are posted as the description with the format, then a turn', !!desc && desc.body.id === 'rg0' && /decisions first/.test(desc.body.text) && desc.body.format === 'deck' && !!(await until(() => sent('/api/report-design/turn').some((t) => t.body.id === 'rg0'))));
|
|
188
|
+
ok('D1 arrives', !!(await until(() => page.$('[data-rd-v="1"]'), 8000)));
|
|
189
|
+
await closeAll(page);
|
|
190
|
+
|
|
191
|
+
/* a document design (#design-doc), then This is it */
|
|
192
|
+
await page.click('[data-rs-reg="rs3"] [data-rs-act="design"]');
|
|
193
|
+
await page.waitForSelector('[data-rd-studio] [data-rd-v]');
|
|
194
|
+
ok('a document design opens with Document selected and Not saved yet', await page.getAttribute('[data-rd-fmt="doc"]', 'aria-pressed') === 'true' && /Not saved yet/.test(await text('[data-rd-studio]')));
|
|
195
|
+
await shot(page, 'design-doc-1440');
|
|
196
|
+
await page.click('[data-rd-act="accept"]');
|
|
197
|
+
ok('This is it posts accept with the current version', !!(await until(() => sent('/api/report-design/accept').length === 1)) && sent('/api/report-design/accept')[0].body.id === 'rs3' && sent('/api/report-design/accept')[0].body.n === 2);
|
|
198
|
+
ok('the saved chip shows the saved version', !!(await until(async () => /Saved design: D2/.test(await text('[data-rd-saved]')))));
|
|
199
|
+
await closeAll(page);
|
|
200
|
+
|
|
201
|
+
/* ── the designed document of rs3 (pmos-sec) ── */
|
|
202
|
+
await openDoc(page, 'planning/reports/rs3/2026-10-01.md');
|
|
203
|
+
const df = await page.$('.rd-docf iframe');
|
|
204
|
+
ok('a document design reads as an interactive document in a sandboxed frame', !!df && await df.getAttribute('sandbox') === 'allow-same-origin' && (await (await df.contentFrame()).$$('section.pmos-sec')).length >= 3);
|
|
205
|
+
ok('the document says Notes on the document', /Add a note on the document/.test(await text('[data-rd-notes]')));
|
|
206
|
+
await closeAll(page);
|
|
207
|
+
|
|
208
|
+
/* ── the deck of rs1, Wed 21 Oct (#deck) ── */
|
|
209
|
+
await openDoc(page, DOC);
|
|
210
|
+
const sb2 = await page.$$eval('.sheet iframe', (a) => a.map((f) => f.getAttribute('sandbox')));
|
|
211
|
+
ok('every frame of the deck is sandbox="allow-same-origin" and none allows scripts', sb2.length >= 6 && sb2.every((s) => s === 'allow-same-origin'), JSON.stringify(sb2.slice(0, 4)));
|
|
212
|
+
ok('the bar shows the design, the slides, the difference, Present, Audience view, Refine the design, Print / PDF and Download .pptx',
|
|
213
|
+
/Design D3/.test(await text('[data-rd-deck]')) && /11 slides/.test(await text('[data-rd-deck]')) && !!(await page.$('[data-rd-act="present"]')) && !!(await page.$('[data-rd-act="audience"]')) && !!(await page.$('[data-rd-act="studio"]')) && !!(await page.$('[data-rd-act="print"]')) && !!(await page.$('[data-rd-act="pptx"]')));
|
|
214
|
+
ok('MORE THAN SLIDES lists what the deck does', /MORE THAN SLIDES/i.test(await text('[data-rd-caps]')) && /Presenter view with your notes/.test(await text('[data-rd-caps]')));
|
|
215
|
+
ok('a Slides report says the Google Slides copy is optional', /Google Slides copy of this report is optional/.test(await text('[data-rd-copynote]')));
|
|
216
|
+
ok('the strip has 11 slides and the first one is current', (await page.$$('[data-rd-go]')).length === 11 && await page.getAttribute('[data-rd-go="0"]', 'aria-current') === 'true' && /Slide 1 of 11/.test(await text('[data-rd-cap]')));
|
|
217
|
+
await page.click('[data-rd-act="next"]');
|
|
218
|
+
ok('next moves to slide 2', !!(await until(async () => /Slide 2 of 11/.test(await text('[data-rd-cap]')))));
|
|
219
|
+
await page.click('[data-rd-act="prev"]');
|
|
220
|
+
ok('prev moves back to slide 1', !!(await until(async () => /Slide 1 of 11/.test(await text('[data-rd-cap]')))));
|
|
221
|
+
await shot(page, 'deck-1440');
|
|
222
|
+
|
|
223
|
+
/* side by side with the last cycle (#deck-cycles) */
|
|
224
|
+
await page.click('[data-rd-act="mode"][data-v="cmp"]');
|
|
225
|
+
ok('Side by side shows the last cycle and this one, each with its state against the design', !!(await until(async () => (await page.$$('[data-rd-cmp] iframe')).length === 2))
|
|
226
|
+
&& /Wed 7 Oct/.test(await text('[data-rd-cmp]')) && /Matches the design/.test(await text('[data-rd-cmp]')) && /difference from the design/.test(await text('[data-rd-cmp]')));
|
|
227
|
+
await shot(page, 'deck-cycles-1440');
|
|
228
|
+
await page.click('[data-rd-act="mode"][data-v="deck"]');
|
|
229
|
+
await until(() => page.$('[data-rd-big] iframe'));
|
|
230
|
+
|
|
231
|
+
/* the data attributes, wired from the page */
|
|
232
|
+
await page.click('[data-rd-go="1"]'); await until(async () => /Slide 2 of 11/.test(await text('[data-rd-cap]')));
|
|
233
|
+
let bf = await until(() => bigFrame());
|
|
234
|
+
await until(async () => (await bf.$$('[data-value]')).length > 0);
|
|
235
|
+
await (await bf.$('[data-value]')).hover();
|
|
236
|
+
ok('data-value: the detail shows on hover', !!(await until(async () => /Login success/.test(await text('.rd-tip:not([hidden])')))));
|
|
237
|
+
await (await bf.$('[data-src]')).click();
|
|
238
|
+
ok('data-src: a click shows where the value comes from', !!(await until(async () => /comes from/.test(await text('[data-rd-src]')))));
|
|
239
|
+
await page.click('[data-rd-act="chg"]');
|
|
240
|
+
bf = await bigFrame();
|
|
241
|
+
ok('data-from: Show what changed adds the value of the last cycle', !!(await until(async () => { const f = await bigFrame(); return f && (await f.$$('.pmos-was')).length > 0; })));
|
|
242
|
+
await page.click('[data-rd-act="chg"]');
|
|
243
|
+
let moreAt = -1; let iniAt = -1;
|
|
244
|
+
for (let i = 0; i < 11 && (moreAt < 0 || iniAt < 0); i++) {
|
|
245
|
+
await page.click('[data-rd-go="' + i + '"]'); await until(async () => new RegExp('Slide ' + (i + 1) + ' of').test(await text('[data-rd-cap]')));
|
|
246
|
+
const f = await until(() => bigFrame()); await until(async () => (await f.$$('section')).length > 0, 3000);
|
|
247
|
+
if (moreAt < 0 && await f.$('[data-more]')) moreAt = i;
|
|
248
|
+
if (iniAt < 0 && await f.$('[data-initiative]')) iniAt = i;
|
|
249
|
+
}
|
|
250
|
+
ok('the deck has a part that expands and a live initiative link', moreAt >= 0 && iniAt >= 0, moreAt + ' / ' + iniAt);
|
|
251
|
+
if (moreAt >= 0) {
|
|
252
|
+
await page.click('[data-rd-go="' + moreAt + '"]'); await sleep(300); bf = await bigFrame();
|
|
253
|
+
const m = await bf.$('[data-more]'); await m.click();
|
|
254
|
+
ok('data-more: a click opens the part', !!(await until(async () => (await (await bigFrame()).$$('[data-more][data-open], details[data-more][open]')).length > 0)));
|
|
255
|
+
}
|
|
256
|
+
await page.click('[data-rd-go="0"]');
|
|
257
|
+
|
|
258
|
+
/* Differs from the design */
|
|
259
|
+
ok('Differs from the design names the extra slide and offers the three choices and Show the slide',
|
|
260
|
+
/one slide more than the design/.test(await text('[data-rd-diff]')) && !!(await page.$('[data-rd-decide="keep"]')) && !!(await page.$('[data-rd-decide="adopt"]')) && !!(await page.$('[data-rd-decide="match"]')) && !!(await page.$('[data-rd-diff] [data-rd-act="showslide"]')));
|
|
261
|
+
await page.click('[data-rd-diff] [data-rd-act="showslide"]');
|
|
262
|
+
ok('Show the slide moves to the extra slide', !!(await until(async () => /Slide 8 of 11/.test(await text('[data-rd-cap]')))));
|
|
263
|
+
await page.click('[data-rd-decide="keep"]');
|
|
264
|
+
const kd = await until(() => sent('/api/report-design/cycle-decision')[0]);
|
|
265
|
+
ok('Keep for this cycle posts keep with the difference', !!kd && kd.body.choice === 'keep' && kd.body.diff === 'added:init-idpf-21' && kd.body.path === DOC);
|
|
266
|
+
ok('the deck then says it was kept for this cycle', !!(await until(() => page.$('[data-rd-kept]'))) && !(await page.$('[data-rd-decide]')));
|
|
267
|
+
|
|
268
|
+
/* notes on the slide (#deck-pin) */
|
|
269
|
+
await page.click('[data-rd-go="1"]'); await until(async () => /Slide 2 of 11/.test(await text('[data-rd-cap]')));
|
|
270
|
+
ok('your pin is blue and the PMOS pin is a dashed purple proposal', !!(await until(() => page.$('[data-rd-big] [data-rd-pin="n1"]'))) && /\bh\b/.test(await page.getAttribute('[data-rd-pin="n1"]', 'class')) && /\ba\b.*\bprop\b/.test(await page.getAttribute('[data-rd-pin="n2"]', 'class')));
|
|
271
|
+
await page.click('[data-rd-big] [data-rd-pin="n2"]');
|
|
272
|
+
ok('a PMOS proposal offers Keep the note, Dismiss and Edit this', !!(await until(() => page.$('[data-rd-pinrow="n2"] [data-rd-pinact="keep"]'))) && !!(await page.$('[data-rd-pinrow="n2"] [data-rd-pinact="dismiss"]')) && !!(await page.$('[data-rd-pinrow="n2"] [data-rd-pinact="editthis"]')));
|
|
273
|
+
await shot(page, 'deck-pin-1440');
|
|
274
|
+
await page.click('[data-rd-pinrow="n2"] [data-rd-pinact="keep"]');
|
|
275
|
+
ok('Keep the note posts keep and the pin is no longer a proposal', !!(await until(() => sent('/api/report-notes/pin').some((p) => p.body.op === 'keep' && p.body.id === 'n2'))) && !!(await until(async () => !/\bprop\b/.test(await page.getAttribute('[data-rd-pin="n2"]', 'class') || 'prop'))));
|
|
276
|
+
await page.click('[data-rd-act="place"]');
|
|
277
|
+
await until(() => page.$('[data-rd-placing]'));
|
|
278
|
+
await clickUntil('[data-rd-big]', 0.3, 0.7, '[data-rd-pindraft]');
|
|
279
|
+
ok('Add a note: a click on the slide asks for the note', !!(await until(() => page.$('[data-rd-pindraft]'))));
|
|
280
|
+
await page.fill('[data-rd-pindraft]', 'Say the outage lasted 40 minutes.');
|
|
281
|
+
await page.click('[data-rd-act="pinsave"]');
|
|
282
|
+
const padd = await until(() => sent('/api/report-notes/pin').find((p) => p.body.op === 'add'));
|
|
283
|
+
ok('the note posts add with the slide and its place in percent', !!padd && padd.body.slide === 'headline' && padd.body.x > 0 && padd.body.x < 100 && padd.body.y > 0 && padd.body.y < 100 && /40 minutes/.test(padd.body.text), JSON.stringify(padd && padd.body));
|
|
284
|
+
ok('the new pin shows on the slide', !!(await until(async () => (await page.$$('[data-rd-big] [data-rd-pin]')).length === 3)));
|
|
285
|
+
|
|
286
|
+
/* speaker notes (#deck-speaker) */
|
|
287
|
+
ok('the speaker note of the slide is in its box, private', /Lead with sandbox keys/.test(await page.$eval('[data-rd-spk]', (x) => x.value)) && /Only you see speaker notes/.test(await text('[data-rd-spksec]')));
|
|
288
|
+
await page.fill('[data-rd-spk]', 'Lead with sandbox keys. Then the beta date.');
|
|
289
|
+
ok('a speaker note saves by itself', !!(await until(() => sent('/api/report-notes/speaker').some((p) => p.body.slide === 'headline' && p.body.text === 'Lead with sandbox keys. Then the beta date.'), 4000)));
|
|
290
|
+
await page.click('[data-rd-go="2"]'); await until(async () => /Slide 3 of 11/.test(await text('[data-rd-cap]')));
|
|
291
|
+
ok('a slide with no note shows the note of the last cycle with Keep and Clear', !!(await until(() => page.$('[data-rd-carry]'))) && /Wed 7 Oct/i.test(await text('[data-rd-carry]')) && !!(await page.$('[data-rd-carry] [data-rd-act="keep"]')) && !!(await page.$('[data-rd-carry] [data-rd-act="clear"]')));
|
|
292
|
+
await page.click('[data-rd-carry] [data-rd-act="keep"]');
|
|
293
|
+
ok('Keep posts keep:true and the note carries forward', !!(await until(() => sent('/api/report-notes/speaker').some((p) => p.body.slide === 'release' && p.body.keep === true))) && !!(await until(async () => /headers work/.test(await page.$eval('[data-rd-spk]', (x) => x.value).catch(() => '')))) && !(await page.$('[data-rd-carry]')));
|
|
294
|
+
let clearAt = -1;
|
|
295
|
+
for (let i = 3; i < 11 && clearAt < 0; i++) { await page.click('[data-rd-go="' + i + '"]'); await sleep(250); if (await page.$('[data-rd-carry]')) clearAt = i; }
|
|
296
|
+
if (clearAt >= 0) {
|
|
297
|
+
await page.click('[data-rd-carry] [data-rd-act="clear"]');
|
|
298
|
+
ok('Clear posts an empty note and the suggestion goes', !!(await until(() => sent('/api/report-notes/speaker').some((p) => p.body.text === '' && !p.body.keep))) && !!(await until(async () => !(await page.$('[data-rd-carry]')))));
|
|
299
|
+
} else ok('Clear posts an empty note and the suggestion goes', false, 'no second suggestion found');
|
|
300
|
+
await shot(page, 'deck-speaker-1440');
|
|
301
|
+
|
|
302
|
+
/* present (#deck-present, #deck-audience) */
|
|
303
|
+
await page.click('[data-rd-go="1"]'); await until(async () => /Slide 2 of 11/.test(await text('[data-rd-cap]')));
|
|
304
|
+
await page.click('[data-rd-act="present"]');
|
|
305
|
+
await page.waitForSelector('[data-rd-ov]');
|
|
306
|
+
ok('Present opens the presenter view: the slide, your note, the next slide, a timer, n / N and Jump to',
|
|
307
|
+
!!(await page.$('[data-rd-ov] [data-rd-ovslide] iframe')) && /Lead with sandbox keys/.test(await text('[data-rd-ovnote]')) && !!(await page.$('[data-rd-ovnext]')) && /\d\d:\d\d/.test(await text('[data-rd-timer]')) && /2 \/ 11/.test(await text('[data-rd-pn]')) && (await page.$$('[data-rd-jump]')).length === 11);
|
|
308
|
+
ok('the presenter view frames are sandboxed too', (await page.$$eval('[data-rd-ov] iframe', (a) => a.every((f) => f.getAttribute('sandbox') === 'allow-same-origin'))));
|
|
309
|
+
await shot(page, 'deck-present-1440');
|
|
310
|
+
const pn = () => text('[data-rd-pn]');
|
|
311
|
+
await page.keyboard.press('ArrowRight'); ok('ArrowRight goes to the next slide', !!(await until(async () => /3 \/ 11/.test(await pn()))));
|
|
312
|
+
await page.keyboard.press('ArrowLeft'); ok('ArrowLeft goes back', !!(await until(async () => /2 \/ 11/.test(await pn()))));
|
|
313
|
+
await page.keyboard.press('PageDown'); ok('PageDown goes on', !!(await until(async () => /3 \/ 11/.test(await pn()))));
|
|
314
|
+
await page.keyboard.press('End'); ok('End goes to the last slide', !!(await until(async () => /11 \/ 11/.test(await pn()))));
|
|
315
|
+
await page.keyboard.press('Home'); ok('Home goes to the first slide', !!(await until(async () => /1 \/ 11/.test(await pn()))));
|
|
316
|
+
await page.click('[data-rd-jump="4"]'); ok('Jump to goes to that slide', !!(await until(async () => /5 \/ 11/.test(await pn()))));
|
|
317
|
+
await page.click('[data-rd-ovact="aud"]');
|
|
318
|
+
ok('Audience view shows only the slide and n / N', !!(await until(() => page.$('[data-rd-ov].rd-aud, [data-rd-ov] [data-rd-count]'))) && !(await page.$('[data-rd-ovnote]')) && /5 \/ 11/.test(await text('[data-rd-count]')));
|
|
319
|
+
await shot(page, 'deck-audience-1440');
|
|
320
|
+
await page.keyboard.press(' '); ok('Space goes on', !!(await until(async () => /6 \/ 11/.test(await text('[data-rd-count]')))));
|
|
321
|
+
await page.evaluate(() => { const o = document.querySelector('[data-rd-ov]'); const mk = (type, x) => { const t = new Touch({ identifier: 1, target: o, clientX: x, clientY: 300 }); o.dispatchEvent(new TouchEvent(type, { changedTouches: [t], touches: type === 'touchend' ? [] : [t], bubbles: true })); }; mk('touchstart', 600); mk('touchend', 300); });
|
|
322
|
+
ok('a swipe to the left goes on', !!(await until(async () => /7 \/ 11/.test(await text('[data-rd-count]')))));
|
|
323
|
+
await page.keyboard.press('Escape');
|
|
324
|
+
ok('Escape closes the view and leaves the document open', !!(await until(async () => !(await page.$('[data-rd-ov]')))) && !!(await page.$('[data-rd-big]')));
|
|
325
|
+
await page.click('[data-rd-act="audience"]');
|
|
326
|
+
ok('Audience view opens straight from the bar', !!(await until(() => page.$('[data-rd-ov] [data-rd-count]'))));
|
|
327
|
+
await page.keyboard.press('Escape'); await until(async () => !(await page.$('[data-rd-ov]')));
|
|
328
|
+
|
|
329
|
+
/* print and download */
|
|
330
|
+
await page.evaluate(() => { window.__printed = []; window.__printCalls = 0; window.open = () => ({ document: { open() {}, write(h) { window.__printed.push(h); }, close() {} }, focus() {}, print() { window.__printCalls++; }, close() {}, addEventListener() {}, onload: null }); });
|
|
331
|
+
await page.click('[data-rd-act="print"]');
|
|
332
|
+
const printed = await until(() => page.evaluate(() => (window.__printed || []).join('')));
|
|
333
|
+
const nSlides = (printed.match(/<section[^>]*class="pmos-slide/g) || []).length;
|
|
334
|
+
ok('Print / PDF writes the slides, one per page', nSlides === 11 && /pmos-print-deck/.test(printed), nSlides + ' slides');
|
|
335
|
+
ok('the print holds no speaker note and no pin', !/Lead with sandbox keys|headers work|rd-pindot|outage on the login service|40 minutes/.test(printed));
|
|
336
|
+
await page.click('[data-rd-act="pptx"]');
|
|
337
|
+
ok('Download .pptx uses the existing export', !!(await until(() => gets.some((g) => /\/api\/report-export\?.*fmt=pptx/.test(g)) || gets.some((g) => /\/api\/report-export/.test(g) && /pptx/.test(g)))), gets.filter((g) => /export/.test(g)).join(' '));
|
|
338
|
+
|
|
339
|
+
/* the Edit log has the pin and the decision; Edit this opens the Edit tab at its section */
|
|
340
|
+
await page.click('[data-dmode="hist"]');
|
|
341
|
+
ok('the Edit log lists the pinned note and the design decision', !!(await until(async () => /Note pinned on the slide/.test(await text('.sheet')))) && /40 minutes/.test(await text('.sheet')) && /Kept the extra slide/.test(await text('.sheet')));
|
|
342
|
+
await page.click('[data-dmode="read"]');
|
|
343
|
+
await until(() => page.$('[data-rd-big]'));
|
|
344
|
+
await page.click('[data-rd-go="1"]'); await until(() => page.$('[data-rd-big] [data-rd-pin="n2"]'));
|
|
345
|
+
await page.click('[data-rd-big] [data-rd-pin="n2"]');
|
|
346
|
+
await until(() => page.$('[data-rd-pinrow="n2"] [data-rd-pinact="editthis"]'));
|
|
347
|
+
await page.click('[data-rd-pinrow="n2"] [data-rd-pinact="editthis"]');
|
|
348
|
+
ok('Edit this opens the Edit tab at the section of the note', !!(await until(() => page.$('[data-doc-editat]'))) && await page.getAttribute('[data-dmode="edit"]', 'aria-pressed') !== 'false');
|
|
349
|
+
await page.click('[data-dmode="read"]');
|
|
350
|
+
await until(() => page.$('[data-rd-big]'));
|
|
351
|
+
|
|
352
|
+
/* Refine the design from the document; an initiative link opens Initiatives */
|
|
353
|
+
await page.click('[data-rd-act="studio"]');
|
|
354
|
+
ok('Refine the design opens the studio', !!(await until(() => page.$('[data-rd-studio]'))));
|
|
355
|
+
await page.keyboard.press('Escape');
|
|
356
|
+
ok('closing the studio opens the document again', !!(await until(() => page.$('[data-rd-deck] [data-rd-big] iframe'))) && !(await page.$('[data-rd-studio]')));
|
|
357
|
+
await page.evaluate(() => { window.__ini = []; window.addEventListener('pmos:ini-go', (e) => window.__ini.push(e.detail)); });
|
|
358
|
+
if (iniAt >= 0) {
|
|
359
|
+
await page.click('[data-rd-go="' + iniAt + '"]'); await sleep(300); bf = await bigFrame();
|
|
360
|
+
const want = await (await bf.$('[data-initiative]')).getAttribute('data-initiative');
|
|
361
|
+
await (await bf.$('[data-initiative]')).click();
|
|
362
|
+
ok('data-initiative: a click shows the initiative with Open the initiative', !!(await until(() => page.$('[data-rd-src] [data-rd-act="openini"]'))) && (await text('[data-rd-src]')).includes(want));
|
|
363
|
+
await page.click('[data-rd-src] [data-rd-act="openini"]');
|
|
364
|
+
ok('Open the initiative opens it on the Initiatives page', !!(await until(() => page.evaluate((w) => (window.__ini || []).some((d) => d && d.id === w), want))), want);
|
|
365
|
+
} else ok('data-initiative: a click opens that initiative', false, 'no initiative link');
|
|
366
|
+
|
|
367
|
+
/* ── a matching cycle and the other two choices, each on a fresh demo ── */
|
|
368
|
+
await goReports(page);
|
|
369
|
+
await openDoc(page, 'planning/reports/rs1/2026-10-07.md');
|
|
370
|
+
ok('a cycle that follows the design shows Matches the design and no difference box', /Matches the design/.test(await text('[data-rd-match="yes"]')) && !(await page.$('[data-rd-diff]')));
|
|
371
|
+
await closeAll(page);
|
|
372
|
+
await openDoc(page, DOC);
|
|
373
|
+
await page.click('[data-rd-decide="adopt"]');
|
|
374
|
+
ok('Make it part of the design posts adopt', !!(await until(() => sent('/api/report-design/cycle-decision').some((p) => p.body.choice === 'adopt'))));
|
|
375
|
+
ok('then the deck matches the new design version', !!(await until(() => page.$('[data-rd-match="yes"]'))) && /Design D4/.test(await text('[data-rd-deck]')) && !(await page.$('[data-rd-diff]')));
|
|
376
|
+
await shot(page, 'deck-adopted-1440');
|
|
377
|
+
await goReports(page);
|
|
378
|
+
await openDoc(page, DOC);
|
|
379
|
+
await page.click('[data-rd-decide="match"]');
|
|
380
|
+
ok('Match the design posts match and PMOS draws the cycle again', !!(await until(() => sent('/api/report-design/cycle-decision').some((p) => p.body.choice === 'match'))) && !!(await until(() => page.$('[data-rd-rendering]'))));
|
|
381
|
+
ok('when it is drawn the deck matches the design with 10 slides', !!(await until(() => page.$('[data-rd-match="yes"]'), 8000)) && (await page.$$('[data-rd-go]')).length === 10);
|
|
382
|
+
|
|
383
|
+
/* ── phone ── */
|
|
384
|
+
const ph = await browser.newContext({ viewport: { width: 400, height: 860 }, hasTouch: true, isMobile: true });
|
|
385
|
+
const pp = await newPage(ph);
|
|
386
|
+
const noScroll = () => pp.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1);
|
|
387
|
+
await goReports(pp);
|
|
388
|
+
await openDoc(pp, DOC);
|
|
389
|
+
ok('400px: the deck has no horizontal scroll', await noScroll());
|
|
390
|
+
await shot(pp, 'deck-400');
|
|
391
|
+
await pp.click('[data-rd-act="present"]'); await pp.waitForSelector('[data-rd-ov]');
|
|
392
|
+
ok('400px: the presenter view has no horizontal scroll', await noScroll());
|
|
393
|
+
await pp.keyboard.press('Escape'); await until(async () => !(await pp.$('[data-rd-ov]')));
|
|
394
|
+
await closeAll(pp);
|
|
395
|
+
await pp.click('[data-rs-reg="rs1"] [data-rs-act="design"]'); await pp.waitForSelector('[data-rd-studio] [data-rd-v]');
|
|
396
|
+
ok('400px: the studio has no horizontal scroll', await noScroll());
|
|
397
|
+
await shot(pp, 'design-400');
|
|
398
|
+
await ph.close();
|
|
399
|
+
|
|
400
|
+
await sleep(300);
|
|
401
|
+
ok('no script error and no failed request', errors.length === 0, errors.slice(0, 4).join(' | '));
|
|
402
|
+
return finish();
|
|
403
|
+
})();
|
|
@@ -109,8 +109,9 @@ const TAB = '[data-rs-tab]';
|
|
|
109
109
|
ok('the Issued filter lists only issued documents', (await page.$$('[data-rs-doc] .tag.ok')).length === (await page.$$('[data-rs-doc]')).length && (await page.$$('[data-rs-doc]')).length >= 4);
|
|
110
110
|
await page.click('[data-rs-act="docf"][data-k="state"][data-v="all"]');
|
|
111
111
|
await page.click('[data-rs-doc$="rs1/2026-10-21.md"] [data-rs-act="opendoc"] >> nth=0');
|
|
112
|
-
ok('a draft opens in the document sheet with its format chip and an Issue button', !!(await until(() => page.$('[data-doc-format]'))) && !!(await page.$('[data-doc-issue]')) && /
|
|
113
|
-
|
|
112
|
+
ok('a draft opens in the document sheet with its format chip and an Issue button', !!(await until(() => page.$('[data-doc-format]'))) && !!(await page.$('[data-doc-issue]')) && /Slide deck/.test(await text('[data-doc-format]')));
|
|
113
|
+
/* rs1 has a saved design in the demo (report-design-studio), so its draft reads as the designed deck: a strip of slides, the title slide first */
|
|
114
|
+
ok('the Slide deck draft reads as its designed deck, with the title slide first', !!(await until(() => page.$('[data-rd-big] iframe'))) && (await page.$$('[data-rd-go]')).length >= 6 && /^Slide 1: Title/.test(await page.getAttribute('[data-rd-go="0"]', 'aria-label')));
|
|
114
115
|
ok('its copy chip says Google Slides: not created', /Google Slides: not created/.test(await text('[data-doc-copychip]')));
|
|
115
116
|
await shot(page, 'rs-draft-1440');
|
|
116
117
|
await page.click('[data-doc-more]');
|
|
@@ -89,7 +89,7 @@ const W = '[data-pm-work]';
|
|
|
89
89
|
/* the six views and their counts (R1) */
|
|
90
90
|
const readBar = () => st((w) => [...document.querySelectorAll(w + ' [data-pm-lenses] [data-lens]')].map((b) => [b.getAttribute('data-lens'), b.textContent.replace(/\d+$/, '').trim()]), W);
|
|
91
91
|
// A redraw puts a new bar in the header and the quiet layout moves it beside Add work 50 ms later; read it once it has settled.
|
|
92
|
-
await until(async () => (await readBar()).length === VIEWS.length,
|
|
92
|
+
await until(async () => (await readBar()).length === VIEWS.length, 10000, 100);
|
|
93
93
|
const bar = await readBar();
|
|
94
94
|
ok('the bar has the six views in order: Inbox, Today, Upcoming, Waiting, Someday, Review', JSON.stringify(bar.map((b) => b[0])) === JSON.stringify(VIEWS) && JSON.stringify(bar.map((b) => b[1])) === JSON.stringify(LABELS), JSON.stringify(bar));
|
|
95
95
|
const c0 = await counts();
|
package/workspace/guide/index.md
CHANGED
|
@@ -74,6 +74,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
74
74
|
- [Report slides](report-slides.md): how the Reports page shows a report as the PM-OS biweekly slides, Present and Print / PDF, and how your notes become the words on the slides.
|
|
75
75
|
- [Reports: dashboards of reports](reports-dashboards.md): the console Reports page, its dashboards at portfolio, board and initiative scope, how to edit one, how to send one as a report, and the Charts tab.
|
|
76
76
|
- [Set up a report by asking, and the report finder](reports-set-up.md): how to ask PMOS to draft a report you send, how the finder finds reports you already send, and how a cycle becomes a document.
|
|
77
|
+
- [Design how a report looks, and read it as a deck](report-design.md): how to design a report in the design studio, read each cycle as a deck or an interactive document, present it, and use speaker notes and notes on the slide.
|
|
77
78
|
- [The document sheet](document-sheet.md): read a document as rich text, edit it, accept the agent's proposals section by section, read the edit log, and clear a stale document.
|
|
78
79
|
- [Ask the agent from the page](ask-the-agent.md): the five presses that start an agent from the console, what the agent may do, and what stays yours.
|
|
79
80
|
- [Sessions in console3](console3-sessions.md): the Sessions panel, how to open the terminal of a session on the same page, and how to end a session.
|
|
@@ -24,18 +24,22 @@ Under the rows are a timeline of all boards (board › initiative › epic) and
|
|
|
24
24
|
|
|
25
25
|
The tabs are **Summary**, **Initiatives**, **Triage**, **Backlog**, **Board**, **Timeline**, **Releases**, **Docs** and **Sprints**. A board with Build and deliver off has no Releases and no Sprints tab.
|
|
26
26
|
|
|
27
|
-
On a screen wider than 600 pixels, the breadcrumb, the board status, the tabs and the Board settings gear are on one row. **Backlog**, **Releases**, **Docs** and **Sprints** are behind **More**. Press **More** to
|
|
27
|
+
On a screen wider than 600 pixels, the breadcrumb, the board status, the tabs and the Board settings gear are on one row. **Backlog**, **Releases**, **Docs** and **Sprints** are behind **More**. Press **More** to open a menu of them. When one of them is the open tab, it shows next to the other five tabs.
|
|
28
28
|
|
|
29
29
|
- **Summary** shows the initiatives at a glance and one bar per category. At a glance lists up to six open initiatives. Initiatives with work, and Now initiatives that are not on track, come first. The others follow by end date. Press a bar to open the Initiatives tab with that category.
|
|
30
30
|
- **Initiatives** lists the initiatives of the board, 25 on each page. See [Initiatives: status, hold, cancel, merge and archive](#initiatives-status-hold-cancel-merge-and-archive).
|
|
31
|
-
- **Triage** lists the new asks. Add an ask, then open it to see its draft. Press **Accept** to start an initiative. An initiative needs an anchor (a KR or a health budget). On a board, **New initiative** lists the anchors that `planning/okrs/` declares. Pick one from the list. Press **Ask for more** to ask a question, or **Decline** and give a reason.
|
|
32
|
-
- **Backlog**, **Board** and **Timeline** show the epics and items. Jira items and PMOS tasks are mixed. The
|
|
31
|
+
- **Triage** lists the new asks. Add an ask, then open it to see its draft. The New asks heading names the workflow and the first stage that **Accept** uses. Press **Accept** to start an initiative. An initiative needs an anchor (a KR or a health budget). On a board, **New initiative** lists the anchors that `planning/okrs/` declares. Pick one from the list. Press **Ask for more** to ask a question, or **Decline** and give a reason.
|
|
32
|
+
- **Backlog**, **Board** and **Timeline** show the epics and items. Jira items and PMOS tasks are mixed. The filters are on one row and are the same on the three tabs:
|
|
33
|
+
- Type in the search box to find an item by key, title or owner.
|
|
34
|
+
- Press **All initiatives** to choose one initiative. Its ID then shows on the button.
|
|
35
|
+
- Press the filter icon to set **Type**, **Source** (Jira or PMOS tasks) and **Done** (Show or Hide). A board with Build and deliver off has no Source. The badge on the icon counts the filters that are on.
|
|
36
|
+
- A filter that is on shows next to the icon. Press its x to remove it, or press **Clear** to remove all.
|
|
33
37
|
- In Backlog, add a PMOS task to an initiative.
|
|
34
38
|
- In Board, drag a PMOS task to a column, or use its menu. A Jira item has a lock: change it in Jira.
|
|
35
39
|
- In Timeline, use **Week**, **Month** and **Quarter** to zoom, and drag the empty chart to pan. Drag a PMOS bar to reschedule the task, or focus the bar and press the Left or Right arrow key. The toast has **Undo**. A Jira bar is locked.
|
|
36
|
-
- **Releases** shows the next release, the later ones and the past ones, with their scope initiatives.
|
|
40
|
+
- **Releases** shows the next release, the later ones and the past ones, with their scope initiatives. Release dates live on the Calendar. Press **On the Calendar** in the Unreleased heading to open it.
|
|
37
41
|
- **Docs** lists the documents of the board by initiative.
|
|
38
|
-
- **Sprints** shows the Jira sprints and the sprint burndown. They are read-only.
|
|
42
|
+
- **Sprints** shows the Jira sprints and the sprint burndown. They are read-only. The heading of the active sprint shows when PMOS last read them from Jira.
|
|
39
43
|
|
|
40
44
|
### Initiatives: status, hold, cancel, merge and archive
|
|
41
45
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Guide
|
|
3
|
+
title: "Design how a report looks, and read it as a deck"
|
|
4
|
+
description: "How to design the look of a report in the design studio, how each cycle reads as a deck or an interactive document, how to present it, and how speaker notes and notes on the slide work."
|
|
5
|
+
timestamp: 2026-10-13
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Design how a report looks
|
|
9
|
+
|
|
10
|
+
A report that you set up has a design. The design tells PMOS how the report looks: a slide deck or a document, the order of the slides, and the style. PMOS reads the design each cycle and draws the draft to match it. The data comes from the cycle. The design does not change the data.
|
|
11
|
+
|
|
12
|
+
There is no template to fill in. You describe the report, add samples, and refine it until it looks right.
|
|
13
|
+
|
|
14
|
+
## Open the design studio
|
|
15
|
+
|
|
16
|
+
Use one of these two buttons:
|
|
17
|
+
|
|
18
|
+
- In the **Set up** list of the Reports tab, press **Design how it looks** on the row of the report.
|
|
19
|
+
- In a report document that has a design, press **Refine the design**.
|
|
20
|
+
|
|
21
|
+
## Describe, add samples and refine
|
|
22
|
+
|
|
23
|
+
1. Select **Deck** or **Document** at the top of the studio.
|
|
24
|
+
2. Write what the report is for and who reads it. For example: "This goes to the progress session every other Thursday. The decisions must stand out."
|
|
25
|
+
3. Press **Add a sample** to give PMOS a report that people liked. You can upload a file, paste a link, or pick a report document that PMOS has. Each sample shows what PMOS took from it.
|
|
26
|
+
4. Press **Make the next version**. PMOS makes a version from your words and this cycle's data. This takes a short time. You can leave the studio. The studio shows when the version is ready.
|
|
27
|
+
5. Read the version in the preview. Write what to change and press **Make the next version** again. Each turn makes a new version: D1, D2, D3 and so on.
|
|
28
|
+
|
|
29
|
+
If a turn fails, the studio shows why. Nothing changes. Try the turn again.
|
|
30
|
+
|
|
31
|
+
### Point at a part
|
|
32
|
+
|
|
33
|
+
Keep **Point and say** on. Click a part of the preview and write what to change there. A numbered marker shows the spot. Your next turn sends the markers with your words.
|
|
34
|
+
|
|
35
|
+
### Compare and go back
|
|
36
|
+
|
|
37
|
+
- Select a version in the strip to show it.
|
|
38
|
+
- Press **Compare two** to show two versions side by side. The list under them tells what changed.
|
|
39
|
+
- On an earlier version, press **Go back to** that version, for example **Go back to D2**. The next turn builds on it. The later versions stay in the strip.
|
|
40
|
+
|
|
41
|
+
### Save the design
|
|
42
|
+
|
|
43
|
+
Press **This is it** to save the version as the design. Every next draft follows it. Press **Design brief** to read the saved design: your description, your samples, the reference version, and what PMOS learned. What PMOS learned has three parts: what stays the same each cycle, what changes each cycle, and what it learned from your turns.
|
|
44
|
+
|
|
45
|
+
## Read a cycle as a deck
|
|
46
|
+
|
|
47
|
+
A cycle of a report with a design opens in the **Read** tab as a deck. A report whose design is a document opens as an interactive document.
|
|
48
|
+
|
|
49
|
+
- Use the strip of slides, or the previous and next buttons, to move.
|
|
50
|
+
- Move the pointer onto a value to show its detail.
|
|
51
|
+
- Click a number or a status to show where it comes from.
|
|
52
|
+
- Click a part that has more to show it.
|
|
53
|
+
- Click an initiative to open it in Initiatives.
|
|
54
|
+
- Press **Show what changed** to mark the values that changed since the last cycle. The value of the last cycle shows next to each one.
|
|
55
|
+
|
|
56
|
+
The deck shows in a frame that cannot run scripts. The console adds each of these actions.
|
|
57
|
+
|
|
58
|
+
### When a draft differs from the design
|
|
59
|
+
|
|
60
|
+
Sometimes the data of a cycle does not fit the design. For example, a new initiative needs its own slide. The **Differs from the design** box tells what is different. Select one:
|
|
61
|
+
|
|
62
|
+
- **Keep for this cycle**: the draft stays as it is. The design does not change.
|
|
63
|
+
- **Make it part of the design**: PMOS adds the change to the design as a new version. Every next cycle follows it.
|
|
64
|
+
- **Match the design**: PMOS draws the cycle again to match the design.
|
|
65
|
+
- **Show the slide**: shows the slide that is different.
|
|
66
|
+
|
|
67
|
+
Each choice is in the Edit log. When the draft follows the design, the deck shows **Matches the design**.
|
|
68
|
+
|
|
69
|
+
## Present
|
|
70
|
+
|
|
71
|
+
- Press **Present** to open the presenter view. It shows the current slide, your speaker note, the next slide, a timer, the slide number and a list to jump to a slide.
|
|
72
|
+
- Press **Audience view** to show only the slide, full screen.
|
|
73
|
+
|
|
74
|
+
To move, use the arrow keys, Page Up and Page Down, or Space. Home goes to the first slide and End goes to the last slide. On a phone, swipe. Press Escape to close.
|
|
75
|
+
|
|
76
|
+
## Speaker notes
|
|
77
|
+
|
|
78
|
+
Each slide has a speaker note. Only you see speaker notes. They are not on the slide, the PDF or the .pptx. Write the note in the box under the slide. PMOS saves it.
|
|
79
|
+
|
|
80
|
+
When the last cycle had a note for the same slide and this cycle has none, the box shows that note. Press **Keep** to use it for this cycle. Press **Clear** to start with no note.
|
|
81
|
+
|
|
82
|
+
## Notes on the slide
|
|
83
|
+
|
|
84
|
+
Notes on the slide are for readers. They do not change the words of the report.
|
|
85
|
+
|
|
86
|
+
- Press **Add a note on the slide**, then click the spot on the slide. Write the note and save it.
|
|
87
|
+
- Your notes are blue. A note that PMOS proposes is purple with a dashed edge.
|
|
88
|
+
- On a note that PMOS proposes, press **Keep the note** or **Dismiss**.
|
|
89
|
+
- Press **Edit this** to change the words of the report. The Edit tab opens at that section.
|
|
90
|
+
|
|
91
|
+
Each note that you add, keep, dismiss, change or remove is in the Edit log.
|
|
92
|
+
|
|
93
|
+
## Print and download
|
|
94
|
+
|
|
95
|
+
- **Print / PDF** prints one slide on each page. It does not print speaker notes or notes on the slide.
|
|
96
|
+
- **Download .pptx** downloads the deck as a PowerPoint file.
|
|
97
|
+
|
|
98
|
+
Present, print and download stay on your device. Nothing is sent.
|
|
99
|
+
|
|
100
|
+
## The output of a report
|
|
101
|
+
|
|
102
|
+
The output **Slide deck** means that the deck lives in PMOS. A report that had a Google Slides copy keeps it. The copy is optional. The document tells you so, with a link to **Copies in other tools**.
|
|
103
|
+
|
|
104
|
+
See also [Set up a report by asking](reports-set-up.md) and [Report slides](report-slides.md).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Report slides: present and print a report"
|
|
4
4
|
description: "How the Reports page shows a report as the PM-OS biweekly slides, how to present and print them, and how your notes become the words on the slides."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-13
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Report slides
|
|
@@ -26,3 +26,5 @@ Readers can sign in with a one-time code.
|
|
|
26
26
|
The type is Launch, Improvement, Fix, Security, Deprecation or Beta. To give an entry its own date, add it after the title, for example `(2026-10-02)`. When the note is empty, the slide says that no release notes are logged.
|
|
27
27
|
|
|
28
28
|
A draft from before the slides has no slides block. Press **Redraft** to add it. Your notes stay as you wrote them.
|
|
29
|
+
|
|
30
|
+
A report that you set up can have its own design, as a deck or an interactive document, with a presenter view, speaker notes and notes on the slide. See [Design how a report looks](report-design.md).
|