452 lines
25 KiB
TypeScript
452 lines
25 KiB
TypeScript
import { h } from 'vue'
|
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { NConfigProvider, NDrawer, NDrawerContent, NMenu, NModal, NPopover, NScrollbar, NTooltip } from 'naive-ui'
|
|
import App from '@/App.vue'
|
|
import WorkspacePage from '@/components/ui/WorkspacePage.vue'
|
|
import AppDialog from '@/components/ui/AppDialog.vue'
|
|
import WorkspaceTools from '@/components/ui/WorkspaceTools.vue'
|
|
import ProjectLayout from '@/features/projects/ProjectLayout.vue'
|
|
import ConfirmAction from '@/features/workflows/ConfirmAction.vue'
|
|
import ThemeToggle from '@/components/ui/ThemeToggle.vue'
|
|
import { projectsApi } from '@/features/projects/api'
|
|
import { readFileSync } from 'node:fs'
|
|
import { drawerPanel } from '@/testing/naive'
|
|
import { readAllStyles } from '@/testing/styles'
|
|
|
|
let wrapper: VueWrapper | undefined
|
|
afterEach(() => {
|
|
wrapper?.unmount()
|
|
wrapper = undefined
|
|
localStorage.clear()
|
|
document.body.innerHTML = ''
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('管理后台组件边界', () => {
|
|
it('Tabs suffix 与标签共用导航行,窄屏换行但不产生外层滚动', () => {
|
|
// DOM 测试不计算坐标;保护对齐尺寸与容器断点,视觉验收仍需浏览器。
|
|
const css = readAllStyles()
|
|
expect(css).toMatch(/\.workspace-tabs > \.n-tabs-nav\s*\{[^}]*align-items:\s*stretch;/)
|
|
expect(css).toMatch(/\.workspace-tabs \.n-tabs-nav__suffix\s*\{[^}]*align-items:\s*center;/)
|
|
expect(css).toMatch(/\.tabs-toolbar-actions\s*\{[^}]*align-items:\s*center;[^}]*min-height:\s*40px;/)
|
|
expect(css).toMatch(
|
|
/\.tabs-toolbar-actions \.n-button\.icon-button\s*\{[^}]*width:\s*32px;[^}]*min-width:\s*32px;[^}]*height:\s*32px;[^}]*padding:\s*0/
|
|
)
|
|
expect(css).toContain('@container tabs-toolbar (max-width: 600px)')
|
|
expect(css).not.toContain('.result-toolbar > .n-button')
|
|
})
|
|
|
|
it('滚动边界样式包含 document 锁定和工作区收缩约束', () => {
|
|
// happy-dom 不计算视口几何;这里只保护 CSS 契约,真实滚动仍需浏览器验收。
|
|
const adminCss = readAllStyles()
|
|
expect(adminCss).toMatch(/html,\s*body,\s*#app,\s*\.app-provider\s*\{[^}]*overflow:\s*hidden/)
|
|
expect(adminCss).toMatch(/\.workspace-page\s*\{[^}]*min-height:\s*0;[^}]*overflow:\s*hidden/)
|
|
})
|
|
it('业务样式不再直接开启浏览器原生滚动条', () => {
|
|
const css = readAllStyles()
|
|
expect(css).not.toMatch(/overflow(?:-[xy])?\s*:\s*(?:auto|scroll)\b/)
|
|
})
|
|
it('历史缩略图尺寸在非分层样式中覆盖 Naive 按钮,原图不能撑大预览或横向条目', () => {
|
|
// happy-dom 不计算几何;专门保护此前失效的层叠和固定尺寸约束。
|
|
const css = readFileSync('src/styles/admin.css', 'utf8')
|
|
expect(css).not.toContain('@layer')
|
|
const all = readAllStyles()
|
|
expect(all).toMatch(
|
|
/\.n-button\.image-history-item\s*\{[^}]*width:\s*128px;[^}]*min-width:\s*128px;[^}]*max-width:\s*128px;[^}]*flex:\s*0 0 128px;[^}]*padding:\s*6px/
|
|
)
|
|
expect(all).toMatch(/\.image-history-item \.asset-image\s*\{[^}]*height:\s*88px;[^}]*min-height:\s*0/)
|
|
expect(all).toMatch(
|
|
/\.asset-image\.asset-image-preview\s*\{[^}]*height:\s*min\(42dvh,\s*420px\);[^}]*aspect-ratio:\s*auto/
|
|
)
|
|
expect(all).toMatch(/\.image-history\.n-scrollbar\s*\{[^}]*max-width:\s*100%;[^}]*min-width:\s*0/)
|
|
expect(all).toMatch(/\.image-history-content\s*\{[^}]*display:\s*flex;[^}]*width:\s*max-content/)
|
|
})
|
|
it('选角面板保留说明间距,卡片按可用宽度排列并覆盖按钮默认高度', () => {
|
|
// 保护间距与换行契约,真实宽高和暗色效果仍需浏览器验收。
|
|
const css = readAllStyles()
|
|
expect(css).toMatch(/\.identity-tools-intro\s*\{[^}]*margin-bottom:\s*24px/)
|
|
expect(css).toMatch(
|
|
/\.casting-list\s*\{[^}]*repeat\(auto-fit, minmax\(min\(100%, 260px\), 1fr\)\);[^}]*gap:\s*12px;[^}]*margin-top:\s*24px/
|
|
)
|
|
expect(css).toMatch(
|
|
/\.n-button\.casting-item\s*\{[^}]*height:\s*auto;[^}]*min-height:\s*76px;[^}]*padding:\s*12px 14px/
|
|
)
|
|
expect(css).toMatch(/\.casting-item-name\s*\{[^}]*overflow-wrap:\s*anywhere/)
|
|
expect(css).toMatch(/\.casting-item-status\s*\{[^}]*flex-shrink:\s*0/)
|
|
expect(css).toMatch(/\.n-button\.casting-item\.selected\s*\{[^}]*var\(--app-selected\)[^}]*var\(--app-accent\)/)
|
|
})
|
|
it('拆解分栏跟随剩余高度收缩,移动端历史区不挤出结果区', () => {
|
|
// DOM 环境不计算几何,保护完整的 flex → grid → NScrollbar 高度链。
|
|
const css = readAllStyles()
|
|
expect(css).toMatch(/\.breakdown-results-section\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0/)
|
|
expect(css).toMatch(/\.breakdown-results-section > \.result-toolbar\s*\{[^}]*flex-shrink:\s*0/)
|
|
expect(css).toMatch(
|
|
/\.breakdown-results-section > \.content-with-history\s*\{[^}]*flex:\s*1;[^}]*height:\s*auto;[^}]*min-height:\s*0;[^}]*grid-template-rows:\s*minmax\(0, 1fr\)/
|
|
)
|
|
expect(css).toMatch(
|
|
/@media \(max-width: 800px\)[\s\S]*?\.breakdown-results-section \.content-with-history:not\(\.history-hidden\) \.history-panel\s*\{[^}]*inset:\s*0/
|
|
)
|
|
expect(css).toMatch(/\.panel-scroll\.n-scrollbar\s*\{[^}]*height:\s*100%;[^}]*min-height:\s*0/)
|
|
})
|
|
it('标题与内容使用各自滚动容器,不随正文一起滚动', () => {
|
|
wrapper = mount(WorkspacePage, {
|
|
slots: { header: '<h2>固定操作区</h2>', default: '<p>正文</p>' }
|
|
})
|
|
expect(wrapper.get('.workspace-heading').text()).toBe('固定操作区')
|
|
const content = wrapper.get('.workspace-scroll')
|
|
expect(content.classes()).toContain('n-scrollbar')
|
|
expect(content.text()).toBe('正文')
|
|
expect(content.find('h2').exists()).toBe(false)
|
|
expect(wrapper.get('.workspace-heading-scroll .n-scrollbar-content').text()).toBe('固定操作区')
|
|
})
|
|
|
|
it('分栏工作区不再包一层整页滚动', () => {
|
|
wrapper = mount(WorkspacePage, {
|
|
props: { split: true },
|
|
slots: { default: '<aside>目录</aside><article>正文</article>' }
|
|
})
|
|
expect(wrapper.findComponent(NScrollbar).exists()).toBe(false)
|
|
expect(wrapper.get('.workspace-split').findAll(':scope > *')).toHaveLength(2)
|
|
})
|
|
|
|
it('图标主题菜单保留三种模式,设置入口位于侧栏底部并支持折叠', async () => {
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [
|
|
{ path: '/projects', component: { render: () => h('p', '项目列表') } },
|
|
{
|
|
path: '/projects/:projectId/:workspace',
|
|
component: { render: () => h(WorkspacePage, {}, () => '镜头详情') }
|
|
}
|
|
]
|
|
})
|
|
await router.push('/projects/test/production')
|
|
wrapper = mount(App, { attachTo: document.body, global: { plugins: [router] } })
|
|
await flushPromises()
|
|
expect(wrapper.findAll('.n-menu .n-menu-item')).toHaveLength(8)
|
|
expect(wrapper.get('#main-content .workspace-page').text()).toBe('镜头详情')
|
|
expect(wrapper.get('.theme-toggle').text()).toBe('')
|
|
expect(wrapper.get('.theme-toggle').attributes('aria-haspopup')).toBe('menu')
|
|
expect(wrapper.getComponent(ThemeToggle).findComponent(NTooltip).exists()).toBe(false)
|
|
expect(wrapper.get('.theme-toggle').attributes('title')).toBeUndefined()
|
|
expect(wrapper.get('.theme-toggle svg').classes()).toContain('lucide-monitor')
|
|
expect(wrapper.get('.admin-topbar [aria-controls="main-navigation"] svg').classes()).toContain('lucide-menu')
|
|
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
for (const [key, label, icon] of [
|
|
['dark', '暗黑模式', 'moon'],
|
|
['light', '浅色模式', 'sun'],
|
|
['system', '跟随系统', 'monitor']
|
|
] as const) {
|
|
await wrapper.get('.theme-toggle').trigger('click')
|
|
await flushPromises()
|
|
const option = [...document.querySelectorAll<HTMLElement>('.n-dropdown-option-body')].find(
|
|
item => item.textContent?.trim() === label
|
|
)
|
|
expect(option).toBeDefined()
|
|
option!.click()
|
|
await flushPromises()
|
|
expect(localStorage.getItem('drama-studio-theme')).toBe(key)
|
|
expect(wrapper.get('.theme-toggle svg').classes()).toContain(`lucide-${icon}`)
|
|
expect(wrapper.get('.theme-toggle').attributes('aria-label')).toBe(`主题模式:${label}`)
|
|
const expectedTheme = key === 'system' ? systemTheme : key
|
|
expect(document.documentElement.dataset.theme).toBe(expectedTheme)
|
|
expect(wrapper.getComponent(NConfigProvider).props('theme')?.name ?? 'light').toBe(expectedTheme)
|
|
}
|
|
expect(wrapper.find('.admin-topbar [aria-label="后端连接"]').exists()).toBe(false)
|
|
expect(wrapper.find('.admin-nav-scroll [aria-label="后端连接"]').exists()).toBe(false)
|
|
expect(wrapper.get('.admin-sider-footer [aria-label="后端连接"]').text()).toBe('后端连接')
|
|
expect(wrapper.getComponent(NMenu).props()).toMatchObject({
|
|
collapsed: false,
|
|
collapsedWidth: 64,
|
|
iconSize: 20,
|
|
collapsedIconSize: 20,
|
|
indent: 22,
|
|
rootIndent: 22
|
|
})
|
|
const firstMenuIcon = () => {
|
|
const [option] = wrapper!.getComponent(NMenu).props('options') as Array<{
|
|
icon: () => ReturnType<typeof h>
|
|
}>
|
|
return option!.icon()
|
|
}
|
|
expect(firstMenuIcon().type).not.toBe(NPopover)
|
|
expect(wrapper.get('.admin-sider-footer').findComponent(NPopover).exists()).toBe(false)
|
|
await wrapper.get('[aria-label="折叠侧栏"]').trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper.find('[aria-label="展开侧栏"]').exists()).toBe(true)
|
|
expect(wrapper.getComponent(NMenu).props('collapsed')).toBe(false)
|
|
expect(firstMenuIcon().type).not.toBe(NPopover)
|
|
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 64px')
|
|
expect(wrapper.get('.admin-sider-footer').findComponent(NPopover).exists()).toBe(false)
|
|
// 悬停只展示整列名称,不能重新创建菜单或改变正文占用宽度。
|
|
const menuElement = wrapper.get('.n-menu').element
|
|
await wrapper.get('.admin-sider').trigger('mouseenter')
|
|
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 64px')
|
|
expect(wrapper.get('.n-menu').element).toBe(menuElement)
|
|
await wrapper.get('.admin-sider').trigger('mouseleave')
|
|
const labels = wrapper.findAll('.n-menu-item-content-header').map(item => item.text())
|
|
expect(labels).toEqual([
|
|
'我的剧本',
|
|
'剧本创作',
|
|
'剧本拆解',
|
|
'视觉风格',
|
|
'主体身份',
|
|
'形态图片',
|
|
'分镜设计',
|
|
'镜头生产'
|
|
])
|
|
const settings = wrapper.get('.admin-sider-footer [aria-label="后端连接"]')
|
|
expect(settings.text()).toBe('后端连接')
|
|
await settings.trigger('click')
|
|
await flushPromises()
|
|
expect(document.querySelector('[role="dialog"][aria-label="后端连接"]')).not.toBeNull()
|
|
expect(wrapper.findAll('.admin-nav-scroll .n-menu .n-menu-item')).toHaveLength(8)
|
|
})
|
|
|
|
it('侧栏菜单独立滚动,窄屏改为不占正文宽度的全屏固定层', () => {
|
|
const css = readAllStyles()
|
|
expect(css).toMatch(/\.admin-sider\s*\{[^}]*display:\s*flex;[^}]*flex-direction:\s*column;[^}]*height:\s*100%/)
|
|
expect(css).toMatch(
|
|
/\.admin-nav-scroll\.n-scrollbar\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0;[^}]*min-width:\s*var\(--admin-sider-expanded\)/
|
|
)
|
|
expect(css).toMatch(/\.admin-sider\.is-collapsed \.n-menu-item-content-header\s*\{[^}]*opacity:\s*0/)
|
|
expect(css).toMatch(/\.admin-sider\.is-collapsed \.n-menu-item-content__icon\s*\{[^}]*pointer-events:\s*none/)
|
|
expect(css).toMatch(/\.admin-sider-footer\s*\{[^}]*flex-shrink:\s*0/)
|
|
expect(css).toMatch(/\.admin-sider\.is-collapsed \.admin-settings-label\s*\{[^}]*opacity:\s*0/)
|
|
expect(css).toMatch(/\.admin-sider\.is-collapsed \.admin-brand span\s*\{[^}]*opacity:\s*0/)
|
|
expect(css).toMatch(/@media \(max-width: 800px\)[\s\S]*?\.admin-brand\s*\{[^}]*height:\s*48px/)
|
|
expect(css).toContain('@media (max-width: 800px)')
|
|
expect(css).toMatch(/\.admin-sider\s*\{[^}]*transition:\s*width\s+0\.32s/)
|
|
expect(css).toMatch(/\.workspace-tools-body\s*\{[^}]*padding:\s*16px/)
|
|
expect(css).toMatch(/\.workspace-tools-drawer \.n-drawer-header[\s\S]*?padding:\s*16px/)
|
|
expect(css).toMatch(
|
|
/@media \(max-width: 800px\)[\s\S]*?\.admin-sider\s*\{[^}]*position:\s*fixed;[^}]*inset:\s*0;[^}]*width:\s*100%\s*!important;[^}]*transform:\s*translateX\(0\)/
|
|
)
|
|
expect(css).toMatch(
|
|
/\.admin-sider\.is-collapsed\s*\{[^}]*transform:\s*translateX\(-100%\);[^}]*visibility:\s*hidden;[^}]*pointer-events:\s*none/
|
|
)
|
|
expect(css).toMatch(/\.admin-layout > \.admin-main\s*\{[^}]*width:\s*100%;[^}]*min-width:\s*0/)
|
|
expect(css).toMatch(/\.mobile-sider-close\s*\{[^}]*display:\s*flex;[^}]*margin-right:\s*8px/)
|
|
expect(css).toContain('@media (prefers-reduced-motion: reduce)')
|
|
expect(css).toContain('.admin-sider.is-collapsed:is(:hover, :has(:focus-visible)) .n-menu-item-content-header')
|
|
expect(css).toContain('clip-path: inset(0);')
|
|
expect(css).toContain('transform: translateX(-6px);')
|
|
expect(css).not.toContain('.admin-sider-mask')
|
|
expect(css).not.toContain('.admin-sider.is-overlay')
|
|
for (const path of ['storyboard/StoryboardPage.vue', 'production/ProductionPage.vue']) {
|
|
const source = readFileSync(`src/features/${path}`, 'utf8')
|
|
expect(source).not.toContain('<span>剧集</span')
|
|
expect(source).toMatch(/aria-label="选择(?:分镜|生产)剧集"/)
|
|
}
|
|
})
|
|
|
|
it('窄屏导航关闭时移出视口,打开后全屏显示并可从内部关闭', async () => {
|
|
const realMatchMedia = window.matchMedia.bind(window)
|
|
vi.stubGlobal('matchMedia', (query: string) => {
|
|
if (query.includes('max-width: 800px')) {
|
|
return {
|
|
matches: true,
|
|
media: query,
|
|
addEventListener() {},
|
|
removeEventListener() {},
|
|
addListener() {},
|
|
removeListener() {},
|
|
dispatchEvent() {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return realMatchMedia(query)
|
|
})
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [
|
|
{ path: '/projects', component: { render: () => h('p', '项目列表') } },
|
|
{
|
|
path: '/projects/:projectId/:workspace',
|
|
component: { render: () => h(WorkspacePage, {}, () => '镜头详情') }
|
|
}
|
|
]
|
|
})
|
|
await router.push('/projects/test/production')
|
|
wrapper = mount(App, { attachTo: document.body, global: { plugins: [router] } })
|
|
await flushPromises()
|
|
expect(wrapper.findAll('.admin-sider')).toHaveLength(1)
|
|
expect(wrapper.get('.admin-sider').classes()).toContain('is-collapsed')
|
|
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 100%')
|
|
expect(wrapper.get('.admin-sider').attributes('aria-hidden')).toBe('true')
|
|
expect(wrapper.find('.admin-sider-mask').exists()).toBe(false)
|
|
expect(wrapper.find('.admin-nav-drawer').exists()).toBe(false)
|
|
expect(wrapper.getComponent(NMenu).props()).toMatchObject({
|
|
collapsedWidth: 64,
|
|
indent: 20,
|
|
rootIndent: 20
|
|
})
|
|
const [mobileOption] = wrapper.getComponent(NMenu).props('options') as Array<{
|
|
icon: () => ReturnType<typeof h>
|
|
}>
|
|
expect(mobileOption!.icon().type).not.toBe(NPopover)
|
|
expect(wrapper.get('.admin-sider-footer').findComponent(NPopover).exists()).toBe(false)
|
|
const collapsedBrand = wrapper.get('.admin-brand')
|
|
expect(collapsedBrand.classes()).toContain('is-collapsed')
|
|
expect(collapsedBrand.text()).toContain('短剧工作台')
|
|
const collapsedSettings = wrapper.get('.admin-sider-footer [aria-label="后端连接"]')
|
|
expect(collapsedSettings.text()).toBe('后端连接')
|
|
expect(collapsedSettings.classes()).toContain('is-collapsed')
|
|
expect(wrapper.get('.admin-topbar [aria-label="打开主菜单"] svg').classes()).toContain('lucide-menu')
|
|
await wrapper.get('.admin-topbar [aria-label="打开主菜单"]').trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper.findAll('.admin-sider')).toHaveLength(1)
|
|
expect(wrapper.get('.admin-sider').classes()).not.toContain('is-collapsed')
|
|
expect(wrapper.get('.admin-sider').classes()).not.toContain('is-overlay')
|
|
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 100%')
|
|
expect(wrapper.get('.admin-sider').attributes('aria-hidden')).toBeUndefined()
|
|
expect(wrapper.find('.admin-sider-mask').exists()).toBe(false)
|
|
expect(wrapper.get('.admin-sider').findAll('.n-menu .n-menu-item')).toHaveLength(8)
|
|
expect(wrapper.get('.admin-topbar [aria-label="关闭主菜单"]').attributes('aria-expanded')).toBe('true')
|
|
const expandedBrand = wrapper.get('.admin-brand')
|
|
expect(expandedBrand.classes()).not.toContain('is-collapsed')
|
|
expect(expandedBrand.text()).toContain('短剧工作台')
|
|
const expandedSettings = wrapper.get('.admin-sider-footer [aria-label="后端连接"]')
|
|
expect(expandedSettings.text()).toBe('后端连接')
|
|
expect(expandedSettings.classes()).not.toContain('is-collapsed')
|
|
expect(wrapper.get('.mobile-sider-close').attributes('aria-label')).toBe('关闭主菜单')
|
|
await wrapper.get('.mobile-sider-close').trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper.get('.admin-sider').classes()).toContain('is-collapsed')
|
|
expect(wrapper.get('.admin-sider').attributes('style')).toContain('width: 100%')
|
|
expect(wrapper.get('.admin-sider').attributes('aria-hidden')).toBe('true')
|
|
expect(wrapper.get('.admin-brand').text()).toContain('短剧工作台')
|
|
expect(wrapper.get('.admin-sider-footer [aria-label="后端连接"]').text()).toBe('后端连接')
|
|
})
|
|
|
|
it('长弹窗使用内部滚动,提交期间禁止遮罩和 Esc 关闭', async () => {
|
|
wrapper = mount(AppDialog, {
|
|
attachTo: document.body,
|
|
props: { open: true, title: '确认生图', description: '费用确认', busy: true },
|
|
slots: { default: '<p>内容</p>' }
|
|
})
|
|
await flushPromises()
|
|
expect(wrapper.getComponent(NModal).props()).toMatchObject({
|
|
closable: false,
|
|
maskClosable: false,
|
|
closeOnEsc: false
|
|
})
|
|
expect(document.querySelector('.dialog-body-scroll .n-scrollbar-container')).not.toBeNull()
|
|
})
|
|
|
|
it('抽屉挂载 body 覆盖完整视口,开关保留列表与配置草稿', async () => {
|
|
wrapper = mount(WorkspacePage, {
|
|
attachTo: document.body,
|
|
props: { split: true, compact: true },
|
|
slots: {
|
|
header: () => h(WorkspaceTools, { title: '测试操作' }, () => h('input', { 'aria-label': '配置草稿' })),
|
|
default: '<article class="test-list">主体列表</article>'
|
|
}
|
|
})
|
|
const list = wrapper.get('.test-list').element
|
|
expect(wrapper.find('.workspace-tools-drawer').exists()).toBe(false)
|
|
expect(wrapper.get('.workspace-heading').find('[data-workspace-tools]').exists()).toBe(true)
|
|
await wrapper.get('[data-workspace-tools]').trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper.getComponent(NDrawer).props()).toMatchObject({
|
|
to: 'body',
|
|
blockScroll: true,
|
|
placement: 'right',
|
|
width: 'min(960px, 100%)',
|
|
trapFocus: true,
|
|
closeOnEsc: true
|
|
})
|
|
expect(wrapper.getComponent(NDrawerContent).props('nativeScrollbar')).toBe(false)
|
|
expect(wrapper.find('.workspace-tools-drawer').exists()).toBe(false)
|
|
const container = drawerPanel().element.closest('.n-drawer-container')!
|
|
expect(container.parentElement).toBe(document.body)
|
|
expect(container.closest('main, .admin-content, .workspace-page')).toBeNull()
|
|
expect(container.querySelector('.n-drawer-mask')).not.toBeNull()
|
|
expect(drawerPanel().find('.n-scrollbar-container').exists()).toBe(true)
|
|
await drawerPanel().get('input[aria-label="配置草稿"]').setValue('保留配置')
|
|
await drawerPanel().get('[aria-label="关闭操作面板"]').trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper.getComponent(NDrawer).props('show')).toBe(false)
|
|
expect(wrapper.get('.test-list').element).toBe(list)
|
|
await wrapper.get('[data-workspace-tools]').trigger('click')
|
|
await flushPromises()
|
|
expect(drawerPanel().get<HTMLInputElement>('input[aria-label="配置草稿"]').element.value).toBe('保留配置')
|
|
container.querySelector<HTMLElement>('.n-drawer-mask')!.click()
|
|
await flushPromises()
|
|
expect(wrapper.getComponent(NDrawer).props('show')).toBe(false)
|
|
expect(wrapper.get('.test-list').element).toBe(list)
|
|
wrapper.unmount()
|
|
wrapper = undefined
|
|
expect(document.querySelector('.n-drawer-container')).toBeNull()
|
|
})
|
|
|
|
it('完成项目解锁全部左侧链接,标题不再显示状态标签和刷新按钮', async () => {
|
|
const detail = vi.spyOn(projectsApi, 'detail').mockResolvedValue({
|
|
id: 'navigation-test',
|
|
title: '导航测试项目',
|
|
topic: '测试主题',
|
|
style: null,
|
|
status: 'completed',
|
|
createdAt: '',
|
|
updatedAt: '',
|
|
episodes: [],
|
|
characters: [],
|
|
world: null
|
|
})
|
|
vi.spyOn(projectsApi, 'checkpoints').mockResolvedValue([])
|
|
const paths = [
|
|
'create-drama',
|
|
'breakdown',
|
|
'visual-style',
|
|
'subject-identity',
|
|
'subject-images',
|
|
'storyboard',
|
|
'production'
|
|
]
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [
|
|
{
|
|
path: '/projects/:projectId',
|
|
component: ProjectLayout,
|
|
children: paths.map(path => ({
|
|
path,
|
|
component: { render: () => h(WorkspacePage, {}, () => path) }
|
|
}))
|
|
}
|
|
]
|
|
})
|
|
await router.push('/projects/navigation-test/create-drama')
|
|
wrapper = mount(App, { attachTo: document.body, global: { plugins: [router] } })
|
|
await flushPromises()
|
|
expect(wrapper.find('[aria-label="项目工作流"]').exists()).toBe(false)
|
|
expect(wrapper.get('.project-title').text()).toBe('导航测试项目')
|
|
for (const path of paths) {
|
|
await wrapper.get(`.n-menu a[href="/projects/navigation-test/${path}"]`).trigger('click')
|
|
await flushPromises()
|
|
expect(router.currentRoute.value.path).toBe('/projects/navigation-test/' + path)
|
|
expect(wrapper.get('.project-view .workspace-page').text()).toBe(path)
|
|
}
|
|
expect(detail).toHaveBeenCalledOnce()
|
|
expect(wrapper.find('.project-header .n-button').exists()).toBe(false)
|
|
expect(wrapper.find('.project-header .n-tag').exists()).toBe(false)
|
|
})
|
|
|
|
it('确认按钮有稳定根元素承接调用方间距,生产按钮显式保留上下留白', () => {
|
|
wrapper = mount(ConfirmAction, {
|
|
props: { label: '测试操作', description: '仅展示,不提交请求' },
|
|
attrs: { class: 'mt-4' }
|
|
})
|
|
expect(wrapper.element.tagName).toBe('SPAN')
|
|
expect(wrapper.classes()).toContain('confirm-action')
|
|
expect(wrapper.classes()).toContain('mt-4')
|
|
expect(wrapper.get('button').text()).toBe('测试操作')
|
|
expect(readAllStyles()).toMatch(
|
|
/\.production-pipeline-card > \.confirm-action\s*\{[^}]*margin-block:\s*16px 4px/
|
|
)
|
|
})
|
|
})
|