fix(workspace): Overview cards show the sidebar's counts (#549)
Closes #547
This commit is contained in:
@@ -602,7 +602,8 @@ async function staleSourceRecovery(page, request, roots) {
|
||||
const pages = [], inspections = [], options = [], mutations = [];
|
||||
page.on('request', request => {
|
||||
const url = new URL(request.url());
|
||||
if (url.pathname.endsWith('/images/output')) pages.push(url);
|
||||
// limit=1 is a sidebar count read (Overview re-reads it on a visit, #547), not a page of images.
|
||||
if (url.pathname.endsWith('/images/output') && url.searchParams.get('limit') !== '1') pages.push(url);
|
||||
if (url.pathname.includes('/images/inspect/')) inspections.push(url);
|
||||
if (request.method() !== 'GET') mutations.push(url.pathname);
|
||||
});
|
||||
|
||||
@@ -176,7 +176,7 @@ test('failed library reads retry while late search responses cannot overwrite ne
|
||||
// D153 (#447): a Browse visit that still names the retired "Highest rated" sort opens the shelf on Recently added.
|
||||
test('a Library visit with the retired rating sort opens on Recently added',async({page})=>{
|
||||
await page.goto(route);await expect(cards(page)).toHaveCount(3);
|
||||
const sorts=[];page.on('request',item=>{const url=new URL(item.url());if(url.pathname==='/prompt_manager/library')sorts.push(url.searchParams.get('sort'));});
|
||||
const sorts=[];page.on('request',item=>{const url=new URL(item.url());if(url.pathname==='/prompt_manager/library'&&url.searchParams.get('limit')!=='1')sorts.push(url.searchParams.get('sort'));});// limit=1 is a sidebar count read, not the shelf
|
||||
await page.goto('/prompt_manager/workspace#/overview');await expect(page.locator('#workspace-main h1')).toHaveText('Overview');
|
||||
await page.evaluate(async()=>{const {browseLibrary}=await import('/prompt_manager/js/workspace/library.mjs');browseLibrary({sort:'rating'});});
|
||||
await expect(cards(page)).toHaveCount(3);
|
||||
|
||||
@@ -60,6 +60,10 @@ test.afterEach(async ({ page, browserName }) => {
|
||||
|
||||
test('workspace starts with real persisted statistics without mutating data', async ({ page, request }) => {
|
||||
const before = await (await request.get('/prompt_manager/stats')).json();
|
||||
const total = async path => (await (await request.get(`/prompt_manager/${path}`)).json()).total;
|
||||
const counts = [['Prompts', await total('library?limit=1')],
|
||||
['Images', await total('images/output?mode=workspace&limit=1')],
|
||||
['Tags', await total('vocabulary/tags?limit=1')]];
|
||||
const mutations = [];
|
||||
page.on('request', request => {
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(request.method())) mutations.push(request.url());
|
||||
@@ -69,10 +73,11 @@ test('workspace starts with real persisted statistics without mutating data', as
|
||||
await expect(page.getByRole('navigation', { name: 'Workspace', exact: true })).toBeVisible();
|
||||
const main = page.locator('#workspace-main');
|
||||
await expect(main.getByRole('heading', { level: 1, name: 'Overview', exact: true })).toBeVisible();
|
||||
for (const [label, key] of [['Prompts', 'total_prompts'], ['Images', 'total_images'], ['Tags', 'total_tags']]) {
|
||||
await expect(statistic(page, label)).toHaveText(String(before.stats[key]));
|
||||
for (const [label, count] of counts) await expect(statistic(page, label)).toHaveText(String(count));
|
||||
// #547: a card and the sidebar badge of the page it names show one number.
|
||||
for (const [route, count] of [['library', counts[0][1]], ['images', counts[1][1]], ['tags', counts[2][1]]]) {
|
||||
await expect(page.locator(`aside.sidebar nav [data-route="${route}"] .nav-count`)).toHaveText(String(count));
|
||||
}
|
||||
expect(before.stats.total_prompts).toBe(2);
|
||||
expect((await (await request.get('/prompt_manager/stats')).json()).stats).toEqual(before.stats);
|
||||
expect(mutations).toEqual([]);
|
||||
await expect(page.locator('iframe')).toHaveCount(0);
|
||||
@@ -373,25 +378,65 @@ test('unavailable host palette falls back to the system theme and tracks system
|
||||
});
|
||||
|
||||
test('overview reports loading, offers retry after failure and renders real empty statistics', async ({ page }) => {
|
||||
const sources = ['**/prompt_manager/library?limit=1', '**/prompt_manager/images/output?mode=workspace&limit=1',
|
||||
'**/prompt_manager/vocabulary/tags?limit=1'];
|
||||
let release;
|
||||
const pending = new Promise(resolve => { release = resolve; });
|
||||
await page.route('**/prompt_manager/stats', async route => {
|
||||
await pending;
|
||||
await route.fulfill({ status: 503, json: { success: false, error: 'Unavailable' } });
|
||||
});
|
||||
for (const source of sources) {
|
||||
await page.route(source, async route => {
|
||||
await pending;
|
||||
await route.fulfill({ status: 503, json: { success: false, error: 'Unavailable' } });
|
||||
});
|
||||
}
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
await expect(page.locator('#workspace-main').getByRole('status')).toBeVisible();
|
||||
release();
|
||||
await expect(page.locator('#workspace-main').getByRole('alert')).toBeVisible();
|
||||
await page.unroute('**/prompt_manager/stats');
|
||||
await page.route('**/prompt_manager/stats', route => route.fulfill({
|
||||
json: { success: true, stats: { total_prompts: 0, total_images: 0, total_tags: 0 } },
|
||||
}));
|
||||
for (const source of sources) {
|
||||
await page.unroute(source);
|
||||
await page.route(source, route => route.fulfill({ json: { success: true, total: 0, items: [], images: [], tags: [] } }));
|
||||
}
|
||||
await page.getByRole('button', { name: 'Retry', exact: true }).click();
|
||||
await expect(page.locator('#workspace-main').getByRole('alert')).toHaveCount(0);
|
||||
for (const label of ['Prompts', 'Images', 'Tags']) {
|
||||
await expect(statistic(page, label)).toHaveText('0');
|
||||
}
|
||||
await expect(page.locator('#workspace-main .empty-state')).toBeVisible();
|
||||
});
|
||||
|
||||
test('a slow or failed Images read leaves the other Overview cards and retries only itself', async ({ page }) => {
|
||||
const images = '**/prompt_manager/images/output?mode=workspace&limit=1';
|
||||
let release;
|
||||
const pending = new Promise(resolve => { release = resolve; });
|
||||
await page.route(images, async route => {
|
||||
await pending;
|
||||
await route.fulfill({ status: 503, json: { success: false, error: 'Unavailable' } });
|
||||
});
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
await expect(statistic(page, 'Prompts')).toHaveText('2');
|
||||
await expect(statistic(page, 'Images')).toHaveText('–');
|
||||
await expect(page.locator('#workspace-main').getByRole('status')).toBeVisible();
|
||||
release();
|
||||
await expect(page.locator('#workspace-main').getByRole('alert')).toBeVisible();
|
||||
await expect(statistic(page, 'Prompts')).toHaveText('2');
|
||||
await page.unroute(images);
|
||||
let libraryReads = 0;
|
||||
page.on('request', request => { if (new URL(request.url()).pathname === '/prompt_manager/library') libraryReads++; });
|
||||
await page.getByRole('button', { name: 'Retry', exact: true }).click();
|
||||
await expect(page.locator('#workspace-main').getByRole('alert')).toHaveCount(0);
|
||||
await expect(statistic(page, 'Images')).toHaveText(/^\d+$/);
|
||||
expect(libraryReads).toBe(0);
|
||||
});
|
||||
|
||||
test('opening Overview reads each count once beside the sidebar', async ({ page }) => {
|
||||
let reads = 0;
|
||||
page.on('request', request => {
|
||||
const url = new URL(request.url());
|
||||
if (url.pathname === '/prompt_manager/images/output' && url.searchParams.get('favorites') === null) reads++;
|
||||
});
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
await expect(statistic(page, 'Images')).toHaveText(/^\d+$/);
|
||||
expect(reads).toBe(1);
|
||||
});
|
||||
|
||||
test('small screens keep navigation and the theme accessible without horizontal page overflow', async ({ page }) => {
|
||||
@@ -410,11 +455,9 @@ test('leaving a loading overview prevents its delayed result from replacing the
|
||||
let completed;
|
||||
const pending = new Promise(resolve => { release = resolve; });
|
||||
const finished = new Promise(resolve => { completed = resolve; });
|
||||
await page.route('**/prompt_manager/stats', async route => {
|
||||
await page.route('**/prompt_manager/library?limit=1', async route => {
|
||||
await pending;
|
||||
await route.fulfill({ json: {
|
||||
success: true, stats: { total_prompts: 999, total_images: 888, total_tags: 777 },
|
||||
} }).catch(() => {});
|
||||
await route.fulfill({ json: { success: true, total: 999, items: [] } }).catch(() => {});
|
||||
completed();
|
||||
});
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
@@ -437,7 +480,7 @@ test('leaving a loading overview prevents its delayed result from replacing the
|
||||
test('cached-page restoration remounts one working shell and refreshes its data', async ({ page }) => {
|
||||
let reads = 0;
|
||||
page.on('request', request => {
|
||||
if (new URL(request.url()).pathname === '/prompt_manager/stats') reads++;
|
||||
if (new URL(request.url()).pathname === '/prompt_manager/vocabulary/tags') reads++;
|
||||
});
|
||||
await page.goto(`${workspace}#/overview`);
|
||||
await expect(statistic(page, 'Prompts')).toHaveText('2');
|
||||
|
||||
@@ -8,7 +8,7 @@ test('workspace serves real library data beside either ComfyUI renderer', async
|
||||
text: `Workspace host smoke ${testInfo.project.name}`, tags: ['workspace-host'],
|
||||
} });
|
||||
expect(saved.ok()).toBe(true);
|
||||
const stats = (await (await request.get('/prompt_manager/stats')).json()).stats;
|
||||
const library = await (await request.get('/prompt_manager/library?limit=1')).json();
|
||||
const workspace = await context.newPage();
|
||||
const errors = [];
|
||||
workspace.on('pageerror', error => errors.push(error.message));
|
||||
@@ -16,7 +16,7 @@ test('workspace serves real library data beside either ComfyUI renderer', async
|
||||
await workspace.goto('/prompt_manager/workspace#/overview');
|
||||
await expect(workspace.locator('#workspace-main h1')).toHaveText('Overview');
|
||||
await expect(workspace.locator('dt').filter({ hasText: /^Prompts$/ }).locator('+ dd'))
|
||||
.toHaveText(stats.total_prompts.toLocaleString());
|
||||
.toHaveText(library.total.toLocaleString());
|
||||
await workspace.getByRole('combobox', { name: 'Theme' }).selectOption('light');
|
||||
await expect(workspace.locator('body')).toHaveAttribute('data-ca-theme', 'light');
|
||||
const imageResponse = workspace.waitForResponse(response => {
|
||||
|
||||
@@ -123,17 +123,29 @@ test('a job of the Activity page is its own deep link', async () => {
|
||||
dispose();
|
||||
});
|
||||
|
||||
test('statistics preserve zero and reject incomplete or invalid counts', async () => {
|
||||
const { summarizeStats } = await source;
|
||||
assert.deepEqual(summarizeStats({total_prompts:0, total_images:12, total_tags:3}), [
|
||||
['Prompts', 0], ['Images', 12], ['Tags', 3],
|
||||
]);
|
||||
for (const stats of [undefined, null, {}, {total_prompts:0, total_images:-1, total_tags:0},
|
||||
{total_prompts:'2', total_images:0, total_tags:0},
|
||||
{total_prompts:Infinity, total_images:0, total_tags:0},
|
||||
{total_prompts:0, total_images:1.5, total_tags:0}]) {
|
||||
assert.throws(() => summarizeStats(stats), /statistics/i);
|
||||
}
|
||||
test('Overview cards show the count of the page they name', async () => {
|
||||
const { summarizeOverviewCounts, OVERVIEW_COUNT_ROUTES } = await source;
|
||||
assert.deepEqual(OVERVIEW_COUNT_ROUTES, ['library', 'images', 'tags']);
|
||||
const counts = new Map([['library', 64], ['images', 122], ['tags', 306], ['favorites', 8]]);
|
||||
assert.deepEqual(summarizeOverviewCounts(counts), {
|
||||
cards: [['Prompts', 64], ['Images', 122], ['Tags', 306]], pending: false, failed: false, empty: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('Overview tells loading, failed and really empty counts apart', async () => {
|
||||
const { summarizeOverviewCounts } = await source;
|
||||
const nothing = summarizeOverviewCounts(new Map());
|
||||
assert.deepEqual([nothing.pending, nothing.failed, nothing.empty], [true, false, false]);
|
||||
assert.deepEqual(nothing.cards, [['Prompts', null], ['Images', null], ['Tags', null]]);
|
||||
// A slow Images read does not hold back the cards already read.
|
||||
const partial = summarizeOverviewCounts(new Map([['library', 0], ['tags', 0]]));
|
||||
assert.deepEqual(partial.cards, [['Prompts', 0], ['Images', null], ['Tags', 0]]);
|
||||
assert.deepEqual([partial.pending, partial.failed, partial.empty], [true, false, false]);
|
||||
const failed = summarizeOverviewCounts(new Map([['library', 2], ['images', null], ['tags', 3]]));
|
||||
assert.deepEqual([failed.pending, failed.failed, failed.empty], [false, true, false]);
|
||||
assert.deepEqual(failed.cards[1], ['Images', null]);
|
||||
const empty = summarizeOverviewCounts(new Map([['library', 0], ['images', 0], ['tags', 0]]));
|
||||
assert.deepEqual([empty.pending, empty.failed, empty.empty], [false, false, true]);
|
||||
});
|
||||
|
||||
test('router normalizes unknown routes, avoids duplicate renders, and disposes its listener', async () => {
|
||||
|
||||
Reference in New Issue
Block a user