feat(library): Filters shows its active count; final review follow-ups
- D44: the shared toolbar's Filters button reads "Filters · N" while N "Refine this view" filters are active (Library: Tags; Images: Source, Folder, Media, Tags), so a Browse-set Tags filter is visible on the shelf. The text is the accessible name. Unit and browser tests cover Browse -> "Filters · 1" -> Clear -> "Filters" and the Images counts. - Renumber the alphabetical tag chips deviation D42 -> D43 (D42 is #264's). - The caption count is a role=status region written only when it changes and kept shown while the shelf reloads. - usedLabel names the year for a use in another calendar year (D23). - prompt-blocks: same `title` option API as #264 (`subject` suffix); the region-name helper's parameter no longer shadows it. - Scope library.css .row-actions rules to the history panels; fix stale CSS and spec comments. - Retry re-reading a failed category list and the no-op Apply/search paths are covered by a browser test. - TESTING.md: D23 amendments (year, button vs link trade-off), D44, the legacy Library link wording.
This commit is contained in:
@@ -188,7 +188,8 @@ test('tag chip filters Images by that tag',async({page,request})=>{
|
||||
await expect(page.locator('article[data-image-key="0:inspector/workflow-only.png"]')).toBeVisible();
|
||||
await expect(page.locator('article[data-image-key="0:inspector/complete.png"]')).toBeVisible();
|
||||
await expect(page.locator('article[data-image-key="0:inspector/empty.png"]')).toHaveCount(0);
|
||||
await page.getByRole('button',{name:'Filters',exact:true}).click();
|
||||
// #249 D44: the chip's Tags filter shows on the toolbar's Filters button.
|
||||
await page.getByRole('button',{name:'Filters · 1',exact:true}).click();
|
||||
await expect(page.getByRole('dialog').getByRole('textbox',{name:'Tags',exact:true})).toHaveValue('geometric');
|
||||
});
|
||||
|
||||
@@ -574,7 +575,8 @@ test('collection viewer follows filtered loaded order beyond mounted cards',asyn
|
||||
await mediaStage(page).focus();await page.keyboard.press('ArrowRight');await expect(position(page)).toHaveText(`2 of 160 loaded: ${loaded[1].filename}`);
|
||||
expect(pages).toBe(0);await modal.getByRole('button',{name:'Close image viewer',exact:true}).click();
|
||||
await expect(opener).toBeFocused();
|
||||
await page.getByRole('button',{name:'Filters',exact:true}).click();
|
||||
// #249 D44: Source and Folder are active.
|
||||
await page.getByRole('button',{name:'Filters · 2',exact:true}).click();
|
||||
await refine.getByLabel('Source',{exact:true}).selectOption('1');await refine.getByLabel('Folder',{exact:true}).selectOption('inspector');
|
||||
await refine.getByRole('button',{name:'Apply filters',exact:true}).click();
|
||||
await selectFile(page,'complete.png',1);await openCollection(page);await expect(position(page)).toHaveText('1 of 1 loaded: complete.png');
|
||||
|
||||
@@ -34,11 +34,11 @@ export function mapLibraryConcept(reference) {
|
||||
meta:card.meta.items[0]?.text===PLURAL_ONE?reference.oracle.meta[index]:card.meta,
|
||||
// D23: a long title wraps inside the title row, so the title button may shrink below its text (`min-width: 0`).
|
||||
title:{...card.title,items:card.title.items.map(item=>item.tag==='button'?{...item,minWidth:'0px'}:item)},
|
||||
// D42: tag chips come in the store's alphabetical order; the row flows again from the concept's own chips.
|
||||
// D43: tag chips come in the store's alphabetical order; the row flows again from the concept's own chips.
|
||||
tags:sortedTags(card.tags),
|
||||
// D23: the stars are aria-hidden, with a visually hidden "Rated N out of 5" spoken in their place. The fixture's
|
||||
// run_count (4) and last_used_at (now) give the concept's own "4 runs" and "Used today", and its texts are short,
|
||||
// so the two-line clamp changes nothing here.
|
||||
// run_count (4) and last_used_at (now) give the concept's own "4 runs" and "Used today". The two-line clamp is
|
||||
// applied below through `clampedCards`, measured on the concept with `CLAMP_CSS` (its samples wrap at 390px).
|
||||
footer:{...card.footer,facts:{...card.footer.facts,spoken:card.footer.facts.spoken.replace(/^(★+)/,stars=>`Rated ${stars.length} out of 5`)}}}));
|
||||
return {...reference,library:{...library,toolbar,
|
||||
// D35: the caption uses the singular for one.
|
||||
@@ -78,7 +78,7 @@ export function clampedCards({grid,cards}) {
|
||||
cards:shrunk.map(({card},index)=>({...card,top:card.top-above(cards[index].top)}))};
|
||||
}
|
||||
|
||||
/** D42: a `.tags` row with its chips in alphabetical order, laid out again from the concept's chip boxes. */
|
||||
/** D43: a `.tags` row with its chips in alphabetical order, laid out again from the concept's chip boxes. */
|
||||
export function sortedTags(tags) {
|
||||
return reflow(tags,[...tags.items].sort((a,b)=>nocase(a.text,b.text)));
|
||||
}
|
||||
@@ -125,7 +125,7 @@ async function conceptInventory(page) {
|
||||
return {...reference,oracle:{meta}};
|
||||
}
|
||||
|
||||
/** The concept page with each card's tag chips in alphabetical order (the D42 oracle); returns the undo. */
|
||||
/** The concept page with each card's tag chips in alphabetical order (the D43 oracle); returns the undo. */
|
||||
async function sortConceptTags(page) {
|
||||
await page.evaluate(()=>{
|
||||
const nocase=(a,b)=>{const x=a.textContent.toLowerCase(),y=b.textContent.toLowerCase();return x<y?-1:x>y?1:0;};
|
||||
@@ -259,7 +259,7 @@ for(const [width,height] of SIZES)for(const theme of THEMES)test(`library geomet
|
||||
const restoreClamp=await mutate(page,{css:CLAMP_CSS});
|
||||
const clamped=(await inventoryOf(page,'#main')).library;
|
||||
await restoreClamp();
|
||||
// ...and with every card's tag chips put in alphabetical order (D42).
|
||||
// ...and with every card's tag chips put in alphabetical order (D43).
|
||||
const restoreTags=await sortConceptTags(page);
|
||||
const sorted=(await inventoryOf(page,'#main')).library.cards.map(card=>card.tags);
|
||||
await restoreTags();
|
||||
|
||||
@@ -16,8 +16,10 @@ const sortBy = page => page.getByRole('combobox', { name: 'Sort results', exact:
|
||||
const caption = page => page.locator('.section-caption');
|
||||
const refineDialog = page => page.getByRole('dialog', { name: 'Refine this view', exact: true });
|
||||
const importDialog = page => page.getByRole('dialog', { name: 'Import & inspect', exact: true });
|
||||
// D44: the Filters button reads "Filters · N" while N Refine filters are active.
|
||||
const filtersButton = page => page.locator('#workspace-main search.toolbar').getByRole('button', { name: /^Filters(?: · \d+)?$/ });
|
||||
async function refine(page, values) {
|
||||
await page.getByRole('button', { name: 'Filters', exact: true }).click();
|
||||
await filtersButton(page).click();
|
||||
const dialog = refineDialog(page);
|
||||
for (const [label, value] of Object.entries(values)) {
|
||||
const control = dialog.getByLabel(label, { exact: true });
|
||||
@@ -266,8 +268,9 @@ test('returning to Images restores the filtered stream, layout, selection and sc
|
||||
await expect(cards(page)).toHaveCount(80);
|
||||
await expect(details(page).getByRole('heading', { level: 2 })).toHaveText(selectedTitle);
|
||||
await expect.poll(async () => Math.abs((await page.evaluate(() => window.scrollY)) - previousScroll)).toBeLessThan(120);
|
||||
// Checked last: closing the dialog focuses Filters, which scrolls the toolbar into view.
|
||||
await page.getByRole('button', { name: 'Filters', exact: true }).click();
|
||||
// Checked last: closing the dialog focuses Filters, which scrolls the toolbar into view. The restored Source
|
||||
// still counts (D44).
|
||||
await page.getByRole('button', { name: 'Filters · 1', exact: true }).click();
|
||||
await expect(refineDialog(page).getByLabel('Source', { exact: true })).toHaveValue('0');
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(refineDialog(page)).toBeHidden();
|
||||
@@ -521,7 +524,7 @@ async function staleSourceRecovery(page, request, roots) {
|
||||
await page.goto(workspace);
|
||||
const source = refineDialog(page).getByLabel('Source', { exact: true });
|
||||
const folder = refineDialog(page).getByLabel('Folder', { exact: true });
|
||||
const filters = page.getByRole('button', { name: 'Filters', exact: true });
|
||||
const filters = filtersButton(page);
|
||||
await filters.click();await expect(folder.locator('option[value="bulk"]')).toHaveCount(1);await page.keyboard.press('Escape');
|
||||
await refine(page, { Source: '0', Folder: 'bulk' });
|
||||
const configured = page.waitForResponse(response => {
|
||||
@@ -566,7 +569,9 @@ for (const [name, roots] of [['reordered', ['input', 'output']], ['removed', ['i
|
||||
expect(recovery.pages.map(url => url.search)).toEqual([]);expect(recovery.inspections).toEqual([]);
|
||||
await expect(page.locator('.filter-row span.chip')).toHaveText('All output folders');
|
||||
for (const name of ['Filter by category', 'Sort results']) await expect(page.getByRole('combobox', { name, exact: true })).toBeDisabled();
|
||||
for (const name of ['Filters', 'Import']) await expect(page.getByRole('button', { name, exact: true })).toBeDisabled();
|
||||
// D44: recovery clears Source and Folder; the Media filter stays and still counts.
|
||||
await expect(recovery.filters).toHaveText('Filters · 1');await expect(recovery.filters).toBeDisabled();
|
||||
await expect(page.getByRole('button', { name: 'Import', exact: true })).toBeDisabled();
|
||||
await expect(search(page)).toBeDisabled();
|
||||
await expect(page.getByRole('button', { name: 'Load more images', exact: true })).toBeDisabled();
|
||||
await page.getByRole('button', { name: 'Load more images', exact: true }).evaluate(node => node.click());await recovery.filters.evaluate(node => node.click());await sortBy(page).dispatchEvent('change');
|
||||
@@ -830,10 +835,13 @@ test('images toolbar and filter row match the concept controls', async ({ page,
|
||||
await page.getByRole('combobox', { name: 'Filter by category', exact: true }).selectOption('');
|
||||
await expect(cards(page)).toHaveCount(2);
|
||||
|
||||
await expect(filtersButton(page)).toHaveText('Filters');
|
||||
await refine(page, { Source: '1', Folder: 'shared', Media: 'image' });
|
||||
await expect(cards(page)).toHaveCount(1);
|
||||
await expect(cards(page)).toHaveAttribute('data-image-key', '1:shared/same.png');
|
||||
await expect(row.locator(':scope > span.chip')).toHaveText(`${roots[1].label} · shared`);
|
||||
// D44: Source, Folder and Media each count once; the toolbar category is not a Refine filter.
|
||||
await expect(filtersButton(page)).toHaveText('Filters · 3');
|
||||
await refine(page, { Media: 'video' });
|
||||
await expect(cards(page)).toHaveCount(0);
|
||||
await refine(page, { Source: 'all', Folder: '', Media: 'all', Tags: 'cats' });
|
||||
@@ -842,11 +850,13 @@ test('images toolbar and filter row match the concept controls', async ({ page,
|
||||
await expect(row.locator(':scope > span.chip')).toHaveText('All output folders');
|
||||
await page.getByRole('combobox', { name: 'Filter by category', exact: true }).selectOption('Landscapes');
|
||||
await expect(cards(page)).toHaveCount(0);
|
||||
await page.getByRole('button', { name: 'Filters', exact: true }).click();
|
||||
await expect(filtersButton(page)).toHaveText('Filters · 1');
|
||||
await page.getByRole('button', { name: 'Filters · 1', exact: true }).click();
|
||||
await expect(refineDialog(page).getByLabel('Tags', { exact: true })).toHaveValue('cats');
|
||||
await refineDialog(page).getByRole('button', { name: 'Clear filters', exact: true }).click();
|
||||
await expect(page.getByRole('combobox', { name: 'Filter by category', exact: true })).toHaveValue('');
|
||||
await expect(cards(page)).toHaveCount(2);
|
||||
await expect(page.getByRole('button', { name: 'Filters', exact: true })).toBeFocused();
|
||||
|
||||
const rescans = [];
|
||||
page.on('request', request => { if (request.url().includes('/prompt_manager/gallery/rescan')) rescans.push(request.method()); });
|
||||
@@ -1495,8 +1505,8 @@ test('Refine offers only the chosen source folders', async ({ page, request }) =
|
||||
await refineDialog(page).getByRole('button', { name: 'Apply filters', exact: true }).click();
|
||||
await expect(cards(page)).toHaveCount(1);
|
||||
await expect(cards(page)).toHaveAttribute('data-image-key', '1:shared/same.png');
|
||||
// Reopened on that Source, the dialog again lists only its folders.
|
||||
await page.getByRole('button', { name: 'Filters', exact: true }).click();
|
||||
// Reopened on that Source, the dialog again lists only its folders. Source and Folder count (D44).
|
||||
await page.getByRole('button', { name: 'Filters · 2', exact: true }).click();
|
||||
expect(await values()).toEqual(['', 'shared']);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ const media=page=>gallery(page).locator('article[data-image-key]');
|
||||
const refine=page=>page.getByRole('dialog',{name:'Refine this view',exact:true});
|
||||
/** The Library's Tags filter lives in "Refine this view" (#249 D14). */
|
||||
async function applyTags(page,tags) {
|
||||
await page.locator('#workspace-main search.toolbar').getByRole('button',{name:'Filters',exact:true}).click();
|
||||
// D44: Filters reads "Filters · 1" while a Tags filter is active.
|
||||
await page.locator('#workspace-main search.toolbar').getByRole('button',{name:/^Filters(?: · 1)?$/}).click();
|
||||
await refine(page).getByRole('textbox',{name:'Tags',exact:true}).fill(tags);
|
||||
await refine(page).getByRole('button',{name:'Apply filters',exact:true}).click();await expect(refine(page)).toHaveCount(0);
|
||||
}
|
||||
@@ -104,6 +105,30 @@ test('library infinite loading keeps unique parents and keyboard-accessible hist
|
||||
await explore.focus();await expect(explore).toBeFocused();await page.keyboard.press('Enter');await expect(history(page)).toBeVisible();
|
||||
});
|
||||
|
||||
// D40/D15: with no Refresh, Retry also re-reads a category list that failed with the shelf.
|
||||
test('Retry re-reads a failed category list with the shelf',async({page})=>{
|
||||
let fail=true;const reads=[];
|
||||
await page.route(url=>['/prompt_manager/library','/prompt_manager/library/options'].includes(url.pathname),async request=>{
|
||||
reads.push(new URL(request.request().url()).pathname);
|
||||
if(fail)return request.fulfill({status:503,json:{success:false,error:'Unavailable'}});
|
||||
await request.continue().catch(()=>{});
|
||||
});
|
||||
await page.goto(route);
|
||||
const retry=page.getByRole('button',{name:'Retry prompt library',exact:true});await expect(retry).toBeVisible();
|
||||
const categoriesFailed=page.getByRole('alert').filter({hasText:'Categories could not be loaded. Open the Prompt library again to retry.'});
|
||||
await expect(categoriesFailed).toBeVisible();
|
||||
fail=false;reads.length=0;await retry.click();await expect(cards(page)).toHaveCount(3);
|
||||
await expect(categoriesFailed).toBeHidden();
|
||||
await expect(page.getByRole('combobox',{name:'Filter by category',exact:true}).locator('option',{hasText:'Portraits'})).toHaveCount(1);
|
||||
expect(reads).toContain('/prompt_manager/library/options');
|
||||
// Apply with the same Tags, and a search that lands on the values already shown, reload nothing.
|
||||
reads.length=0;
|
||||
await page.locator('#workspace-main search.toolbar').getByRole('button',{name:'Filters',exact:true}).click();
|
||||
await refine(page).getByRole('button',{name:'Apply filters',exact:true}).click();await expect(refine(page)).toHaveCount(0);
|
||||
const search=page.getByRole('searchbox',{name:'Search workspace',exact:true});await search.fill(' ');
|
||||
await page.waitForTimeout(400);expect(reads).toEqual([]);await expect(cards(page)).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('failed library reads retry while late search responses cannot overwrite newer results',async({page})=>{
|
||||
let fail=true;let release;const hold=new Promise(resolve=>{release=resolve;});
|
||||
await page.route('**/prompt_manager/library?*',async request=>{
|
||||
@@ -131,7 +156,9 @@ test('library toolbar matches the concept and tags live in Refine this view',asy
|
||||
await expect(page.locator('.library-shelf > search.toolbar:first-child')).toHaveCount(1);
|
||||
// Tags apply from the dialog only; Clear clears them and the toolbar category, as the concept's does.
|
||||
await page.getByRole('combobox',{name:'Filter by category',exact:true}).selectOption('Portraits');await applyTags(page,'studio');await expect(cards(page)).toHaveCount(1);
|
||||
await toolbar.getByRole('button',{name:'Filters',exact:true}).click();
|
||||
// D44: the active Tags filter shows on the button; the category is the toolbar's own and is not counted.
|
||||
await expect(toolbar.getByRole('button')).toHaveText(['Filters · 1']);
|
||||
await toolbar.getByRole('button',{name:'Filters · 1',exact:true}).click();
|
||||
await expect(refine(page).locator('.field label')).toHaveText(['Tags']);await expect(refine(page).getByRole('textbox',{name:'Tags',exact:true})).toHaveValue('studio');
|
||||
await refine(page).getByRole('button',{name:'Clear filters',exact:true}).click();await expect(refine(page)).toHaveCount(0);
|
||||
await expect(toolbar.getByRole('button',{name:'Filters',exact:true})).toBeFocused();
|
||||
@@ -152,6 +179,25 @@ test('caption counts equal the filtered cards',async({page,request})=>{
|
||||
}
|
||||
});
|
||||
|
||||
// #249 final review M5: the count is a status region, written only when it changes, so Load more never re-announces it.
|
||||
test('the caption count is announced once per change and kept while the shelf reloads',async({page,request})=>{
|
||||
await request.post('/__test__/reset',{data:{native_library:true,extra_prompts:200}});
|
||||
await page.goto(route);const more=page.getByRole('button',{name:'Load more prompts',exact:true});
|
||||
const count=page.locator('#workspace-main .section-caption > span').first();
|
||||
await expect(count).toHaveAttribute('role','status');await expect(count).toHaveText(/^203 positive prompts · \d+ negative variants?$/);
|
||||
await count.evaluate(node=>{window.__countWrites=0;new MutationObserver(records=>{window.__countWrites+=records.length;}).observe(node,{childList:true,characterData:true,subtree:true});});
|
||||
await loadShelfPages(page,more,1);
|
||||
expect(await page.evaluate(()=>window.__countWrites)).toBe(0);
|
||||
// A filter change keeps the last count shown while the next page loads, then writes the new one.
|
||||
let release;const held=new Promise(resolve=>{release=resolve;});
|
||||
await page.route(url=>url.pathname==='/prompt_manager/library'&&url.searchParams.get('category')==='Portraits',async route=>{await held;await route.continue();});
|
||||
await page.getByRole('combobox',{name:'Filter by category',exact:true}).selectOption('Portraits');
|
||||
await expect(page.locator('.library-status')).toHaveText('Loading prompt library…');
|
||||
await expect(page.locator('#workspace-main .section-caption')).toBeVisible();await expect(count).toHaveText(/^203 positive prompts/);
|
||||
release();await expect(count).not.toHaveText(/^203 /);
|
||||
expect(await page.evaluate(()=>window.__countWrites)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('a Library-embedded media card stars its file and the star survives a reload',async({page})=>{
|
||||
await page.goto(route);await openHistory(page);
|
||||
await history(page).getByRole('button',{name:'All images for this positive',exact:true}).click();await expect(media(page)).toHaveCount(3);
|
||||
|
||||
@@ -30,8 +30,9 @@ async function expectLibrary(page,{tags='',category='',count}) {
|
||||
if(!category) await expect(select.locator('option:checked')).toHaveText('All categories');
|
||||
await expect(page.locator('#workspace-main .section-caption > span').first()).toHaveText(new RegExp(`^${count} positive prompts? · \\d+ negative variants?$`));
|
||||
await expect(page.locator('article[data-prompt-id]')).toHaveCount(count);
|
||||
// Browse sets exactly this Tags filter, which lives in "Refine this view" (#249 D14).
|
||||
await page.locator('#workspace-main search.toolbar').getByRole('button',{name:'Filters',exact:true}).click();
|
||||
// Browse sets exactly this Tags filter, which lives in "Refine this view" (#249 D14); Filters counts it (D44).
|
||||
const filters=page.locator('#workspace-main search.toolbar').getByRole('button',{name:tags?'Filters · 1':'Filters',exact:true});
|
||||
await expect(filters).toBeVisible();await filters.click();
|
||||
const refine=page.getByRole('dialog',{name:'Refine this view',exact:true});
|
||||
await expect(refine.getByRole('textbox',{name:'Tags',exact:true})).toHaveValue(tags);
|
||||
await refine.getByRole('button',{name:'Close dialog',exact:true}).click();await expect(refine).toHaveCount(0);
|
||||
@@ -89,6 +90,28 @@ test('prompt counts equal Library Browse results',async({page})=>{
|
||||
}
|
||||
});
|
||||
|
||||
// #249 D44: a Browse-set Tags filter is visible on the shelf as "Filters · 1", and Clear filters returns the full shelf.
|
||||
test('a tag Browse shows Filters · 1 until its filter is cleared',async({page})=>{
|
||||
await page.goto('/prompt_manager/workspace#/library');
|
||||
const shelf=page.locator('article[data-prompt-id]');await expect(shelf.first()).toBeVisible();const total=await shelf.count();
|
||||
const toolbar=page.locator('#workspace-main search.toolbar');
|
||||
await expect(toolbar.getByRole('button')).toHaveText(['Filters']);
|
||||
await page.goto(route);
|
||||
const row=tagRow(page,'cinematic');await row.getByRole('button',{name:'Browse',exact:true}).click();
|
||||
await expectLibrary(page,{tags:'cinematic',count:2});
|
||||
await expect(toolbar.getByRole('button')).toHaveText(['Filters · 1']);
|
||||
await toolbar.getByRole('button',{name:'Filters · 1',exact:true}).click();
|
||||
const refine=page.getByRole('dialog',{name:'Refine this view',exact:true});
|
||||
await refine.getByRole('button',{name:'Clear filters',exact:true}).click();await expect(refine).toHaveCount(0);
|
||||
await expect(toolbar.getByRole('button',{name:'Filters',exact:true})).toBeFocused();
|
||||
await expect(shelf).toHaveCount(total);
|
||||
// The cleared filter stays cleared on the next in-app visit (the shelf remembers its filters).
|
||||
const nav=page.getByRole('navigation',{name:'Workspace',exact:true});
|
||||
await nav.getByRole('link',{name:/^Tags & categories(?:\s*[\d,]+)?$/}).click();await expect(main(page).getByRole('heading',{level:1})).toHaveText('Tags & categories');
|
||||
await nav.getByRole('link',{name:/^Prompt library(?:\s*[\d,]+)?$/}).click();
|
||||
await expect(shelf).toHaveCount(total);await expect(toolbar.getByRole('button')).toHaveText(['Filters']);
|
||||
});
|
||||
|
||||
test('category Browse sets only the category constraint',async({page})=>{
|
||||
await page.goto('/prompt_manager/workspace#/library');
|
||||
const search=page.getByRole('searchbox',{name:'Search workspace',exact:true});
|
||||
|
||||
@@ -248,6 +248,15 @@ test('the scope chip summarizes source and folder', async () => {
|
||||
assert.equal(scopeLabel({root:'7',subfolder:''},roots),'Source 7 · All output folders');
|
||||
});
|
||||
|
||||
test('the Filters count names each active Refine filter once (D44)', async () => {
|
||||
const {activeRefinements}=await source;
|
||||
assert.equal(activeRefinements({root:'all',subfolder:'',media:'all',tags:[]}),0);
|
||||
assert.equal(activeRefinements({root:'',subfolder:'',media:'all',tags:[]}),0);
|
||||
assert.equal(activeRefinements({root:'1',subfolder:'',media:'all',tags:[]}),1);
|
||||
assert.equal(activeRefinements({root:'all',subfolder:'bulk',media:'video',tags:['a','b']}),3);
|
||||
assert.equal(activeRefinements({root:'0',subfolder:'bulk',media:'image',tags:['a']}),4);
|
||||
});
|
||||
|
||||
test('the card title is the confirmed linked title, else the filename (D10)', async () => {
|
||||
const {cardTitle}=await source;
|
||||
const linked=(status,title)=>({filename:'alpine_00001.png',linked:{status,positive_id:1,title,title_source:'title',category:null,rating:null}});
|
||||
|
||||
@@ -23,6 +23,9 @@ test('used label boundaries in UTC and local time', async () => {
|
||||
assert.equal(usedLabel('2026-10-14T00:00:00Z', now), 'Used 6 days ago');
|
||||
assert.equal(usedLabel('2026-10-13T23:59:59Z', now), 'Used Oct 13');
|
||||
assert.equal(usedLabel('2026-01-02T08:00:00Z', now), 'Used Jan 2');
|
||||
// D23: a use in another calendar year names its year, so last year's Oct 21 never reads as this year's.
|
||||
assert.equal(usedLabel('2025-10-21T08:00:00Z', now), 'Used Oct 21, 2025');
|
||||
assert.equal(usedLabel('2025-12-31T23:59:59Z', now), 'Used Dec 31, 2025');
|
||||
// A clock ahead of this browser is not a future use: it reads as today.
|
||||
assert.equal(usedLabel('2026-10-21T09:00:00Z', now), 'Used today');
|
||||
});
|
||||
|
||||
@@ -611,3 +611,30 @@ test('every route change closes the shared workspace dialog', async () => {
|
||||
const body = render.slice(0, render.indexOf('viewScope = createScope();'));
|
||||
assert.match(body, /closeWorkspaceDialog\(\);/, 'render closes the dialog before the next page mounts');
|
||||
});
|
||||
|
||||
// D44: the concept has no indicator, so a filter set in "Refine this view" (or by Browse) was invisible on the page.
|
||||
test('the Filters button shows the active refinement count', async () => {
|
||||
const { mountToolbar } = await toolbarModule;
|
||||
const { createScope } = await import('../../web/js/shared/toolkit/core.js');
|
||||
const dom = fakeDOM();
|
||||
const scope = createScope();
|
||||
try {
|
||||
const container = new dom.Element('div');
|
||||
const toolbar = mountToolbar(container, { scope, categories: [], sorts: [['newest', 'Recently added']],
|
||||
activeFilters: 2, onChange() {}, onFilters() {} });
|
||||
const filtersButton = container.children[0].children[3];
|
||||
// The visible text is the accessible name, so the count is spoken too.
|
||||
assert.equal(filtersButton.textContent, 'Filters · 2');
|
||||
toolbar.setActiveFilters(1);
|
||||
assert.equal(filtersButton.textContent, 'Filters · 1');
|
||||
toolbar.setActiveFilters(0);
|
||||
assert.equal(filtersButton.textContent, 'Filters');
|
||||
for (const bad of [-1, 1.5, '1', null, Number.NaN]) assert.throws(() => toolbar.setActiveFilters(bad), TypeError);
|
||||
assert.equal(filtersButton.textContent, 'Filters');
|
||||
assert.throws(() => mountToolbar(new dom.Element('div'), { scope, categories: [], sorts: [['newest', 'Recently added']],
|
||||
activeFilters: -1, onChange() {}, onFilters() {} }), TypeError);
|
||||
} finally {
|
||||
scope.dispose();
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user