mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-21 03:51:47 +00:00
Merge pull request #7320 from ouchihao/codex/bug-image-upload-read-order
fix(upload): cancel superseded image reads
This commit is contained in:
@@ -56,7 +56,7 @@
|
||||
v-if="modelValue"
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm text-red-600 hover:text-red-700 dark:text-red-400"
|
||||
@click="$emit('update:modelValue', '')"
|
||||
@click="removeImage"
|
||||
>
|
||||
<Icon name="trash" size="sm" class="mr-1.5" :stroke-width="2" />
|
||||
{{ resolvedRemoveLabel }}
|
||||
@@ -69,7 +69,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { sanitizeSvg } from '@/utils/sanitize'
|
||||
@@ -98,6 +98,14 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const error = ref('')
|
||||
let reader: FileReader | null = null
|
||||
|
||||
onBeforeUnmount(() => reader?.abort())
|
||||
|
||||
function removeImage() {
|
||||
reader?.abort()
|
||||
emit('update:modelValue', '')
|
||||
}
|
||||
|
||||
const resolvedUploadLabel = computed(() => props.uploadLabel || t('common.upload'))
|
||||
const resolvedRemoveLabel = computed(() => props.removeLabel || t('common.remove'))
|
||||
@@ -118,6 +126,7 @@ function handleUpload(event: Event) {
|
||||
error.value = ''
|
||||
|
||||
if (!file) return
|
||||
reader?.abort()
|
||||
|
||||
if (props.maxSize && file.size > props.maxSize) {
|
||||
error.value = t('common.fileTooLargeKb', {
|
||||
@@ -128,7 +137,7 @@ function handleUpload(event: Event) {
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader = new FileReader()
|
||||
if (props.mode === 'svg') {
|
||||
reader.onload = (e) => {
|
||||
const text = e.target?.result as string
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, mount } from '@vue/test-utils'
|
||||
import ImageUpload from '../ImageUpload.vue'
|
||||
|
||||
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) }))
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
class ControlledReader {
|
||||
static instances: ControlledReader[] = []
|
||||
onload: ((event: { target: { result: string } }) => void) | null = null
|
||||
onerror: (() => void) | null = null
|
||||
aborted = false
|
||||
constructor() { ControlledReader.instances.push(this) }
|
||||
readAsText = vi.fn()
|
||||
readAsDataURL = vi.fn()
|
||||
abort() { this.aborted = true }
|
||||
complete(result: string) {
|
||||
if (!this.aborted) this.onload?.({ target: { result } })
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
ControlledReader.instances = []
|
||||
vi.stubGlobal('FileReader', ControlledReader)
|
||||
})
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
function uploader(mode: 'image' | 'svg' = 'image') {
|
||||
const wrapper = mount(ImageUpload, { props: { modelValue: 'existing', mode } })
|
||||
const input = wrapper.get('input')
|
||||
return {
|
||||
wrapper,
|
||||
async choose(type = 'image/png') {
|
||||
Object.defineProperty(input.element, 'files', {
|
||||
configurable: true, value: [new File(['image'], 'upload', { type })]
|
||||
})
|
||||
await input.trigger('change')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('image upload read lifecycle', () => {
|
||||
it.each(['image', 'svg'] as const)('keeps the newest %s selection when reads finish out of order', async mode => {
|
||||
const { wrapper, choose } = uploader(mode)
|
||||
await choose()
|
||||
await choose()
|
||||
ControlledReader.instances[1].complete('new image')
|
||||
ControlledReader.instances[0].complete('old image')
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([['new image']])
|
||||
})
|
||||
|
||||
it('does not restore an image after the remove button is clicked', async () => {
|
||||
const { wrapper, choose } = uploader()
|
||||
await choose()
|
||||
await wrapper.get('button').trigger('click')
|
||||
ControlledReader.instances[0].complete('removed image')
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([['']])
|
||||
})
|
||||
|
||||
it('does not apply a pending image after a later invalid selection', async () => {
|
||||
const { wrapper, choose } = uploader()
|
||||
await choose()
|
||||
await choose('text/plain')
|
||||
ControlledReader.instances[0].complete('old image')
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect(wrapper.text()).toContain('common.selectImageFile')
|
||||
})
|
||||
|
||||
it('aborts a pending read when the uploader unmounts', async () => {
|
||||
const { wrapper, choose } = uploader()
|
||||
await choose()
|
||||
wrapper.unmount()
|
||||
expect(ControlledReader.instances[0].aborted).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user