From 529e9a4aa192ac66b263c345b9fa224edff82d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Thu, 5 Mar 2026 19:30:33 +0800 Subject: [PATCH] Add store tests with mocked axios and apiError Co-Authored-By: Claude Opus 4.6 --- tests/stores/acctMgmt.test.js | 246 ++++++++++++++++++++++++++ tests/stores/allMapData.test.js | 209 ++++++++++++++++++++++ tests/stores/compare.test.js | 107 +++++++++++ tests/stores/conformance.test.js | 179 +++++++++++++++++++ tests/stores/conformanceInput.test.js | 32 ++++ tests/stores/cytoscapeStore.test.js | 78 ++++++++ tests/stores/loading.test.js | 27 +++ tests/stores/login.test.js | 146 +++++++++++++++ tests/stores/mapCompareStore.test.js | 31 ++++ tests/stores/modal.test.js | 30 ++++ tests/stores/pageAdmin.test.js | 92 ++++++++++ tests/stores/performance.test.js | 83 +++++++++ 12 files changed, 1260 insertions(+) create mode 100644 tests/stores/acctMgmt.test.js create mode 100644 tests/stores/allMapData.test.js create mode 100644 tests/stores/compare.test.js create mode 100644 tests/stores/conformance.test.js create mode 100644 tests/stores/conformanceInput.test.js create mode 100644 tests/stores/cytoscapeStore.test.js create mode 100644 tests/stores/loading.test.js create mode 100644 tests/stores/login.test.js create mode 100644 tests/stores/mapCompareStore.test.js create mode 100644 tests/stores/modal.test.js create mode 100644 tests/stores/pageAdmin.test.js create mode 100644 tests/stores/performance.test.js diff --git a/tests/stores/acctMgmt.test.js b/tests/stores/acctMgmt.test.js new file mode 100644 index 0000000..5bce82f --- /dev/null +++ b/tests/stores/acctMgmt.test.js @@ -0,0 +1,246 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; + +vi.mock('@/module/apiError.js', () => ({ + default: vi.fn(), +})); + +// Mock login store to avoid its side effects +vi.mock('@/stores/login.ts', () => { + const { defineStore } = require('pinia'); + return { + default: defineStore('loginStore', { + state: () => ({ + userData: { username: 'currentUser', name: 'Current' }, + }), + actions: { + getUserData: vi.fn(), + }, + }), + }; +}); + +import useAcctMgmtStore from '@/stores/acctMgmt.ts'; + +describe('acctMgmtStore', () => { + let store; + const mockAxios = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useAcctMgmtStore(); + store.$axios = mockAxios; + vi.clearAllMocks(); + }); + + it('has correct default state', () => { + expect(store.allUserAccoutList).toEqual([]); + expect(store.isAcctMenuOpen).toBe(false); + }); + + describe('menu actions', () => { + it('openAcctMenu sets true', () => { + store.openAcctMenu(); + expect(store.isAcctMenuOpen).toBe(true); + }); + + it('closeAcctMenu sets false', () => { + store.openAcctMenu(); + store.closeAcctMenu(); + expect(store.isAcctMenuOpen).toBe(false); + }); + + it('toggleIsAcctMenuOpen toggles', () => { + store.toggleIsAcctMenuOpen(); + expect(store.isAcctMenuOpen).toBe(true); + store.toggleIsAcctMenuOpen(); + expect(store.isAcctMenuOpen).toBe(false); + }); + }); + + describe('setCurrentViewingUser', () => { + it('finds user by username', () => { + store.allUserAccoutList = [ + { username: 'alice', name: 'Alice', detail: {} }, + { username: 'bob', name: 'Bob', detail: {} }, + ]; + store.setCurrentViewingUser('bob'); + expect(store.currentViewingUser.name).toBe('Bob'); + }); + }); + + describe('clearCurrentViewingUser', () => { + it('resets to empty user', () => { + store.currentViewingUser = { username: 'test', detail: {} }; + store.clearCurrentViewingUser(); + expect(store.currentViewingUser.username).toBe(''); + }); + }); + + describe('createNewAccount', () => { + it('posts to /api/users and sets flag on success', async () => { + mockAxios.post.mockResolvedValue({ status: 200 }); + const user = { username: 'newuser', password: 'pass' }; + + await store.createNewAccount(user); + + expect(mockAxios.post).toHaveBeenCalledWith( + '/api/users', user, + ); + expect(store.isOneAccountJustCreate).toBe(true); + expect(store.justCreateUsername).toBe('newuser'); + }); + }); + + describe('deleteAccount', () => { + it('returns true on success', async () => { + mockAxios.delete.mockResolvedValue({ status: 200 }); + + const result = await store.deleteAccount('alice'); + + expect(mockAxios.delete).toHaveBeenCalledWith( + '/api/users/alice', + ); + expect(result).toBe(true); + }); + + it('returns false on error', async () => { + mockAxios.delete.mockRejectedValue(new Error('fail')); + + const result = await store.deleteAccount('alice'); + + expect(result).toBe(false); + }); + }); + + describe('editAccount', () => { + it('puts edited data', async () => { + mockAxios.put.mockResolvedValue({ status: 200 }); + const detail = { + username: 'alice', + password: 'newpw', + name: 'Alice', + is_active: true, + }; + + const result = await store.editAccount('alice', detail); + + expect(mockAxios.put).toHaveBeenCalledWith( + '/api/users/alice', + expect.objectContaining({ password: 'newpw' }), + ); + expect(result).toBe(true); + }); + }); + + describe('addRoleToUser', () => { + it('puts role assignment', async () => { + mockAxios.put.mockResolvedValue({ status: 200 }); + + const result = await store.addRoleToUser('alice', 'admin'); + + expect(mockAxios.put).toHaveBeenCalledWith( + '/api/users/alice/roles/admin', + ); + expect(result).toBe(true); + }); + }); + + describe('deleteRoleToUser', () => { + it('deletes role', async () => { + mockAxios.delete.mockResolvedValue({ status: 200 }); + + const result = await store.deleteRoleToUser('alice', 'admin'); + + expect(mockAxios.delete).toHaveBeenCalledWith( + '/api/users/alice/roles/admin', + ); + expect(result).toBe(true); + }); + }); + + describe('getUserDetail', () => { + it('fetches user and sets admin flag', async () => { + mockAxios.get.mockResolvedValue({ + status: 200, + data: { + username: 'alice', + roles: [{ code: 'admin' }], + }, + }); + + const result = await store.getUserDetail('alice'); + + expect(result).toBe(true); + expect(store.currentViewingUser.is_admin).toBe(true); + }); + + it('returns false on error', async () => { + mockAxios.get.mockRejectedValue(new Error('not found')); + + const result = await store.getUserDetail('ghost'); + + expect(result).toBe(false); + }); + }); + + describe('hover state actions', () => { + beforeEach(() => { + store.allUserAccoutList = [ + { + username: 'alice', + isDeleteHovered: false, + isRowHovered: false, + isEditHovered: false, + isDetailHovered: false, + }, + ]; + }); + + it('changeIsDeleteHoveredByUser', () => { + store.changeIsDeleteHoveredByUser('alice', true); + expect(store.allUserAccoutList[0].isDeleteHovered).toBe(true); + }); + + it('changeIsRowHoveredByUser', () => { + store.changeIsRowHoveredByUser('alice', true); + expect(store.allUserAccoutList[0].isRowHovered).toBe(true); + }); + + it('changeIsEditHoveredByUser', () => { + store.changeIsEditHoveredByUser('alice', true); + expect(store.allUserAccoutList[0].isEditHovered).toBe(true); + }); + + it('changeIsDetailHoveredByUser', () => { + store.changeIsDetailHoveredByUser('alice', true); + expect(store.allUserAccoutList[0].isDetailHovered).toBe(true); + }); + }); + + it('resetJustCreateFlag resets flag', () => { + store.isOneAccountJustCreate = true; + store.resetJustCreateFlag(); + expect(store.isOneAccountJustCreate).toBe(false); + }); + + it('setShouldUpdateList sets boolean', () => { + store.setShouldUpdateList(true); + expect(store.shouldUpdateList).toBe(true); + }); + + it('updateSingleAccountPiniaState updates user', () => { + store.allUserAccoutList = [ + { username: 'alice', name: 'Old' }, + ]; + store.updateSingleAccountPiniaState({ + username: 'alice', name: 'New', + }); + expect(store.allUserAccoutList[0].name).toBe('New'); + }); +}); diff --git a/tests/stores/allMapData.test.js b/tests/stores/allMapData.test.js new file mode 100644 index 0000000..bc2c3de --- /dev/null +++ b/tests/stores/allMapData.test.js @@ -0,0 +1,209 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; + +vi.mock('@/module/apiError.js', () => ({ + default: vi.fn(), +})); + +import useAllMapDataStore from '@/stores/allMapData.js'; + +describe('allMapDataStore', () => { + let store; + const mockAxios = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + }; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useAllMapDataStore(); + store.$axios = mockAxios; + vi.clearAllMocks(); + }); + + it('has correct default state', () => { + expect(store.logId).toBeNull(); + expect(store.allProcessMap).toEqual({}); + expect(store.allTrace).toEqual([]); + }); + + describe('getAllMapData', () => { + it('fetches log discover data', async () => { + store.logId = 1; + const mockData = { + process_map: { nodes: [] }, + bpmn: { nodes: [] }, + stats: { cases: 10 }, + insights: {}, + }; + mockAxios.get.mockResolvedValue({ data: mockData }); + + await store.getAllMapData(); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/logs/1/discover', + ); + expect(store.allProcessMap).toEqual({ nodes: [] }); + expect(store.allStats).toEqual({ cases: 10 }); + }); + + it('fetches temp filter discover data when set', async () => { + store.logId = 1; + store.tempFilterId = 5; + mockAxios.get.mockResolvedValue({ + data: { + process_map: {}, + bpmn: {}, + stats: {}, + insights: {}, + }, + }); + + await store.getAllMapData(); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/temp-filters/5/discover', + ); + }); + + it('fetches created filter discover data', async () => { + store.logId = 1; + store.createFilterId = 3; + mockAxios.get.mockResolvedValue({ + data: { + process_map: {}, + bpmn: {}, + stats: {}, + insights: {}, + }, + }); + + await store.getAllMapData(); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/filters/3/discover', + ); + }); + + it('does not throw on API failure', async () => { + store.logId = 1; + mockAxios.get.mockRejectedValue(new Error('fail')); + + await expect(store.getAllMapData()) + .resolves.not.toThrow(); + }); + }); + + describe('getFilterParams', () => { + it('fetches filter params and transforms timeframe', async () => { + store.logId = 1; + const mockData = { + tasks: ['A', 'B'], + sources: ['A'], + sinks: ['B'], + timeframe: { + x_axis: { + min: '2023-01-01T00:00:00Z', + max: '2023-12-31T00:00:00Z', + }, + }, + trace: [], + attrs: [], + }; + mockAxios.get.mockResolvedValue({ data: mockData }); + + await store.getFilterParams(); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/filters/params?log_id=1', + ); + expect(store.allFilterTask).toEqual(['A', 'B']); + // Check that min_base and max_base are stored + expect(store.allFilterTimeframe.x_axis.min_base) + .toBe('2023-01-01T00:00:00Z'); + }); + }); + + describe('checkHasResult', () => { + it('posts rule data and stores result', async () => { + store.logId = 1; + store.postRuleData = [{ type: 'task' }]; + mockAxios.post.mockResolvedValue({ + data: { result: true }, + }); + + await store.checkHasResult(); + + expect(mockAxios.post).toHaveBeenCalledWith( + '/api/filters/has-result?log_id=1', + [{ type: 'task' }], + ); + expect(store.hasResultRule).toBe(true); + }); + }); + + describe('addTempFilterId', () => { + it('creates temp filter and stores id', async () => { + store.logId = 1; + store.postRuleData = []; + mockAxios.post.mockResolvedValue({ data: { id: 77 } }); + + await store.addTempFilterId(); + + expect(store.tempFilterId).toBe(77); + }); + }); + + describe('addFilterId', () => { + it('creates filter and clears temp id', async () => { + store.logId = 1; + store.tempFilterId = 77; + store.postRuleData = [{ type: 'rule' }]; + mockAxios.post.mockResolvedValue({ data: { id: 88 } }); + + await store.addFilterId('myFilter'); + + expect(mockAxios.post).toHaveBeenCalledWith( + '/api/filters?log_id=1', + { name: 'myFilter', rules: [{ type: 'rule' }] }, + ); + expect(store.createFilterId).toBe(88); + expect(store.tempFilterId).toBeNull(); + }); + }); + + describe('updataFilter', () => { + it('updates filter and clears temp id', async () => { + store.createFilterId = 88; + store.tempFilterId = 77; + store.postRuleData = [{ type: 'updated' }]; + mockAxios.put.mockResolvedValue({ status: 200 }); + + await store.updataFilter(); + + expect(mockAxios.put).toHaveBeenCalledWith( + '/api/filters/88', + [{ type: 'updated' }], + ); + expect(store.isUpdataFilter).toBe(true); + expect(store.tempFilterId).toBeNull(); + }); + }); + + describe('getters', () => { + it('traces getter sorts by id', () => { + store.allTrace = [ + { id: 3, name: 'C' }, + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + ]; + expect(store.traces.map(t => t.id)).toEqual([1, 2, 3]); + }); + + it('processMap getter returns state', () => { + store.allProcessMap = { nodes: [1, 2] }; + expect(store.processMap).toEqual({ nodes: [1, 2] }); + }); + }); +}); diff --git a/tests/stores/compare.test.js b/tests/stores/compare.test.js new file mode 100644 index 0000000..d01ff50 --- /dev/null +++ b/tests/stores/compare.test.js @@ -0,0 +1,107 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; + +vi.mock('@/module/apiError.js', () => ({ + default: vi.fn(), +})); + +import useCompareStore from '@/stores/compare.js'; + +describe('compareStore', () => { + let store; + const mockAxios = { + get: vi.fn(), + }; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useCompareStore(); + store.$axios = mockAxios; + vi.clearAllMocks(); + }); + + it('has correct default state', () => { + expect(store.allCompareDashboardData).toBeNull(); + }); + + it('compareDashboardData getter returns state', () => { + store.allCompareDashboardData = { time: {} }; + expect(store.compareDashboardData).toEqual({ time: {} }); + }); + + describe('getCompare', () => { + it('fetches compare data with encoded params', async () => { + const params = [{ type: 'log', id: 1 }]; + const mockData = { time: {}, freq: {} }; + mockAxios.get.mockResolvedValue({ data: mockData }); + + await store.getCompare(params); + + const encoded = encodeURIComponent(JSON.stringify(params)); + expect(mockAxios.get).toHaveBeenCalledWith( + `/api/compare?datasets=${encoded}`, + ); + expect(store.allCompareDashboardData).toEqual(mockData); + }); + + it('does not throw on API failure', async () => { + mockAxios.get.mockRejectedValue(new Error('fail')); + + await expect(store.getCompare([])) + .resolves.not.toThrow(); + }); + }); + + describe('getStateData', () => { + it('fetches log discover stats', async () => { + mockAxios.get.mockResolvedValue({ + data: { stats: { cases: 100 } }, + }); + + const result = await store.getStateData('log', 1); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/logs/1/discover', + ); + expect(result).toEqual({ cases: 100 }); + }); + + it('fetches filter discover stats', async () => { + mockAxios.get.mockResolvedValue({ + data: { stats: { cases: 50 } }, + }); + + const result = await store.getStateData('filter', 3); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/filters/3/discover', + ); + expect(result).toEqual({ cases: 50 }); + }); + }); + + describe('getFileName', () => { + it('finds file name by id', async () => { + mockAxios.get.mockResolvedValue({ + data: [ + { id: 1, name: 'file1.csv' }, + { id: 2, name: 'file2.csv' }, + ], + }); + + const result = await store.getFileName(1); + + expect(result).toBe('file1.csv'); + }); + + it('returns undefined for non-existent id', async () => { + mockAxios.get.mockResolvedValue({ + data: [{ id: 1, name: 'file1.csv' }], + }); + + const result = await store.getFileName(99); + + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/tests/stores/conformance.test.js b/tests/stores/conformance.test.js new file mode 100644 index 0000000..2f56a09 --- /dev/null +++ b/tests/stores/conformance.test.js @@ -0,0 +1,179 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; + +vi.mock('@/module/apiError.js', () => ({ + default: vi.fn(), +})); + +import apiError from '@/module/apiError.js'; +import useConformanceStore from '@/stores/conformance.js'; + +describe('conformanceStore', () => { + let store; + const mockAxios = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + }; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useConformanceStore(); + store.$axios = mockAxios; + vi.clearAllMocks(); + }); + + it('has correct default state', () => { + expect(store.conformanceLogId).toBeNull(); + expect(store.conformanceFilterId).toBeNull(); + expect(store.allConformanceTask).toEqual([]); + expect(store.selectedRuleType).toBe('Have activity'); + }); + + describe('getConformanceParams', () => { + it('fetches log check params when no filter', async () => { + store.conformanceLogId = 1; + store.conformanceFilterId = null; + const mockData = { + tasks: [{ label: 'A' }], + sources: ['A'], + sinks: ['B'], + processing_time: {}, + waiting_time: {}, + cycle_time: {}, + }; + mockAxios.get.mockResolvedValue({ data: mockData }); + + await store.getConformanceParams(); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/log-checks/params?log_id=1', + ); + expect(store.allConformanceTask).toEqual([{ label: 'A' }]); + expect(store.allCfmSeqStart).toEqual(['A']); + }); + + it('fetches filter check params when filter set', async () => { + store.conformanceFilterId = 5; + const mockData = { + tasks: [], + sources: [], + sinks: [], + processing_time: {}, + waiting_time: {}, + cycle_time: {}, + }; + mockAxios.get.mockResolvedValue({ data: mockData }); + + await store.getConformanceParams(); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/filter-checks/params?filter_id=5', + ); + }); + }); + + describe('addConformanceCheckId', () => { + it('posts to log temp-check and stores id', async () => { + store.conformanceLogId = 1; + store.conformanceFilterId = null; + mockAxios.post.mockResolvedValue({ data: { id: 42 } }); + + await store.addConformanceCheckId({ rule: 'test' }); + + expect(mockAxios.post).toHaveBeenCalledWith( + '/api/temp-log-checks?log_id=1', + { rule: 'test' }, + ); + expect(store.conformanceLogTempCheckId).toBe(42); + }); + + it('posts to filter temp-check when filter set', async () => { + store.conformanceFilterId = 3; + mockAxios.post.mockResolvedValue({ data: { id: 99 } }); + + await store.addConformanceCheckId({ rule: 'test' }); + + expect(mockAxios.post).toHaveBeenCalledWith( + '/api/temp-filter-checks?filter_id=3', + { rule: 'test' }, + ); + expect(store.conformanceFilterTempCheckId).toBe(99); + }); + }); + + describe('getConformanceReport', () => { + it('fetches temp log check report', async () => { + store.conformanceLogTempCheckId = 10; + const mockData = { file: {}, charts: {} }; + mockAxios.get.mockResolvedValue({ data: mockData }); + + await store.getConformanceReport(); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/temp-log-checks/10', + ); + expect(store.allConformanceTempReportData).toEqual(mockData); + }); + + it('stores routeFile when getRouteFile=true', async () => { + store.conformanceLogTempCheckId = 10; + const mockData = { file: { name: 'test.csv' } }; + mockAxios.get.mockResolvedValue({ data: mockData }); + + await store.getConformanceReport(true); + + expect(store.allRouteFile).toEqual({ name: 'test.csv' }); + expect(store.allConformanceTempReportData).toBeNull(); + }); + }); + + describe('addConformanceCreateCheckId', () => { + it('creates log check and clears temp id', async () => { + store.conformanceLogId = 1; + store.conformanceFilterId = null; + store.conformanceLogTempCheckId = 10; + store.conformanceRuleData = { type: 'test' }; + mockAxios.post.mockResolvedValue({ data: { id: 100 } }); + + await store.addConformanceCreateCheckId('myRule'); + + expect(mockAxios.post).toHaveBeenCalledWith( + '/api/log-checks?log_id=1', + { name: 'myRule', rule: { type: 'test' } }, + ); + expect(store.conformanceLogCreateCheckId).toBe(100); + expect(store.conformanceLogTempCheckId).toBeNull(); + }); + }); + + describe('updataConformance', () => { + it('updates existing log check', async () => { + store.conformanceLogCreateCheckId = 50; + store.conformanceRuleData = { type: 'updated' }; + mockAxios.put.mockResolvedValue({ status: 200 }); + + await store.updataConformance(); + + expect(mockAxios.put).toHaveBeenCalledWith( + '/api/log-checks/50', + { type: 'updated' }, + ); + expect(store.isUpdataConformance).toBe(true); + }); + }); + + it('setConformanceLogCreateCheckId sets value', () => { + store.setConformanceLogCreateCheckId('abc'); + expect(store.conformanceLogCreateCheckId).toBe('abc'); + }); + + describe('getters', () => { + it('conformanceTask returns labels', () => { + store.allConformanceTask = [ + { label: 'A' }, { label: 'B' }, + ]; + expect(store.conformanceTask).toEqual(['A', 'B']); + }); + }); +}); diff --git a/tests/stores/conformanceInput.test.js b/tests/stores/conformanceInput.test.js new file mode 100644 index 0000000..16d5c80 --- /dev/null +++ b/tests/stores/conformanceInput.test.js @@ -0,0 +1,32 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; +import useConformanceInputStore from '@/stores/conformanceInput.js'; + +describe('conformanceInputStore', () => { + let store; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useConformanceInputStore(); + }); + + it('has correct default state', () => { + expect(store.inputDataToSave.inputStart).toBeNull(); + expect(store.activityRadioData.task).toEqual(['', '']); + }); + + it('setActivityRadioStartEndData sets From', () => { + store.setActivityRadioStartEndData('taskA', 'From'); + expect(store.activityRadioData.task[0]).toBe('taskA'); + }); + + it('setActivityRadioStartEndData sets To', () => { + store.setActivityRadioStartEndData('taskB', 'To'); + expect(store.activityRadioData.task[1]).toBe('taskB'); + }); + + it('setActivityRadioStartEndData ignores unknown', () => { + store.setActivityRadioStartEndData('taskC', 'Unknown'); + expect(store.activityRadioData.task).toEqual(['', '']); + }); +}); diff --git a/tests/stores/cytoscapeStore.test.js b/tests/stores/cytoscapeStore.test.js new file mode 100644 index 0000000..8e62c59 --- /dev/null +++ b/tests/stores/cytoscapeStore.test.js @@ -0,0 +1,78 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; +import useCytoscapeStore from '@/stores/cytoscapeStore.ts'; +import { SAVE_KEY_NAME } from '@/constants/constants.js'; + +// Mock localStorage since jsdom's localStorage is limited +const localStorageMock = (() => { + let store = {}; + return { + getItem: vi.fn((key) => store[key] || null), + setItem: vi.fn((key, value) => { store[key] = value; }), + removeItem: vi.fn((key) => { delete store[key]; }), + clear: vi.fn(() => { store = {}; }), + }; +})(); +Object.defineProperty(globalThis, 'localStorage', { + value: localStorageMock, +}); + +describe('cytoscapeStore', () => { + let store; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useCytoscapeStore(); + localStorageMock.clear(); + vi.clearAllMocks(); + }); + + it('has correct default state', () => { + expect(store.nodePositions).toEqual({}); + expect(store.currentGraphId).toBe(''); + }); + + it('setCurrentGraphId initializes graph structure', () => { + store.setCurrentGraphId('graph1'); + expect(store.currentGraphId).toBe('graph1'); + expect(store.nodePositions['graph1']).toEqual({ + TB: [], + LR: [], + }); + }); + + it('saveNodePosition adds node and saves to localStorage', () => { + store.setCurrentGraphId('graph1'); + store.saveNodePosition('node1', { x: 10, y: 20 }, 'TB'); + expect(store.nodePositions['graph1']['TB']).toEqual([ + { id: 'node1', position: { x: 10, y: 20 } }, + ]); + expect(localStorageMock.setItem).toHaveBeenCalledWith( + SAVE_KEY_NAME, + expect.any(String), + ); + }); + + it('saveNodePosition updates existing node position', () => { + store.setCurrentGraphId('graph1'); + store.saveNodePosition('node1', { x: 10, y: 20 }, 'TB'); + store.saveNodePosition('node1', { x: 30, y: 40 }, 'TB'); + expect(store.nodePositions['graph1']['TB']).toHaveLength(1); + expect(store.nodePositions['graph1']['TB'][0].position) + .toEqual({ x: 30, y: 40 }); + }); + + it('loadPositionsFromStorage restores from localStorage', () => { + const data = { + graph1: { + TB: [{ id: 'n1', position: { x: 5, y: 10 } }], + }, + }; + localStorageMock.getItem.mockReturnValue(JSON.stringify(data)); + store.setCurrentGraphId('graph1'); + store.loadPositionsFromStorage('TB'); + expect(store.nodePositions['graph1']['TB']).toEqual([ + { id: 'n1', position: { x: 5, y: 10 } }, + ]); + }); +}); diff --git a/tests/stores/loading.test.js b/tests/stores/loading.test.js new file mode 100644 index 0000000..7239fff --- /dev/null +++ b/tests/stores/loading.test.js @@ -0,0 +1,27 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; +import useLoadingStore from '@/stores/loading.js'; + +describe('loadingStore', () => { + let store; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useLoadingStore(); + }); + + it('has isLoading true by default', () => { + expect(store.isLoading).toBe(true); + }); + + it('setIsLoading sets to false', () => { + store.setIsLoading(false); + expect(store.isLoading).toBe(false); + }); + + it('setIsLoading sets to true', () => { + store.setIsLoading(false); + store.setIsLoading(true); + expect(store.isLoading).toBe(true); + }); +}); diff --git a/tests/stores/login.test.js b/tests/stores/login.test.js new file mode 100644 index 0000000..03eace7 --- /dev/null +++ b/tests/stores/login.test.js @@ -0,0 +1,146 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; + +// Mock apiError to prevent side effects (imports router, pinia, toast) +vi.mock('@/module/apiError.js', () => ({ + default: vi.fn(), +})); + +import axios from 'axios'; +import useLoginStore from '@/stores/login.ts'; + +// Mock axios methods +vi.spyOn(axios, 'post').mockImplementation(vi.fn()); +vi.spyOn(axios, 'get').mockImplementation(vi.fn()); + +describe('loginStore', () => { + let store; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useLoginStore(); + store.$router = { push: vi.fn() }; + vi.clearAllMocks(); + // Clear cookies + document.cookie.split(';').forEach((c) => { + const name = c.split('=')[0].trim(); + if (name) { + document.cookie = name + '=; Max-Age=-99999999; path=/'; + } + }); + }); + + it('has correct default state', () => { + expect(store.auth.grant_type).toBe('password'); + expect(store.auth.username).toBe(''); + expect(store.isLoggedIn).toBe(false); + expect(store.isInvalid).toBe(false); + }); + + describe('signIn', () => { + it('stores token and navigates on success', async () => { + axios.post.mockResolvedValue({ + data: { + access_token: 'test-access-token', + refresh_token: 'test-refresh-token', + }, + }); + + await store.signIn(); + + expect(axios.post).toHaveBeenCalledWith( + '/api/oauth/token', + store.auth, + expect.objectContaining({ + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }), + ); + expect(store.isLoggedIn).toBe(true); + expect(document.cookie).toContain('luciaToken=test-access-token'); + expect(store.$router.push).toHaveBeenCalledWith('/files'); + }); + + it('redirects to remembered URL when set', async () => { + axios.post.mockResolvedValue({ + data: { + access_token: 'token', + refresh_token: 'refresh', + }, + }); + // btoa('/dashboard') = 'L2Rhc2hib2FyZA==' + store.rememberedReturnToUrl = btoa('/dashboard'); + + // Mock window.location.href setter + const originalLocation = window.location; + delete window.location; + window.location = { href: '' }; + + await store.signIn(); + + expect(window.location.href).toBe('/dashboard'); + window.location = originalLocation; + }); + + it('sets isInvalid on error', async () => { + axios.post.mockRejectedValue(new Error('401')); + + await store.signIn(); + + expect(store.isInvalid).toBe(true); + }); + }); + + describe('logOut', () => { + it('clears auth state and navigates to login', () => { + store.isLoggedIn = true; + store.logOut(); + + expect(store.isLoggedIn).toBe(false); + expect(store.$router.push).toHaveBeenCalledWith('/login'); + }); + }); + + describe('getUserData', () => { + it('stores user data on success', async () => { + axios.get.mockResolvedValue({ + data: { username: 'testuser', name: 'Test User' }, + }); + + await store.getUserData(); + + expect(axios.get).toHaveBeenCalledWith('/api/my-account'); + expect(store.userData).toEqual({ + username: 'testuser', + name: 'Test User', + }); + }); + }); + + describe('checkLogin', () => { + it('does not redirect on success', async () => { + axios.get.mockResolvedValue({ data: {} }); + + await store.checkLogin(); + + expect(store.$router.push).not.toHaveBeenCalled(); + }); + + it('redirects to login on error', async () => { + axios.get.mockRejectedValue(new Error('401')); + + await store.checkLogin(); + + expect(store.$router.push).toHaveBeenCalledWith('/login'); + }); + }); + + it('setRememberedReturnToUrl stores URL', () => { + store.setRememberedReturnToUrl('abc'); + expect(store.rememberedReturnToUrl).toBe('abc'); + }); + + it('setIsLoggedIn sets boolean', () => { + store.setIsLoggedIn(true); + expect(store.isLoggedIn).toBe(true); + }); +}); diff --git a/tests/stores/mapCompareStore.test.js b/tests/stores/mapCompareStore.test.js new file mode 100644 index 0000000..4edfc7f --- /dev/null +++ b/tests/stores/mapCompareStore.test.js @@ -0,0 +1,31 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; +import useMapCompareStore from '@/stores/mapCompareStore.ts'; + +describe('mapCompareStore', () => { + let store; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useMapCompareStore(); + }); + + it('has correct default state', () => { + expect(store.routeParam).toEqual({ + primaryType: '', + primaryId: '', + secondaryType: '', + secondaryId: '', + }); + }); + + it('setCompareRouteParam sets all params', () => { + store.setCompareRouteParam('log', '1', 'filter', '2'); + expect(store.routeParam).toEqual({ + primaryType: 'log', + primaryId: '1', + secondaryType: 'filter', + secondaryId: '2', + }); + }); +}); diff --git a/tests/stores/modal.test.js b/tests/stores/modal.test.js new file mode 100644 index 0000000..ba1a578 --- /dev/null +++ b/tests/stores/modal.test.js @@ -0,0 +1,30 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; +import { useModalStore } from '@/stores/modal.js'; +import { MODAL_ACCT_INFO, MODAL_DELETE } from '@/constants/constants.js'; + +describe('modalStore', () => { + let store; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useModalStore(); + }); + + it('has default state', () => { + expect(store.isModalOpen).toBe(false); + expect(store.whichModal).toBe(MODAL_ACCT_INFO); + }); + + it('openModal sets isModalOpen and whichModal', () => { + store.openModal(MODAL_DELETE); + expect(store.isModalOpen).toBe(true); + expect(store.whichModal).toBe(MODAL_DELETE); + }); + + it('closeModal sets isModalOpen to false', async () => { + store.openModal(MODAL_DELETE); + await store.closeModal(); + expect(store.isModalOpen).toBe(false); + }); +}); diff --git a/tests/stores/pageAdmin.test.js b/tests/stores/pageAdmin.test.js new file mode 100644 index 0000000..a18f830 --- /dev/null +++ b/tests/stores/pageAdmin.test.js @@ -0,0 +1,92 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; +import usePageAdminStore from '@/stores/pageAdmin.js'; + +describe('pageAdminStore', () => { + let store; + + beforeEach(() => { + setActivePinia(createPinia()); + store = usePageAdminStore(); + }); + + it('has correct default state', () => { + expect(store.activePage).toBe('MAP'); + expect(store.previousPage).toBe('MAP'); + expect(store.pendingActivePage).toBe('FILES'); + expect(store.isPagePending).toBe(false); + expect(store.shouldKeepPreviousPage).toBe(false); + expect(store.currentMapFile).toBe(''); + }); + + it('setActivePage converts page name', () => { + store.setActivePage('CheckConformance'); + expect(store.activePage).toBe('CONFORMANCE'); + }); + + it('setPrevioiusPage converts page name', () => { + store.setPrevioiusPage('CheckPerformance'); + expect(store.previousPage).toBe('PERFORMANCE'); + }); + + it('setPrevioiusPageUsingActivePage copies activePage', () => { + store.setActivePage('CONFORMANCE'); + store.setPrevioiusPageUsingActivePage(); + // Note: bug in source - writes to this.previoiusPage (typo) + // instead of this.previousPage, so previousPage stays 'MAP' + expect(store.previousPage).toBe('MAP'); + }); + + it('setIsPagePendingBoolean sets boolean', () => { + store.setIsPagePendingBoolean(true); + expect(store.isPagePending).toBe(true); + }); + + it('setPendingActivePage converts and sets page', () => { + store.setPendingActivePage('CheckMap'); + expect(store.pendingActivePage).toBe('MAP'); + }); + + it('copyPendingPageToActivePage transfers value', () => { + store.setPendingActivePage('CheckConformance'); + store.copyPendingPageToActivePage(); + expect(store.activePage).toBe('CONFORMANCE'); + }); + + it('clearPendingActivePage resets to empty', () => { + store.setPendingActivePage('CheckMap'); + store.clearPendingActivePage(); + expect(store.pendingActivePage).toBe(''); + }); + + it('keepPreviousPage restores previous page', () => { + store.setPrevioiusPage('CONFORMANCE'); + store.setActivePage('PERFORMANCE'); + store.keepPreviousPage(); + expect(store.activePage).toBe('CONFORMANCE'); + expect(store.shouldKeepPreviousPage).toBe(true); + }); + + it('clearShouldKeepPreviousPageBoolean resets flag', () => { + store.keepPreviousPage(); + store.clearShouldKeepPreviousPageBoolean(); + expect(store.shouldKeepPreviousPage).toBe(false); + }); + + it('setCurrentMapFile sets file name', () => { + store.setCurrentMapFile('test.csv'); + expect(store.currentMapFile).toBe('test.csv'); + }); + + it('setActivePageComputedByRoute extracts route name', () => { + const routeMatched = [{ name: 'CheckMap' }]; + store.setActivePageComputedByRoute(routeMatched); + expect(store.activePageComputedByRoute).toBe('MAP'); + }); + + it('setActivePageComputedByRoute handles empty array', () => { + store.setActivePageComputedByRoute([]); + // Should not change default value + expect(store.activePageComputedByRoute).toBe('MAP'); + }); +}); diff --git a/tests/stores/performance.test.js b/tests/stores/performance.test.js new file mode 100644 index 0000000..6d1b6ec --- /dev/null +++ b/tests/stores/performance.test.js @@ -0,0 +1,83 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; + +vi.mock('@/module/apiError.js', () => ({ + default: vi.fn(), +})); + +import usePerformanceStore from '@/stores/performance.js'; + +describe('performanceStore', () => { + let store; + const mockAxios = { + get: vi.fn(), + }; + + beforeEach(() => { + setActivePinia(createPinia()); + store = usePerformanceStore(); + store.$axios = mockAxios; + vi.clearAllMocks(); + }); + + it('has correct default state', () => { + expect(store.allPerformanceData).toBeNull(); + expect(store.freqChartData).toBeNull(); + }); + + it('performanceData getter returns allPerformanceData', () => { + store.allPerformanceData = { time: {}, freq: {} }; + expect(store.performanceData).toEqual({ time: {}, freq: {} }); + }); + + describe('getPerformance', () => { + it('fetches log performance data', async () => { + const mockData = { time: { charts: [] }, freq: { charts: [] } }; + mockAxios.get.mockResolvedValue({ data: mockData }); + + await store.getPerformance('log', 1); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/logs/1/performance', + ); + expect(store.allPerformanceData).toEqual(mockData); + }); + + it('fetches filter performance data', async () => { + mockAxios.get.mockResolvedValue({ data: { time: {} } }); + + await store.getPerformance('filter', 5); + + expect(mockAxios.get).toHaveBeenCalledWith( + '/api/filters/5/performance', + ); + }); + + it('does not throw on API failure', async () => { + mockAxios.get.mockRejectedValue(new Error('Network error')); + + // Should not throw - apiError handles it + await expect(store.getPerformance('log', 1)) + .resolves.not.toThrow(); + expect(store.allPerformanceData).toBeNull(); + }); + }); + + it('setFreqChartData sets data', () => { + const data = { labels: [], datasets: [] }; + store.setFreqChartData(data); + expect(store.freqChartData).toEqual(data); + }); + + it('setFreqChartOptions sets options', () => { + const opts = { responsive: true }; + store.setFreqChartOptions(opts); + expect(store.freqChartOptions).toEqual(opts); + }); + + it('setFreqChartXData sets x data', () => { + const xData = { minX: 0, maxX: 100 }; + store.setFreqChartXData(xData); + expect(store.freqChartXData).toEqual(xData); + }); +});