Merge pull request #8509 from cmpadden/remove-deprecations-release-2021-11-11
Remove Deprecations (release-2021-11-11)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/test-utils': minor
|
||||
---
|
||||
|
||||
Removed deprecated `Keyboard` class which has been superseded by `@testing-library/user-event#userEvent`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/config-loader': minor
|
||||
---
|
||||
|
||||
Removed deprecated option `configPaths` as it has been superseded by `configTargets`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': minor
|
||||
---
|
||||
|
||||
Removed deprecated types `ApiRefType` and `ApiRefsToTypes`
|
||||
@@ -202,7 +202,6 @@ export async function loadBackendConfig(options: {
|
||||
const config = new ObservableConfigProxy(options.logger);
|
||||
const { appConfigs } = await loadConfig({
|
||||
configRoot: paths.targetRoot,
|
||||
configPaths: [],
|
||||
configTargets: configTargets,
|
||||
watch: {
|
||||
onChange(newConfigs) {
|
||||
|
||||
@@ -59,7 +59,6 @@ export async function loadCliConfig(options: Options) {
|
||||
? async name => process.env[name] || 'x'
|
||||
: undefined,
|
||||
configRoot: paths.targetRoot,
|
||||
configPaths: [],
|
||||
configTargets: configTargets,
|
||||
});
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ export function loadConfig(
|
||||
// @public
|
||||
export type LoadConfigOptions = {
|
||||
configRoot: string;
|
||||
configPaths: string[];
|
||||
configTargets: ConfigTarget[];
|
||||
env?: string;
|
||||
experimentalEnvFunc?: (name: string) => Promise<string | undefined>;
|
||||
@@ -103,8 +102,4 @@ export type TransformFunc<T extends number | string | boolean> = (
|
||||
visibility: ConfigVisibility;
|
||||
},
|
||||
) => T | undefined;
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/loader.d.ts:33:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/config-loader" does not have an export "configTargets"
|
||||
```
|
||||
|
||||
@@ -118,7 +118,6 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [],
|
||||
env: 'production',
|
||||
}),
|
||||
@@ -146,7 +145,6 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [{ url: configUrl }],
|
||||
env: 'production',
|
||||
remote: {
|
||||
@@ -173,8 +171,10 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: ['/root/app-config2.yaml'],
|
||||
configTargets: [{ path: '/root/app-config.yaml' }],
|
||||
configTargets: [
|
||||
{ path: '/root/app-config.yaml' },
|
||||
{ path: '/root/app-config2.yaml' },
|
||||
],
|
||||
env: 'production',
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
@@ -207,7 +207,6 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: ['/root/app-config.yaml'],
|
||||
configTargets: [{ path: '/root/app-config.yaml' }],
|
||||
env: 'production',
|
||||
}),
|
||||
@@ -231,7 +230,6 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [
|
||||
{ path: '/root/app-config.yaml' },
|
||||
{ path: '/root/app-config.development.yaml' },
|
||||
@@ -274,7 +272,6 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [{ path: '/root/app-config.substitute.yaml' }],
|
||||
env: 'development',
|
||||
}),
|
||||
@@ -302,7 +299,6 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [],
|
||||
watch: {
|
||||
onChange: onChange.resolve,
|
||||
@@ -353,7 +349,6 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [{ url: configUrl }],
|
||||
watch: {
|
||||
onChange: onChange.resolve,
|
||||
@@ -401,7 +396,6 @@ describe('loadConfig', () => {
|
||||
|
||||
await loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [],
|
||||
watch: {
|
||||
onChange: () => {
|
||||
|
||||
@@ -59,11 +59,6 @@ export type LoadConfigOptions = {
|
||||
// The root directory of the config loading context. Used to find default configs.
|
||||
configRoot: string;
|
||||
|
||||
/** Absolute paths to load config files from. Configs from earlier paths have lower priority.
|
||||
* @deprecated Use {@link configTargets} instead.
|
||||
*/
|
||||
configPaths: string[];
|
||||
|
||||
// Paths to load config files from. Configs from earlier paths have lower priority.
|
||||
configTargets: ConfigTarget[];
|
||||
|
||||
@@ -114,13 +109,6 @@ export async function loadConfig(
|
||||
.filter((e): e is { path: string } => e.hasOwnProperty('path'))
|
||||
.map(configTarget => configTarget.path);
|
||||
|
||||
// Append deprecated configPaths to the absolute config paths received via configTargets.
|
||||
options.configPaths.forEach(cp => {
|
||||
if (!configPaths.includes(cp)) {
|
||||
configPaths.push(cp);
|
||||
}
|
||||
});
|
||||
|
||||
const configUrls: string[] = options.configTargets
|
||||
.slice()
|
||||
.filter((e): e is { url: string } => e.hasOwnProperty('url'))
|
||||
|
||||
@@ -144,18 +144,6 @@ export type ApiRefConfig = {
|
||||
description?: string;
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
export type ApiRefsToTypes<
|
||||
T extends {
|
||||
[key in string]: ApiRef<unknown>;
|
||||
},
|
||||
> = {
|
||||
[key in keyof T]: ApiRefType<T[key]>;
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
export type ApiRefType<T> = T extends ApiRef<infer U> ? U : never;
|
||||
|
||||
// @public
|
||||
export type AppComponents = {
|
||||
NotFoundErrorPage: ComponentType<{}>;
|
||||
|
||||
@@ -31,33 +31,13 @@ export type ApiRef<T> = {
|
||||
*/
|
||||
export type AnyApiRef = ApiRef<unknown>;
|
||||
|
||||
/**
|
||||
* Transforms ApiRef type into its inner API type.
|
||||
*
|
||||
* @public
|
||||
* @deprecated unused type.
|
||||
*/
|
||||
export type ApiRefType<T> = T extends ApiRef<infer U> ? U : never;
|
||||
|
||||
/**
|
||||
* Wraps a type with API properties into a type holding their respective {@link ApiRef}s.
|
||||
* Reverse type transform of {@link ApiRefsToTypes}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TypesToApiRefs<T> = { [key in keyof T]: ApiRef<T[key]> };
|
||||
|
||||
/**
|
||||
* Unwraps type with {@link ApiRef} properties into a type holding their respective API types.
|
||||
* Reverse type transform of {@link TypesToApiRefs}.
|
||||
*
|
||||
* @public
|
||||
* @deprecated unused type.
|
||||
*/
|
||||
export type ApiRefsToTypes<T extends { [key in string]: ApiRef<unknown> }> = {
|
||||
[key in keyof T]: ApiRefType<T[key]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides lookup of APIs through their {@link ApiRef}s.
|
||||
*
|
||||
|
||||
@@ -23,17 +23,6 @@ import {
|
||||
} from './types';
|
||||
import { OldIconComponent } from '../icons/types';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @internal
|
||||
*/
|
||||
export type RouteRefConfig<Params extends AnyParams> = {
|
||||
params?: ParamKeys<Params>;
|
||||
path?: string;
|
||||
icon?: OldIconComponent;
|
||||
title: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
||||
@@ -142,23 +142,6 @@ export type AnyRouteRef =
|
||||
| SubRouteRef<any>
|
||||
| ExternalRouteRef<any, any>;
|
||||
|
||||
// TODO(Rugvip): None of these should be found in the wild anymore, remove in next minor release
|
||||
/**
|
||||
* @deprecated
|
||||
* @internal
|
||||
*/
|
||||
export type ConcreteRoute = {};
|
||||
/**
|
||||
* @deprecated
|
||||
* @internal
|
||||
*/
|
||||
export type AbsoluteRouteRef = RouteRef<{}>;
|
||||
/**
|
||||
* @deprecated
|
||||
* @internal
|
||||
*/
|
||||
export type MutableRouteRef = RouteRef<{}>;
|
||||
|
||||
/**
|
||||
* A duplicate of the react-router RouteObject, but with routeRef added
|
||||
* @internal
|
||||
|
||||
@@ -34,52 +34,6 @@ export type ErrorWithContext = {
|
||||
context?: ErrorApiErrorContext;
|
||||
};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export class Keyboard {
|
||||
constructor(
|
||||
target: any,
|
||||
{
|
||||
debug,
|
||||
}?: {
|
||||
debug?: boolean | undefined;
|
||||
},
|
||||
);
|
||||
// (undocumented)
|
||||
click(): Promise<void>;
|
||||
// (undocumented)
|
||||
debug: boolean;
|
||||
// (undocumented)
|
||||
document: any;
|
||||
// (undocumented)
|
||||
enter(value: any): Promise<void>;
|
||||
// (undocumented)
|
||||
escape(): Promise<void>;
|
||||
// (undocumented)
|
||||
get focused(): any;
|
||||
// (undocumented)
|
||||
static fromReadableInput(input: any): any;
|
||||
// (undocumented)
|
||||
_log(message: any, ...args: any[]): void;
|
||||
// (undocumented)
|
||||
_pretty(element: any): string;
|
||||
// (undocumented)
|
||||
send(chars: any): Promise<void>;
|
||||
// (undocumented)
|
||||
_sendKey(key: any, charCode: any, action: any): Promise<void>;
|
||||
// (undocumented)
|
||||
tab(): Promise<void>;
|
||||
// (undocumented)
|
||||
static toReadableInput(chars: any): any;
|
||||
// (undocumented)
|
||||
toString(): string;
|
||||
// (undocumented)
|
||||
static type(target: any, input: any): Promise<void>;
|
||||
// (undocumented)
|
||||
type(input: any): Promise<void>;
|
||||
// (undocumented)
|
||||
static typeDebug(target: any, input: any): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type LogCollector = AsyncLogCollector | SyncLogCollector;
|
||||
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
/*
|
||||
* 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 superseded by {@link @testing-library/user-event#userEvent}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* 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']);
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,6 @@ 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';
|
||||
export { TestApiProvider, TestApiRegistry } from './TestApiProvider';
|
||||
|
||||
Reference in New Issue
Block a user