Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

99 lignes
7.2 KiB

  1. import fs from 'node:fs'
  2. import { fileURLToPath } from 'node:url'
  3. import path from 'node:path'
  4. import assert from 'node:assert/strict'
  5. import { createRequire } from 'node:module'
  6. const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..')
  7. const require = createRequire(path.join(root, 'ai_person/ape2e/package.json'))
  8. const { chromium, expect: baseExpect } = require('@playwright/test')
  9. const expect = baseExpect.configure({ timeout: 25000 })
  10. const report = path.join(root, 'unreal_tran/ute2e/reports/open-platform-style-20260909')
  11. fs.mkdirSync(report, { recursive: true })
  12. const values = new Map()
  13. for (const line of fs.readFileSync(path.join(root, 'ai_person/账号.MD'), 'utf8').split(/\r?\n/)) {
  14. const match = line.match(/^\s*([^::]+)\s*[::]\s*(.*?)\s*$/)
  15. if (match?.[1] && match[2]) values.set(match[1].trim(), match[2].trim())
  16. }
  17. const base = 'http://127.0.0.1:8003'
  18. const response = await fetch(base + '/api/auth/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: values.get('账号') || values.get('用户名'), password: values.get('密码'), roleCode: 'admin' }) })
  19. const payload = await response.json()
  20. assert.equal(payload.code, 0, 'login must succeed')
  21. const token = payload.data.accessToken || payload.data.tokens?.accessToken
  22. const nav = await fetch(base + '/api/auth/v1/menus/navigation', { headers: { Authorization: 'Bearer ' + token } }).then(r => r.json())
  23. assert.equal(nav.code, 0)
  24. assert(!JSON.stringify(nav.data).includes('settings-open-api-docs'), 'docs must not be in server menu')
  25. assert(JSON.stringify(nav.data).includes('settings-open-applications'))
  26. const schema = await fetch(base + '/open-api/v1/openapi.json').then(r => r.json())
  27. const operationCount = Object.values(schema.paths).reduce((n, methods) => n + Object.keys(methods).length, 0)
  28. const checks = ['服务端菜单移除文档,保留开放应用']
  29. const browser = await chromium.launch({ channel: 'msedge', headless: true })
  30. const errors = []
  31. try {
  32. const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } })
  33. page.on('pageerror', error => errors.push(error.message))
  34. await page.goto(base + '/login')
  35. await page.evaluate(token => sessionStorage.setItem('ai-person:web:access-token:v1', token), token)
  36. await page.goto(base + '/settings/open-applications')
  37. await page.getByText('数字车间开发联调', { exact: true }).waitFor()
  38. await expect(page.locator('#main-sidebar')).not.toContainText('开放 API 文档')
  39. const search = page.getByPlaceholder('搜索应用名称或 APPID')
  40. const searchResponse = page.waitForResponse(response => response.url().includes('open-applications?keyword=%') && response.request().method() === 'GET')
  41. await search.fill('数字车间开发联调')
  42. await page.getByRole('button', { name: '搜索', exact: true }).click()
  43. assert.equal((await (await searchResponse).json()).data.total, 1)
  44. await expect(page.locator('.applications-table .el-table__row')).toHaveCount(1)
  45. await page.getByRole('button', { name: '重置', exact: true }).click()
  46. await expect(search).toHaveValue('')
  47. await expect(page.locator('.applications-table .el-table__row')).toHaveCount(2)
  48. checks.push('应用搜索、重置、分页区域正常')
  49. await page.screenshot({ path: path.join(report, '01-applications-light.png') })
  50. await page.getByRole('button', { name: '开放 API 文档', exact: true }).click()
  51. await expect(page).toHaveURL(/settings\/open-api-docs/)
  52. await expect(page.locator('.endpoint-section')).toHaveCount(operationCount)
  53. assert.equal(operationCount, 13)
  54. assert.equal(await page.locator('.el-collapse').count(), 0)
  55. for (const heading of ['1. 接口描述', '2. 输入参数', '3. 输出参数', '4. 错误码']) {
  56. await expect(page.locator('.endpoint-section').getByRole('heading', { name: heading, exact: true })).toHaveCount(13)
  57. }
  58. await expect(page.locator('#main-sidebar')).not.toContainText('开放 API 文档')
  59. await expect(page.locator('#main-sidebar .el-menu-item.is-active')).toContainText('开放应用')
  60. checks.push('文档作为二级页,开放应用菜单正确高亮', '13 个接口与服务端契约一致,均直接展示四个章节和返回示例')
  61. await page.screenshot({ path: path.join(report, '02-docs-overview-light.png') })
  62. await page.locator('.docs-nav a[href="#endpoint-9"]').click()
  63. await expect(page.locator('#endpoint-9')).toBeInViewport()
  64. await page.screenshot({ path: path.join(report, '09-generation-input.png') })
  65. await page.locator('#endpoint-9').screenshot({ path: path.join(report, '03-generation-document.png') })
  66. const idem = page.locator('#endpoint-9 .parameter-table').first().getByRole('row').filter({ hasText: 'Idempotency-Key' })
  67. await expect(idem).toContainText('是')
  68. await expect(page.locator('#endpoint-9')).toContainText('默认值:1280')
  69. checks.push('目录定位、生成幂等请求头必填和默认尺寸参数正确')
  70. await page.getByRole('button', { name: '切换到深色主题' }).click()
  71. await page.getByRole('link', { name: '接入说明', exact: true }).click()
  72. await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
  73. await page.screenshot({ path: path.join(report, '04-docs-dark.png') })
  74. await page.getByRole('button', { name: '返回开放应用', exact: true }).click()
  75. await page.getByText('数字车间开发联调', { exact: true }).waitFor()
  76. await page.screenshot({ path: path.join(report, '05-applications-dark.png') })
  77. await page.locator('tr').filter({ hasText: '数字车间开发联调' }).getByRole('button', { name: '配置', exact: true }).click()
  78. await expect(page.getByRole('dialog')).toBeVisible()
  79. await expect(page.getByRole('dialog').getByText('授权资源', { exact: true })).toBeVisible()
  80. await page.screenshot({ path: path.join(report, '06-application-dialog-dark.png') })
  81. await page.getByRole('dialog').getByRole('button', { name: '取消', exact: true }).click()
  82. checks.push('浅色与深色页面及配置弹窗正常,返回开放应用正常')
  83. const publicPage = await browser.newPage({ viewport: { width: 390, height: 844 } })
  84. publicPage.on('pageerror', error => errors.push(error.message))
  85. await publicPage.goto(base + '/developer')
  86. await expect(publicPage.locator('.endpoint-section')).toHaveCount(13)
  87. await expect(publicPage.getByRole('button', { name: '返回开放应用' })).toHaveCount(0)
  88. const dimensions = await publicPage.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }))
  89. assert(dimensions.scroll <= dimensions.width + 1, `mobile overflow: ${JSON.stringify(dimensions)}`)
  90. await publicPage.screenshot({ path: path.join(report, '07-docs-mobile.png') })
  91. await publicPage.locator('#endpoint-1').scrollIntoViewIfNeeded()
  92. await publicPage.screenshot({ path: path.join(report, '08-docs-mobile-parameters.png') })
  93. checks.push('公共文档无需登录,390px 窄屏无页面横向溢出')
  94. assert.deepEqual(errors, [])
  95. checks.push('浏览器无未捕获脚本错误')
  96. fs.writeFileSync(path.join(report, 'results.json'), JSON.stringify({ passed: true, operationCount, checks, errors, timestamp: new Date().toISOString() }, null, 2))
  97. console.log(JSON.stringify({ passed: true, operationCount, checks: checks.length, report }))
  98. } finally { await browser.close() }