882 lines
51 KiB
JavaScript
882 lines
51 KiB
JavaScript
import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage.mjs';
|
||
import { test, expect } from './fixtures.mjs';
|
||
|
||
// Collections (#331): the sidebar COLLECTIONS group (D136, concept index.html 28–31, app.js 336–339), the collection
|
||
// page `#/collection/<id>` with its Edit (D137, concept render a1186–1189) and saved views on a collection (D140).
|
||
const workspace = '/prompt_manager/workspace';
|
||
const API = '/prompt_manager/collections';
|
||
const RESTORE_APPLY_URL = '/prompt_manager/restore/apply';
|
||
const main = page => page.locator('#workspace-main');
|
||
const heading = page => main(page).getByRole('heading', { level: 1 });
|
||
const breadcrumb = page => page.locator('#workspace-breadcrumb');
|
||
const description = page => main(page).locator('.heading p.muted');
|
||
const missingNote = page => main(page).locator('.heading p.small');
|
||
const search = page => page.getByRole('searchbox', { name: 'Search workspace', exact: true });
|
||
const imageCards = page => page.locator('article[data-image-key]');
|
||
const collectionsNav = page => page.getByRole('navigation', { name: 'Collections', exact: true });
|
||
const collectionsLabel = page => page.locator('aside.sidebar .sidebar-label', { hasText: 'COLLECTIONS' });
|
||
const rows = page => collectionsNav(page).locator('a[data-route^="collection/"]');
|
||
const row = (page, name) => rows(page).filter({ hasText: name });
|
||
const savedNav = page => page.getByRole('navigation', { name: 'Saved views', exact: true });
|
||
const editButton = page => main(page).locator('.heading-actions > button').first();
|
||
const editDialog = page => page.getByRole('dialog', { name: 'Edit collection', exact: true });
|
||
const deleteDialog = page => page.getByRole('dialog', { name: 'Delete collection', exact: true });
|
||
const nameField = page => editDialog(page).getByRole('textbox', { name: 'Collection name', exact: true });
|
||
const toast = page => page.locator('#workspace-toast');
|
||
|
||
async function seed(request) {
|
||
const response = await request.post('/__test__/collections/seed');
|
||
await expect(response).toBeOK();
|
||
return response.json();
|
||
}
|
||
|
||
async function listed(request) {
|
||
const response = await request.get(API);
|
||
await expect(response).toBeOK();
|
||
return (await response.json()).collections;
|
||
}
|
||
|
||
test.beforeEach(async ({ page, request, browserName }) => {
|
||
await startCoverage(page, browserName);
|
||
await expect(await request.post('/__test__/reset')).toBeOK();
|
||
page.collectionErrors = [];
|
||
page.on('pageerror', error => page.collectionErrors.push(error.message));
|
||
// D51: deleting asks inside the dialog; no browser dialog (window.confirm) may ever open.
|
||
page.on('dialog', dialog => { page.collectionErrors.push(`browser ${dialog.type()}`); dialog.dismiss().catch(() => {}); });
|
||
});
|
||
|
||
test.afterEach(async ({ page, browserName }) => {
|
||
await saveCoverage(page, browserName);
|
||
expect(page.collectionErrors).toEqual([]);
|
||
});
|
||
|
||
test('a collection row opens its page', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Landscapes;
|
||
await page.setViewportSize({ width: 1600, height: 1100 });
|
||
await page.goto(`${workspace}#/images`);
|
||
// D136: name order, the listed-member count, no "PROPOSED" badge.
|
||
await expect(collectionsLabel(page)).toBeVisible();
|
||
await expect(collectionsLabel(page)).toHaveText('COLLECTIONS');
|
||
await expect(rows(page)).toHaveCount(2);
|
||
await expect(rows(page).nth(0)).toContainText('Experiments');
|
||
await expect(rows(page).nth(1)).toContainText('Landscapes');
|
||
await expect(row(page, 'Experiments').locator('.nav-count')).toHaveText('8');
|
||
await expect(row(page, 'Landscapes').locator('.nav-count')).toHaveText('12');
|
||
await expect(row(page, 'Landscapes')).toHaveAttribute('href', `#/collection/${id}`);
|
||
await expect(row(page, 'Landscapes').locator('svg')).toHaveCount(1);
|
||
await expect(page.locator('aside.sidebar .new-label')).toHaveCount(0);
|
||
await expect(imageCards(page)).toHaveCount(21);
|
||
|
||
await row(page, 'Landscapes').click();
|
||
await expect(page).toHaveURL(new RegExp(`#/collection/${id}$`));
|
||
await expect(heading(page)).toHaveText('Landscapes');
|
||
await expect(breadcrumb(page)).toHaveText('Collection');
|
||
await expect(page).toHaveTitle('Collection · Prompt Manager');
|
||
await expect(row(page, 'Landscapes')).toHaveAttribute('aria-current', 'page');
|
||
await expect(description(page)).toHaveText('Your generations, their prompts, and the next idea.');
|
||
await expect(imageCards(page)).toHaveCount(12);
|
||
for (const path of manifest.files.Landscapes) {
|
||
await expect(page.locator(`article[data-image-key$=":${path}"]`)).toHaveCount(1);
|
||
}
|
||
// MC3: the member whose file is gone is not listed and is counted in the note.
|
||
await expect(missingNote(page)).toHaveText('1 image in this collection is not in your current sources.');
|
||
|
||
// Every Images filter narrows the members.
|
||
await search(page).fill('landscape_03');
|
||
await expect(imageCards(page)).toHaveCount(1);
|
||
await search(page).fill('');
|
||
await expect(imageCards(page)).toHaveCount(12);
|
||
|
||
await row(page, 'Experiments').click();
|
||
await expect(heading(page)).toHaveText('Experiments');
|
||
await expect(imageCards(page)).toHaveCount(8);
|
||
await expect(missingNote(page)).toHaveCount(0);
|
||
});
|
||
|
||
test('without collections the group shows no label and no rows', async ({ page }) => {
|
||
await page.goto(`${workspace}#/images`);
|
||
await expect(imageCards(page).first()).toBeVisible();
|
||
await expect(collectionsNav(page)).toHaveCount(1);
|
||
await expect(rows(page)).toHaveCount(0);
|
||
await expect(collectionsLabel(page)).toBeHidden();
|
||
});
|
||
|
||
test('a collection page neither restores nor replaces the Images return state', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
await page.goto(`${workspace}#/images`);
|
||
await search(page).fill('experiment');
|
||
await expect(imageCards(page)).toHaveCount(8);
|
||
await row(page, 'Landscapes').click();
|
||
await expect(search(page)).toHaveValue('');
|
||
await expect(imageCards(page)).toHaveCount(12);
|
||
await search(page).fill('landscape_01');
|
||
await expect(imageCards(page)).toHaveCount(1);
|
||
await page.goto(`${workspace}#/images`);
|
||
await expect(search(page)).toHaveValue('experiment');
|
||
expect(manifest.collections.Landscapes.count).toBe(12);
|
||
});
|
||
|
||
test('Edit renames the open collection; a taken name stays in the dialog', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Landscapes;
|
||
await page.goto(`${workspace}#/collection/${id}`);
|
||
await expect(heading(page)).toHaveText('Landscapes');
|
||
await expect(editButton(page)).toHaveText('Edit');
|
||
await editButton(page).click();
|
||
await expect(nameField(page)).toHaveValue('Landscapes');
|
||
await expect(editDialog(page).locator('.dialog-actions button')).toHaveText(['Delete…', 'Cancel', 'Save']);
|
||
|
||
// CUJ 3: names are unique ignoring case.
|
||
await nameField(page).fill('experiments');
|
||
await editDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(editDialog(page).getByRole('alert')).toHaveText('A collection named “experiments” already exists.');
|
||
await expect(heading(page)).toHaveText('Landscapes');
|
||
|
||
await nameField(page).fill(' Landscape grids ');
|
||
await editDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(editDialog(page)).toHaveCount(0);
|
||
await expect(toast(page)).toHaveText('Collection renamed.');
|
||
await expect(heading(page)).toHaveText('Landscape grids');
|
||
await expect(breadcrumb(page)).toHaveText('Collection');
|
||
await expect(row(page, 'Landscape grids')).toHaveAttribute('aria-current', 'page');
|
||
await expect(rows(page).nth(1)).toContainText('Landscape grids');
|
||
expect((await listed(request)).map(item => item.name)).toEqual(['Experiments', 'Landscape grids']);
|
||
});
|
||
|
||
test('a failed rename keeps the dialog; a failed delete changes nothing', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Experiments;
|
||
await page.goto(`${workspace}#/collection/${id}`);
|
||
await expect(heading(page)).toHaveText('Experiments');
|
||
let failing = true;
|
||
await page.route(`**${API}/${id}`, route => failing
|
||
? route.fulfill({ status: 500, contentType: 'application/json', body: '{"success":false,"error":"The collection could not be changed."}' })
|
||
: route.fallback());
|
||
await editButton(page).click();
|
||
await nameField(page).fill('Tests');
|
||
await editDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(toast(page)).toHaveText('The collection could not be renamed. Try again.');
|
||
await expect(editDialog(page)).toBeVisible();
|
||
await expect(heading(page)).toHaveText('Experiments');
|
||
await editDialog(page).getByRole('button', { name: 'Delete…', exact: true }).click();
|
||
await expect(deleteDialog(page).locator('p')).toHaveText(
|
||
'Delete “Experiments”? Its 8 images stay where they are; only the collection is removed.');
|
||
await deleteDialog(page).getByRole('button', { name: 'Delete', exact: true }).click();
|
||
await expect(toast(page)).toHaveText('The collection could not be deleted. Try again.');
|
||
await expect(page).toHaveURL(new RegExp(`#/collection/${id}$`));
|
||
expect(await listed(request)).toHaveLength(2);
|
||
failing = false;
|
||
});
|
||
|
||
test('Delete removes the collection only after confirming, and leaves every file', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Landscapes;
|
||
await page.goto(`${workspace}#/collection/${id}`);
|
||
await editButton(page).click();
|
||
await editDialog(page).getByRole('button', { name: 'Delete…', exact: true }).click();
|
||
// The stored count: 12 listed members and the missing one.
|
||
await expect(deleteDialog(page).locator('p')).toHaveText(
|
||
'Delete “Landscapes”? Its 13 images stay where they are; only the collection is removed.');
|
||
await deleteDialog(page).getByRole('button', { name: 'Cancel', exact: true }).click();
|
||
await expect(deleteDialog(page)).toHaveCount(0);
|
||
expect(await listed(request)).toHaveLength(2);
|
||
|
||
await editButton(page).click();
|
||
await editDialog(page).getByRole('button', { name: 'Delete…', exact: true }).click();
|
||
await deleteDialog(page).getByRole('button', { name: 'Delete', exact: true }).click();
|
||
await expect(toast(page)).toHaveText('Collection deleted.');
|
||
await expect(page).toHaveURL(/#\/images$/);
|
||
await expect(heading(page)).toHaveText('Images');
|
||
await expect(rows(page)).toHaveCount(1);
|
||
await expect(imageCards(page)).toHaveCount(21);
|
||
expect((await listed(request)).map(item => item.name)).toEqual(['Experiments']);
|
||
});
|
||
|
||
test('an unknown collection says it no longer exists, with the way back to Images', async ({ page, request }) => {
|
||
await seed(request);
|
||
await page.goto(`${workspace}#/collection/999999`);
|
||
await expect(heading(page)).toHaveText('Collection');
|
||
await expect(breadcrumb(page)).toHaveText('Collection');
|
||
await expect(description(page)).toHaveText('This collection no longer exists.');
|
||
await expect(imageCards(page)).toHaveCount(0);
|
||
await main(page).getByRole('link', { name: 'Open Images', exact: true }).click();
|
||
await expect(page).toHaveURL(/#\/images$/);
|
||
await expect(imageCards(page)).toHaveCount(21);
|
||
});
|
||
|
||
test('a failed list says so and Retry reads it again', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Experiments;
|
||
let failing = true;
|
||
await page.route(`**${API}`, route => failing
|
||
? route.fulfill({ status: 500, contentType: 'application/json', body: '{"success":false,"error":"Collections could not be read."}' })
|
||
: route.fallback());
|
||
await page.goto(`${workspace}#/collection/${id}`);
|
||
await expect(description(page)).toHaveText('Collections could not be loaded. Your saved data has not changed.');
|
||
await expect(description(page)).toHaveAttribute('role', 'alert');
|
||
await expect(rows(page)).toHaveCount(0);
|
||
failing = false;
|
||
await main(page).getByRole('button', { name: 'Retry', exact: true }).click();
|
||
await expect(heading(page)).toHaveText('Experiments');
|
||
await expect(imageCards(page)).toHaveCount(8);
|
||
await expect(rows(page)).toHaveCount(2);
|
||
});
|
||
|
||
test('a saved view keeps its collection, and says so once the collection is deleted (MC6)', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const view = manifest.saved_view;
|
||
await page.goto(`${workspace}#/images`);
|
||
await savedNav(page).locator(`a[data-route="saved/${view.id}"]`).click();
|
||
await expect(heading(page)).toHaveText(view.name);
|
||
await expect(breadcrumb(page)).toHaveText(view.name);
|
||
await expect(imageCards(page)).toHaveCount(8);
|
||
// A saved view is not the collection page: no missing note.
|
||
await expect(missingNote(page)).toHaveCount(0);
|
||
|
||
// Save view on a collection page stores the collection.
|
||
await row(page, 'Landscapes').click();
|
||
await expect(imageCards(page)).toHaveCount(12);
|
||
await search(page).fill('landscape_1');
|
||
await expect(imageCards(page)).toHaveCount(3);
|
||
await main(page).getByRole('button', { name: 'Save view', exact: true }).click();
|
||
const save = page.getByRole('dialog', { name: 'Save this view', exact: true });
|
||
await save.getByRole('textbox', { name: 'View name', exact: true }).fill('Grid seed');
|
||
await save.getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(toast(page)).toHaveText('Saved view added to the sidebar.');
|
||
const views = (await (await request.get('/prompt_manager/saved-views')).json()).views;
|
||
const saved = views.find(item => item.name === 'Grid seed');
|
||
expect(saved.state).toMatchObject({ q: 'landscape_1', favorites: false, collection: String(manifest.collections.Landscapes.id) });
|
||
|
||
await expect(await request.delete(`${API}/${manifest.collections.Experiments.id}`)).toBeOK();
|
||
await page.goto(`${workspace}#/saved/${view.id}`);
|
||
await expect(heading(page)).toHaveText(view.name);
|
||
await expect(main(page).locator('.empty')).toHaveText('The collection this view uses was deleted.');
|
||
await expect(main(page).locator('.empty')).toBeVisible();
|
||
await expect(imageCards(page)).toHaveCount(0);
|
||
await expect(main(page).getByRole('button', { name: 'Retry', exact: true })).toBeHidden();
|
||
});
|
||
|
||
test('a collection deleted in another tab shows the deleted-collection page', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Experiments;
|
||
await page.goto(`${workspace}#/images`);
|
||
await expect(rows(page)).toHaveCount(2);
|
||
await expect(await request.delete(`${API}/${id}`)).toBeOK();
|
||
await row(page, 'Experiments').click();
|
||
await expect(description(page)).toHaveText('This collection no longer exists.');
|
||
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();
|
||
await expect(await request.post('/__test__/jobs/restart')).toBeOK();
|
||
try {
|
||
const backup = await request.get('/prompt_manager/backup');
|
||
await expect(backup).toBeOK();
|
||
const bytes = await backup.body();
|
||
const [experiments] = await listed(request);
|
||
await expect(await request.delete(`${API}/${experiments.id}`)).toBeOK();
|
||
await page.goto(`${workspace}#/system/backups`);
|
||
await expect(rows(page)).toHaveCount(1);
|
||
const chooser = page.waitForEvent('filechooser');
|
||
await page.getByRole('button', { name: 'Review restore…', exact: true }).click();
|
||
await (await chooser).setFiles({ name: 'prompts_backup_20261027_093000.db', mimeType: 'application/octet-stream', buffer: bytes });
|
||
// Wait for the restore job itself, as workspace-system-backups.spec.mjs does: under load a restore outlasts the
|
||
// 5 s expect default, and a restore still running after this test replaces the next tests' database (#520).
|
||
const applied = page.waitForResponse(response => response.url().endsWith(RESTORE_APPLY_URL));
|
||
await page.locator('dialog#modal[open]').getByRole('button', { name: 'Restore', exact: true }).click();
|
||
const { job } = await (await applied).json();
|
||
await expect(await request.get(`/prompt_manager/jobs/${job.id}/events`)).toBeOK();
|
||
await expect(toast(page)).toHaveText(/^Database restore finished · /);
|
||
// The list was read again without a reload.
|
||
await expect(rows(page)).toHaveCount(2);
|
||
await expect(row(page, 'Experiments').locator('.nav-count')).toHaveText('8');
|
||
} finally {
|
||
await request.post('/__test__/jobs/restart');
|
||
await request.post('/__test__/system/reset');
|
||
}
|
||
});
|
||
|
||
test('renaming a collection deleted in another tab says so and shows the deleted-collection page', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Experiments;
|
||
await page.goto(`${workspace}#/collection/${id}`);
|
||
await expect(heading(page)).toHaveText('Experiments');
|
||
await editButton(page).click();
|
||
await expect(await request.delete(`${API}/${id}`)).toBeOK();
|
||
await nameField(page).fill('Tests');
|
||
await editDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(toast(page)).toHaveText('That collection no longer exists.');
|
||
await expect(editDialog(page)).toHaveCount(0);
|
||
await expect(description(page)).toHaveText('This collection no longer exists.');
|
||
await expect(rows(page)).toHaveCount(1);
|
||
});
|
||
|
||
/** A Playwright hold on the collections list read: `held` fills as reads arrive; `release()` lets them all through. */
|
||
async function holdListReads(page, answer = route => route.fallback()) {
|
||
const held = [];
|
||
let open = false;
|
||
await page.route(`**${API}`, route => {
|
||
if (route.request().method() !== 'GET') return route.fallback();
|
||
if (open) return answer(route);
|
||
held.push(route);
|
||
return undefined;
|
||
});
|
||
return {
|
||
held,
|
||
async release() {
|
||
open = true;
|
||
for (const route of held.splice(0)) await answer(route).catch(() => {});
|
||
},
|
||
};
|
||
}
|
||
|
||
test('a list read superseded by another read still opens a collection made since (review M1)', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const listing = await (await request.get('/prompt_manager/images/output?mode=workspace&limit=1')).json();
|
||
const [file] = listing.images;
|
||
await page.goto(`${workspace}#/collection/${manifest.collections.Landscapes.id}`);
|
||
await expect(heading(page)).toHaveText('Landscapes');
|
||
// Another tab makes "Fresh"; this tab does not list it yet.
|
||
const added = await request.post(`${API}/items`, { data: { name: 'Fresh',
|
||
items: [{ root_index: file.root_index, root_scope: file.root_scope, path: file.path }] } });
|
||
await expect(added).toBeOK();
|
||
const fresh = (await added.json()).collection.id;
|
||
await expect(rows(page)).toHaveCount(2);
|
||
const hold = await holdListReads(page);
|
||
// A confirmed rename schedules the 300 ms count re-read; opening "Fresh" first reads the list once more. The
|
||
// timer's read supersedes that one while both are held. The rename's answer is held until "Fresh"'s read is, so
|
||
// the timer starts only then: with the answer let through at once, a slow host could fire the timer before the
|
||
// hash change, and the page would then wait on the timer's read alone (#459).
|
||
let releaseRename;
|
||
const renameHeld = new Promise(resolve => { releaseRename = resolve; });
|
||
await page.route(`**${API}/${manifest.collections.Landscapes.id}`, async route => {
|
||
if (route.request().method() !== 'PATCH') return route.fallback();
|
||
await renameHeld;
|
||
return route.fallback();
|
||
});
|
||
await editButton(page).click();
|
||
await nameField(page).fill('Landscape grids');
|
||
await editDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await page.evaluate(hash => { window.location.hash = hash; }, `#/collection/${fresh}`);
|
||
await expect.poll(() => hold.held.length).toBe(1);
|
||
releaseRename();
|
||
// The rename landed (the sidebar row has the new name), so the count read is now due.
|
||
await expect(row(page, 'Landscape grids')).toHaveCount(1);
|
||
await expect.poll(() => hold.held.length).toBe(2);
|
||
await expect(main(page)).not.toContainText('Collections could not be loaded.');
|
||
await hold.release();
|
||
await expect(heading(page)).toHaveText('Fresh');
|
||
await expect(imageCards(page)).toHaveCount(1);
|
||
});
|
||
|
||
test('a listing that finds the collection gone while the list still has it offers Retry (review M2)', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Experiments;
|
||
let gone = true;
|
||
await page.route('**/prompt_manager/images/output?*', route => gone && route.request().url().includes(`collection=${id}`)
|
||
? route.fulfill({ status: 404, contentType: 'application/json',
|
||
body: '{"success":false,"error":"Collection not found","code":"collection-not-found"}' })
|
||
: route.fallback());
|
||
await page.goto(`${workspace}#/images`);
|
||
await expect(rows(page)).toHaveCount(2);
|
||
// The shell's re-read is held, then fails: the page must not stay blank.
|
||
const hold = await holdListReads(page, route => route.fulfill({ status: 500, contentType: 'application/json',
|
||
body: '{"success":false,"error":"Collections could not be read."}' }));
|
||
await row(page, 'Experiments').click();
|
||
await expect.poll(() => hold.held.length).toBe(1);
|
||
await hold.release();
|
||
const retry = main(page).getByRole('button', { name: 'Retry', exact: true });
|
||
await expect(retry).toBeVisible();
|
||
await expect(main(page).locator('#gallery-continuation p')).toHaveText('Images could not be loaded. Retry or rescan the collection.');
|
||
gone = false;
|
||
await page.unroute(`**${API}`);
|
||
await retry.click();
|
||
await expect(imageCards(page)).toHaveCount(8);
|
||
|
||
// The list read again still lists it: the same Retry, never a blank page.
|
||
gone = true;
|
||
await page.evaluate(() => { window.location.hash = '#/images'; });
|
||
await expect(heading(page)).toHaveText('Images');
|
||
await row(page, 'Experiments').click();
|
||
await expect(retry).toBeVisible();
|
||
gone = false;
|
||
await retry.click();
|
||
await expect(imageCards(page)).toHaveCount(8);
|
||
});
|
||
|
||
test('the missing-members note follows the list read again (review L2)', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Landscapes;
|
||
let missing = null;
|
||
await page.route(`**${API}`, async route => {
|
||
if (route.request().method() !== 'GET' || missing === null) return route.fallback();
|
||
const response = await route.fetch();
|
||
const data = await response.json();
|
||
data.collections = data.collections.map(item => item.id === id ? { ...item, missing } : item);
|
||
return route.fulfill({ response, json: data });
|
||
});
|
||
await page.goto(`${workspace}#/collection/${id}`);
|
||
await expect(missingNote(page)).toHaveText('1 image in this collection is not in your current sources.');
|
||
const rename = async name => {
|
||
await editButton(page).click();
|
||
await nameField(page).fill(name);
|
||
await editDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(toast(page)).toHaveText('Collection renamed.');
|
||
};
|
||
missing = 3;
|
||
await rename('Landscapes A');
|
||
await expect(missingNote(page)).toHaveText('3 images in this collection are not in your current sources.');
|
||
missing = 0;
|
||
await rename('Landscapes B');
|
||
await expect(missingNote(page)).toHaveCount(0);
|
||
missing = 1;
|
||
await rename('Landscapes C');
|
||
await expect(missingNote(page)).toHaveText('1 image in this collection is not in your current sources.');
|
||
});
|
||
|
||
test('a rename that settles after its dialog closed leaves the next dialog open (review L1)', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Landscapes;
|
||
await page.goto(`${workspace}#/collection/${id}`);
|
||
await expect(imageCards(page)).toHaveCount(12);
|
||
let release;
|
||
const released = new Promise(resolve => { release = resolve; });
|
||
await page.route(`**${API}/${id}`, async route => {
|
||
if (route.request().method() !== 'PATCH') return route.fallback();
|
||
await released;
|
||
return route.fallback();
|
||
});
|
||
await editButton(page).click();
|
||
await nameField(page).fill('Later');
|
||
await editDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await page.keyboard.press('Escape');
|
||
await expect(editDialog(page)).toHaveCount(0);
|
||
await main(page).getByRole('button', { name: 'Save view', exact: true }).click();
|
||
const save = page.getByRole('dialog', { name: 'Save this view', exact: true });
|
||
await expect(save).toBeVisible();
|
||
release();
|
||
await expect(heading(page)).toHaveText('Later');
|
||
await expect(save).toBeVisible();
|
||
});
|
||
|
||
test('a page that says a collection or view could not be opened takes focus after a route change (review L3)', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
await page.goto(`${workspace}#/images`);
|
||
await expect(heading(page)).toHaveText('Images');
|
||
await page.evaluate(() => { window.location.hash = '#/collection/999999'; });
|
||
await expect(description(page)).toHaveText('This collection no longer exists.');
|
||
await expect(heading(page)).toBeFocused();
|
||
|
||
await page.route(`**${API}`, route => route.request().method() === 'GET'
|
||
? route.fulfill({ status: 500, contentType: 'application/json', body: '{"success":false,"error":"x"}' })
|
||
: route.fallback());
|
||
await page.evaluate(() => { window.location.hash = '#/images'; });
|
||
await expect(heading(page)).toHaveText('Images');
|
||
await page.evaluate(() => { window.location.hash = '#/collection/999998'; });
|
||
await expect(main(page).getByRole('button', { name: 'Retry', exact: true })).toBeVisible();
|
||
await expect(heading(page)).toBeFocused();
|
||
|
||
await page.route('**/prompt_manager/saved-views', route => route.request().method() === 'GET'
|
||
? route.fulfill({ status: 500, contentType: 'application/json', body: '{"success":false,"error":"x"}' })
|
||
: route.fallback());
|
||
// An id the seed never made (its one view is manifest.saved_view), whatever ids earlier tests used (#520).
|
||
await page.evaluate(id => { window.location.hash = `#/saved/${id}`; }, manifest.saved_view.id + 1000);
|
||
await expect(description(page)).toHaveText('Saved views could not be loaded. Your saved data has not changed.');
|
||
await expect(heading(page)).toBeFocused();
|
||
});
|
||
|
||
// #331 D138 (design §7): the selection bar's Add to collection and Remove from collection.
|
||
const bar = page => page.locator('#bulkbar > .bulkbar');
|
||
const barButton = (page, name) => bar(page).getByRole('button', { name, exact: true });
|
||
const addDialog = page => page.getByRole('dialog', { name: 'Add selected images to collection', exact: true });
|
||
const addField = page => addDialog(page).getByRole('combobox', { name: 'Collection name', exact: true });
|
||
const check = (page, file) => page.getByRole('checkbox', { name: `Select ${file}`, exact: true }).check();
|
||
const card = (page, path) => page.locator(`article[data-image-key$=":${path}"]`);
|
||
|
||
/** Add the checked images under `name` through the bar's dialog. */
|
||
async function addChecked(page, name) {
|
||
await barButton(page, 'Add to collection').click();
|
||
await expect(addDialog(page)).toBeVisible();
|
||
await addField(page).fill(name);
|
||
await addDialog(page).getByRole('button', { name: 'Add', exact: true }).click();
|
||
await expect(addDialog(page)).toHaveCount(0);
|
||
}
|
||
|
||
test('Add to collection creates a collection and adds to it ignoring case (CUJ 1–3)', async ({ page, request }) => {
|
||
await seed(request);
|
||
await page.goto(`${workspace}#/images`);
|
||
await expect(imageCards(page)).toHaveCount(21);
|
||
for (const file of ['loose.png', 'landscape_01.png', 'experiment_01.png']) await check(page, file);
|
||
await expect(bar(page).locator('button')).toHaveText(['Add tags', 'Auto-tag selected', 'Set category', 'Add to collection',
|
||
'Export metadata', 'Delete…', 'Clear']);
|
||
await barButton(page, 'Add to collection').click();
|
||
// The concept's simpleInput with Add (D138) and the existing names as suggestions.
|
||
await expect(addDialog(page).locator('label')).toHaveText('Collection name');
|
||
await expect(addField(page)).toHaveValue('');
|
||
await expect(addField(page)).toHaveAttribute('maxlength', '100');
|
||
await expect(addDialog(page).locator('datalist option')).toHaveCount(2);
|
||
expect(await addDialog(page).locator('datalist option').evaluateAll(options => options.map(option => option.value)))
|
||
.toEqual(['Experiments', 'Landscapes']);
|
||
await expect(addDialog(page).locator('.dialog-actions button')).toHaveText(['Cancel', 'Add']);
|
||
// An empty name keeps the dialog on its field.
|
||
await addDialog(page).getByRole('button', { name: 'Add', exact: true }).click();
|
||
await expect(addField(page)).toBeFocused();
|
||
await addField(page).fill(' LoRa testing ');
|
||
await addDialog(page).getByRole('button', { name: 'Add', exact: true }).click();
|
||
await expect(toast(page)).toHaveText('Added 3 images to “LoRa testing”.');
|
||
await expect(bar(page).locator('strong')).toHaveText('3 selected');
|
||
await expect(row(page, 'LoRa testing').locator('.nav-count')).toHaveText('3');
|
||
await expect(rows(page)).toHaveCount(3);
|
||
|
||
// CUJ 2: one of two already in it.
|
||
await bar(page).getByRole('button', { name: 'Clear', exact: true }).click();
|
||
await check(page, 'loose.png');
|
||
await check(page, 'landscape_02.png');
|
||
await addChecked(page, 'LoRa testing');
|
||
await expect(toast(page)).toHaveText('Added 1 image to “LoRa testing”. 1 was already in it.');
|
||
await expect(row(page, 'LoRa testing').locator('.nav-count')).toHaveText('4');
|
||
|
||
// CUJ 3: a case variant joins the same collection.
|
||
await bar(page).getByRole('button', { name: 'Clear', exact: true }).click();
|
||
await check(page, 'landscape_03.png');
|
||
await addChecked(page, 'lora TESTING');
|
||
await expect(toast(page)).toHaveText('Added 1 image to “LoRa testing”.');
|
||
await expect(rows(page)).toHaveCount(3);
|
||
await expect(row(page, 'LoRa testing').locator('.nav-count')).toHaveText('5');
|
||
const created = (await listed(request)).find(item => item.name === 'LoRa testing');
|
||
expect(created).toMatchObject({ count: 5, missing: 0 });
|
||
|
||
// Everything already in it.
|
||
await addChecked(page, 'LoRa testing');
|
||
await expect(toast(page)).toHaveText('All selected images were already in “LoRa testing”.');
|
||
});
|
||
|
||
test('Remove from collection takes the checked images off the open collection (CUJ 5)', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { id } = manifest.collections.Landscapes;
|
||
await page.goto(`${workspace}#/collection/${id}`);
|
||
await expect(imageCards(page)).toHaveCount(12);
|
||
await check(page, 'landscape_01.png');
|
||
await check(page, 'landscape_02.png');
|
||
await expect(bar(page).locator('button')).toHaveText(['Add tags', 'Auto-tag selected', 'Set category', 'Add to collection',
|
||
'Remove from collection', 'Export metadata', 'Delete…', 'Clear']);
|
||
await barButton(page, 'Remove from collection').click();
|
||
await expect(toast(page)).toHaveText('Removed 2 images from “Landscapes”.');
|
||
await expect(imageCards(page)).toHaveCount(10);
|
||
await expect(card(page, 'collections/landscape_01.png')).toHaveCount(0);
|
||
// The removed files left the listing, so the bar has nothing checked.
|
||
await expect(bar(page)).toHaveCount(0);
|
||
await expect(row(page, 'Landscapes').locator('.nav-count')).toHaveText('10');
|
||
// A checked file still listed stays checked across the reload.
|
||
await check(page, 'landscape_03.png');
|
||
await check(page, 'landscape_04.png');
|
||
await page.route(`**${API}/items`, async route => {
|
||
if (route.request().method() !== 'DELETE') return route.fallback();
|
||
// Remove only landscape_04; landscape_03 stays listed and checked.
|
||
const body = route.request().postDataJSON();
|
||
await route.continue({ postData: JSON.stringify({ ...body, items: body.items.filter(item => item.path.endsWith('_04.png')) }) });
|
||
});
|
||
await barButton(page, 'Remove from collection').click();
|
||
await expect(toast(page)).toHaveText('Removed 1 image from “Landscapes”.');
|
||
await expect(imageCards(page)).toHaveCount(9);
|
||
await expect(bar(page).locator('strong')).toHaveText('1 selected');
|
||
await expect(page.getByRole('checkbox', { name: 'Select landscape_03.png', exact: true })).toBeChecked();
|
||
// No file changed: the plain Images page still lists all of them.
|
||
await page.goto(`${workspace}#/images`);
|
||
await expect(imageCards(page)).toHaveCount(21);
|
||
});
|
||
|
||
test('Images without a collection filter has no Remove from collection', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
await page.goto(`${workspace}#/images`);
|
||
await check(page, 'loose.png');
|
||
await expect(barButton(page, 'Add to collection')).toBeVisible();
|
||
await expect(barButton(page, 'Remove from collection')).toHaveCount(0);
|
||
// A saved view on a collection has its filter active: Remove shows (design §5).
|
||
await page.goto(`${workspace}#/saved/${manifest.saved_view.id}`);
|
||
await expect(imageCards(page)).toHaveCount(8);
|
||
await check(page, 'experiment_01.png');
|
||
await barButton(page, 'Remove from collection').click();
|
||
await expect(toast(page)).toHaveText('Removed 1 image from “Experiments”.');
|
||
await expect(imageCards(page)).toHaveCount(7);
|
||
});
|
||
|
||
test('a failed add keeps the selection; a stale source reloads the images', async ({ page, request }) => {
|
||
await seed(request);
|
||
await page.goto(`${workspace}#/images`);
|
||
await expect(imageCards(page)).toHaveCount(21);
|
||
let reply = { status: 500, body: { success: false, error: 'The collection could not be changed.' } };
|
||
await page.route(`**${API}/items`, route => route.fulfill({ status: reply.status, contentType: 'application/json',
|
||
body: JSON.stringify(reply.body) }));
|
||
await check(page, 'loose.png');
|
||
await check(page, 'landscape_05.png');
|
||
await addChecked(page, 'Grids');
|
||
await expect(toast(page)).toHaveText('The images could not be added. Try again.');
|
||
await expect(bar(page).locator('strong')).toHaveText('2 selected');
|
||
await expect(rows(page)).toHaveCount(2);
|
||
|
||
reply = { status: 404, body: { success: false, code: 'files-not-found', error: 'x' } };
|
||
await addChecked(page, 'Grids');
|
||
await expect(toast(page)).toHaveText('None of the selected images could be found. Refresh the images and try again.');
|
||
await expect(bar(page).locator('strong')).toHaveText('2 selected');
|
||
|
||
reply = { status: 409, body: { success: false, code: 'source-changed', error: 'x' } };
|
||
const reloaded = page.waitForRequest(request => request.url().includes('/prompt_manager/gallery/subfolders'));
|
||
await addChecked(page, 'Grids');
|
||
await expect(toast(page)).toHaveText('The sources changed. Refresh the images and try again.');
|
||
await reloaded;
|
||
await expect(imageCards(page)).toHaveCount(21);
|
||
await expect(bar(page)).toHaveCount(0);
|
||
});
|
||
|
||
test('more than 500 or an unloaded "all matching" selection is refused with the cap', async ({ page, request }) => {
|
||
await seed(request);
|
||
await page.goto(`${workspace}#/images`);
|
||
await expect(imageCards(page)).toHaveCount(21);
|
||
// Every loaded image checked and the listing says more match: "Select all N matching" (#392, D135).
|
||
await page.route('**/prompt_manager/images/output?*', async route => {
|
||
// A next page never answers, so the listing stays at its loaded 21 of 900.
|
||
if (new URL(route.request().url()).searchParams.has('cursor')) return;
|
||
const response = await route.fetch();
|
||
const data = await response.json();
|
||
await route.fulfill({ response, json: { ...data, total: 900, has_more: true, next_cursor: 'more' } });
|
||
});
|
||
await page.reload();
|
||
await expect(imageCards(page)).toHaveCount(21);
|
||
await page.locator('#image-grid .image-open').first().focus();
|
||
await page.keyboard.press('ControlOrMeta+a');
|
||
await bar(page).getByRole('button', { name: 'Select all 900 matching', exact: true }).click();
|
||
await expect(bar(page).locator('strong')).toHaveText('All 900 matching selected');
|
||
await barButton(page, 'Add to collection').click();
|
||
await expect(toast(page)).toHaveText('Select at most 500 images.');
|
||
await expect(addDialog(page)).toHaveCount(0);
|
||
});
|
||
|
||
for (const theme of ['dark', 'light']) {
|
||
test(`the Add to collection dialog fits 390x844 ${theme}`, async ({ page, request }) => {
|
||
await seed(request);
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
await page.goto(`${workspace}#/images`);
|
||
await page.getByRole('combobox', { name: 'Theme', exact: true }).selectOption(theme);
|
||
await check(page, 'loose.png');
|
||
await expect(barButton(page, 'Add to collection')).toBeVisible();
|
||
await barButton(page, 'Add to collection').click();
|
||
await expect(addDialog(page)).toBeVisible();
|
||
const fits = await addDialog(page).evaluate(dialog => {
|
||
const box = dialog.getBoundingClientRect();
|
||
return box.left >= 0 && box.right <= window.innerWidth && dialog.scrollWidth <= dialog.clientWidth;
|
||
});
|
||
expect(fits).toBe(true);
|
||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
|
||
await addField(page).fill('Phone');
|
||
await addDialog(page).getByRole('button', { name: 'Add', exact: true }).click();
|
||
await expect(toast(page)).toHaveText('Added 1 image to “Phone”.');
|
||
await expect(row(page, 'Phone')).toBeVisible();
|
||
});
|
||
}
|
||
|
||
// #331 D139 (design §8): the shared inspector's Collections… in the Images side panel and in the viewer.
|
||
const panel = page => page.locator('#image-inspector');
|
||
const collectionsDialog = page => page.getByRole('dialog', { name: 'Collections', exact: true });
|
||
const choice = (page, name) => collectionsDialog(page).getByRole('checkbox', { name, exact: true });
|
||
const newCollection = page => collectionsDialog(page).getByRole('textbox', { name: 'New collection', exact: true });
|
||
|
||
async function membership(request, path) {
|
||
const [image] = (await (await request.get(`/prompt_manager/images/output?mode=workspace&q=${encodeURIComponent(path)}`)).json()).images;
|
||
const query = new URLSearchParams({ root_index: String(image.root_index), root_scope: image.root_scope, path: image.path });
|
||
const response = await request.get(`${API}/membership?${query}`);
|
||
await expect(response).toBeOK();
|
||
return (await response.json()).collection_ids;
|
||
}
|
||
|
||
test('Collections… in the side panel adds, removes and creates for one image (CUJ 6)', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { Landscapes, Experiments } = manifest.collections;
|
||
await page.goto(`${workspace}#/images`);
|
||
// The newest image, landscape_01, is the panel's.
|
||
await expect(panel(page).getByRole('heading', { level: 2 })).toHaveText('landscape_01.png');
|
||
const open = panel(page).getByRole('button', { name: 'Collections…', exact: true });
|
||
// #480 D159: Delete… follows Collections… at the end of the panel's actions.
|
||
await expect(panel(page).locator('.detail-footer > button').nth(-2)).toHaveText('Collections…');
|
||
await open.click();
|
||
await expect(collectionsDialog(page)).toBeVisible();
|
||
await expect(choice(page, 'Landscapes')).toBeChecked();
|
||
await expect(choice(page, 'Experiments')).not.toBeChecked();
|
||
await expect(collectionsDialog(page).locator('label.tag-suggestion')).toHaveText(['Experiments', 'Landscapes']);
|
||
await expect(collectionsDialog(page).locator('.dialog-actions button')).toHaveText(['Cancel', 'Save']);
|
||
await choice(page, 'Landscapes').uncheck();
|
||
await choice(page, 'Experiments').check();
|
||
await newCollection(page).fill('Grids');
|
||
await collectionsDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(collectionsDialog(page)).toHaveCount(0);
|
||
await expect(toast(page)).toHaveText('Collections updated.');
|
||
await expect(open).toBeFocused();
|
||
await expect(row(page, 'Grids').locator('.nav-count')).toHaveText('1');
|
||
await expect(row(page, 'Landscapes').locator('.nav-count')).toHaveText('11');
|
||
await expect(row(page, 'Experiments').locator('.nav-count')).toHaveText('9');
|
||
const grids = (await listed(request)).find(item => item.name === 'Grids');
|
||
expect((await membership(request, 'landscape_01')).sort()).toEqual([Experiments.id, grids.id].sort());
|
||
expect(Landscapes.id).not.toBe(grids.id);
|
||
// The Images page lists the same files; the dialog opened again shows the new state.
|
||
await expect(imageCards(page)).toHaveCount(21);
|
||
await open.click();
|
||
await expect(choice(page, 'Grids')).toBeChecked();
|
||
await expect(choice(page, 'Experiments')).toBeChecked();
|
||
await expect(choice(page, 'Landscapes')).not.toBeChecked();
|
||
// Save without a change sends nothing.
|
||
let writes = 0;
|
||
page.on('request', request => { if (request.url().includes(`${API}/items`)) writes++; });
|
||
await collectionsDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(collectionsDialog(page)).toHaveCount(0);
|
||
expect(writes).toBe(0);
|
||
});
|
||
|
||
test('Collections… in the viewer keeps the viewer until it closes (CUJ 7)', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
await page.goto(`${workspace}#/collection/${manifest.collections.Landscapes.id}`);
|
||
await expect(imageCards(page)).toHaveCount(12);
|
||
await page.getByRole('button', { name: 'Open landscape_01.png', exact: true }).click();
|
||
const viewer = page.locator('#viewer-dialog[open]');
|
||
await expect(viewer.locator('#viewer-title')).toContainText('landscape_01.png');
|
||
// #532: the arrows move through the collection from the first moment.
|
||
await page.keyboard.press('ArrowRight');
|
||
await expect(viewer.locator('#viewer-title')).toContainText('landscape_02.png');
|
||
await page.keyboard.press('ArrowLeft');
|
||
await expect(viewer.locator('#viewer-title')).toContainText('landscape_01.png');
|
||
// Review L7: no listing request while the viewer is open.
|
||
let listings = 0;
|
||
const countListing = request => { if (new URL(request.url()).pathname === '/prompt_manager/images/output') listings++; };
|
||
page.on('request', countListing);
|
||
await viewer.locator('#viewer-details').getByRole('button', { name: 'Collections…', exact: true }).click();
|
||
await expect(choice(page, 'Landscapes')).toBeChecked();
|
||
// Review L6: focus moves to the first checkbox once listed.
|
||
await expect(choice(page, 'Experiments')).toBeFocused();
|
||
await choice(page, 'Landscapes').uncheck();
|
||
await collectionsDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(toast(page)).toHaveText('Collections updated.');
|
||
await expect(collectionsDialog(page)).toHaveCount(0);
|
||
// The viewer stays on the image; the page keeps listing it while the viewer is open.
|
||
await expect(viewer.locator('#viewer-title')).toContainText('landscape_01.png');
|
||
await expect(row(page, 'Landscapes').locator('.nav-count')).toHaveText('11');
|
||
await expect(imageCards(page)).toHaveCount(12);
|
||
expect(listings, 'no listing request while the viewer is open').toBe(0);
|
||
page.off('request', countListing);
|
||
await viewer.getByRole('button', { name: 'Close image viewer', exact: true }).click();
|
||
await expect(viewer).toHaveCount(0);
|
||
await expect(imageCards(page)).toHaveCount(11);
|
||
await expect(card(page, 'collections/landscape_01.png')).toHaveCount(0);
|
||
});
|
||
|
||
test('a failed membership read says so and Retry reads it again', async ({ page, request }) => {
|
||
await seed(request);
|
||
let failing = true;
|
||
await page.route(`**${API}/membership?*`, route => failing
|
||
? route.fulfill({ status: 500, contentType: 'application/json', body: '{"success":false,"error":"Collections could not be read."}' })
|
||
: route.fallback());
|
||
await page.goto(`${workspace}#/images`);
|
||
await panel(page).getByRole('button', { name: 'Collections…', exact: true }).click();
|
||
await expect(collectionsDialog(page).getByRole('alert').first()).toHaveText('Collections could not be loaded.');
|
||
await expect(collectionsDialog(page).getByRole('checkbox')).toHaveCount(0);
|
||
await expect(collectionsDialog(page).getByRole('button', { name: 'Save', exact: true })).toHaveAttribute('aria-disabled', 'true');
|
||
failing = false;
|
||
await collectionsDialog(page).getByRole('button', { name: 'Retry', exact: true }).click();
|
||
await expect(choice(page, 'Landscapes')).toBeChecked();
|
||
await expect(collectionsDialog(page).getByRole('button', { name: 'Retry', exact: true })).toBeHidden();
|
||
// A failed write keeps the dialog with the current membership.
|
||
await page.route(`**${API}/items`, route => route.fulfill({ status: 500, contentType: 'application/json',
|
||
body: '{"success":false,"error":"The collection could not be changed."}' }));
|
||
await choice(page, 'Experiments').check();
|
||
await collectionsDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(collectionsDialog(page).getByRole('alert')).toHaveText('The collections could not be updated. Try again.');
|
||
await expect(choice(page, 'Experiments')).not.toBeChecked();
|
||
await expect(choice(page, 'Landscapes')).toBeChecked();
|
||
});
|
||
|
||
test('a file in no collection with no collections at all can start one', async ({ page }) => {
|
||
await page.goto(`${workspace}#/images`);
|
||
await panel(page).getByRole('button', { name: 'Collections…', exact: true }).click();
|
||
await expect(collectionsDialog(page)).toContainText('No collections yet. Name one below.');
|
||
await newCollection(page).fill('First');
|
||
await collectionsDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(toast(page)).toHaveText('Collections updated.');
|
||
await expect(rows(page)).toHaveCount(1);
|
||
await expect(row(page, 'First').locator('.nav-count')).toHaveText('1');
|
||
});
|
||
|
||
for (const theme of ['dark', 'light']) {
|
||
test(`the Collections dialog fits 390x844 ${theme}`, async ({ page, request }) => {
|
||
await seed(request);
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
await page.goto(`${workspace}#/images`);
|
||
await page.getByRole('combobox', { name: 'Theme', exact: true }).selectOption(theme);
|
||
await panel(page).getByRole('button', { name: 'Collections…', exact: true }).click();
|
||
await expect(choice(page, 'Landscapes')).toBeChecked();
|
||
const fits = await collectionsDialog(page).evaluate(dialog => {
|
||
const box = dialog.getBoundingClientRect();
|
||
return box.left >= 0 && box.right <= window.innerWidth && dialog.scrollWidth <= dialog.clientWidth;
|
||
});
|
||
expect(fits).toBe(true);
|
||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
|
||
});
|
||
}
|
||
|
||
// Review M2: on a collection page, a Save that removed the file from the open collection and then failed keeps its
|
||
// dialog and message; the listing is read again only once the dialog closes.
|
||
test('a partly failed Save from the side panel keeps its dialog until it closes', async ({ page, request }) => {
|
||
const manifest = await seed(request);
|
||
const { Experiments, Landscapes } = manifest.collections;
|
||
// experiment_01 is in both collections; Experiments sorts first, so its remove is sent before Landscapes'.
|
||
const [image] = (await (await request.get('/prompt_manager/images/output?mode=workspace&q=experiment_01')).json()).images;
|
||
const item = { root_index: image.root_index, root_scope: image.root_scope, path: image.path };
|
||
await expect(await request.post(`${API}/items`, { data: { collection_id: Landscapes.id, items: [item] } })).toBeOK();
|
||
await page.route(`**${API}/items`, route => {
|
||
const body = route.request().postDataJSON();
|
||
return route.request().method() === 'DELETE' && body.collection_id === Landscapes.id
|
||
? route.fulfill({ status: 500, contentType: 'application/json', body: '{"success":false,"error":"x"}' })
|
||
: route.fallback();
|
||
});
|
||
await page.goto(`${workspace}#/collection/${Experiments.id}`);
|
||
await expect(imageCards(page)).toHaveCount(8);
|
||
await expect(panel(page).getByRole('heading', { level: 2 })).toHaveText('experiment_01.png');
|
||
await panel(page).getByRole('button', { name: 'Collections…', exact: true }).click();
|
||
await expect(choice(page, 'Landscapes')).toBeChecked();
|
||
await choice(page, 'Experiments').uncheck();
|
||
await choice(page, 'Landscapes').uncheck();
|
||
await collectionsDialog(page).getByRole('button', { name: 'Save', exact: true }).click();
|
||
await expect(collectionsDialog(page).getByRole('alert')).toHaveText('The collections could not be updated. Try again.');
|
||
// The remove from Experiments went through; the dialog shows it and the page has not reloaded under it.
|
||
await expect(choice(page, 'Experiments')).not.toBeChecked();
|
||
await expect(choice(page, 'Landscapes')).toBeChecked();
|
||
await expect(imageCards(page)).toHaveCount(8);
|
||
await collectionsDialog(page).getByRole('button', { name: 'Cancel', exact: true }).click();
|
||
await expect(collectionsDialog(page)).toHaveCount(0);
|
||
await expect(imageCards(page)).toHaveCount(7);
|
||
await expect(card(page, 'collections/experiment_01.png')).toHaveCount(0);
|
||
});
|