From 69ab0ca7312f90a9286d7f40e77e47328925fd07 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Tue, 27 Oct 2026 19:44:54 -0700 Subject: [PATCH] test(collections): compare the COLLECTIONS group with the concept The concept hides COLLECTIONS at 950 px and below, which would leave a phone no way to open a collection; production shows the rows there in the wrapped layout of every other sidebar nav (D136). The shell comparison now compares the group instead of filtering it out: the concept fixture seeds Landscapes (12) and Experiments (8), the rows compare by name order, and at phone width the nav compares against the concept's own sidebar-nav layout. The collection rows are checked at 1600x1100 and 390x844 in both themes. Part of #331 --- tests/browser/shell-concept.spec.mjs | 42 ++++++++++++++++---- tests/browser/workspace-collections.spec.mjs | 23 +++++++++++ tests/browser/workspace_concept_fixture.py | 18 +++++++++ web/js/workspace/workspace.css | 3 ++ 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/tests/browser/shell-concept.spec.mjs b/tests/browser/shell-concept.spec.mjs index 878b9c55..2a79d67f 100644 --- a/tests/browser/shell-concept.spec.mjs +++ b/tests/browser/shell-concept.spec.mjs @@ -76,13 +76,41 @@ function savedNav(nav) { return {...nav,rows:nav.rows.map(row=>row.text==='Needs review'?{...row,count:{...row.count,text:'0'}}:row)}; } +/** + * D136 (#331): the concept's COLLECTIONS rows (app.js a336–339) as production lists them, by name (DC1): the fixture's + * "Landscapes" and "Experiments" hold the concept's 12 and 8 images, so text and counts compare as they are and each + * place keeps the concept's row geometry. At <=950px the concept hides the nav, which would leave a phone no way to + * open a collection: production shows it as the concept lays out every other sidebar nav there (its SAVED VIEWS nav), + * its rows styled as that nav's rows. Text, widths and wrap gaps then follow the rows' own content. + */ +function collectionsNav(nav,navs) { + const byName=[...nav.rows].sort((a,b)=>a.textb.text?1:0); + if(nav.shown)return {...nav,rows:byName.map((row,index)=>({...row,gapAfter:nav.rows[index].gapAfter}))}; + const saved=navs.find(item=>item.id==='saved-nav'); + const {id,label,rows,...frame}=saved; + return {...nav,...frame,id:nav.id,label:nav.label,rows:byName.map(row=>({...rows[0],text:row.text, + count:row.count&&rows[0].count?{...rows[0].count,text:row.count.text}:rows[0].count, + icon:rows[0].icon&&row.icon?{...rows[0].icon,d:row.icon.d}:row.icon, + width:'(content)',hugs:'(content)',gapAfter:'(content)',current:null}))}; +} + +/** The production collections nav at <=950px with its content-sized values replaced, as collectionsNav maps them. */ +function contentFree(navs) { + return navs.map(nav=>nav.id!=='collections-nav'||!nav.shown?nav:{...nav, + rows:nav.rows.map(row=>({...row,width:'(content)',hugs:'(content)',gapAfter:'(content)'}))}); +} + +/** D136: where the concept hides COLLECTIONS, the production rows' content-sized values are not compared. */ +function comparableNavs(reference,navs) { + return reference.chrome.navs.find(nav=>nav.id==='collections-nav')?.shown?navs:contentFree(navs); +} + /** Recorded shell deviations only (spec §8). Everything else in the concept inventory must equal production. */ function mapShellConcept(reference) { const {chrome}=reference; const navs=chrome.navs - // D28: COLLECTIONS is a proposal with no persisted store, so its nav is not rendered. - .filter(nav=>nav.id!=='collections-nav') .map(nav=>{ + if(nav.id==='collections-nav')return collectionsNav(nav,chrome.navs); if(nav.id==='saved-nav')return savedNav(nav); if(nav.id!=='system-nav')return nav; const system=rowNamed(nav,'System'); @@ -99,9 +127,8 @@ function mapShellConcept(reference) { return {...reference,chrome:{...chrome, // D29: no prototype suffix in the document title. title:chrome.title.replace(/ · Prompt Manager concept$/,' · Prompt Manager'), - // D28: no COLLECTIONS label. D49: SAVED VIEWS without its "PROPOSED" badge. - labels:chrome.labels.filter(label=>label.text!=='COLLECTIONS PROPOSED') - .map(label=>label.text==='SAVED VIEWS PROPOSED'?{...label,text:'SAVED VIEWS'}:label), + // D49: SAVED VIEWS without its "PROPOSED" badge. D136: COLLECTIONS without it either. + labels:chrome.labels.map(label=>label.text.endsWith(' PROPOSED')?{...label,text:label.text.replace(/ PROPOSED$/,'')}:label), navs, connection:{...chrome.connection, // D28: truthful copy instead of "Demo connection · Classic / Nodes 2.0". @@ -134,8 +161,9 @@ function headingFrame(heading) { function compare(route,reference,actual) { const expected=mapShellConcept(reference); const heading=FULL_HEADING.has(route); + const chrome={...actual.chrome,navs:comparableNavs(reference,actual.chrome.navs)}; return [ - ...diffInventory({chrome:expected.chrome,noPageScroll:expected.noPageScroll},{chrome:actual.chrome,noPageScroll:actual.noPageScroll}), + ...diffInventory({chrome:expected.chrome,noPageScroll:expected.noPageScroll},{chrome,noPageScroll:actual.noPageScroll}), ...diffInventory(heading?expected.heading:headingFrame(expected.heading),heading?actual.heading:headingFrame(actual.heading)) .map(item=>({...item,path:`heading.${item.path}`})), ]; @@ -242,7 +270,7 @@ for(const [width,height] of SIZES)test(`a saved view row matches the concept's $ expect(reference.chrome.navs.find(nav=>nav.id==='saved-nav').rows.map(row=>row.text)).toEqual(['Favorites','Needs review','Golden hour']); await openProduction(page,'images','dark'); const actual=await save('Save','#workspace-main'); - expect(diffInventory(mapShellConcept(reference).chrome.navs,actual.chrome.navs)).toEqual([]); + expect(diffInventory(mapShellConcept(reference).chrome.navs,comparableNavs(reference,actual.chrome.navs))).toEqual([]); const referenceRows=reference.chrome.navs.find(nav=>nav.id==='saved-nav').rows; expect(actual.chrome.navs.find(nav=>nav.id==='saved-nav').rows.map(row=>[row.text,row.count])) .toEqual([['Favorites',referenceRows[0].count],['Needs review',{...referenceRows[1].count,text:'0'}],['Golden hour',null]]); diff --git a/tests/browser/workspace-collections.spec.mjs b/tests/browser/workspace-collections.spec.mjs index 50e2e2bb..49fb3e3b 100644 --- a/tests/browser/workspace-collections.spec.mjs +++ b/tests/browser/workspace-collections.spec.mjs @@ -267,6 +267,29 @@ test('a collection deleted in another tab shows the deleted-collection page', as await expect(rows(page)).toHaveCount(1); }); +// D136 / CUJ 15: the rows show and open at desktop and phone width in both themes. The concept's own phone rule hides +// every nav count there, so the rows keep their counts only at desktop width. +for (const [width, height] of [[1600, 1100], [390, 844]]) for (const theme of ['dark', 'light']) { + test(`collection rows show and open at ${width}x${height} ${theme}`, async ({ page, request }) => { + const manifest = await seed(request); + await page.setViewportSize({ width, height }); + await page.goto(`${workspace}#/images`); + await page.getByRole('combobox', { name: 'Theme', exact: true }).selectOption(theme); + for (const name of ['Experiments', 'Landscapes']) await expect(row(page, name)).toBeVisible(); + const count = row(page, 'Landscapes').locator('.nav-count'); + if (width > 950) await expect(count).toBeVisible(); + else await expect(count).toBeHidden(); + // Phone width: the label stays hidden as every sidebar label is; desktop shows it. + if (width > 950) await expect(collectionsLabel(page)).toBeVisible(); + else await expect(collectionsLabel(page)).toBeHidden(); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); + await row(page, 'Landscapes').click(); + await expect(page).toHaveURL(new RegExp(`#/collection/${manifest.collections.Landscapes.id}$`)); + await expect(heading(page)).toHaveText('Landscapes'); + await expect(imageCards(page)).toHaveCount(12); + }); +} + test('a finished restore reads the collections again', async ({ page, request }) => { await seed(request); await expect(await request.post('/__test__/system/reset')).toBeOK(); diff --git a/tests/browser/workspace_concept_fixture.py b/tests/browser/workspace_concept_fixture.py index 286cf054..55da0ee0 100644 --- a/tests/browser/workspace_concept_fixture.py +++ b/tests/browser/workspace_concept_fixture.py @@ -194,6 +194,10 @@ _TABLES = ( ) +# The concept's collections and the slice of its images each holds (#331). +CONCEPT_COLLECTIONS = (("Landscapes", slice(0, 12)), ("Experiments", slice(12, 20))) + + def concept_starred(prompt_index: int, round_index: int) -> bool: """The concept's favorites (app.js ``starred``): each other prompt's first image.""" return round_index == 0 and prompt_index % 2 == 0 @@ -496,6 +500,19 @@ def seed_workspace_concept( ) images.reverse() runs.reverse() + # The concept's COLLECTIONS rows (app.js a336–339): "Landscapes" holds the + # first 12 images in the concept's order and "Experiments" the next 8, so + # the rows and counts compare as the concept shows them (#331 D136). + collections = {} + for name, members in CONCEPT_COLLECTIONS: + added = db.add_to_collection( + name=name, + items=[ + (favorite_root, _favorite_relative_key(image["path"])) + for image in images[members] + ], + ) + collections[name] = added["collection"]["id"] seedless = [] for number in range(seedless_images): name = f"seedless_{number + 1:05}.png" @@ -532,4 +549,5 @@ def seed_workspace_concept( "images": images, "runs": runs, "tags": tags, + "collections": collections, } diff --git a/web/js/workspace/workspace.css b/web/js/workspace/workspace.css index 8d29e3f2..24612f69 100644 --- a/web/js/workspace/workspace.css +++ b/web/js/workspace/workspace.css @@ -12,6 +12,9 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; } sidebar's min-content (the concept's `1fr`); the column is capped at the viewport as production's shell was. */ @media (max-width: 950px) { .app-shell { grid-template-columns: minmax(0, 1fr); /* D31 */ } + /* D136 (#331): the concept hides COLLECTIONS here, which would leave a phone no way to open a collection; its rows + wrap like every other sidebar nav (the concept's `.sidebar nav` flex row). Its label stays hidden, as all are. */ + #collections-nav { display: flex; /* D136 */ } } @media (forced-colors: active) { nav a[aria-current="page"] { border: 2px solid Highlight; color: LinkText; }