gaoluyang
15 小时以前 cd8b52bdf89a4d1101f72e0ece9f4df22a3d3f7a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import assert from 'node:assert/strict';
import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
 
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(path.join(root, 'package.json'));
const { parse, compileScript } = require('vue/compiler-sfc');
const ts = require('typescript');
const { chromium } = require('C:/Users/云/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const source = fs.readFileSync(path.join(root, 'src/views/bi/dashboard/modules/RealtimeTable.vue'), 'utf8');
const { descriptor } = parse(source);
const script = compileScript(descriptor, { id: 'qa', inlineTemplate: true });
const module = ts.transpileModule(script.content, {
  compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext },
}).outputText.replace(/from ['"]vue['"]/g, 'from "/vue.js"');
const html = `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>BI 滚动验证</title>
<style>body{background:#020817;color:#ebf0fc;font-family:Microsoft YaHei,sans-serif;margin:24px}h1{font-size:18px}main{max-width:1100px;margin:auto}.card{padding:16px;background:#0a1834;border:1px solid #153554;border-radius:8px} ${descriptor.styles.map(s => s.content).join('\n')}</style>
<main><h1>最近入库记录</h1><div class="card"><div id="app"></div></div></main>
<script type="module">
import {createApp, h, ref} from '/vue.js';import Table from '/table.js';
const rows=ref([]), height=ref(180), show=ref(true); const makeRows=n=>Array.from({length:n},(_,i)=>({'入库单号':'RK-'+String(i+1).padStart(4,'0'),'产品名称':'验证产品 '+(i+1),'仓库':'南通仓','数量':100+i}));
rows.value=makeRows(20); window.qa={setRows:n=>rows.value=makeRows(n), refresh:()=>rows.value=[...rows.value], setHeight:n=>height.value=n, hide:()=>show.value=false};
createApp({render:()=>show.value?h(Table,{rows:rows.value,maxHeight:height.value,speed:30}):null}).mount('#app');
</script></html>`;
const vueFile = path.join(path.dirname(require.resolve('vue/package.json')), 'dist/vue.esm-browser.js');
const server = http.createServer((request, response) => {
  response.setHeader('Content-Type', request.url === '/' ? 'text/html; charset=utf-8' : 'text/javascript; charset=utf-8');
  response.end(request.url === '/table.js' ? module : request.url === '/vue.js' ? fs.readFileSync(vueFile) : html);
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
let browser;
const errors = [];
const output = path.join(root, '../scripts/bi-realtime-scroll-20261014');
fs.mkdirSync(output, { recursive: true });
try {
  browser = await chromium.launch({ headless: true, executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe' });
  const page = await browser.newPage({ viewport: { width: 1440, height: 800 } });
  page.on('pageerror', error => errors.push(error.message));
  await page.goto(`http://127.0.0.1:${server.address().port}`);
  await page.waitForFunction(() => document.querySelector('.table-track')?.getAnimations().length === 1);
  const offset = () => page.locator('.table-track').evaluate(el => new DOMMatrix(getComputedStyle(el).transform).m42);
  const top = await page.locator('.header-table').evaluate(el => el.getBoundingClientRect().top);
  const first = await offset();
  await page.waitForTimeout(500);
  assert.ok((await offset()) < first - 8, 'Rows must move upwards');
  assert.equal(await page.locator('.header-table').evaluate(el => el.getBoundingClientRect().top), top, 'Header must stay fixed');
  await page.locator('.realtime-table').hover();
  const paused = await offset();
  await page.waitForTimeout(250);
  assert.ok(Math.abs((await offset()) - paused) < 1, 'Hover must pause');
  await page.mouse.move(0, 0);
  await page.waitForTimeout(200);
  assert.ok((await offset()) < paused - 3, 'Mouse leave must resume');
  const refreshOffset = await offset();
  await page.evaluate(() => window.qa.refresh());
  await page.waitForTimeout(100);
  assert.ok((await offset()) < refreshOffset, 'Refresh must preserve scroll position');
  await page.locator('.table-track').evaluate(el => {
    const animation = el.getAnimations()[0];
    animation.currentTime = Number(animation.effect.getTiming().duration) - 200;
  });
  await page.waitForTimeout(400);
  assert.ok((await offset()) > -20, 'Loop must wrap seamlessly');
  await page.locator('.realtime-table').focus();
  const focused = await offset();
  await page.waitForTimeout(100);
  assert.ok(Math.abs((await offset()) - focused) < 1, 'Focus must pause');
  await page.keyboard.press('ArrowDown');
  assert.ok((await offset()) < focused - 35, 'Keyboard must advance rows');
  await page.evaluate(() => document.activeElement.blur());
  await page.screenshot({ path: path.join(output, 'desktop.png'), fullPage: true });
  await page.setViewportSize({ width: 390, height: 844 });
  await page.waitForTimeout(150);
  assert.ok(await page.locator('.table-track').evaluate(el => el.getAnimations().length === 1), 'Resize must keep one animation');
  await page.screenshot({ path: path.join(output, 'mobile.png'), fullPage: true });
  await page.evaluate(() => window.qa.setRows(2));
  await page.waitForTimeout(150);
  assert.equal(await page.locator('.table-track').evaluate(el => el.getAnimations().length), 0, 'Short table must not scroll');
  assert.equal(await page.locator('table[aria-hidden="true"]').count(), 0, 'Short table must not duplicate rows');
  await page.evaluate(() => window.qa.setRows(30));
  await page.waitForFunction(() => document.querySelector('.table-track').getAnimations().length === 1);
  await page.emulateMedia({ reducedMotion: 'reduce' });
  await page.waitForTimeout(150);
  assert.equal(await page.locator('.table-track').evaluate(el => el.getAnimations().length), 0, 'Reduced motion must disable autoplay');
  assert.equal(await page.locator('.table-viewport').evaluate(el => getComputedStyle(el).overflowY), 'auto', 'Reduced motion must allow manual scroll');
  await page.emulateMedia({ reducedMotion: 'no-preference' });
  await page.waitForFunction(() => document.querySelector('.table-track').getAnimations().length === 1);
  await page.evaluate(() => window.qa.setHeight(2000));
  await page.waitForTimeout(150);
  assert.equal(await page.locator('.table-track').evaluate(el => el.getAnimations().length), 0, 'Larger viewport must stop scrolling when rows fit');
  await page.evaluate(() => window.qa.hide());
  await page.waitForTimeout(100);
  assert.equal(await page.evaluate(() => document.getAnimations().length), 0, 'Unmount must clean up animations');
  assert.deepEqual(errors, []);
  const result = { passed: true, checks: ['continuous scroll', 'fixed header', 'hover pause/resume', 'refresh continuity', 'seamless loop', 'keyboard navigation', 'responsive resize', 'short data', 'reduced motion', 'unmount cleanup'], screenshots: ['desktop.png', 'mobile.png'] };
  fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify(result, null, 2));
  console.log(JSON.stringify(result));
} finally {
  await browser?.close();
  server.close();
}