Review metadata on the Overview's review row now opens #/review, the concept's go-review, instead of staying aria-disabled.
258 lines
16 KiB
JavaScript
258 lines
16 KiB
JavaScript
import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage.mjs';
|
||
import { test, expect } from './fixtures.mjs';
|
||
import { seedConcept } from './concept-inventory.mjs';
|
||
|
||
// System › Overview (#249 System S2 Task 6) on the concept-shaped real fixture: the metrics, rows, Statistics dialog and
|
||
// Health report read the server, never sample values (D93, D95, D96). The hooks are workspace_system_fixture.py's.
|
||
const workspace = '/prompt_manager/workspace';
|
||
const OVERVIEW_URL = '/prompt_manager/system/overview';
|
||
const THUMBNAILS_URL = '/prompt_manager/images/thumbnails/status';
|
||
const number = value => value.toLocaleString('en-US');
|
||
|
||
test.beforeEach(async ({ page, request, browserName }) => {
|
||
await startCoverage(page, browserName);
|
||
await seedConcept(request);
|
||
await expect(await request.post('/__test__/system/reset')).toBeOK();
|
||
await expect(await request.post('/__test__/jobs/restart')).toBeOK();
|
||
page.systemErrors = [];
|
||
page.on('pageerror', error => page.systemErrors.push(error.message));
|
||
});
|
||
|
||
test.afterEach(async ({ page, request, browserName }) => {
|
||
await saveCoverage(page, browserName);
|
||
await request.post('/__test__/release');
|
||
await request.post('/__test__/system/reset');
|
||
expect(page.systemErrors).toEqual([]);
|
||
});
|
||
|
||
async function serverJson(request, url) {
|
||
const response = await request.get(url);
|
||
await expect(response).toBeOK();
|
||
return response.json();
|
||
}
|
||
|
||
const layoutOf = page => page.locator('#workspace-main .settings-layout');
|
||
|
||
async function openOverview(page) {
|
||
await page.goto(`${workspace}#/system/overview`);
|
||
await expect(layoutOf(page).locator(':scope > .summary-metrics')).toBeVisible();
|
||
return layoutOf(page);
|
||
}
|
||
|
||
/** The rows of one Overview surface: `[h3, p, button]`. */
|
||
function rowsOf(layout, heading) {
|
||
return layout.locator('.two-col > section.surface').filter({ has: layout.page().getByRole('heading', { name: heading, exact: true }) })
|
||
.locator('.operation').evaluateAll(rows => rows.map(row => [row.querySelector('h3').textContent,
|
||
row.querySelector('p').textContent, row.querySelector('.row-actions button').textContent]));
|
||
}
|
||
|
||
test('overview shows real counts and Not checked', async ({ page, request }) => {
|
||
const writes = [];
|
||
page.on('request', item => { if (!['GET', 'HEAD', 'OPTIONS'].includes(item.method())) writes.push(item.url()); });
|
||
const overview = await serverJson(request, OVERVIEW_URL);
|
||
const thumbnails = await serverJson(request, THUMBNAILS_URL);
|
||
// The concept fixture: six positives, their saved negatives and 24 linked images, all with thumbnails.
|
||
expect([overview.library.positive_prompts, overview.images.indexed, thumbnails.pending]).toEqual([6, 24, 0]);
|
||
// I2: the Overview never walks the sources for thumbnails; the row reads the status route itself.
|
||
expect(overview).not.toHaveProperty('thumbnails');
|
||
// The fixture records no ambiguous file: the Needs review queue is empty.
|
||
expect(overview.needs_review).toEqual({ pending: 0 });
|
||
const layout = await openOverview(page);
|
||
expect(await layout.locator('.summary-metrics > .metric').evaluateAll(items => items.map(item =>
|
||
[item.querySelector('strong').textContent, item.querySelector('span').textContent]))).toEqual([
|
||
[number(overview.library.positive_prompts), 'Positive prompts'],
|
||
[number(overview.library.negative_variants), 'Negative variants'],
|
||
[number(overview.images.indexed), 'Images'],
|
||
['Not checked', 'Database integrity'],
|
||
]);
|
||
expect(await rowsOf(layout, 'Library health')).toEqual([
|
||
['0 ambiguous image links', 'Review metadata before assigning a positive / negative pair.', 'Review metadata'],
|
||
['Backup status', 'No retained backup record.', 'View backups'],
|
||
['No missing thumbnails', 'Generate only missing previews when necessary.', 'Review thumbnails'],
|
||
]);
|
||
expect(await rowsOf(layout, 'Workspace preferences')).toEqual([
|
||
['One display configuration', 'Grid and density settings are shared across views.', 'Appearance'],
|
||
['2 output sources', 'Monitoring and enrichment share one source list.', 'Manage sources'],
|
||
['Statistics', 'Runs and ratings by category, with library totals.', 'Explore'],
|
||
]);
|
||
expect(writes).toEqual([]);
|
||
});
|
||
|
||
test('Run health checks opens Health report and updates metric 4', async ({ page, request }) => {
|
||
await page.goto(`${workspace}#/system/maintenance`);
|
||
const layout = layoutOf(page);
|
||
await expect(layout.locator(':scope > .notice h2')).toHaveText('Check first. Review changes. Then apply.');
|
||
// The rest of Maintenance is still the existing tools' (D94).
|
||
await expect(layout.locator(':scope > section.handoff h2')).toHaveText('Your current tools are ready');
|
||
const run = layout.getByRole('button', { name: 'Run health checks', exact: true });
|
||
await run.click();
|
||
const dialog = page.locator('dialog#modal[open]');
|
||
await expect(dialog.locator('#modal-title')).toHaveText('Health report');
|
||
const { report } = await serverJson(request, '/prompt_manager/system/health');
|
||
expect(report.status).toBe('incomplete');
|
||
expect(await dialog.locator('.operation-list > .operation').evaluateAll(rows => rows.map(row =>
|
||
[row.querySelector('h3').textContent, row.querySelector('span.chip').textContent]))).toEqual([
|
||
['Integrity check', 'Passed'], ['Foreign keys & pair references', 'Passed'], ['Canonical hashes', 'Passed'],
|
||
['Pair uniqueness', 'Passed'], ['Missing files · output', 'Passed'], ['Missing files · input', 'Passed'],
|
||
['Ambiguous metadata', 'Not available yet'], ['Consistency', 'Passed'],
|
||
]);
|
||
await expect(dialog.locator('#modal-body > p.small')).toHaveText(/^Checked .+\. Some checks could not run, so this report is incomplete\.$/);
|
||
await expect(dialog.locator('.dialog-actions button')).toHaveText(['Close', 'Review missing links']);
|
||
await dialog.getByRole('button', { name: 'Close', exact: true }).click();
|
||
await expect(page.locator('dialog#modal[open]')).toHaveCount(0);
|
||
await expect(run).toBeFocused();
|
||
await page.locator('#workspace-main .section-nav').getByRole('button', { name: 'Overview', exact: true }).click();
|
||
await expect(layout.locator('.metric').nth(3).locator('strong')).toHaveText('Incomplete');
|
||
|
||
// A missing file and an unmounted source: the report counts the readable sources and names the other one.
|
||
await expect(await request.post('/__test__/system/missing-file')).toBeOK();
|
||
await expect(await request.post('/__test__/system/missing-source')).toBeOK();
|
||
await page.locator('#workspace-main .section-nav').getByRole('button', { name: 'Maintenance', exact: true }).click();
|
||
await layout.getByRole('button', { name: 'Run health checks', exact: true }).click();
|
||
await expect(dialog.locator('.operation').filter({ hasText: 'Missing files' }).locator('.chip'))
|
||
.toHaveText(['1 link needs review', 'Passed', 'Not available']);
|
||
await expect(dialog.locator('.operation h3').filter({ hasText: 'Missing files' }))
|
||
.toHaveText(['Missing files · output', 'Missing files · input', 'Missing files · unmounted-drive']);
|
||
// Review missing links goes to Maintenance, where the cleanup's existing tool is linked.
|
||
await dialog.getByRole('button', { name: 'Review missing links', exact: true }).click();
|
||
await expect(page.locator('dialog#modal[open]')).toHaveCount(0);
|
||
await expect(page).toHaveURL(`${workspace}#/system/maintenance`);
|
||
await page.locator('#workspace-main .section-nav').getByRole('button', { name: 'Overview', exact: true }).click();
|
||
await expect(layout.locator('.metric').nth(3).locator('strong')).toHaveText('1 issue');
|
||
});
|
||
|
||
test('a health check that fails says so and keeps the last result', async ({ page, request }) => {
|
||
await expect(await request.post('/__test__/system/fail', { data: { path: '/prompt_manager/system/health' } })).toBeOK();
|
||
await page.goto(`${workspace}#/system/maintenance`);
|
||
const run = layoutOf(page).getByRole('button', { name: 'Run health checks', exact: true });
|
||
await run.click();
|
||
await expect(page.locator('#workspace-toast')).toHaveText('The health check could not finish. Nothing was changed.');
|
||
await expect(page.locator('dialog#modal[open]')).toHaveCount(0);
|
||
await expect(run).not.toHaveAttribute('aria-disabled', 'true');
|
||
await openOverview(page);
|
||
await expect(layoutOf(page).locator('.metric').nth(3).locator('strong')).toHaveText('Not checked');
|
||
});
|
||
|
||
test('thumbnail row reflects the status endpoint', async ({ page, request }) => {
|
||
await expect(await request.post('/__test__/system/unthumbnailed', { data: { count: 3 } })).toBeOK();
|
||
const status = await serverJson(request, '/prompt_manager/images/thumbnails/status');
|
||
expect(status.pending).toBe(3);
|
||
const layout = await openOverview(page);
|
||
const row = layout.locator('.operation').filter({ has: page.getByRole('button', { name: 'Review thumbnails', exact: true }) });
|
||
await expect(row.locator('h3')).toHaveText('3 missing thumbnails');
|
||
// Until the thumbnail review dialog (S5), Review thumbnails opens Sources.
|
||
await row.getByRole('button', { name: 'Review thumbnails', exact: true }).click();
|
||
await expect(page).toHaveURL(`${workspace}#/system/sources`);
|
||
});
|
||
|
||
test('the thumbnail row paints when its own status read answers', async ({ page, request }) => {
|
||
const held = async () => (await serverJson(request, '/__test__/hold')).held[THUMBNAILS_URL] ?? 0;
|
||
await expect(await request.post('/__test__/hold', { data: { path: THUMBNAILS_URL } })).toBeOK();
|
||
const layout = await openOverview(page);
|
||
const row = layout.locator('.operation').filter({ has: page.getByRole('button', { name: 'Review thumbnails', exact: true }) });
|
||
// The other values do not wait for the walk of every source.
|
||
await expect(row.locator('h3')).toHaveText('Checking thumbnails…');
|
||
await expect(layout.locator('.summary-metrics > .metric')).toHaveCount(4);
|
||
await expect.poll(held).toBe(1);
|
||
await expect(await request.post('/__test__/release')).toBeOK();
|
||
await expect(row.locator('h3')).toHaveText('No missing thumbnails');
|
||
});
|
||
|
||
test('a failed thumbnail read is unavailable, never zero', async ({ page }) => {
|
||
await page.route(`**${THUMBNAILS_URL}`, route => route.fulfill({ status: 500, contentType: 'application/json',
|
||
body: JSON.stringify({ success: false, error: 'Injected failure' }) }));
|
||
const layout = await openOverview(page);
|
||
const row = layout.locator('.operation').filter({ has: page.getByRole('button', { name: 'Review thumbnails', exact: true }) });
|
||
await expect(row.locator('h3')).toHaveText('Thumbnail status unavailable');
|
||
await expect(layout.getByRole('alert')).toHaveCount(0);
|
||
await expect(layout.locator('.metric').nth(2).locator('strong')).toHaveText('24');
|
||
});
|
||
|
||
test('ambiguous row reads the pending count and Review metadata opens Needs review', async ({ page }) => {
|
||
const layout = await openOverview(page);
|
||
const row = layout.locator('.operation').filter({ hasText: 'ambiguous image links' });
|
||
await expect(row.locator('h3')).toHaveText('0 ambiguous image links');
|
||
await expect(row.locator('p')).toHaveText('Review metadata before assigning a positive / negative pair.');
|
||
const review = row.getByRole('button', { name: 'Review metadata', exact: true });
|
||
await expect(review).not.toHaveAttribute('aria-disabled');
|
||
await review.click();
|
||
// The concept's go-review: the Needs review page.
|
||
await expect(page).toHaveURL(`${workspace}#/review`);
|
||
await expect(page.getByRole('heading', { name: 'Metadata review', exact: true })).toBeVisible();
|
||
});
|
||
|
||
test('View backups / Appearance / Manage sources switch sections', async ({ page }) => {
|
||
for (const [label, section, pressed] of [['View backups', 'backups', 'Backup & export'], ['Appearance', 'appearance', 'Appearance'],
|
||
['Manage sources', 'sources', 'Sources & scans']]) {
|
||
const layout = await openOverview(page);
|
||
await layout.getByRole('button', { name: label, exact: true }).click();
|
||
await expect(page).toHaveURL(`${workspace}#/system/${section}`);
|
||
await expect(page.locator('#workspace-main .section-nav [aria-pressed="true"]')).toHaveText(pressed);
|
||
await expect(layout.locator(':scope > .summary-metrics')).toHaveCount(0);
|
||
}
|
||
});
|
||
|
||
test('statistics dialog lists runs by category and library totals', async ({ page, request }) => {
|
||
const statistics = await serverJson(request, '/prompt_manager/system/statistics');
|
||
expect(statistics.by_category.length).toBeGreaterThan(1);
|
||
const layout = await openOverview(page);
|
||
const explore = layout.getByRole('button', { name: 'Explore', exact: true });
|
||
await explore.click();
|
||
const dialog = page.locator('dialog#modal[open]');
|
||
await expect(dialog.locator('#modal-title')).toHaveText('Usage statistics');
|
||
const tables = dialog.locator('#modal-body > table');
|
||
await expect(tables.locator('caption')).toHaveText(['Runs by category', 'Library totals']);
|
||
await expect(tables.first().locator('thead th')).toHaveText(['Category', 'Runs', 'Avg. rating']);
|
||
const cells = table => table.locator('tbody tr').evaluateAll(rows => rows.map(row => [...row.cells].map(cell => cell.textContent)));
|
||
expect(await cells(tables.first())).toEqual(statistics.by_category.map(row => [row.category ?? 'Uncategorized',
|
||
number(row.runs), row.average_rating === null ? 'Not rated' : `${number(row.average_rating)} / 5`]));
|
||
expect(await cells(tables.nth(1))).toEqual([['Prompts', number(statistics.totals.total_prompts)],
|
||
['Tags', number(statistics.totals.total_tags)], ['Images', number(statistics.totals.total_images)],
|
||
['Rated images', number(statistics.rated_images)]]);
|
||
await page.keyboard.press('Escape');
|
||
await expect(explore).toBeFocused();
|
||
// A statistics read that fails says so; the Overview stays.
|
||
await expect(await request.post('/__test__/system/fail', { data: { path: '/prompt_manager/system/statistics' } })).toBeOK();
|
||
await explore.click();
|
||
await expect(page.locator('#workspace-toast')).toHaveText('Statistics could not be loaded. Your saved data has not changed.');
|
||
await expect(page.locator('dialog#modal[open]')).toHaveCount(0);
|
||
});
|
||
|
||
test('load failure shows Retry and no zeros', async ({ page, request }) => {
|
||
await expect(await request.post('/__test__/system/fail', { data: { path: OVERVIEW_URL } })).toBeOK();
|
||
await page.goto(`${workspace}#/system/overview`);
|
||
const layout = layoutOf(page);
|
||
await expect(layout.getByRole('alert')).toHaveText('The overview could not be loaded. Your saved data has not changed.');
|
||
await expect(layout.locator('.metric')).toHaveCount(0);
|
||
await expect(layout).not.toContainText('0');
|
||
await layout.getByRole('button', { name: 'Retry', exact: true }).click();
|
||
await expect(layout.locator('.summary-metrics > .metric')).toHaveCount(4);
|
||
await expect(layout.getByRole('alert')).toHaveCount(0);
|
||
});
|
||
|
||
test('out-of-order overview responses never repaint a newer section', async ({ page, request }) => {
|
||
const held = async () => (await serverJson(request, '/__test__/hold')).held[OVERVIEW_URL] ?? 0;
|
||
await expect(await request.post('/__test__/hold', { data: { path: OVERVIEW_URL } })).toBeOK();
|
||
await page.goto(`${workspace}#/system/overview`);
|
||
const layout = layoutOf(page);
|
||
await expect(layout.getByRole('status')).toHaveText('Loading the overview…');
|
||
await expect.poll(held).toBe(1);
|
||
// Leave for Appearance, come back (a second read), then leave again before either answers.
|
||
const nav = page.locator('#workspace-main .section-nav');
|
||
await nav.getByRole('button', { name: 'Appearance', exact: true }).click();
|
||
await nav.getByRole('button', { name: 'Overview', exact: true }).click();
|
||
await expect.poll(held).toBe(2);
|
||
await nav.getByRole('button', { name: 'Sources & scans', exact: true }).click();
|
||
await expect(await request.post('/__test__/release')).toBeOK();
|
||
await expect.poll(held).toBe(0);
|
||
// Both late answers reached a section that had left: Sources keeps its own body.
|
||
await expect(layout.locator(':scope > section.handoff')).toBeVisible();
|
||
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
||
await expect(layout.locator('.summary-metrics')).toHaveCount(0);
|
||
expect(await layout.evaluate(node => [...node.children].map(child => child.className))).toEqual(['surface handoff', 'save-row']);
|
||
// Back on Overview, one fresh read paints one set of metrics.
|
||
await nav.getByRole('button', { name: 'Overview', exact: true }).click();
|
||
await expect(layout.locator('.summary-metrics')).toHaveCount(1);
|
||
await expect(layout.locator('.two-col')).toHaveCount(1);
|
||
});
|