diff --git a/packages/redux-devtools-rtk-query-monitor/.eslintignore b/packages/redux-devtools-rtk-query-monitor/.eslintignore index d3c23dcb..de820eed 100644 --- a/packages/redux-devtools-rtk-query-monitor/.eslintignore +++ b/packages/redux-devtools-rtk-query-monitor/.eslintignore @@ -1,2 +1,3 @@ demo lib +dist diff --git a/packages/redux-devtools-rtk-query-monitor/.eslintrc.js b/packages/redux-devtools-rtk-query-monitor/.eslintrc.js index 87abc2e1..e62f5871 100644 --- a/packages/redux-devtools-rtk-query-monitor/.eslintrc.js +++ b/packages/redux-devtools-rtk-query-monitor/.eslintrc.js @@ -9,5 +9,13 @@ module.exports = { project: ['./tsconfig.json'], }, }, + { + files: ['test/**/*.ts', 'test/**/*.tsx'], + extends: '../../eslintrc.ts.react.jest.base.json', + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.test.json'], + }, + }, ], }; diff --git a/packages/redux-devtools-rtk-query-monitor/jest.config.js b/packages/redux-devtools-rtk-query-monitor/jest.config.js new file mode 100644 index 00000000..bf9761dc --- /dev/null +++ b/packages/redux-devtools-rtk-query-monitor/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + moduleNameMapper: { + '\\.css$': '/test/__mocks__/styleMock.ts', + }, + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.test.json', + }, + }, +}; diff --git a/packages/redux-devtools-rtk-query-monitor/package.json b/packages/redux-devtools-rtk-query-monitor/package.json index 77910b5a..c6dca145 100644 --- a/packages/redux-devtools-rtk-query-monitor/package.json +++ b/packages/redux-devtools-rtk-query-monitor/package.json @@ -39,6 +39,7 @@ "clean": "rimraf lib", "lint": "eslint . --ext .ts,.tsx", "type-check": "tsc --noEmit", + "test": "jest", "prepack": "yarn run clean && yarn run build", "prepublish": "yarn run type-check && yarn run lint" }, @@ -67,18 +68,27 @@ "@babel/preset-typescript": "^7.16.7", "@redux-devtools/core": "^3.11.0", "@reduxjs/toolkit": "^1.8.1", + "@testing-library/jest-dom": "^5.16.3", + "@testing-library/react": "^12.1.4", "@types/hex-rgba": "^1.0.1", + "@types/jest": "^27.4.1", "@types/lodash.debounce": "^4.0.6", "@types/react": "^17.0.43", + "@types/react-redux": "^7.1.23", "@typescript-eslint/eslint-plugin": "^5.17.0", "@typescript-eslint/parser": "^5.17.0", "eslint": "^8.12.0", "eslint-config-prettier": "^8.5.0", + "eslint-plugin-jest": "^26.1.3", "eslint-plugin-react": "~7.28.0", "eslint-plugin-react-hooks": "^4.4.0", + "jest": "^27.5.1", "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-redux": "^7.2.8", "redux": "^4.1.2", "rimraf": "^3.0.2", + "ts-jest": "^27.1.4", "typescript": "~4.5.5" }, "peerDependencies": { diff --git a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewHeader.tsx b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewHeader.tsx index 427cd963..b4fcbde9 100644 --- a/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewHeader.tsx +++ b/packages/redux-devtools-rtk-query-monitor/src/components/QueryPreviewHeader.tsx @@ -1,7 +1,7 @@ import React, { ReactNode } from 'react'; import { StyleUtilsContext } from '../styles/createStylingFromTheme'; import { QueryPreviewTabs, TabOption } from '../types'; -import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y'; +import { renderTabPanelButtonId } from '../utils/a11y'; import { emptyArray } from '../utils/object'; export interface QueryPreviewHeaderProps { diff --git a/packages/redux-devtools-rtk-query-monitor/test/__mocks__/styleMock.ts b/packages/redux-devtools-rtk-query-monitor/test/__mocks__/styleMock.ts new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/packages/redux-devtools-rtk-query-monitor/test/__mocks__/styleMock.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/redux-devtools-rtk-query-monitor/test/devtools.mocks.tsx b/packages/redux-devtools-rtk-query-monitor/test/devtools.mocks.tsx new file mode 100644 index 00000000..906203a2 --- /dev/null +++ b/packages/redux-devtools-rtk-query-monitor/test/devtools.mocks.tsx @@ -0,0 +1,7 @@ +import * as React from 'react'; +import { createDevTools } from '@redux-devtools/core'; +import { RtkQueryMonitor } from '../src'; + +const MonitorAsAny = RtkQueryMonitor as any; + +export const ReduxDevTools = createDevTools(); diff --git a/packages/redux-devtools-rtk-query-monitor/test/integration.spec.tsx b/packages/redux-devtools-rtk-query-monitor/test/integration.spec.tsx new file mode 100644 index 00000000..4eaf9895 --- /dev/null +++ b/packages/redux-devtools-rtk-query-monitor/test/integration.spec.tsx @@ -0,0 +1,119 @@ +import * as React from 'react'; +import { Provider } from 'react-redux'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { ReduxDevTools } from './devtools.mocks'; +import { BaseQueryJestMockFunction, setupStore } from './rtk-query.mocks'; + +function Providers({ + store, + children, +}: { + store: ReturnType['store']; + children?: React.ComponentProps['children']; +}) { + const AnyProvider = Provider as any; + + return ( +
+ + {children} + + +
+ ); +} + +describe('rtk-query-monitor standalone integration', () => { + // Hushes symbol.observable warning + // @see https://github.com/reduxjs/redux-devtools/issues/1002 + jest.spyOn(console, 'warn'); + // eslint-disable-next-line @typescript-eslint/no-empty-function + (console.warn as jest.Mock).mockImplementation(() => {}); + + const dataPanelDomId = '#rtk-query-monitor-tab-panel-0'; + + const childrenTextContent = 'Renders children'; + const fetchBaseQueryMock: BaseQueryJestMockFunction> = + jest.fn((...fetchArgs) => + Promise.resolve({ + data: { + name: fetchArgs[0], + }, + }) + ); + const { store, pokemonApi } = setupStore(fetchBaseQueryMock, ReduxDevTools); + + beforeAll(() => { + // let's populate api + (store.dispatch as any)( + pokemonApi.endpoints.getPokemonByName.initiate('bulbasaur') + ); + }); + + beforeEach(() => { + fetchBaseQueryMock.mockClear(); + }); + + afterAll(() => { + (console.warn as jest.Mock).mockRestore(); + }); + + it('renders on a standalone app without crashing', () => { + const { container } = render( + +
{childrenTextContent}
+
+ ); + + expect(screen.getByTestId('children').textContent).toBe( + childrenTextContent + ); + + expect( + screen + .getByRole('tab', { name: /actions/i }) + ?.textContent?.toLowerCase() + .trim() + ).toBe('actions'); + expect( + screen + .getByRole('tab', { name: /data/i }) + ?.textContent?.toLowerCase() + .trim() + ).toBe('data'); + expect( + screen + .getByRole('tab', { name: /api/i }) + ?.textContent?.toLowerCase() + .trim() + ).toBe('api'); + expect( + container.querySelector( + 'form[id="rtk-query-monitor-query-selection-form"]' + ) + ).toBeDefined(); + }); + + it('displays query data tab content', async () => { + // `Promise.resolve()` hushes `@typescript-eslint/await-thenable` + await Promise.resolve(pokemonApi.util.getRunningOperationPromises()); + + const { container } = render( + +
{childrenTextContent}
+
+ ); + + // We need to select the query & the correct tab + fireEvent.click(screen.getByRole('tab', { name: /data/i })); + fireEvent.click(screen.getByText(/bulbasaur/i)); + + await waitFor(() => + expect(container.querySelector(dataPanelDomId)).not.toBeNull() + ); + + expect(container.querySelector(dataPanelDomId)?.textContent).toMatch( + /name\W+pokemon\/bulbasaur/i + ); + }); +}); diff --git a/packages/redux-devtools-rtk-query-monitor/test/rtk-query.mocks.ts b/packages/redux-devtools-rtk-query-monitor/test/rtk-query.mocks.ts new file mode 100644 index 00000000..d1d8c167 --- /dev/null +++ b/packages/redux-devtools-rtk-query-monitor/test/rtk-query.mocks.ts @@ -0,0 +1,84 @@ +import { + combineReducers, + configureStore, + EnhancedStore, + Middleware, +} from '@reduxjs/toolkit'; +import { createApi } from '@reduxjs/toolkit/query/react'; +import type { BaseQueryFn, FetchArgs } from '@reduxjs/toolkit/query'; +import type { ReduxDevTools } from './devtools.mocks'; + +export type MockBaseQuery< + Result, + Args = string | FetchArgs, + Meta = { status?: number } +> = BaseQueryFn; + +export type BaseQueryJestMockFunction = jest.Mock< + ReturnType>, + Parameters> +>; + +export function createMockBaseQuery( + jestMockFn: BaseQueryJestMockFunction +): MockBaseQuery { + return async function mockBaseQuery(param, api, extra) { + try { + const output = await jestMockFn(param, api, extra); + + return output; + } catch (error) { + return { + error, + }; + } + }; +} + +export function createPokemonApi( + jestMockFn: BaseQueryJestMockFunction> +) { + return createApi({ + reducerPath: 'pokemonApi', + keepUnusedDataFor: 9999, + baseQuery: createMockBaseQuery(jestMockFn), + tagTypes: ['pokemon'], + endpoints: (builder) => ({ + getPokemonByName: builder.query, string>({ + query: (name: string) => `pokemon/${name}`, + providesTags: (result, error, name: string) => [ + { type: 'pokemon' }, + { type: 'pokemon', id: name }, + ], + }), + }), + }); +} + +export function setupStore( + jestMockFn: BaseQueryJestMockFunction>, + devTools: typeof ReduxDevTools +) { + const pokemonApi = createPokemonApi(jestMockFn); + + const reducer = combineReducers({ + [pokemonApi.reducerPath]: pokemonApi.reducer, + }); + + const store: EnhancedStore> = configureStore({ + reducer, + devTools: false, + // adding the api middleware enables caching, invalidation, polling and other features of `rtk-query` + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat([pokemonApi.middleware]) as Middleware[], + enhancers: [devTools.instrument()], + }); + + return { + jestMockFn, + devTools, + store, + reducer, + pokemonApi, + }; +} diff --git a/packages/redux-devtools-rtk-query-monitor/tsconfig.test.json b/packages/redux-devtools-rtk-query-monitor/tsconfig.test.json new file mode 100644 index 00000000..9d56eb9d --- /dev/null +++ b/packages/redux-devtools-rtk-query-monitor/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.react.base.json", + "compilerOptions": { + "types": ["jest"] + }, + "include": ["src", "test"], + "exclude": ["dist"] +} diff --git a/yarn.lock b/yarn.lock index a7956803..c46b0599 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5403,29 +5403,38 @@ __metadata: "@redux-devtools/core": ^3.11.0 "@redux-devtools/ui": ^1.2.1 "@reduxjs/toolkit": ^1.8.1 + "@testing-library/jest-dom": ^5.16.3 + "@testing-library/react": ^12.1.4 "@types/hex-rgba": ^1.0.1 + "@types/jest": ^27.4.1 "@types/lodash.debounce": ^4.0.6 "@types/prop-types": ^15.7.4 "@types/react": ^17.0.43 + "@types/react-redux": ^7.1.23 "@types/redux-devtools-themes": ^1.0.0 "@typescript-eslint/eslint-plugin": ^5.17.0 "@typescript-eslint/parser": ^5.17.0 eslint: ^8.12.0 eslint-config-prettier: ^8.5.0 + eslint-plugin-jest: ^26.1.3 eslint-plugin-react: ~7.28.0 eslint-plugin-react-hooks: ^4.4.0 hex-rgba: ^1.0.2 immutable: ^4.0.0 + jest: ^27.5.1 jss: ^10.9.0 jss-preset-default: ^10.9.0 lodash.debounce: ^4.0.8 prop-types: ^15.8.1 react: ^17.0.2 react-base16-styling: ^0.9.1 + react-dom: ^17.0.2 react-json-tree: ^0.16.1 + react-redux: ^7.2.8 redux: ^4.1.2 redux-devtools-themes: ^1.0.0 rimraf: ^3.0.2 + ts-jest: ^27.1.4 typescript: ~4.5.5 peerDependencies: "@redux-devtools/core": ^3.7.0