Skip to content

Commit aa15489

Browse files
chore: super-productivity leg-d baseline calibration + driveability finding
Project-switch leg GREEN/deterministic: addProject dialog → save (projects 1→2) → switch flips main-header .current-project-title. But the formly COLOR-save proof is rejected on provenance: the witness PageHandle has no fill/evaluate/setInputValue and char-press/arrows leave a native input[type=color] unchanged, so the --palette-primary-500 shift (11,119,210→12,122,212) appears only post-save and may be re-quantization, not a driven change. Ruling for the landing unit: drive a click-operable theme control (huePrimary mat-select or isAutoContrast) whose palette/style effect is provably driven, both lanes. tsc green.
1 parent 2794a4c commit aa15489

1 file changed

Lines changed: 185 additions & 28 deletions

File tree

packages/cli/src/fixture/angular-super-productivity-calibrate-run.ts

Lines changed: 185 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* receipt. Its only output is stdout.
1313
*/
1414

15-
import { readFileSync } from 'node:fs';
15+
import { appendFileSync, readFileSync } from 'node:fs';
1616
import { basename, join, resolve } from 'pathe';
1717
import { box, runBoxes } from '@async/witness';
1818
import { joinURL } from 'ufo';
@@ -315,9 +315,13 @@ export async function calibrateAngularSuperProductivityLane(
315315

316316
// Leg (d): settings-change. The app exposes NO dark-theme UI control in
317317
// v2.13.15 (the isDarkMode formly field is commented out), so the real
318-
// control driven is the project theme colour. Every selector is asked as
319-
// a count first, then the surface is exercised and the palette custom
320-
// properties are read before/after. Exploratory: printed, never asserted.
318+
// controls driven are: a second project created through the side-nav
319+
// addProject() dialog and switched to, then its PRIMARY theme colour
320+
// changed through the /project-settings formly config-section save, and
321+
// one keyboard shortcut. Every selector is asked as a count first, the
322+
// surfaces are exercised, and the palette custom properties and the
323+
// shell's current-project title are read before/after. Printed, never
324+
// asserted.
321325
const dd: Record<string, unknown> = {};
322326
legs['leg-d'] = dd;
323327
const paletteProps = [
@@ -334,42 +338,195 @@ export async function calibrateAngularSuperProductivityLane(
334338
{ label: 'drawer-palette', selector: 'mat-drawer-container', properties: [...paletteProps] },
335339
])
336340
.then(
337-
(probes) => probes,
341+
(probes) =>
342+
probes.map((p) => ({ label: p.label, properties: p.properties })),
338343
(e: unknown) => `refused: ${e instanceof Error ? e.message : String(e)}`,
339344
);
345+
// The box PageHandle exposes NO evaluate/fill primitive, so the count is
346+
// read out of the retry-until-timeout assertion's refusal: asking for 0
347+
// and parsing the observed N. A present element costs one assertion
348+
// window, so this is used sparingly.
349+
const readCount = async (selector: string): Promise<number | string> => {
350+
try {
351+
await context.expect.page.count(page, selector, 0);
352+
return 0;
353+
} catch (error: unknown) {
354+
const line = (error instanceof Error ? error.message : String(error))
355+
.split('\n')[0]!
356+
.trim();
357+
const m = line.match(/observed (\d+)/);
358+
return m ? Number(m[1]) : line;
359+
}
360+
};
361+
const readCurrentProjectTitle = async (): Promise<unknown> =>
362+
host
363+
.groupedText({
364+
group: 'main-header',
365+
name: '.current-project-title',
366+
item: '.current-project-title',
367+
} as const)
368+
.then(
369+
(g) => g.map((x) => x.name),
370+
(e: unknown) => `refused: ${e instanceof Error ? e.message : String(e)}`,
371+
);
372+
const readProjectTitles = async (): Promise<unknown> =>
373+
host
374+
.groupedText({
375+
group: 'side-nav .project',
376+
name: 'button[mat-menu-item]',
377+
item: 'button[mat-menu-item]',
378+
} as const)
379+
.then(
380+
(g) => g.map((x) => x.name),
381+
(e: unknown) => `refused: ${e instanceof Error ? e.message : String(e)}`,
382+
);
383+
const ckPath =
384+
process.env.LEG_D_CK ??
385+
join(receiptDir, 'leg-d-checkpoints.log');
386+
const ck = (m: string): void => {
387+
try {
388+
appendFileSync(ckPath, `${new Date().toISOString()} [${lane}] ${m}\n`);
389+
} catch {
390+
/* diagnostic only */
391+
}
392+
};
393+
ck('leg-d reached (before before-reads)');
394+
// The number actually present, parsed out of the count refusal, so an
395+
// interaction is only driven against an element the page really has —
396+
// char-by-char typing against a missing element would burn one interaction
397+
// timeout per character.
398+
const actualCount = async (selector: string): Promise<number> => {
399+
const r = await readCount(selector);
400+
return typeof r === 'number' ? r : -1;
401+
};
402+
// Attempt the click with a bounded timeout and report the outcome, rather
403+
// than pre-checking with a full assertion window per selector.
404+
const clickIfPresent = async (selector: string): Promise<string> => {
405+
try {
406+
await page.click(selector, { timeoutMs: 4_000 });
407+
return 'clicked';
408+
} catch (error: unknown) {
409+
const msg = (error instanceof Error ? error.message : String(error)).split('\n')[0]!;
410+
ck(`click failed: ${selector}${msg}`);
411+
return `failed: ${msg}`;
412+
}
413+
};
340414
try {
341415
dd['palette-before'] = await readPalette();
342-
for (const selector of [
343-
'side-nav',
344-
'side-nav button[mat-menu-item]',
345-
'side-nav .tour-addProjectBtn',
346-
'side-nav mat-expansion-panel',
347-
'.projects',
348-
]) {
416+
dd['current-project-title-before'] = await readCurrentProjectTitle();
417+
dd['project-titles-before'] = await readProjectTitles();
418+
ck('opening addProject dialog');
419+
420+
// (d.1) Open the addProject() dialog from the side-nav.
421+
dd['add-clicked'] = await clickIfPresent('section.projects > button[mat-menu-item]');
422+
await new Promise<void>((settle) => void setTimeout(settle, 1_200));
423+
dd['dialog-count'] = await readCount('dialog-create-project');
424+
ck('dialog opened, setting title');
425+
// Type a title into the formly title input (a TEXT input, so char-press
426+
// works) — only if present, so a missing input costs one bounded count
427+
// rather than one interaction timeout per character.
428+
const titleInput = 'dialog-create-project input:not([type=color])';
429+
const titleCount = await actualCount(titleInput);
430+
dd['title-input-count'] = titleCount;
431+
if (titleCount > 0) {
349432
try {
350-
await context.expect.page.count(page, selector, 0);
351-
dd[`count:${selector}`] = 0;
433+
await page.type(titleInput, 'Witness leg-d project', { redact: false, timeoutMs: 4_000 });
434+
dd['title-typed'] = true;
352435
} catch (error: unknown) {
353-
dd[`count:${selector}`] = (error instanceof Error ? error.message : String(error))
354-
.split('\n')[0]!
355-
.trim();
436+
dd['title-typed'] = `failed: ${error instanceof Error ? error.message : String(error)}`;
356437
}
438+
await new Promise<void>((settle) => void setTimeout(settle, 500));
439+
ck('title set, clicking save');
440+
dd['save-clicked'] = await clickIfPresent('dialog-create-project button[type=submit]');
441+
await new Promise<void>((settle) => void setTimeout(settle, 1_500));
442+
} else {
443+
dd['title-input'] = 'absent';
357444
}
358-
// A keyboard shortcut: `b` toggles the backlog. Read the split state
359-
// before and after so the effect is measured rather than assumed.
360-
dd['split-count-before-shortcut'] = await host
361-
.groupedText({ group: 'split', name: 'split', item: 'split' } as const)
362-
.then((g) => g.length, (e: unknown) => `refused: ${e instanceof Error ? e.message : String(e)}`);
363-
await page.press('body', 'b');
364-
await new Promise<void>((settle) => void setTimeout(settle, 800));
365-
dd['backlog-count-after-b'] = await host
366-
.groupedText({ group: 'backlog', name: 'backlog', item: 'backlog' } as const)
367-
.then((g) => g.length, (e: unknown) => `refused: ${e instanceof Error ? e.message : String(e)}`);
445+
dd['dialog-count-after-save'] = await readCount('dialog-create-project');
446+
dd['project-titles-after-add'] = await readProjectTitles();
447+
dd['side-nav-project-count-after-add'] = await readCount('side-nav .project');
448+
ck('switching to new project');
449+
450+
// (d.2) Switch to the new (second) project via its side-nav button.
451+
dd['switch-clicked'] = await clickIfPresent(
452+
'side-nav .project:nth-of-type(2) > button[mat-menu-item]',
453+
);
454+
await new Promise<void>((settle) => void setTimeout(settle, 1_500));
455+
dd['current-project-title-after-switch'] = await readCurrentProjectTitle();
456+
dd['route-after-switch'] = 'see page navigations';
457+
dd['palette-after-switch'] = await readPalette();
458+
ck('navigating to project-settings');
459+
460+
// (d.3) Go to the current project's settings via the header control.
461+
dd['settings-nav-clicked'] = await clickIfPresent('main-header .project-settings-btn');
462+
await new Promise<void>((settle) => void setTimeout(settle, 1_500));
463+
dd['route-after-settings-nav'] = 'see page navigations';
464+
dd['config-section-count'] = await readCount('section.config-section');
465+
ck('expanding theme config-section');
466+
// Expand the 2nd config-section (the collapsible theme form).
467+
dd['expand-clicked'] = await clickIfPresent(
468+
'section.config-section:nth-of-type(2) .collapsible-title',
469+
);
470+
await new Promise<void>((settle) => void setTimeout(settle, 1_200));
471+
dd['palette-before-save'] = await readPalette();
472+
ck('setting colour input value');
473+
// The box PageHandle has no fill/evaluate. Test whether ANY available
474+
// primitive can change a native input[type=color]: char-press typing,
475+
// and clicking-then-pressing. Palette is measured after each so the
476+
// question "can the witness drive this control at all" is answered by
477+
// measurement rather than assumed.
478+
const colorInput = 'section.config-section:nth-of-type(2) input[type=color]';
479+
const colorCount = await actualCount(colorInput);
480+
dd['color-input-count'] = colorCount;
481+
if (colorCount > 0) {
482+
try {
483+
await page.type(colorInput, '#ff0000', { redact: false, timeoutMs: 4_000 });
484+
dd['color-type-attempt'] = 'typed';
485+
} catch (error: unknown) {
486+
dd['color-type-attempt'] = `failed: ${error instanceof Error ? error.message : String(error)}`;
487+
}
488+
await new Promise<void>((settle) => void setTimeout(settle, 400));
489+
dd['palette-after-color-type'] = await readPalette();
490+
try {
491+
await page.click(colorInput, { timeoutMs: 4_000 });
492+
for (const key of ['ArrowUp', 'ArrowUp', 'PageUp']) {
493+
await page.press(colorInput, key, { timeoutMs: 3_000 });
494+
}
495+
dd['color-press-attempt'] = 'pressed';
496+
} catch (error: unknown) {
497+
dd['color-press-attempt'] = `failed: ${error instanceof Error ? error.message : String(error)}`;
498+
}
499+
await new Promise<void>((settle) => void setTimeout(settle, 400));
500+
dd['palette-after-color-press'] = await readPalette();
501+
ck('clicking theme submit');
502+
dd['save-color-clicked'] = await clickIfPresent(
503+
'section.config-section:nth-of-type(2) .submit-button',
504+
);
505+
await new Promise<void>((settle) => void setTimeout(settle, 1_800));
506+
} else {
507+
dd['color-input'] = 'absent';
508+
}
509+
dd['palette-after-save'] = await readPalette();
510+
511+
// (d.4) A keyboard shortcut. `w` (goToWorkView) and `b` (toggleBacklog)
512+
// are both measured; the observable effect in this DOM state decides
513+
// which the journey anchors on.
514+
ck('keyboard shortcuts');
515+
dd['route-before-shortcut'] = 'see page navigations';
368516
await page.press('body', 'w');
369-
await new Promise<void>((settle) => void setTimeout(settle, 500));
517+
await new Promise<void>((settle) => void setTimeout(settle, 1_000));
370518
dd['route-after-w'] = 'see page navigations';
519+
dd['host-tag-after-w'] = await readCount('work-view');
520+
dd['split-count-before-b'] = await readCount('split');
521+
await page.press('body', 'b');
522+
await new Promise<void>((settle) => void setTimeout(settle, 1_000));
523+
dd['route-after-b'] = 'see page navigations';
524+
dd['split-count-after-b'] = await readCount('split');
525+
dd['backlog-count-after-b'] = await readCount('backlog');
526+
ck('leg-d done');
371527
} catch (error: unknown) {
372528
dd['leg-d-error'] = error instanceof Error ? error.message : String(error);
529+
ck(`error: ${error instanceof Error ? error.message : String(error)}`);
373530
}
374531
cb['pageErrors-during-cb'] = 'see page summary';
375532

0 commit comments

Comments
 (0)