Merge pull request #7727 from backstage/test-utils-core
Move test-utils-core into test-utils
This commit is contained in:
@@ -21,4 +21,3 @@
|
||||
*/
|
||||
|
||||
export * from './testUtils';
|
||||
export * from '@backstage/test-utils-core';
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
|
||||
const codes = {
|
||||
Tab: 9,
|
||||
Enter: 10,
|
||||
Click: 17 /* This keyboard can click, deal with it */,
|
||||
Esc: 27,
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated because it has no usages. Perhaps resurfaced in the future when need be.
|
||||
*/
|
||||
export class Keyboard {
|
||||
static async type(target, input) {
|
||||
await new Keyboard(target).type(input);
|
||||
}
|
||||
|
||||
static async typeDebug(target, input) {
|
||||
await new Keyboard(target, { debug: true }).type(input);
|
||||
}
|
||||
|
||||
static toReadableInput(chars) {
|
||||
return chars.split('').map(char => {
|
||||
switch (char.charCodeAt(0)) {
|
||||
case codes.Tab:
|
||||
return '<Tab>';
|
||||
case codes.Enter:
|
||||
return '<Enter>';
|
||||
case codes.Click:
|
||||
return '<Click>';
|
||||
case codes.Esc:
|
||||
return '<Esc>';
|
||||
default:
|
||||
return char;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static fromReadableInput(input) {
|
||||
return input.trim().replace(/\s*<([a-zA-Z]+)>\s*/g, (match, name) => {
|
||||
if (name in codes) {
|
||||
return String.fromCharCode(codes[name]);
|
||||
}
|
||||
throw new Error(`Unknown char name: '${name}'`);
|
||||
});
|
||||
}
|
||||
|
||||
constructor(target, { debug = false } = {}) {
|
||||
this.debug = debug;
|
||||
|
||||
if (target.ownerDocument) {
|
||||
this.document = target.ownerDocument;
|
||||
} else if (target.baseElement) {
|
||||
this.document = target.baseElement.ownerDocument;
|
||||
} else {
|
||||
throw new TypeError(
|
||||
'Keyboard(target): target must be DOM node or react-testing-library render() output',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Keyboard{document=${this.document}, debug=${this.debug}}`;
|
||||
}
|
||||
|
||||
_log(message, ...args) {
|
||||
if (this.debug) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Keyboard] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
_pretty(element) {
|
||||
const attrs = [...element.attributes]
|
||||
.map(attr => `${attr.name}="${attr.value}"`)
|
||||
.join(' ');
|
||||
return `<${element.nodeName.toLocaleLowerCase('en-US')} ${attrs}>`;
|
||||
}
|
||||
|
||||
get focused() {
|
||||
return this.document.activeElement;
|
||||
}
|
||||
|
||||
async type(input) {
|
||||
this._log(
|
||||
`sending sequence '${input}' with initial focus ${this._pretty(
|
||||
this.focused,
|
||||
)}`,
|
||||
);
|
||||
await this.send(Keyboard.fromReadableInput(input));
|
||||
}
|
||||
|
||||
async send(chars) {
|
||||
for (const key of chars.split('')) {
|
||||
const charCode = key.charCodeAt(0);
|
||||
|
||||
if (charCode === codes.Tab) {
|
||||
await this.tab();
|
||||
continue;
|
||||
}
|
||||
|
||||
const focused = this.focused;
|
||||
if (!focused || focused === this.document.body) {
|
||||
throw Error(
|
||||
`No element focused in document while trying to type '${Keyboard.toReadableInput(
|
||||
chars,
|
||||
)}'`,
|
||||
);
|
||||
}
|
||||
const nextValue = (focused.value || '') + key;
|
||||
|
||||
if (charCode >= 32) {
|
||||
await this._sendKey(key, charCode, () => {
|
||||
this._log(
|
||||
`sending +${key} = '${nextValue}' to ${this._pretty(focused)}`,
|
||||
);
|
||||
fireEvent.change(focused, {
|
||||
target: { value: nextValue },
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
});
|
||||
} else if (charCode === codes.Enter) {
|
||||
await this.enter(focused.value || '');
|
||||
} else if (charCode === codes.Esc) {
|
||||
await this.escape();
|
||||
} else if (charCode === codes.Click) {
|
||||
await this.click();
|
||||
} else {
|
||||
throw new Error(`Unsupported char code, ${charCode}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async click() {
|
||||
this._log(`clicking ${this._pretty(this.focused)}`);
|
||||
await act(async () => fireEvent.click(this.focused));
|
||||
}
|
||||
|
||||
async tab() {
|
||||
await this._sendKey('Tab', codes.Tab, () => {
|
||||
const focusable = this.document.querySelectorAll(
|
||||
[
|
||||
'a[href]',
|
||||
'area[href]',
|
||||
'input:not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
'button:not([disabled])',
|
||||
'iframe',
|
||||
'object',
|
||||
'embed',
|
||||
'*[tabindex]',
|
||||
'*[contenteditable]',
|
||||
].join(','),
|
||||
);
|
||||
|
||||
const tabbable = [...focusable].filter(el => {
|
||||
return el.tabIndex >= 0;
|
||||
});
|
||||
|
||||
const focused = this.document.activeElement;
|
||||
const focusedIndex = tabbable.indexOf(focused);
|
||||
const nextFocus = tabbable[focusedIndex + (1 % tabbable.length)];
|
||||
|
||||
this._log(
|
||||
`tabbing to ${this._pretty(nextFocus)} ${this.focused.textContent}`,
|
||||
);
|
||||
nextFocus.focus();
|
||||
});
|
||||
}
|
||||
|
||||
async enter(value) {
|
||||
this._log(`submitting '${value}' via ${this._pretty(this.focused)}`);
|
||||
await act(() =>
|
||||
this._sendKey('Enter', codes.Enter, () => {
|
||||
if (this.focused.type === 'button') {
|
||||
fireEvent.click(this.focused, { target: { value } });
|
||||
} else {
|
||||
fireEvent.submit(this.focused, {
|
||||
target: { value },
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async escape() {
|
||||
this._log(`escape from ${this._pretty(this.focused)}`);
|
||||
await act(async () => this._sendKey('Escape', codes.Esc));
|
||||
}
|
||||
|
||||
async _sendKey(key, charCode, action) {
|
||||
const event = { key, charCode, keyCode: charCode, which: charCode };
|
||||
const focused = this.focused;
|
||||
|
||||
if (fireEvent.keyDown(focused, event)) {
|
||||
if (fireEvent.keyPress(focused, event)) {
|
||||
if (action) {
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
fireEvent.keyUp(focused, event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Keyboard } from './Keyboard';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
describe('testUtils.Keyboard', () => {
|
||||
it('types into some inputs with focus and submits a form', async () => {
|
||||
const typed1 = [];
|
||||
const typed2 = [];
|
||||
const typed3 = [];
|
||||
|
||||
let submitted = false;
|
||||
const handleSubmit = event => {
|
||||
event.preventDefault();
|
||||
submitted = true;
|
||||
};
|
||||
|
||||
const rendered = render(
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input onChange={({ target: { value } }) => typed1.push(value)} />
|
||||
<input
|
||||
onChange={({ target: { value } }) => typed2.push(value)}
|
||||
/* eslint-disable-next-line jsx-a11y/no-autofocus */
|
||||
autoFocus
|
||||
/>
|
||||
<input onChange={({ target: { value } }) => typed3.push(value)} />
|
||||
</form>,
|
||||
);
|
||||
|
||||
const keyboard = new Keyboard(rendered);
|
||||
await keyboard.send('xy');
|
||||
await keyboard.tab();
|
||||
await keyboard.send('abc');
|
||||
await keyboard.enter();
|
||||
|
||||
expect(typed1).toEqual([]);
|
||||
expect(typed2).toEqual(['x', 'xy']);
|
||||
expect(typed3).toEqual(['a', 'ab', 'abc']);
|
||||
expect(submitted).toBe(true);
|
||||
});
|
||||
|
||||
it('can use Keyboard.type to send readable input', async () => {
|
||||
const typed1 = [];
|
||||
const typed2 = [];
|
||||
const typed3 = [];
|
||||
|
||||
let submitted = false;
|
||||
const handleSubmit = event => {
|
||||
event.preventDefault();
|
||||
submitted = true;
|
||||
};
|
||||
|
||||
const rendered = render(
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
defaultValue="1"
|
||||
onChange={({ target: { value } }) => typed1.push(value)}
|
||||
/>
|
||||
<input
|
||||
defaultValue="2"
|
||||
onChange={({ target: { value } }) => typed2.push(value)}
|
||||
/>
|
||||
<input
|
||||
defaultValue="3"
|
||||
onChange={({ target: { value } }) => typed3.push(value)}
|
||||
/>
|
||||
</form>,
|
||||
);
|
||||
|
||||
await Keyboard.type(rendered, '<Tab> a <Tab> b <Tab> c <Enter>');
|
||||
|
||||
expect(typed1).toEqual(['1a']);
|
||||
expect(typed2).toEqual(['2b']);
|
||||
expect(typed3).toEqual(['3c']);
|
||||
expect(submitted).toBe(true);
|
||||
});
|
||||
|
||||
it('should be able to navigate a radio input with click', async () => {
|
||||
const selections = [];
|
||||
|
||||
const rendered = render(
|
||||
<div onChange={({ target: { value } }) => selections.push(value)}>
|
||||
<input type="radio" name="group" value="a" />
|
||||
<input type="radio" name="group" value="b" />
|
||||
<input type="radio" name="group" value="c" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
await Keyboard.type(rendered, '<Tab> <Click> <Tab> <Tab> <Click>');
|
||||
|
||||
expect(selections).toEqual(['a', 'c']);
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* Mock implementation of {@link core-plugin-api#AnalyticsApi} with helpers to ensure that events are sent correctly.
|
||||
* Use getEvents in tests to verify captured events.
|
||||
* @public
|
||||
*/
|
||||
export class MockAnalyticsApi implements AnalyticsApi {
|
||||
private events: AnalyticsEvent[] = [];
|
||||
|
||||
|
||||
@@ -17,11 +17,20 @@
|
||||
import { ErrorApi, ErrorContext } from '@backstage/core-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
|
||||
type Options = {
|
||||
/**
|
||||
* Constructor arguments for {@link MockErrorApi}
|
||||
* @public
|
||||
*/
|
||||
export type MockErrorApiOptions = {
|
||||
// Need to be true if getErrors is used in testing.
|
||||
collect?: boolean;
|
||||
};
|
||||
|
||||
type ErrorWithContext = {
|
||||
/**
|
||||
* ErrorWithContext contains error and ErrorContext
|
||||
* @public
|
||||
*/
|
||||
export type ErrorWithContext = {
|
||||
error: Error;
|
||||
context?: ErrorContext;
|
||||
};
|
||||
@@ -39,11 +48,16 @@ const nullObservable = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock implementation of the {@link core-plugin-api#ErrorApi} to be used in tests.
|
||||
* Incudes withForError and getErrors methods for error testing.
|
||||
* @public
|
||||
*/
|
||||
export class MockErrorApi implements ErrorApi {
|
||||
private readonly errors = new Array<ErrorWithContext>();
|
||||
private readonly waiters = new Set<Waiter>();
|
||||
|
||||
constructor(private readonly options: Options = {}) {}
|
||||
constructor(private readonly options: MockErrorApiOptions = {}) {}
|
||||
|
||||
post(error: Error, context?: ErrorContext) {
|
||||
if (this.options.collect) {
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { MockErrorApi } from './MockErrorApi';
|
||||
export type { MockErrorApiOptions, ErrorWithContext } from './MockErrorApi';
|
||||
|
||||
@@ -18,8 +18,16 @@ import { StorageApi, StorageValueChange } from '@backstage/core-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
|
||||
/**
|
||||
* Type for map holding data in {@link MockStorageApi}
|
||||
* @public
|
||||
*/
|
||||
export type MockStorageBucket = { [key: string]: any };
|
||||
|
||||
/**
|
||||
* Mock implementation of the {@link core-plugin-api#StorageApi} to be used in tests
|
||||
* @public
|
||||
*/
|
||||
export class MockStorageApi implements StorageApi {
|
||||
private readonly namespace: string;
|
||||
private readonly data: MockStorageBucket;
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
useApi,
|
||||
useRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { withLogCollector } from '@backstage/test-utils-core';
|
||||
import { withLogCollector } from './logCollector';
|
||||
import { render } from '@testing-library/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Routes } from 'react-router';
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
createRouteRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { RenderResult } from '@testing-library/react';
|
||||
import { renderWithEffects } from '@backstage/test-utils-core';
|
||||
import { renderWithEffects } from './testingLibrary';
|
||||
import { mockApis } from './mockApis';
|
||||
|
||||
const ErrorBoundaryFallback = ({ error }: { error: Error }) => {
|
||||
@@ -43,8 +43,9 @@ const Progress = () => <div data-testid="progress" />;
|
||||
|
||||
/**
|
||||
* Options to customize the behavior of the test app wrapper.
|
||||
* @public
|
||||
*/
|
||||
type TestAppOptions = {
|
||||
export type TestAppOptions = {
|
||||
/**
|
||||
* Initial route entries to pass along as `initialEntries` to the router.
|
||||
*/
|
||||
@@ -56,11 +57,11 @@ type TestAppOptions = {
|
||||
* used by `useRouteRef` in the rendered elements.
|
||||
*
|
||||
* @example
|
||||
* wrapInTestApp(<MyComponent />, {
|
||||
* mountedRoutes: {
|
||||
* wrapInTestApp(<MyComponent />, \{
|
||||
* mountedRoutes: \{
|
||||
* '/my-path': myRouteRef,
|
||||
* }
|
||||
* })
|
||||
* \}
|
||||
* \})
|
||||
* // ...
|
||||
* const link = useRouteRef(myRouteRef)
|
||||
*/
|
||||
@@ -80,6 +81,7 @@ function isExternalRouteRef(
|
||||
*
|
||||
* @param Component - A component or react node to render inside the test app.
|
||||
* @param options - Additional options for the rendering.
|
||||
* @public
|
||||
*/
|
||||
export function wrapInTestApp(
|
||||
Component: ComponentType | ReactNode,
|
||||
@@ -171,6 +173,7 @@ export function wrapInTestApp(
|
||||
*
|
||||
* @param Component - A component or react node to render inside the test app.
|
||||
* @param options - Additional options for the rendering.
|
||||
* @public
|
||||
*/
|
||||
export async function renderInTestApp(
|
||||
Component: ComponentType | ReactNode,
|
||||
|
||||
@@ -17,4 +17,8 @@
|
||||
export * from './apis';
|
||||
export { default as mockBreakpoint } from './mockBreakpoint';
|
||||
export { wrapInTestApp, renderInTestApp } from './appWrappers';
|
||||
export type { TestAppOptions } from './appWrappers';
|
||||
export * from './msw';
|
||||
export * from './Keyboard';
|
||||
export * from './logCollector';
|
||||
export * from './testingLibrary';
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { withLogCollector } from './logCollector';
|
||||
|
||||
describe('logCollector', () => {
|
||||
it('should collect some logs synchronously', () => {
|
||||
const logs = withLogCollector(() => {
|
||||
console.log('a');
|
||||
console.warn('b');
|
||||
console.error('c');
|
||||
console.error('3');
|
||||
console.warn('2');
|
||||
console.log('1');
|
||||
});
|
||||
|
||||
expect(logs.log).toEqual(['a', '1']);
|
||||
expect(logs.warn).toEqual(['b', '2']);
|
||||
expect(logs.error).toEqual(['c', '3']);
|
||||
});
|
||||
|
||||
it('should collect some logs asynchrnously', async () => {
|
||||
const logs = await withLogCollector(async () => {
|
||||
console.log('a');
|
||||
console.warn('b');
|
||||
console.error('c');
|
||||
console.error('3');
|
||||
console.warn('2');
|
||||
console.log('1');
|
||||
});
|
||||
|
||||
expect(logs.log).toEqual(['a', '1']);
|
||||
expect(logs.warn).toEqual(['b', '2']);
|
||||
expect(logs.error).toEqual(['c', '3']);
|
||||
});
|
||||
|
||||
it('should collect specific logs synchronously', () => {
|
||||
const missedLogs = withLogCollector(() => {
|
||||
const logs = withLogCollector(['warn', 'log'], () => {
|
||||
console.log('a');
|
||||
console.warn('b');
|
||||
console.error('c');
|
||||
console.error('3');
|
||||
console.warn('2');
|
||||
console.log('1');
|
||||
});
|
||||
|
||||
expect(logs.log).toEqual(['a', '1']);
|
||||
expect(logs.warn).toEqual(['b', '2']);
|
||||
// @ts-ignore
|
||||
expect(logs.error).toEqual([]);
|
||||
});
|
||||
|
||||
expect(missedLogs.log).toEqual([]);
|
||||
expect(missedLogs.warn).toEqual([]);
|
||||
expect(missedLogs.error).toEqual(['c', '3']);
|
||||
});
|
||||
|
||||
it('should collect specific logs asynchrnously', async () => {
|
||||
const missedLogs = await withLogCollector(async () => {
|
||||
const logs = await withLogCollector(['error'], async () => {
|
||||
console.log('a');
|
||||
console.warn('b');
|
||||
console.error('c');
|
||||
console.error('3');
|
||||
console.warn('2');
|
||||
console.log('1');
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
expect(logs.log).toEqual([]);
|
||||
// @ts-ignore
|
||||
expect(logs.warn).toEqual([]);
|
||||
expect(logs.error).toEqual(['c', '3']);
|
||||
});
|
||||
|
||||
expect(missedLogs.log).toEqual(['a', '1']);
|
||||
expect(missedLogs.warn).toEqual(['b', '2']);
|
||||
expect(missedLogs.error).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
/**
|
||||
* Severity levels of {@link CollectedLogs}
|
||||
* @public */
|
||||
export type LogFuncs = 'log' | 'warn' | 'error';
|
||||
/**
|
||||
* AsyncLogCollector type used in {@link (withLogCollector:1)} callback function.
|
||||
* @public */
|
||||
export type AsyncLogCollector = () => Promise<void>;
|
||||
/**
|
||||
* SyncLogCollector type used in {@link (withLogCollector:2)} callback function.
|
||||
* @public */
|
||||
export type SyncLogCollector = () => void;
|
||||
/**
|
||||
* Union type used in {@link (withLogCollector:3)} callback function.
|
||||
* @public */
|
||||
export type LogCollector = AsyncLogCollector | SyncLogCollector;
|
||||
/**
|
||||
* Map of severity level and corresponding log lines.
|
||||
* @public */
|
||||
export type CollectedLogs<T extends LogFuncs> = { [key in T]: string[] };
|
||||
|
||||
const allCategories = ['log', 'warn', 'error'];
|
||||
|
||||
/**
|
||||
* Asynchronous log collector with that collects all categories
|
||||
* @public */
|
||||
export function withLogCollector(
|
||||
callback: AsyncLogCollector,
|
||||
): Promise<CollectedLogs<LogFuncs>>;
|
||||
|
||||
/**
|
||||
* Synchronous log collector with that collects all categories
|
||||
* @public */
|
||||
export function withLogCollector(
|
||||
callback: SyncLogCollector,
|
||||
): CollectedLogs<LogFuncs>;
|
||||
|
||||
/**
|
||||
* Asynchronous log collector with that only collects selected categories
|
||||
* @public
|
||||
*/
|
||||
export function withLogCollector<T extends LogFuncs>(
|
||||
logsToCollect: T[],
|
||||
callback: AsyncLogCollector,
|
||||
): Promise<CollectedLogs<T>>;
|
||||
|
||||
/**
|
||||
* Synchronous log collector with that only collects selected categories
|
||||
* @public */
|
||||
export function withLogCollector<T extends LogFuncs>(
|
||||
logsToCollect: T[],
|
||||
callback: SyncLogCollector,
|
||||
): CollectedLogs<T>;
|
||||
|
||||
/**
|
||||
* Log collector that collect logs either from a sync or async collector.
|
||||
* @public
|
||||
* @deprecated import from test-utils instead
|
||||
* */
|
||||
export function withLogCollector(
|
||||
logsToCollect: LogFuncs[] | LogCollector,
|
||||
callback?: LogCollector,
|
||||
): CollectedLogs<LogFuncs> | Promise<CollectedLogs<LogFuncs>> {
|
||||
const oneArg = !callback;
|
||||
const actualCallback = (oneArg ? logsToCollect : callback) as LogCollector;
|
||||
const categories = (oneArg ? allCategories : logsToCollect) as LogFuncs[];
|
||||
|
||||
const logs = {
|
||||
log: new Array<string>(),
|
||||
warn: new Array<string>(),
|
||||
error: new Array<string>(),
|
||||
};
|
||||
|
||||
const origLog = console.log;
|
||||
const origWarn = console.warn;
|
||||
const origError = console.error;
|
||||
|
||||
if (categories.includes('log')) {
|
||||
console.log = (message: string) => {
|
||||
logs.log.push(message);
|
||||
};
|
||||
}
|
||||
if (categories.includes('warn')) {
|
||||
console.warn = (message: string) => {
|
||||
logs.warn.push(message);
|
||||
};
|
||||
}
|
||||
if (categories.includes('error')) {
|
||||
console.error = (message: string) => {
|
||||
logs.error.push(message);
|
||||
};
|
||||
}
|
||||
|
||||
const restore = () => {
|
||||
console.log = origLog;
|
||||
console.warn = origWarn;
|
||||
console.error = origError;
|
||||
};
|
||||
|
||||
try {
|
||||
const ret = actualCallback();
|
||||
|
||||
if (!ret || !ret.then) {
|
||||
restore();
|
||||
return logs;
|
||||
}
|
||||
|
||||
return ret.then(
|
||||
() => {
|
||||
restore();
|
||||
return logs;
|
||||
},
|
||||
error => {
|
||||
restore();
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
restore();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
* If there are any updates from MUI React on testing `useMediaQuery` this mock should be replaced
|
||||
* https://material-ui.com/components/use-media-query/#testing
|
||||
*
|
||||
* @param matchMediaOptions
|
||||
* @public
|
||||
*/
|
||||
export default function mockBreakpoint({ matches = false }) {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
|
||||
@@ -14,14 +14,31 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated use {@link setupRequestMockHandlers} instead which can be called directly with the worker.
|
||||
* @public
|
||||
*/
|
||||
export const msw = {
|
||||
setupDefaultHandlers: (worker: {
|
||||
listen: (t: any) => void;
|
||||
close: () => void;
|
||||
resetHandlers: () => void;
|
||||
}) => {
|
||||
beforeAll(() => worker.listen({ onUnhandledRequest: 'error' }));
|
||||
afterAll(() => worker.close());
|
||||
afterEach(() => worker.resetHandlers());
|
||||
setupRequestMockHandlers(worker);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up handlers for request mocking
|
||||
* @public
|
||||
* @param worker - service worker
|
||||
*/
|
||||
export function setupRequestMockHandlers(worker: {
|
||||
listen: (t: any) => void;
|
||||
close: () => void;
|
||||
resetHandlers: () => void;
|
||||
}) {
|
||||
beforeAll(() => worker.listen({ onUnhandledRequest: 'error' }));
|
||||
afterAll(() => worker.close());
|
||||
afterEach(() => worker.resetHandlers());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { render, RenderResult } from '@testing-library/react';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Simplifies rendering of async components in by taking care of the wrapping inside act
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async
|
||||
* act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit
|
||||
* cleaner, since act doesn't return the result of the evaluated function.
|
||||
* https://github.com/testing-library/react-testing-library/issues/281
|
||||
* https://github.com/facebook/react/pull/14853
|
||||
*/
|
||||
export async function renderWithEffects(
|
||||
nodes: ReactElement,
|
||||
): Promise<RenderResult> {
|
||||
let value: RenderResult;
|
||||
await act(async () => {
|
||||
value = render(nodes);
|
||||
});
|
||||
// @ts-ignore
|
||||
return value;
|
||||
}
|
||||
Reference in New Issue
Block a user