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) } })