import { afterEach, describe, expect, it, vi } from 'vitest' import { defineComponent, ref } from 'vue' import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' import ElementPlus from 'element-plus' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { compileStyle, parse } from 'vue/compiler-sfc' import AppDialog from './AppDialog.vue' import AppDrawer from './AppDrawer.vue' import DataTable from './DataTable.vue' let wrapper: VueWrapper | undefined const styles: HTMLStyleElement[] = [] function installStyles() { const add = (css: string) => { const style = document.createElement('style') style.textContent = css.replace(/@import[^;]+;/g, '') document.head.append(style) styles.push(style) } for (const name of ['base', 'el-dialog', 'el-drawer', 'el-form', 'el-form-item', 'el-select', 'el-button']) { add(readFileSync(resolve(process.cwd(), `node_modules/element-plus/theme-chalk/${name}.css`), 'utf8')) } for (const [name, component] of [['AppDialog', AppDialog], ['AppDrawer', AppDrawer], ['DataTable', DataTable]] as const) { const source = readFileSync(resolve(process.cwd(), `src/shared/components/${name}.vue`), 'utf8') for (const style of parse(source).descriptor.styles) { const compiled = compileStyle({ source: style.content, filename: `${name}.vue`, scoped: style.scoped, id: component.__scopeId || 'data-v-layout' }) expect(compiled.errors).toEqual([]) add(compiled.code) } } for (const name of ['adminDensityTokens', 'adminDensity', 'adminWorkbenchTokens', 'adminWorkbench']) { add(readFileSync(resolve(process.cwd(), `src/styles/${name}.css`), 'utf8')) } } async function render(template: string) { installStyles() wrapper = mount(defineComponent({ components: { AppDialog, AppDrawer, DataTable }, setup: () => ({ open: ref(true), childOpen: ref(true), name: ref(''), valid: ref(false) }), template, }), { attachTo: document.body, global: { plugins: [ElementPlus] } }) await flushPromises() return wrapper } function element(selector: string) { const result = document.querySelector(selector) expect(result, selector).not.toBeNull() return result! } const computed = (selector: string) => getComputedStyle(element(selector)) afterEach(() => { wrapper?.unmount() wrapper = undefined styles.splice(0).forEach(style => style.remove()) document.body.innerHTML = '' }) describe('real Element floating frame boundaries', () => { it.each(['workbench-table', 'workbench-toolbar'])('isolates %s toolbar actions from nested dialogs while preserving wrappers and dropdowns', async (surface) => { await render(`
`) expect(element('.toolbar-editor').closest('.data-table__business-actions')).not.toBeNull() // Check matching against real DOM, not a source-order approximation of the cascade. const workbench = compileStyle({ source: readFileSync(resolve(process.cwd(), 'src/styles/adminWorkbench.css'), 'utf8'), filename: 'workbench.css', id: 'toolbar-regression' }).rawResult!.root const toolbarSelectors: string[] = [] workbench.walkRules(rule => { if (/data-table__(toolbar|business-actions)/.test(rule.selector) && rule.selector.includes('el-button')) toolbarSelectors.push(...rule.selectors) }) for (const selector of ['.direct-action', '.wrapped-action', '.dropdown-action', '.split-actions .el-button', '.grouped-action', '.start-action', '.end-action']) { expect(toolbarSelectors.some(rule => element(selector).matches(rule)), selector).toBe(true) } for (const button of document.querySelectorAll('.toolbar-editor .el-button, .nested-table-action')) { expect(toolbarSelectors.filter(rule => button.matches(rule))).toEqual([]) } const table = parse(readFileSync(resolve(process.cwd(), 'src/shared/components/DataTable.vue'), 'utf8')).descriptor for (const style of table.styles) { const rules = compileStyle({ source: style.content, filename: 'DataTable.vue', scoped: style.scoped, id: DataTable.__scopeId || 'data-v-layout' }).rawResult!.root rules.walkRules(rule => { if (!/toolbar|business-actions|utility-actions/.test(rule.selector) || !rule.selector.includes('el-button')) return for (const button of document.querySelectorAll('.toolbar-editor .el-button, .nested-table-action')) { expect(rule.selectors.some(selector => button.matches(selector)), rule.selector).toBe(false) } }) } expect(computed('.toolbar-editor .app-dialog__footer > .el-button--primary').height).toBe('38px') expect(computed('.toolbar-editor .app-dialog__footer > .el-button--primary').boxShadow).not.toContain('4px 12px') }) it('keeps a comfortable parent from changing nested dense and native dialog chrome', async () => { await render(`

预览内容

预览内容

`) expect(element('.inner').closest('.outer')).not.toBeNull() expect(element('.outer').classList.contains('el-dialog')).toBe(true) for (const field of ['.parent-form > .el-form-item', '.parent-grid > .el-form-item']) { expect(computed(`.outer ${field}`).marginBottom).toBe('20px') expect(computed(`.outer ${field} > .el-form-item__label`).marginBottom).toBe('8px') expect(computed(`.outer ${field} > .el-form-item__label`).fontWeight).toBe('500') expect(computed(`.outer ${field} .el-select__wrapper`).minHeight).toBe('40px') } for (const part of ['header', 'body', 'footer']) { expect(computed(`.inner > .el-dialog__${part}`).padding).toBe(computed(`.reference > .el-dialog__${part}`).padding) } expect(computed('.nested-native > .el-dialog__header').padding).toBe(computed('.reference-native > .el-dialog__header').padding) expect(computed('.nested-native > .el-dialog__body').overflowY).toBe(computed('.reference-native > .el-dialog__body').overflowY) expect(computed('.inner > .el-dialog__header > .el-dialog__title').paddingRight).toBe(computed('.reference > .el-dialog__header > .el-dialog__title').paddingRight) for (const form of ['.density-form', '.el-form--label-top']) { expect(computed(`.inner ${form} .el-form-item`).marginBottom).toBe(computed(`.reference ${form} .el-form-item`).marginBottom) expect(computed(`.inner ${form} .el-form-item__label`).marginBottom).toBe(computed(`.reference ${form} .el-form-item__label`).marginBottom) expect(computed(`.inner ${form} .el-form-item__label`).fontWeight).toBe(computed(`.reference ${form} .el-form-item__label`).fontWeight) expect(computed(`.inner ${form} .el-select__wrapper`).minHeight).toBe(computed(`.reference ${form} .el-select__wrapper`).minHeight) } }) it('isolates drawer chrome and honors an explicitly teleported nested dialog', async () => { await render(`

详情

编辑内容

详情

`) expect(element('.outer-drawer').classList.contains('el-drawer')).toBe(true) expect(element('.nested-drawer').closest('.outer-drawer')).not.toBeNull() expect(element('.portal').closest('.outer-drawer')).toBeNull() expect(element('.portal').classList.contains('admin-workbench')).toBe(true) for (const part of ['header', 'body']) { expect(computed(`.nested-drawer > .el-drawer__${part}`).padding).toBe(computed(`.reference-drawer > .el-drawer__${part}`).padding) } }) it('keeps the native header and footer around a flexible body through fullscreen and reopen', async () => { const view = await render(` `) expect(element('.editor > .el-dialog__header > .el-dialog__title').textContent).toBe('船期表单') expect(computed('.editor > .el-dialog__body').flexGrow).toBe('1') expect(computed('.editor > .el-dialog__body').minHeight).toBe('0px') expect(computed('.editor > .el-dialog__body').overflowY).toBe('auto') expect(computed('.editor > .el-dialog__footer').flexShrink).toBe('0') expect(computed('.editor > .el-dialog__footer').flexWrap).toBe('wrap') await view.get('input[aria-label="船名"]').setValue('已填写船名') element('.editor button[aria-label="展开全屏"]').click() await flushPromises() expect(element('.editor').classList.contains('is-fullscreen')).toBe(true) expect(computed('.editor > .el-dialog__body').flexGrow).toBe('1') expect((element('.editor input') as HTMLInputElement).value).toBe('已填写船名') const dialog = view.findComponent(AppDialog) dialog.vm.$emit('update:modelValue', false) await flushPromises() dialog.vm.$emit('update:modelValue', true) await flushPromises() expect(element('.editor').classList.contains('is-fullscreen')).toBe(false) expect((element('.editor input') as HTMLInputElement).value).toBe('已填写船名') expect(element('.editor > .el-dialog__footer').querySelectorAll('button')).toHaveLength(4) }) it('preserves workspace dimensions and lets multiline cell errors occupy space inside long forms', async () => { await render(`
明细 {{ index }}
`) expect(computed('.workspace').height).toBe('92vh') expect(computed('.workspace').maxHeight).toBe('92vh') expect(element('.workspace .el-form-item.is-required')).toBeDefined() await vi.waitFor(() => expect(document.querySelector('.workspace .el-form-item__error')).not.toBeNull()) expect(element('.workspace .el-form-item__error').textContent).toContain('下一行字段') expect(computed('.workspace .el-form-item__error').position).toBe('static') expect(computed('.workspace .el-form-item__error').flexBasis).toBe('100%') expect(element('.workspace table').querySelectorAll('tr')).toHaveLength(31) expect(element('.workspace > .el-dialog__footer').closest('.el-dialog__body')).toBeNull() }) })