feat: 收口组件样式并调整移动端侧栏
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { NImage } from 'naive-ui'
|
||||
import IdentityThumbnail from '@/features/subject-identity/components/IdentityThumbnail.vue'
|
||||
import { identityFixture, identityImageFixture } from '@/features/subject-identity/testing/fixtures'
|
||||
|
||||
let wrapper: VueWrapper | undefined
|
||||
let observers: {
|
||||
callback: IntersectionObserverCallback
|
||||
observe: ReturnType<typeof vi.fn<(element: Element) => void>>
|
||||
disconnect: ReturnType<typeof vi.fn<() => void>>
|
||||
}[] = []
|
||||
const props = {
|
||||
projectId: 'project',
|
||||
subjectId: 'subject-db-1',
|
||||
name: '书店',
|
||||
module: 'scene',
|
||||
identityId: 'identity-db-1',
|
||||
refreshKey: 0
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
observers = []
|
||||
vi.stubGlobal(
|
||||
'IntersectionObserver',
|
||||
class {
|
||||
observe = vi.fn<(element: Element) => void>()
|
||||
disconnect = vi.fn<() => void>()
|
||||
unobserve = vi.fn<(element: Element) => void>()
|
||||
constructor(callback: IntersectionObserverCallback) {
|
||||
observers.push({ callback, observe: this.observe, disconnect: this.disconnect })
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
wrapper = undefined
|
||||
document.body.innerHTML = ''
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** 仅触发主体缩略图的可见事件,不触发 NImage 内部图片懒加载观察器。 */
|
||||
async function reveal() {
|
||||
const observer = observers.find(item => item.observe.mock.calls.some(([element]) => element === wrapper!.element))!
|
||||
observer.callback([{ isIntersecting: true } as IntersectionObserverEntry], {} as IntersectionObserver)
|
||||
await flushPromises()
|
||||
expect(observer.disconnect).toHaveBeenCalled()
|
||||
}
|
||||
|
||||
/** 测试接口返回独立 Response,读取图片元数据不消耗生成额度。 */
|
||||
function response(data: unknown) {
|
||||
return new Response(JSON.stringify({ data }))
|
||||
}
|
||||
|
||||
describe('主体目录母版缩略图', () => {
|
||||
it.each([
|
||||
['character', '人物'],
|
||||
['scene', '场景'],
|
||||
['prop', '道具']
|
||||
])('没有母版的 %s 显示类型占位,不补查或生成图片', async (module, label) => {
|
||||
const fetcher = vi.fn<typeof fetch>()
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
wrapper = mount(IdentityThumbnail, { props: { ...props, module, source: null } })
|
||||
await reveal()
|
||||
expect(wrapper.text()).toBe(label)
|
||||
expect(wrapper.get('[role="img"]').attributes('aria-label')).toContain('暂无母版')
|
||||
expect(wrapper.findComponent(NImage).exists()).toBe(false)
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('直接复用目录母版地址,图片不抢占选择点击,加载失败显示默认占位', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>()
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
wrapper = mount(IdentityThumbnail, {
|
||||
attachTo: document.body,
|
||||
props: { ...props, source: '/storage/anchor.png' }
|
||||
})
|
||||
await reveal()
|
||||
expect(wrapper.getComponent(NImage).props()).toMatchObject({ objectFit: 'cover', previewDisabled: true })
|
||||
await wrapper.get('img').trigger('click')
|
||||
expect(document.querySelector('.n-image-preview-container')).toBeNull()
|
||||
await wrapper.get('img').trigger('error')
|
||||
expect(wrapper.get('[role="img"]').attributes('aria-label')).toContain('母版暂不可用')
|
||||
await wrapper.setProps({ source: '/storage/new-anchor.png' })
|
||||
expect(wrapper.get('img').attributes('src')).toContain('/storage/new-anchor.png')
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('进入可视区域后才 GET 图库,仅选择权威母版,刷新时重新读取', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(async () =>
|
||||
response([
|
||||
identityImageFixture({
|
||||
id: 'candidate',
|
||||
isAnchor: false,
|
||||
enabled: false,
|
||||
imageUrl: '/storage/candidate.png'
|
||||
}),
|
||||
identityImageFixture({
|
||||
id: 'front',
|
||||
isAnchor: false,
|
||||
viewType: 'front',
|
||||
imageUrl: '/storage/front.png'
|
||||
}),
|
||||
identityImageFixture({ imageUrl: '/storage/anchor.png' })
|
||||
])
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
wrapper = mount(IdentityThumbnail, { props })
|
||||
await flushPromises()
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
await reveal()
|
||||
expect(fetcher).toHaveBeenCalledOnce()
|
||||
expect(fetcher.mock.calls[0]?.[0]).toBe('/api/subjects/subject-db-1/identity/images')
|
||||
expect(wrapper.get('img').attributes('src')).toContain('/storage/anchor.png')
|
||||
await wrapper.setProps({ refreshKey: 1 })
|
||||
await flushPromises()
|
||||
expect(fetcher).toHaveBeenCalledTimes(2)
|
||||
expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true)
|
||||
})
|
||||
|
||||
it('目录未附带身份摘要时先校验正式身份,再读取母版', async () => {
|
||||
const fetcher = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockImplementation(async url =>
|
||||
response(String(url).endsWith('/images') ? [identityImageFixture()] : identityFixture())
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
wrapper = mount(IdentityThumbnail, { props: { ...props, identityId: undefined } })
|
||||
await reveal()
|
||||
expect(fetcher.mock.calls.map(([url]) => url)).toEqual([
|
||||
'/api/subjects/subject-db-1/identity',
|
||||
'/api/subjects/subject-db-1/identity/images'
|
||||
])
|
||||
expect(wrapper.find('img').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('切项目中止旧请求,即使迟到也不能覆盖新主体占位', async () => {
|
||||
let finish!: (response: Response) => void
|
||||
const fetcher = vi.fn<typeof fetch>().mockImplementation(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
finish = resolve
|
||||
})
|
||||
)
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
wrapper = mount(IdentityThumbnail, { props })
|
||||
await reveal()
|
||||
await wrapper.setProps({
|
||||
projectId: 'other-project',
|
||||
subjectId: 'other-subject',
|
||||
name: '另一主体',
|
||||
source: null
|
||||
})
|
||||
expect(fetcher.mock.calls[0]?.[1]?.signal?.aborted).toBe(true)
|
||||
finish(response([identityImageFixture()]))
|
||||
await flushPromises()
|
||||
expect(wrapper.find('img').exists()).toBe(false)
|
||||
expect(wrapper.get('[role="img"]').attributes('aria-label')).toBe('另一主体 · 暂无母版')
|
||||
})
|
||||
|
||||
it('候选与辅助视角不能冒充母版,跨身份图片返回错误占位', async () => {
|
||||
const fetcher = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(response([identityImageFixture({ isAnchor: false })]))
|
||||
.mockResolvedValueOnce(response([identityImageFixture({ identityId: 'wrong-identity' })]))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
wrapper = mount(IdentityThumbnail, { props })
|
||||
await reveal()
|
||||
expect(wrapper.get('[role="img"]').attributes('aria-label')).toContain('暂无母版')
|
||||
await wrapper.setProps({ refreshKey: 1 })
|
||||
await flushPromises()
|
||||
expect(wrapper.get('[role="img"]').attributes('aria-label')).toContain('母版暂不可用')
|
||||
expect(wrapper.find('img').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('危险地址不写入图片,无效图片也不会触发生图', () => {
|
||||
wrapper = mount(IdentityThumbnail, { props: { ...props, source: 'javascript:alert(1)' } })
|
||||
expect(wrapper.find('img').exists()).toBe(false)
|
||||
expect(wrapper.get('[role="img"]').attributes('aria-label')).toContain('母版暂不可用')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user