diff --git a/plugins/techdocs-addons/package.json b/plugins/techdocs-addons/package.json index 1ff346442f..3e441eb56e 100644 --- a/plugins/techdocs-addons/package.json +++ b/plugins/techdocs-addons/package.json @@ -25,13 +25,18 @@ "@backstage/catalog-model": "^0.13.0", "@backstage/core-components": "^0.9.1", "@backstage/core-plugin-api": "^0.8.0", + "@backstage/test-utils": "^0.3.0", "@material-ui/core": "^4.12.2", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.11.0", "jss": "~10.8.2", + "lodash.debounce": "^4.0.8", + "react-dom": "^17.0.2", "react-helmet": "6.1.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^17.2.4" + "react-use": "^17.2.4", + "testing-library__dom": "^7.29.4-beta.1", + "@testing-library/react": "^12.1.3" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", @@ -39,7 +44,8 @@ }, "devDependencies": { "@testing-library/react-hooks": "^7.0.2", - "@backstage/test-utils": "^0.3.0" + "@testing-library/jest-dom": "^5.10.1", + "@types/lodash.debounce": "^4.0.6" }, "files": [ "dist" diff --git a/plugins/techdocs-addons/src/test-utils/index.ts b/plugins/techdocs-addons/src/test-utils/index.ts new file mode 100644 index 0000000000..3bcf5f5ab8 --- /dev/null +++ b/plugins/techdocs-addons/src/test-utils/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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. + */ + +export * from './test-utils'; diff --git a/plugins/techdocs-addons/src/test-utils/mocks.ts b/plugins/techdocs-addons/src/test-utils/mocks.ts new file mode 100644 index 0000000000..fd4665237c --- /dev/null +++ b/plugins/techdocs-addons/src/test-utils/mocks.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2022 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. + */ + +export const useTechDocsReaderDom = jest.fn(); +export const useParams = jest.fn(); +jest.mock('@backstage/plugin-techdocs', () => ({ + ...(jest.requireActual('@backstage/plugin-techdocs') as {}), + useTechDocsReaderDom, + withTechDocsReaderProvider: jest.fn(x => x), + TechDocsStateIndicator: jest.fn(() => null), +})); +// todo(backstage/techdocs-core): Use core test-utils' `routeEntries` option. +jest.mock('react-router', () => ({ + ...(jest.requireActual('react-router') as {}), + useParams, +})); diff --git a/plugins/techdocs-addons/src/test-utils/test-utils.tsx b/plugins/techdocs-addons/src/test-utils/test-utils.tsx new file mode 100644 index 0000000000..4362bb8cf7 --- /dev/null +++ b/plugins/techdocs-addons/src/test-utils/test-utils.tsx @@ -0,0 +1,219 @@ +/* + * Copyright 2022 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 order matters for jest manual mocks! import this first. +import { useTechDocsReaderDom, useParams } from './mocks'; + +import React, { ReactElement, Fragment } from 'react'; + +// Shadow DOM support for the simple and complete DOM testing utilities +// https://github.com/testing-library/dom-testing-library/issues/742#issuecomment-674987855 +import { screen } from 'testing-library__dom'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { Route, Routes } from 'react-router-dom'; +import { act, render } from '@testing-library/react'; +import { AsyncState } from 'react-use/lib/useAsyncFn'; + +import { + wrapInTestApp, + TestApiProvider, + TestApiProviderProps, +} from '@backstage/test-utils'; + +import { TechDocsEntityMetadata, TechDocsMetadata } from '../types'; +import { TechDocsReaderPage, TechDocsAddons } from '..'; + +type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; + +type Apis = TestApiProviderProps['apis']; + +export type TechDocsAddonsBuilder = { + dom: ReactElement; + entity: RecursivePartial; + metadata: RecursivePartial; + componentId: string; + apis: Apis; + path: string; +}; + +const defaultOptions: TechDocsAddonsBuilder = { + dom: <>, + entity: {}, + metadata: {}, + componentId: 'docs', + apis: [], + path: '', +}; + +const defaultMetadata = { + site_name: 'Tech Docs', + site_description: 'Tech Docs', +}; + +const defaultEntity = { + kind: 'Component', + metadata: { namespace: 'default', name: 'docs' }, +}; + +const defaultDom = ( + + + +
+
+
+
+
+ + +); + +export class TechDocsAddonBuilder { + private options: TechDocsAddonsBuilder = defaultOptions; + private addons: ReactElement[]; + + static buildAddonsInTechDocs(addons: ReactElement[]) { + return new TechDocsAddonBuilder(addons); + } + + constructor(addons: ReactElement[]) { + this.addons = addons; + } + + withApis(apis: Apis) { + const refs = apis.map(([ref]) => ref); + this.options.apis = this.options.apis + .filter(([ref]) => !refs.includes(ref)) + .concat(apis); + return this; + } + + withDom(dom: ReactElement) { + this.options.dom = dom; + return this; + } + + withMetadata(metadata: RecursivePartial) { + this.options.metadata = metadata; + return this; + } + + withEntity(entity: RecursivePartial) { + this.options.entity = entity; + return this; + } + + atPath(path: string) { + this.options.path = path; + return this; + } + + build() { + const apis = [...this.options.apis]; + const entityName = { + namespace: + this.options.entity?.metadata?.namespace || + defaultEntity.metadata.namespace, + kind: this.options.entity?.kind || defaultEntity.kind, + name: this.options.entity?.metadata?.name || defaultEntity.metadata.name, + }; + + const techDocsMetadata: AsyncState = { + loading: false, + error: undefined, + value: (this.options.metadata || { + ...defaultMetadata, + }) as TechDocsMetadata, + }; + + const entityMetadata: AsyncState = { + loading: false, + error: undefined, + value: (this.options.entity || { + ...defaultEntity, + }) as TechDocsEntityMetadata, + }; + + const dom = document.createElement('html'); + dom.innerHTML = renderToStaticMarkup(this.options.dom || defaultDom); + useTechDocsReaderDom.mockReturnValue(dom); + // todo(backstage/techdocs-core): Use core test-utils' `routeEntries` option to mock + // the current path. We use jest mocks instead for now because of a bug in + // react-router that prevents '*' params from being mocked. + useParams.mockReturnValue({ + ...entityName, + '*': this.options.path, + }); + + return wrapInTestApp( + + + + } + > + + {this.addons.map((addon, index) => ( + {addon} + ))} + + + + , + ); + } + + render(): typeof screen & { shadowRoot: ShadowRoot | null } { + render(this.build()); + + const shadowHost = screen.getByTestId('techdocs-native-shadowroot'); + + return { + ...screen, + shadowRoot: shadowHost?.shadowRoot, + }; + } + + // 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 + async renderWithEffects(): Promise< + ReturnType + > { + await act(async () => { + this.render(); + }); + + const shadowHost = screen.getByTestId('techdocs-native-shadowroot'); + + return { + ...screen, + shadowRoot: shadowHost?.shadowRoot, + }; + } +} + +export default TechDocsAddonBuilder.buildAddonsInTechDocs; diff --git a/yarn.lock b/yarn.lock index 03de40b659..90e3ad3e7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6555,6 +6555,18 @@ dependencies: "@types/node" "*" +"@types/lodash.debounce@^4.0.6": + version "4.0.6" + resolved "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60" + integrity sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.180" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" + integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g== + "@types/lodash@^4.14.151", "@types/lodash@^4.14.173", "@types/lodash@^4.14.175": version "4.14.178" resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" @@ -24094,6 +24106,11 @@ testcontainers@^8.1.2: ssh-remote-port-forward "^1.0.4" tar-fs "^2.1.1" +testing-library__dom@^7.29.4-beta.1: + version "7.29.4-beta.1" + resolved "https://registry.npmjs.org/testing-library__dom/-/testing-library__dom-7.29.4-beta.1.tgz#dc755f485837e923efbe12c1b7ae43b0ed326f96" + integrity sha512-vb/SMg8rXYcYYFY2eQ2n2a0p2VWNAseM4WHLfsckIyLwxRz5fYqKysUzUYpAX8SwRfruRK+tZqLuL4ND+D1s7Q== + text-extensions@^1.0.0: version "1.9.0" resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"