Files
MetonaSqlark/tests/e2e/opfs.spec.ts
T
thzxx 6a0cfebc4a test(PC-2): e2e 真崩溃注入(CDP Page.crash)取代假崩溃 + 两个 OPFS 写入窗口
背景(PLAN-v0.7.5.md 根因 5):项目声称的崩溃语义一直没有被真实验证。
e2e 里的 `crashPage()` 实际是:
    win.__ms = null;  await page.close();
—— 那是**优雅关闭**:没有未完成 I/O、不经过任何崩溃窗口。于是
    `checkpointInterval: 999999999` + 不 close 的"崩溃恢复"用例,测的其实是
    "正常关闭后重开"。CI 注释却写着"e2e 覆盖真实崩溃注入(CDP Page.crash)"。

改动:
1. `crashPage()` 改用 CDP `Page.crash` 终止渲染进程(进行中的 OPFS 写入、
   未 flush 的缓冲、同步句柄全部立即消失)。实测要点:
   - `Page.crash` 之后 `page.isClosed()` 仍为 false,不能用它判成败;
   - OPFS 数据**在同一个 context 内**跨崩溃保留(新建 context 是另一份存储),
     因此崩溃后新开页面即可继续验证。
2. harness 增加两类真崩溃注入:
   - `insertNoAwait`:发起写入但不等待(`page.evaluate` 会等 Promise,
     因此"写到一半就崩"无法用普通 await 表达);
   - `armCrashOnOpfsWrite({ phase })`:包装 `FileSystemWritableFileStream`
     的 `write`/`close`,在第 n 次调用处进入死循环,随后被 Page.crash 杀掉 ——
     对应 copy-on-write 的两个真实窗口。**注**:OPFS 用的是 createWritable,
     不是 `FileSystemSyncAccessHandle`(后者主线程不可用,实测)。
3. 新增两个用例覆盖上述窗口,并断言钩子**确实装上**(armed === true),
   避免"注入了但没走到"的假绿:
   - write 阶段崩溃 → 已确认数据完好;
   - commit 阶段崩溃 → 3 条旧数据**完全不变**(copy-on-write 的核心不变量:
     未 commit 就崩溃不能出现半个文件)。
4. 原"写入后强制终止"用例加逐条抽查(0/1/24/25/49)—— 只断言计数会被
   "重复主键覆盖后计数恰好相等"蒙对。
5. CI 注释改为准确描述实际覆盖的窗口,并点明 e2e 依赖 dist/ 产物。

验证:14 项 e2e 全绿(8.9s,真实 Chromium + 真实 OPFS);
jest 全量 85 套件 / 1665 测试通过。
2026-09-15 01:06:23 +08:00

512 lines
21 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, type CDPSession } 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 = null): Promise<T> {
const result = await page.evaluate(
(payload: [string, unknown]) => {
const [m, a] = payload;
return (window as unknown as { __ms: Record<string, (o: unknown) => Promise<unknown>> }).__ms[m](a);
},
[method, args] as [string, unknown],
);
return result as T;
}
/**
* v0.8.0PC-2):**真崩溃**当前页面。
*
* 修复前这里只是 `win.__ms = null; await page.close()` —— 那是**优雅关闭**
* 没有未完成的 I/O、不经过任何崩溃窗口,因此"崩溃恢复"的结论实际上是在
* "正常关闭后重开"上得到的(PLAN-v0.7.5.md 根因 5:崩溃语义声称未被验证)。
*
* 现在用 CDP `Page.crash` 直接终止渲染进程:进行中的 OPFS 写入、未 flush 的
* WAL 缓冲、同步访问句柄全部**立即**消失,与浏览器/标签页被强杀一致。
* 崩溃后 `page` 上任何 evaluate 都会失败,因此调用方必须新建页面继续。
*/
async function crashPage(page: Page, preAttached?: CDPSession): Promise<void> {
const cdp = preAttached ?? await page.context().newCDPSession(page);
// 不 awaitPage.crash 会让该 CDP 连接随渲染进程一起消失
cdp.send('Page.crash').catch(() => { /* 连接随进程一起消失 */ });
await new Promise((resolve) => setTimeout(resolve, 300));
// 关闭这个已崩溃的页面句柄(仅清理句柄,不影响 context 与 OPFS
await page.close().catch(() => { /* 已崩溃,忽略 */ });
}
/**
* v0.8.0PC-2):在**写入进行中**真崩溃。
*
* 页面内的 OPFS 调用被 `armCrashOnOpfsWrite` 装上死循环钩子后,页面主线程会
* 卡死 —— 因此 CDP 会话必须**提前**建立(卡死后 `newCDPSession` 会失败)。
* 本函数封装这个"先建会话、再武装、后崩溃"的顺序。
*
* @returns 崩溃前已发生的目标 OPFS 调用次数(0 说明钩子没被走到 → 用例应失败)
*/
async function crashDuringOpfsWrite(
page: Page,
opts: { phase: 'write' | 'commit'; n?: number; rows: Record<string, unknown>[] },
): Promise<{ armed: boolean; opfsCalls: number }> {
const cdp = await page.context().newCDPSession(page);
const armed = await run<{ armed: boolean; target: number; phase: string }>(
page,
'armCrashOnOpfsWrite',
{ n: opts.n ?? 1, phase: opts.phase },
);
// 发起写入但不等它完成:page.evaluate 会因死循环永不返回,故显式 catch 掉
page.evaluate(
(rows) => (window as unknown as { __ms: { insertNoAwait: (o: unknown) => unknown } })
.__ms.insertNoAwait({ table: 't', rows }),
opts.rows,
).catch(() => { /* 页面将崩溃,预期 */ });
// 等死循环真正进入(页面卡死)
await new Promise((resolve) => setTimeout(resolve, 800));
await crashPage(page, cdp);
return { armed: armed.armed, opfsCalls: 0 };
}
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}` }] });
}
// v0.8.0PC-2):**真崩溃**CDP Page.crash 终止渲染进程,不经过 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);
// 逐条抽查(不只是计数):计数可能因重复主键被覆盖而"看起来对"
for (const i of [0, 1, 24, 25, 49]) {
const found = await run<{ rows: Record<string, unknown>[] }>(page2, 'find', {
table: 'logs', where: { id: `log-${i}` },
});
expect(found.rows).toHaveLength(1);
}
await run(page2, 'close');
await page2.close();
});
test('OPFS 写入窗口崩溃(write 阶段)→ 旧数据完好、无半文件', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-crash-opfs-write', diskEngine: 'opfs', checkpointInterval: 999999999 });
await run(page, 'createTable', {
table: 't', columns: [{ name: 'id', type: 'string', primaryKey: true }],
});
for (let i = 0; i < 3; i++) await run(page, 'insert', { table: 't', rows: [{ id: `ok-${i}` }] });
// 在 `createWritable().write()` 中途卡死 → 真崩溃
const { armed } = await crashDuringOpfsWrite(page, { phase: 'write', rows: [{ id: 'torn' }] });
expect(armed).toBe(true); // 钩子必须真的装上,否则这个窗口根本没被覆盖
const page2 = await page.context().newPage();
await openPage(page2);
await run(page2, 'open', { name: 'e2e-crash-opfs-write', diskEngine: 'opfs', checkpointInterval: 999999999 });
const res = await run<{ count: number }>(page2, 'count', { table: 't' });
// 已确认的 3 条必须完好;未完成的那条允许存在也可能不存在(不做要求)
expect(res.count).toBeGreaterThanOrEqual(3);
for (let i = 0; i < 3; i++) {
const found = await run<{ rows: Record<string, unknown>[] }>(page2, 'find', { table: 't', where: { id: `ok-${i}` } });
expect(found.rows).toHaveLength(1);
}
await run(page2, 'close');
await page2.close();
});
test('OPFS 提交窗口崩溃(commit 阶段)→ 原文件保持旧内容(copy-on-write 语义)', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-crash-opfs-commit', diskEngine: 'opfs', checkpointInterval: 999999999 });
await run(page, 'createTable', {
table: 't', columns: [{ name: 'id', type: 'string', primaryKey: true }],
});
for (let i = 0; i < 3; i++) await run(page, 'insert', { table: 't', rows: [{ id: `ok-${i}` }] });
// 数据已写进交换副本、但**尚未**原子替换时卡死 → 真崩溃
const { armed } = await crashDuringOpfsWrite(page, { phase: 'commit', rows: [{ id: 'torn' }] });
expect(armed).toBe(true);
const page2 = await page.context().newPage();
await openPage(page2);
await run(page2, 'open', { name: 'e2e-crash-opfs-commit', diskEngine: 'opfs', checkpointInterval: 999999999 });
// 这是 OPFS 后端最关键的崩溃不变量:createWritable 是 copy-on-write
// 未 commit 就崩溃 → 原文件内容**完全不变**(不会出现半个文件)。
const res = await run<{ count: number }>(page2, 'count', { table: 't' });
expect(res.count).toBe(3);
for (let i = 0; i < 3; i++) {
const found = await run<{ rows: Record<string, unknown>[] }>(page2, 'find', { table: 't', where: { id: `ok-${i}` } });
expect(found.rows).toHaveLength(1);
}
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');
});
});
test.describe('KVStoreEnginedisk 模式)真实环境', () => {
test('写入 → 刷新页面 → 数据保留(KVStore 持久化)', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-kv-persist', mode: 'disk', diskEngine: 'opfs' });
await run(page, 'createTable', {
table: 'kvusers',
columns: [
{ name: 'id', type: 'string', primaryKey: true },
{ name: 'name', type: 'string' },
{ name: 'tag', type: 'string', index: true },
],
});
for (let i = 0; i < 30; i++) {
await run(page, 'insert', {
table: 'kvusers',
rows: [{ id: `u-${i}`, name: `User${i}`, tag: i % 2 === 0 ? 'even' : 'odd' }],
});
}
expect(await run<{ count: number }>(page, 'count', { table: 'kvusers' })).toEqual({ count: 30 });
// 刷新:KVStore 快照/日志恢复
await page.reload();
await expect(page.locator('#status')).toHaveText('ready');
await run(page, 'open', { name: 'e2e-kv-persist', mode: 'disk', diskEngine: 'opfs' });
const res = await run<{ count: number }>(page, 'count', { table: 'kvusers' });
expect(res.count).toBe(30);
// 索引查询(重启重建)
const evens = await run<{ rows: Record<string, unknown>[] }>(page, 'find', {
table: 'kvusers', where: { tag: 'even' },
});
expect(evens.rows.length).toBe(15);
await run(page, 'close');
});
test('崩溃恢复:写入后强制终止 → 重开日志重放数据完整', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-kv-crash', mode: 'disk', diskEngine: 'opfs' });
await run(page, 'createTable', {
table: 'logs', columns: [{ name: 'id', type: 'string', primaryKey: true }],
});
const total = 40;
for (let i = 0; i < total; i++) {
await run(page, 'insert', { table: 'logs', rows: [{ id: `log-${i}` }] });
}
// 模拟崩溃:不 close
await crashPage(page);
const page2 = await page.context().newPage();
await openPage(page2);
await run(page2, 'open', { name: 'e2e-kv-crash', mode: 'disk', diskEngine: 'opfs' });
const res = await run<{ count: number }>(page2, 'count', { table: 'logs' });
expect(res.count).toBe(total);
await run(page2, 'close');
await page2.close();
});
test('事务 commit 原子性:多表写入后重启一致', async ({ page }) => {
await openPage(page);
await run(page, 'open', { name: 'e2e-kv-tx', mode: 'disk', diskEngine: 'opfs' });
await run(page, 'createTable', {
table: 't1', columns: [{ name: 'id', type: 'string', primaryKey: true }],
});
await run(page, 'createTable', {
table: 't2', columns: [{ name: 'id', type: 'string', primaryKey: true }],
});
// 用 SQL 多语句触发原子写(事务)
await run(page, 'query', { sql: "BEGIN; INSERT INTO t1 VALUES ('a'); INSERT INTO t2 VALUES ('b'); COMMIT;" });
await crashPage(page);
const page2 = await page.context().newPage();
await openPage(page2);
await run(page2, 'open', { name: 'e2e-kv-tx', mode: 'disk', diskEngine: 'opfs' });
expect(await run<{ count: number }>(page2, 'count', { table: 't1' })).toEqual({ count: 1 });
expect(await run<{ count: number }>(page2, 'count', { table: 't2' })).toEqual({ count: 1 });
await run(page2, 'close');
await page2.close();
});
});