@@ -34,7 +34,7 @@ import { existsSync, mkdirSync } from 'node:fs';
3434import { dirname , join } from 'node:path' ;
3535
3636import { resolveStackCollection } from '@objectstack/core' ;
37- import { expandViewContainer } from '@objectstack/spec/ui' ;
37+ import { deriveViewContainerObject , expandViewContainer } from '@objectstack/spec/ui' ;
3838
3939import type { LoadedScenario } from '../loader.js' ;
4040import { scenarioNamespace } from '../schema.js' ;
@@ -81,22 +81,23 @@ export function taggedViewKinds(card: Pick<LoadedScenario['card'], 'capabilities
8181
8282const nameOf = ( v : unknown ) : string | undefined => ( typeof v === 'object' && v !== null && typeof ( v as { name ?: unknown } ) . name === 'string' ? ( v as { name : string } ) . name : undefined ) ;
8383
84- /** Every list view the build declares: `{ object, name: '<object>.<key>', type }`, in declaration order. */
84+ /** Every list view the build declares: `{ object, name: '<object>.<key>', type }`, in declaration order, containers expanded by the spec's own composer . */
8585export function declaredListViews ( artifact : unknown ) : Array < { object : string ; name : string ; type : string } > {
8686 const out : Array < { object : string ; name : string ; type : string } > = [ ] ;
8787 for ( const raw of resolveStackCollection ( artifact , 'views' ) ) {
8888 if ( typeof raw !== 'object' || raw === null ) continue ;
8989 const item = raw as Record < string , unknown > ;
90- const object = typeof item . object === 'string' ? item . object : nameOf ( item ) ;
91- if ( ! object ) continue ;
9290 if ( 'list' in item || 'listViews' in item ) {
91+ // The platform's one spelling of a container's binding (a built container usually carries no `object` of its own).
92+ const object = deriveViewContainerObject ( item ) ;
93+ if ( ! object ) continue ;
9394 for ( const v of expandViewContainer ( object , item ) ) {
9495 if ( v . viewKind !== 'list' ) continue ;
9596 const type = ( v . config as { type ?: unknown } | undefined ) ?. type ;
9697 out . push ( { object, name : v . name , type : typeof type === 'string' ? type : 'grid' } ) ;
9798 }
98- } else if ( typeof item . type === 'string' && typeof item . name === 'string' ) {
99- out . push ( { object, name : item . name . includes ( '.' ) ? item . name : `${ object } .${ item . name } ` , type : item . type } ) ;
99+ } else if ( typeof item . type === 'string' && typeof item . name === 'string' && typeof item . object === 'string' ) {
100+ out . push ( { object : item . object , name : item . name . includes ( '.' ) ? item . name : `${ item . object } .${ item . name } ` , type : item . type } ) ;
100101 }
101102 }
102103 return out ;
@@ -240,12 +241,24 @@ export function playwrightBrowser(opts: PlaywrightBrowserOptions = {}): Screensh
240241 const signIn = await context . request . post ( `${ baseUrl } /api/v1/auth/sign-in/email` , { data : admin } ) ;
241242 if ( ! signIn . ok ( ) ) throw new Error ( `the browser's admin sign-in was refused: ${ signIn . status ( ) } ` ) ;
242243 const page = await context . newPage ( ) ;
244+ const visit = async ( path : string ) => {
245+ await page . goto ( `${ baseUrl } ${ path } ` , { waitUntil : 'load' } ) ;
246+ // A page that never goes quiet (a stream, a poll) is shot after the budget, not skipped.
247+ await page . waitForLoadState ( 'networkidle' , { timeout : opts . settleTimeoutMs ?? 30_000 } ) . catch ( ( ) => undefined ) ;
248+ await page . waitForTimeout ( 1_000 ) ;
249+ } ;
250+ // An administrator's first visit to an app page can open first-run UI over it (measured on
251+ // 18.3: a default-currency dialog over the first app page, once per browser session; the
252+ // Console's landing page does not take it). So the first shot's page is visited once
253+ // unshot, and nothing is shot under that dialog.
254+ let warmed = false ;
243255 return {
244256 async capture ( path , file ) {
245- await page . goto ( `${ baseUrl } ${ path } ` , { waitUntil : 'load' } ) ;
246- // A page that never goes quiet (a stream, a poll) is shot after the budget, not skipped.
247- await page . waitForLoadState ( 'networkidle' , { timeout : opts . settleTimeoutMs ?? 30_000 } ) . catch ( ( ) => undefined ) ;
248- await page . waitForTimeout ( 1_000 ) ;
257+ if ( ! warmed ) {
258+ await visit ( path ) ;
259+ warmed = true ;
260+ }
261+ await visit ( path ) ;
249262 await page . screenshot ( { path : file } ) ;
250263 } ,
251264 close : ( ) => browser . close ( ) ,
0 commit comments