From 9680c46a29ae69ce66bc79ed8965dec16853c420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=81=82=E6=A9=99?= Date: Sat, 24 Oct 2026 09:58:17 +0800 Subject: [PATCH] fix(resource): exclude inline payloads from fallback searches --- __tests__/unit/resource/loader.test.ts | 126 +++++++++++++++++++++++++ src/resource/loader.ts | 24 ++--- 2 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 __tests__/unit/resource/loader.test.ts diff --git a/__tests__/unit/resource/loader.test.ts b/__tests__/unit/resource/loader.test.ts new file mode 100644 index 000000000..eda84ef8d --- /dev/null +++ b/__tests__/unit/resource/loader.test.ts @@ -0,0 +1,126 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { loadResource } from '../../../src/resource/loader'; +import { + loadImageBase64Resource, + loadRemoteResource, + loadSearchResource, + loadSVGResource, +} from '../../../src/resource/loaders'; +import { getCustomResourceLoader } from '../../../src/resource/registry'; +import type { ResourceConfig } from '../../../src/resource/types'; + +vi.mock('../../../src/resource/loaders', () => ({ + loadImageBase64Resource: vi.fn(), + loadRemoteResource: vi.fn(), + loadSearchResource: vi.fn(), + loadSVGResource: vi.fn(), +})); + +vi.mock('../../../src/resource/registry', () => ({ + getCustomResourceLoader: vi.fn(), +})); + +describe('resource fallback queries', () => { + let svg: SVGSVGElement; + + beforeEach(() => { + vi.resetAllMocks(); + svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + vi.mocked(loadSVGResource).mockReturnValue(null); + vi.mocked(loadImageBase64Resource).mockResolvedValue(null); + vi.mocked(loadRemoteResource).mockRejectedValue(new Error('unavailable')); + vi.mocked(loadSearchResource).mockImplementation(async () => + document.createElementNS('http://www.w3.org/2000/svg', 'symbol'), + ); + vi.mocked(getCustomResourceLoader).mockReturnValue(null); + }); + + it('uses the item label when an inline resource fails', async () => { + const id = await loadResource( + svg, + 'icon', + { source: 'inline', format: 'svg', encoding: 'raw', data: ' { + await loadResource( + svg, + 'icon', + { + source: 'inline', + format: 'image', + data: 'data:image/png;base64,invalid', + }, + { label: ' ', desc: ' Cloud storage ' }, + ); + + expect(loadSearchResource).toHaveBeenCalledWith('Cloud storage', 'image'); + }); + + it.each([ + { source: 'inline', format: 'svg', encoding: 'raw', data: ' { + await loadResource(svg, 'icon', config); + + expect(loadSearchResource).toHaveBeenCalledWith('icon', config.format); + }, + ); + + it('uses the illustration default for failed inline illustrations', async () => { + await loadResource(svg, 'illus', { + source: 'inline', + format: 'svg', + encoding: 'raw', + data: ' { + await loadResource(svg, 'icon', ' computer network '); + + expect(loadSearchResource).toHaveBeenCalledWith( + 'computer network', + undefined, + ); + }); + + it('preserves explicit search text over item context', async () => { + await loadResource(svg, 'icon', ' plane ', { label: 'Other subject' }); + + expect(loadSearchResource).toHaveBeenCalledWith('plane', undefined); + }); + + it('does not search when the primary loader succeeds', async () => { + vi.mocked(loadRemoteResource).mockResolvedValue( + document.createElementNS('http://www.w3.org/2000/svg', 'symbol'), + ); + + await loadResource( + svg, + 'icon', + 'ref:remote:svg:https://example.com/icon.svg', + { + label: 'Computer network', + }, + ); + + expect(loadSearchResource).not.toHaveBeenCalled(); + }); +}); diff --git a/src/resource/loader.ts b/src/resource/loader.ts index 2a6902ee1..8f21ef251 100644 --- a/src/resource/loader.ts +++ b/src/resource/loader.ts @@ -107,19 +107,19 @@ function getFallbackQuery( datum?: ItemDatum, ): string { const defaultQuery = scene === 'illus' ? 'illustration' : 'icon'; - const datumQuery = - normalizeQuery(cfg.data) || - normalizeQuery(datum?.label) || - normalizeQuery(datum?.desc); - if (datumQuery) return datumQuery; - const data = normalizeQuery(cfg.data); - if (!data) return defaultQuery; - if (cfg.source === 'inline') return defaultQuery; - if (data.startsWith('data:')) return defaultQuery; - if (data.startsWith('