131 lines
4.9 KiB
JavaScript
131 lines
4.9 KiB
JavaScript
// 第三轮补拍:柔性甘特 / 世界对比 / 利用率(复制现场世界,修复页签滚动)
|
|
import { spawn } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { createRequire } from 'node:module';
|
|
|
|
const ROOT = path.resolve(import.meta.dirname, '..');
|
|
const require = createRequire(path.join(ROOT, 'apps', 'web', 'package.json'));
|
|
const { chromium } = require('playwright');
|
|
|
|
const SHOTS = path.join(ROOT, '_shots');
|
|
const TMP = path.join(SHOTS, 'tmp3');
|
|
fs.rmSync(TMP, { recursive: true, force: true });
|
|
fs.mkdirSync(TMP, { recursive: true });
|
|
|
|
const PY = path.join(ROOT, '.venv', 'Scripts', 'python.exe');
|
|
const VITE = path.join(ROOT, 'apps', 'web', 'node_modules', 'vite', 'bin', 'vite.js');
|
|
const API = 'http://127.0.0.1:8100';
|
|
const UI = 'http://localhost:5399';
|
|
const JMS = {
|
|
tenant: process.env.E2E_JMS_TENANT?.trim() || '',
|
|
username: process.env.E2E_JMS_USER?.trim() || '',
|
|
password: process.env.E2E_JMS_PASSWORD || '',
|
|
};
|
|
const missingJmsConfig = Object.entries(JMS).filter(([, value]) => !value).map(([key]) => key);
|
|
if (missingJmsConfig.length) {
|
|
throw new Error(`截图脚本缺少 JMS 登录环境变量:${missingJmsConfig.join(', ')}`);
|
|
}
|
|
|
|
for (const f of ['world.json', 'master.db', 'approvals.json', 'knowledge.json', 'config.json']) {
|
|
const src = path.join(ROOT, 'server', 'data', f);
|
|
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(TMP, f));
|
|
}
|
|
|
|
const children = [];
|
|
function run(name, cmd, args, opts = {}) {
|
|
const log = fs.createWriteStream(path.join(SHOTS, `${name}3.log`));
|
|
const p = spawn(cmd, args, { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'], ...opts });
|
|
p.stdout.pipe(log); p.stderr.pipe(log);
|
|
children.push(p);
|
|
}
|
|
function cleanup() {
|
|
for (const p of children) {
|
|
try { spawn('taskkill', ['/PID', String(p.pid), '/T', '/F'], { stdio: 'ignore' }); } catch {}
|
|
}
|
|
}
|
|
process.on('exit', cleanup);
|
|
|
|
async function waitHttp(url, timeoutMs) {
|
|
const t0 = Date.now();
|
|
while (Date.now() - t0 < timeoutMs) {
|
|
try { const r = await fetch(url); if (r.status < 500) return; } catch {}
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
}
|
|
throw new Error('waitHttp timeout: ' + url);
|
|
}
|
|
|
|
run('backend', PY, ['-m', 'uvicorn', 'server.main:app', '--port', '8100'], {
|
|
env: {
|
|
...process.env,
|
|
APS_PORT: '8100',
|
|
APS_WORLD_PATH: path.join(TMP, 'world.json'),
|
|
APS_DB_PATH: path.join(TMP, 'master.db'),
|
|
APS_APPROVAL_PATH: path.join(TMP, 'approvals.json'),
|
|
APS_KNOWLEDGE_PATH: path.join(TMP, 'knowledge.json'),
|
|
APS_WEB_DEMO_SEED: '1',
|
|
},
|
|
});
|
|
run('frontend', process.execPath, [VITE, '--port', '5399', '--strictPort'], {
|
|
cwd: path.join(ROOT, 'apps', 'web'),
|
|
env: { ...process.env, VITE_API_TARGET: API },
|
|
});
|
|
|
|
console.log('等待服务…');
|
|
await waitHttp(API + '/api/health', 120_000);
|
|
await waitHttp(UI, 120_000);
|
|
|
|
const browser = await chromium.launch();
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1.5 });
|
|
|
|
async function shot(name) {
|
|
await page.waitForTimeout(700);
|
|
await page.screenshot({ path: path.join(SHOTS, name + '.png') });
|
|
console.log(' ✓', name);
|
|
}
|
|
|
|
// 登录
|
|
await page.goto(UI);
|
|
await page.locator('.guest-login-trigger').waitFor({ state: 'visible', timeout: 60_000 });
|
|
await page.locator('.guest-login-trigger').click();
|
|
const dialog = page.locator('.auth-login-dialog');
|
|
await dialog.locator('input[placeholder="请输入企业名称"]').fill(JMS.tenant);
|
|
await dialog.locator('input[placeholder="请输入用户名"]').fill(JMS.username);
|
|
await dialog.locator('input[placeholder="请输入密码"]').fill(JMS.password);
|
|
await dialog.locator('.login-submit').click();
|
|
const composer = page.locator('textarea[placeholder^="输入排产指令"]');
|
|
await composer.waitFor({ state: 'visible', timeout: 60_000 });
|
|
await page.waitForTimeout(2500);
|
|
|
|
// 柔性排产(让柔性甘特有数据)
|
|
await composer.fill('跑一版柔性排产');
|
|
await composer.press('Enter');
|
|
await page.locator('.flex-block-title', { hasText: '柔性排产' }).first()
|
|
.waitFor({ state: 'visible', timeout: 170_000 });
|
|
await page.waitForTimeout(1500);
|
|
|
|
// 打开视口
|
|
const resultBtn = page.locator('.right-rail button[data-tip="会话结果"], .right-rail button[data-tip="再点收起"]').first();
|
|
if (!(await page.locator('.vp-toolbar').isVisible().catch(() => false))) {
|
|
try { await resultBtn.click(); } catch {}
|
|
}
|
|
await page.locator('.vp-toolbar').waitFor({ state: 'visible', timeout: 20_000 }).catch(() => {});
|
|
|
|
const tab = async (label, name) => {
|
|
try {
|
|
const t = page.locator('.seg .tab', { hasText: label }).first();
|
|
await t.scrollIntoViewIfNeeded({ timeout: 8_000 });
|
|
await t.click({ timeout: 8_000 });
|
|
await page.waitForTimeout(1500);
|
|
await shot(name);
|
|
} catch (e) { console.log(' ✗', label, e.message.slice(0, 60)); }
|
|
};
|
|
await tab('柔性甘特', '21-柔性甘特');
|
|
await tab('世界对比', '22-世界对比');
|
|
await tab('利用率', '23-利用率');
|
|
|
|
await browser.close();
|
|
console.log('补拍完成');
|
|
cleanup();
|
|
process.exit(0);
|