From c01fe7964293fd1e41ed0ff67556b0f4a18d3e40 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 8 Jun 2020 17:47:07 +0200 Subject: [PATCH 1/2] packages/test-utils: added MockErrorApi --- .../apis/ErrorApi/MockErrorApi.test.ts | 104 ++++++++++++++++++ .../testUtils/apis/ErrorApi/MockErrorApi.ts | 91 +++++++++++++++ .../src/testUtils/apis/ErrorApi/index.ts | 17 +++ .../test-utils/src/testUtils/apis/index.ts | 17 +++ packages/test-utils/src/testUtils/index.tsx | 1 + 5 files changed, 230 insertions(+) create mode 100644 packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.test.ts create mode 100644 packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts create mode 100644 packages/test-utils/src/testUtils/apis/ErrorApi/index.ts create mode 100644 packages/test-utils/src/testUtils/apis/index.ts diff --git a/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.test.ts b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.test.ts new file mode 100644 index 0000000000..6a798b32b0 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { MockErrorApi } from './MockErrorApi'; + +async function ifResolved(promise: Promise): Promise { + return Promise.race([promise, Promise.resolve<'not-yet'>('not-yet')]); +} + +describe('MockErrorApi', () => { + it('should throw errors by default', () => { + const api = new MockErrorApi(); + expect(() => api.post(new Error('NOPE'))).toThrow( + 'MockErrorApi received unexpected error, Error: NOPE', + ); + }); + + it('should collect errors', () => { + const api = new MockErrorApi({ collect: true }); + + api.post(new Error('e1')); + api.post(new Error('e2'), { hidden: true }); + api.post(new Error('e3')); + + expect(api.getErrors()).toEqual([ + { + error: new Error('e1'), + }, + { + error: new Error('e2'), + context: { hidden: true }, + }, + { + error: new Error('e3'), + }, + ]); + }); + + it('should not emit values', async () => { + const api = new MockErrorApi({ collect: true }); + + const promise = new Promise((resolve, reject) => { + api.error$().subscribe({ + next({ error }) { + reject(error); + }, + error(error) { + reject(error); + }, + complete() { + reject(new Error('observable was completed')); + }, + }); + + setTimeout(() => resolve('timed-out'), 100); + }); + + await expect(promise).resolves.toBe('timed-out'); + }); + + it('should wait for errors', async () => { + const api = new MockErrorApi({ collect: true }); + + const wait1 = api.waitForError(/1/); + const wait2 = api.waitForError(/2/); + + await expect(ifResolved(wait1)).resolves.toBe('not-yet'); + await expect(ifResolved(wait2)).resolves.toBe('not-yet'); + api.post(new Error('e0')); + await expect(ifResolved(wait1)).resolves.toBe('not-yet'); + await expect(ifResolved(wait2)).resolves.toBe('not-yet'); + api.post(new Error('e1')); + await expect(ifResolved(wait1)).resolves.toEqual({ + error: new Error('e1'), + }); + await expect(ifResolved(wait2)).resolves.toBe('not-yet'); + api.post(new Error('e2'), { hidden: true }); + await expect(ifResolved(wait2)).resolves.toEqual({ + error: new Error('e2'), + context: { hidden: true }, + }); + }); + + it('should time out waiting for error', async () => { + const api = new MockErrorApi({ collect: true }); + + await expect(api.waitForError(/1/, 1)).rejects.toThrow( + 'Timed out waiting for error', + ); + }); +}); diff --git a/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts new file mode 100644 index 0000000000..9b22a352e8 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/ErrorApi/MockErrorApi.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + ErrorApi, + ErrorContext, + errorApiRef, + Observable, +} from '@backstage/core-api'; + +type Options = { + collect?: boolean; +}; + +type ErrorWithContext = { + error: Error; + context?: ErrorContext; +}; + +type Waiter = { + pattern: RegExp; + resolve: (err: ErrorWithContext) => void; +}; + +const nullObservable = { + subscribe: () => ({ unsubscribe: () => {}, closed: true }), +}; + +export class MockErrorApi implements ErrorApi { + static factory = { + implements: errorApiRef, + deps: {}, + factory: () => new MockErrorApi(), + }; + + private readonly errors = new Array(); + private readonly waiters = new Set(); + + constructor(private readonly options: Options = {}) {} + + post(error: Error, context?: ErrorContext) { + if (this.options.collect) { + this.errors.push({ error, context }); + + for (const waiter of this.waiters) { + if (waiter.pattern.test(error.message)) { + this.waiters.delete(waiter); + waiter.resolve({ error, context }); + } + } + + return; + } + + throw new Error(`MockErrorApi received unexpected error, ${error}`); + } + + error$(): Observable<{ error: Error; context?: ErrorContext }> { + return nullObservable; + } + + getErrors(): ErrorWithContext[] { + return this.errors; + } + + waitForError( + pattern: RegExp, + timeoutMs: number = 2000, + ): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error('Timed out waiting for error')); + }, timeoutMs); + + this.waiters.add({ resolve, pattern }); + }); + } +} diff --git a/packages/test-utils/src/testUtils/apis/ErrorApi/index.ts b/packages/test-utils/src/testUtils/apis/ErrorApi/index.ts new file mode 100644 index 0000000000..d2f2b820c2 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/ErrorApi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export { MockErrorApi } from './MockErrorApi'; diff --git a/packages/test-utils/src/testUtils/apis/index.ts b/packages/test-utils/src/testUtils/apis/index.ts new file mode 100644 index 0000000000..55d6d10dd6 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export * from './ErrorApi'; diff --git a/packages/test-utils/src/testUtils/index.tsx b/packages/test-utils/src/testUtils/index.tsx index 302bdb405f..4c5eb642bd 100644 --- a/packages/test-utils/src/testUtils/index.tsx +++ b/packages/test-utils/src/testUtils/index.tsx @@ -14,5 +14,6 @@ * limitations under the License. */ +export * from './apis'; export { default as mockBreakpoint } from './mockBreakpoint'; export { wrapInTestApp, renderInTestApp } from './appWrappers'; From b5b95138f455b4f848ff1e8e64ae002b58c8c238 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 8 Jun 2020 17:56:51 +0200 Subject: [PATCH 2/2] packages/test-utils: initial test registry with ErrorApi mock + tests --- .../src/testUtils/appWrappers.test.tsx | 49 ++++++++++++++++++- .../test-utils/src/testUtils/appWrappers.tsx | 5 +- .../src/testUtils/mockApiRegistry.ts | 26 ++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 packages/test-utils/src/testUtils/mockApiRegistry.ts diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index a30e8f9823..34a20be63b 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -14,11 +14,18 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React, { FC, useEffect } from 'react'; import { render } from '@testing-library/react'; import { wrapInTestApp, renderInTestApp } from './appWrappers'; import { Route } from 'react-router'; import { withLogCollector } from '@backstage/test-utils-core'; +import { + useApi, + errorApiRef, + ApiProvider, + ApiRegistry, +} from '@backstage/core-api'; +import { MockErrorApi } from './apis'; describe('wrapInTestApp', () => { it('should provide routing and warn about missing act()', async () => { @@ -69,4 +76,44 @@ describe('wrapInTestApp', () => { const rendered = await renderInTestApp(); expect(rendered.getByText('foo')).toBeInTheDocument(); }); + + it('should provide mock API implementations', async () => { + const A: FC<{}> = () => { + const errorApi = useApi(errorApiRef); + errorApi.post(new Error('NOPE')); + return null; + }; + + const { error } = await withLogCollector(['error'], async () => { + await expect(renderInTestApp(A)).rejects.toThrow('NOPE'); + }); + + expect(error).toEqual([ + expect.stringMatching( + /^Error: Uncaught \[Error: MockErrorApi received unexpected error, Error: NOPE\]/, + ), + expect.stringMatching(/^The above error occurred in the component:/), + ]); + }); + + it('should allow custom API implementations', async () => { + const mockErrorApi = new MockErrorApi({ collect: true }); + + const A: FC<{}> = () => { + const errorApi = useApi(errorApiRef); + useEffect(() => { + errorApi.post(new Error('NOPE')); + }, [errorApi]); + return

foo

; + }; + + const rendered = await renderInTestApp( + +
+ , + ); + + expect(rendered.getByText('foo')).toBeInTheDocument(); + expect(mockErrorApi.getErrors()).toEqual([{ error: new Error('NOPE') }]); + }); }); diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 6d48082b7c..89e68c143a 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -20,11 +20,11 @@ import { Route } from 'react-router-dom'; import { lightTheme } from '@backstage/theme'; import privateExports, { defaultSystemIcons, - ApiTestRegistry, BootErrorPageProps, } from '@backstage/core-api'; import { RenderResult } from '@testing-library/react'; import { renderWithEffects } from '@backstage/test-utils-core'; +import { createMockApiRegistry } from './mockApiRegistry'; const { PrivateAppImpl } = privateExports; const NotFoundErrorPage = () => { @@ -57,9 +57,10 @@ export function wrapInTestApp( options: TestAppOptions = {}, ): ReactElement { const { routeEntries = ['/'] } = options; + const apis = createMockApiRegistry(); const app = new PrivateAppImpl({ - apis: new ApiTestRegistry(), + apis, components: { NotFoundErrorPage, BootErrorPage, diff --git a/packages/test-utils/src/testUtils/mockApiRegistry.ts b/packages/test-utils/src/testUtils/mockApiRegistry.ts new file mode 100644 index 0000000000..96d12a9df2 --- /dev/null +++ b/packages/test-utils/src/testUtils/mockApiRegistry.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { ApiTestRegistry } from '@backstage/core-api'; +import { MockErrorApi } from './apis'; + +export function createMockApiRegistry(): ApiTestRegistry { + const registry = new ApiTestRegistry(); + + registry.register(MockErrorApi.factory); + + return registry; +}