import { mount, type VueWrapper } from '@vue/test-utils' import { afterEach, describe, expect, it, vi } from 'vitest' import { NButton } from 'naive-ui' import { readFileSync } from 'node:fs' import DirectoryItem from './DirectoryItem.vue' let wrapper: VueWrapper | undefined afterEach(() => wrapper?.unmount()) describe('统一目录条目', () => { it('主体缩略图使用可选前导槽位,与标题一起保留满行点击区域', () => { wrapper = mount(DirectoryItem, { props: { active: true }, slots: { leading: '母版缩略图', default: '林默', eyebrow: '@CH0001' } }) expect(wrapper.classes()).toContain('has-leading') expect(wrapper.get('.directory-item-leading').text()).toBe('母版缩略图') expect(wrapper.get('.directory-item-title').text()).toBe('林默') expect(wrapper.get('.directory-item-leading').element.closest('button')).toBe(wrapper.element) }) it('编号、长标题和多项状态各占独立层级,沿用 Naive 按钮', () => { const title = '雨夜霓虹街全景与记忆当铺外的人群和闪烁灯牌' wrapper = mount(DirectoryItem, { props: { active: false }, slots: { eyebrow: 'BEAT 2 / SHOT 3', default: title, meta: '设计已保存待状态' } }) expect(wrapper.getComponent(NButton).props()).toMatchObject({ quaternary: true, block: true }) expect(wrapper.get('button').attributes('type')).toBe('button') expect(wrapper.get('.directory-item-eyebrow').text()).toBe('BEAT 2 / SHOT 3') expect(wrapper.get('.directory-item-title').text()).toBe(title) expect(wrapper.get('.directory-item-meta').findAll('.directory-status')).toHaveLength(2) expect(wrapper.find('.truncate').exists()).toBe(false) expect(wrapper.findAll('.directory-item-body > span')).toHaveLength(3) }) it('点击透传给业务层,选中态与无障碍属性保持同步', async () => { const onClick = vi.fn<() => void>() wrapper = mount(DirectoryItem, { props: { active: false }, attrs: { onClick }, slots: { default: '林默' } }) await wrapper.get('button').trigger('click') expect(onClick).toHaveBeenCalledOnce() expect(wrapper.attributes('aria-current')).toBeUndefined() await wrapper.setProps({ active: true }) expect(wrapper.get('button').classes()).toContain('selected') expect(wrapper.attributes('aria-current')).toBe('true') expect(wrapper.find('.directory-item-eyebrow').exists()).toBe(false) expect(wrapper.find('.directory-item-meta').exists()).toBe(false) }) it('显式保护最小高度、留白、换行和窄屏横向条目,避免旧按钮样式重新挤压目录', () => { // DOM 环境不计算视觉几何;这些约束不能替代浏览器验收。 const css = readFileSync('src/admin.css', 'utf8') expect(css).toMatch(/\.n-button\.directory-item\s*\{[^}]*min-height:\s*88px;[^}]*padding:\s*12px 14px/) expect(css).toMatch(/\.directory-item-body\s*\{[^}]*flex-direction:\s*column;[^}]*gap:\s*7px/) expect(css).toMatch(/\.directory-item-title\s*\{[^}]*overflow-wrap:\s*anywhere/) expect(css).toMatch(/\.directory-list-content\s*\{[^}]*flex-direction:\s*row;[^}]*width:\s*max-content/) expect(css).toContain('grid-template-columns: clamp(240px, 22%, 280px) minmax(0, 1fr)') expect(css).toMatch(/\.identity-subject-list-content\s*\{[^}]*gap:\s*2px;[^}]*padding:\s*0 0 8px/) expect(css).toMatch(/\.n-button\.identity-subject-item\s*\{[^}]*min-height:\s*86px;[^}]*padding:\s*10px 14px/) expect(css).not.toMatch(/\.n-button\.identity-subject-item\s*\{[^}]*border-bottom:/) expect(css).toMatch(/\.identity-thumbnail\s*\{[^}]*width:\s*48px;[^}]*height:\s*64px/) expect(css).toMatch(/\.n-button\.identity-subject-item:not\(\.selected\)\s*\{[^}]*var\(--app-surface\)/) }) it('侧栏与主体、镜头、剧集目录统一直角满行,保留文字内边距和移动端滚动', () => { // happy-dom 无法计算伪元素几何,保护实际控制选中背景宽度的 CSS 规则。 const css = readFileSync('src/admin.css', 'utf8') expect(css).toMatch( /\.admin-sider \.n-menu \.n-menu-item-content::before\s*\{[^}]*left:\s*0;[^}]*right:\s*0;[^}]*border-radius:\s*0/ ) expect(css).toMatch(/\.directory-list-content\s*\{[^}]*padding:\s*8px 0 16px/) expect(css).toMatch(/\.reader-directory-content\s*\{[^}]*padding:\s*4px 0 18px/) expect(css).not.toMatch(/\.reader-directory-content\s*\{[^}]*padding-inline:\s*[1-9]/) for (const selector of ['directory-item', 'reader-episode-link']) { expect(css).toMatch( new RegExp( `\\.n-button\\.${selector}\\s*\\{[^}]*width:\\s*100%;[^}]*padding:\\s*12px 14px;[^}]*border-radius:\\s*0` ) ) } expect(css).toMatch(/\.directory-group-heading\s*\{[^}]*padding:\s*10px 14px/) expect(css).toMatch(/\.directory-list-content\s*\{[^}]*flex-direction:\s*row;[^}]*width:\s*max-content/) }) it('导出工具栏与筛选器使用受约束的网格,不让 Tabs 和 Select 默认宽度强制换行', () => { const css = readFileSync('src/admin.css', 'utf8') expect(css).toMatch(/\.result-toolbar\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) auto/) expect(css).toMatch( /\.form-image-filters\s*\{[^}]*grid-template-columns:\s*minmax\(200px, 360px\) 152px minmax\(260px, 1fr\) auto/ ) expect(css).toMatch( /@container \(max-width: 900px\)\s*\{\s*\.form-image-filters\s*\{[^}]*minmax\(0, 1fr\) 152px/ ) }) })