Files
ycode/scripts/image-renderer-regression.tsx
T

169 lines
6.8 KiB
TypeScript

import assert from 'node:assert/strict'
import { renderToStaticMarkup } from 'react-dom/server'
import { downloadImageSafely, ImagePreview } from '../src/renderer/src/workbuddy/images/ImagePreview'
import { MarkdownImage } from '../src/renderer/src/workbuddy/images/MarkdownImage'
import { PreviewIcon } from '../src/renderer/src/workbuddy/images/PreviewIcon'
import {
clampPreviewIndex,
isSafeImageUrl,
makeImagePreviewRequest,
resolveImageUrl,
requiresHostImageResolution,
stepPreviewIndex
} from '../src/renderer/src/workbuddy/images/image-url'
async function testSafeImageSchemesAndResolution(): Promise<void> {
for (const url of [
'data:image/png;base64,AA==',
'https://example.test/image.png',
'HTTP://example.test/image.png',
'blob:https://example.test/id',
'file:///C:/images/photo.png',
'local-file://image/photo.png'
]) assert.equal(isSafeImageUrl(url), true, `${url} should be safe`)
for (const url of [
'javascript:alert(1)',
'data:text/html,unsafe',
'../images/photo.png',
'not a URL'
]) assert.equal(isSafeImageUrl(url), false, `${url} should be rejected`)
let resolverCalls = 0
assert.equal(await resolveImageUrl('https://example.test/image.png', async () => {
resolverCalls++
return 'data:image/png;base64,AA=='
}), 'https://example.test/image.png')
assert.equal(resolverCalls, 0, 'safe sources should bypass host resolution')
assert.equal(await resolveImageUrl('C:\\workspace\\photo.png', async (source) => {
assert.equal(source, 'C:\\workspace\\photo.png')
return 'data:image/png;base64,BB=='
}), 'data:image/png;base64,BB==')
assert.equal(requiresHostImageResolution('file:///C:/workspace/photo.png'), true)
assert.equal(requiresHostImageResolution('local-file://workspace/photo.png'), true)
assert.equal(requiresHostImageResolution('https://example.test/photo.png'), false)
let hostCalls = 0
assert.equal(await resolveImageUrl('file:///C:/workspace/photo.png', async () => {
hostCalls++
return 'data:image/png;base64,CC=='
}, true), 'data:image/png;base64,CC==')
assert.equal(hostCalls, 1, 'preferred host resolution should run for local file schemes')
assert.equal(await resolveImageUrl('https://example.test/photo.png', async () => {
hostCalls++
return 'data:image/png;base64,DD=='
}, true), 'https://example.test/photo.png')
assert.equal(hostCalls, 1, 'preferred host resolution should not intercept remote images')
assert.equal(await resolveImageUrl('/unsafe/path.png', async () => 'javascript:alert(1)'), undefined)
assert.equal(await resolveImageUrl('/no-resolver.png'), undefined)
await assert.rejects(resolveImageUrl('/reject.png', async () => { throw new Error('resolver failed') }), /resolver failed/)
}
function testSafePreviewFilteringAndIndex(): void {
assert.deepEqual(makeImagePreviewRequest([
'https://example.test/one.png',
'../unsafe.png',
'data:image/png;base64,AA==',
'javascript:alert(1)'
], 'data:image/png;base64,AA=='), {
urlList: ['https://example.test/one.png', 'data:image/png;base64,AA=='],
initialIndex: 1
})
assert.deepEqual(makeImagePreviewRequest(['../one.png'], 'data:image/png;base64,AA=='), {
urlList: [],
initialIndex: 0
})
assert.equal(clampPreviewIndex(-3, 4), 0)
assert.equal(clampPreviewIndex(9, 4), 3)
assert.equal(clampPreviewIndex(1, 0), 0)
assert.equal(stepPreviewIndex(1, 4, -1), 0)
assert.equal(stepPreviewIndex(2, 4, 1), 3)
}
async function testRejectedImageDownloadIsReported(): Promise<void> {
let message = ''
await downloadImageSafely(async () => {
throw new Error('disk unavailable')
}, 'https://example.test/image.png', (value) => { message = value })
assert.equal(message, '下载失败:disk unavailable')
}
function testMarkdownImageServerFallback(): void {
const html = renderToStaticMarkup(
<MarkdownImage
src="C:\\workspace\\photo.png"
alt="source diagram"
urlList={['https://example.test/photo.png']}
imageUrlResolver={async () => 'data:image/png;base64,AA=='}
/>
)
assert.match(html, /class="[^"]*cr-image-block-fallback[^"]*"/)
assert.match(html, /aria-busy="true"/)
assert.match(html, /role="img"/)
assert.match(html, />source diagram<\/span>/)
const source = 'C:\\workspace\\source-only.png'
const sourceFallback = renderToStaticMarkup(<MarkdownImage src={source} urlList={[]} />)
assert.ok(!sourceFallback.includes(source), 'unresolved source is never fallback text')
assert.ok(sourceFallback.length < 500)
const synthetic = 'data:image/png;base64,' + 'A'.repeat(24000)
const first = renderToStaticMarkup(<MarkdownImage src={synthetic} alt="" urlList={[synthetic]} />)
assert.match(first, /<img /)
assert.ok(first.includes(`src="${synthetic}"`))
assert.ok(!first.includes('cr-image-block-fallback'))
assert.ok(!first.replace(/<[^>]*>/g, '').includes('data:image'))
for (const invalid of ['javascript:alert(1)', 'data:text/html,' + 'A'.repeat(24000)]) {
const markup = renderToStaticMarkup(<MarkdownImage src={invalid} alt="" urlList={[]} />)
assert.ok(markup.length < 500)
assert.ok(!markup.includes(invalid))
assert.ok(!markup.includes('<img '))
}
}
function testPreviewDialogServerStructure(): void {
const html = renderToStaticMarkup(
<ImagePreview
urlList={['https://example.test/one.png', 'https://example.test/two.png']}
initialIndex={1}
onClose={() => undefined}
onDownload={() => undefined}
/>
)
assert.match(html, /role="dialog"/)
assert.match(html, /aria-modal="true"/)
assert.match(html, /src="https:\/\/example\.test\/two\.png"/)
assert.match(html, /crossorigin="use-credentials"/)
assert.match(html, /aria-label="上一张"/)
assert.match(html, /aria-label="下一张"/)
assert.match(html, /aria-label="下载图片"/)
assert.match(html, /2\/2/)
}
function testRecoveredPreviewIconVectors(): void {
const actualSize = renderToStaticMarkup(<PreviewIcon name="actual-size" />)
assert.match(actualSize, /d="M2\.5 0L1\.25 0L1\.25 1L0 1L0 2\.25/)
const zoomIn = renderToStaticMarkup(<PreviewIcon name="zoom-in" />)
assert.match(zoomIn, /viewBox="0 0 16 16"/)
assert.match(zoomIn, /transform="matrix\(1 0 0 1 0\.882352 0\.882294\)"/)
const close = renderToStaticMarkup(<PreviewIcon name="close" size={20} />)
assert.match(close, /viewBox="0 0 12 12"/)
assert.match(close, /d="M9\.375 2\.625L2\.625 9\.375"/)
}
async function main(): Promise<void> {
await testSafeImageSchemesAndResolution()
testSafePreviewFilteringAndIndex()
await testRejectedImageDownloadIsReported()
testMarkdownImageServerFallback()
testPreviewDialogServerStructure()
testRecoveredPreviewIconVectors()
console.log('image-renderer-regression: all checks passed')
}
void main().catch((error: unknown) => {
console.error('image-renderer-regression failed:', error)
process.exitCode = 1
})