| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- import assert from 'node:assert/strict'
- import { readFileSync } from 'node:fs'
- import test from 'node:test'
- import { computed, reactive } from 'vue'
- import * as Vue from 'vue'
- import { compileTemplate, parse } from '@vue/compiler-sfc'
- import ts from 'typescript'
- import { isMpWebviewPath } from '../src/utils/mp-webview'
- const components = [
- { path: 'attachment-preview-dialog', mobileClass: 'attachment-preview-dialog--mobile' },
- { path: 'attachment-upload-field', mobileClass: 'attachment-upload-preview--mobile' }
- ]
- for (const component of components) {
- test(`${component.path}: teleported dialog mobile class follows /m routes, never desktop compact`, () => {
- const file = new URL(`../src/components/business/${component.path}/index.vue`, import.meta.url)
- const { descriptor } = parse(readFileSync(file, 'utf8'))
- const ast = ts.createSourceFile(
- 'preview.ts',
- descriptor.scriptSetup!.content,
- ts.ScriptTarget.Latest,
- true
- )
- const names = [
- 'route',
- 'useTiles',
- 'isMpPreviewToolbar',
- 'isCustomerPreviewToolbar',
- 'mpBtnSize'
- ]
- const source = ast.statements
- .filter(
- (node) =>
- ts.isVariableStatement(node) &&
- names.includes(node.declarationList.declarations[0].name.getText(ast))
- )
- .map((node) => node.getText(ast))
- .join('\n')
- const dialog = descriptor
- .template!.ast!.children.flatMap((node: any) =>
- node.tag === 'el-dialog' ? [node] : node.children || []
- )
- .find((node: any) => node.tag === 'el-dialog') as any
- assert.ok(dialog, 'real preview el-dialog must exist')
- assert.ok(dialog.props.some((prop: any) => prop.name === 'append-to-body'))
- const classBinding = dialog.props.find(
- (prop: any) => prop.name === 'bind' && prop.arg?.content === 'class'
- )
- assert.ok(classBinding, 'mobile styling must bind to the teleported dialog root')
- const code = ts.transpile(source, { target: ts.ScriptTarget.ESNext })
- for (const compact of [false, true]) {
- const route = reactive({ path: '/crm/customer' })
- const bindings = { computed, isMpWebviewPath, useRoute: () => route, props: { compact } }
- const state = new Function(
- ...Object.keys(bindings),
- `${code}\nreturn { isMpPreviewToolbar, mpBtnSize, isCustomerPreviewToolbar: typeof isCustomerPreviewToolbar === 'undefined' ? undefined : isCustomerPreviewToolbar, useTiles: typeof useTiles === 'undefined' ? undefined : useTiles }`
- )(...Object.values(bindings))
- const readClass = () =>
- new Function(
- 'isMpPreviewToolbar',
- 'useTiles',
- 'isCustomerPreviewToolbar',
- `return (${classBinding.exp.content})`
- )(
- state.isMpPreviewToolbar.value,
- state.useTiles?.value,
- state.isCustomerPreviewToolbar?.value
- )
- assert.equal(Boolean(readClass()[component.mobileClass]), false)
- assert.equal(
- state.mpBtnSize.value,
- component.path === 'attachment-upload-field' && compact ? 'small' : 'default'
- )
- route.path = '/m/crm/customer/form'
- assert.equal(readClass()[component.mobileClass], true)
- assert.equal(state.mpBtnSize.value, 'small')
- route.path = '/crm/customer'
- assert.equal(Boolean(readClass()[component.mobileClass]), false)
- }
- })
- }
- test('actual preview render applies touch ownership only to exact customer mobile route family', () => {
- const { descriptor } = parse(
- readFileSync(
- new URL('../src/components/business/attachment-preview-dialog/index.vue', import.meta.url),
- 'utf8'
- )
- )
- const ast = ts.createSourceFile(
- 'preview.ts',
- descriptor.scriptSetup!.content,
- ts.ScriptTarget.Latest,
- true
- )
- const names = ['route', 'isMpPreviewToolbar', 'isCustomerPreviewToolbar', 'mpBtnSize']
- const source = ast.statements
- .filter(
- (node) =>
- ts.isVariableStatement(node) &&
- names.includes(node.declarationList.declarations[0].name.getText(ast))
- )
- .map((node) => node.getText(ast))
- .join('\n')
- const route = reactive({ path: '/m/crm/customer' })
- const state = new Function(
- 'computed',
- 'useRoute',
- 'isMpWebviewPath',
- `${ts.transpile(source, { target: ts.ScriptTarget.ESNext })}; return { isMpPreviewToolbar, mpBtnSize, isCustomerPreviewToolbar: typeof isCustomerPreviewToolbar === 'undefined' ? undefined : isCustomerPreviewToolbar }`
- )(computed, () => route, isMpWebviewPath)
- const template = compileTemplate({
- id: 'preview',
- filename: 'index.vue',
- source: descriptor.template!.content
- })
- assert.deepEqual(template.errors, [])
- const exports: any = {}
- new Function(
- 'require',
- 'exports',
- ts.transpile(template.code, { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ESNext })
- )(
- () => ({
- ...Vue,
- resolveComponent: (name: string) => name,
- resolveDirective: () => undefined,
- withDirectives: (node: any) => node
- }),
- exports
- )
- const ctx = Vue.proxyRefs({
- ...state,
- visible: false,
- fileName: '附件',
- loading: false,
- kind: 'other',
- handleClosed: () => {}
- })
- for (const [path, owned] of [
- ['/m/crm/customer', true],
- ['/m/crm/customer/new', true],
- ['/m/crm/customer/123/edit', true],
- ['/m/crm/customer-extra', false],
- ['/m/crm/customerPool', false],
- ['/m/crm/quote/123', false],
- ['/m/crm/product', false],
- ['/crm/customer', false],
- ['/crm/customer/123', false]
- ] as const) {
- route.path = path
- const node = exports.render(ctx, [])
- assert.equal(node.props.class.includes('attachment-preview-dialog--customer'), owned, path)
- }
- })
|