Files
pj_admin_cloud/tests/file-image.test.cjs
dzc d86bc810cc fix:图片访问
fix:权限修改
2026-10-29 14:26:56 +08:00

165 lines
8.4 KiB
JavaScript

const { test } = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const vue = require('vue')
const { parse, compileScript } = require('@vue/compiler-sfc')
const { runtime, compile, root } = require('./helpers/file-access-runtime.cjs')
const raw = file => '/admin/sys-file/oss/file?fileName=' + file
const settle = async () => { await new Promise(resolve => setImmediate(resolve)); await vue.nextTick() }
function mountImages(api) {
const descriptor = parse(fs.readFileSync(path.join(root, 'src/components/FileImage/index.vue'), 'utf8')).descriptor
const component = compileScript(descriptor, { id: 'file-image-regression', inlineTemplate: true }).content
const loaded = { exports: {} }
const loads = [], failures = []
// Element Plus restarts loading when its src prop changes. Record that boundary
// while executing the real wrapper's setup, template, events and Vue lifecycle.
const ElImage = vue.defineComponent({
props: ['src', 'previewSrcList'], emits: ['load', 'error', 'show', 'switch', 'close'],
setup(props, { emit }) {
vue.watch(() => props.src, value => loads.push(value), { immediate: true })
return () => vue.h('img', {
src: props.src, preview: props.previewSrcList,
onLoad: event => emit('load', event), onError: event => emit('error', event),
onShow: () => emit('show'), onSwitch: index => emit('switch', index),
})
},
})
new Function('module', 'exports', 'require', compile(component))(loaded, loaded.exports, id => {
if (id === 'element-plus') return { ElImage }
if (id === '/@/utils/file-access') return api
return require(id)
})
const renderer = vue.createRenderer({
createElement: type => ({ type, props: {}, children: [] }), createText: text => ({ text }), createComment: text => ({ text }),
setText: (node, text) => { node.text = text }, setElementText: (node, text) => { node.text = text },
parentNode: node => node.parent, nextSibling: node => node.parent?.children[node.parent.children.indexOf(node) + 1],
patchProp: (node, key, previous, next) => { node.props[key] = next },
insert(node, parent, anchor) {
if (node.parent) node.parent.children.splice(node.parent.children.indexOf(node), 1)
node.parent = parent
const index = anchor ? parent.children.indexOf(anchor) : -1
parent.children.splice(index < 0 ? parent.children.length : index, 0, node)
},
remove(node) { if (node.parent) node.parent.children.splice(node.parent.children.indexOf(node), 1) },
})
const sources = vue.ref([raw('good.jpg'), raw('other.jpg')])
const container = { children: [] }
const app = renderer.createApp({
setup() {
return () => vue.h('section', sources.value.map((source, index) => vue.h(loaded.exports.default, {
key: index, src: api.accessFileUrl(source), previewSrcList: api.fileUrls([source]),
onError: () => failures.push(index),
})))
},
})
app.mount(container)
return { loads, failures, sources, images: () => container.children[0].children, close: () => app.unmount() }
}
test('one failed image renews only itself, coalesces concurrent retries, and remains bounded', async () => {
const env = runtime(), { api, clock } = env
try {
const good = api.fileUrl(raw('good.jpg')), bad = api.fileUrl(raw('missing.jpg'))
clock.now += 1000
assert.deepEqual(await Promise.all([api.retryFileAccess(raw('missing.jpg')), api.retryFileAccess(raw('missing.jpg'))]), [true, true])
assert.equal(clock.requests, 1)
assert.equal(api.fileUrl(raw('good.jpg')), good)
assert.notEqual(api.fileUrl(raw('missing.jpg')), bad)
assert.equal(await api.retryFileAccess(raw('missing.jpg')), false)
assert.equal(clock.requests, 1)
const offline = api.fileUrl(raw('offline.jpg'))
clock.offline = true; clock.now += 1000
assert.equal(await api.retryFileAccess(raw('offline.jpg')), true)
assert.notEqual(api.fileUrl(raw('offline.jpg')), offline)
assert.equal(api.fileUrl(raw('good.jpg')), good)
} finally { env.close() }
})
test('owned anchors fetch through the app downloader, while external and already handled links keep their behaviour', () => {
const env = runtime(), calls = []
try {
const event = (href, prevented = false) => ({
defaultPrevented: prevented,
target: { closest: () => ({ getAttribute: name => name === 'href' ? href : '' }) },
preventDefault() { this.defaultPrevented = true }, stopImmediatePropagation() { this.stopped = true },
})
const source = raw(encodeURIComponent('附件 +&%.pdf')), signed = env.api.fileUrl(source)
const click = event(signed)
assert.equal(env.api.handleFileLinkClick(click, (...args) => calls.push(args)), true)
assert.equal(click.defaultPrevented, true); assert.equal(click.stopped, true)
assert.equal(calls.length, 1)
assert.equal(calls[0][2], '附件 +&%.pdf')
assert.doesNotMatch(calls[0][0], /[?&](?:sign|ts|kid|appId)=/)
for (const href of ['https://other.example/file.pdf', 'blob:local-document', '/home']) {
const outside = event(href)
assert.equal(env.api.handleFileLinkClick(outside, () => assert.fail('unrelated link intercepted')), false)
assert.equal(outside.defaultPrevented, false)
}
assert.equal(env.api.handleFileLinkClick(event(signed, true), () => assert.fail('already handled')), false)
} finally { env.close() }
})
test('loaded thumbnails never reload on minute checks, expiry, or foreground clock sync; previews receive fresh signatures', async () => {
const env = runtime(), page = mountImages(env.api)
try {
page.images().forEach(image => image.props.onLoad(new Event('load')))
const before = page.images().map(image => image.props.src)
for (let minute = 0; minute < 31; minute++) {
env.clock.now += 60000
env.api.fileAccessRevision.value++
await vue.nextTick()
assert.deepEqual(page.images().map(image => image.props.src), before)
}
await env.api.refreshFileAccess(true); await settle()
assert.equal(page.loads.length, 2)
assert.deepEqual(page.images().map(image => image.props.src), before)
page.images()[0].props.onShow(); await settle()
const preview = page.images()[0].props.preview[0]
assert.notEqual(preview, before[0])
assert.equal(preview, env.api.fileUrl(raw('good.jpg')))
assert.ok(Number(new URL(preview).searchParams.get('ts')) + 1800 > env.clock.now / 1000)
assert.equal(page.loads.length, 2)
assert.equal(page.sources.value[0], raw('good.jpg'))
env.clock.now += 31 * 60000; env.api.fileAccessRevision.value++; await settle()
assert.equal(page.images()[0].props.preview[0], preview)
page.images()[0].props.onSwitch(0); await settle()
assert.notEqual(page.images()[0].props.preview[0], preview)
} finally { page.close(); env.close() }
})
test('failed thumbnails retry once without resetting neighbours; a changed business image still loads', async () => {
const env = runtime(), page = mountImages(env.api)
try {
page.images()[1].props.onLoad(new Event('load'))
const neighbour = page.images()[1].props.src
env.clock.now += 1000
page.images()[0].props.onError(new Event('error')); await settle()
assert.equal(page.loads.length, 3)
assert.equal(page.images()[1].props.src, neighbour)
page.images()[0].props.onError(new Event('error')); await settle()
assert.equal(page.loads.length, 3)
assert.deepEqual(page.failures, [0])
env.api.fileAccessRevision.value++; await settle()
assert.equal(page.loads.length, 3)
page.sources.value[0] = raw('replacement.jpg'); await settle()
assert.equal(new URL(page.images()[0].props.src).searchParams.get('fileName'), 'replacement.jpg')
assert.equal(page.images()[1].props.src, neighbour)
} finally { page.close(); env.close() }
})
test('offscreen images keep a valid URL for their first lazy request without reloading an already visible image', async () => {
const env = runtime(), page = mountImages(env.api)
try {
page.images()[1].props.onLoad(new Event('load'))
const before = page.images().map(image => image.props.src)
env.clock.now += 31 * 60000; env.api.fileAccessRevision.value++; await settle()
assert.notEqual(page.images()[0].props.src, before[0])
assert.equal(new URL(page.images()[0].props.src).searchParams.get('ts'), String(env.clock.now / 1000))
assert.equal(page.images()[1].props.src, before[1])
assert.equal(env.clock.requests, 0)
} finally { page.close(); env.close() }
})