From 2ed085f8d7dd9d13ab35abf087179bfb68a7915b Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sat, 24 Oct 2026 14:08:12 -0700 Subject: [PATCH] fix(thumbnails): say when the thumbnail settings could not be read Like Appearance's other server rows, the Thumbnails rows show 'Server settings could not be read.' while the read failed, instead of only staying disabled. Firefox exposed the gap: its JPEG XL note arrived first and changed the row notes the page workflow counted. --- tests/browser/workspace-system.spec.mjs | 8 +++++--- tests/js/workspace-system-thumbnails.test.js | 16 ++++++++++++++-- web/js/workspace/system-thumbnails.mjs | 18 ++++++++++-------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/tests/browser/workspace-system.spec.mjs b/tests/browser/workspace-system.spec.mjs index 5c55a938..1f56f989 100644 --- a/tests/browser/workspace-system.spec.mjs +++ b/tests/browser/workspace-system.spec.mjs @@ -650,14 +650,16 @@ test('server settings that cannot be read say so in their rows', async ({ page } ? route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ success: false, error: 'Injected failure' }) }) : route.continue())); await page.goto(`${workspace}#/system/appearance`); - for (const id of ['open-mode', 'timeout']) { + // #317: the Thumbnails rows are server settings too. + for (const id of ['open-mode', 'timeout', 'thumbnail-format', 'thumbnail-quality', 'thumbnail-edge']) { const row = page.locator('#workspace-main .setting-row').filter({ has: page.locator(`#${id}`) }); - await expect(row.locator('p.small')).toHaveText('Server settings could not be read.'); + await expect(row.locator('p.small').first()).toHaveText('Server settings could not be read.'); await expect(page.locator(`#${id}`)).toBeDisabled(); } // The browser rows are this browser's, and work as before. await expect(page.locator('#default-view')).toBeEnabled(); - await expect(page.locator('#workspace-main .setting-row p.small')).toHaveCount(2); + await expect(page.locator('#workspace-main .setting-row p.small', { hasText: 'Server settings could not be read.' })).toHaveCount(5); + await expect(page.locator('#workspace-main section.surface').first().locator('.setting-row p.small')).toHaveCount(2); }); test('stale revision shows the conflict status', async ({ page, request }) => { diff --git a/tests/js/workspace-system-thumbnails.test.js b/tests/js/workspace-system-thumbnails.test.js index 07d98ecf..e7169f11 100644 --- a/tests/js/workspace-system-thumbnails.test.js +++ b/tests/js/workspace-system-thumbnails.test.js @@ -67,7 +67,7 @@ const HOST = { thumbnail_format: 'webp', thumbnail_quality: 80, thumbnail_edge: const NO_JXL = { webp: { available: true }, jpeg: { available: true }, jxl: { available: false, encoder: null, reason: 'No JPEG XL encoder found. Install pillow-jxl-plugin.' } }; -async function mounted({ host = HOST, browserJxl = true, writeHost = async () => ({ success: true, persisted: true, revision: 'sha256:two' }) } = {}) { +async function mounted({ host = HOST, browserJxl = true, hostFails = false, writeHost = async () => ({ success: true, persisted: true, revision: 'sha256:two' }) } = {}) { const { mountThumbnailSettings } = await thumbnailsModule; const { createSystemDrafts } = await stateModule; const { createScope } = await coreModule; @@ -75,7 +75,7 @@ async function mounted({ host = HOST, browserJxl = true, writeHost = async () => let hostValues = null; const drafts = createSystemDrafts({ readBrowser: () => ({}), readRuntime: () => ({}), - readHost: async () => ({ values: hostValues, revision: 'sha256:one' }), + readHost: async () => { if (hostFails) throw new Error('offline'); return { values: hostValues, revision: 'sha256:one' }; }, writeBrowser: async () => ({ persisted: true }), writeHost, writeRuntime: async () => ({}), }); const scope = createScope(); @@ -181,3 +181,15 @@ test('Convert outdated opens Thumbnail maintenance on its convert scope; a forma assert.equal(reviews.length, 1, 'nothing starts by itself'); } finally { scope.dispose(); assert.equal(dom.listenerCount(), 0); dom.restore(); } }); + +test('rows whose server settings could not be read say so, like Appearance’s host rows', async () => { + const { HOST_UNAVAILABLE } = await import('../../web/js/workspace/system-appearance.mjs'); + const { dom, drafts, scope, surface, byId } = await mounted({ hostFails: true }); + const notes = () => surface.children.slice(2, 5).map(row => row.children[0].children.slice(2).map(note => note.textContent)); + try { + assert.deepEqual(notes(), [[], [], []]); + assert.equal(await drafts.reloadHost(), false); + assert.deepEqual(notes(), [[HOST_UNAVAILABLE], [HOST_UNAVAILABLE], [HOST_UNAVAILABLE]]); + for (const id of ['thumbnail-format', 'thumbnail-quality', 'thumbnail-edge']) assert.equal(byId(id).disabled, true, id); + } finally { scope.dispose(); assert.equal(dom.listenerCount(), 0); dom.restore(); } +}); diff --git a/web/js/workspace/system-thumbnails.mjs b/web/js/workspace/system-thumbnails.mjs index e51895e5..33b20b29 100644 --- a/web/js/workspace/system-thumbnails.mjs +++ b/web/js/workspace/system-thumbnails.mjs @@ -14,11 +14,13 @@ * automatic. After a save that changed the format, its line says existing previews keep their old format until * converted. * - * Host rows stay disabled until the server settings were read, like Appearance's. + * Host rows stay disabled until the server settings were read, like Appearance's, and say "Server settings could not + * be read." while a read failed. */ import { settingRow, operationRow } from './system-controls.mjs'; import { detectJxlSupport } from './thumbnail-support.mjs'; import { openThumbnailReview } from './thumbnail-review.mjs'; +import { HOST_UNAVAILABLE } from './system-appearance.mjs'; export const FORMAT_OPTIONS = Object.freeze([['webp', 'WebP'], ['jxl', 'JPEG XL'], ['jpeg', 'JPEG']]); export const QUALITY_LIMIT = Object.freeze({ min: 1, max: 100 }); @@ -84,7 +86,6 @@ export function mountThumbnailSettings(container, { scope, drafts, api, activity let browserJxl; let savedFormat; const format = controls[0]; - const notes = []; const repaint = () => { const encoders = drafts.get('host', 'thumbnail_encoders'); for (const { input, key } of controls) { @@ -95,12 +96,13 @@ export function mountThumbnailSettings(container, { scope, drafts, api, activity const jxl = Array.from(format.input.children).find(option => option.value === 'jxl'); const unavailable = encoders?.jxl?.available === false; if (jxl) jxl.disabled = unavailable; - for (const note of notes.splice(0)) note.remove(); - const lines = [unavailable ? encoders.jxl.reason : null, browserJxl === false ? BROWSER_NO_JXL : null]; - for (const line of lines.filter(Boolean)) { - const note = el('p', line, 'small'); - format.text.append(note); - notes.push(note); + const failed = drafts.hostState?.() === 'failed'; + for (const row of controls) { + for (const note of row.notes ?? []) note.remove(); + const lines = [failed && drafts.get('host', row.key) === undefined ? HOST_UNAVAILABLE : null]; + if (row === format) lines.push(unavailable ? encoders.jxl.reason : null, browserJxl === false ? BROWSER_NO_JXL : null); + row.notes = lines.filter(Boolean).map(line => el('p', line, 'small')); + row.text.append(...row.notes); } // A format save (not the first read) leaves the existing previews outdated: say so, never convert. const saved = drafts.saved('host').thumbnail_format;