🤡 Unit testing scaffold
This commit is contained in:
1
__mocks__/fileMock.js
Normal file
1
__mocks__/fileMock.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = 'test-file-stub';
|
||||
28
jest.config.js
Normal file
28
jest.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
||||
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js',
|
||||
},
|
||||
testMatch: [
|
||||
'**/__tests__/**/*.+(ts|tsx|js)',
|
||||
'**/?(*.)+(spec|test).+(ts|tsx|js)',
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': ['ts-jest', {
|
||||
tsconfig: {
|
||||
jsx: 'react',
|
||||
esModuleInterop: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
},
|
||||
}],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,tsx}',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/**/*.stories.tsx',
|
||||
],
|
||||
};
|
||||
25
jest.setup.js
Normal file
25
jest.setup.js
Normal file
@@ -0,0 +1,25 @@
|
||||
require('@testing-library/jest-dom');
|
||||
|
||||
// Mock window.matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
};
|
||||
global.localStorage = localStorageMock;
|
||||
22564
package-lock.json
generated
Normal file
22564
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,9 @@
|
||||
"dev": "rimraf dist && npm run build && vite",
|
||||
"start": "npm run build && vite",
|
||||
"docs": "jsdoc -c jsdoc.json",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
@@ -92,6 +95,9 @@
|
||||
"@storybook/testing-library": "^0.2.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.0.5",
|
||||
"@tanstack/react-query-devtools": "^5.1.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@types/ellipsize": "^0.1.1",
|
||||
"@types/express": "^4.17.8",
|
||||
@@ -114,8 +120,10 @@
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-storybook": "^0.6.13",
|
||||
"express": "^4.20.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"install": "^0.13.0",
|
||||
"jest": "^29.6.3",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"postcss": "^8.4.32",
|
||||
"postcss-import": "^15.1.0",
|
||||
@@ -125,6 +133,7 @@
|
||||
"sass": "^1.77.0",
|
||||
"storybook": "^7.6.21",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.4.6",
|
||||
"tui-jsdoc-template": "^1.2.2",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
|
||||
493
src/client/components/Import/Import.test.tsx
Normal file
493
src/client/components/Import/Import.test.tsx
Normal file
@@ -0,0 +1,493 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { Import } from './Import';
|
||||
|
||||
// Mock axios
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.MockedFunction<any>;
|
||||
|
||||
// Mock zustand store
|
||||
const mockGetSocket = jest.fn();
|
||||
const mockDisconnectSocket = jest.fn();
|
||||
const mockSetStatus = jest.fn();
|
||||
|
||||
jest.mock('../../store', () => ({
|
||||
useStore: jest.fn((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'drained',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock socket.io-client
|
||||
const mockSocket = {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
mockGetSocket.mockReturnValue(mockSocket);
|
||||
|
||||
// Helper function to create a wrapper with QueryClient
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Import Component - Numerical Indices', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should display numerical indices in the Past Imports table', async () => {
|
||||
// Mock API response with 3 import sessions
|
||||
const mockData = [
|
||||
{
|
||||
sessionId: 'session-1',
|
||||
earliestTimestamp: '2024-01-01T10:00:00Z',
|
||||
completedJobs: 5,
|
||||
failedJobs: 0
|
||||
},
|
||||
{
|
||||
sessionId: 'session-2',
|
||||
earliestTimestamp: '2024-01-02T10:00:00Z',
|
||||
completedJobs: 3,
|
||||
failedJobs: 1
|
||||
},
|
||||
{
|
||||
sessionId: 'session-3',
|
||||
earliestTimestamp: '2024-01-03T10:00:00Z',
|
||||
completedJobs: 8,
|
||||
failedJobs: 2
|
||||
},
|
||||
];
|
||||
|
||||
(axios as any).mockResolvedValue({ data: mockData });
|
||||
(axios.request as jest.Mock) = jest.fn().mockResolvedValue({ data: {} });
|
||||
|
||||
render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
// Wait for the "Past Imports" heading to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Past Imports')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify that the "#" column header exists
|
||||
expect(screen.getByText('#')).toBeInTheDocument();
|
||||
|
||||
// Verify that numerical indices (1, 2, 3) are displayed in the first column of each row
|
||||
const rows = screen.getAllByRole('row');
|
||||
// Skip header row (index 0), check data rows
|
||||
expect(rows[1].querySelectorAll('td')[0]).toHaveTextContent('1');
|
||||
expect(rows[2].querySelectorAll('td')[0]).toHaveTextContent('2');
|
||||
expect(rows[3].querySelectorAll('td')[0]).toHaveTextContent('3');
|
||||
});
|
||||
|
||||
test('should display correct indices for larger datasets', async () => {
|
||||
// Mock API response with 10 import sessions
|
||||
const mockData = Array.from({ length: 10 }, (_, i) => ({
|
||||
sessionId: `session-${i + 1}`,
|
||||
earliestTimestamp: `2024-01-${String(i + 1).padStart(2, '0')}T10:00:00Z`,
|
||||
completedJobs: i + 1,
|
||||
failedJobs: 0,
|
||||
}));
|
||||
|
||||
(axios as any).mockResolvedValue({ data: mockData });
|
||||
(axios.request as jest.Mock) = jest.fn().mockResolvedValue({ data: {} });
|
||||
|
||||
render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Past Imports')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify indices 1 through 10 are present in the first column
|
||||
const rows = screen.getAllByRole('row');
|
||||
// Skip header row (index 0)
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const row = rows[i];
|
||||
const cells = row.querySelectorAll('td');
|
||||
expect(cells[0]).toHaveTextContent(i.toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import Component - Button Visibility', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(axios as any).mockResolvedValue({ data: [] });
|
||||
(axios.request as jest.Mock) = jest.fn().mockResolvedValue({ data: {} });
|
||||
});
|
||||
|
||||
test('should show Start Import button when queue status is drained', async () => {
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'drained',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Start Import')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify Pause and Resume buttons are NOT visible
|
||||
expect(screen.queryByText('Pause')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Resume')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show Start Import button when queue status is undefined', async () => {
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: undefined,
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Start Import')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should hide Start Import button and show Pause button when queue is running', async () => {
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'running',
|
||||
successfulJobCount: 5,
|
||||
failedJobCount: 1,
|
||||
mostRecentImport: 'Comic #123',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Start Import')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Pause')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify Import Activity section is visible
|
||||
expect(screen.getByText('Import Activity')).toBeInTheDocument();
|
||||
expect(screen.getByText('5')).toBeInTheDocument(); // successful count
|
||||
expect(screen.getByText('1')).toBeInTheDocument(); // failed count
|
||||
});
|
||||
|
||||
test('should hide Start Import button and show Resume button when queue is paused', async () => {
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'paused',
|
||||
successfulJobCount: 3,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: 'Comic #456',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Start Import')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Resume')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import Component - SessionId and Socket Reconnection', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers();
|
||||
localStorage.clear();
|
||||
(axios as any).mockResolvedValue({ data: [] });
|
||||
(axios.request as jest.Mock) = jest.fn().mockResolvedValue({ data: {} });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('should clear sessionId and reconnect socket when starting import after queue is drained', async () => {
|
||||
// Setup: Set old sessionId in localStorage
|
||||
localStorage.setItem('sessionId', 'old-session-id');
|
||||
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'drained',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
// Click the "Start Import" button
|
||||
const startButton = await screen.findByText('Start Import');
|
||||
fireEvent.click(startButton);
|
||||
|
||||
// Verify sessionId is cleared immediately
|
||||
expect(localStorage.getItem('sessionId')).toBeNull();
|
||||
|
||||
// Verify disconnectSocket is called
|
||||
expect(mockDisconnectSocket).toHaveBeenCalledWith('/');
|
||||
|
||||
// Fast-forward 100ms
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(100);
|
||||
});
|
||||
|
||||
// Verify getSocket is called after 100ms
|
||||
await waitFor(() => {
|
||||
expect(mockGetSocket).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
// Fast-forward another 500ms
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
// Verify initiateImport is called and status is set to running
|
||||
await waitFor(() => {
|
||||
expect(axios.request).toHaveBeenCalledWith({
|
||||
url: 'http://localhost:3000/api/library/newImport',
|
||||
method: 'POST',
|
||||
data: { sessionId: null },
|
||||
});
|
||||
expect(mockSetStatus).toHaveBeenCalledWith('running');
|
||||
});
|
||||
});
|
||||
|
||||
test('should NOT clear sessionId when starting import with undefined status', async () => {
|
||||
// Setup: Set existing sessionId in localStorage
|
||||
localStorage.setItem('sessionId', 'existing-session-id');
|
||||
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: undefined,
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
// Click the "Start Import" button
|
||||
const startButton = await screen.findByText('Start Import');
|
||||
fireEvent.click(startButton);
|
||||
|
||||
// Verify sessionId is NOT cleared
|
||||
expect(localStorage.getItem('sessionId')).toBe('existing-session-id');
|
||||
|
||||
// Verify disconnectSocket is NOT called
|
||||
expect(mockDisconnectSocket).not.toHaveBeenCalled();
|
||||
|
||||
// Verify status is set to running immediately
|
||||
expect(mockSetStatus).toHaveBeenCalledWith('running');
|
||||
|
||||
// Verify initiateImport is called immediately (no delay)
|
||||
await waitFor(() => {
|
||||
expect(axios.request).toHaveBeenCalledWith({
|
||||
url: 'http://localhost:3000/api/library/newImport',
|
||||
method: 'POST',
|
||||
data: { sessionId: 'existing-session-id' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import Component - Real-time Updates', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(axios as any).mockResolvedValue({ data: [] });
|
||||
(axios.request as jest.Mock) = jest.fn().mockResolvedValue({ data: {} });
|
||||
});
|
||||
|
||||
test('should refetch table data when LS_COVER_EXTRACTED event is received', async () => {
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'running',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
// Wait for component to mount and socket listeners to be attached
|
||||
await waitFor(() => {
|
||||
expect(mockSocket.on).toHaveBeenCalledWith('LS_COVER_EXTRACTED', expect.any(Function));
|
||||
});
|
||||
|
||||
// Get the event handler that was registered
|
||||
const coverExtractedHandler = mockSocket.on.mock.calls.find(
|
||||
(call) => call[0] === 'LS_COVER_EXTRACTED'
|
||||
)?.[1];
|
||||
|
||||
// Clear previous axios calls
|
||||
(axios as any).mockClear();
|
||||
|
||||
// Simulate the socket event
|
||||
if (coverExtractedHandler) {
|
||||
coverExtractedHandler();
|
||||
}
|
||||
|
||||
// Verify that the API is called again (refetch)
|
||||
await waitFor(() => {
|
||||
expect(axios).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:3000/api/jobqueue/getJobResultStatistics',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should refetch table data when LS_IMPORT_QUEUE_DRAINED event is received', async () => {
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'running',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
// Wait for component to mount and socket listeners to be attached
|
||||
await waitFor(() => {
|
||||
expect(mockSocket.on).toHaveBeenCalledWith('LS_IMPORT_QUEUE_DRAINED', expect.any(Function));
|
||||
});
|
||||
|
||||
// Get the event handler that was registered
|
||||
const queueDrainedHandler = mockSocket.on.mock.calls.find(
|
||||
(call) => call[0] === 'LS_IMPORT_QUEUE_DRAINED'
|
||||
)?.[1];
|
||||
|
||||
// Clear previous axios calls
|
||||
(axios as any).mockClear();
|
||||
|
||||
// Simulate the socket event
|
||||
if (queueDrainedHandler) {
|
||||
queueDrainedHandler();
|
||||
}
|
||||
|
||||
// Verify that the API is called again (refetch)
|
||||
await waitFor(() => {
|
||||
expect(axios).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:3000/api/jobqueue/getJobResultStatistics',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should cleanup socket listeners on unmount', async () => {
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'drained',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
const { unmount } = render(<Import path="/test" />, { wrapper: createWrapper() });
|
||||
|
||||
// Wait for socket listeners to be attached
|
||||
await waitFor(() => {
|
||||
expect(mockSocket.on).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Unmount the component
|
||||
unmount();
|
||||
|
||||
// Verify that socket listeners are removed
|
||||
expect(mockSocket.off).toHaveBeenCalledWith('LS_COVER_EXTRACTED', expect.any(Function));
|
||||
expect(mockSocket.off).toHaveBeenCalledWith('LS_IMPORT_QUEUE_DRAINED', expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactElement, useCallback, useEffect } from "react";
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from "react";
|
||||
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||
import { format } from "date-fns";
|
||||
import Loader from "react-loader-spinner";
|
||||
@@ -27,21 +27,24 @@ interface IProps {
|
||||
|
||||
export const Import = (props: IProps): ReactElement => {
|
||||
const queryClient = useQueryClient();
|
||||
const { importJobQueue, getSocket } = useStore(
|
||||
const [socketReconnectTrigger, setSocketReconnectTrigger] = useState(0);
|
||||
const { importJobQueue, getSocket, disconnectSocket } = useStore(
|
||||
useShallow((state) => ({
|
||||
importJobQueue: state.importJobQueue,
|
||||
getSocket: state.getSocket,
|
||||
disconnectSocket: state.disconnectSocket,
|
||||
})),
|
||||
);
|
||||
|
||||
const sessionId = localStorage.getItem("sessionId");
|
||||
const { mutate: initiateImport } = useMutation({
|
||||
mutationFn: async () =>
|
||||
await axios.request({
|
||||
mutationFn: async () => {
|
||||
const sessionId = localStorage.getItem("sessionId");
|
||||
return await axios.request({
|
||||
url: `http://localhost:3000/api/library/newImport`,
|
||||
method: "POST",
|
||||
data: { sessionId },
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { data, isError, isLoading, refetch } = useQuery({
|
||||
@@ -83,7 +86,7 @@ export const Import = (props: IProps): ReactElement => {
|
||||
socket.off("LS_IMPORT_QUEUE_DRAINED", handleQueueDrained);
|
||||
socket.off("LS_COVER_EXTRACTED", handleCoverExtracted);
|
||||
};
|
||||
}, [getSocket, refetch]);
|
||||
}, [getSocket, refetch, socketReconnectTrigger]);
|
||||
|
||||
const toggleQueue = (queueAction: string, queueStatus: string) => {
|
||||
const socket = getSocket("/");
|
||||
@@ -193,8 +196,26 @@ export const Import = (props: IProps): ReactElement => {
|
||||
<button
|
||||
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-5 py-3 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||
onClick={() => {
|
||||
initiateImport();
|
||||
importJobQueue.setStatus("running");
|
||||
// Clear old sessionId when starting a new import after queue is drained
|
||||
if (importJobQueue.status === "drained") {
|
||||
localStorage.removeItem("sessionId");
|
||||
// Disconnect and reconnect socket to get new sessionId
|
||||
disconnectSocket("/");
|
||||
// Wait for socket to reconnect and get new sessionId before starting import
|
||||
setTimeout(() => {
|
||||
getSocket("/");
|
||||
// Trigger useEffect to re-attach event listeners
|
||||
setSocketReconnectTrigger(prev => prev + 1);
|
||||
// Wait a bit more for sessionInitialized event to fire
|
||||
setTimeout(() => {
|
||||
initiateImport();
|
||||
importJobQueue.setStatus("running");
|
||||
}, 500);
|
||||
}, 100);
|
||||
} else {
|
||||
initiateImport();
|
||||
importJobQueue.setStatus("running");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="text-md">Start Import</span>
|
||||
|
||||
@@ -50,12 +50,14 @@ export const useStore = create<StoreState>((set, get) => ({
|
||||
console.log(`✅ Connected to ${namespace}:`, socket.id);
|
||||
});
|
||||
|
||||
// Always listen for sessionInitialized in case backend creates a new session
|
||||
socket.on("sessionInitialized", (id) => {
|
||||
console.log("Session initialized with ID:", id);
|
||||
localStorage.setItem("sessionId", id);
|
||||
});
|
||||
|
||||
if (sessionId) {
|
||||
socket.emit("call", "socket.resumeSession", { sessionId, namespace });
|
||||
} else {
|
||||
socket.on("sessionInitialized", (id) => {
|
||||
localStorage.setItem("sessionId", id);
|
||||
});
|
||||
}
|
||||
|
||||
socket.on("RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION", (data) => {
|
||||
@@ -90,14 +92,17 @@ export const useStore = create<StoreState>((set, get) => ({
|
||||
});
|
||||
|
||||
socket.on("LS_IMPORT_QUEUE_DRAINED", () => {
|
||||
localStorage.removeItem("sessionId");
|
||||
set((state) => ({
|
||||
importJobQueue: {
|
||||
...state.importJobQueue,
|
||||
status: "drained",
|
||||
},
|
||||
}));
|
||||
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
|
||||
// Delay query invalidation and sessionId removal to ensure backend has persisted data
|
||||
setTimeout(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
|
||||
localStorage.removeItem("sessionId");
|
||||
}, 500);
|
||||
});
|
||||
|
||||
socket.on("CV_SCRAPING_STATUS", (data) => {
|
||||
|
||||
Reference in New Issue
Block a user