349 lines
21 KiB
JavaScript
349 lines
21 KiB
JavaScript
// Browser E2E of the 统计数据 page (public/js/screens/stats.js, ui/stats.js; PR #323 by @2321Robin, reworked for 0.2.2)
|
||
// against the real server, headless Chrome (puppeteer-core + system Chrome). Opt-in: SP_E2E=1 node --test test/ui/stats.e2e.test.js
|
||
//
|
||
// 1. A seeded history (records built from three REAL m.result payloads, test/fixtures/stats-results.json) on desktop
|
||
// 1920×1080 and a phone in landscape 844×390: the four tabs, the counting rule on screen (the match left in the first
|
||
// round is listed as 中途退出 / 不计入统计 and left out of the totals), the row click that re-views a settlement and
|
||
// its 返回统计, Esc, the title screen's entry; zero console errors. Screenshots: test/e2e/out/stats-*.png
|
||
// 2. The data actions: 导出 hands out a file the page itself imports again (nothing new), a corrupt file and a newer
|
||
// one are refused with a message and change nothing, 清空 asks first (the confirm dialog sits above the overlay and
|
||
// Esc closes only it), unreadable storage is set aside with a notice, a newer build's data is never overwritten.
|
||
// 3. The real flows: a solo match given up in the briefing is history only; one given up after round 1 counts (1 round
|
||
// cleared, the server's own rule); a match played to its settlement is recorded once — also after a reload (the
|
||
// lobby replays m.result).
|
||
|
||
import { test, describe, before, after } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync, writeFileSync, mkdirSync, mkdtempSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { Client, sleep, startRealServer, hasChrome, OUT } from '../e2e/client.mjs';
|
||
import { buildRecord, buildQuitRecord, createLiveMatch, observeMatch, STATS_VERSION } from '../../public/js/ui/stats.js';
|
||
|
||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||
const ENABLED = process.env.SP_E2E === '1' && hasChrome();
|
||
const FIX = JSON.parse(readFileSync(path.join(ROOT, 'test/fixtures/stats-results.json'), 'utf8'));
|
||
const BANDS = Object.keys(JSON.parse(readFileSync(path.join(ROOT, 'data/bands.json'), 'utf8')));
|
||
const NOW = Date.parse('2026-10-08T21:30:00');
|
||
const DAY = 86400000;
|
||
|
||
/** n settled records from the real payloads (cycling solo / 4-player co-op, difficulties, outcomes, bands) + two quit records. */
|
||
function seed(n = 24) {
|
||
const recs = [];
|
||
const names = ['solo-normal', 'coop4-hard', 'coop4-abyss'];
|
||
for (let i = 0; i < n; i++) {
|
||
const res = JSON.parse(JSON.stringify(FIX[names[i % 3]]));
|
||
const solo = i % 3 === 0;
|
||
const diff = ['FUNNY', 'NORMAL', 'HARD', 'ABYSS'][i % 4];
|
||
const win = i % 5 < 2;
|
||
Object.assign(res, { seed: 1000 + i, difficulty: diff, modeId: `mode_${solo ? 'single' : 'multi'}_${diff.toLowerCase()}`, victory: win, reason: win ? 'victory' : 'defeat', roundsPassed: win ? 14 : 4 + (i % 8), durationMs: (8 + (i % 13)) * 60000 });
|
||
const me = res.players.find((p) => p.playerId === 'p_0');
|
||
Object.assign(me, { bandId: BANDS[i % 7], victory: win, alive: win, roundsPassed: res.roundsPassed, title: i % 3 ? { id: `comment_${1 + (i % 6)}` } : null });
|
||
recs.unshift(buildRecord(res, { myId: 'p_0', roomMode: solo ? 'solo' : 'coop', now: NOW - i * 0.6 * DAY }));
|
||
}
|
||
const state = (phase, round) => ({
|
||
me: { playerId: 'p_0' }, room: { code: 'ABCD', mode: 'solo' },
|
||
match: { public: { phase, round, lastRound: 14, modeId: 'mode_single_hard', difficulty: 'HARD', players: [{ playerId: 'p_0', seat: 0, name: 'Doctor', alive: true, lp: 30, bandId: BANDS[2] }] }, result: null },
|
||
});
|
||
const quit = (phase, round, t) => { const live = createLiveMatch(); observeMatch(live, state('INFO_CHECK', 0), t - 600000); return buildQuitRecord(state(phase, round), { live, now: t }); };
|
||
recs.push(quit('PREP', 1, NOW - 2 * 3600e3), quit('PREP', 7, NOW - 1.3 * DAY));
|
||
return { v: STATS_VERSION, records: recs.sort((a, b) => b.t - a.t) };
|
||
}
|
||
|
||
describe('统计数据 page (real server, headless Chrome)', { skip: !ENABLED && 'set SP_E2E=1 (and have Chrome) to run' }, () => {
|
||
let srv;
|
||
let puppeteer;
|
||
const clients = [];
|
||
|
||
before(async () => {
|
||
puppeteer = (await import('puppeteer-core')).default;
|
||
srv = await startRealServer();
|
||
mkdirSync(OUT, { recursive: true });
|
||
});
|
||
after(async () => { for (const c of clients) await c.close(); await srv?.stop(); });
|
||
|
||
/** A fresh browser on `base` with `sp.pref.stats` (a string) stored before the page loads. */
|
||
async function open({ w = 1920, h = 1080, stored = null, entered = true, base = srv.base, lang = null } = {}) {
|
||
const c = new Client(puppeteer, base, 'stats', { prefix: 'stats', w, h });
|
||
clients.push(c);
|
||
c.browser = await puppeteer.launch({
|
||
executablePath: process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true,
|
||
args: ['--no-sandbox', '--mute-audio', '--force-device-scale-factor=1'],
|
||
});
|
||
const [first] = await c.browser.pages();
|
||
c.page = first || await c.browser.newPage();
|
||
await c.page.setViewport({ width: w, height: h, hasTouch: w < 1000, isMobile: w < 1000 });
|
||
c.page.on('console', (m) => { if (m.type() === 'error') c.problems.push(`console: ${m.text()}`); });
|
||
c.page.on('pageerror', (e) => c.problems.push(`pageerror: ${e.message}`));
|
||
await c.page.evaluateOnNewDocument((stored, entered, lang) => {
|
||
localStorage.setItem('sp.name', 'Doctor');
|
||
if (entered) sessionStorage.setItem('sp.entered', '1');
|
||
if (lang) localStorage.setItem('sp.pref.lang', JSON.stringify(lang));
|
||
if (stored != null && !sessionStorage.getItem('sp.e2e.seeded')) { localStorage.setItem('sp.pref.stats', stored); sessionStorage.setItem('sp.e2e.seeded', '1'); }
|
||
}, stored, entered, lang);
|
||
await c.page.goto(`${base}/`, { waitUntil: 'domcontentloaded' });
|
||
await c.page.waitForFunction((needOnline) => (!needOnline || globalThis.__SP__?.store.get().connection.status === 'online') && !!document.querySelector('.screen'), { timeout: 30000 }, entered);
|
||
return c;
|
||
}
|
||
const texts = (c, sel) => c.page.$$eval(sel, (els) => els.map((e) => e.innerText.replace(/\s+/g, ' ').trim()));
|
||
const stored = (c) => c.page.evaluate(() => { const r = localStorage.getItem('sp.pref.stats'); return r ? JSON.parse(r) : null; });
|
||
const openStats = async (c, from = '.lobby-screen .me-chip--btn') => {
|
||
await c.click(from, from.includes('me-chip') ? null : '统计');
|
||
await c.page.waitForSelector('.st .st-tab', { visible: true, timeout: 15000 });
|
||
};
|
||
const toast = (c) => c.page.waitForFunction(() => document.querySelector('.toast')?.textContent || false, { timeout: 8000 }).then((h) => h.jsonValue());
|
||
const noProblems = (c) => assert.deepEqual(c.problems, [], 'no console errors / failed requests');
|
||
|
||
test('desktop: tabs, the counting rule on screen, a settlement re-viewed, Esc, the title entry', async () => {
|
||
const env = seed();
|
||
const c = await open({ stored: JSON.stringify(env) });
|
||
await c.page.waitForSelector('.lobby-screen');
|
||
await openStats(c);
|
||
// 总览: 24 settled + the quit after 6 rounds count, the first-round one does not
|
||
const cards = await texts(c, '.st-card__num');
|
||
assert.equal(cards[0], '25', 'games');
|
||
assert.match(await c.page.$eval('.st-rule', (e) => e.textContent), /另有 1 局未计入统计/);
|
||
assert.equal(await c.page.$$eval('.st-title', (els) => els.length), 6, 'the six 评语');
|
||
await c.page.screenshot({ path: path.join(OUT, 'stats-overview-desktop.png') });
|
||
// 策略 / 战斗累计
|
||
await c.click('.st-tab[data-tab="bands"]');
|
||
assert.ok((await c.page.$$eval('.st-table tbody tr', (r) => r.length)) >= 5, 'strategy rows');
|
||
await c.page.screenshot({ path: path.join(OUT, 'stats-bands-desktop.png') });
|
||
await c.click('.st-tab[data-tab="totals"]');
|
||
assert.ok((await c.page.$$eval('.st-sum', (r) => r.length)) >= 8, 'combat totals');
|
||
// 对局记录: every record, the left-early ones marked, the first-round one dimmed, only settled rows open
|
||
await c.click('.st-tab[data-tab="history"]');
|
||
const rows = await texts(c, '.st-table--history tbody tr');
|
||
assert.equal(rows.length, 26);
|
||
assert.equal(rows.filter((r) => r.includes('中途退出')).length, 2);
|
||
assert.equal(rows.filter((r) => r.includes('不计入统计')).length, 1);
|
||
assert.equal(await c.page.$$eval('.st-table--history tr.is-excluded', (r) => r.length), 1);
|
||
assert.equal(await c.page.$$eval('.st-table--history tr.is-clickable', (r) => r.length), 24);
|
||
await c.page.screenshot({ path: path.join(OUT, 'stats-history-desktop.png') });
|
||
// a row re-views the settlement (the real result screen), 返回统计 comes back to the list, Esc closes
|
||
await c.click('.st-table--history tr.is-clickable');
|
||
await c.page.waitForSelector('.st--replay .result', { visible: true, timeout: 20000 });
|
||
await sleep(900);
|
||
await c.page.screenshot({ path: path.join(OUT, 'stats-replay-desktop.png') });
|
||
assert.ok(await c.page.$('.st--replay .rcard.is-self'), 'the player\'s own card is marked 你 (the record\'s own id)');
|
||
await c.page.keyboard.press('Escape');
|
||
await c.page.waitForSelector('.st .st-tab', { timeout: 5000 });
|
||
assert.ok(!(await c.exists('.st--replay')), 'Esc in the re-view goes back to the list');
|
||
await c.page.keyboard.press('Escape');
|
||
await c.page.waitForFunction(() => !document.querySelector('.st'), { timeout: 5000 });
|
||
assert.ok(await c.exists('.lobby-screen'));
|
||
noProblems(c);
|
||
await c.close();
|
||
// the title screen's entry (not entered yet: no connection needed), in English
|
||
const t = await open({ stored: JSON.stringify(env), entered: false, lang: 'en' });
|
||
await t.click('.title-corner .btn', 'Stats');
|
||
await t.page.waitForSelector('.st .st-tab', { visible: true, timeout: 15000 });
|
||
assert.match(await t.page.$eval('.st-top', (e) => e.textContent), /Overview/);
|
||
await t.page.screenshot({ path: path.join(OUT, 'stats-overview-title-en.png') });
|
||
noProblems(t);
|
||
await t.close();
|
||
});
|
||
|
||
test('phone in landscape (844×390): the page and the re-view fit; no console errors', async () => {
|
||
const c = await open({ w: 844, h: 390, stored: JSON.stringify(seed()) });
|
||
await c.page.waitForSelector('.lobby-screen');
|
||
await openStats(c);
|
||
const fits = () => c.page.evaluate(() => {
|
||
const st = document.querySelector('.st').getBoundingClientRect();
|
||
const top = document.querySelector('.st-top').getBoundingClientRect();
|
||
const right = document.querySelector('.st-top__right').getBoundingClientRect();
|
||
const tabs = document.querySelector('.st-tabs').getBoundingClientRect();
|
||
return { w: st.width, topH: top.height, rightEdge: right.right, tabsRight: tabs.right, rightLeft: right.left, scrollW: document.documentElement.scrollWidth };
|
||
});
|
||
const f = await fits();
|
||
assert.ok(f.rightEdge <= f.w + 1 && f.tabsRight <= f.rightLeft + 1, `the top bar fits: ${JSON.stringify(f)}`);
|
||
assert.ok(f.scrollW <= 845, 'no horizontal page scroll');
|
||
await c.page.screenshot({ path: path.join(OUT, 'stats-overview-phone.png') });
|
||
await c.click('.st-tab[data-tab="history"]');
|
||
await c.page.screenshot({ path: path.join(OUT, 'stats-history-phone.png') });
|
||
await c.click('.st-table--history tr.is-clickable');
|
||
await c.page.waitForSelector('.st--replay .result', { visible: true, timeout: 20000 });
|
||
await sleep(900);
|
||
await c.page.screenshot({ path: path.join(OUT, 'stats-replay-phone.png') });
|
||
await c.click('.st--replay .btn--primary', '返回统计');
|
||
await c.page.waitForSelector('.st .st-tab', { timeout: 5000 });
|
||
noProblems(c);
|
||
});
|
||
|
||
test('the data actions: export → import (nothing new), a corrupt file, a newer file, 清空 with its confirm dialog', async () => {
|
||
const c = await open({ stored: JSON.stringify(seed(6)) });
|
||
await c.page.waitForSelector('.lobby-screen');
|
||
await openStats(c);
|
||
// 导出: capture the blob the page hands to the browser
|
||
await c.page.evaluate(() => {
|
||
HTMLAnchorElement.prototype.click = function click() { globalThis.__dl = { name: this.download, href: this.href }; };
|
||
});
|
||
await c.click('[data-testid="stats-export"]');
|
||
await c.page.waitForFunction(() => !!globalThis.__dl, { timeout: 5000 });
|
||
const exported = await c.page.evaluate(async () => { const r = await fetch(globalThis.__dl.href); return { name: globalThis.__dl.name, text: await r.text() }; });
|
||
assert.match(exported.name, /^stronghold-stats-\d{8}-\d{4}\.json$/);
|
||
const payload = JSON.parse(exported.text);
|
||
assert.equal(payload.kind, 'local-stats');
|
||
assert.equal(payload.records.length, 8);
|
||
const dir = mkdtempSync(path.join(tmpdir(), 'sp-stats-'));
|
||
const upload = async (name, text) => {
|
||
const file = path.join(dir, name);
|
||
writeFileSync(file, text);
|
||
const input = await c.page.$('.st input[type="file"]');
|
||
await input.uploadFile(file);
|
||
};
|
||
await c.page.evaluate(() => document.querySelectorAll('.toast').forEach((e) => e.remove()));
|
||
// 导入 the same file: nothing new
|
||
await upload('same.json', exported.text);
|
||
assert.match(await toast(c), /没有新记录/);
|
||
assert.equal((await stored(c)).records.length, 8);
|
||
await c.page.evaluate(() => document.querySelectorAll('.toast').forEach((e) => e.remove()));
|
||
// a corrupt file and a newer one change nothing
|
||
await upload('bad.json', '{"records": [1, 2');
|
||
assert.match(await toast(c), /不是有效的统计导出文件/);
|
||
await c.page.evaluate(() => document.querySelectorAll('.toast').forEach((e) => e.remove()));
|
||
await upload('newer.json', JSON.stringify({ kind: 'local-stats', v: 99, records: [] }));
|
||
assert.match(await toast(c), /更新版本/);
|
||
assert.equal((await stored(c)).records.length, 8);
|
||
await c.page.evaluate(() => document.querySelectorAll('.toast').forEach((e) => e.remove()));
|
||
// 导入 a file with one new record: merged, newest first
|
||
const extra = { ...payload.records[0], id: 'r1.cafe0001', t: NOW + DAY };
|
||
await upload('extra.json', JSON.stringify({ ...payload, records: [extra] }));
|
||
assert.match(await toast(c), /已导入 1 条记录/);
|
||
assert.equal((await stored(c)).records[0].id, 'r1.cafe0001');
|
||
// 清空: the confirm dialog is above the overlay, Esc closes only it, the second time confirms
|
||
await c.click('[data-testid="stats-clear"]');
|
||
await c.page.waitForSelector('.modal', { visible: true });
|
||
assert.match(await c.page.$eval('.modal', (e) => e.textContent), /将删除本机保存的 9 条对局记录/);
|
||
await sleep(250); // the dialog's key handler is attached by its effect, a moment after it paints
|
||
await c.page.keyboard.press('Escape');
|
||
await c.page.waitForFunction(() => !document.querySelector('.modal'), { timeout: 3000 });
|
||
assert.ok(await c.exists('.st'), 'Esc closed the dialog only');
|
||
assert.equal((await stored(c)).records.length, 9, 'nothing deleted');
|
||
await c.click('[data-testid="stats-clear"]');
|
||
await c.page.waitForSelector('.modal', { visible: true });
|
||
await c.click('.modal__actions button.btn--danger');
|
||
await c.page.waitForFunction(() => /还没有记录/.test(document.querySelector('.st-body')?.textContent || ''), { timeout: 5000 });
|
||
assert.deepEqual((await stored(c)).records, []);
|
||
noProblems(c);
|
||
});
|
||
|
||
test('corrupt and newer storage: a notice, no crash; the newer build\'s data is never overwritten', async () => {
|
||
const bad = '{"v":1,"records":[{"players":[';
|
||
const c = await open({ stored: bad });
|
||
await c.page.waitForSelector('.lobby-screen');
|
||
await openStats(c);
|
||
assert.match(await c.page.$eval('.st-note.is-warn', (e) => e.textContent), /无法读取,已另存为备份/);
|
||
assert.equal(await c.page.evaluate(() => localStorage.getItem('sp.pref.stats.damaged')), bad, 'kept as a backup');
|
||
noProblems(c);
|
||
await c.close();
|
||
const newer = JSON.stringify({ v: STATS_VERSION + 3, records: [{ players: [{ playerId: 'x' }], future: true }] });
|
||
const n = await open({ stored: newer });
|
||
await n.page.waitForSelector('.lobby-screen');
|
||
await openStats(n);
|
||
assert.match(await n.page.$eval('.st-body', (e) => e.textContent), /更新版本的统计数据/);
|
||
assert.ok(await n.page.$eval('[data-testid="stats-clear"]', (b) => b.disabled), 'no 清空 over data this build cannot read');
|
||
assert.equal(await n.page.evaluate(() => localStorage.getItem('sp.pref.stats')), newer);
|
||
noProblems(n);
|
||
});
|
||
});
|
||
|
||
describe('统计数据: the real flows (a fast real server, headless Chrome)', { skip: !ENABLED && 'set SP_E2E=1 (and have Chrome) to run' }, () => {
|
||
let puppeteer;
|
||
const servers = [];
|
||
before(async () => { puppeteer = (await import('puppeteer-core')).default; });
|
||
after(async () => { for (const s of servers) await s.stop(); });
|
||
|
||
const stored = (c) => c.page.evaluate(() => { const r = localStorage.getItem('sp.pref.stats'); return r ? JSON.parse(r) : null; });
|
||
async function toSoloMatch(c) {
|
||
await c.click('.mode-card', '独立模拟');
|
||
await sleep(300);
|
||
await c.click('.diff-card', '标准模拟');
|
||
await c.click('.create-box button', '开始独立模拟');
|
||
await c.waitFor((s) => !!s.room, 'solo room');
|
||
if (!(await c.st()).phase) await c.click('.room-bar__right button', '开始模拟', { timeout: 20000 });
|
||
await c.waitFor((s) => s.phase === 'INFO_CHECK', 'briefing', 30000);
|
||
}
|
||
async function toPrep(c) {
|
||
await c.click('.brief__foot .btn--primary', '准备就绪');
|
||
await c.waitFor((s) => s.phase === 'BAND_DRAFT', 'band draft', 30000);
|
||
await c.click('.dband', null, { nth: 1 });
|
||
await c.click('.draft-detail__btns .btn--primary', '确认选择');
|
||
await c.waitFor((s) => s.phase === 'PREP' && s.round === 1, 'prep round 1', 60000);
|
||
await sleep(1500);
|
||
}
|
||
async function giveUp(c) {
|
||
await c.click('.stephead .btn--danger, .gtop__exit', null, { timeout: 8000 });
|
||
await c.click('.modal__actions button', '放弃模拟');
|
||
await c.waitFor((s) => !s.room && !s.phase, 'back in the lobby', 15000);
|
||
}
|
||
|
||
test('given up in the briefing: history only; given up after round 1: it counts', async () => {
|
||
const srv = await startRealServer({ fast: { timerScale: 0.25, combatSpeed: 8 } });
|
||
servers.push(srv);
|
||
const c = new Client(puppeteer, srv.base, 'stats', { prefix: 'stats', w: 1600, h: 900 });
|
||
try {
|
||
await c.open();
|
||
await c.enter('Doc');
|
||
// entered and left right away
|
||
await toSoloMatch(c);
|
||
await giveUp(c);
|
||
let env = await stored(c);
|
||
assert.equal(env.records.length, 1);
|
||
assert.equal(env.records[0].end, 'quit');
|
||
assert.equal(env.records[0].players[0].roundsPassed, 0);
|
||
// through round 1, then given up in round 2
|
||
await toSoloMatch(c);
|
||
await toPrep(c);
|
||
await c.click('.readybtn');
|
||
await c.waitFor((s) => s.round >= 2 && ['PREP', 'ROUND_START', 'SP_DRAFT'].includes(s.phase), 'round 2 (round 1 cleared)', 120000);
|
||
await giveUp(c);
|
||
env = await stored(c);
|
||
assert.equal(env.records.length, 2);
|
||
assert.equal(env.records[0].end, 'quit');
|
||
assert.equal(env.records[0].players[0].roundsPassed, 1, 'one round cleared — the server\'s own count for a quit in round 2');
|
||
assert.ok(env.records[0].players[0].bandId, 'its strategy');
|
||
// the page: one game counted, one left out, both in the history
|
||
await c.click('.lobby-screen .me-chip--btn');
|
||
await c.page.waitForSelector('.st .st-card', { visible: true });
|
||
assert.equal((await c.page.$$eval('.st-card__num', (els) => els.map((e) => e.textContent)))[0], '1');
|
||
assert.match(await c.page.$eval('.st-rule', (e) => e.textContent), /另有 1 局未计入统计/);
|
||
await c.click('.st-tab[data-tab="history"]');
|
||
const rows = await c.page.$$eval('.st-table--history tbody tr', (trs) => trs.map((tr) => tr.innerText.replace(/\s+/g, ' ').trim()));
|
||
assert.equal(rows.length, 2);
|
||
assert.ok(rows[0].includes('中途退出') && !rows[0].includes('不计入统计'), rows[0]);
|
||
assert.ok(rows[1].includes('中途退出') && rows[1].includes('不计入统计'), rows[1]);
|
||
await c.shot('history-real');
|
||
assert.deepEqual(c.problems, []);
|
||
} finally { await c.close(); }
|
||
});
|
||
|
||
test('played to its settlement: one record, and a reload (the lobby replays m.result) adds no second one', async () => {
|
||
const srv = await startRealServer({ fast: { timerScale: 0.25, combatSpeed: 8, finishAfter: 1 } });
|
||
servers.push(srv);
|
||
const c = new Client(puppeteer, srv.base, 'stats', { prefix: 'stats', w: 1600, h: 900 });
|
||
try {
|
||
await c.open();
|
||
await c.enter('Doc');
|
||
await toSoloMatch(c);
|
||
await toPrep(c);
|
||
await c.click('.readybtn');
|
||
await c.waitFor((s) => s.result, 'the settlement', 120000);
|
||
await sleep(500);
|
||
let env = await stored(c);
|
||
assert.equal(env.records.length, 1);
|
||
assert.equal(env.records[0].end, 'settled');
|
||
assert.equal(env.records[0].roomMode, 'solo');
|
||
assert.equal(env.records[0].selfId, (await c.st()).me);
|
||
await c.page.reload({ waitUntil: 'domcontentloaded' });
|
||
await c.page.waitForFunction(() => !!globalThis.__SP__ && !!document.querySelector('.screen'), { timeout: 30000 });
|
||
await c.waitFor((s) => s.result, 'the replayed settlement after a reload', 30000);
|
||
await sleep(500);
|
||
env = await stored(c);
|
||
assert.equal(env.records.length, 1, 'the replay is the same record');
|
||
assert.deepEqual(c.problems, []);
|
||
} finally { await c.close(); }
|
||
});
|
||
});
|