create-pmos 0.197.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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-report-design-real-test.js +225 -0
  7. package/workspace/c3-report-design-test.js +403 -0
  8. package/workspace/c3-report-setup-test.js +3 -2
  9. package/workspace/c3-work-test.js +1 -1
  10. package/workspace/guide/index.md +1 -0
  11. package/workspace/guide/report-design.md +104 -0
  12. package/workspace/guide/report-slides.md +3 -1
  13. package/workspace/guide/reports-set-up.md +2 -1
  14. package/workspace/lib/headless.js +24 -6
  15. package/workspace/lib/report-design.js +741 -0
  16. package/workspace/lib/report-draft.js +8 -3
  17. package/workspace/lib/report-setups.js +1 -1
  18. package/workspace/lib/route-modules.js +1 -0
  19. package/workspace/lib/routes-doc-sheet.js +8 -1
  20. package/workspace/lib/routes-report-design.js +481 -0
  21. package/workspace/lib/routes-reports.js +2 -1
  22. package/workspace/package-lock.json +2 -2
  23. package/workspace/package.json +1 -1
  24. package/workspace/public/pm/c3-demo-rd.js +426 -0
  25. package/workspace/public/pm/c3-demo-rep2.js +2 -0
  26. package/workspace/public/pm/c3-demo-rs.js +5 -1
  27. package/workspace/public/pm/c3-design-studio.js +308 -0
  28. package/workspace/public/pm/c3-doc-formats.js +3 -2
  29. package/workspace/public/pm/c3-doc-sheet.js +30 -5
  30. package/workspace/public/pm/c3-initiatives.js +2 -1
  31. package/workspace/public/pm/c3-report-deck.css +139 -0
  32. package/workspace/public/pm/c3-report-deck.js +558 -0
  33. package/workspace/public/pm/c3-report-frame.css +20 -0
  34. package/workspace/public/pm/c3-report-setup.js +18 -6
  35. package/workspace/server.js +17 -5
  36. package/workspace/skills/report-design.skill +61 -0
  37. package/workspace/test-lists/report-design-backend.json +5 -0
  38. package/workspace/test-lists/report-design-studio-ui.json +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.197.0",
3
+ "version": "0.198.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@5be70d5
1
+ kernel-source: pmos@94c2b4f
2
2
  assembled: 2026-10-13
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-13T09:43:42Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T10:14:18Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-13T09:43:42Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T10:14:18Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-13T09:43:42Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T10:14:18Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* c3-report-design-real-test.js — the report design studio and the deck of a cycle against the REAL backend (initiative report-design-studio).
4
+ * Not demo mode: a real server on a scratch repository, with a stub `claude` that plays the report-design job (as report-design-server.test.js
5
+ * does), and a real Chromium on /c3.html. It proves the page and the server agree on the API contract end to end.
6
+ *
7
+ * setup a recurring work-sourced report set up through the API (its first cycle drafted)
8
+ * studio Design how it looks on the set-up row; describe; upload a small .pptx; the first turn lands D1 in the strip and the preview
9
+ * frame shows the render; a point-and-say marker goes with the second turn and lands D2; Compare two; This is it; the Design brief
10
+ * cycle Draft now on the report: the cycle hook renders the cycle to match the saved design
11
+ * deck the Read tab shows the deck from /api/report-design/cycle-html in frames with sandbox exactly "allow-same-origin"; a source
12
+ * popover; a speaker note and a pin are saved; the pin is in the Edit log; Present answers ArrowRight and Escape
13
+ * No page error and no failed request may be raised. Synthetic repositories only; the stub is the only "model"; nothing reaches the network. */
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { spawn } = require('child_process');
17
+ const { findBrowser } = require('./lib/find-browser');
18
+ const T = require('./test-run').configure({ name: 'c3-report-design-real' });
19
+ const { makeRepos, codeRepo, write, git } = require('./plan-test-repos');
20
+ const RS = require('./lib/report-setups.js');
21
+ const D = require('./lib/report-design.js');
22
+ const RX = require('./lib/report-export.js');
23
+
24
+ const BASE = T.BASE;
25
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
26
+ const until = async (fn, ms = 15000, step = 150) => { const t0 = Date.now(); for (;;) { let v; try { v = await fn(); } catch { v = null; } if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); } };
27
+ const results = [];
28
+ const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + String(extra).slice(0, 600) : ''}`); };
29
+
30
+ /* ── the scratch repository and the stub `claude` ── */
31
+ const R = makeRepos('c3-report-design-real');
32
+ const team = codeRepo(path.join(R.base, 'team'));
33
+ fs.mkdirSync(path.join(team, 'pmos', 'state'), { recursive: true });
34
+ write(team, 'pmos/state/.keep', '');
35
+ git(team, ['add', '-A']); git(team, ['commit', '-q', '-m', 'state']);
36
+ const fakeBin = path.join(R.base, 'bin'); fs.mkdirSync(fakeBin, { recursive: true });
37
+ /* The stub plays the report-design run: it reads the job block, writes the render to the output path and prints the JSON result.
38
+ * A turn makes a title slide (with a data-src part when the cycle document exists) and a headline slide, plus a risks chart when a marker came. */
39
+ fs.writeFileSync(path.join(fakeBin, 'claude'), `#!/usr/bin/env node
40
+ const fs = require('fs');
41
+ let input = ''; process.stdin.on('data', (d) => { input += d; }); process.stdin.on('end', () => {
42
+ const line = (k) => { const m = new RegExp('^- ' + k + ': (.*)$', 'm').exec(input); return m ? m[1].trim() : null; };
43
+ const mode = line('Mode'); const out = line('Output path');
44
+ const done = (o) => { process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', is_error: false, result: JSON.stringify(o), num_turns: 2, duration_ms: 500, permission_denials: [] })); process.exit(0); };
45
+ const sec = (id, kind, title, body) => '<section class="pmos-slide" data-slide-id="' + id + '" data-kind="' + kind + '" data-title="' + title + '">' + (body || '<h2>' + title + '</h2>') + '</section>';
46
+ if (mode === 'turn') {
47
+ const doc = line('Current cycle document');
48
+ const hasDoc = doc && fs.existsSync(doc);
49
+ const points = /Point markers:\\n {2}- 1\\./.test(input);
50
+ const html = '<style>.pmos-slide{width:1280px;height:720px;background:#fff}.pmos-slide h1{font-size:64px;margin:80px}</style>'
51
+ + sec('title', 'title', 'Report', '<h1>Report</h1>' + (hasDoc ? '<p><span data-src="doc:cycle" data-value="Read from this cycle">from the cycle</span></p>' : ''))
52
+ + sec('headline', 'headline', 'Headline') + (points ? sec('risks', 'chart', 'Risks') : '');
53
+ const took = [...input.matchAll(/^ {2}- (s-[0-9a-f]{8}) \\(NEW\\) "([^"]*)"/gm)].map((m) => ({ id: m[1], took: 'Numbered blue header band from ' + m[2], readable: true }));
54
+ fs.writeFileSync(out, html);
55
+ // The first turn takes a moment, so the page's running state can be seen on a fast runner too.
56
+ setTimeout(() => done({ answer: points ? 'Made the risks a chart, as you pointed.' : 'Built the first version from your description.', took, rules: { same: ['The slide order'], changes: ['The numbers'], learned: points ? ['Risks as a chart'] : [] } }), points ? 0 : 3000);
57
+ } else {
58
+ const ref = fs.readFileSync(line('Reference version'), 'utf8');
59
+ fs.writeFileSync(out, mode === 'match' ? ref : ref + sec('init-new', 'initiative', 'New initiative'));
60
+ done({ answer: 'Rendered the cycle.' });
61
+ }
62
+ });
63
+ `, { mode: 0o755 });
64
+ /* a small .pptx with a thumbnail, as a PM's past deck */
65
+ const jpg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 1, 2, 3, 4]);
66
+ const PPTX = RX.zip([{ name: 'ppt/slides/slide1.xml', data: '<p:sld><a:p><a:r><a:t>Numbered blue header</a:t></a:r></a:p></p:sld>' }, { name: 'docProps/thumbnail.jpeg', data: jpg }]);
67
+
68
+ let srv = null; let browser = null;
69
+ const finish = async (code) => {
70
+ if (browser) await browser.close().catch(() => {});
71
+ if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
72
+ T.killTmux();
73
+ R.cleanup();
74
+ const bad = results.filter((x) => !x.cond).length;
75
+ console.log(`\n${results.length - bad}/${results.length} c3-report-design-real assertions passed`);
76
+ process.exit(code !== undefined ? code : (bad ? 1 : 0));
77
+ };
78
+ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
79
+
80
+ (async () => {
81
+ const exe = findBrowser();
82
+ if (!exe) { console.log('✗ no Chrome/Chromium on this machine, c3-report-design-real cannot run'); return finish(1); }
83
+ let chromium;
84
+ try { ({ chromium } = require('playwright-core')); } catch { console.log('✗ playwright-core is not installed, c3-report-design-real cannot run'); return finish(1); }
85
+
86
+ srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore',
87
+ env: T.env({ PMOS_WS_PORT: String(T.PORT), PMOS_SELF_ROOT: team, PMOS_NO_OPEN: '1', PMOS_TICK_MS: '300', PMOS_REPORT_FIND: 'off', PATH: fakeBin + path.delimiter + process.env.PATH }) });
88
+ await T.waitForServer(BASE);
89
+ const press = await T.pressHeaders(BASE);
90
+ const call = async (method, p, body) => { const r = await fetch(BASE + p, { method, headers: method === 'POST' ? press : {}, body: body === undefined ? undefined : JSON.stringify(body) }); const t = await r.text(); let j = null; try { j = JSON.parse(t); } catch { /* not JSON */ } return { status: r.status, body: j, raw: t }; };
91
+
92
+ /* ── setup: a recurring work-sourced report, through the API ── */
93
+ const ws = (await call('POST', '/api/workspaces', { root: team, focus: true })).body.workspace.id;
94
+ const proposal = { name: 'IDPF biweekly', purpose: 'Tell the attendees what shipped.', audience: 'eShop attendees', source: 'work', output: 'google-slides',
95
+ schedule: { cadence: 'weekly', day: 'Wed', time: '09:00' }, sources: ['The local ledgers'], sections: RS.FRAME.slice(), learned_from: 'The June copy.', fixes: [] };
96
+ const made = await call('POST', '/api/report-setups/setup', { ws, proposal });
97
+ const id = made.body && made.body.setup && made.body.setup.id;
98
+ const firstPath = made.body && made.body.drafted && made.body.drafted.path;
99
+ ok('setup: a recurring work-sourced report is set up and its first cycle drafted', !!ws && !!id && !!firstPath, made.raw);
100
+ if (!id) return finish(1);
101
+
102
+ browser = await chromium.launch({ executablePath: exe });
103
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
104
+ const page = await ctx.newPage();
105
+ const errors = [];
106
+ page.on('pageerror', (e) => errors.push('pageerror: ' + String((e && e.message) || e)));
107
+ page.on('requestfailed', (r) => { const f = r.failure(); if (!/ERR_ABORTED/.test((f && f.errorText) || '')) errors.push('failed ' + r.url() + ' ' + ((f && f.errorText) || '')); });
108
+ page.on('response', (r) => { if (r.status() >= 400) errors.push('http ' + r.status() + ' ' + r.request().method() + ' ' + r.url()); });
109
+ page.on('dialog', (d) => d.dismiss().catch(() => {}));
110
+ const text = (sel) => page.textContent(sel).catch(() => '');
111
+ const clickAt = async (sel, fx, fy) => { await page.$eval(sel, (n) => n.scrollIntoView({ block: 'center' })); await sleep(150); const b = await (await page.$(sel)).boundingBox(); await page.mouse.click(b.x + b.width * fx, b.y + b.height * fy); };
112
+ /* a click inside a frame lands only once the frame has loaded and been wired: click again until the answer shows */
113
+ const clickUntil = async (sel, fx, fy, want) => { for (let i = 0; i < 6; i++) { await clickAt(sel, fx, fy); if (await until(() => page.$(want), 1500)) return true; } return false; };
114
+ const closeAll = async () => { for (let i = 0; i < 3 && await page.$('.sheet'); i++) { await page.keyboard.press('Escape'); await sleep(250); } };
115
+ const goReports = async () => { await page.goto(BASE + '/c3.html?tab=rep'); await page.waitForSelector('[data-rp3]'); await page.click('[data-rp3-tab="reports"]'); await page.waitForSelector('[data-rs-reg="' + id + '"]', { timeout: 15000 }); };
116
+
117
+ /* ── studio ── */
118
+ await goReports();
119
+ ok('the set-up row has Design how it looks', !!(await page.$('[data-rs-reg="' + id + '"] [data-rs-act="design"]')));
120
+ await page.click('[data-rs-reg="' + id + '"] [data-rs-act="design"]');
121
+ ok('the studio opens on a report with no version yet', !!(await until(() => page.$('[data-rd-studio] [data-rd-noversion]'))));
122
+ await page.click('[data-rd-fmt="deck"]');
123
+ await page.click('[data-rd-act="panel"]'); await page.waitForSelector('[data-rd-panel]');
124
+ await page.setInputFiles('[data-rd-file]', { name: 'Biweekly update, 26 Mar.pptx', mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', buffer: PPTX });
125
+ ok('the uploaded sample shows in the conversation with its thumbnail', !!(await until(async () => /26 Mar\.pptx/.test(await text('[data-rd-chat]')) && !!(await page.$('[data-rd-chat] img.rd-dks-thumb')))));
126
+ await page.fill('[data-rd-in]', 'A biweekly deck for leadership: numbered blue header band, one slide per initiative.');
127
+ await page.click('[data-rd-act="send"]');
128
+ ok('while the job runs the conversation shows PMOS working', !!(await until(() => page.$('[data-rd-running]'), 8000)));
129
+ ok('D1 lands in the strip as the current version', !!(await until(async () => await page.getAttribute('[data-rd-v="1"]', 'aria-pressed') === 'true', 30000)));
130
+ const sframe = async () => { const h = await page.$('[data-rd-sbig] iframe'); return h ? h.contentFrame() : null; };
131
+ ok('the preview frame shows the render of D1', !!(await until(async () => { const f = await sframe(); return f && (await f.$$('section.pmos-slide[data-slide-id="title"]')).length === 1; })));
132
+ ok('the sample says what PMOS took from it', !!(await until(async () => /PMOS took: Numbered blue header band/.test(await text('[data-rd-chat]')))));
133
+ ok('the description and the PMOS line naming D1 are in the conversation', !!(await page.$('[data-rd-log="describe"]')) && /D1:/.test(await text('[data-rd-chat]')));
134
+
135
+ /* point and say → D2 */
136
+ await page.click('[data-rd-sgo="1"]');
137
+ await until(async () => /Slide 2 of 2/.test(await text('[data-rd-scap]')));
138
+ ok('clicking the preview places a marker and asks what to change there', await clickUntil('[data-rd-sbig]', 0.5, 0.45, '[data-rd-mkdraft]'));
139
+ await page.fill('[data-rd-mkdraft]', 'Risks as a bar chart here');
140
+ await page.click('[data-rd-act="mksave"]');
141
+ ok('the marker is listed under Point and say', !!(await until(() => page.$('[data-rd-mkrow="1"]'))));
142
+ await page.fill('[data-rd-in]', 'Add the risks.');
143
+ await page.click('[data-rd-act="send"]');
144
+ ok('the second turn lands D2 as the current version', !!(await until(async () => await page.getAttribute('[data-rd-v="2"]', 'aria-pressed') === 'true', 30000)));
145
+ const g2 = (await call('GET', `/api/report-design?ws=${ws}&id=${id}`)).body;
146
+ const turn2 = g2 && g2.design.log.find((l) => l.kind === 'turn' && l.points && l.points.length);
147
+ ok('the server got the marker with the turn (slide, x, y, text) and D2 has the risks slide', !!turn2 && turn2.points[0].slide === 'headline' && turn2.points[0].text === 'Risks as a bar chart here' && g2.design.versions[1].outline.some((o) => o.id === 'risks'), JSON.stringify(g2 && g2.design.log));
148
+ ok('the markers are cleared after the turn', (await page.$$('[data-rd-mkrow]')).length === 0);
149
+
150
+ /* Compare two */
151
+ await page.click('[data-rd-act="cmp"]');
152
+ ok('Compare two shows D1 and D2 side by side', !!(await until(async () => (await page.$$('[data-rd-compare] iframe')).length >= 2)) && await page.getAttribute('[data-rd-a="1"]', 'aria-pressed') === 'true');
153
+ ok('What changed names the added slide', !!(await until(async () => /What changed from D1 to D2/i.test(await text('[data-rd-changed]')) && /Added: Risks/.test(await text('[data-rd-changed]')))));
154
+ await page.click('[data-rd-act="cmp"]');
155
+
156
+ /* This is it → the Design brief */
157
+ await until(async () => await page.getAttribute('[data-rd-v="2"]', 'aria-pressed') === 'true');
158
+ await page.click('[data-rd-act="accept"]');
159
+ ok('This is it saves D2', !!(await until(async () => /Saved design: D2/.test(await text('[data-rd-saved]')))));
160
+ const P = D.paths(team, id);
161
+ ok('brief.md is written', fs.existsSync(P.brief));
162
+ await page.click('[data-rd-act="brief"]');
163
+ await page.waitForSelector('[data-rd-brief]');
164
+ ok('the Design brief shows the design, the description, the sample, the reference version and the rules',
165
+ !!(await until(async () => /Design D2/.test(await text('[data-rd-design]')))) && /numbered blue header band/.test(await text('[data-rd-desc]')) && (await page.$$('[data-rd-sample]')).length === 1
166
+ && !!(await until(() => page.$('[data-rd-ref] iframe'))) && !!(await page.$('[data-rd-rule="learned"]')));
167
+ ok('the brief lists the cycles drawn with the design', !!(await until(() => page.$('[data-rd-briefcycle]'))));
168
+ await closeAll();
169
+
170
+ /* ── Draft now: the cycle hook renders the cycle ── */
171
+ const nextSlot = new Date(Date.parse(D.cyclePaths(team, firstPath).slot + 'T12:00:00Z') + 7 * 86400000).toISOString().slice(0, 10);
172
+ const dn = await call('POST', '/api/report/draft', { ws, cadence: id, slot: nextSlot });
173
+ const cyc = dn.body && dn.body.path;
174
+ ok('Draft now writes the next cycle', dn.status === 200 && !!cyc && cyc !== firstPath, dn.raw);
175
+ const cy = await until(async () => { const c = (await call('GET', `/api/report-design/cycle?ws=${ws}&path=${encodeURIComponent(cyc)}`)).body; return c && c.design_version && !c.rendering ? c : null; }, 30000);
176
+ ok('the cycle hook renders it to match the saved design', !!cy && cy.designed && cy.design_version === 2 && cy.differences.length === 1, JSON.stringify(cy));
177
+
178
+ /* ── the report document: the deck in the Read tab ── */
179
+ await goReports();
180
+ await page.waitForSelector('[data-rs-doc="' + cyc + '"]', { timeout: 15000 });
181
+ await page.click('[data-rs-doc="' + cyc + '"] [data-rs-act="opendoc"] >> nth=0');
182
+ ok('the Read tab shows the deck of the cycle', !!(await until(() => page.$('[data-rd-deck] [data-rd-big] iframe'))));
183
+ const bigFrame = async () => { const h = await page.$('[data-rd-big] iframe'); return h ? h.contentFrame() : null; };
184
+ ok('the deck is the cycle render: four slides with the new initiative slide', (await page.$$('[data-rd-go]')).length === 4 && /Design D2/.test(await text('[data-rd-deck]')) && /Slide 1 of 4/.test(await text('[data-rd-cap]')));
185
+ const sbx = await page.$$eval('.sheet iframe', (a) => a.map((f) => f.getAttribute('sandbox')));
186
+ ok('every frame of the deck is sandbox exactly "allow-same-origin"', sbx.length >= 4 && sbx.every((s) => s === 'allow-same-origin'), JSON.stringify(sbx));
187
+ ok('Differs from the design names the new slide', /Differs from the design/.test(await text('[data-rd-diff]')));
188
+ /* the source popover: the title slide's data-src part */
189
+ let srcOk = false;
190
+ for (let i = 0; i < 6 && !srcOk; i++) {
191
+ const f = await until(() => bigFrame()); const n = f && await until(() => f.$('[data-src]'), 3000);
192
+ if (n) { await n.click().catch(() => {}); srcOk = !!(await until(async () => /Where "from the cycle" comes from/.test(await text('[data-rd-src]')), 1500)); }
193
+ }
194
+ ok('a source chip opens its popover: where the value comes from', srcOk && /Document: cycle/.test(await text('[data-rd-src]')));
195
+ /* a speaker note */
196
+ await page.fill('[data-rd-spk]', 'Open with the release.');
197
+ const spk = await until(async () => { const n = (await call('GET', `/api/report-notes?ws=${ws}&path=${encodeURIComponent(cyc)}`)).body; return n && n.speaker && n.speaker.title && n.speaker.title.text === 'Open with the release.' ? n : null; }, 6000);
198
+ ok('a speaker note saves on the server', !!spk);
199
+ /* a pin */
200
+ await page.click('[data-rd-act="place"]');
201
+ await until(() => page.$('[data-rd-placing]'));
202
+ ok('Add a note: a click on the slide asks for the note', await clickUntil('[data-rd-big]', 0.3, 0.7, '[data-rd-pindraft]'));
203
+ await page.fill('[data-rd-pindraft]', 'Say the outage lasted 40 minutes.');
204
+ await page.click('[data-rd-act="pinsave"]');
205
+ ok('the pin shows on the slide', !!(await until(() => page.$('[data-rd-big] [data-rd-pin]'))));
206
+ const pins = (await call('GET', `/api/report-notes?ws=${ws}&path=${encodeURIComponent(cyc)}`)).body;
207
+ ok('the pin is on the server with its slide and its place in percent', !!pins && pins.pins.length === 1 && pins.pins[0].slide === 'title' && pins.pins[0].x > 0 && pins.pins[0].x < 100 && pins.pins[0].by === 'you', JSON.stringify(pins));
208
+ await page.click('[data-dmode="hist"]');
209
+ ok('the Edit log lists the pinned note', !!(await until(async () => /Note pinned on the slide/.test(await text('.sheet')))) && /40 minutes/.test(await text('.sheet')));
210
+ await page.click('[data-dmode="read"]');
211
+ await until(() => page.$('[data-rd-big] iframe'));
212
+
213
+ /* Present */
214
+ await page.click('[data-rd-act="present"]');
215
+ await page.waitForSelector('[data-rd-ov]');
216
+ ok('Present opens the presenter view with the note of the slide', /1 \/ 4/.test(await text('[data-rd-pn]')) && /Open with the release/.test(await text('[data-rd-ovnote]')));
217
+ await page.keyboard.press('ArrowRight');
218
+ ok('ArrowRight goes to the next slide', !!(await until(async () => /2 \/ 4/.test(await text('[data-rd-pn]')))));
219
+ await page.keyboard.press('Escape');
220
+ ok('Escape closes the view and leaves the document open', !!(await until(async () => !(await page.$('[data-rd-ov]')))) && !!(await page.$('[data-rd-big]')));
221
+
222
+ await sleep(400);
223
+ ok('no page error and no failed request', errors.length === 0, errors.slice(0, 6).join(' | '));
224
+ return finish();
225
+ })().catch((e) => { console.error(e); finish(1); });