mobile-attachment-preview.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import test from 'node:test'
  4. import { computed, reactive } from 'vue'
  5. import * as Vue from 'vue'
  6. import { compileTemplate, parse } from '@vue/compiler-sfc'
  7. import ts from 'typescript'
  8. import { isMpWebviewPath } from '../src/utils/mp-webview'
  9. const components = [
  10. { path: 'attachment-preview-dialog', mobileClass: 'attachment-preview-dialog--mobile' },
  11. { path: 'attachment-upload-field', mobileClass: 'attachment-upload-preview--mobile' }
  12. ]
  13. for (const component of components) {
  14. test(`${component.path}: teleported dialog mobile class follows /m routes, never desktop compact`, () => {
  15. const file = new URL(`../src/components/business/${component.path}/index.vue`, import.meta.url)
  16. const { descriptor } = parse(readFileSync(file, 'utf8'))
  17. const ast = ts.createSourceFile(
  18. 'preview.ts',
  19. descriptor.scriptSetup!.content,
  20. ts.ScriptTarget.Latest,
  21. true
  22. )
  23. const names = [
  24. 'route',
  25. 'useTiles',
  26. 'isMpPreviewToolbar',
  27. 'isCustomerPreviewToolbar',
  28. 'mpBtnSize'
  29. ]
  30. const source = ast.statements
  31. .filter(
  32. (node) =>
  33. ts.isVariableStatement(node) &&
  34. names.includes(node.declarationList.declarations[0].name.getText(ast))
  35. )
  36. .map((node) => node.getText(ast))
  37. .join('\n')
  38. const dialog = descriptor
  39. .template!.ast!.children.flatMap((node: any) =>
  40. node.tag === 'el-dialog' ? [node] : node.children || []
  41. )
  42. .find((node: any) => node.tag === 'el-dialog') as any
  43. assert.ok(dialog, 'real preview el-dialog must exist')
  44. assert.ok(dialog.props.some((prop: any) => prop.name === 'append-to-body'))
  45. const classBinding = dialog.props.find(
  46. (prop: any) => prop.name === 'bind' && prop.arg?.content === 'class'
  47. )
  48. assert.ok(classBinding, 'mobile styling must bind to the teleported dialog root')
  49. const code = ts.transpile(source, { target: ts.ScriptTarget.ESNext })
  50. for (const compact of [false, true]) {
  51. const route = reactive({ path: '/crm/customer' })
  52. const bindings = { computed, isMpWebviewPath, useRoute: () => route, props: { compact } }
  53. const state = new Function(
  54. ...Object.keys(bindings),
  55. `${code}\nreturn { isMpPreviewToolbar, mpBtnSize, isCustomerPreviewToolbar: typeof isCustomerPreviewToolbar === 'undefined' ? undefined : isCustomerPreviewToolbar, useTiles: typeof useTiles === 'undefined' ? undefined : useTiles }`
  56. )(...Object.values(bindings))
  57. const readClass = () =>
  58. new Function(
  59. 'isMpPreviewToolbar',
  60. 'useTiles',
  61. 'isCustomerPreviewToolbar',
  62. `return (${classBinding.exp.content})`
  63. )(
  64. state.isMpPreviewToolbar.value,
  65. state.useTiles?.value,
  66. state.isCustomerPreviewToolbar?.value
  67. )
  68. assert.equal(Boolean(readClass()[component.mobileClass]), false)
  69. assert.equal(
  70. state.mpBtnSize.value,
  71. component.path === 'attachment-upload-field' && compact ? 'small' : 'default'
  72. )
  73. route.path = '/m/crm/customer/form'
  74. assert.equal(readClass()[component.mobileClass], true)
  75. assert.equal(state.mpBtnSize.value, 'small')
  76. route.path = '/crm/customer'
  77. assert.equal(Boolean(readClass()[component.mobileClass]), false)
  78. }
  79. })
  80. }
  81. test('actual preview render applies touch ownership only to exact customer mobile route family', () => {
  82. const { descriptor } = parse(
  83. readFileSync(
  84. new URL('../src/components/business/attachment-preview-dialog/index.vue', import.meta.url),
  85. 'utf8'
  86. )
  87. )
  88. const ast = ts.createSourceFile(
  89. 'preview.ts',
  90. descriptor.scriptSetup!.content,
  91. ts.ScriptTarget.Latest,
  92. true
  93. )
  94. const names = ['route', 'isMpPreviewToolbar', 'isCustomerPreviewToolbar', 'mpBtnSize']
  95. const source = ast.statements
  96. .filter(
  97. (node) =>
  98. ts.isVariableStatement(node) &&
  99. names.includes(node.declarationList.declarations[0].name.getText(ast))
  100. )
  101. .map((node) => node.getText(ast))
  102. .join('\n')
  103. const route = reactive({ path: '/m/crm/customer' })
  104. const state = new Function(
  105. 'computed',
  106. 'useRoute',
  107. 'isMpWebviewPath',
  108. `${ts.transpile(source, { target: ts.ScriptTarget.ESNext })}; return { isMpPreviewToolbar, mpBtnSize, isCustomerPreviewToolbar: typeof isCustomerPreviewToolbar === 'undefined' ? undefined : isCustomerPreviewToolbar }`
  109. )(computed, () => route, isMpWebviewPath)
  110. const template = compileTemplate({
  111. id: 'preview',
  112. filename: 'index.vue',
  113. source: descriptor.template!.content
  114. })
  115. assert.deepEqual(template.errors, [])
  116. const exports: any = {}
  117. new Function(
  118. 'require',
  119. 'exports',
  120. ts.transpile(template.code, { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ESNext })
  121. )(
  122. () => ({
  123. ...Vue,
  124. resolveComponent: (name: string) => name,
  125. resolveDirective: () => undefined,
  126. withDirectives: (node: any) => node
  127. }),
  128. exports
  129. )
  130. const ctx = Vue.proxyRefs({
  131. ...state,
  132. visible: false,
  133. fileName: '附件',
  134. loading: false,
  135. kind: 'other',
  136. handleClosed: () => {}
  137. })
  138. for (const [path, owned] of [
  139. ['/m/crm/customer', true],
  140. ['/m/crm/customer/new', true],
  141. ['/m/crm/customer/123/edit', true],
  142. ['/m/crm/customer-extra', false],
  143. ['/m/crm/customerPool', false],
  144. ['/m/crm/quote/123', false],
  145. ['/m/crm/product', false],
  146. ['/crm/customer', false],
  147. ['/crm/customer/123', false]
  148. ] as const) {
  149. route.path = path
  150. const node = exports.render(ctx, [])
  151. assert.equal(node.props.class.includes('attachment-preview-dialog--customer'), owned, path)
  152. }
  153. })