Feat #1126 Add Data Tab to rtk-query-monitor (#1129)

* fix: broken rtk-query-monitor demo not working #1126

Description:

downgrades framer-motion in order to remove the runtime error "_framerMotion.motion.custom is not a function"

See: https://stackoverflow.com/questions/66703410/next-js-framermotion-motion-custom-is-not-a-function

* feat(rtk-query): add Data tab #1126

* fix: bump min popup width to 700px #1126

Description:

improve UI of rtk-query right side tabs

* fix: bump min popup window width again to 760px #1126

* chore: add changeset

* feat(rtk-query): improve a11y of rtk-query-monitor tab panel #1126

* chore(rtk-query): add few integration tests to rtk-query-monitor #1126

* Fix merge

* Deduplicate msw

Co-authored-by: Nathan Bierema <nbierema@gmail.com>
This commit is contained in:
Fabrizio Vitale 2022-07-05 15:31:38 +02:00 committed by GitHub
parent 6cf1865f55
commit 24f60a7aa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 432 additions and 80 deletions

View File

@ -0,0 +1,5 @@
---
'@redux-devtools/rtk-query-monitor': minor
---
feat(rtk-query): add Data tab to rtk-query-monitor #1126 #1129

View File

@ -0,0 +1,5 @@
---
'rtk-query-demo': patch
---
fix: rtk-query-monitor demo not working #1126 #1129

View File

@ -0,0 +1,5 @@
---
'remotedev-redux-devtools-extension': patch
---
bump min popup window width to 760px #1126 #1129

View File

@ -7,7 +7,7 @@ style.
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
width: 100%; width: 100%;
min-width: 350px; min-width: 760px;
min-height: 400px; min-height: 400px;
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -17,6 +17,7 @@ style.
color: #fff; color: #fff;
} }
#root { #root {
min-width: 760px;
height: 100%; height: 100%;
} }
#root > div { #root > div {

View File

@ -1,2 +1,3 @@
demo demo
lib lib
dist

View File

@ -9,5 +9,13 @@ module.exports = {
project: ['./tsconfig.json'], project: ['./tsconfig.json'],
}, },
}, },
{
files: ['test/**/*.ts', 'test/**/*.tsx'],
extends: '../../eslintrc.ts.react.jest.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.test.json'],
},
},
], ],
}; };

View File

@ -20,7 +20,7 @@
"@redux-devtools/rtk-query-monitor": "^3.0.0", "@redux-devtools/rtk-query-monitor": "^3.0.0",
"@reduxjs/toolkit": "^1.8.2", "@reduxjs/toolkit": "^1.8.2",
"framer-motion": "^6.3.15", "framer-motion": "^6.3.15",
"msw": "^0.42.3", "msw": "^0.43.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.4.0", "react-icons": "^4.4.0",

View File

@ -0,0 +1,12 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapper: {
'\\.css$': '<rootDir>/test/__mocks__/styleMock.ts',
},
globals: {
'ts-jest': {
tsconfig: 'tsconfig.test.json',
},
},
};

View File

@ -37,10 +37,11 @@
"build:esm": "babel src --config-file ./babel.config.esm.json --extensions \".ts,.tsx\" --out-dir lib/esm", "build:esm": "babel src --config-file ./babel.config.esm.json --extensions \".ts,.tsx\" --out-dir lib/esm",
"build:types": "tsc --emitDeclarationOnly", "build:types": "tsc --emitDeclarationOnly",
"clean": "rimraf lib", "clean": "rimraf lib",
"test": "jest",
"lint": "eslint . --ext .ts,.tsx", "lint": "eslint . --ext .ts,.tsx",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"prepack": "pnpm run clean && pnpm run build", "prepack": "pnpm run clean && pnpm run build",
"prepublish": "pnpm run type-check && pnpm run lint" "prepublish": "pnpm run type-check && pnpm run lint && pnpm run test"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.3", "@babel/runtime": "^7.18.3",
@ -68,6 +69,9 @@
"@babel/preset-typescript": "^7.17.12", "@babel/preset-typescript": "^7.17.12",
"@redux-devtools/core": "^3.13.1", "@redux-devtools/core": "^3.13.1",
"@reduxjs/toolkit": "^1.8.2", "@reduxjs/toolkit": "^1.8.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@types/jest": "^27.5.2",
"@types/hex-rgba": "^1.0.1", "@types/hex-rgba": "^1.0.1",
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.7",
"@types/react": "^18.0.14", "@types/react": "^18.0.14",
@ -75,11 +79,16 @@
"@typescript-eslint/parser": "^5.29.0", "@typescript-eslint/parser": "^5.29.0",
"eslint": "^8.18.0", "eslint": "^8.18.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^26.5.3",
"eslint-plugin-react": "^7.30.1", "eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"jest": "^27.5.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.2",
"redux": "^4.2.0", "redux": "^4.2.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-jest": "^27.1.5",
"typescript": "~4.7.4" "typescript": "~4.7.4"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -128,6 +128,7 @@ export class QueryForm extends React.PureComponent<
{({ styling, base16Theme }) => { {({ styling, base16Theme }) => {
return ( return (
<form <form
id="rtk-query-monitor-query-selection-form"
action="#" action="#"
onSubmit={this.handleSubmit} onSubmit={this.handleSubmit}
{...styling('queryForm')} {...styling('queryForm')}

View File

@ -1,8 +1,10 @@
import { createSelector, Selector } from '@reduxjs/toolkit'; import { createSelector, Selector } from '@reduxjs/toolkit';
import React, { ReactNode, PureComponent } from 'react'; import React, { ReactNode, PureComponent } from 'react';
import { Action, AnyAction } from 'redux'; import { Action, AnyAction } from 'redux';
import { QueryPreviewTabs } from '../types';
import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y';
import { emptyRecord, identity } from '../utils/object'; import { emptyRecord, identity } from '../utils/object';
import { TreeView } from './TreeView'; import { TreeView, TreeViewProps } from './TreeView';
export interface QueryPreviewActionsProps { export interface QueryPreviewActionsProps {
isWideLayout: boolean; isWideLayout: boolean;
@ -11,6 +13,12 @@ export interface QueryPreviewActionsProps {
const keySep = ' - '; const keySep = ' - ';
const rootProps: TreeViewProps['rootProps'] = {
'aria-labelledby': renderTabPanelButtonId(QueryPreviewTabs.actions),
id: renderTabPanelId(QueryPreviewTabs.actions),
role: 'tabpanel',
};
export class QueryPreviewActions extends PureComponent<QueryPreviewActionsProps> { export class QueryPreviewActions extends PureComponent<QueryPreviewActionsProps> {
selectFormattedActions: Selector< selectFormattedActions: Selector<
AnyAction[], AnyAction[],
@ -74,6 +82,7 @@ export class QueryPreviewActions extends PureComponent<QueryPreviewActionsProps>
return ( return (
<TreeView <TreeView
rootProps={rootProps}
data={this.selectFormattedActions(actionsOfQuery)} data={this.selectFormattedActions(actionsOfQuery)}
isWideLayout={isWideLayout} isWideLayout={isWideLayout}
shouldExpandNode={this.shouldExpandNode} shouldExpandNode={this.shouldExpandNode}

View File

@ -1,7 +1,8 @@
import React, { ReactNode, PureComponent } from 'react'; import React, { ReactNode, PureComponent } from 'react';
import { ApiStats, RtkQueryApiState } from '../types'; import { ApiStats, QueryPreviewTabs, RtkQueryApiState } from '../types';
import { StyleUtilsContext } from '../styles/createStylingFromTheme'; import { StyleUtilsContext } from '../styles/createStylingFromTheme';
import { TreeView } from './TreeView'; import { TreeView, TreeViewProps } from './TreeView';
import { renderTabPanelId, renderTabPanelButtonId } from '../utils/a11y';
export interface QueryPreviewApiProps { export interface QueryPreviewApiProps {
apiStats: ApiStats | null; apiStats: ApiStats | null;
@ -9,6 +10,12 @@ export interface QueryPreviewApiProps {
isWideLayout: boolean; isWideLayout: boolean;
} }
const rootProps: TreeViewProps['rootProps'] = {
'aria-labelledby': renderTabPanelButtonId(QueryPreviewTabs.apiConfig),
id: renderTabPanelId(QueryPreviewTabs.apiConfig),
role: 'tabpanel',
};
export class QueryPreviewApi extends PureComponent<QueryPreviewApiProps> { export class QueryPreviewApi extends PureComponent<QueryPreviewApiProps> {
shouldExpandApiStateNode = ( shouldExpandApiStateNode = (
keyPath: (string | number)[], keyPath: (string | number)[],
@ -33,7 +40,7 @@ export class QueryPreviewApi extends PureComponent<QueryPreviewApiProps> {
return ( return (
<StyleUtilsContext.Consumer> <StyleUtilsContext.Consumer>
{({ styling }) => ( {({ styling }) => (
<article {...styling('tabContent')}> <article {...rootProps} {...styling('tabContent')}>
<h2>{apiState.config.reducerPath}</h2> <h2>{apiState.config.reducerPath}</h2>
<TreeView <TreeView
before={<h3>State</h3>} before={<h3>State</h3>}

View File

@ -0,0 +1,38 @@
import React, { ReactNode, PureComponent } from 'react';
import { QueryPreviewTabs, RtkResourceInfo } from '../types';
import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y';
import { TreeView, TreeViewProps } from './TreeView';
export interface QueryPreviewDataProps {
data: RtkResourceInfo['state']['data'];
isWideLayout: boolean;
}
const rootProps: TreeViewProps['rootProps'] = {
'aria-labelledby': renderTabPanelButtonId(QueryPreviewTabs.data),
id: renderTabPanelId(QueryPreviewTabs.data),
role: 'tabpanel',
};
export class QueryPreviewData extends PureComponent<QueryPreviewDataProps> {
shouldExpandNode = (
keyPath: (string | number)[],
value: unknown,
layer: number
): boolean => {
return layer <= 1;
};
render(): ReactNode {
const { data, isWideLayout } = this.props;
return (
<TreeView
rootProps={rootProps}
data={data}
isWideLayout={isWideLayout}
shouldExpandNode={this.shouldExpandNode}
/>
);
}
}

View File

@ -1,6 +1,7 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { StyleUtilsContext } from '../styles/createStylingFromTheme'; import { StyleUtilsContext } from '../styles/createStylingFromTheme';
import { QueryPreviewTabs, TabOption } from '../types'; import { QueryPreviewTabs, TabOption } from '../types';
import { renderTabPanelButtonId } from '../utils/a11y';
import { emptyArray } from '../utils/object'; import { emptyArray } from '../utils/object';
export interface QueryPreviewHeaderProps { export interface QueryPreviewHeaderProps {
@ -28,7 +29,11 @@ export class QueryPreviewHeader extends React.Component<QueryPreviewHeaderProps>
<div {...styling('previewHeader')}> <div {...styling('previewHeader')}>
<div {...styling('tabSelector')}> <div {...styling('tabSelector')}>
{tabs.map((tab) => ( {tabs.map((tab) => (
<div <button
type="button"
id={renderTabPanelButtonId(tab.value)}
aria-selected={tab.value === selectedTab}
role={'tab'}
onClick={() => this.handleTabClick(tab)} onClick={() => this.handleTabClick(tab)}
key={tab.value} key={tab.value}
{...styling( {...styling(
@ -42,7 +47,7 @@ export class QueryPreviewHeader extends React.Component<QueryPreviewHeaderProps>
<span> <span>
{renderTabLabel ? renderTabLabel(tab) : tab.label} {renderTabLabel ? renderTabLabel(tab) : tab.label}
</span> </span>
</div> </button>
))} ))}
</div> </div>
</div> </div>

View File

@ -1,11 +1,12 @@
import { createSelector, Selector } from '@reduxjs/toolkit'; import { createSelector, Selector } from '@reduxjs/toolkit';
import { QueryStatus } from '@reduxjs/toolkit/dist/query'; import { QueryStatus } from '@reduxjs/toolkit/dist/query';
import React, { ReactNode, PureComponent } from 'react'; import React, { ReactNode, PureComponent } from 'react';
import { RtkResourceInfo, RTKStatusFlags } from '../types'; import { QueryPreviewTabs, RtkResourceInfo, RTKStatusFlags } from '../types';
import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y';
import { formatMs } from '../utils/formatters'; import { formatMs } from '../utils/formatters';
import { identity } from '../utils/object'; import { identity } from '../utils/object';
import { getQueryStatusFlags } from '../utils/rtk-query'; import { getQueryStatusFlags } from '../utils/rtk-query';
import { TreeView } from './TreeView'; import { TreeView, TreeViewProps } from './TreeView';
type QueryTimings = { type QueryTimings = {
startedAt: string; startedAt: string;
@ -23,6 +24,12 @@ type FormattedQuery = {
| { query: RtkResourceInfo['state'] } | { query: RtkResourceInfo['state'] }
); );
const rootProps: TreeViewProps['rootProps'] = {
'aria-labelledby': renderTabPanelButtonId(QueryPreviewTabs.queryinfo),
id: renderTabPanelId(QueryPreviewTabs.queryinfo),
role: 'tabpanel',
};
export interface QueryPreviewInfoProps { export interface QueryPreviewInfoProps {
resInfo: RtkResourceInfo; resInfo: RtkResourceInfo;
isWideLayout: boolean; isWideLayout: boolean;
@ -97,6 +104,7 @@ export class QueryPreviewInfo extends PureComponent<QueryPreviewInfoProps> {
return ( return (
<TreeView <TreeView
rootProps={rootProps}
data={formattedQuery} data={formattedQuery}
isWideLayout={isWideLayout} isWideLayout={isWideLayout}
shouldExpandNode={this.shouldExpandNode} shouldExpandNode={this.shouldExpandNode}

View File

@ -1,6 +1,15 @@
import React, { ReactNode, PureComponent } from 'react'; import React, { ReactNode, PureComponent } from 'react';
import { RtkQueryApiState } from '../types'; import { QueryPreviewTabs, RtkQueryApiState } from '../types';
import { TreeView } from './TreeView'; import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y';
import { TreeView, TreeViewProps } from './TreeView';
const rootProps: TreeViewProps['rootProps'] = {
'aria-labelledby': renderTabPanelButtonId(
QueryPreviewTabs.querySubscriptions
),
id: renderTabPanelId(QueryPreviewTabs.querySubscriptions),
role: 'tabpanel',
};
export interface QueryPreviewSubscriptionsProps { export interface QueryPreviewSubscriptionsProps {
subscriptions: RtkQueryApiState['subscriptions'][keyof RtkQueryApiState['subscriptions']]; subscriptions: RtkQueryApiState['subscriptions'][keyof RtkQueryApiState['subscriptions']];
@ -12,7 +21,11 @@ export class QueryPreviewSubscriptions extends PureComponent<QueryPreviewSubscri
const { subscriptions } = this.props; const { subscriptions } = this.props;
return ( return (
<TreeView data={subscriptions} isWideLayout={this.props.isWideLayout} /> <TreeView
rootProps={rootProps}
data={subscriptions}
isWideLayout={this.props.isWideLayout}
/>
); );
} }
} }

View File

@ -1,11 +1,18 @@
import React, { ReactNode, PureComponent } from 'react'; import React, { ReactNode, PureComponent } from 'react';
import { RtkQueryTag } from '../types'; import { QueryPreviewTabs, RtkQueryTag } from '../types';
import { TreeView } from './TreeView'; import { renderTabPanelButtonId, renderTabPanelId } from '../utils/a11y';
import { TreeView, TreeViewProps } from './TreeView';
interface QueryPreviewTagsState { interface QueryPreviewTagsState {
data: { tags: RtkQueryTag[] }; data: { tags: RtkQueryTag[] };
} }
const rootProps: TreeViewProps['rootProps'] = {
'aria-labelledby': renderTabPanelButtonId(QueryPreviewTabs.queryTags),
id: renderTabPanelId(QueryPreviewTabs.queryTags),
role: 'tabpanel',
};
export interface QueryPreviewTagsProps { export interface QueryPreviewTagsProps {
tags: RtkQueryTag[]; tags: RtkQueryTag[];
isWideLayout: boolean; isWideLayout: boolean;
@ -26,6 +33,8 @@ export class QueryPreviewTags extends PureComponent<
render(): ReactNode { render(): ReactNode {
const { isWideLayout, tags } = this.props; const { isWideLayout, tags } = this.props;
return <TreeView data={tags} isWideLayout={isWideLayout} />; return (
<TreeView rootProps={rootProps} data={tags} isWideLayout={isWideLayout} />
);
} }
} }

View File

@ -22,6 +22,9 @@ export interface TreeViewProps
before?: ReactNode; before?: ReactNode;
after?: ReactNode; after?: ReactNode;
children?: ReactNode; children?: ReactNode;
rootProps?: Partial<
Omit<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style'>
>;
} }
export class TreeView extends React.PureComponent<TreeViewProps> { export class TreeView extends React.PureComponent<TreeViewProps> {
@ -80,13 +83,14 @@ export class TreeView extends React.PureComponent<TreeViewProps> {
keyPath, keyPath,
shouldExpandNode, shouldExpandNode,
hideRoot, hideRoot,
rootProps,
} = this.props; } = this.props;
return ( return (
<StyleUtilsContext.Consumer> <StyleUtilsContext.Consumer>
{({ styling, invertTheme, base16Theme }) => { {({ styling, invertTheme, base16Theme }) => {
return ( return (
<div {...styling('treeWrapper')}> <div {...rootProps} {...styling('treeWrapper')}>
{before} {before}
<JSONTree <JSONTree
keyPath={keyPath} keyPath={keyPath}

View File

@ -33,6 +33,10 @@ import {
QueryPreviewActionsProps, QueryPreviewActionsProps,
} from '../components/QueryPreviewActions'; } from '../components/QueryPreviewActions';
import { isTabVisible } from '../utils/tabs'; import { isTabVisible } from '../utils/tabs';
import {
QueryPreviewData,
QueryPreviewDataProps,
} from '../components/QueryPreviewData';
export interface QueryPreviewProps<S = unknown> { export interface QueryPreviewProps<S = unknown> {
readonly selectedTab: QueryPreviewTabs; readonly selectedTab: QueryPreviewTabs;
@ -66,6 +70,14 @@ const MappedQueryPreviewInfo = mapProps<
QueryPreviewInfoProps QueryPreviewInfoProps
>(({ resInfo, isWideLayout }) => ({ resInfo, isWideLayout }))(QueryPreviewInfo); >(({ resInfo, isWideLayout }) => ({ resInfo, isWideLayout }))(QueryPreviewInfo);
const MappedQueryPreviewData = mapProps<
QueryPreviewTabProps,
QueryPreviewDataProps
>(({ resInfo, isWideLayout }) => ({
data: resInfo?.state?.data,
isWideLayout,
}))(QueryPreviewData);
const MappedQuerySubscriptipns = mapProps< const MappedQuerySubscriptipns = mapProps<
QueryPreviewTabProps, QueryPreviewTabProps,
QueryPreviewSubscriptionsProps QueryPreviewSubscriptionsProps
@ -93,6 +105,16 @@ const MappedQueryPreviewActions = mapProps<
const tabs: ReadonlyArray< const tabs: ReadonlyArray<
TabOption<QueryPreviewTabs, QueryPreviewTabProps, RtkResourceInfo['type']> TabOption<QueryPreviewTabs, QueryPreviewTabProps, RtkResourceInfo['type']>
> = [ > = [
{
label: 'Data',
value: QueryPreviewTabs.data,
component: MappedQueryPreviewData,
visible: {
query: true,
mutation: true,
default: true,
},
},
{ {
label: 'Query', label: 'Query',
value: QueryPreviewTabs.queryinfo, value: QueryPreviewTabs.queryinfo,

View File

@ -258,6 +258,7 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
], ],
(subscriptions, actions, tags) => { (subscriptions, actions, tags) => {
return { return {
[QueryPreviewTabs.data]: 0,
[QueryPreviewTabs.queryTags]: tags.length, [QueryPreviewTabs.queryTags]: tags.length,
[QueryPreviewTabs.querySubscriptions]: Object.keys(subscriptions ?? {}) [QueryPreviewTabs.querySubscriptions]: Object.keys(subscriptions ?? {})
.length, .length,

View File

@ -220,6 +220,8 @@ const getSheetFromColorMap = (map: ColorMap) => {
padding: '0 8px', padding: '0 8px',
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
boxShadow: 'none',
outline: 'none',
color: map.TEXT_COLOR, color: map.TEXT_COLOR,
'border-style': 'solid', 'border-style': 'solid',
'border-width': '1px', 'border-width': '1px',

View File

@ -8,6 +8,7 @@ import type { QueryComparators } from './utils/comparators';
import type { QueryFilters } from './utils/filters'; import type { QueryFilters } from './utils/filters';
export enum QueryPreviewTabs { export enum QueryPreviewTabs {
data,
queryinfo, queryinfo,
apiConfig, apiConfig,
querySubscriptions, querySubscriptions,

View File

@ -0,0 +1,9 @@
import { QueryPreviewTabs } from '../types';
export function renderTabPanelId(value: QueryPreviewTabs): string {
return `rtk-query-monitor-tab-panel-${value}`;
}
export function renderTabPanelButtonId(value: QueryPreviewTabs): string {
return renderTabPanelId(value) + '-button';
}

View File

@ -0,0 +1 @@
export default {};

View File

@ -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(<MonitorAsAny />);

View File

@ -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<typeof setupStore>['store'];
children?: React.ComponentProps<typeof Provider>['children'];
}) {
const AnyProvider = Provider as any;
return (
<div id="app-root">
<AnyProvider store={store}>
{children}
<ReduxDevTools />
</AnyProvider>
</div>
);
}
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<void>).mockImplementation(() => {});
const dataPanelDomId = '#rtk-query-monitor-tab-panel-0';
const childrenTextContent = 'Renders children';
const fetchBaseQueryMock: BaseQueryJestMockFunction<Record<string, unknown>> =
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<void>).mockRestore();
});
it('renders on a standalone app without crashing', () => {
const { container } = render(
<Providers store={store}>
<div data-testid="children">{childrenTextContent}</div>
</Providers>
);
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(
<Providers store={store}>
<div data-testid="children">{childrenTextContent}</div>
</Providers>
);
// 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
);
});
});

View File

@ -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<Args, Result, unknown, Meta>;
export type BaseQueryJestMockFunction<Result> = jest.Mock<
ReturnType<MockBaseQuery<Result>>,
Parameters<MockBaseQuery<Result>>
>;
export function createMockBaseQuery<Result>(
jestMockFn: BaseQueryJestMockFunction<Result>
): MockBaseQuery<Result> {
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<Record<string, any>>
) {
return createApi({
reducerPath: 'pokemonApi',
keepUnusedDataFor: 9999,
baseQuery: createMockBaseQuery(jestMockFn),
tagTypes: ['pokemon'],
endpoints: (builder) => ({
getPokemonByName: builder.query<Record<string, any>, string>({
query: (name: string) => `pokemon/${name}`,
providesTags: (result, error, name: string) => [
{ type: 'pokemon' },
{ type: 'pokemon', id: name },
],
}),
}),
});
}
export function setupStore(
jestMockFn: BaseQueryJestMockFunction<Record<string, any>>,
devTools: typeof ReduxDevTools
) {
const pokemonApi = createPokemonApi(jestMockFn);
const reducer = combineReducers({
[pokemonApi.reducerPath]: pokemonApi.reducer,
});
const store: EnhancedStore<ReturnType<typeof reducer>> = 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,
};
}

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.react.base.json",
"compilerOptions": {
"types": ["jest"]
},
"include": ["src", "test"],
"exclude": ["dist"]
}

View File

@ -1788,7 +1788,10 @@ importers:
'@redux-devtools/core': ^3.13.1 '@redux-devtools/core': ^3.13.1
'@redux-devtools/ui': ^1.3.0 '@redux-devtools/ui': ^1.3.0
'@reduxjs/toolkit': ^1.8.2 '@reduxjs/toolkit': ^1.8.2
'@testing-library/jest-dom': ^5.16.4
'@testing-library/react': ^13.3.0
'@types/hex-rgba': ^1.0.1 '@types/hex-rgba': ^1.0.1
'@types/jest': ^27.5.2
'@types/lodash': ^4.14.182 '@types/lodash': ^4.14.182
'@types/lodash.debounce': ^4.0.7 '@types/lodash.debounce': ^4.0.7
'@types/prop-types': ^15.7.5 '@types/prop-types': ^15.7.5
@ -1798,20 +1801,25 @@ importers:
'@typescript-eslint/parser': ^5.29.0 '@typescript-eslint/parser': ^5.29.0
eslint: ^8.18.0 eslint: ^8.18.0
eslint-config-prettier: ^8.5.0 eslint-config-prettier: ^8.5.0
eslint-plugin-jest: ^26.5.3
eslint-plugin-react: ^7.30.1 eslint-plugin-react: ^7.30.1
eslint-plugin-react-hooks: ^4.6.0 eslint-plugin-react-hooks: ^4.6.0
hex-rgba: ^1.0.2 hex-rgba: ^1.0.2
immutable: ^4.1.0 immutable: ^4.1.0
jest: ^27.5.1
jss: ^10.9.0 jss: ^10.9.0
jss-preset-default: ^10.9.0 jss-preset-default: ^10.9.0
lodash.debounce: ^4.0.8 lodash.debounce: ^4.0.8
prop-types: ^15.8.1 prop-types: ^15.8.1
react: ^18.2.0 react: ^18.2.0
react-base16-styling: ^0.9.1 react-base16-styling: ^0.9.1
react-dom: ^18.2.0
react-json-tree: ^0.17.0 react-json-tree: ^0.17.0
react-redux: ^8.0.2
redux: ^4.2.0 redux: ^4.2.0
redux-devtools-themes: ^1.0.0 redux-devtools-themes: ^1.0.0
rimraf: ^3.0.2 rimraf: ^3.0.2
ts-jest: ^27.1.5
typescript: ~4.7.4 typescript: ~4.7.4
dependencies: dependencies:
'@babel/runtime': 7.18.3 '@babel/runtime': 7.18.3
@ -1837,19 +1845,27 @@ importers:
'@babel/preset-react': 7.17.12_@babel+core@7.18.5 '@babel/preset-react': 7.17.12_@babel+core@7.18.5
'@babel/preset-typescript': 7.17.12_@babel+core@7.18.5 '@babel/preset-typescript': 7.17.12_@babel+core@7.18.5
'@redux-devtools/core': link:../redux-devtools '@redux-devtools/core': link:../redux-devtools
'@reduxjs/toolkit': 1.8.2_react@18.2.0 '@reduxjs/toolkit': 1.8.2_kkwg4cbsojnjnupd3btipussee
'@testing-library/jest-dom': 5.16.4
'@testing-library/react': 13.3.0_biqbaboplfbrettd7655fr4n2y
'@types/hex-rgba': 1.0.1 '@types/hex-rgba': 1.0.1
'@types/jest': 27.5.2
'@types/lodash.debounce': 4.0.7 '@types/lodash.debounce': 4.0.7
'@types/react': 18.0.14 '@types/react': 18.0.14
'@typescript-eslint/eslint-plugin': 5.29.0_qqmbkyiaixvppdwswpytuf2hgm '@typescript-eslint/eslint-plugin': 5.29.0_qqmbkyiaixvppdwswpytuf2hgm
'@typescript-eslint/parser': 5.29.0_b5e7v2qnwxfo6hmiq56u52mz3e '@typescript-eslint/parser': 5.29.0_b5e7v2qnwxfo6hmiq56u52mz3e
eslint: 8.18.0 eslint: 8.18.0
eslint-config-prettier: 8.5.0_eslint@8.18.0 eslint-config-prettier: 8.5.0_eslint@8.18.0
eslint-plugin-jest: 26.5.3_hkaktba3mic4t6mktu5mmyzyhi
eslint-plugin-react: 7.30.1_eslint@8.18.0 eslint-plugin-react: 7.30.1_eslint@8.18.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.18.0 eslint-plugin-react-hooks: 4.6.0_eslint@8.18.0
jest: 27.5.1
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-redux: 8.0.2_42iqcqzqjdyq32nxzztmetzyhu
redux: 4.2.0 redux: 4.2.0
rimraf: 3.0.2 rimraf: 3.0.2
ts-jest: 27.1.5_7giygfzd3xe4pjz4ngu3nwlqbm
typescript: 4.7.4 typescript: 4.7.4
packages/redux-devtools-rtk-query-monitor/demo: packages/redux-devtools-rtk-query-monitor/demo:
@ -1886,7 +1902,7 @@ importers:
fork-ts-checker-webpack-plugin: ^7.2.11 fork-ts-checker-webpack-plugin: ^7.2.11
framer-motion: ^6.3.15 framer-motion: ^6.3.15
html-webpack-plugin: ^5.5.0 html-webpack-plugin: ^5.5.0
msw: ^0.42.3 msw: ^0.43.0
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
react-icons: ^4.4.0 react-icons: ^4.4.0
@ -1911,7 +1927,7 @@ importers:
'@redux-devtools/rtk-query-monitor': link:.. '@redux-devtools/rtk-query-monitor': link:..
'@reduxjs/toolkit': 1.8.2_kkwg4cbsojnjnupd3btipussee '@reduxjs/toolkit': 1.8.2_kkwg4cbsojnjnupd3btipussee
framer-motion: 6.3.15_biqbaboplfbrettd7655fr4n2y framer-motion: 6.3.15_biqbaboplfbrettd7655fr4n2y
msw: 0.42.3_typescript@4.7.4 msw: 0.43.0_typescript@4.7.4
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
react-icons: 4.4.0_react@18.2.0 react-icons: 4.4.0_react@18.2.0
@ -5999,29 +6015,10 @@ packages:
dependencies: dependencies:
immer: 9.0.15 immer: 9.0.15
react: 18.2.0 react: 18.2.0
react-redux: 8.0.2_fzknh3pugl53jp7xsetxkg5ani react-redux: 8.0.2_42iqcqzqjdyq32nxzztmetzyhu
redux: 4.2.0 redux: 4.2.0
redux-thunk: 2.4.1_redux@4.2.0 redux-thunk: 2.4.1_redux@4.2.0
reselect: 4.1.6 reselect: 4.1.6
dev: false
/@reduxjs/toolkit/1.8.2_react@18.2.0:
resolution: {integrity: sha512-CtPw5TkN1pHRigMFCOS/0qg3b/yfPV5qGCsltVnIz7bx4PKTJlGHYfIxm97qskLknMzuGfjExaYdXJ77QTL0vg==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18 || 18
react-redux: ^7.2.1 || ^8.0.0-beta
peerDependenciesMeta:
react:
optional: true
react-redux:
optional: true
dependencies:
immer: 9.0.15
react: 18.2.0
redux: 4.2.0
redux-thunk: 2.4.1_redux@4.2.0
reselect: 4.1.6
dev: true
/@restart/hooks/0.4.7_react@18.2.0: /@restart/hooks/0.4.7_react@18.2.0:
resolution: {integrity: sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A==} resolution: {integrity: sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A==}
@ -16403,43 +16400,6 @@ packages:
/ms/2.1.3: /ms/2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
/msw/0.42.3_typescript@4.7.4:
resolution: {integrity: sha512-zrKBIGCDsNUCZLd3DLSeUtRruZ0riwJgORg9/bSDw3D0PTI8XUGAK3nC0LJA9g0rChGuKaWK/SwObA8wpFrz4g==}
engines: {node: '>=14'}
hasBin: true
requiresBuild: true
peerDependencies:
typescript: '>= 4.2.x <= 4.7.x'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@mswjs/cookies': 0.2.1
'@mswjs/interceptors': 0.16.6
'@open-draft/until': 1.0.3
'@types/cookie': 0.4.1
'@types/js-levenshtein': 1.1.1
chalk: 4.1.1
chokidar: 3.5.3
cookie: 0.4.2
graphql: 16.5.0
headers-polyfill: 3.0.7
inquirer: 8.2.4
is-node-process: 1.0.1
js-levenshtein: 1.1.6
node-fetch: 2.6.7
outvariant: 1.3.0
path-to-regexp: 6.2.1
statuses: 2.0.1
strict-event-emitter: 0.2.4
type-fest: 1.4.0
typescript: 4.7.4
yargs: 17.5.1
transitivePeerDependencies:
- encoding
- supports-color
dev: false
/msw/0.43.0_typescript@4.7.4: /msw/0.43.0_typescript@4.7.4:
resolution: {integrity: sha512-XJylZP0qW3D5WUGWh9FFefJEl3MGG4y1I+/8a833d0eedm6B+GaPm6wPVZNcnlS2YVTagvEgShVJ7ZtY66tTRQ==} resolution: {integrity: sha512-XJylZP0qW3D5WUGWh9FFefJEl3MGG4y1I+/8a833d0eedm6B+GaPm6wPVZNcnlS2YVTagvEgShVJ7ZtY66tTRQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -16476,7 +16436,6 @@ packages:
- encoding - encoding
- supports-color - supports-color
dev: false dev: false
optional: true
/multicast-dns/7.2.5: /multicast-dns/7.2.5:
resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==}
@ -18192,7 +18151,6 @@ packages:
react-is: 18.2.0 react-is: 18.2.0
redux: 4.2.0 redux: 4.2.0
use-sync-external-store: 1.2.0_react@18.2.0 use-sync-external-store: 1.2.0_react@18.2.0
dev: true
/react-redux/8.0.2_fzknh3pugl53jp7xsetxkg5ani: /react-redux/8.0.2_fzknh3pugl53jp7xsetxkg5ani:
resolution: {integrity: sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA==} resolution: {integrity: sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA==}