test: OPFSEngine 12项mock测试 + Schema约束5项测试 → 543 passed
This commit is contained in:
@@ -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<string, string>();
|
||||
|
||||
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<typeof mockOPFS>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user