create-pmos 0.240.0 → 0.241.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-report-structure-real-test.js +211 -0
- package/workspace/c3-report-structure-test.js +382 -0
- package/workspace/guide/index.md +2 -0
- package/workspace/guide/report-design.md +4 -0
- package/workspace/guide/report-structure.md +91 -0
- package/workspace/guide/reports-dashboards.md +1 -1
- package/workspace/guide/reports-set-up.md +2 -0
- package/workspace/lib/charts-store.js +124 -0
- package/workspace/lib/headless.js +17 -3
- package/workspace/lib/needs-providers.js +1 -0
- package/workspace/lib/report-structure-needs.js +64 -0
- package/workspace/lib/report-structure.js +515 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-report-setups.js +11 -6
- package/workspace/lib/routes-report-structure.js +619 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-rd.js +4 -0
- package/workspace/public/pm/c3-design-studio.js +7 -3
- package/workspace/public/pm/c3-report-setup.js +3 -0
- package/workspace/public/pm/c3-report-structure.css +40 -0
- package/workspace/public/pm/c3-report-structure.js +455 -0
- package/workspace/public/pm/c3-reports.css +2 -0
- package/workspace/public/pm/c3-reports.js +22 -3
- package/workspace/public/pm/c3-shell.js +2 -0
- package/workspace/server.js +38 -9
- package/workspace/skills/report-source-check.skill +45 -0
- package/workspace/skills/report-structure-dry.skill +51 -0
- package/workspace/skills/report-structure.skill +61 -0
- package/workspace/skills/report-template.skill +10 -0
- package/workspace/test-lists/report-structure-backend.json +5 -0
- package/workspace/test-lists/report-structure-ui.json +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.241.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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T14:33:36Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T14:33:36Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-14T14:33:36Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/* c3-report-structure-real-test.js — the Structure step of the report studio against the REAL backend (initiative report-structure, PRD done-criterion 10).
|
|
4
|
+
* Not demo mode and no route interception: a real server on a scratch repository, with a stub `claude` that plays the report-structure and
|
|
5
|
+
* report-structure-dry jobs (as report-structure-server.test.js does) and a stub for the source check (PMOS_REPORT_CHECK_STUB), and a real
|
|
6
|
+
* Chromium on /c3.html. It proves the page and the server agree on the API contract end to end.
|
|
7
|
+
*
|
|
8
|
+
* setup a work-sourced report set up through the API
|
|
9
|
+
* turn the Structure step opens from the set-up row (Shape the report); a purpose and one turn land S1 with sources, charts and outline;
|
|
10
|
+
* a second turn lands S2; the strip lists both; going back to S1 works
|
|
11
|
+
* dry Try it with real data shows the headline and the values
|
|
12
|
+
* source a Chrome source is added with the check stubbed to pass: the check view shows its steps
|
|
13
|
+
* accept Use this structure: the setup sheet shows the saved structure, brief.md is on disk, charts.jsonl holds the new charts
|
|
14
|
+
* charts the Charts tab lists a data chart with its source and Used in naming the report
|
|
15
|
+
* No page error and no failed request may be raised. Synthetic repositories only; the stub is the only "model"; nothing reaches the network. */
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { spawn } = require('child_process');
|
|
19
|
+
const { findBrowser } = require('./lib/find-browser');
|
|
20
|
+
const T = require('./test-run').configure({ name: 'c3-report-structure-real' });
|
|
21
|
+
const { makeRepos, codeRepo, write, git } = require('./plan-test-repos');
|
|
22
|
+
const RS = require('./lib/report-setups.js');
|
|
23
|
+
const S = require('./lib/report-structure.js');
|
|
24
|
+
const CS = require('./lib/charts-store.js');
|
|
25
|
+
|
|
26
|
+
const BASE = T.BASE;
|
|
27
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
28
|
+
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); } };
|
|
29
|
+
const results = [];
|
|
30
|
+
const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + String(extra).slice(0, 600) : ''}`); };
|
|
31
|
+
|
|
32
|
+
/* ── the scratch repository, the stub `claude` and the check stub ── */
|
|
33
|
+
const R = makeRepos('c3-report-structure-real');
|
|
34
|
+
const team = codeRepo(path.join(R.base, 'team'));
|
|
35
|
+
fs.mkdirSync(path.join(team, 'pmos', 'state'), { recursive: true });
|
|
36
|
+
write(team, 'pmos/state/.keep', '');
|
|
37
|
+
git(team, ['add', '-A']); git(team, ['commit', '-q', '-m', 'state']);
|
|
38
|
+
const fakeBin = path.join(R.base, 'bin'); fs.mkdirSync(fakeBin, { recursive: true });
|
|
39
|
+
const fakeHome = path.join(R.base, 'home'); fs.mkdirSync(path.join(fakeHome, '.claude'), { recursive: true });
|
|
40
|
+
const stubFile = path.join(R.base, 'check-stub.json');
|
|
41
|
+
fs.writeFileSync(stubFile, JSON.stringify({ default: 'pass' }));
|
|
42
|
+
/* The stub plays the structure runs: it reads the job block, writes the plan (or the dry result) to the output path and prints the JSON result.
|
|
43
|
+
* The first turn builds the plan with its questions; a turn with "refine" in it adds the retention chart; a later turn builds on the previous file. */
|
|
44
|
+
fs.writeFileSync(path.join(fakeBin, 'claude'), `#!/usr/bin/env node
|
|
45
|
+
const fs = require('fs');
|
|
46
|
+
let input = ''; process.stdin.on('data', (d) => { input += d; }); process.stdin.on('end', () => {
|
|
47
|
+
const line = (k) => { const m = new RegExp('^- ' + k + ': (.*)$', 'm').exec(input); return m ? m[1].trim() : null; };
|
|
48
|
+
const mode = line('Mode'); const out = line('Output path'); const turn = line('Turn') || '';
|
|
49
|
+
const done = (o) => { process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', is_error: false, result: typeof o === 'string' ? o : JSON.stringify(o), num_turns: 2, duration_ms: 500, permission_denials: [] })); process.exit(0); };
|
|
50
|
+
if (!mode || !out) return done('Drafted.');
|
|
51
|
+
const base = () => ({
|
|
52
|
+
purpose: { readers: 'Leadership', decision: 'Where to invest next quarter', scope: 'The app', cadence: 'monthly', questions: ['Are we growing?'] },
|
|
53
|
+
sources: [{ id: 'ga', kind: 'connector', name: 'Google Analytics', via: 'Supermetrics', ref: '', use: 'Users and events', state: 'reached' }, { id: 'board', kind: 'pmos-level', name: 'Board: MySPH', ref: 'mysph', use: 'Releases', state: 'reached' }],
|
|
54
|
+
charts: [{ id: 'users', chart: null, name: 'New users', source: 'ga', from: 'GA4 newUsers', how: 'Month total', compare: 'Previous month', flag: 'Down 10%', question: 'Are we growing?', query: 'newUsers', why: 'Top of funnel' },
|
|
55
|
+
{ id: 'releases', chart: null, name: 'Releases', source: 'board', from: 'PMOS releases', how: 'Count in the month', compare: '', flag: '', question: 'What changed?', query: '', why: '' }],
|
|
56
|
+
outline: [{ id: 'headline', title: 'Headline', fixed: true, charts: [], notes: '', show: 'Text' }, { id: 'funnel', title: 'The funnel', charts: ['users'], notes: '', show: 'Chart' },
|
|
57
|
+
{ id: 'changes', title: 'What changed', charts: ['releases'], notes: '', show: 'List' }, { id: 'needs', title: 'Needs your call', fixed: true, charts: [], notes: '', show: 'List' }, { id: 'sources', title: 'Sources', fixed: true, charts: [], notes: '', show: 'List' }],
|
|
58
|
+
options: [{ name: 'Funnel first', sections: ['The funnel'], good: 'Shows drops', misses: 'Revenue', recommended: true }, { name: 'By channel', sections: ['Channels'], good: 'Spend', misses: 'Retention' }, { name: 'By release', sections: ['Releases'], good: 'Cause', misses: 'Money' }],
|
|
59
|
+
});
|
|
60
|
+
if (mode === 'dry') {
|
|
61
|
+
const m = /Version to try: S(\\d+) \\((.*)\\)/.exec(input);
|
|
62
|
+
const plan = JSON.parse(fs.readFileSync(m[2], 'utf8'));
|
|
63
|
+
fs.writeFileSync(out, JSON.stringify({ n: Number(m[1]), period: '2026-09', headline: 'New users fell 9%.', values: plan.charts.slice(0, 1).map((c) => ({ chart: c.id, value: '48,210', was: '52,930', delta: '-8.9%', flag: true, note: 'Paid paused' })),
|
|
64
|
+
missing: plan.sources.filter((s) => s.kind === 'chrome').map((s) => ({ source: s.id, reason: 'Needs Chrome' })), options: ['Fund retention'] }));
|
|
65
|
+
return done({ ok: true, note: '1 chart returned a value' });
|
|
66
|
+
}
|
|
67
|
+
const prev = line('Previous version');
|
|
68
|
+
const plan = prev && prev.startsWith('/') ? JSON.parse(fs.readFileSync(prev, 'utf8')) : base();
|
|
69
|
+
if (/refine/.test(turn)) {
|
|
70
|
+
plan.charts.push({ id: 'ret', chart: null, name: 'D30 retention', source: 'ga', from: 'GA4 cohorts', how: 'Share active on day 30', compare: 'Last 3 cohorts', flag: 'Below 25%', question: 'Do they stay?', query: 'cohortActiveUsers', why: 'You asked' });
|
|
71
|
+
plan.outline.find((o) => o.id === 'funnel').charts.push('ret');
|
|
72
|
+
}
|
|
73
|
+
fs.writeFileSync(out, JSON.stringify(plan));
|
|
74
|
+
const res = { answer: /refine/.test(turn) ? 'S2: D30 retention added.' : 'Built the first version from your answers.' };
|
|
75
|
+
if (!prev || !prev.startsWith('/')) { res.questions = [{ q: 'Who reads it?', chips: ['Leadership', 'Product team', 'Both'] }]; res.options = plan.options; }
|
|
76
|
+
setTimeout(() => done(res), 1500);
|
|
77
|
+
});
|
|
78
|
+
`, { mode: 0o755 });
|
|
79
|
+
|
|
80
|
+
let srv = null; let browser = null;
|
|
81
|
+
const finish = async (code) => {
|
|
82
|
+
if (browser) await browser.close().catch(() => {});
|
|
83
|
+
if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
|
|
84
|
+
T.killTmux();
|
|
85
|
+
R.cleanup();
|
|
86
|
+
const bad = results.filter((x) => !x.cond).length;
|
|
87
|
+
console.log(`\n${results.length - bad}/${results.length} c3-report-structure-real assertions passed`);
|
|
88
|
+
process.exit(code !== undefined ? code : (bad ? 1 : 0));
|
|
89
|
+
};
|
|
90
|
+
process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
91
|
+
|
|
92
|
+
(async () => {
|
|
93
|
+
const exe = findBrowser();
|
|
94
|
+
if (!exe) { console.log('✗ no Chrome/Chromium on this machine, c3-report-structure-real cannot run'); return finish(1); }
|
|
95
|
+
let chromium;
|
|
96
|
+
try { ({ chromium } = require('playwright-core')); } catch { console.log('✗ playwright-core is not installed, c3-report-structure-real cannot run'); return finish(1); }
|
|
97
|
+
|
|
98
|
+
srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore',
|
|
99
|
+
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', PMOS_CORE_DOCS: 'off', HOME: fakeHome,
|
|
100
|
+
PMOS_REPORT_CHECK_STUB: stubFile, PMOS_REPORT_CHECK_STUB_MS: '120', PMOS_REPORT_CHECK_PLATFORM: 'darwin', PATH: fakeBin + path.delimiter + process.env.PATH }) });
|
|
101
|
+
await T.waitForServer(BASE);
|
|
102
|
+
const press = await T.pressHeaders(BASE);
|
|
103
|
+
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 }; };
|
|
104
|
+
|
|
105
|
+
/* ── setup: a work-sourced report, through the API ── */
|
|
106
|
+
const ws = (await call('POST', '/api/workspaces', { root: team, focus: true })).body.workspace.id;
|
|
107
|
+
const proposal = { name: 'MySPH app performance', purpose: 'Monthly product performance for leadership.', audience: 'Leadership', source: 'work', output: 'google-slides',
|
|
108
|
+
schedule: { cadence: 'monthly', dayOfMonth: 3, time: '09:00' }, sources: ['The local ledgers'], sections: RS.FRAME.slice(), learned_from: 'The June copy.', fixes: [] };
|
|
109
|
+
const made = await call('POST', '/api/report-setups/setup', { ws, proposal });
|
|
110
|
+
const id = made.body && made.body.setup && made.body.setup.id;
|
|
111
|
+
ok('setup: a work-sourced report is set up through the API', !!ws && !!id, made.raw);
|
|
112
|
+
if (!id) return finish(1);
|
|
113
|
+
const P = S.paths(team, id);
|
|
114
|
+
|
|
115
|
+
browser = await chromium.launch({ executablePath: exe });
|
|
116
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
|
117
|
+
const page = await ctx.newPage();
|
|
118
|
+
const errors = [];
|
|
119
|
+
page.on('pageerror', (e) => errors.push('pageerror: ' + String((e && e.message) || e)));
|
|
120
|
+
page.on('requestfailed', (r) => { const f = r.failure(); if (!/ERR_ABORTED/.test((f && f.errorText) || '')) errors.push('failed ' + r.url() + ' ' + ((f && f.errorText) || '')); });
|
|
121
|
+
page.on('response', (r) => { if (r.status() >= 400) errors.push('http ' + r.status() + ' ' + r.request().method() + ' ' + r.url()); });
|
|
122
|
+
page.on('dialog', (d) => d.dismiss().catch(() => {}));
|
|
123
|
+
const text = (sel) => page.textContent(sel).catch(() => '');
|
|
124
|
+
const idle = () => page.waitForFunction(() => { const b = document.querySelector('[data-st-act="send"]'); return b && !b.disabled && !document.querySelector('[data-st-running]'); }, null, { timeout: 20000 });
|
|
125
|
+
const verOn = async (n) => await page.getAttribute('[data-st-v="' + n + '"]', 'aria-pressed') === 'true';
|
|
126
|
+
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 }); };
|
|
127
|
+
|
|
128
|
+
/* ── the Structure step from the set-up row ── */
|
|
129
|
+
await goReports();
|
|
130
|
+
ok('the set-up row has Shape the report', !!(await page.$('[data-rs-reg="' + id + '"] [data-rs-act="shape"]')));
|
|
131
|
+
await page.click('[data-rs-reg="' + id + '"] [data-rs-act="shape"]');
|
|
132
|
+
await page.waitForSelector('[data-st-studio]');
|
|
133
|
+
ok('the Structure step opens with nothing saved yet and the Use button waiting', /Not saved yet/.test(await text('[data-st-studio]')) && await page.$eval('[data-st-act="save"]', (b) => b.disabled));
|
|
134
|
+
|
|
135
|
+
/* ── a purpose and the first turn: S1 ── */
|
|
136
|
+
await page.fill('[data-st-in]', 'Monthly product performance for leadership, numbers from Google Analytics.');
|
|
137
|
+
await page.click('[data-st-act="send"]');
|
|
138
|
+
ok('while the job runs the conversation says PMOS is working', !!(await until(() => page.$('[data-st-running]'), 8000)));
|
|
139
|
+
await page.waitForSelector('[data-st-v="1"]', { timeout: 30000 });
|
|
140
|
+
ok('the turn lands S1 as the current version', await verOn(1));
|
|
141
|
+
await page.waitForSelector('[data-st-purpose]');
|
|
142
|
+
ok('the plan shows what it answers', /Where to invest next quarter/.test(await text('[data-st-purpose]')) && /Are we growing/.test(await text('[data-st-purpose]')));
|
|
143
|
+
ok('the plan lists the sources, the charts and the outline', (await page.$$('[data-st-src]')).length === 2 && !!(await page.$('[data-st-chart="users"]')) && !!(await page.$('[data-st-chart="releases"]'))
|
|
144
|
+
&& JSON.stringify(await page.$$eval('[data-st-section]', (a) => a.map((x) => x.getAttribute('data-st-section')))) === '["headline","funnel","changes","needs","sources"]');
|
|
145
|
+
ok('PMOS asks its question with answer chips', (await page.$$('[data-st-question] [data-st-act="chip"]')).length === 3);
|
|
146
|
+
await idle();
|
|
147
|
+
|
|
148
|
+
/* ── a second turn: S2; the strip; back to S1 ── */
|
|
149
|
+
await page.fill('[data-st-in]', 'Please refine: add D30 retention.');
|
|
150
|
+
await page.click('[data-st-act="send"]');
|
|
151
|
+
await page.waitForSelector('[data-st-v="2"]', { timeout: 30000 });
|
|
152
|
+
ok('the second turn lands S2 and the strip lists both', (await page.$$('[data-st-v]')).length === 2 && await verOn(2));
|
|
153
|
+
await page.waitForSelector('[data-st-chart="ret"]');
|
|
154
|
+
ok('S2 has the retention chart', !!(await page.$('[data-st-chart="ret"]')));
|
|
155
|
+
await idle();
|
|
156
|
+
await page.click('[data-st-v="1"]');
|
|
157
|
+
ok('S1 shows again without the retention chart', !!(await until(async () => await verOn(1) && !(await page.$('[data-st-chart="ret"]')))));
|
|
158
|
+
await page.click('[data-st-act="revert"]');
|
|
159
|
+
ok('Go back to S1 makes S1 current on the server', !!(await until(async () => (await call('GET', `/api/report-structure?ws=${ws}&id=${id}`)).body.structure.current === 1)));
|
|
160
|
+
await until(async () => !(await page.$('[data-st-act="revert"]')));
|
|
161
|
+
|
|
162
|
+
/* ── a Chrome source, the check stubbed to pass ── */
|
|
163
|
+
await page.click('[data-st-act="panel"][aria-expanded]');
|
|
164
|
+
await page.waitForSelector('[data-st-panel]');
|
|
165
|
+
await page.click('[data-st-panel] [data-st-act="opt"][data-v="chrome"]');
|
|
166
|
+
await page.waitForSelector('[data-st-form="chrome"]');
|
|
167
|
+
await page.fill('[data-st-form="chrome"] [data-st-field="name"]', 'Looker Studio: Paid acquisition');
|
|
168
|
+
await page.fill('[data-st-form="chrome"] [data-st-field="ref"]', 'https://lookerstudio.google.com/reporting/abc');
|
|
169
|
+
await page.fill('[data-st-form="chrome"] [data-st-field="use"]', 'Spend by campaign').catch(() => {});
|
|
170
|
+
await page.click('[data-st-form="chrome"] [data-st-act="add"]');
|
|
171
|
+
await page.waitForSelector('[data-st-v="3"]', { timeout: 15000 });
|
|
172
|
+
await page.waitForSelector('[data-st-check]', { timeout: 15000 });
|
|
173
|
+
ok('the Chrome source makes S3 and the check view shows its five steps', (await page.$$('[data-st-check] [data-st-step]')).length === 5, await text('[data-st-check]'));
|
|
174
|
+
ok('the check ends with every step Works and the source Reached', !!(await until(async () => /All steps work/.test(await text('[data-st-check-state]')) && (await page.$$eval('[data-st-check] [data-st-step-state="works"]', (a) => a.length)) === 5, 20000))
|
|
175
|
+
&& !!(await until(async () => /Reached/.test(await text('[data-st-src="looker-studio-paid-acquisition"] [data-st-state]')))));
|
|
176
|
+
|
|
177
|
+
/* ── Try it with real data ── */
|
|
178
|
+
await page.click('[data-st-view="dry"]');
|
|
179
|
+
await page.waitForSelector('[data-st-nodry]');
|
|
180
|
+
await page.click('[data-st-act="dryrun"]');
|
|
181
|
+
await page.waitForSelector('[data-st-dry-headline]', { timeout: 30000 });
|
|
182
|
+
ok('the dry-run view shows the headline', /New users fell 9%/.test(await text('[data-st-dry-headline]')));
|
|
183
|
+
ok('the dry-run view shows the values with their comparison', /48,210/.test(await text('[data-st-dry-values]')) && /52,930/.test(await text('[data-st-dry-values]')) && /Flagged/.test(await text('[data-st-dry-values]')));
|
|
184
|
+
ok('what it could not get names the Chrome source', /Looker Studio/.test(await text('[data-st-dry-missing]')) && /Needs Chrome/.test(await text('[data-st-dry-missing]')));
|
|
185
|
+
await page.click('[data-st-view="plan"]');
|
|
186
|
+
|
|
187
|
+
/* ── Use this structure ── */
|
|
188
|
+
await idle();
|
|
189
|
+
await page.click('[data-st-act="save"]');
|
|
190
|
+
await page.waitForSelector('[data-rd-brief]', { timeout: 15000 });
|
|
191
|
+
await page.waitForSelector('[data-st-saved-wrap]');
|
|
192
|
+
ok('the setup sheet shows the saved structure', /Structure S3/.test(await text('[data-st-saved-tag]')) && /Where to invest next quarter/.test(await text('[data-st-answers]')) && /New users/.test(await text('[data-st-brief]')) && /Looker Studio/.test(await text('[data-st-brief]')));
|
|
193
|
+
ok('brief.md exists on disk', fs.existsSync(P.brief) && /type: Reference/.test(fs.readFileSync(P.brief, 'utf8')));
|
|
194
|
+
const lines = CS.readEvents(team);
|
|
195
|
+
ok('charts.jsonl has the new charts (New users, Releases)', lines.length === 2 && lines.every((l) => l.ev === 'add') && lines.map((l) => l.chart.name).sort().join() === 'New users,Releases', JSON.stringify(lines));
|
|
196
|
+
const saved = (await call('GET', `/api/report-structure?ws=${ws}&id=${id}`)).body;
|
|
197
|
+
ok('the server holds S3 as saved', saved.structure.saved === 3 && saved.structure.current === 3);
|
|
198
|
+
await page.keyboard.press('Escape'); await sleep(250);
|
|
199
|
+
|
|
200
|
+
/* ── the Charts tab ── */
|
|
201
|
+
await goReports();
|
|
202
|
+
await page.click('[data-rp3-tab="charts"]');
|
|
203
|
+
const newUsersId = (lines.find((l) => l.chart.name === 'New users') || {}).id;
|
|
204
|
+
await page.waitForSelector('[data-rp3-data-chart="' + newUsersId + '"]', { timeout: 15000 });
|
|
205
|
+
ok('the Charts tab lists a data chart beside the built-in ones', !!(await page.$('[data-rp3-lib] [data-home="north"]')) && /Data chart/.test(await text('[data-rp3-data-chart="' + newUsersId + '"]')));
|
|
206
|
+
ok('the data chart shows its source and Used in naming the report', /Google Analytics/.test(await text('[data-rp3-source="' + newUsersId + '"]')) && /Used in: .*MySPH app performance/.test(await text('[data-rp3-data-chart="' + newUsersId + '"] [data-rp3-used]')), await text('[data-rp3-data-chart="' + newUsersId + '"]'));
|
|
207
|
+
|
|
208
|
+
await sleep(400);
|
|
209
|
+
ok('no page error and no failed request', errors.length === 0, errors.slice(0, 6).join(' | '));
|
|
210
|
+
return finish();
|
|
211
|
+
})().catch((e) => { console.error(e); finish(1); });
|