483 lines
31 KiB
JavaScript
483 lines
31 KiB
JavaScript
import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage.mjs';
|
||
import { test, expect } from './fixtures.mjs';
|
||
import { seedConcept } from './concept-inventory.mjs';
|
||
|
||
// System › Integrations (#249 System S10 Task 27) on the concept-shaped fixture with a fake LoraManager install
|
||
// (workspace_integrations_fixture.py): the D106 ComfyUI rows and renderer status, the LoRA rows and the D114 key, saved
|
||
// through the one Save row, and the D107 import review run as an Activity job. No test reaches the network: CivitAI
|
||
// downloads are answered by the fixture's stub opener.
|
||
const workspace = '/prompt_manager/workspace';
|
||
const PREVIEW_URL = '/prompt_manager/lora/import/preview';
|
||
const APPLY_URL = '/prompt_manager/lora/import/apply';
|
||
const ENABLE_URL = '/prompt_manager/lora/enable';
|
||
const KEPT = 'The import failed; the previous LoRA imports were kept. Images already downloaded stay in the image cache.';
|
||
|
||
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__/integrations/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__/integrations/gate', { data: { hold: false } });
|
||
await request.post('/__test__/release');
|
||
await request.post('/__test__/jobs/restart');
|
||
await request.post('/__test__/integrations/reset');
|
||
await request.post('/__test__/system/reset');
|
||
expect(page.systemErrors).toEqual([]);
|
||
});
|
||
|
||
const toast = page => page.locator('#workspace-toast');
|
||
const layoutOf = page => page.locator('#workspace-main .settings-layout');
|
||
const dialog = page => page.locator('dialog#modal[open]');
|
||
const status = page => page.locator('#settings-status');
|
||
const save = page => page.getByRole('button', { name: 'Save preferences', exact: true });
|
||
const settingOf = (page, label) => layoutOf(page).locator('.setting-row').filter({ has: page.locator(`label:text-is("${label}")`) });
|
||
|
||
async function serverJson(request, url) {
|
||
const response = await request.get(url);
|
||
await expect(response).toBeOK();
|
||
return response.json();
|
||
}
|
||
|
||
async function lora(request, data = {}) {
|
||
await expect(await request.post('/__test__/integrations/lora-manager', { data })).toBeOK();
|
||
}
|
||
|
||
async function openIntegrations(page) {
|
||
// A fresh page each time: the drafts live for the page session (Q10), and a hash-only goto would not reload.
|
||
if (page.url().endsWith('#/system/integrations')) await page.reload();
|
||
else await page.goto(`${workspace}#/system/integrations`);
|
||
await expect(layoutOf(page).locator(':scope > section.surface')).toHaveCount(2);
|
||
// The LoRA rows are painted once the server's LoRA state was read.
|
||
await expect(settingOf(page, 'LoraManager integration').locator('input')).toBeEnabled();
|
||
// The renderer line settles first: its new text moves the rows below, so no click lands mid-shift.
|
||
await expect(settingOf(page, 'Host compatibility').getByRole('status')).not.toHaveText('Looking for open ComfyUI windows…');
|
||
return layoutOf(page);
|
||
}
|
||
|
||
/** Enable the integration (and optionally its trigger words) through the Save row. */
|
||
async function enable(page, { triggers = false } = {}) {
|
||
await settingOf(page, 'LoraManager integration').locator('input').check();
|
||
if (triggers) await settingOf(page, 'Automatically include trigger words').locator('input').check();
|
||
await save(page).click();
|
||
await expect(status(page)).toHaveText('Saved to the server configuration.');
|
||
}
|
||
|
||
/** The open review's `[dt, dd]` rows. */
|
||
const gridOf = page => dialog(page).locator('dl.meta-grid > div').evaluateAll(items => items.map(item =>
|
||
[item.querySelector('dt').textContent, item.querySelector('dd').textContent]));
|
||
|
||
async function ended(request, id) {
|
||
await expect.poll(async () => (await serverJson(request, `/prompt_manager/jobs/${id}`)).job.state).not.toBe('running');
|
||
return (await serverJson(request, `/prompt_manager/jobs/${id}`)).job;
|
||
}
|
||
|
||
/** A second page of this origin that serves a fake ComfyUI window on the send bridge. */
|
||
async function fakeComfyWindow(context, renderer) {
|
||
const host = await context.newPage();
|
||
await host.goto(`${workspace}#/overview`);
|
||
await host.evaluate(async drawn => {
|
||
const { createBridgeHost } = await import('/prompt_manager/js/shared/toolkit/prompt-bridge.js');
|
||
window.fakeHost = createBridgeHost({ namespace: '/', origin: location.origin, label: 'PromptManager — ComfyUI',
|
||
adapter: { listTargets: () => (drawn ? [{ id: '7', label: 'Prompt Manager', type: 'PromptManager', roles: ['positive'],
|
||
renderer: drawn }] : []), preview() { throw new Error('unused'); }, apply() { throw new Error('unused'); } } });
|
||
}, renderer);
|
||
return host;
|
||
}
|
||
|
||
test('send destination offers Ask each time; unsupported options disabled with the reason (D106)', async ({ page }) => {
|
||
const layout = await openIntegrations(page);
|
||
expect(await layout.locator(':scope > section.surface > h2').allTextContents()).toEqual(['ComfyUI', 'LoRA & resources']);
|
||
const send = settingOf(page, 'Default send destination');
|
||
expect(await send.locator('option').evaluateAll(options => options.map(option => [option.textContent, option.disabled])))
|
||
.toEqual([['Ask each time', false], ['Selected Prompt Manager node', true], ['Create a Prompt Manager (+/-) node', true]]);
|
||
await expect(send.locator('select')).toHaveValue('Ask each time');
|
||
await expect(send.locator('p.small')).toHaveText(
|
||
'Sending asks when there is more than one ComfyUI window or node, so “Ask each time” is the only option.');
|
||
const renderer = settingOf(page, 'Host compatibility');
|
||
expect(await renderer.locator('option').evaluateAll(options => options.map(option => [option.textContent, option.disabled])))
|
||
.toEqual([['Follow host renderer', false], ['Classic', true], ['Nodes 2.0', true]]);
|
||
await expect(renderer.locator('p.small').first()).toHaveText('Prompt Manager nodes always follow ComfyUI’s own renderer setting.');
|
||
const back = settingOf(page, 'Return to canvas after send');
|
||
await expect(back.locator('input')).toBeDisabled();
|
||
await expect(back.locator('input')).not.toBeChecked();
|
||
await expect(back.locator('p.small')).toHaveText('Not available: sending applies the text in ComfyUI and keeps this page open.');
|
||
// Nothing on this surface is ever drafted: the Save row stays clean.
|
||
await expect(status(page)).toHaveText('No unsaved changes.');
|
||
await expect(save(page)).toHaveAttribute('aria-disabled', 'true');
|
||
});
|
||
|
||
test('host compatibility shows the detected renderer as status (D106)', async ({ page, context }) => {
|
||
await openIntegrations(page);
|
||
const line = settingOf(page, 'Host compatibility').getByRole('status');
|
||
await expect(line).toHaveText('No ComfyUI window is open in this browser, so its renderer is not known.');
|
||
const host = await fakeComfyWindow(context, 'nodes2');
|
||
try {
|
||
await page.reload();
|
||
await expect(line).toHaveText('Detected: Nodes 2.0 (1 ComfyUI window).');
|
||
await host.evaluate(() => window.fakeHost.dispose());
|
||
await host.evaluate(async () => {
|
||
const { createBridgeHost } = await import('/prompt_manager/js/shared/toolkit/prompt-bridge.js');
|
||
window.fakeHost = createBridgeHost({ namespace: '/', origin: location.origin, label: 'PromptManager — ComfyUI',
|
||
adapter: { listTargets: () => [], preview() {}, apply() {} } });
|
||
});
|
||
await page.reload();
|
||
await expect(line).toHaveText('ComfyUI is open; add a Prompt Manager node to see which renderer it uses.');
|
||
} finally {
|
||
await host.close();
|
||
}
|
||
});
|
||
|
||
test('LoRA rows show detected / enabled / cache-loaded separately', async ({ page, request }) => {
|
||
await openIntegrations(page);
|
||
const integration = settingOf(page, 'LoraManager integration');
|
||
const triggers = settingOf(page, 'Automatically include trigger words');
|
||
await expect(integration.locator('p').first()).toHaveText('Not detected in ComfyUI’s custom_nodes folders.');
|
||
await expect(integration.locator('input')).not.toBeChecked();
|
||
await expect(triggers.locator('p.small')).toHaveText('Trigger words are not loaded.');
|
||
// Enabling what is not installed is refused by the server: the toggle stays highlighted, nothing was saved.
|
||
await integration.locator('input').check();
|
||
await save(page).click();
|
||
await expect(status(page)).toHaveText('Some changes were not saved. Review the highlighted settings.');
|
||
await expect(integration.locator('input')).toHaveAttribute('aria-invalid', 'true');
|
||
expect((await serverJson(request, '/__test__/integrations/state')).enabled).toBe(false);
|
||
|
||
await lora(request);
|
||
await openIntegrations(page);
|
||
await expect(integration.locator('p').first()).toHaveText('Detected: custom_nodes/ComfyUI-Lora-Manager.');
|
||
await expect(integration.locator('input')).not.toBeChecked();
|
||
await enable(page, { triggers: true });
|
||
await expect(integration.locator('input')).toBeChecked();
|
||
await expect(triggers.locator('p.small')).toHaveText('Trigger words are loaded.');
|
||
expect(await serverJson(request, '/__test__/integrations/state')).toMatchObject({ enabled: true, trigger_words: true });
|
||
// A reload reads the saved state back.
|
||
await openIntegrations(page);
|
||
await expect(integration.locator('input')).toBeChecked();
|
||
await expect(triggers.locator('input')).toBeChecked();
|
||
await expect(status(page)).toHaveText('No unsaved changes.');
|
||
});
|
||
|
||
test('LoraManager path row shows the detected path read-only, as legacy (D106)', async ({ page, request }) => {
|
||
await openIntegrations(page);
|
||
const row = settingOf(page, 'LoraManager path');
|
||
const input = row.locator('input');
|
||
await expect(input).toHaveAttribute('readonly', '');
|
||
await expect(input).toHaveValue('');
|
||
await expect(input).toHaveAttribute('placeholder', 'Not detected');
|
||
await lora(request);
|
||
await openIntegrations(page);
|
||
await expect(input).toHaveValue('custom_nodes/ComfyUI-Lora-Manager');
|
||
await input.focus();
|
||
await page.keyboard.type('elsewhere');
|
||
await expect(input).toHaveValue('custom_nodes/ComfyUI-Lora-Manager');
|
||
await expect(status(page)).toHaveText('No unsaved changes.');
|
||
});
|
||
|
||
test('key never prefilled; blank keeps; Clear saved key clears; saved through Save preferences (D114)', async ({ page, request }) => {
|
||
const enables = [];
|
||
page.on('request', item => { if (item.url().endsWith(ENABLE_URL)) enables.push(item.postDataJSON()); });
|
||
await lora(request);
|
||
await openIntegrations(page);
|
||
const key = settingOf(page, 'CivitAI API key');
|
||
const field = key.locator('input[type="password"]');
|
||
const clear = key.getByRole('button', { name: 'Clear saved key', exact: true });
|
||
await expect(field).toHaveValue('');
|
||
await expect(key.locator('p.small')).toHaveText('No key saved.');
|
||
await expect(clear).toHaveCount(0);
|
||
// A typed key is dropped when the section is left: the field comes back empty with nothing hidden to save (N4).
|
||
await field.fill('sk-dropped');
|
||
await expect(status(page)).toHaveText('Unsaved changes.');
|
||
await page.locator('#workspace-main .section-nav').getByRole('button', { name: 'Appearance', exact: true }).click();
|
||
await page.locator('#workspace-main .section-nav').getByRole('button', { name: 'Integrations', exact: true }).click();
|
||
await expect(status(page)).toHaveText('No unsaved changes.');
|
||
await expect(key.locator('p.small')).toHaveText('No key saved.');
|
||
// Typing a key drafts it; Save sends it once through /lora/enable with the settings revision.
|
||
await field.fill('sk-test-key');
|
||
await expect(key.locator('p.small')).toHaveText('The new key replaces the saved one when you save.');
|
||
await expect(status(page)).toHaveText('Unsaved changes.');
|
||
await save(page).click();
|
||
await expect(status(page)).toHaveText('Saved to the server configuration.');
|
||
expect(enables).toHaveLength(1);
|
||
expect(enables[0]).toMatchObject({ civitai_api_key: 'sk-test-key', enabled: false });
|
||
expect(enables[0].expected_revision).toMatch(/^(sha256:|absent)/);
|
||
await expect(field).toHaveValue('');
|
||
await expect(key.locator('p.small')).toHaveText('A key is saved.');
|
||
expect((await serverJson(request, '/__test__/integrations/state')).key_saved).toBe(true);
|
||
// Never prefilled, even after a reload; the page never receives the key.
|
||
const statusBodies = [];
|
||
page.on('response', async response => {
|
||
if (response.url().endsWith('/prompt_manager/lora/status')) statusBodies.push(await response.text());
|
||
});
|
||
await openIntegrations(page);
|
||
await expect(field).toHaveValue('');
|
||
await expect(key.locator('p.small')).toHaveText('A key is saved.');
|
||
expect(statusBodies.join('')).not.toContain('sk-test-key');
|
||
// The announced line is rewritten only when its text changes, not on every edit elsewhere (S10 re-review N8).
|
||
await page.evaluate(() => {
|
||
window.__keyNoteWrites = 0;
|
||
new MutationObserver(records => { window.__keyNoteWrites += records.length; })
|
||
.observe(document.querySelector('#civitai-key-status'), { childList: true, characterData: true, subtree: true });
|
||
});
|
||
const triggers = settingOf(page, 'Automatically include trigger words').locator('input');
|
||
await triggers.check();
|
||
await expect(status(page)).toHaveText('Unsaved changes.');
|
||
await triggers.uncheck();
|
||
await expect(status(page)).toHaveText('No unsaved changes.');
|
||
expect(await page.evaluate(() => window.__keyNoteWrites)).toBe(0);
|
||
// A blank field keeps the saved key: nothing to save.
|
||
await field.fill(' ');
|
||
await expect(status(page)).toHaveText('No unsaved changes.');
|
||
await field.fill('');
|
||
// Clear saved key drafts the removal; Save removes it. The keyboard stays on the key field, and the line that
|
||
// says so is announced (S10 review M2).
|
||
await expect(key.locator('p.small')).toHaveAttribute('role', 'status');
|
||
await expect(field).toHaveAttribute('aria-describedby', 'civitai-key-status');
|
||
await clear.focus();
|
||
await page.keyboard.press('Enter');
|
||
await expect(key.locator('p.small')).toHaveText('The saved key is removed when you save.');
|
||
await expect(clear).toHaveCount(0);
|
||
await expect(field).toBeFocused();
|
||
await save(page).click();
|
||
await expect(status(page)).toHaveText('Saved to the server configuration.');
|
||
expect(enables.at(-1)).toMatchObject({ clear_civitai_api_key: true });
|
||
expect(enables.at(-1)).not.toHaveProperty('civitai_api_key');
|
||
await expect(key.locator('p.small')).toHaveText('No key saved.');
|
||
expect((await serverJson(request, '/__test__/integrations/state')).key_saved).toBe(false);
|
||
});
|
||
|
||
test('a LoRA save over settings changed elsewhere is a conflict, never a blind write', async ({ page, request }) => {
|
||
await lora(request);
|
||
await openIntegrations(page);
|
||
await settingOf(page, 'Automatically include trigger words').locator('input').check();
|
||
// Another page saves the settings meanwhile: the revision this page read is stale.
|
||
await expect(await request.post('/prompt_manager/settings', { data: { result_timeout: 42 } })).toBeOK();
|
||
await save(page).click();
|
||
await expect(status(page)).toHaveText('Settings changed elsewhere. Reload this section to see them.');
|
||
expect((await serverJson(request, '/__test__/integrations/state')).trigger_words).toBe(false);
|
||
// The draft stays; the next Save carries the reread revision and succeeds.
|
||
await save(page).click();
|
||
await expect(status(page)).toHaveText('Saved to the server configuration.');
|
||
expect((await serverJson(request, '/__test__/integrations/state')).trigger_words).toBe(true);
|
||
});
|
||
|
||
test('a save where only the LoRA part is written says so; the next save needs no conflict (S10 review I3)',
|
||
async ({ page, request }) => {
|
||
await lora(request);
|
||
await expect(await request.post('/__test__/system/missing-source', { data: { inside: true } })).toBeOK();
|
||
await openIntegrations(page);
|
||
const triggers = settingOf(page, 'Automatically include trigger words').locator('input');
|
||
await triggers.check();
|
||
// In Sources, the drive is unplugged after the page read the list, and another source is removed.
|
||
const nav = page.locator('#workspace-main .section-nav');
|
||
await nav.getByRole('button', { name: 'Sources & scans', exact: true }).click();
|
||
const sourceRow = title => layoutOf(page).locator('#source-list .operation').filter({ has: page.locator('h3', { hasText: title }) });
|
||
await expect(sourceRow('unplugged')).toBeVisible();
|
||
await expect(await request.post('/__test__/system/unplug')).toBeOK();
|
||
await sourceRow('ComfyUI input').getByRole('button', { name: 'Remove', exact: true }).click();
|
||
await save(page).click();
|
||
// The LoRA part was saved, the sources were refused: the status gives the server's reason for the source, and
|
||
// only the source row is highlighted.
|
||
await expect(status(page)).toHaveText("output/unplugged can't be used: Gallery root does not exist. Remove it or reconnect it, then save.");
|
||
await expect(sourceRow('unplugged')).toHaveAttribute('aria-invalid', 'true');
|
||
expect((await serverJson(request, '/__test__/integrations/state')).trigger_words).toBe(true);
|
||
await nav.getByRole('button', { name: 'Integrations', exact: true }).click();
|
||
await expect(triggers).toBeChecked();
|
||
await expect(triggers).not.toHaveAttribute('aria-invalid', 'true');
|
||
// The next save carries the revision the saved part left: it saves, with no "changed elsewhere".
|
||
await nav.getByRole('button', { name: 'Sources & scans', exact: true }).click();
|
||
await sourceRow('unplugged').getByRole('button', { name: 'Remove', exact: true }).click();
|
||
await save(page).click();
|
||
await expect(status(page)).toHaveText('Saved to the server configuration.');
|
||
expect((await serverJson(request, '/prompt_manager/settings')).sources.configured.map(source => source.path)).toEqual(['output']);
|
||
});
|
||
|
||
test('a partial save made from Integrations keeps its outcome when Sources opens (S10 re-review R1)',
|
||
async ({ page, request }) => {
|
||
await lora(request);
|
||
await expect(await request.post('/__test__/system/missing-source', { data: { inside: true } })).toBeOK();
|
||
await openIntegrations(page);
|
||
const nav = page.locator('#workspace-main .section-nav');
|
||
const sourceRow = title => layoutOf(page).locator('#source-list .operation').filter({ has: page.locator('h3', { hasText: title }) });
|
||
await nav.getByRole('button', { name: 'Sources & scans', exact: true }).click();
|
||
await expect(sourceRow('unplugged')).toBeVisible();
|
||
await expect(await request.post('/__test__/system/unplug')).toBeOK();
|
||
await sourceRow('ComfyUI input').getByRole('button', { name: 'Remove', exact: true }).click();
|
||
// Back in Integrations, a LoRA change, then Save from there.
|
||
await nav.getByRole('button', { name: 'Integrations', exact: true }).click();
|
||
await settingOf(page, 'Automatically include trigger words').locator('input').check();
|
||
await save(page).click();
|
||
const reason = "output/unplugged can't be used: Gallery root does not exist. Remove it or reconnect it, then save.";
|
||
await expect(status(page)).toHaveText(reason);
|
||
// Leaving Integrations (no key was typed) keeps the outcome: Sources shows the reason on the refused row.
|
||
await nav.getByRole('button', { name: 'Sources & scans', exact: true }).click();
|
||
await expect(sourceRow('unplugged')).toHaveAttribute('aria-invalid', 'true');
|
||
await expect(status(page)).toHaveText(reason);
|
||
});
|
||
|
||
test('Review import shows replacement, blocked items and network effects with the backup box; Start opens the job (D107)',
|
||
async ({ page, request }) => {
|
||
await lora(request, { loras: 3, civitai: true, preview: false, previous: 2 });
|
||
const previews = [];
|
||
const applies = [];
|
||
page.on('request', item => {
|
||
if (item.url().endsWith(PREVIEW_URL)) previews.push(item.postDataJSON());
|
||
if (item.url().endsWith(APPLY_URL)) applies.push(item.postDataJSON());
|
||
});
|
||
await openIntegrations(page);
|
||
await enable(page);
|
||
const before = await serverJson(request, '/__test__/integrations/state');
|
||
const review = layoutOf(page).locator('.operation').filter({ hasText: 'LoRA reference data' })
|
||
.getByRole('button', { name: 'Review import', exact: true });
|
||
await review.click();
|
||
await expect(dialog(page).locator('#modal-title')).toHaveText('Review LoRA import');
|
||
await expect(dialog(page).locator('#modal-body > h3')).toHaveText('LoRA reference data');
|
||
expect(previews).toEqual([{}]);
|
||
expect(await gridOf(page)).toEqual([
|
||
['LoraManager', 'environment settings · alongside storage · library default'],
|
||
['Scope', 'Replace 2 imported prompts with 3 LoRAs'],
|
||
['Found', '3 metadata files · 3 LoRAs · 0 with local images'],
|
||
['Protected', 'Nothing is protected'],
|
||
['Network', 'Downloads up to 3 example images from CivitAI'],
|
||
['Backup', 'Create and verify before changes (browser.db.backup_<date and time>)'],
|
||
['Media on disk', 'No files are removed; image links are replaced'],
|
||
]);
|
||
// #355: the settings file the server read is the LoraManager row's hover title.
|
||
await expect(dialog(page).locator('dl.meta-grid > div').first().locator('dd'))
|
||
.toHaveAttribute('title', /lora-manager-settings[\/\\]settings\.json$/);
|
||
await expect(dialog(page).locator('details > summary')).toHaveText('LoRAs found (3)');
|
||
await expect(dialog(page).locator('.notice h3')).toHaveText('Back up the database first (recommended)');
|
||
await expect(dialog(page).locator('p.small').last()).toContainText('stay in the image cache');
|
||
// The review wrote and downloaded nothing; Cancel neither.
|
||
await dialog(page).getByRole('button', { name: 'Cancel', exact: true }).click();
|
||
await expect(dialog(page)).toHaveCount(0);
|
||
await expect(review).toBeFocused();
|
||
expect(await serverJson(request, '/__test__/integrations/state')).toEqual(before);
|
||
|
||
await review.click();
|
||
await dialog(page).getByRole('button', { name: 'Start import', exact: true }).click();
|
||
await expect(dialog(page)).toHaveCount(0);
|
||
await expect(page).toHaveURL(/#\/activity\/[0-9a-f]{32}$/);
|
||
await expect(toast(page)).toHaveText(/LoRA reference import started\.|LoRA import finished · 3 imported · 0 errors/);
|
||
expect(applies).toHaveLength(1);
|
||
expect(Object.keys(applies[0])).toEqual(['token']);
|
||
const job = await ended(request, page.url().split('/').pop());
|
||
expect([job.kind, job.title, job.state]).toEqual(['lora_import', 'LoRA reference import', 'finished']);
|
||
expect(job.counters).toMatchObject({ imported: 3, downloaded: 3, download_errors: 0, errors: 0 });
|
||
const main = page.locator('#workspace-main');
|
||
await expect(main.locator('h1')).toHaveText('LoRA reference import');
|
||
await expect(main.locator('.stat-row .stat dt')).toHaveText(['Imported', 'Linked to existing', 'Skipped', 'Images downloaded',
|
||
'Download errors', 'Errors']);
|
||
await expect(main.getByRole('button', { name: 'Cancel import', exact: true })).toHaveCount(0);
|
||
const after = await serverJson(request, '/__test__/integrations/state');
|
||
expect(after.imports).toEqual(['style_01 example prompt', 'style_02 example prompt', 'style_03 example prompt']);
|
||
expect(after.downloads).toBe(3);
|
||
});
|
||
|
||
test('saved pairs block the import: the review explains it and offers only Close', async ({ page, request }) => {
|
||
await lora(request, { previous: 1 });
|
||
await expect(await request.post('/__test__/integrations/pair')).toBeOK();
|
||
await openIntegrations(page);
|
||
await enable(page);
|
||
await layoutOf(page).getByRole('button', { name: 'Review import', exact: true }).click();
|
||
// gridOf reads once; poll until the review has rendered its rows.
|
||
await expect.poll(async () => (await gridOf(page))[3]).toEqual(['Protected', '1 imported prompt used by saved pairs']);
|
||
await expect(dialog(page).locator('p.small')).toHaveText(
|
||
'Delete or change the saved pairs that use these prompts, then review the import again. Nothing was changed.');
|
||
expect(await dialog(page).locator('.dialog-actions button').allTextContents()).toEqual(['Close']);
|
||
await dialog(page).getByRole('button', { name: 'Close', exact: true }).click();
|
||
await expect(dialog(page)).toHaveCount(0);
|
||
});
|
||
|
||
test('unusable LoraManager settings: the review says why and offers only Close (#355)', async ({ page, request }) => {
|
||
await lora(request, { previous: 1, settings: 'invalid' });
|
||
await openIntegrations(page);
|
||
await enable(page);
|
||
const before = await serverJson(request, '/__test__/integrations/state');
|
||
await layoutOf(page).getByRole('button', { name: 'Review import', exact: true }).click();
|
||
await expect.poll(async () => (await gridOf(page))[0])
|
||
.toEqual(['LoraManager', 'Not usable: the settings file is not valid JSON']);
|
||
await expect(dialog(page).locator('p.small')).toHaveText(
|
||
'LoraManager settings: not usable: the settings file is not valid JSON. Nothing was changed.');
|
||
expect(await dialog(page).locator('.dialog-actions button').allTextContents()).toEqual(['Close']);
|
||
await dialog(page).getByRole('button', { name: 'Close', exact: true }).click();
|
||
await expect(dialog(page)).toHaveCount(0);
|
||
expect(await serverJson(request, '/__test__/integrations/state')).toEqual(before);
|
||
});
|
||
|
||
test('imported LoRA previews show on the Library card and open in the inspector and viewer (#355 G6)', async ({ page, request }) => {
|
||
await lora(request, { loras: 1 });
|
||
await openIntegrations(page);
|
||
await enable(page);
|
||
await layoutOf(page).getByRole('button', { name: 'Review import', exact: true }).click();
|
||
await dialog(page).getByRole('button', { name: 'Start import', exact: true }).click();
|
||
await expect(page).toHaveURL(/#\/activity\/[0-9a-f]{32}$/);
|
||
expect((await ended(request, page.url().split('/').pop())).state).toBe('finished');
|
||
await page.goto(`${workspace}#/library`);
|
||
const card = page.locator('#workspace-main article.prompt-card').filter({ hasText: 'Style 01' });
|
||
const cover = card.locator('.card-art img');
|
||
await expect(cover).toHaveAttribute('src', /\/prompt_manager\/images\/serve\/style_01\.png\?root=\d+&root_scope=[0-9a-f]{64}$/);
|
||
await expect.poll(() => cover.evaluate(node => node.complete && node.naturalWidth)).toBeGreaterThan(0);
|
||
await expect(card.locator('.card-art')).not.toContainText('No available image previews');
|
||
// The prompt's gallery: Info inspects the file and the viewer opens it; no Tag action or star for LoRA media.
|
||
await card.getByRole('button', { name: 'Explore history', exact: true }).click();
|
||
const item = page.getByRole('region', { name: 'Shared gallery', exact: true }).locator('article[data-image-key]').first();
|
||
await expect(item.getByRole('button', { name: 'Tag or auto-tag style_01.png', exact: true })).toHaveCount(0);
|
||
await expect(item.locator('.star-button')).toHaveCount(0);
|
||
await item.getByRole('button', { name: 'Inspect style_01.png', exact: true }).click();
|
||
const inspector = page.getByRole('complementary', { name: 'Selected image details', exact: true });
|
||
await expect(inspector).toContainText('style_01.png');
|
||
await item.getByRole('button', { name: 'Open style_01.png', exact: true }).click();
|
||
await expect(page.locator('dialog[open] .viewer-canvas img')).toBeVisible();
|
||
await expect.poll(() => page.locator('dialog[open] .viewer-canvas img').evaluate(node => node.complete && node.naturalWidth))
|
||
.toBeGreaterThan(0);
|
||
await page.keyboard.press('Escape');
|
||
});
|
||
|
||
test('a failed import keeps old imports and its job says so', async ({ page, request }) => {
|
||
await lora(request, { loras: 2, previous: 2 });
|
||
await openIntegrations(page);
|
||
await enable(page);
|
||
const before = await serverJson(request, '/__test__/integrations/state');
|
||
await expect(await request.post('/__test__/integrations/fail-import')).toBeOK();
|
||
await layoutOf(page).getByRole('button', { name: 'Review import', exact: true }).click();
|
||
await dialog(page).getByRole('button', { name: 'Start import', exact: true }).click();
|
||
await expect(page).toHaveURL(/#\/activity\/[0-9a-f]{32}$/);
|
||
await expect(toast(page)).toHaveText(`LoRA import failed · ${KEPT}`);
|
||
const job = await ended(request, page.url().split('/').pop());
|
||
expect([job.state, job.scope.failure, job.counters.errors]).toEqual(['failed', 'import-failed', 1]);
|
||
await expect(page.locator('#workspace-main')).toContainText(KEPT);
|
||
expect((await serverJson(request, '/__test__/integrations/state')).imports).toEqual(before.imports);
|
||
expect(before.imports).toEqual(['previous lora import 1', 'previous lora import 2']);
|
||
});
|
||
|
||
test('the import can be cancelled while it downloads; then nothing is written', async ({ page, request }) => {
|
||
await lora(request, { loras: 2, civitai: true, preview: false, previous: 1 });
|
||
await openIntegrations(page);
|
||
await enable(page);
|
||
const before = await serverJson(request, '/__test__/integrations/state');
|
||
await expect(await request.post('/__test__/integrations/gate', { data: { hold: true } })).toBeOK();
|
||
await layoutOf(page).getByRole('button', { name: 'Review import', exact: true }).click();
|
||
await dialog(page).getByRole('button', { name: 'Start import', exact: true }).click();
|
||
await expect(page).toHaveURL(/#\/activity\/[0-9a-f]{32}$/);
|
||
const id = page.url().split('/').pop();
|
||
await expect.poll(async () => (await serverJson(request, '/__test__/integrations/state')).downloads).toBeGreaterThan(0);
|
||
const cancel = page.locator('#workspace-main').getByRole('button', { name: 'Cancel import', exact: true });
|
||
await expect(cancel).toBeVisible();
|
||
// The cancel must have reached the server before the held download goes on (S10 review M1).
|
||
await Promise.all([
|
||
page.waitForResponse(response => /\/prompt_manager\/jobs\/[0-9a-f]+\/cancel$/.test(response.url()) && response.ok()),
|
||
cancel.click(),
|
||
]);
|
||
await expect(await request.post('/__test__/integrations/gate', { data: { hold: false } })).toBeOK();
|
||
const job = await ended(request, id);
|
||
expect(job.state).toBe('cancelled');
|
||
expect((await serverJson(request, '/__test__/integrations/state')).imports).toEqual(before.imports);
|
||
await expect(cancel).toHaveCount(0);
|
||
});
|