Files
MetonaSqlark/tests/e2e/opfs.spec.ts
T
thzxx 24b3ca1079
CI / test (20.x) (push) Successful in 10m53s
CI / test (22.x) (push) Successful in 10m47s
CI / test (24.x) (push) Successful in 10m42s
CI / e2e (push) Successful in 10m27s
CI / test (18.x) (push) Successful in 11m1s
release: v0.6.0 — 完全移除 IndexedDB,自研 KVStore 事务存储引擎(多key原子写/快照日志恢复/CRC自愈)+ KVStoreEngine + 旧库迁移工具 + 10万级压力验证 + 崩溃注入e2e
2026-08-10 13:56:04 +08:00

330 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MetonaSqlark E2E — 真实 Chromium + OPFS 环境验证
*
* 覆盖:
* 1. OPFS 持久化:写入 → 刷新页面 → 数据保留(跨页面生命周期)
* 2. 优雅关闭后重开:数据完整
* 3. 崩溃恢复:写入后强制终止(不 close)→ 重开 → WAL 重放数据完整
* 4. 多标签页锁:页 A 持锁 → 页 B 打开同一库 → ARIA_LOCKED
* 5. 加密库密码往返(真实 WebCrypto)
* 6. 页面化 + OPFS 端到端
* 7. repair() 自愈(真实文件系统)
*/
import { test, expect, type Page } from '@playwright/test';
const HARNESS = '/tests/e2e/harness.html';
async function openPage(page: Page): Promise<void> {
await page.goto(HARNESS);
await expect(page.locator('#status')).toHaveText('ready');
}
/** 在页面中执行 harness 方法(统一包装错误信息) */
async function run<T>(page: Page, method: string, args: unknown): Promise<T> {
const result = await page.evaluate(
([m, a]) => (window as unknown as { __ms: Record<string, (o: unknown) => Promise<unknown>> }).__ms[m](a),
[method, args] as const,
);
return result as T;
}
/** 强制"崩溃"当前页面:终止渲染进程(不触发 beforeunload/close */
async function crashPage(page: Page): Promise<void> {
await page.evaluate(() => {
// 模拟崩溃:直接终止。使用 CDP Page.crash 等价效果 —— 通过无限循环让渲染进程被杀
// 但更可靠的方式是关闭页面连接(Playwright 无 Page.crash 公共 API),
// 这里用 context.close 前先把页面里的清理路径断开:
const win = window as unknown as { __ms: { __db: unknown } | null };
// 断开所有引用(模拟进程死亡:不调用 close)
win.__ms = null;
});
await page.close();
}
test.describe('OPFS 真实环境', () => {
test('写入 → 刷新页面 → 数据保留(持久化)', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-persist', diskEngine: 'opfs' });
await run(page, 'createTable', {
table: 'users',
columns: [
{ name: 'id', type: 'string', primaryKey: true },
{ name: 'name', type: 'string' },
{ name: 'age', type: 'number' },
],
});
await run(page, 'insert', {
table: 'users',
rows: [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 25 },
],
});
expect(await run<{ count: number }>(page, 'count', { table: 'users' })).toEqual({ count: 2 });
// 刷新页面(完全重新加载,OPFS 数据必须保留)
await page.reload();
await expect(page.locator('#status')).toHaveText('ready');
await run(page, 'open', { name: 'e2e-persist', diskEngine: 'opfs' });
const res = await run<{ count: number }>(page, 'count', { table: 'users' });
expect(res.count).toBe(2);
const found = await run<{ rows: Record<string, unknown>[] }>(page, 'find', {
table: 'users', where: { id: '1' },
});
expect(found.rows[0].name).toBe('Alice');
await run(page, 'close');
});
test('优雅关闭后重开:数据完整', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-close-reopen', diskEngine: 'opfs' });
await run(page, 'createTable', {
table: 't', columns: [{ name: 'id', type: 'string', primaryKey: true }],
});
for (let i = 0; i < 20; i++) {
await run(page, 'insert', { table: 't', rows: [{ id: `r-${i}` }] });
}
await run(page, 'close');
await run(page, 'open', { name: 'e2e-close-reopen', diskEngine: 'opfs' });
expect(await run<{ count: number }>(page, 'count', { table: 't' })).toEqual({ count: 20 });
await run(page, 'close');
});
test('崩溃恢复:写入后强制终止 → 重开 WAL 重放数据完整', async ({ page }) => {
await openPage(page);
// 不 checkpointinterval 极大),全部写入留在 WAL
await run(page, 'open', {
name: 'e2e-crash', diskEngine: 'opfs', checkpointInterval: 999999999,
});
await run(page, 'createTable', {
table: 'logs', columns: [{ name: 'id', type: 'string', primaryKey: true }],
});
const total = 50;
for (let i = 0; i < total; i++) {
await run(page, 'insert', { table: 'logs', rows: [{ id: `log-${i}` }] });
}
// 模拟崩溃:直接终止页面(不调用 close → 不 checkpoint
await crashPage(page);
// 新页面重开:WAL 重放
const page2 = await page.context().newPage();
await openPage(page2);
await run(page2, 'open', {
name: 'e2e-crash', diskEngine: 'opfs', checkpointInterval: 999999999,
});
const res = await run<{ count: number }>(page2, 'count', { table: 'logs' });
expect(res.count).toBe(total);
await run(page2, 'close');
await page2.close();
});
test('写入进行中崩溃(不等待完成)→ 重开数据不损坏且可恢复已确认写入', async ({ page }) => {
await openPage(page);
await run(page, 'open', {
name: 'e2e-crash-mid', diskEngine: 'opfs', checkpointInterval: 999999999,
});
await run(page, 'createTable', {
table: 't', columns: [{ name: 'id', type: 'string', primaryKey: true }],
});
// 前 20 条确认完成
for (let i = 0; i < 20; i++) {
await run(page, 'insert', { table: 't', rows: [{ id: `c-${i}` }] });
}
// 发起一批写入后立即崩溃(不等 Promise 完成)
const pending = page.evaluate(async () => {
const ms = (window as unknown as { __ms: { insert: (o: unknown) => Promise<unknown> } }).__ms;
const rows = [];
for (let i = 0; i < 30; i++) rows.push({ id: `p-${i}` });
await ms.insert({ table: 't', rows });
});
await new Promise((r) => setTimeout(r, 50));
await crashPage(page);
await pending.catch(() => {});
// 重开:库可打开,已确认 20 条完整;写入中数据要么全部可见要么部分(日志尾部截断)
const page2 = await page.context().newPage();
await openPage(page2);
await run(page2, 'open', {
name: 'e2e-crash-mid', diskEngine: 'opfs', checkpointInterval: 999999999,
});
const res = await run<{ count: number }>(page2, 'count', { table: 't' });
expect(res.count).toBeGreaterThanOrEqual(20);
// 已确认的 20 条必须完整
for (let i = 0; i < 20; i++) {
const found = await run<{ rows: Record<string, unknown>[] }>(page2, 'find', {
table: 't', where: { id: `c-${i}` },
});
expect(found.rows).toHaveLength(1);
}
await run(page2, 'close');
await page2.close();
});
test('checkpoint 前后崩溃 → 快照/日志双路径恢复一致', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-crash-cp', diskEngine: 'opfs', checkpointInterval: 999999999 });
await run(page, 'createTable', {
table: 't', columns: [{ name: 'id', type: 'string', primaryKey: true }],
});
for (let i = 0; i < 10; i++) {
await run(page, 'insert', { table: 't', rows: [{ id: `a-${i}` }] });
}
// 触发 checkpoint(强制落盘)
await run(page, 'repair'); // repair 内含 flush+checkpoint
for (let i = 0; i < 10; i++) {
await run(page, 'insert', { table: 't', rows: [{ id: `b-${i}` }] });
}
// 崩溃(checkpoint 后数据在快照,后续在日志)
await crashPage(page);
const page2 = await page.context().newPage();
await openPage(page2);
await run(page2, 'open', { name: 'e2e-crash-cp', diskEngine: 'opfs', checkpointInterval: 999999999 });
const res = await run<{ count: number }>(page2, 'count', { table: 't' });
expect(res.count).toBe(20);
await run(page2, 'close');
await page2.close();
});
test('多标签页锁:第二个标签页打开同一库 → ARIA_LOCKED', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-lock', diskEngine: 'opfs' });
await run(page, 'createTable', {
table: 't', columns: [{ name: 'id', type: 'string', primaryKey: true }],
});
// 第二个标签页尝试打开同一库
const page2 = await page.context().newPage();
await openPage(page2);
const err = await page2.evaluate(async () => {
try {
await (window as unknown as { __ms: { open: (o: unknown) => Promise<unknown> } }).__ms.open(
{ name: 'e2e-lock', diskEngine: 'opfs' },
);
return { ok: true };
} catch (e) {
return { ok: false, code: (e as { code?: string }).code, msg: (e as Error).message };
}
});
expect(err.ok).toBe(false);
expect((err as { code?: string }).code).toBe('ARIA_LOCKED');
// 第一个标签页关闭后 → 第二标签页可打开
await run(page, 'close');
const retry = await page2.evaluate(async () => {
const ms = (window as unknown as { __ms: { open: (o: unknown) => Promise<unknown>; close: () => Promise<unknown> } }).__ms;
await ms.open({ name: 'e2e-lock', diskEngine: 'opfs' });
return { ok: true };
});
expect(retry.ok).toBe(true);
await run(page2, 'close');
await page2.close();
});
test('加密库:密码往返(真实 WebCrypto AES-GCM', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-enc', diskEngine: 'opfs', encryptionPassword: 'correct-horse' });
await run(page, 'createTable', {
table: 'secrets',
columns: [
{ name: 'id', type: 'string', primaryKey: true },
{ name: 'payload', type: 'string' },
],
});
await run(page, 'insert', {
table: 'secrets',
rows: [{ id: 's1', payload: 'top-secret-content' }],
});
await run(page, 'close');
// 错误密码 → 打开失败
const page2 = await page.context().newPage();
await openPage(page2);
const err = await page2.evaluate(async () => {
try {
await (window as unknown as { __ms: { open: (o: unknown) => Promise<unknown> } }).__ms.open(
{ name: 'e2e-enc', diskEngine: 'opfs', encryptionPassword: 'wrong-password' },
);
return { ok: true };
} catch (e) {
return { ok: false, code: (e as { code?: string }).code };
}
});
expect((err as { code?: string }).code).toBe('ARIA_DECRYPT_ERROR');
// 正确密码 → 数据完整
await run(page2, 'open', { name: 'e2e-enc', diskEngine: 'opfs', encryptionPassword: 'correct-horse' });
const found = await run<{ rows: Record<string, unknown>[] }>(page2, 'find', {
table: 'secrets', where: { id: 's1' },
});
expect(found.rows[0].payload).toBe('top-secret-content');
await run(page2, 'close');
await page2.close();
});
test('OPFS 页面化端到端:大数据量 + 二级索引查询', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-paged', diskEngine: 'opfs' });
await run(page, 'createTable', {
table: 'docs',
columns: [
{ name: 'id', type: 'string', primaryKey: true },
{ name: 'tag', type: 'string', index: true },
{ name: 'body', type: 'string' },
],
});
const total = 100;
for (let batch = 0; batch < 5; batch++) {
const rows = [];
for (let i = 0; i < 20; i++) {
const idx = batch * 20 + i;
rows.push({ id: `d-${idx}`, tag: idx % 3 === 0 ? 'red' : 'blue', body: '页面化端到端内容'.repeat(30) });
}
await run(page, 'insert', { table: 'docs', rows });
}
expect(await run<{ count: number }>(page, 'count', { table: 'docs' })).toEqual({ count: total });
// 二级索引查询(真实浏览器中走 LSM 索引)
const reds = await run<{ rows: Record<string, unknown>[] }>(page, 'find', {
table: 'docs', where: { tag: 'red' },
});
expect(reds.rows.length).toBeGreaterThan(0);
// SQL 查询
const sql = await run<{ rows: Record<string, unknown>[] }>(page, 'query', {
sql: 'SELECT COUNT(*) AS n FROM docs WHERE tag = \'red\'',
});
expect(Number(sql.rows[0].n)).toBe(reds.rows.length);
await run(page, 'close');
});
test('repair() 自愈(真实文件系统)', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-repair', diskEngine: 'opfs' });
await run(page, 'createTable', {
table: 't',
columns: [
{ name: 'id', type: 'string', primaryKey: true },
{ name: 'v', type: 'string' },
],
});
await run(page, 'insert', { table: 't', rows: [{ id: 'a', v: 'keep-me' }] });
// 通过 SQL 触发 checkpoint + 手动 repair
await run(page, 'repair');
expect(await run<{ count: number }>(page, 'count', { table: 't' })).toEqual({ count: 1 });
// clearAll 后重建表(表结构随 clearAll 清空)
await run(page, 'clearAll');
await run(page, 'createTable', {
table: 't',
columns: [
{ name: 'id', type: 'string', primaryKey: true },
{ name: 'v', type: 'string' },
],
});
expect(await run<{ count: number }>(page, 'count', { table: 't' })).toEqual({ count: 0 });
await run(page, 'close');
});
});