diff --git a/README.md b/README.md
index fb2542a..363a414 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
+
> 基于 TypeScript 的**前端关系型数据库**,支持完整 SQL 查询、Query Builder 链式 API、与 **AriaEngine 自研页面式存储引擎**。
@@ -294,7 +294,7 @@ npm run typecheck # 类型检查
| 指标 | 数值 |
|------|------|
-| 测试用例 | 526 |
+| 测试用例 | 543 |
| 测试套件 | 27 |
| 行覆盖率 | 91.0% |
| SQL 关键字 | 33 |
diff --git a/tests/engine/opfs.test.ts b/tests/engine/opfs.test.ts
new file mode 100644
index 0000000..35a6106
--- /dev/null
+++ b/tests/engine/opfs.test.ts
@@ -0,0 +1,158 @@
+/**
+ * OPFSEngine 测试 — mock FileSystemDirectoryHandle,验证逻辑不卡死
+ * 注:jsdom 无 OPFS API,全部使用 mock 模拟
+ */
+import { OPFSEngine } from '../../src/engine/opfs';
+import { createSchema } from '../../src/table/schema';
+
+// Mock navigator.storage.getDirectory + FileSystemDirectoryHandle
+function mockOPFS() {
+ const files = new Map();
+
+ const dirMock = {
+ getDirectoryHandle: async (_name: string, _opts?: any) => dirMock as any,
+ getFileHandle: async (name: string, opts?: any) => {
+ if (opts?.create) {
+ return { createWritable: async () => ({ write: async (d: string) => { files.set(name, d); }, close: async () => {} }) };
+ }
+ if (!files.has(name)) throw new Error('Not found');
+ return { getFile: async () => ({ text: async () => files.get(name)!, arrayBuffer: async () => new ArrayBuffer(0) }) };
+ },
+ removeEntry: async (name: string) => { files.delete(name); },
+ };
+
+ // 为 entries() 添加可迭代接口
+ (dirMock as any).entries = () => ({
+ [Symbol.asyncIterator]: async function* () {
+ for (const [k] of files) yield [k];
+ },
+ });
+
+ // jsdom 环境下 navigator 可能已存在,直接设置 storage 属性
+ const nav = (globalThis as any).navigator || {};
+ nav.storage = { getDirectory: async () => dirMock };
+ (globalThis as any).navigator = nav;
+
+ return { files };
+}
+
+const userSchema = createSchema('users', {
+ id: { type: 'string', primaryKey: true },
+ name: { type: 'string', required: true },
+ age: { type: 'number', default: 0 },
+});
+
+describe('OPFSEngine', () => {
+ let engine: OPFSEngine;
+ let mocks: ReturnType;
+
+ beforeEach(async () => {
+ mocks = mockOPFS();
+ engine = new OPFSEngine();
+ await engine.open('test-opfs', 1);
+ });
+
+ afterEach(async () => {
+ await engine.close();
+ });
+
+ // ---- 生命周期 ----
+ it('打开后 isOpen 返回 true', () => {
+ expect(engine.isOpen()).toBe(true);
+ });
+
+ it('关闭后 isOpen 返回 false', async () => {
+ await engine.close();
+ expect(engine.isOpen()).toBe(false);
+ });
+
+ // ---- 表管理 ----
+ it('创建表后 hasTable 返回 true', async () => {
+ await engine.createTable(userSchema);
+ expect(await engine.hasTable('users')).toBe(true);
+ });
+
+ it('获取表名列表', async () => {
+ await engine.createTable(userSchema);
+ const names = await engine.getTableNames();
+ expect(names).toContain('users');
+ });
+
+ it('删除表后 hasTable 返回 false', async () => {
+ await engine.createTable(userSchema);
+ await engine.dropTable('users');
+ expect(await engine.hasTable('users')).toBe(false);
+ });
+
+ // ---- CRUD ----
+ it('插入后立即可查询', async () => {
+ await engine.createTable(userSchema);
+ await engine.insert('users', [
+ { id: '1', name: 'Alice', age: 30 },
+ { id: '2', name: 'Bob', age: 25 },
+ ]);
+ const rows = await engine.find('users', { table: 'users' });
+ expect(rows).toHaveLength(2);
+ });
+
+ it('count 返回正确行数', async () => {
+ await engine.createTable(userSchema);
+ await engine.insert('users', [{ id: '1', name: 'Alice' }]);
+ expect(await engine.count('users')).toBe(1);
+ });
+
+ it('更新后数据变化', async () => {
+ await engine.createTable(userSchema);
+ await engine.insert('users', [{ id: '1', name: 'Alice', age: 30 }]);
+ await engine.update('users', { table: 'users', where: { id: '1' } }, { age: 31 });
+ const rows = await engine.find('users', { table: 'users', where: { id: '1' } });
+ expect(rows[0].age).toBe(31);
+ });
+
+ it('删除后 count 减少', async () => {
+ await engine.createTable(userSchema);
+ await engine.insert('users', [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }]);
+ await engine.delete('users', { table: 'users', where: { id: '1' } });
+ expect(await engine.count('users')).toBe(1);
+ });
+
+ // ---- 数据持久化(模拟重启) ----
+ it('close + reopen 后数据恢复', async () => {
+ // 写入数据
+ await engine.createTable(userSchema);
+ await engine.insert('users', [
+ { id: '1', name: 'Alice', age: 30 },
+ { id: '2', name: 'Bob', age: 25 },
+ ]);
+ const beforeRows = await engine.find('users', { table: 'users' });
+ expect(beforeRows).toHaveLength(2);
+
+ // 关闭后重新打开(模拟重启)
+ await engine.close();
+ engine = new OPFSEngine();
+ await engine.open('test-opfs', 1);
+
+ // 数据应恢复
+ const afterRows = await engine.find('users', { table: 'users' });
+ expect(afterRows).toHaveLength(2);
+ expect(afterRows.map((r: any) => r.name).sort()).toEqual(['Alice', 'Bob']);
+ });
+
+ // ---- 事务 ----
+ it('事务 commit 后数据持久化', async () => {
+ await engine.createTable(userSchema);
+ await engine.beginTransaction();
+ await engine.insert('users', [{ id: '1', name: 'Alice' }]);
+ await engine.commitTransaction();
+ expect(await engine.count('users')).toBe(1);
+ });
+
+ it('事务 rollback 后数据恢复', async () => {
+ await engine.createTable(userSchema);
+ await engine.insert('users', [{ id: '1', name: 'Alice' }]);
+ await engine.beginTransaction();
+ await engine.insert('users', [{ id: '2', name: 'Bob' }]);
+ await engine.rollbackTransaction();
+ expect(await engine.count('users')).toBe(1);
+ });
+});
diff --git a/tests/table/schema.test.ts b/tests/table/schema.test.ts
index 379101f..491e303 100644
--- a/tests/table/schema.test.ts
+++ b/tests/table/schema.test.ts
@@ -145,6 +145,30 @@ describe('Schema 边缘场景', () => {
});
});
+ // ---- 约束校验(v0.2.3) ----
+ describe('约束校验', () => {
+ it('string 超出 maxLength 抛出错误', () => {
+ expect(() => checkFieldType('t', 'c', 'string', 'hello', { type: 'string', maxLength: 3 }))
+ .toThrow('exceeds max length');
+ });
+ it('string 未超 maxLength 不报错', () => {
+ expect(() => checkFieldType('t', 'c', 'string', 'hi', { type: 'string', maxLength: 5 }))
+ .not.toThrow();
+ });
+ it('number 低于 min 抛出错误', () => {
+ expect(() => checkFieldType('t', 'c', 'number', -5, { type: 'number', min: 0 }))
+ .toThrow('below minimum');
+ });
+ it('number 高于 max 抛出错误', () => {
+ expect(() => checkFieldType('t', 'c', 'number', 200, { type: 'number', max: 100 }))
+ .toThrow('above maximum');
+ });
+ it('number 在范围内不报错', () => {
+ expect(() => checkFieldType('t', 'c', 'number', 50, { type: 'number', min: 0, max: 100 }))
+ .not.toThrow();
+ });
+ });
+
// ---- astColumnToColumnDef ----
describe('astColumnToColumnDef', () => {