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
This commit is contained in:
@@ -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.text<b.text?-1:a.text>b.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]]);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user