test: fix tests and bump various dependencies

This commit is contained in:
Nicolas Meienberger 2023-02-02 08:46:29 +01:00 committed by Nicolas Meienberger
parent 29c7f98a69
commit 79f1da00d0
17 changed files with 300 additions and 315 deletions

View file

@ -1,5 +1,5 @@
module.exports = {
plugins: ['@typescript-eslint', 'import', 'react', 'jest'],
plugins: ['@typescript-eslint', 'import', 'react', 'jest', 'jsdoc'],
extends: [
'plugin:@typescript-eslint/recommended',
'next/core-web-vitals',
@ -10,6 +10,7 @@ module.exports = {
'plugin:import/typescript',
'prettier',
'plugin:react/recommended',
'plugin:jsdoc/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
@ -28,7 +29,7 @@ module.exports = {
'react/jsx-props-no-spreading': 0,
'react/no-unused-prop-types': 0,
'react/button-has-type': 0,
'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/mocks/**', 'tests/**'] }],
'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/*.factory.{ts,tsx}', '**/mocks/**', 'tests/**'] }],
'no-underscore-dangle': 0,
},
globals: {

View file

@ -1,120 +1,107 @@
import path from 'path';
const fs: {
__createMockFiles: typeof createMockFiles;
__resetAllMocks: typeof resetAllMocks;
readFileSync: typeof readFileSync;
existsSync: typeof existsSync;
writeFileSync: typeof writeFileSync;
mkdirSync: typeof mkdirSync;
rmSync: typeof rmSync;
readdirSync: typeof readdirSync;
copyFileSync: typeof copyFileSync;
copySync: typeof copyFileSync;
createFileSync: typeof createFileSync;
unlinkSync: typeof unlinkSync;
} = jest.genMockFromModule('fs-extra');
class FsMock {
private static instance: FsMock;
let mockFiles = Object.create(null);
private mockFiles = Object.create(null);
const createMockFiles = (newMockFiles: Record<string, string>) => {
mockFiles = Object.create(null);
// private constructor() {}
// Create folder tree
Object.keys(newMockFiles).forEach((file) => {
const dir = path.dirname(file);
if (!mockFiles[dir]) {
mockFiles[dir] = [];
static getInstance(): FsMock {
if (!FsMock.instance) {
FsMock.instance = new FsMock();
}
mockFiles[dir].push(path.basename(file));
mockFiles[file] = newMockFiles[file];
});
};
const readFileSync = (p: string) => mockFiles[p];
const existsSync = (p: string) => mockFiles[p] !== undefined;
const writeFileSync = (p: string, data: string | string[]) => {
mockFiles[p] = data;
};
const mkdirSync = (p: string) => {
mockFiles[p] = Object.create(null);
};
const rmSync = (p: string) => {
if (mockFiles[p] instanceof Array) {
mockFiles[p].forEach((file: string) => {
delete mockFiles[path.join(p, file)];
});
return FsMock.instance;
}
delete mockFiles[p];
};
__createMockFiles = (newMockFiles: Record<string, string>) => {
this.mockFiles = Object.create(null);
const readdirSync = (p: string) => {
const files: string[] = [];
// Create folder tree
Object.keys(newMockFiles).forEach((file) => {
const dir = path.dirname(file);
const depth = p.split('/').length;
Object.keys(mockFiles).forEach((file) => {
if (file.startsWith(p)) {
const fileDepth = file.split('/').length;
if (fileDepth === depth + 1) {
files.push(file.split('/').pop() || '');
if (!this.mockFiles[dir]) {
this.mockFiles[dir] = [];
}
this.mockFiles[dir].push(path.basename(file));
this.mockFiles[file] = newMockFiles[file];
});
};
__resetAllMocks = () => {
this.mockFiles = Object.create(null);
};
readFileSync = (p: string) => this.mockFiles[p];
existsSync = (p: string) => this.mockFiles[p] !== undefined;
writeFileSync = (p: string, data: string | string[]) => {
this.mockFiles[p] = data;
};
mkdirSync = (p: string) => {
this.mockFiles[p] = Object.create(null);
};
rmSync = (p: string) => {
if (this.mockFiles[p] instanceof Array) {
this.mockFiles[p].forEach((file: string) => {
delete this.mockFiles[path.join(p, file)];
});
}
});
return files;
};
delete this.mockFiles[p];
};
const copyFileSync = (source: string, destination: string) => {
mockFiles[destination] = mockFiles[source];
};
readdirSync = (p: string) => {
const files: string[] = [];
const copySync = (source: string, destination: string) => {
mockFiles[destination] = mockFiles[source];
const depth = p.split('/').length;
if (mockFiles[source] instanceof Array) {
mockFiles[source].forEach((file: string) => {
mockFiles[`${destination}/${file}`] = mockFiles[`${source}/${file}`];
Object.keys(this.mockFiles).forEach((file) => {
if (file.startsWith(p)) {
const fileDepth = file.split('/').length;
if (fileDepth === depth + 1) {
files.push(file.split('/').pop() || '');
}
}
});
}
};
const createFileSync = (p: string) => {
mockFiles[p] = '';
};
return files;
};
const resetAllMocks = () => {
mockFiles = Object.create(null);
};
copyFileSync = (source: string, destination: string) => {
this.mockFiles[destination] = this.mockFiles[source];
};
const unlinkSync = (p: string) => {
if (mockFiles[p] instanceof Array) {
mockFiles[p].forEach((file: string) => {
delete mockFiles[path.join(p, file)];
});
}
delete mockFiles[p];
};
copySync = (source: string, destination: string) => {
this.mockFiles[destination] = this.mockFiles[source];
fs.unlinkSync = unlinkSync;
fs.readdirSync = readdirSync;
fs.existsSync = existsSync;
fs.readFileSync = readFileSync;
fs.writeFileSync = writeFileSync;
fs.mkdirSync = mkdirSync;
fs.rmSync = rmSync;
fs.copyFileSync = copyFileSync;
fs.copySync = copySync;
fs.createFileSync = createFileSync;
fs.__createMockFiles = createMockFiles;
fs.__resetAllMocks = resetAllMocks;
if (this.mockFiles[source] instanceof Array) {
this.mockFiles[source].forEach((file: string) => {
this.mockFiles[`${destination}/${file}`] = this.mockFiles[`${source}/${file}`];
});
}
};
export default fs;
createFileSync = (p: string) => {
this.mockFiles[p] = '';
};
unlinkSync = (p: string) => {
if (this.mockFiles[p] instanceof Array) {
this.mockFiles[p].forEach((file: string) => {
delete this.mockFiles[path.join(p, file)];
});
}
delete this.mockFiles[p];
};
getMockFiles = () => this.mockFiles;
}
export default FsMock.getInstance();

View file

@ -22,11 +22,11 @@
"@runtipi/postgres-migrations": "^5.3.0",
"@tabler/core": "1.0.0-beta16",
"@tabler/icons": "^1.109.0",
"@tanstack/react-query": "^4.20.4",
"@tanstack/react-query": "^4.24.4",
"@trpc/client": "^10.7.0",
"@trpc/next": "^10.7.0",
"@trpc/react-query": "^10.7.0",
"@trpc/server": "^10.7.0",
"@trpc/next": "^10.9.1",
"@trpc/react-query": "^10.9.1",
"@trpc/server": "^10.9.1",
"argon2": "^0.29.1",
"clsx": "^1.1.1",
"fs-extra": "^10.1.0",
@ -90,12 +90,13 @@
"eslint-config-next": "13.1.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-jest": "^27.1.7",
"eslint-plugin-jsdoc": "^39.6.9",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react-hooks": "^4.6.0",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"msw": "^0.49.2",
"msw": "^1.0.0",
"next-router-mock": "^0.8.0",
"nodemon": "^2.0.15",
"prisma": "^4.8.0",

View file

@ -6,10 +6,6 @@ import { server } from '../../../../mocks/server';
import { useToastStore } from '../../../../state/toastStore';
import { SettingsContainer } from './SettingsContainer';
beforeEach(() => {
localStorage.removeItem('token');
});
describe('Test: SettingsContainer', () => {
describe('UI', () => {
it('renders without crashing', () => {
@ -58,8 +54,7 @@ describe('Test: SettingsContainer', () => {
it('should remove token from local storage on success', async () => {
const current = '0.0.1';
const latest = faker.system.semver();
localStorage.setItem('token', 'token');
const removeItem = jest.spyOn(localStorage, 'removeItem');
render(<SettingsContainer data={{ current, latest }} />);
const updateButton = screen.getByText('Update');
@ -67,11 +62,9 @@ describe('Test: SettingsContainer', () => {
fireEvent.click(updateButton);
});
// wait 500 ms because localStore cannot be awaited in tests
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 500));
expect(localStorage.getItem('token')).toBeNull();
await waitFor(() => {
expect(removeItem).toBeCalledWith('token');
});
});
it('should display error toast on error', async () => {
@ -98,7 +91,7 @@ describe('Test: SettingsContainer', () => {
describe('Restart', () => {
it('should remove token from local storage on success', async () => {
const current = faker.system.semver();
localStorage.setItem('token', 'token');
const removeItem = jest.spyOn(localStorage, 'removeItem');
render(<SettingsContainer data={{ current }} />);
const restartButton = screen.getByTestId('settings-modal-restart-button');
@ -106,11 +99,9 @@ describe('Test: SettingsContainer', () => {
fireEvent.click(restartButton);
});
// wait 500 ms because localStore cannot be awaited in tests
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 500));
expect(localStorage.getItem('token')).toBeNull();
await waitFor(() => {
expect(removeItem).toBeCalledWith('token');
});
});
it('should display error toast on error', async () => {

View file

@ -17,7 +17,7 @@ function MyApp({ Component, pageProps }: AppProps) {
const { setDarkMode } = useUIStore();
const { setStatus, setVersion } = useSystemStore();
// trpc.system.status.useQuery(undefined, { refetchInterval: 1000, networkMode: 'online', onSuccess: (d) => setStatus(d.status || SystemStatus.RUNNING) });
trpc.system.status.useQuery(undefined, { refetchInterval: 1000, networkMode: 'online', onSuccess: (d) => setStatus(d.status || SystemStatus.RUNNING) });
const version = trpc.system.getVersion.useQuery(undefined, { networkMode: 'online' });
useEffect(() => {

View file

@ -0,0 +1 @@
export const notEmpty = <TValue>(value: TValue | null | undefined): value is TValue => value !== null && value !== undefined;

View file

@ -2,7 +2,7 @@
* @jest-environment node
*/
import fs from 'fs-extra';
import { EventDispatcher, EventTypes } from '.';
import { EventDispatcher } from '.';
const WATCH_FILE = '/runtipi/state/events';
@ -19,18 +19,18 @@ beforeEach(() => {
describe('EventDispatcher - dispatchEvent', () => {
it('should dispatch an event', () => {
const event = EventDispatcher.dispatchEvent(EventTypes.APP);
const event = EventDispatcher.dispatchEvent('app');
expect(event.id).toBeDefined();
});
it('should dispatch an event with args', () => {
const event = EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
const event = EventDispatcher.dispatchEvent('app', ['--help']);
expect(event.id).toBeDefined();
});
it('Should put events into queue', async () => {
EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
EventDispatcher.dispatchEvent('app', ['--help']);
EventDispatcher.dispatchEvent('app', ['--help']);
// @ts-expect-error - private method
const { queue } = EventDispatcher;
@ -39,8 +39,8 @@ describe('EventDispatcher - dispatchEvent', () => {
});
it('Should put first event into lock after 1 sec', async () => {
EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
EventDispatcher.dispatchEvent(EventTypes.UPDATE, ['--help']);
EventDispatcher.dispatchEvent('app', ['--help']);
EventDispatcher.dispatchEvent('update', ['--help']);
// @ts-expect-error - private method
const { queue } = EventDispatcher;
@ -52,13 +52,13 @@ describe('EventDispatcher - dispatchEvent', () => {
expect(queue.length).toBe(2);
expect(lock).toBeDefined();
expect(lock?.type).toBe(EventTypes.APP);
expect(lock?.type).toBe('app');
});
it('Should clear event once its status is success', async () => {
// @ts-expect-error - private method
jest.spyOn(EventDispatcher, 'getEventStatus').mockReturnValueOnce('success');
EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
EventDispatcher.dispatchEvent('app', ['--help']);
await wait(1050);
@ -71,7 +71,7 @@ describe('EventDispatcher - dispatchEvent', () => {
it('Should clear event once its status is error', async () => {
// @ts-expect-error - private method
jest.spyOn(EventDispatcher, 'getEventStatus').mockReturnValueOnce('error');
EventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
EventDispatcher.dispatchEvent('app', ['--help']);
await wait(1050);
@ -86,7 +86,7 @@ describe('EventDispatcher - dispatchEventAsync', () => {
it('Should dispatch an event and wait for it to finish', async () => {
// @ts-expect-error - private method
jest.spyOn(EventDispatcher, 'getEventStatus').mockReturnValueOnce('success');
const { success } = await EventDispatcher.dispatchEventAsync(EventTypes.APP, ['--help']);
const { success } = await EventDispatcher.dispatchEventAsync('app', ['--help']);
expect(success).toBe(true);
});
@ -95,7 +95,7 @@ describe('EventDispatcher - dispatchEventAsync', () => {
// @ts-expect-error - private method
jest.spyOn(EventDispatcher, 'getEventStatus').mockReturnValueOnce('error');
const { success } = await EventDispatcher.dispatchEventAsync(EventTypes.APP, ['--help']);
const { success } = await EventDispatcher.dispatchEventAsync('app', ['--help']);
expect(success).toBe(false);
});
@ -104,7 +104,7 @@ describe('EventDispatcher - dispatchEventAsync', () => {
describe('EventDispatcher - runEvent', () => {
it('Should do nothing if there is a lock', async () => {
// @ts-expect-error - private method
EventDispatcher.lock = { id: '123', type: EventTypes.APP, args: [] };
EventDispatcher.lock = { id: '123', type: 'app', args: [] };
// @ts-expect-error - private method
await EventDispatcher.runEvent();
@ -136,7 +136,7 @@ describe('EventDispatcher - getEventStatus', () => {
it('Should return error if event is expired', async () => {
const dateFiveMinutesAgo = new Date(new Date().getTime() - 5 * 60 * 10000);
// @ts-expect-error - private method
EventDispatcher.queue = [{ id: '123', type: EventTypes.APP, args: [], creationDate: dateFiveMinutesAgo }];
EventDispatcher.queue = [{ id: '123', type: 'app', args: [], creationDate: dateFiveMinutesAgo }];
// @ts-expect-error - private method
const status = EventDispatcher.getEventStatus('123');
@ -145,7 +145,7 @@ describe('EventDispatcher - getEventStatus', () => {
it('Should be waiting if line is not found in the file', async () => {
// @ts-expect-error - private method
EventDispatcher.queue = [{ id: '123', type: EventTypes.APP, args: [], creationDate: new Date() }];
EventDispatcher.queue = [{ id: '123', type: 'app', args: [], creationDate: new Date() }];
// @ts-expect-error - private method
const status = EventDispatcher.getEventStatus('123');
@ -155,7 +155,7 @@ describe('EventDispatcher - getEventStatus', () => {
describe('EventDispatcher - clearEvent', () => {
it('Should clear event', async () => {
const event = { id: '123', type: EventTypes.APP, args: [], creationDate: new Date() };
const event = { id: '123', type: 'app', args: [], creationDate: new Date() };
// @ts-expect-error - private method
EventDispatcher.queue = [event];
// @ts-expect-error - private method

View file

@ -1,19 +1,28 @@
/* eslint-disable vars-on-top */
import fs from 'fs-extra';
import { Logger } from '../Logger';
import { getConfig } from '../TipiConfig';
export enum EventTypes {
// System events
RESTART = 'restart',
UPDATE = 'update',
CLONE_REPO = 'clone_repo',
UPDATE_REPO = 'update_repo',
APP = 'app',
SYSTEM_INFO = 'system_info',
declare global {
// eslint-disable-next-line no-var
var EventDispatcher: EventDispatcher | undefined;
}
export const EVENT_TYPES = {
// System events
RESTART: 'restart',
UPDATE: 'update',
CLONE_REPO: 'clone_repo',
UPDATE_REPO: 'update_repo',
APP: 'app',
SYSTEM_INFO: 'system_info',
} as const;
export type EventType = typeof EVENT_TYPES[keyof typeof EVENT_TYPES];
type SystemEvent = {
id: string;
type: EventTypes;
type: EventType;
args: string[];
creationDate: Date;
};
@ -27,6 +36,8 @@ const WATCH_FILE = '/runtipi/state/events';
class EventDispatcher {
private static instance: EventDispatcher | null;
private dispatcherId = EventDispatcher.generateId();
private queue: SystemEvent[] = [];
private lock: SystemEvent | null = null;
@ -77,7 +88,7 @@ class EventDispatcher {
* Poll queue and run events
*/
private pollQueue() {
Logger.info('EventDispatcher: Polling queue...');
Logger.info(`EventDispatcher(${this.dispatcherId}): Polling queue...`);
if (!this.interval) {
const id = setInterval(() => {
@ -148,7 +159,7 @@ class EventDispatcher {
* @param args - Event arguments
* @returns - Event object
*/
public dispatchEvent(type: EventTypes, args?: string[]): SystemEvent {
public dispatchEvent(type: EventType, args?: string[]): SystemEvent {
const event: SystemEvent = {
id: EventDispatcher.generateId(),
type,
@ -185,7 +196,7 @@ class EventDispatcher {
* @param args - Event arguments
* @returns - Promise that resolves when the event is done
*/
public async dispatchEventAsync(type: EventTypes, args?: string[]): Promise<{ success: boolean; stdout?: string }> {
public async dispatchEventAsync(type: EventType, args?: string[]): Promise<{ success: boolean; stdout?: string }> {
const event = this.dispatchEvent(type, args);
return new Promise((resolve) => {
@ -222,4 +233,8 @@ class EventDispatcher {
}
}
export const EventDispatcherInstance = EventDispatcher.getInstance();
export const EventDispatcherInstance = global.EventDispatcher || EventDispatcher.getInstance();
if (getConfig().NODE_ENV !== 'production') {
global.EventDispatcher = EventDispatcherInstance;
}

View file

@ -1,2 +1,3 @@
export { EventDispatcherInstance as EventDispatcher } from './EventDispatcher';
export { EventTypes } from './EventDispatcher';
export type { EventType } from './EventDispatcher';
export { EVENT_TYPES } from './EventDispatcher';

View file

@ -3,13 +3,12 @@ import fs from 'fs-extra';
import { getConfig, setConfig, TipiConfig } from '.';
import { readJsonFile } from '../../common/fs.helpers';
jest.mock('fs-extra');
beforeEach(async () => {
jest.resetModules();
jest.resetAllMocks();
// @ts-expect-error - We are mocking fs
fs.__resetAllMocks();
jest.mock('fs-extra');
});
describe('Test: getConfig', () => {

View file

@ -4,11 +4,12 @@ import nextConfig from 'next/config';
import { readJsonFile } from '../../common/fs.helpers';
import { Logger } from '../Logger';
enum AppSupportedArchitecturesEnum {
ARM = 'arm',
ARM64 = 'arm64',
AMD64 = 'amd64',
}
export const ARCHITECTURES = {
ARM: 'arm',
ARM64: 'arm64',
AMD64: 'amd64',
} as const;
export type Architecture = typeof ARCHITECTURES[keyof typeof ARCHITECTURES];
const {
NODE_ENV,
@ -27,13 +28,13 @@ const {
POSTGRES_PASSWORD,
POSTGRES_PORT = 5432,
} = nextConfig()?.serverRuntimeConfig || process.env;
// Use process.env if nextConfig is not available (e.g. in in server-preload.ts)
// Use process.env if nextConfig is not available
const configSchema = z.object({
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
REDIS_HOST: z.string(),
status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
architecture: z.nativeEnum(AppSupportedArchitecturesEnum),
architecture: z.nativeEnum(ARCHITECTURES),
dnsIp: z.string(),
rootFolder: z.string(),
internalIp: z.string(),

View file

@ -6,11 +6,11 @@ import { EventDispatcher } from '../../core/EventDispatcher';
import { setConfig } from '../../core/TipiConfig';
import TipiCache from '../../core/TipiCache';
jest.mock('fs-extra');
jest.mock('axios');
jest.mock('redis');
beforeEach(async () => {
jest.mock('fs-extra');
jest.resetModules();
jest.resetAllMocks();
});

View file

@ -1,7 +1,7 @@
import semver from 'semver';
import { z } from 'zod';
import { readJsonFile } from '../../common/fs.helpers';
import { EventDispatcher, EventTypes } from '../../core/EventDispatcher';
import { EventDispatcher } from '../../core/EventDispatcher';
import { Logger } from '../../core/Logger';
import TipiCache from '../../core/TipiCache';
import { getConfig, setConfig } from '../../core/TipiConfig';
@ -66,7 +66,7 @@ const restart = async (): Promise<boolean> => {
}
setConfig('status', 'RESTARTING');
EventDispatcher.dispatchEventAsync(EventTypes.RESTART);
EventDispatcher.dispatchEventAsync('restart');
return true;
};
@ -91,12 +91,12 @@ const update = async (): Promise<boolean> => {
}
if (semver.major(current) !== semver.major(latest)) {
throw new Error('The major version has changed. Please update manually');
throw new Error('The major version has changed. Please update manually (instructions on GitHub)');
}
setConfig('status', 'UPDATING');
EventDispatcher.dispatchEventAsync(EventTypes.UPDATE);
EventDispatcher.dispatchEventAsync('update');
return true;
};

View file

@ -1,8 +1,8 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createTRPCReact, httpLink, loggerLink } from '@trpc/react-query';
import SuperJSON from 'superjson';
import React from 'react';
import React, { useState } from 'react';
import fetch from 'isomorphic-fetch';
import superjson from 'superjson';
import type { AppRouter } from '../src/server/routers/_app';
@ -17,28 +17,36 @@ export const trpc = createTRPCReact<AppRouter>({
},
});
const queryClient = new QueryClient();
const trpcClient = trpc.createClient({
links: [
loggerLink({
enabled: () => false,
}),
httpLink({
url: 'http://localhost:3000/api/trpc',
headers() {
return {};
},
fetch: async (input, init?) =>
fetch(input, {
...init,
}),
}),
],
transformer: SuperJSON,
});
export function TRPCTestClientProvider(props: { children: React.ReactNode }) {
const { children } = props;
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
}),
);
const [trpcClient] = useState(() =>
trpc.createClient({
transformer: superjson,
links: [
loggerLink({
enabled: () => false,
}),
httpLink({
url: 'http://localhost:3000/api/trpc',
fetch: async (input, init?) =>
fetch(input, {
...init,
}),
}),
],
}),
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>

View file

@ -1,8 +1,6 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import 'whatwg-fetch';
import { server } from '../../src/client/mocks/server';
import { mockApolloClient } from '../test-utils';
import { useToastStore } from '../../src/client/state/toastStore';
// Mock next/router
@ -18,6 +16,27 @@ jest.mock('remark-mdx', () => () => ({}));
console.error = jest.fn();
// Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {};
return {
getItem(key: string) {
return store[key] || null;
},
setItem(key: string, value: string) {
store[key] = value.toString();
},
removeItem(key: string) {
delete store[key];
},
clear() {
store = {};
},
};
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
beforeAll(() => {
// Enable the mocking in tests.
server.listen();
@ -25,10 +44,6 @@ beforeAll(() => {
beforeEach(async () => {
useToastStore.getState().clearToasts();
// Ensure Apollo cache is cleared between tests.
// https://www.apollographql.com/docs/react/api/core/ApolloClient/#ApolloClient.clearStore
await mockApolloClient.clearStore();
await mockApolloClient.cache.reset();
});
afterEach(() => {

View file

@ -1,27 +1,8 @@
import React, { FC, ReactElement } from 'react';
import { render, RenderOptions, renderHook } from '@testing-library/react';
import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache } from '@apollo/client';
import fetch from 'isomorphic-fetch';
import { TRPCTestClientProvider } from './TRPCTestClientProvider';
const link = new HttpLink({
uri: 'http://localhost:3000/graphql',
// Use explicit `window.fetch` so tha outgoing requests
// are captured and deferred until the Service Worker is ready.
fetch: (...args) => fetch(...args),
});
// create a mock of Apollo Client
export const mockApolloClient = new ApolloClient({
cache: new InMemoryCache({}),
link,
});
const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => (
<TRPCTestClientProvider>
<ApolloProvider client={mockApolloClient}>{children}</ApolloProvider>
</TRPCTestClientProvider>
);
const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => <TRPCTestClientProvider>{children}</TRPCTestClientProvider>;
const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) => render(ui, { wrapper: AllTheProviders, ...options });
const customRenderHook = (callback: () => any, options?: Omit<RenderOptions, 'wrapper'>) => renderHook(callback, { wrapper: AllTheProviders, ...options });

View file

@ -1,121 +1,105 @@
import path from 'path';
const fs: {
__createMockFiles: typeof createMockFiles;
__resetAllMocks: typeof resetAllMocks;
readFileSync: typeof readFileSync;
existsSync: typeof existsSync;
writeFileSync: typeof writeFileSync;
mkdirSync: typeof mkdirSync;
rmSync: typeof rmSync;
readdirSync: typeof readdirSync;
copyFileSync: typeof copyFileSync;
copySync: typeof copyFileSync;
createFileSync: typeof createFileSync;
unlinkSync: typeof unlinkSync;
} = jest.genMockFromModule('fs-extra');
class FsMock {
private static instance: FsMock;
let mockFiles = Object.create(null);
private mockFiles = Object.create(null);
const createMockFiles = (newMockFiles: Record<string, string>) => {
mockFiles = Object.create(null);
// Create folder tree
Object.keys(newMockFiles).forEach((file) => {
const dir = path.dirname(file);
if (!mockFiles[dir]) {
mockFiles[dir] = [];
static getInstance(): FsMock {
if (!FsMock.instance) {
FsMock.instance = new FsMock();
}
mockFiles[dir].push(path.basename(file));
mockFiles[file] = newMockFiles[file];
});
};
const readFileSync = (p: string) => mockFiles[p];
const existsSync = (p: string) => mockFiles[p] !== undefined;
const writeFileSync = (p: string, data: string | string[]) => {
mockFiles[p] = data;
};
const mkdirSync = (p: string) => {
mockFiles[p] = Object.create(null);
};
const rmSync = (p: string) => {
if (mockFiles[p] instanceof Array) {
mockFiles[p].forEach((file: string) => {
delete mockFiles[path.join(p, file)];
});
return FsMock.instance;
}
delete mockFiles[p];
};
__createMockFiles = (newMockFiles: Record<string, string>) => {
this.mockFiles = Object.create(null);
const readdirSync = (p: string) => {
const files: string[] = [];
// Create folder tree
Object.keys(newMockFiles).forEach((file) => {
const dir = path.dirname(file);
const depth = p.split('/').length;
Object.keys(mockFiles).forEach((file) => {
if (file.startsWith(p)) {
const fileDepth = file.split('/').length;
if (fileDepth === depth + 1) {
files.push(file.split('/').pop() || '');
if (!this.mockFiles[dir]) {
this.mockFiles[dir] = [];
}
this.mockFiles[dir].push(path.basename(file));
this.mockFiles[file] = newMockFiles[file];
});
};
__resetAllMocks = () => {
this.mockFiles = Object.create(null);
};
readFileSync = (p: string) => this.mockFiles[p];
existsSync = (p: string) => this.mockFiles[p] !== undefined;
writeFileSync = (p: string, data: string | string[]) => {
this.mockFiles[p] = data;
};
mkdirSync = (p: string) => {
this.mockFiles[p] = Object.create(null);
};
rmSync = (p: string) => {
if (this.mockFiles[p] instanceof Array) {
this.mockFiles[p].forEach((file: string) => {
delete this.mockFiles[path.join(p, file)];
});
}
});
return files;
};
delete this.mockFiles[p];
};
const copyFileSync = (source: string, destination: string) => {
mockFiles[destination] = mockFiles[source];
};
readdirSync = (p: string) => {
const files: string[] = [];
const copySync = (source: string, destination: string) => {
mockFiles[destination] = mockFiles[source];
const depth = p.split('/').length;
if (mockFiles[source] instanceof Array) {
mockFiles[source].forEach((file: string) => {
mockFiles[`${destination}/${file}`] = mockFiles[`${source}/${file}`];
Object.keys(this.mockFiles).forEach((file) => {
if (file.startsWith(p)) {
const fileDepth = file.split('/').length;
if (fileDepth === depth + 1) {
files.push(file.split('/').pop() || '');
}
}
});
}
};
const createFileSync = (p: string) => {
mockFiles[p] = '';
};
return files;
};
const resetAllMocks = () => {
mockFiles = Object.create(null);
};
copyFileSync = (source: string, destination: string) => {
this.mockFiles[destination] = this.mockFiles[source];
};
const unlinkSync = (p: string) => {
if (mockFiles[p] instanceof Array) {
mockFiles[p].forEach((file: string) => {
delete mockFiles[path.join(p, file)];
});
}
delete mockFiles[p];
};
copySync = (source: string, destination: string) => {
this.mockFiles[destination] = this.mockFiles[source];
fs.unlinkSync = unlinkSync;
fs.readdirSync = readdirSync;
fs.existsSync = existsSync;
fs.readFileSync = readFileSync;
fs.writeFileSync = writeFileSync;
fs.mkdirSync = mkdirSync;
fs.rmSync = rmSync;
fs.copyFileSync = copyFileSync;
fs.copySync = copySync;
fs.createFileSync = createFileSync;
fs.__createMockFiles = createMockFiles;
fs.__resetAllMocks = resetAllMocks;
if (this.mockFiles[source] instanceof Array) {
this.mockFiles[source].forEach((file: string) => {
this.mockFiles[`${destination}/${file}`] = this.mockFiles[`${source}/${file}`];
});
}
};
export default fs;
// module.exports = fs;
createFileSync = (p: string) => {
this.mockFiles[p] = '';
};
unlinkSync = (p: string) => {
if (this.mockFiles[p] instanceof Array) {
this.mockFiles[p].forEach((file: string) => {
delete this.mockFiles[path.join(p, file)];
});
}
delete this.mockFiles[p];
};
getMockFiles = () => this.mockFiles;
}
export default FsMock.getInstance();