Skip to content

Commit 2e91b41

Browse files
committed
test: Add redux capabilities to testing
test: Add backend server mock support for testing
1 parent 1dc26f5 commit 2e91b41

13 files changed

Lines changed: 815 additions & 114 deletions

File tree

package-lock.json

Lines changed: 350 additions & 80 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"lodash": "^4.17.21",
9393
"mime": "2.4.4",
9494
"minimist": "^1.2.7",
95+
"msw": "^2.12.1",
9596
"node-7z": "3.0.0",
9697
"open": "^10.1.0",
9798
"react": "19.2.0",

src/back/SocketServer.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { api_handle_message, api_register, api_register_any, api_unregister, api
55
import { create_server, server_add_client, server_broadcast, server_request, server_send, SocketServerData } from '@shared/socket/SocketServer';
66
import { SocketRequestData, SocketResponseData } from '@shared/socket/types';
77
import * as ws from 'ws';
8-
import { VERBOSE } from '.';
98
import { genPipelineBackOut, MiddlewareRes, PipelineRes } from './SocketServerMiddleware';
109
import { createNewDialog } from './util/dialog';
1110

11+
const verbose = false;
12+
1213
type BackAPI = SocketAPIData<BackIn, BackInTemplate, MsgEvent>
1314
type BackClients = SocketServerData<BackOut, BackOutTemplate, ws>
1415
export type BackClient = BackClients['clients'][number]
@@ -74,7 +75,7 @@ export class SocketServer {
7475
public async listen(minPort: number, maxPort: number, host: string | undefined): Promise<void> {
7576
this.host = host;
7677
const result = await startServer(this.port !== -1 ? this.port : minPort, this.port !== -1 ? this.port : maxPort, host);
77-
result.server.on('connection', this.onConnect.bind(this));
78+
result.server.on('connection', this.onConnect);
7879
this.server = result.server;
7980
this.port = result.port;
8081
this.retryCounter = 0; // Reset retries on a good connection
@@ -283,7 +284,7 @@ export class SocketServer {
283284

284285
// Event Handlers
285286

286-
protected onConnect(socket: ws): void {
287+
onConnect = (socket: ws) => {
287288
// Read the first message as a "secret key"
288289
socket.onmessage = (event) => {
289290
if (event.data === 'flashpoint-launcher') {
@@ -297,9 +298,9 @@ export class SocketServer {
297298
socket.close();
298299
}
299300
};
300-
}
301+
};
301302

302-
protected async onMessage(event: ws.MessageEvent): Promise<void> {
303+
async onMessage(event: ws.MessageEvent): Promise<void> {
303304
const [parsed_data, parse_error] = parse_message_data(event.data);
304305

305306
if (parse_error) {
@@ -360,7 +361,7 @@ export class SocketServer {
360361
const start = performance.now();
361362
const [inc, out] = await api_handle_message(this.api, data, msg_event);
362363
const end = performance.now();
363-
if (VERBOSE.enabled && 'type' in data && data.type !== BackIn.KEEP_ALIVE) {
364+
if (verbose && 'type' in data && data.type !== BackIn.KEEP_ALIVE) {
364365
console.log(`${Math.floor(end - start)}ms - "${BackIn[data.type]}"`);
365366
}
366367

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { setMainState } from '@renderer/store/main/slice';
2+
import { renderWithProviders } from '@test/redux';
3+
import { waitFor } from '@testing-library/react';
4+
import { describe, expect, it } from 'vitest';
5+
import { SplashScreen } from './SplashScreen';
6+
7+
describe('SplashScreen', () => {
8+
it('should unmount 2000ms after loaded all', async () => {
9+
const { container, store } = renderWithProviders(<SplashScreen/>);
10+
11+
expect(container.querySelector('.splash-screen')).toBeInTheDocument();
12+
13+
store.dispatch(setMainState({
14+
loadedAll: true,
15+
}));
16+
17+
// Wait for it to disappear (with timeout slightly longer than 2000ms)
18+
await waitFor(
19+
() => {
20+
expect(container.querySelector('.splash-screen')).not.toBeInTheDocument();
21+
},
22+
{ timeout: 3000 }
23+
);
24+
});
25+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { setCurrentCuration } from '@renderer/store/curate/slice';
2+
import { mockCuration } from '@test/mocks/curate';
3+
import { useTestServer } from '@test/useTestServer';
4+
import { describe, expect, it } from 'vitest';
5+
import { setupStore } from '../store';
6+
7+
describe('Curate Redux Store', () => {
8+
useTestServer();
9+
10+
it('selection ranges with ctrl + shift modifiers', async () => {
11+
12+
const curations = [
13+
mockCuration(),
14+
mockCuration(),
15+
mockCuration(),
16+
mockCuration(),
17+
mockCuration()
18+
];
19+
20+
const store = setupStore({
21+
curate: {
22+
curations,
23+
loaded: true,
24+
groups: [],
25+
collapsedGroups: [],
26+
current: '',
27+
selected: [],
28+
lastSelected: '',
29+
curationTemplates: []
30+
}
31+
});
32+
33+
// Select third
34+
store.dispatch(setCurrentCuration({
35+
folder: curations[2].folder
36+
}));
37+
expect(store.getState().curate.current).toBe(curations[2].folder);
38+
expect(store.getState().curate.lastSelected).toBe(curations[2].folder);
39+
expect(store.getState().curate.selected).toHaveLength(1);
40+
expect(store.getState().curate.selected).toEqual(expect.arrayContaining([curations[2].folder]));
41+
42+
// Select top 3 via shift select
43+
store.dispatch(setCurrentCuration({
44+
shift: true,
45+
folder: curations[0].folder
46+
}));
47+
expect(store.getState().curate.current).toBe(curations[2].folder);
48+
expect(store.getState().curate.lastSelected).toBe(curations[2].folder);
49+
expect(store.getState().curate.selected).toHaveLength(3);
50+
expect(store.getState().curate.selected).toEqual(expect.arrayContaining([
51+
curations[0].folder,
52+
curations[1].folder,
53+
curations[2].folder
54+
]));
55+
56+
// Select bottom 3 via shift select
57+
store.dispatch(setCurrentCuration({
58+
shift: true,
59+
folder: curations[4].folder
60+
}));
61+
expect(store.getState().curate.current).toBe(curations[2].folder);
62+
expect(store.getState().curate.lastSelected).toBe(curations[2].folder);
63+
expect(store.getState().curate.selected).toHaveLength(3);
64+
expect(store.getState().curate.selected).toEqual(expect.arrayContaining([
65+
curations[2].folder,
66+
curations[3].folder,
67+
curations[4].folder
68+
]));
69+
70+
// Select first + bottom 3 via ctrl select
71+
store.dispatch(setCurrentCuration({
72+
ctrl: true,
73+
folder: curations[0].folder
74+
}));
75+
expect(store.getState().curate.current).toBe(curations[2].folder);
76+
expect(store.getState().curate.lastSelected).toBe(curations[0].folder);
77+
expect(store.getState().curate.selected).toHaveLength(4);
78+
expect(store.getState().curate.selected).toEqual(expect.arrayContaining([
79+
curations[0].folder,
80+
curations[2].folder,
81+
curations[3].folder,
82+
curations[4].folder
83+
]));
84+
});
85+
});

src/renderer/store/store.ts

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { configureStore } from '@reduxjs/toolkit';
1+
import { combineReducers, configureStore } from '@reduxjs/toolkit';
22
import { addCurationMiddleware } from './curate/middleware';
33
import curateReducer from './curate/slice';
44
import downloadsReducer from './downloads/slice';
55
import fpfssReducer from './fpfss/slice';
6+
import historyReducer from './history/slice';
67
import { listenerMiddleware } from './listenerMiddleware';
78
import logsReducer from './logs/slice';
89
import { addMainMiddleware } from './main/middleware';
@@ -13,41 +14,48 @@ import { addSearchMiddleware } from './search/middleware';
1314
import searchReducer from './search/slice';
1415
import tagCategoriesReducer from './tagCategories/slice';
1516
import tasksReducer from './tasks/slice';
16-
import historyReducer from './history/slice';
1717

1818
// Initialize all store middleware
1919
addSearchMiddleware();
2020
addCurationMiddleware();
2121
addMainMiddleware();
2222
addPreferencesMiddleware();
2323

24-
// Create store
25-
export const store = configureStore({
26-
reducer: {
27-
curate: curateReducer,
28-
fpfss: fpfssReducer,
29-
main: mainReducer,
30-
search: searchReducer,
31-
tagCategories: tagCategoriesReducer,
32-
tasks: tasksReducer,
33-
logs: logsReducer,
34-
downloads: downloadsReducer,
35-
preferences: prefsReducer,
36-
history: historyReducer,
37-
},
38-
devTools: true,
39-
middleware: (getDefaultMiddleware) => {
40-
const middleware = getDefaultMiddleware({
41-
serializableCheck: {
42-
ignoredPaths: ['search', 'downloads'] // Big performance drop in dev with this on search views
43-
},
44-
});
45-
middleware.push(listenerMiddleware.middleware);
46-
return middleware;
47-
}
24+
const rootReducer = combineReducers({
25+
curate: curateReducer,
26+
fpfss: fpfssReducer,
27+
main: mainReducer,
28+
search: searchReducer,
29+
tagCategories: tagCategoriesReducer,
30+
tasks: tasksReducer,
31+
logs: logsReducer,
32+
downloads: downloadsReducer,
33+
preferences: prefsReducer,
34+
history: historyReducer,
4835
});
4936

37+
export function setupStore(preloadedState?: Partial<RootState>) {
38+
return configureStore({
39+
reducer: rootReducer,
40+
devTools: true,
41+
middleware: (getDefaultMiddleware) => {
42+
const middleware = getDefaultMiddleware({
43+
serializableCheck: {
44+
ignoredPaths: ['search', 'downloads'] // Big performance drop in dev with this on search views
45+
},
46+
});
47+
middleware.push(listenerMiddleware.middleware);
48+
return middleware;
49+
},
50+
preloadedState
51+
});
52+
}
53+
54+
// Create store
55+
export const store = setupStore();
56+
5057
// Create typings for the store
51-
export type RootState = ReturnType<typeof store.getState>;
58+
export type RootState = ReturnType<typeof rootReducer>;
59+
export type AppStore = ReturnType<typeof setupStore>;
5260
export type AppDispatch = typeof store.dispatch;
5361
export default store;

src/test/mocks/backend-setup.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { MsgEvent, SocketServer } from '@back/SocketServer';
2+
import { BackIn, BackInTemplate } from '@shared/back/types';
3+
import { ws } from 'msw';
4+
5+
type Callback<T, U extends (...args: any[]) => any> = (event: T, ...args: Parameters<U>) => (ReturnType<U> | Promise<ReturnType<U>>);
6+
7+
export const mockServerUrl = 'ws://localhost:10000';
8+
export type MockHandlers = Partial<{
9+
[key in keyof BackInTemplate]: Callback<MsgEvent, BackInTemplate[key]>;
10+
}>;
11+
const mockServer = ws.link(mockServerUrl);
12+
13+
export const defaultHandlers: MockHandlers = {
14+
[BackIn.GET_START_TIME]: () => 0,
15+
};
16+
17+
// Helper to create server handlers with specific mocked functions
18+
export function createBackendHandler(
19+
customHandlers: MockHandlers = {}
20+
) {
21+
const mergedHandlers = { ...defaultHandlers, ...customHandlers };
22+
const server: SocketServer = new SocketServer();
23+
server.port = 10000;
24+
server.api.registered = mergedHandlers;
25+
26+
return mockServer.addEventListener('connection', ({ client }) => {
27+
server.onConnect(client as any);
28+
});
29+
}

0 commit comments

Comments
 (0)