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