t29-basemap-walkthrough.mjs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. /**
  2. * T29 真机走查:底图影像确实按配置挂上引擎(票 29)。
  3. *
  4. * 逻辑脚本(t29-basemap.mjs)把计划层测透了,这里只测浏览器里才成立的部分:
  5. * · 默认(source=none)= 不挂底图,宿主 data-map-basemap 为 not-configured(票 27 行为不变);
  6. * · source=natural-earth = 真的去取 Cesium 自带离线瓦片(断言发生 NaturalEarthII 请求且 200);
  7. * · source=xyz = 真的按 `{z}/{x}/{y}` 模板逐块取图(用仓库自带那份 TMS 目录当靶子,
  8. * 只证明通道通、模板被正确替换,不证明该地址的语义——TMS 的 y 轴与 XYZ 相反);
  9. * · source=tianditu 但没填 tk = 明确失败态(invalid + 浮标写明缺 tk),不是白屏也不是静默无底图。
  10. *
  11. * 天地图的**真实取图**需要真 tk,本票不伪造:模板拼装与开发代理/生产子域由逻辑脚本覆盖,
  12. * 真 key 联调留给维护者(或票 21 的底图校验)。
  13. *
  14. * 运行(需先起 `yarn dev`,服务在 http://localhost:8000/):
  15. * node .scratch/cockpit-shell-panel-layout/verify/t29-basemap-walkthrough.mjs
  16. */
  17. import { join } from 'node:path'
  18. import { tmpdir } from 'node:os'
  19. const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
  20. const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/#/cockpit`
  21. const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
  22. const PUPPETEER = 'file:///C:/Users/ASUS/.dsh/profiles/desktop/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js'
  23. /** 仓库自带的离线瓦片目录(TMS 布局,仅作 xyz 通道的靶子)。 */
  24. const LOCAL_TILES = 'cesium/Assets/Textures/NaturalEarthII'
  25. const TILE_HINT = 'Assets/Textures/NaturalEarthII'
  26. const results = []
  27. function check(id, desc, pass, evidence) {
  28. results.push({ id, desc, pass })
  29. console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${desc}${evidence ? ` — ${evidence}` : ''}`)
  30. }
  31. const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
  32. const puppeteer = (await import(PUPPETEER)).default
  33. const browser = await puppeteer.launch({
  34. executablePath: CHROME,
  35. headless: true,
  36. userDataDir: join(tmpdir(), 'cockpit-t29-profile'),
  37. defaultViewport: { width: 1680, height: 950 },
  38. args: ['--no-sandbox', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check'],
  39. })
  40. console.log(`Chrome: ${await browser.version()}`)
  41. /** 开一页并记录它发出的瓦片请求(URL + 状态),等引擎到底。 */
  42. async function openPage(injectConfig) {
  43. const page = await browser.newPage()
  44. const tiles = []
  45. page.on('response', (response) => {
  46. const url = response.url()
  47. if (url.includes(TILE_HINT)) tiles.push({ url: url.replace(BASE, ''), status: response.status() })
  48. })
  49. if (injectConfig) await page.evaluateOnNewDocument(injectConfig)
  50. await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 })
  51. await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
  52. await page
  53. .waitForFunction(
  54. () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
  55. { timeout: 60000 },
  56. )
  57. .catch(() => {})
  58. await sleep(1500)
  59. return { page, tiles }
  60. }
  61. const readState = (page) =>
  62. page.evaluate(() => {
  63. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  64. return {
  65. engine: (host && host.dataset.mapEngine) || '',
  66. basemap: (host && host.dataset.mapBasemap) || '',
  67. pill: host?.querySelector('[data-map-engine-status]')?.textContent?.trim() || '',
  68. }
  69. })
  70. try {
  71. // ---------- A1 默认:不配底图(票 27 行为不变) ----------
  72. {
  73. // 本机 .env.local 可能已打开配置文件层(票 21 的本地 GIS 配置);本条测内置默认,显式关掉。
  74. const { page, tiles } = await openPage(() => {
  75. window.__COCKPIT_CONFIG_URL__ = ''
  76. })
  77. const state = await readState(page)
  78. check(
  79. 'A1-none-by-default',
  80. '默认(map.basemap.source 缺省)不挂底图:data-map-basemap=not-configured,浮标照旧说「底图未配置」',
  81. state.engine === 'ready' && state.basemap === 'not-configured' && state.pill.includes('底图未配置'),
  82. JSON.stringify(state),
  83. )
  84. check('A1-no-tile-requests', '不配底图时不发任何影像请求', tiles.length === 0, `tiles=${tiles.length}`)
  85. await page.close()
  86. }
  87. // ---------- A2 内置离线影像:真的取到瓦片 ----------
  88. {
  89. const { page, tiles } = await openPage(() => {
  90. window.__COCKPIT_CONFIG__ = { map: { basemap: { source: 'natural-earth' } } }
  91. })
  92. const state = await readState(page)
  93. const ok = tiles.filter((t) => t.status === 200)
  94. check(
  95. 'A2-natural-earth-live',
  96. 'source=natural-earth:宿主标成 natural-earth、浮标显示底图名、且真的取到离线瓦片(HTTP 200)',
  97. state.engine === 'ready' && state.basemap === 'natural-earth' && state.pill.includes('底图:') && ok.length > 0,
  98. JSON.stringify({ state, tiles: tiles.slice(0, 3), okCount: ok.length }),
  99. )
  100. await page.close()
  101. }
  102. // ---------- A3 自定义 XYZ:模板被逐块替换 ----------
  103. {
  104. const { page, tiles } = await openPage(() => {
  105. window.__COCKPIT_CONFIG__ = {
  106. map: {
  107. basemap: {
  108. source: 'xyz',
  109. url: 'cesium/Assets/Textures/NaturalEarthII/{z}/{x}/{y}.jpg',
  110. minimumLevel: 0,
  111. maximumLevel: 2,
  112. },
  113. },
  114. }
  115. })
  116. const state = await readState(page)
  117. const ok = tiles.filter((t) => t.status === 200)
  118. const zxy = ok.filter((t) => /NaturalEarthII\/\d+\/\d+\/\d+\.jpg$/.test(t.url))
  119. check(
  120. 'A3-xyz-template-live',
  121. 'source=xyz:按 {z}/{x}/{y} 模板真的逐块取图(HTTP 200),模板占位符被替换成数字',
  122. state.basemap === 'xyz' && zxy.length > 0,
  123. JSON.stringify({ basemap: state.basemap, sample: zxy.slice(0, 2), okCount: ok.length }),
  124. )
  125. await page.close()
  126. }
  127. // ---------- A4 天地图缺 tk:明确失败态,不白屏 ----------
  128. {
  129. const { page } = await openPage(() => {
  130. window.__COCKPIT_CONFIG__ = { map: { basemap: { source: 'tianditu' } } }
  131. })
  132. const state = await readState(page)
  133. check(
  134. 'A4-tianditu-without-token',
  135. 'source=tianditu 但没有 tk:data-map-basemap=invalid、浮标写明缺 tk 并说明回退,引擎仍就绪',
  136. state.engine === 'ready' &&
  137. state.basemap === 'invalid' &&
  138. state.pill.includes('底图配置无效') &&
  139. state.pill.includes('tk') &&
  140. state.pill.includes('回退'),
  141. JSON.stringify(state),
  142. )
  143. await page.close()
  144. }
  145. // ---------- A5 配置写错来源:归一化退回 none,不炸 ----------
  146. {
  147. const { page } = await openPage(() => {
  148. window.__COCKPIT_CONFIG__ = { map: { basemap: { source: 'baidu' } } }
  149. })
  150. const state = await readState(page)
  151. check(
  152. 'A5-unknown-source-falls-back',
  153. '未知底图来源:退回 none(not-configured)而不是报错或挂出坏底图',
  154. state.engine === 'ready' && state.basemap === 'not-configured' && state.pill.includes('底图未配置'),
  155. JSON.stringify(state),
  156. )
  157. await page.close()
  158. }
  159. } finally {
  160. await browser.close()
  161. }
  162. const failed = results.filter((r) => !r.pass)
  163. console.log('')
  164. console.log(`T29 走查:${results.length - failed.length}/${results.length} 项通过。`)
  165. if (failed.length) {
  166. console.log('未通过:' + failed.map((f) => f.id).join(', '))
  167. process.exit(1)
  168. }
  169. console.log('T29 走查全部通过。')