feat(ai): add ADR-036 unified OCR architecture and frontend test coverage
- Add ADR-036 unified OCR architecture (typhoon-ocr via Ollama) - Extend AI execution profiles for OCR sandbox configuration - Add comprehensive frontend test coverage (components, hooks, services) - Add backend test coverage for document-numbering services - Update OCR sidecar with typhoon-ocr integration - Add AI policy service and execution profile management - Update AGENTS.md and architecture documentation
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// File: frontend/lib/stores/__tests__/auth-store.test.ts
|
||||
// Change Log:
|
||||
// - 2026-06-14: Add coverage for auth state transitions and permission helpers
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { useAuthStore, User } from '../auth-store';
|
||||
|
||||
const user: User = {
|
||||
id: '019505a1-7c3e-7000-8000-abc123def100',
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def100',
|
||||
username: 'frontend.tester',
|
||||
email: 'tester@example.local',
|
||||
firstName: 'Frontend',
|
||||
lastName: 'Tester',
|
||||
role: 'User',
|
||||
permissions: ['documents.read', 'workflow.execute'],
|
||||
primaryOrganizationName: 'NP DMS',
|
||||
};
|
||||
|
||||
describe('useAuthStore', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
act(() => {
|
||||
useAuthStore.setState({ user: null, token: null, isAuthenticated: false });
|
||||
});
|
||||
});
|
||||
|
||||
it('ควรมีค่า default เป็นสถานะยังไม่ authenticated', () => {
|
||||
const { result } = renderHook(() => useAuthStore());
|
||||
expect(result.current.user).toBeNull();
|
||||
expect(result.current.token).toBeNull();
|
||||
expect(result.current.isAuthenticated).toBe(false);
|
||||
});
|
||||
|
||||
it('setAuth ควรบันทึก user, token และสถานะ authenticated', () => {
|
||||
const { result } = renderHook(() => useAuthStore());
|
||||
act(() => {
|
||||
result.current.setAuth(user, 'access-token');
|
||||
});
|
||||
expect(result.current.user?.publicId).toBe(user.publicId);
|
||||
expect(result.current.token).toBe('access-token');
|
||||
expect(result.current.isAuthenticated).toBe(true);
|
||||
});
|
||||
|
||||
it('logout ควรล้างข้อมูล session ออกจาก store', () => {
|
||||
const { result } = renderHook(() => useAuthStore());
|
||||
act(() => {
|
||||
result.current.setAuth(user, 'access-token');
|
||||
result.current.logout();
|
||||
});
|
||||
expect(result.current.user).toBeNull();
|
||||
expect(result.current.token).toBeNull();
|
||||
expect(result.current.isAuthenticated).toBe(false);
|
||||
});
|
||||
|
||||
it('hasPermission ควรตรวจ permission ของ user ปัจจุบัน', () => {
|
||||
const { result } = renderHook(() => useAuthStore());
|
||||
expect(result.current.hasPermission('documents.read')).toBe(false);
|
||||
act(() => {
|
||||
result.current.setAuth(user, 'access-token');
|
||||
});
|
||||
expect(result.current.hasPermission('documents.read')).toBe(true);
|
||||
expect(result.current.hasPermission('admin.manage')).toBe(false);
|
||||
});
|
||||
|
||||
it('hasPermission ควรให้ Admin ผ่านทุก permission', () => {
|
||||
const { result } = renderHook(() => useAuthStore());
|
||||
act(() => {
|
||||
result.current.setAuth({ ...user, role: 'admin', permissions: [] }, 'access-token');
|
||||
});
|
||||
expect(result.current.hasPermission('admin.manage')).toBe(true);
|
||||
});
|
||||
|
||||
it('hasRole ควรเทียบ role แบบตรงตัวกับ user ปัจจุบัน', () => {
|
||||
const { result } = renderHook(() => useAuthStore());
|
||||
expect(result.current.hasRole('User')).toBe(false);
|
||||
act(() => {
|
||||
result.current.setAuth(user, 'access-token');
|
||||
});
|
||||
expect(result.current.hasRole('User')).toBe(true);
|
||||
expect(result.current.hasRole('Admin')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
// File: frontend/lib/stores/__tests__/draft-store.test.ts
|
||||
// Change Log:
|
||||
// - 2026-06-13: Initial creation - test coverage for useDraftStore (Zustand)
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useDraftStore } from '../draft-store';
|
||||
|
||||
describe('useDraftStore', () => {
|
||||
beforeEach(() => {
|
||||
// รีเซ็ต store ก่อนแต่ละ test
|
||||
act(() => {
|
||||
useDraftStore.setState({ drafts: {} });
|
||||
});
|
||||
});
|
||||
|
||||
it('ค่า default ควรเป็น drafts: {}', () => {
|
||||
const { result } = renderHook(() => useDraftStore());
|
||||
expect(result.current.drafts).toEqual({});
|
||||
});
|
||||
|
||||
it('saveDraft ควรบันทึก draft data ด้วย key ที่กำหนด', () => {
|
||||
const { result } = renderHook(() => useDraftStore());
|
||||
const draftData = { title: 'Test Document', projectId: '019505a1-7c3e-7000-8000-abc123def456' };
|
||||
act(() => {
|
||||
result.current.saveDraft('rfa-new', draftData);
|
||||
});
|
||||
expect(result.current.drafts['rfa-new']).toEqual(draftData);
|
||||
});
|
||||
|
||||
it('getDraft ควรดึงข้อมูล draft ตาม key', () => {
|
||||
const { result } = renderHook(() => useDraftStore());
|
||||
const draftData = { subject: 'Correspondence Test', content: 'Body text' };
|
||||
act(() => {
|
||||
result.current.saveDraft('corr-edit', draftData);
|
||||
});
|
||||
const retrieved = result.current.getDraft('corr-edit');
|
||||
expect(retrieved).toEqual(draftData);
|
||||
});
|
||||
|
||||
it('getDraft ควร return undefined หาก key ไม่มีใน store', () => {
|
||||
const { result } = renderHook(() => useDraftStore());
|
||||
const retrieved = result.current.getDraft('non-existent-key');
|
||||
expect(retrieved).toBeUndefined();
|
||||
});
|
||||
|
||||
it('clearDraft ควรลบ draft ออกตาม key', () => {
|
||||
const { result } = renderHook(() => useDraftStore());
|
||||
act(() => {
|
||||
result.current.saveDraft('rfa-draft', { title: 'To Delete' });
|
||||
});
|
||||
expect(result.current.drafts['rfa-draft']).toBeDefined();
|
||||
act(() => {
|
||||
result.current.clearDraft('rfa-draft');
|
||||
});
|
||||
expect(result.current.drafts['rfa-draft']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('saveDraft ไม่ควรลบ draft อื่นที่ไม่ใช่ key เดียวกัน', () => {
|
||||
const { result } = renderHook(() => useDraftStore());
|
||||
act(() => {
|
||||
result.current.saveDraft('key-a', { data: 'A' });
|
||||
result.current.saveDraft('key-b', { data: 'B' });
|
||||
});
|
||||
act(() => {
|
||||
result.current.clearDraft('key-a');
|
||||
});
|
||||
expect(result.current.drafts['key-a']).toBeUndefined();
|
||||
expect(result.current.drafts['key-b']).toEqual({ data: 'B' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
// File: frontend/lib/stores/__tests__/project-store.test.ts
|
||||
// Change Log:
|
||||
// - 2026-06-13: Initial creation - test coverage for useProjectStore (Zustand)
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useProjectStore } from '../project-store';
|
||||
|
||||
describe('useProjectStore', () => {
|
||||
beforeEach(() => {
|
||||
// รีเซ็ต store ก่อนแต่ละ test
|
||||
act(() => {
|
||||
useProjectStore.setState({ selectedProjectId: null });
|
||||
});
|
||||
});
|
||||
|
||||
it('ค่า default ควรเป็น selectedProjectId: null', () => {
|
||||
const { result } = renderHook(() => useProjectStore());
|
||||
expect(result.current.selectedProjectId).toBeNull();
|
||||
});
|
||||
|
||||
it('setSelectedProjectId ควรตั้งค่า selectedProjectId ด้วย UUIDv7 ที่กำหนด', () => {
|
||||
const { result } = renderHook(() => useProjectStore());
|
||||
const projectId = '019505a1-7c3e-7000-8000-abc123def456';
|
||||
act(() => {
|
||||
result.current.setSelectedProjectId(projectId);
|
||||
});
|
||||
expect(result.current.selectedProjectId).toBe(projectId);
|
||||
});
|
||||
|
||||
it('setSelectedProjectId ควรเปลี่ยน selectedProjectId จาก UUID เป็น null ได้', () => {
|
||||
const projectId = '019505a1-7c3e-7000-8000-abc123def456';
|
||||
act(() => {
|
||||
useProjectStore.setState({ selectedProjectId: projectId });
|
||||
});
|
||||
const { result } = renderHook(() => useProjectStore());
|
||||
act(() => {
|
||||
result.current.setSelectedProjectId(null);
|
||||
});
|
||||
expect(result.current.selectedProjectId).toBeNull();
|
||||
});
|
||||
|
||||
it('setSelectedProjectId ควรเปลี่ยน project ได้หลายครั้ง', () => {
|
||||
const { result } = renderHook(() => useProjectStore());
|
||||
const projectId1 = '019505a1-7c3e-7000-8000-abc123def001';
|
||||
const projectId2 = '019505a1-7c3e-7000-8000-abc123def002';
|
||||
act(() => {
|
||||
result.current.setSelectedProjectId(projectId1);
|
||||
});
|
||||
expect(result.current.selectedProjectId).toBe(projectId1);
|
||||
act(() => {
|
||||
result.current.setSelectedProjectId(projectId2);
|
||||
});
|
||||
expect(result.current.selectedProjectId).toBe(projectId2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
// File: frontend/lib/stores/__tests__/ui-store.test.ts
|
||||
// Change Log:
|
||||
// - 2026-06-13: Initial creation - test coverage for useUIStore (Zustand)
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useUIStore } from '../ui-store';
|
||||
|
||||
describe('useUIStore', () => {
|
||||
beforeEach(() => {
|
||||
// รีเซ็ต store ก่อนแต่ละ test
|
||||
act(() => {
|
||||
useUIStore.setState({ isSidebarOpen: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('ค่า default ควรเป็น isSidebarOpen: true', () => {
|
||||
const { result } = renderHook(() => useUIStore());
|
||||
expect(result.current.isSidebarOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('toggleSidebar ควรสลับค่า isSidebarOpen จาก true เป็น false', () => {
|
||||
const { result } = renderHook(() => useUIStore());
|
||||
act(() => {
|
||||
result.current.toggleSidebar();
|
||||
});
|
||||
expect(result.current.isSidebarOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('toggleSidebar ควรสลับค่า isSidebarOpen จาก false เป็น true', () => {
|
||||
act(() => {
|
||||
useUIStore.setState({ isSidebarOpen: false });
|
||||
});
|
||||
const { result } = renderHook(() => useUIStore());
|
||||
act(() => {
|
||||
result.current.toggleSidebar();
|
||||
});
|
||||
expect(result.current.isSidebarOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('closeSidebar ควรตั้งค่า isSidebarOpen เป็น false', () => {
|
||||
const { result } = renderHook(() => useUIStore());
|
||||
act(() => {
|
||||
result.current.closeSidebar();
|
||||
});
|
||||
expect(result.current.isSidebarOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('openSidebar ควรตั้งค่า isSidebarOpen เป็น true', () => {
|
||||
act(() => {
|
||||
useUIStore.setState({ isSidebarOpen: false });
|
||||
});
|
||||
const { result } = renderHook(() => useUIStore());
|
||||
act(() => {
|
||||
result.current.openSidebar();
|
||||
});
|
||||
expect(result.current.isSidebarOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user