@@ -0,0 +1,50 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
function fixture() {
|
||||
const panel = { innerHTML: '', isConnected: true };
|
||||
const requests = new Map();
|
||||
const context = vm.createContext({ document: { readyState: 'loading', addEventListener() {},
|
||||
getElementById: id => id === 'metadata-content' ? panel : null },
|
||||
window: {}, console: { error() {} },
|
||||
});
|
||||
const Admin = vm.runInContext(fs.readFileSync(path.join(__dirname, '../../web/js/admin.js'), 'utf8') + '\nPromptAdmin;', context);
|
||||
const manager = Object.create(Admin.prototype);
|
||||
manager.extractImageMetadata = url => new Promise((resolve, reject) => requests.set(url, { resolve, reject }));
|
||||
manager.updateMetadataSidebar = metadata => { panel.innerHTML = metadata?.imagePath || 'No metadata'; };
|
||||
manager.hideModal = () => {};
|
||||
const load = url => manager.loadMetadataForImage({ getAttribute: () => url });
|
||||
return { manager, panel, requests, load };
|
||||
}
|
||||
test('admin metadata and copy actions stay on the selected image', async () => {
|
||||
const { manager, panel, requests, load } = fixture();
|
||||
const old = load('old.png');
|
||||
const current = load('current.png');
|
||||
requests.get('current.png').resolve({ imagePath: 'current.png' }); await current;
|
||||
requests.get('old.png').resolve({ imagePath: 'old.png' }); await old;
|
||||
assert.equal(panel.innerHTML, 'current.png');
|
||||
assert.equal(manager.currentMetadata.imagePath, 'current.png');
|
||||
});
|
||||
test('navigation clears old action data and late errors cannot replace current metadata', async () => {
|
||||
const { manager, panel, requests, load } = fixture();
|
||||
manager.currentMetadata = { imagePath: 'previous.png' };
|
||||
const old = load('old.png');
|
||||
assert.equal(manager.currentMetadata, null);
|
||||
const current = load('current.png');
|
||||
requests.get('current.png').resolve({ imagePath: 'current.png' }); await current;
|
||||
requests.get('old.png').reject(new Error('old error')); await old;
|
||||
assert.equal(panel.innerHTML, 'current.png');
|
||||
});
|
||||
for (const close of ['close gallery', 'detach sidebar']) {
|
||||
test(`${close} prevents pending admin metadata from restoring copy state`, async () => {
|
||||
const { manager, panel, requests, load } = fixture();
|
||||
const pending = load('old.png');
|
||||
if (close === 'close gallery') manager.closeGallery();
|
||||
else panel.isConnected = false;
|
||||
requests.get('old.png').resolve({ imagePath: 'old.png' }); await pending;
|
||||
assert.equal(manager.currentMetadata, null);
|
||||
assert.notEqual(panel.innerHTML, 'old.png');
|
||||
});
|
||||
}
|
||||
Vendored
+62
@@ -206,3 +206,65 @@ test('metadata follows the selected image when responses finish out of order', a
|
||||
await page.evaluate(() => metadataRequests.get('http://viewer.test/original/2.svg')({ positivePrompt: 'Closed prompt' }));
|
||||
expect(await page.evaluate(() => gallery.currentMetadata)).toBeNull();
|
||||
});
|
||||
|
||||
for (const mode of ['gallery', 'filmstrip']) {
|
||||
test(`admin ${mode} metadata ignores stale responses and clears copy state on close`, async ({ page }) => {
|
||||
const urlFor = index => mode === 'gallery' ? `http://viewer.test/prompt_manager/images/${index}/file` : `http://viewer.test/original/${index}.svg`;
|
||||
const adminSource = source('web/js/admin.js');
|
||||
await page.addScriptTag({ content: adminSource.slice(0, adminSource.indexOf(' function startAdmin()')) + '\nwindow.TestAdmin = PromptAdmin;' });
|
||||
await page.evaluate(async mode => {
|
||||
const admin = window.admin = Object.create(TestAdmin.prototype);
|
||||
const images = Array.from({ length: 3 }, (_, i) => ({ id: i, filename: `${i}.svg`, url: `http://viewer.test/original/${i}.svg`, size: 100, created_at: 1 }));
|
||||
window.pendingMetadata = new Map();
|
||||
Object.assign(admin, { prompts: [{ id: 1, text: 'test' }],
|
||||
getImageUrl: image => image.url, loadFilmStripImages: async () => images,
|
||||
extractImageMetadata: url => new Promise(resolve => pendingMetadata.set(url, resolve)),
|
||||
});
|
||||
if (mode === 'gallery') {
|
||||
admin.renderGallery(images, 1);
|
||||
} else {
|
||||
await admin.openFilmStripViewer(1, 0);
|
||||
}
|
||||
}, mode);
|
||||
if (mode === 'gallery') {
|
||||
await expect.poll(() => page.evaluate(() => Boolean(admin.galleryViewer))).toBe(true);
|
||||
await page.evaluate(() => admin.galleryViewer.show());
|
||||
}
|
||||
await expect.poll(() => page.evaluate(url => pendingMetadata.has(url), urlFor(0))).toBe(true);
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect.poll(() => page.evaluate(url => pendingMetadata.has(url), urlFor(1))).toBe(true);
|
||||
await page.evaluate(url => pendingMetadata.get(url)({ positivePrompt: 'Selected prompt', negativePrompt: '', imagePath: '1.svg' }), urlFor(1));
|
||||
await expect(page.locator('#metadata-content')).toContainText('Selected prompt');
|
||||
await page.evaluate(url => pendingMetadata.get(url)({ positivePrompt: 'Stale prompt', negativePrompt: '', imagePath: '0.svg' }), urlFor(0));
|
||||
await expect(page.locator('#metadata-content')).not.toContainText('Stale prompt');
|
||||
expect(await page.evaluate(() => admin.currentMetadata.imagePath)).toBe('1.svg');
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect.poll(() => page.evaluate(url => pendingMetadata.has(url), urlFor(2))).toBe(true);
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.locator('#metadata-sidebar')).toHaveCount(0);
|
||||
await page.evaluate(url => pendingMetadata.get(url)({ positivePrompt: 'Closed prompt', negativePrompt: '', imagePath: '2.svg' }), urlFor(2));
|
||||
expect(await page.evaluate(() => admin.currentMetadata)).toBeNull();
|
||||
});
|
||||
}
|
||||
|
||||
test('Tags selection stays current during a delayed prompt response', async ({ page }) => {
|
||||
await page.addScriptTag({ path: join(root, 'web/js/tags-page.js') });
|
||||
await page.evaluate(() => {
|
||||
const grid = document.createElement('div'); grid.id = 'tagPromptsGrid'; document.body.append(grid);
|
||||
const manager = window.tagsManager = Object.create(TagsPageManager.prototype);
|
||||
window.pendingTagPrompts = [];
|
||||
Object.assign(manager, { selectedTags: ['cats'], filterMode: 'and', promptLimit: 20, prompts: [],
|
||||
fetchJson: url => new Promise(resolve => pendingTagPrompts.push({ url, resolve })),
|
||||
renderTagPrompts() { grid.textContent = this.prompts.map(prompt => prompt.text).join(','); },
|
||||
updatePromptCountBadge() {}, updateLoadMoreButton() {}, refreshPromptObserver() {},
|
||||
});
|
||||
manager.loadTagPrompts();
|
||||
manager.selectedTags = ['dogs'];
|
||||
manager.loadTagPrompts();
|
||||
});
|
||||
expect(await page.evaluate(() => pendingTagPrompts.length)).toBe(2);
|
||||
await page.evaluate(() => pendingTagPrompts[1].resolve({ success: true, prompts: [{ text: 'dog prompt' }], pagination: { total: 1, has_more: false } }));
|
||||
await expect(page.locator('#tagPromptsGrid')).toHaveText('dog prompt');
|
||||
await page.evaluate(() => pendingTagPrompts[0].resolve({ success: true, prompts: [{ text: 'cat prompt' }], pagination: { total: 1, has_more: false } }));
|
||||
await expect(page.locator('#tagPromptsGrid')).toHaveText('dog prompt');
|
||||
});
|
||||
|
||||
@@ -1656,6 +1656,7 @@
|
||||
// Initialize ViewerJS for the gallery after DOM is updated
|
||||
setTimeout(() => {
|
||||
if (this.galleryViewer) {
|
||||
this.invalidateMetadataRequest();
|
||||
this.galleryViewer.destroy();
|
||||
}
|
||||
|
||||
@@ -1699,6 +1700,7 @@
|
||||
viewed: (event) => {
|
||||
this.loadMetadataForImage(event.detail.image);
|
||||
},
|
||||
hide: () => this.invalidateMetadataRequest(),
|
||||
hidden: () => {
|
||||
const sidebar = document.getElementById('metadata-sidebar');
|
||||
if (sidebar) sidebar.remove();
|
||||
@@ -1711,7 +1713,13 @@
|
||||
|
||||
|
||||
|
||||
invalidateMetadataRequest() {
|
||||
this.metadataRequest = null;
|
||||
this.currentMetadata = null;
|
||||
}
|
||||
|
||||
closeGallery() {
|
||||
this.invalidateMetadataRequest();
|
||||
this.hideModal("galleryModal");
|
||||
}
|
||||
|
||||
@@ -2896,6 +2904,11 @@
|
||||
const metadataContent = document.getElementById('metadata-content');
|
||||
if (!metadataContent) return;
|
||||
|
||||
const request = {};
|
||||
this.metadataRequest = request;
|
||||
this.currentMetadata = null;
|
||||
const isCurrent = () => this.metadataRequest === request && metadataContent.isConnected !== false;
|
||||
|
||||
// Show loading state
|
||||
metadataContent.innerHTML = `
|
||||
<div class="metadata-loading">
|
||||
@@ -2911,9 +2924,11 @@
|
||||
|
||||
try {
|
||||
const metadata = await this.extractImageMetadata(imageUrl);
|
||||
if (!isCurrent()) return;
|
||||
this.currentMetadata = metadata;
|
||||
this.updateMetadataSidebar(metadata);
|
||||
} catch (error) {
|
||||
if (!isCurrent()) return;
|
||||
console.error('Failed to load metadata:', error);
|
||||
metadataContent.innerHTML = `
|
||||
<div class="text-center text-pm-error py-8">
|
||||
@@ -3515,6 +3530,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
viewed: (event) => {
|
||||
this.loadMetadataForImage(event.detail.image);
|
||||
},
|
||||
hide: () => this.invalidateMetadataRequest(),
|
||||
hidden: () => {
|
||||
const sidebar = document.getElementById('metadata-sidebar');
|
||||
if (sidebar) sidebar.remove();
|
||||
|
||||
Reference in New Issue
Block a user