diff --git a/src/module/apiError.js b/src/module/apiError.js index 869003a..83e8111 100644 --- a/src/module/apiError.js +++ b/src/module/apiError.js @@ -1,13 +1,10 @@ import router from "@/router/index.ts"; import loadingStore from '@/stores/loading.js'; -import pinia from '@/stores/main.ts'; import {useToast} from 'vue-toast-notification'; import 'vue-toast-notification/dist/theme-sugar.css'; import axios from "axios"; import { deleteCookie } from "@/utils/cookieUtil.js"; -const loading = loadingStore(pinia); -const $toast = useToast(); // Delay loading and toast const delay = (s = 0) => new Promise((resolve, reject) => setTimeout(resolve, s)); @@ -23,6 +20,8 @@ export default async function apiError(error, toastMessage) { deleteCookie("luciaToken"); return router.push('/login'); } + const loading = loadingStore(); + const $toast = useToast(); await delay(); loading.isLoading = true; await delay(1000); diff --git a/src/module/cytoscapeMap.js b/src/module/cytoscapeMap.js index 22a9939..49ec9c7 100644 --- a/src/module/cytoscapeMap.js +++ b/src/module/cytoscapeMap.js @@ -10,7 +10,6 @@ import { getTimeLabel } from '@/module/timeLabel.js'; // 時間格式轉換器 import CytoscapeStore from '@/stores/cytoscapeStore'; import { SAVE_KEY_NAME } from '@/constants/constants.js'; -const mapPathStore = MapPathStore(); const composeFreqTypeText = (baseText, dataLayerOption, optionValue) => { //sonar-qube let text = baseText; const textInt = dataLayerOption === 'rel_freq' ? baseText + optionValue * 100 + "%" : baseText + optionValue; @@ -233,12 +232,12 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu // 按下節點光暈效果與鄰邊光暈效果 cy.on('tap, mousedown', 'node', function (event) { - mapPathStore.onNodeClickHighlightEdges(event.target); + MapPathStore().onNodeClickHighlightEdges(event.target); }); // 按下線段光暈效果與兩端點光暈效果 cy.on('tap, mousedown', 'edge', function (event) { - mapPathStore.onEdgeClickHighlightNodes(event.target); + MapPathStore().onEdgeClickHighlightNodes(event.target); }); // creat tippy.js diff --git a/src/stores/files.js b/src/stores/files.js index 76e7463..18a7543 100644 --- a/src/stores/files.js +++ b/src/stores/files.js @@ -4,11 +4,8 @@ import moment from 'moment'; import apiError from '@/module/apiError.js'; import Swal from 'sweetalert2'; import { uploadFailedFirst, uploadFailedSecond, uploadloader, uploadSuccess, deleteSuccess } from '@/module/alertModal.js'; -import pinia from '@/stores/main.ts'; import loadingStore from '@/stores/loading.js'; -const loading = loadingStore(pinia); - export default defineStore('filesStore', { state: () => ({ allEventFiles: [ @@ -261,6 +258,7 @@ export default defineStore('filesStore', { if(id == null || isNaN(id)) { return $toast.default('Delete File API Error.', {position: 'bottom'}); }; + const loading = loadingStore(); loading.isLoading = true; switch (type) { case 'log': @@ -294,6 +292,7 @@ export default defineStore('filesStore', { async deletionRecord(id) { let api = ''; + const loading = loadingStore(); loading.isLoading = true; api = `/api/deletion/${id}`; try { diff --git a/tests/stores/files.test.js b/tests/stores/files.test.js new file mode 100644 index 0000000..22f7a61 --- /dev/null +++ b/tests/stores/files.test.js @@ -0,0 +1,201 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { setActivePinia, createPinia } from 'pinia'; + +// Mock modules that have deep import chains (router, Swal, pinia, toast) +vi.mock('@/module/apiError.js', () => ({ + default: vi.fn(), +})); +vi.mock('@/module/alertModal.js', () => ({ + uploadFailedFirst: vi.fn(), + uploadFailedSecond: vi.fn(), + uploadloader: vi.fn(), + uploadSuccess: vi.fn(), + deleteSuccess: vi.fn(), +})); +vi.mock('sweetalert2', () => ({ + default: { close: vi.fn(), fire: vi.fn() }, +})); +// Prevent module-level store init in cytoscapeMap.js (loaded via router → Map.vue) +vi.mock('@/module/cytoscapeMap.js', () => ({})); +vi.mock('@/router/index.ts', () => ({ + default: { push: vi.fn(), currentRoute: { value: { path: '/' } } }, +})); + +import axios from 'axios'; +import useFilesStore from '@/stores/files.js'; + +vi.spyOn(axios, 'get').mockImplementation(vi.fn()); +vi.spyOn(axios, 'post').mockImplementation(vi.fn()); +vi.spyOn(axios, 'put').mockImplementation(vi.fn()); +vi.spyOn(axios, 'delete').mockImplementation(vi.fn()); + +describe('filesStore', () => { + let store; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useFilesStore(); + store.$router = { push: vi.fn() }; + vi.clearAllMocks(); + }); + + it('has correct default state', () => { + expect(store.filesTag).toBe('ALL'); + expect(store.httpStatus).toBe(200); + expect(store.uploadId).toBeNull(); + }); + + describe('allFiles getter', () => { + it('filters files by current filesTag', () => { + store.allEventFiles = [ + { fileType: 'Log', name: 'a.xes' }, + { fileType: 'Filter', name: 'b' }, + { fileType: 'Design', name: 'c' }, + ]; + + store.filesTag = 'COMPARE'; + expect(store.allFiles.map((f) => f.name)).toEqual(['a.xes', 'b']); + + store.filesTag = 'ALL'; + expect(store.allFiles).toHaveLength(3); + }); + }); + + describe('fetchAllFiles', () => { + it('fetches and transforms file data', async () => { + axios.get.mockResolvedValue({ + data: [ + { + type: 'log', + name: 'test.xes', + owner: { name: 'Alice' }, + updated_at: '2024-01-15T10:00:00Z', + accessed_at: '2024-01-15T11:00:00Z', + }, + { + type: 'filter', + name: 'filter1', + parent: { name: 'test.xes' }, + owner: { name: 'Bob' }, + updated_at: '2024-01-16T10:00:00Z', + accessed_at: null, + }, + ], + }); + + await store.fetchAllFiles(); + + expect(axios.get).toHaveBeenCalledWith('/api/files'); + expect(store.allEventFiles).toHaveLength(2); + expect(store.allEventFiles[0].fileType).toBe('Log'); + expect(store.allEventFiles[0].icon).toBe('work_history'); + expect(store.allEventFiles[0].ownerName).toBe('Alice'); + expect(store.allEventFiles[1].fileType).toBe('Filter'); + expect(store.allEventFiles[1].parentLog).toBe('test.xes'); + expect(store.allEventFiles[1].accessed_at).toBeNull(); + }); + + it('does not throw on API failure', async () => { + axios.get.mockRejectedValue(new Error('Network error')); + await expect(store.fetchAllFiles()).resolves.toBeUndefined(); + }); + }); + + describe('upload', () => { + it('uploads file and navigates to Upload page', async () => { + axios.post.mockResolvedValue({ data: { id: 42 } }); + const formData = new FormData(); + + await store.upload(formData); + + expect(axios.post).toHaveBeenCalledWith( + '/api/logs/csv-uploads', + formData, + expect.objectContaining({ + headers: { 'Content-Type': 'multipart/form-data' }, + }), + ); + expect(store.uploadId).toBe(42); + expect(store.$router.push).toHaveBeenCalledWith({ name: 'Upload' }); + }); + }); + + describe('getUploadDetail', () => { + it('fetches upload preview', async () => { + store.uploadId = 10; + axios.get.mockResolvedValue({ + data: { preview: { columns: ['a', 'b'] } }, + }); + + await store.getUploadDetail(); + + expect(axios.get).toHaveBeenCalledWith('/api/logs/csv-uploads/10'); + expect(store.allUploadDetail).toEqual({ columns: ['a', 'b'] }); + }); + }); + + describe('rename', () => { + it('renames a log file', async () => { + axios.put.mockResolvedValue({}); + axios.get.mockResolvedValue({ data: [] }); + + await store.rename('log', 5, 'new-name'); + + expect(axios.put).toHaveBeenCalledWith('/api/logs/5/name', { + name: 'new-name', + }); + }); + }); + + describe('getDependents', () => { + it('fetches dependents for a log', async () => { + axios.get.mockResolvedValue({ data: [{ id: 1 }, { id: 2 }] }); + + await store.getDependents('log', 7); + + expect(axios.get).toHaveBeenCalledWith('/api/logs/7/dependents'); + expect(store.allDependentsData).toEqual([{ id: 1 }, { id: 2 }]); + }); + }); + + describe('deleteFile', () => { + it('deletes a log file via axios.delete', async () => { + axios.get.mockResolvedValue({ data: [] }); + axios.delete.mockResolvedValue({}); + + await store.deleteFile('log', 1); + + expect(axios.delete).toHaveBeenCalledWith('/api/logs/1'); + }); + }); + + describe('deletionRecord', () => { + it('deletes a deletion record', async () => { + axios.delete.mockResolvedValue({}); + + await store.deletionRecord(5); + + expect(axios.delete).toHaveBeenCalledWith('/api/deletion/5'); + }); + }); + + describe('downloadFileCSV', () => { + it('downloads CSV for a log', async () => { + axios.get.mockResolvedValue({ data: 'col1,col2\na,b' }); + + window.URL.createObjectURL = vi.fn().mockReturnValue('blob:test'); + window.URL.revokeObjectURL = vi.fn(); + + await store.downloadFileCSV('log', 3, 'my-file'); + + expect(axios.get).toHaveBeenCalledWith('/api/logs/3/csv'); + expect(window.URL.revokeObjectURL).toHaveBeenCalledWith('blob:test'); + }); + + it('returns early for unsupported type', async () => { + await store.downloadFileCSV('log-check', 3, 'file'); + + expect(axios.get).not.toHaveBeenCalled(); + }); + }); +});