/** * Vue 集成 composables 测试(v0.2.6 补强) * @module tests/integrations/vue * * 项目零运行时依赖(vue 为 peer dependency), * 使用最小 Vue mock 验证 composables 的真实逻辑: * - onMounted 时执行查询 * - watch sql/deps 变化重新执行 * - refresh 手动刷新 * - 表名校验(SQL 注入防护) */ import 'fake-indexeddb/auto'; // ---- 最小 Vue mock(自包含:jest.mock 工厂不能引用外部变量) ---- jest.mock('vue', () => { const mountQueue: Array<() => void | Promise> = []; const watchList: Array<{ sources: any[]; cb: () => void | Promise }> = []; return { ref: (init: any) => { const box: { value: any } = { value: init }; return box; }, watch: (sources: any[], cb: any) => { watchList.push({ sources, cb }); }, onMounted: (fn: any) => { mountQueue.push(fn); }, __mockMounted: mountQueue, __mockWatch: watchList, __mockReset: () => { mountQueue.length = 0; watchList.length = 0; }, }; }, { virtual: true }); import { useSqlarkQuery, useSqlarkTable, useSqlarkDatabase } from '../../src/integrations/vue'; /** mock 模块内的挂载/监听队列(自包含作用域) */ const vueMock = jest.requireMock('vue') as { __mockMounted: Array<() => void | Promise>; __mockWatch: Array<{ sources: any[]; cb: () => void | Promise }>; __mockReset: () => void; }; beforeEach(() => { vueMock.__mockReset(); }); /** 模拟组件挂载:执行 onMounted 注册的回调 */ async function flushMounted(): Promise { const fns = vueMock.__mockMounted.splice(0); for (const fn of fns) { await fn(); } } /** 触发 watch 回调 */ async function flushWatch(): Promise { const pairs = vueMock.__mockWatch.splice(0); for (const { cb } of pairs) { await cb(); } } /** 构造最小响应式引用(与 vue.mock 的 ref 等价) */ function makeRef(init: T): { value: T } { return { value: init }; } describe('useSqlarkQuery', () => { test('onMounted 时执行 SQL 查询并更新响应式 data', async () => { const db = { query: jest.fn().mockResolvedValue([{ id: '1' }]) } as any; const hook = useSqlarkQuery(db, 'SELECT * FROM users'); expect(hook.loading.value).toBe(true); expect(db.query).not.toHaveBeenCalled(); await flushMounted(); expect(db.query).toHaveBeenCalledWith('SELECT * FROM users'); expect(hook.loading.value).toBe(false); expect(hook.data.value).toEqual([{ id: '1' }]); expect(hook.error.value).toBeNull(); }); test('查询失败时设置 error', async () => { const db = { query: jest.fn().mockRejectedValue(new Error('vue boom')) } as any; const hook = useSqlarkQuery(db, 'SELECT * FROM users'); await flushMounted(); expect(hook.error.value).toBeInstanceOf(Error); expect((hook.error.value as Error).message).toBe('vue boom'); expect(hook.data.value).toEqual([]); }); test('refresh 重新执行查询', async () => { let call = 0; const db = { query: jest.fn().mockImplementation(async () => [{ n: ++call }]) } as any; const hook = useSqlarkQuery(db, 'SELECT * FROM users'); await flushMounted(); expect(hook.data.value).toEqual([{ n: 1 }]); await hook.refresh(); expect(db.query).toHaveBeenCalledTimes(2); expect(hook.data.value).toEqual([{ n: 2 }]); }); test('watch 注册在 sql 与 deps 上', async () => { const db = { query: jest.fn().mockResolvedValue([]) } as any; const sqlRef = makeRef('SELECT * FROM users'); useSqlarkQuery(db, sqlRef.value, [sqlRef]); expect(vueMock.__mockWatch).toHaveLength(1); expect(vueMock.__mockWatch[0].sources).toHaveLength(2); // [() => sql, ...deps] // 模拟依赖变化触发 watch sqlRef.value = 'SELECT * FROM orders'; await flushWatch(); expect(db.query).toHaveBeenCalledWith('SELECT * FROM users'); }); }); describe('useSqlarkTable', () => { test('合法表名执行全表查询', async () => { const db = { query: jest.fn().mockResolvedValue([{ id: 1 }]) } as any; const hook = useSqlarkTable(db, 'users'); await flushMounted(); expect(db.query).toHaveBeenCalledWith('SELECT * FROM users'); expect(hook.data.value).toEqual([{ id: 1 }]); }); test('非法表名抛出校验错误(SQL 注入防护)', () => { const db = { query: jest.fn() } as any; expect(() => useSqlarkTable(db, 'users; DELETE FROM orders')).toThrow(/Invalid table name/); expect(() => useSqlarkTable(db, 'users--')).toThrow(/Invalid table name/); expect(db.query).not.toHaveBeenCalled(); }); }); describe('useSqlarkDatabase', () => { test('onMounted 创建并初始化数据库实例', async () => { const hook = useSqlarkDatabase({ name: 'vue-hook' }); expect(hook.ready.value).toBe(false); await flushMounted(); expect(hook.db.value).not.toBeNull(); expect(hook.ready.value).toBe(true); expect(hook.error.value).toBeNull(); }); test('初始化失败时设置 error', async () => { // 使用非法配置触发初始化错误(mode 未知) const hook = useSqlarkDatabase({ name: 'vue-fail', mode: 'unknown-mode' as any }); await flushMounted(); expect(hook.db.value).toBeNull(); expect(hook.ready.value).toBe(false); expect(hook.error.value).toBeInstanceOf(Error); }); });