diff --git a/plugins/techdocs-addons-test-utils/.eslintrc.js b/plugins/techdocs-addons-test-utils/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/techdocs-addons-test-utils/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/techdocs-addons-test-utils/README.md b/plugins/techdocs-addons-test-utils/README.md new file mode 100644 index 0000000000..fbae6b381a --- /dev/null +++ b/plugins/techdocs-addons-test-utils/README.md @@ -0,0 +1,13 @@ +# techdocs-addons-test-utils + +Welcome to the techdocs-addons-test-utils plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/techdocs-addons-test-utils](http://localhost:3000/techdocs-addons-test-utils). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/techdocs-addons-test-utils/api-report.md b/plugins/techdocs-addons-test-utils/api-report.md new file mode 100644 index 0000000000..52972bb05d --- /dev/null +++ b/plugins/techdocs-addons-test-utils/api-report.md @@ -0,0 +1,48 @@ +## API Report File for "@backstage/plugin-techdocs-addons-test-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { default as React_2 } from 'react'; +import { ReactElement } from 'react'; +import { screen as screen_2 } from 'testing-library__dom'; +import { TechDocsEntityMetadata } from '@backstage/plugin-techdocs-react'; +import { TechDocsMetadata } from '@backstage/plugin-techdocs-react'; +import { TestApiProviderProps } from '@backstage/test-utils'; + +// @public (undocumented) +export type Apis = TestApiProviderProps['apis']; + +// @public (undocumented) +export type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; + +// @public (undocumented) +export class TechDocsAddonBuilder { + constructor(addons: ReactElement[]); + // (undocumented) + atPath(path: string): this; + // (undocumented) + build(): React_2.ReactElement< + any, + string | React_2.JSXElementConstructor + >; + // (undocumented) + static buildAddonsInTechDocs(addons: ReactElement[]): TechDocsAddonBuilder; + // (undocumented) + renderWithEffects(): Promise< + typeof screen_2 & { + shadowRoot: ShadowRoot | null; + } + >; + // (undocumented) + withApis(apis: Apis): this; + // (undocumented) + withDom(dom: ReactElement): this; + // (undocumented) + withEntity(entity: RecursivePartial): this; + // (undocumented) + withMetadata(metadata: RecursivePartial): this; +} +``` diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json new file mode 100644 index 0000000000..367ec5d734 --- /dev/null +++ b/plugins/techdocs-addons-test-utils/package.json @@ -0,0 +1,71 @@ +{ + "name": "@backstage/plugin-techdocs-addons-test-utils", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/techdocs-addons-test-utils" + }, + "keywords": [ + "backstage", + "techdocs" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.9.3", + "@backstage/core-app-api": "^1.0.1", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/plugin-catalog": "^1.1.0", + "@backstage/plugin-search-react": "^0.1.0", + "@backstage/plugin-techdocs": "^1.1.0", + "@backstage/plugin-techdocs-react": "^0.1.0", + "@backstage/test-utils": "^1.0.1", + "@backstage/theme": "^0.2.15", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "react-use": "^17.2.4", + "react-router-dom": "6.0.0-beta.0", + "@testing-library/react": "^12.1.3", + "testing-library__dom": "^7.29.4-beta.1" + }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.0", + "@backstage/dev-utils": "^1.0.1", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/jest": "^26.0.7", + "@types/node": "^16.11.26", + "msw": "^0.35.0", + "cross-fetch": "^3.1.5" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/techdocs-addons-test-utils/src/index.ts b/plugins/techdocs-addons-test-utils/src/index.ts new file mode 100644 index 0000000000..2e4348fe2a --- /dev/null +++ b/plugins/techdocs-addons-test-utils/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * A package containing test utilities for TechDocs Addons. + * + * @packageDocumentation + */ + +export * from './test-utils'; diff --git a/plugins/techdocs-addons-test-utils/src/setupTests.ts b/plugins/techdocs-addons-test-utils/src/setupTests.ts new file mode 100644 index 0000000000..9bb3e72355 --- /dev/null +++ b/plugins/techdocs-addons-test-utils/src/setupTests.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. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/techdocs-addons-test-utils/src/test-utils.tsx b/plugins/techdocs-addons-test-utils/src/test-utils.tsx new file mode 100644 index 0000000000..17c25ed939 --- /dev/null +++ b/plugins/techdocs-addons-test-utils/src/test-utils.tsx @@ -0,0 +1,240 @@ +/* + * 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 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 } from 'react-router-dom'; +import { act, render } from '@testing-library/react'; + +import { + wrapInTestApp, + TestApiProvider, + TestApiProviderProps, +} from '@backstage/test-utils'; +import { FlatRoutes } from '@backstage/core-app-api'; + +import { + TechDocsAddons, + techdocsApiRef, + TechDocsEntityMetadata, + TechDocsMetadata, + techdocsStorageApiRef, +} from '@backstage/plugin-techdocs-react'; +import { TechDocsReaderPage, techdocsPlugin } from '@backstage/plugin-techdocs'; +import { catalogPlugin } from '@backstage/plugin-catalog'; +import { searchApiRef } from '@backstage/plugin-search-react'; + +/** + * @public + */ +export type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; + +const techdocsApi = { + getTechDocsMetadata: jest.fn(), + getEntityMetadata: jest.fn(), +}; + +const techdocsStorageApi = { + getApiOrigin: jest.fn(), + getEntityDocs: jest.fn(), + syncEntityDocs: jest.fn(), +}; + +const searchApi = { + query: jest.fn().mockResolvedValue({ results: [] }), +}; + +/** + * @public + */ +export type Apis = TestApiProviderProps['apis']; + +type TechDocsAddonBuilderOptions = { + dom: ReactElement; + entity: RecursivePartial; + metadata: RecursivePartial; + componentId: string; + apis: Apis; + path: string; +}; + +const defaultOptions: TechDocsAddonBuilderOptions = { + 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 = ( + + + +
+
+
+
+
+ + +); + +/** + * @public + */ + +export class TechDocsAddonBuilder { + private options: TechDocsAddonBuilderOptions = 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 = [ + [techdocsApiRef, techdocsApi], + [techdocsStorageApiRef, techdocsStorageApi], + [searchApiRef, searchApi], + ...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, + }; + + techdocsApi.getTechDocsMetadata.mockReturnValue( + this.options.metadata || { ...defaultMetadata }, + ); + techdocsApi.getEntityMetadata.mockResolvedValue( + this.options.entity || { ...defaultEntity }, + ); + + techdocsStorageApi.syncEntityDocs.mockResolvedValue('cached'); + techdocsStorageApi.getApiOrigin.mockResolvedValue( + 'https://backstage.example.com/api/techdocs', + ); + + techdocsStorageApi.getEntityDocs.mockResolvedValue( + renderToStaticMarkup(this.options.dom || defaultDom), + ); + + const TechDocsAddonsPage = () => { + return ( + + + } + > + + {this.addons.map((addon, index) => ( + {addon} + ))} + + + + + ); + }; + + return wrapInTestApp(, { + routeEntries: [ + `/docs/${entityName.namespace}/${entityName.kind}/${entityName.name}/${this.options.path}`, + ], + mountedRoutes: { + '/docs': techdocsPlugin.routes.root, + '/docs/:namespace/:kind/:name/*': techdocsPlugin.routes.docRoot, + '/catalog/:namespace/:kind/:name': catalogPlugin.routes.catalogEntity, + }, + }); + } + + // 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< + typeof screen & { shadowRoot: ShadowRoot | null } + > { + await act(async () => { + render(this.build()); + }); + + const shadowHost = screen.getByTestId('techdocs-native-shadowroot'); + + return { + ...screen, + shadowRoot: shadowHost?.shadowRoot || null, + }; + } +} + +export default TechDocsAddonBuilder.buildAddonsInTechDocs; diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 13bd654b4d..0c60b51511 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -25,6 +25,7 @@ import { createApiRef } from '@backstage/core-plugin-api'; * Utility API reference for the {@link TechDocsStorageApi}. * * @public + * @deprecated Import from `@backstage/plugin-techdocs-react` instead */ export const techdocsStorageApiRef = createApiRef({ id: 'plugin.techdocs.storageservice', @@ -44,6 +45,7 @@ export const techdocsApiRef = createApiRef({ * The outcome of a docs sync operation. * * @public + * @deprecated Import from `@backstage/plugin-techdocs-react` instead */ export type SyncResult = 'cached' | 'updated'; @@ -51,6 +53,7 @@ export type SyncResult = 'cached' | 'updated'; * API which talks to TechDocs storage to fetch files to render. * * @public + * @deprecated Import from `@backstage/plugin-techdocs-react` instead */ export interface TechDocsStorageApi { getApiOrigin(): Promise; diff --git a/yarn.lock b/yarn.lock index 728e88bf33..74efe55b34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23940,6 +23940,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"