@@ -256,6 +256,133 @@ test("workspace: opening an app replaces the welcome page with a dock", async (a
256256 // (workspace.closeDock), and no later test asserts welcome-page state.
257257} ) ;
258258
259+ // --- Workspace (A4) — closing the last dock brings the welcome page back ---
260+ //
261+ // Spec §2.A A4: from A3 state, close fixture A's dock. Closing the last dock
262+ // also unloads the module by design (WorkspaceArea::pluginClosed →
263+ // unloadUiModule), so the gates double as a regression guard for the
264+ // currentVisibleApp clear on unload of the visible app.
265+ //
266+ // closeDock is invoked through the inspector's evaluate, NOT callMethod:
267+ // callMethod does not marshal the QString argument correctly (logos-qt-mcp
268+ // limitation), while the evaluate path's JS engine converts it fine.
269+
270+ test ( "workspace: closing the last dock brings the welcome page back" , async ( app ) => {
271+ // Same stable evaluate anchor as A3 — has `backend` in context and
272+ // survives the dock teardown.
273+ let welcome = null ;
274+ await app . waitFor ( async ( ) => {
275+ welcome = await findWelcomePage ( app ) ;
276+ if ( ! welcome ) throw new Error ( "no WelcomePage instance in the QML tree" ) ;
277+ } , { timeout : 10000 , interval : 500 , description : "WelcomePage instance to exist" } ) ;
278+
279+ const workspace = await findByObjectName ( app . inspector , "workspace" ) ;
280+ if ( ! workspace ) {
281+ throw new Error ( 'WorkspaceArea (objectName "workspace") not found' ) ;
282+ }
283+
284+ // Establish the A3 end state without assuming A3 left it: fixture A's
285+ // dock must be open before we can close it.
286+ const preCount = await app . inspector . send ( "evaluate" , {
287+ objectId : workspace . id , expression : "dockCount" ,
288+ } ) ;
289+ if ( preCount . error ) throw new Error ( `evaluate(dockCount) failed: ${ preCount . error } ` ) ;
290+ if ( preCount . result !== 1 ) {
291+ let tile = null ;
292+ try {
293+ await app . waitFor ( async ( ) => {
294+ tile = await findByObjectName ( app . inspector , `sidebar.app.${ FIXTURE_A . name } ` ) ;
295+ if ( ! tile ) throw new Error ( `sidebar.app.${ FIXTURE_A . name } not in the tree` ) ;
296+ } , { timeout : 10000 , interval : 500 , description : "fixture A sidebar tile to appear" } ) ;
297+ } catch ( e ) {
298+ if ( ! CI_MODE ) {
299+ console . log (
300+ ` SKIP: fixture A (${ FIXTURE_A . name } ) is not installed in this ` +
301+ `app instance (spec §0.A: skip, not fail, outside --ci)` ) ;
302+ return ;
303+ }
304+ throw new Error (
305+ `no dock open and fixture A sidebar tile never appeared — ` +
306+ `integration-test pre-seeds ${ FIXTURE_A . name } at boot, so this is ` +
307+ `a real failure: ${ e . message } ` ) ;
308+ }
309+ const clicked = await app . inspector . send ( "callMethod" , {
310+ objectId : tile . id , method : "clicked" ,
311+ } ) ;
312+ if ( clicked . error ) {
313+ throw new Error ( `clicking sidebar.app.${ FIXTURE_A . name } failed: ${ clicked . error } ` ) ;
314+ }
315+ }
316+ await app . waitFor ( async ( ) => {
317+ const count = await app . inspector . send ( "evaluate" , {
318+ objectId : workspace . id , expression : "dockCount" ,
319+ } ) ;
320+ if ( count . error ) throw new Error ( `evaluate(dockCount) failed: ${ count . error } ` ) ;
321+ if ( count . result !== 1 ) {
322+ throw new Error ( `WorkspaceArea.dockCount=${ count . result } (expected 1)` ) ;
323+ }
324+ const visibleApp = await app . inspector . send ( "evaluate" , {
325+ objectId : welcome . id , expression : "backend.currentVisibleApp" ,
326+ } ) ;
327+ if ( visibleApp . error ) {
328+ throw new Error ( `evaluate(backend.currentVisibleApp) failed: ${ visibleApp . error } ` ) ;
329+ }
330+ if ( visibleApp . result !== FIXTURE_A . name ) {
331+ throw new Error (
332+ `backend.currentVisibleApp=${ JSON . stringify ( visibleApp . result ) } ` +
333+ `(expected "${ FIXTURE_A . name } ")` ) ;
334+ }
335+ } , { timeout : 10000 , interval : 500 ,
336+ description : `fixture A dock to be open and front-most` } ) ;
337+
338+ // Close the dock.
339+ const closed = await app . inspector . send ( "evaluate" , {
340+ objectId : workspace . id ,
341+ expression : `closeDock(${ JSON . stringify ( FIXTURE_A . name ) } )` ,
342+ } ) ;
343+ if ( closed . error ) throw new Error ( `evaluate(closeDock) failed: ${ closed . error } ` ) ;
344+
345+ // Gate: dock count reaches 0 within 5 s.
346+ await app . waitFor ( async ( ) => {
347+ const res = await app . inspector . send ( "evaluate" , {
348+ objectId : workspace . id , expression : "dockCount" ,
349+ } ) ;
350+ if ( res . error ) throw new Error ( `evaluate(dockCount) failed: ${ res . error } ` ) ;
351+ if ( res . result !== 0 ) {
352+ throw new Error ( `WorkspaceArea.dockCount=${ res . result } (expected 0)` ) ;
353+ }
354+ } , { timeout : 5000 , interval : 250 , description : "workspace dockCount to reach 0" } ) ;
355+
356+ // Gate: the welcome page is visible again…
357+ await app . waitFor ( async ( ) => {
358+ if ( ( await welcomePageHidden ( app , welcome . id ) ) !== false ) {
359+ throw new Error ( "welcome page is still hidden after closing the last dock" ) ;
360+ }
361+ } , { timeout : 5000 , interval : 250 , description : "welcome page to reappear" } ) ;
362+
363+ // …with the installed-apps greeting — closing unloads fixture A but does
364+ // not uninstall it, so launcherApps stays non-empty and the greeting is
365+ // "Welcome back", not the first-launch text.
366+ await app . waitFor (
367+ async ( ) => { await app . expectTexts ( [ "Welcome back" ] ) ; } ,
368+ { timeout : 5000 , interval : 250 , description : '"Welcome back" greeting to render' }
369+ ) ;
370+
371+ // Gate: the backend no longer reports a front-most app.
372+ await app . waitFor ( async ( ) => {
373+ const res = await app . inspector . send ( "evaluate" , {
374+ objectId : welcome . id , expression : "backend.currentVisibleApp" ,
375+ } ) ;
376+ if ( res . error ) {
377+ throw new Error ( `evaluate(backend.currentVisibleApp) failed: ${ res . error } ` ) ;
378+ }
379+ if ( res . result !== "" ) {
380+ throw new Error (
381+ `backend.currentVisibleApp=${ JSON . stringify ( res . result ) } (expected "")` ) ;
382+ }
383+ } , { timeout : 5000 , interval : 250 , description : "currentVisibleApp to clear" } ) ;
384+ } ) ;
385+
259386// --- Package Manager ---
260387//
261388// PMUI is no longer launched from the sidebar app launcher (filtered out
0 commit comments