1212 * receipt. Its only output is stdout.
1313 */
1414
15- import { readFileSync } from 'node:fs' ;
15+ import { appendFileSync , readFileSync } from 'node:fs' ;
1616import { basename , join , resolve } from 'pathe' ;
1717import { box , runBoxes } from '@async/witness' ;
1818import { 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 ( / o b s e r v e d ( \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