Merge pull request #11073 from realandersn/techdocs/addon-test-utils
[Techdocs] Addons test utilities
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@techdocs/cli': patch
|
||||
---
|
||||
|
||||
The TechDocs CLI's embedded app now imports all API refs from the `@backstage/plugin-techdocs-react` package.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-react': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
The `TechDocsStorageApi` and its associated ref are now exported by `@backstage/plugin-techdocs-react`. The API interface, ref, and types are now deprecated in `@backstage/plugin-techdocs` and will be removed in a future release.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-addons-test-utils': minor
|
||||
---
|
||||
|
||||
Introducing a package with utilities to help test TechDocs Addons.
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
techdocsApiRef,
|
||||
TechDocsStorageApi,
|
||||
techdocsStorageApiRef,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
|
||||
// TODO: Export type from plugin-techdocs and import this here
|
||||
// import { ParsedEntityId } from '@backstage/plugin-techdocs'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,3 @@
|
||||
# techdocs-addons-test-utils
|
||||
|
||||
A package containing utilities for testing TechDocs Addons.
|
||||
@@ -0,0 +1,39 @@
|
||||
## 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 { ApiRef } from '@backstage/core-plugin-api';
|
||||
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';
|
||||
|
||||
// @public (undocumented)
|
||||
export class TechDocsAddonTester {
|
||||
// (undocumented)
|
||||
atPath(path: string): this;
|
||||
// (undocumented)
|
||||
build(): React_2.ReactElement<
|
||||
any,
|
||||
string | React_2.JSXElementConstructor<any>
|
||||
>;
|
||||
// (undocumented)
|
||||
static buildAddonsInTechDocs(addons: ReactElement[]): TechDocsAddonTester;
|
||||
// (undocumented)
|
||||
renderWithEffects(): Promise<
|
||||
typeof screen_2 & {
|
||||
shadowRoot: ShadowRoot | null;
|
||||
}
|
||||
>;
|
||||
// (undocumented)
|
||||
withApis<T>(apis: TechdocsAddonTesterApis<T>): this;
|
||||
// (undocumented)
|
||||
withDom(dom: ReactElement): this;
|
||||
// (undocumented)
|
||||
withEntity(entity: Partial<TechDocsEntityMetadata>): this;
|
||||
// (undocumented)
|
||||
withMetadata(metadata: Partial<TechDocsMetadata>): this;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"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/integration-react": "^1.1.0-next.0",
|
||||
"@backstage/plugin-catalog": "^1.2.0-next.0",
|
||||
"@backstage/plugin-search-react": "^0.1.1-next.0",
|
||||
"@backstage/plugin-techdocs": "^1.1.1-next.0",
|
||||
"@backstage/plugin-techdocs-react": "^0.1.1-next.0",
|
||||
"@backstage/test-utils": "^1.0.2-next.0",
|
||||
"@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.1-next.0",
|
||||
"@backstage/dev-utils": "^1.0.2-next.0",
|
||||
"@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"
|
||||
]
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* 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 } from '@backstage/test-utils';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
import { ApiRef } from '@backstage/core-plugin-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';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
|
||||
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: [] }),
|
||||
};
|
||||
|
||||
const scmIntegrationsApi = {
|
||||
fromConfig: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
/** @ignore */
|
||||
type TechDocsAddonTesterTestApiPair<TApi> = TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>]
|
||||
: never;
|
||||
|
||||
/** @ignore */
|
||||
type TechdocsAddonTesterApis<T> = TechDocsAddonTesterTestApiPair<T>[];
|
||||
|
||||
type TechDocsAddonTesterOptions = {
|
||||
dom: ReactElement;
|
||||
entity: Partial<TechDocsEntityMetadata>;
|
||||
metadata: Partial<TechDocsMetadata>;
|
||||
componentId: string;
|
||||
apis: TechdocsAddonTesterApis<any>;
|
||||
path: string;
|
||||
};
|
||||
|
||||
const defaultOptions: TechDocsAddonTesterOptions = {
|
||||
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 = (
|
||||
<html lang="en">
|
||||
<head />
|
||||
<body>
|
||||
<div data-md-component="container">
|
||||
<div data-md-component="navigation" />
|
||||
<div data-md-component="toc" />
|
||||
<div data-md-component="main" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
||||
export class TechDocsAddonTester {
|
||||
private options: TechDocsAddonTesterOptions = defaultOptions;
|
||||
private addons: ReactElement[];
|
||||
|
||||
static buildAddonsInTechDocs(addons: ReactElement[]) {
|
||||
return new TechDocsAddonTester(addons);
|
||||
}
|
||||
|
||||
private constructor(addons: ReactElement[]) {
|
||||
this.addons = addons;
|
||||
}
|
||||
|
||||
withApis<T>(apis: TechdocsAddonTesterApis<T>) {
|
||||
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: Partial<TechDocsMetadata>) {
|
||||
this.options.metadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
withEntity(entity: Partial<TechDocsEntityMetadata>) {
|
||||
this.options.entity = entity;
|
||||
return this;
|
||||
}
|
||||
|
||||
atPath(path: string) {
|
||||
this.options.path = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
const apis: TechdocsAddonTesterApis<any> = [
|
||||
[techdocsApiRef, techdocsApi],
|
||||
[techdocsStorageApiRef, techdocsStorageApi],
|
||||
[searchApiRef, searchApi],
|
||||
[scmIntegrationsApiRef, scmIntegrationsApi],
|
||||
...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 (
|
||||
<TestApiProvider apis={apis}>
|
||||
<FlatRoutes>
|
||||
<Route
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={<TechDocsReaderPage />}
|
||||
>
|
||||
<TechDocsAddons>
|
||||
{this.addons.map((addon, index) => (
|
||||
<Fragment key={index}>{addon}</Fragment>
|
||||
))}
|
||||
</TechDocsAddons>
|
||||
</Route>
|
||||
</FlatRoutes>
|
||||
</TestApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
return wrapInTestApp(<TechDocsAddonsPage />, {
|
||||
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 TechDocsAddonTester.buildAddonsInTechDocs;
|
||||
@@ -53,6 +53,7 @@
|
||||
"@backstage/cli": "^0.17.1-next.0",
|
||||
"@backstage/core-app-api": "^1.0.1",
|
||||
"@backstage/dev-utils": "^1.0.2-next.0",
|
||||
"@backstage/plugin-techdocs-addons-test-utils": "^0.0.0",
|
||||
"@backstage/test-utils": "^1.0.2-next.0",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
|
||||
@@ -66,7 +66,7 @@ const getUrl = (repository: Repository, template: ReportIssueTemplate) => {
|
||||
if (type === 'github') {
|
||||
return `${url}/issues/new?title=${encodedTitle}&body=${encodedBody}`;
|
||||
}
|
||||
return `${url}/issues/new?[title]=${encodedTitle}&[body]=${encodedBody}`;
|
||||
return `${url}/issues/new?issue[title]=${encodedTitle}&issue[description]=${encodedBody}`;
|
||||
};
|
||||
|
||||
export const IssueLink = ({ template, repository }: IssueLinkProps) => {
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* 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 { TechDocsAddonTester } from '@backstage/plugin-techdocs-addons-test-utils';
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
import { ReportIssue } from '..';
|
||||
|
||||
const byUrl = jest.fn();
|
||||
|
||||
const fireSelectionChangeEvent = (window: Window) => {
|
||||
const selectionChangeEvent = window.document.createEvent('Event');
|
||||
selectionChangeEvent.initEvent('selectionchange', true, true);
|
||||
window.document.addEventListener('selectionchange', () => {}, false);
|
||||
fireEvent(window.document, selectionChangeEvent);
|
||||
};
|
||||
|
||||
describe('ReportIssue', () => {
|
||||
const selection = {
|
||||
type: 'Range',
|
||||
rangeCount: 1,
|
||||
isCollapsed: true,
|
||||
getRangeAt: () => ({
|
||||
startContainer: 'this is a sentence',
|
||||
endContainer: 'this is a sentence',
|
||||
startOffset: 1,
|
||||
endOffset: 3,
|
||||
getBoundingClientRect: () => ({
|
||||
right: 100,
|
||||
top: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
}),
|
||||
}),
|
||||
toString: () => 'his ',
|
||||
containsNode: () => true,
|
||||
} as unknown as Selection;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders github link without exploding', async () => {
|
||||
byUrl.mockReturnValue({ type: 'github' });
|
||||
const { shadowRoot, getByText } =
|
||||
await TechDocsAddonTester.buildAddonsInTechDocs([
|
||||
<ReportIssue debounceTime={0} />,
|
||||
])
|
||||
.withDom(
|
||||
<html lang="en">
|
||||
<head />
|
||||
<body>
|
||||
<div data-md-component="content">
|
||||
<div data-md-component="navigation" />
|
||||
<div data-md-component="toc" />
|
||||
<div data-md-component="sidebar" />
|
||||
|
||||
<div data-md-component="main">
|
||||
<div className="md-content">
|
||||
<article>
|
||||
<a
|
||||
title="Leave feedback for this page"
|
||||
href="https://github.com/backstage/backstage/issues/new"
|
||||
>
|
||||
Leave feedback
|
||||
</a>
|
||||
<a
|
||||
title="Edit this page"
|
||||
href="https://github.com/backstage/backstage/edit/master/docs/README.md"
|
||||
>
|
||||
Edit page
|
||||
</a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
)
|
||||
.withApis([[scmIntegrationsApiRef, { byUrl }]])
|
||||
.renderWithEffects();
|
||||
|
||||
(shadowRoot as ShadowRoot & Pick<Document, 'getSelection'>).getSelection =
|
||||
() => selection;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Edit page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireSelectionChangeEvent(window);
|
||||
|
||||
await waitFor(() => {
|
||||
const link = getByText('Open new Github issue');
|
||||
expect(link).toHaveAttribute(
|
||||
'href',
|
||||
'https://github.com/backstage/backstage/issues/new?title=Documentation%20feedback%3A%20his%20&body=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20his%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%2F%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgithub.com%2Fbackstage%2Fbackstage%2Fblob%2Fmaster%2Fdocs%2FREADME.md%3E',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders gitlab link without exploding', async () => {
|
||||
byUrl.mockReturnValue({ type: 'gitlab' });
|
||||
const { shadowRoot, getByText } =
|
||||
await TechDocsAddonTester.buildAddonsInTechDocs([
|
||||
<ReportIssue debounceTime={0} />,
|
||||
])
|
||||
.withDom(
|
||||
<html lang="en">
|
||||
<head />
|
||||
<body>
|
||||
<div data-md-component="content">
|
||||
<div data-md-component="navigation" />
|
||||
<div data-md-component="toc" />
|
||||
<div data-md-component="sidebar" />
|
||||
|
||||
<div data-md-component="main">
|
||||
<div className="md-content">
|
||||
<article>
|
||||
<a
|
||||
title="Leave feedback for this page"
|
||||
href="https://gitlab.com/backstage/backstage/issues/new"
|
||||
>
|
||||
Leave feedback
|
||||
</a>
|
||||
<a
|
||||
title="Edit this page"
|
||||
href="https://gitlab.com/backstage/backstage/-/edit/master/docs/README.md"
|
||||
>
|
||||
Edit page
|
||||
</a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
)
|
||||
.withApis([[scmIntegrationsApiRef, { byUrl }]])
|
||||
.renderWithEffects();
|
||||
|
||||
(shadowRoot as ShadowRoot & Pick<Document, 'getSelection'>).getSelection =
|
||||
() => selection;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Edit page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireSelectionChangeEvent(window);
|
||||
|
||||
await waitFor(() => {
|
||||
const link = getByText('Open new Gitlab issue');
|
||||
expect(link).toHaveAttribute(
|
||||
'href',
|
||||
'https://gitlab.com/backstage/backstage/issues/new?issue[title]=Documentation%20feedback%3A%20his%20&issue[description]=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20his%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%2F%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgitlab.com%2Fbackstage%2Fbackstage%2F-%2Fblob%2Fmaster%2Fdocs%2FREADME.md%3E',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders using a custom template builder', async () => {
|
||||
byUrl.mockReturnValue({ type: 'gitlab' });
|
||||
|
||||
const templateBuilder = (options: { selection: Selection }) => ({
|
||||
title: 'Custom',
|
||||
body: options.selection.toString().trim(),
|
||||
});
|
||||
|
||||
const { shadowRoot, getByText } =
|
||||
await TechDocsAddonTester.buildAddonsInTechDocs([
|
||||
<ReportIssue debounceTime={0} templateBuilder={templateBuilder} />,
|
||||
])
|
||||
.withDom(
|
||||
<html lang="en">
|
||||
<head />
|
||||
<body>
|
||||
<div data-md-component="content">
|
||||
<div data-md-component="navigation" />
|
||||
<div data-md-component="toc" />
|
||||
<div data-md-component="sidebar" />
|
||||
|
||||
<div data-md-component="main">
|
||||
<div className="md-content">
|
||||
<article>
|
||||
<a
|
||||
title="Leave feedback for this page"
|
||||
href="https://gitlab.com/backstage/backstage/issues/new"
|
||||
>
|
||||
Leave feedback
|
||||
</a>
|
||||
<a
|
||||
title="Edit this page"
|
||||
href="https://gitlab.com/backstage/backstage/-/edit/master/docs/README.md"
|
||||
>
|
||||
Edit page
|
||||
</a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
)
|
||||
.withApis([[scmIntegrationsApiRef, { byUrl }]])
|
||||
.renderWithEffects();
|
||||
|
||||
(shadowRoot as ShadowRoot & Pick<Document, 'getSelection'>).getSelection =
|
||||
() => selection;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Edit page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireSelectionChangeEvent(window);
|
||||
|
||||
await waitFor(() => {
|
||||
const link = getByText('Open new Gitlab issue');
|
||||
expect(link).toHaveAttribute(
|
||||
'href',
|
||||
'https://gitlab.com/backstage/backstage/issues/new?issue[title]=Custom&issue[description]=his',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,9 @@ export function createTechDocsAddonExtension<TComponentProps>(
|
||||
// @alpha (undocumented)
|
||||
export const defaultTechDocsReaderPageValue: TechDocsReaderPageValue;
|
||||
|
||||
// @public
|
||||
export type SyncResult = 'cached' | 'updated';
|
||||
|
||||
// @alpha
|
||||
export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1';
|
||||
|
||||
@@ -103,6 +106,32 @@ export type TechDocsReaderPageValue = {
|
||||
onReady?: () => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface TechDocsStorageApi {
|
||||
// (undocumented)
|
||||
getApiOrigin(): Promise<string>;
|
||||
// (undocumented)
|
||||
getBaseUrl(
|
||||
oldBaseUrl: string,
|
||||
entityId: CompoundEntityRef,
|
||||
path: string,
|
||||
): Promise<string>;
|
||||
// (undocumented)
|
||||
getBuilder(): Promise<string>;
|
||||
// (undocumented)
|
||||
getEntityDocs(entityId: CompoundEntityRef, path: string): Promise<string>;
|
||||
// (undocumented)
|
||||
getStorageUrl(): Promise<string>;
|
||||
// (undocumented)
|
||||
syncEntityDocs(
|
||||
entityId: CompoundEntityRef,
|
||||
logHandler?: (line: string) => void,
|
||||
): Promise<SyncResult>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const techdocsStorageApiRef: ApiRef<TechDocsStorageApi>;
|
||||
|
||||
// @alpha
|
||||
export const useShadowRoot: () => ShadowRoot | undefined;
|
||||
|
||||
|
||||
@@ -39,3 +39,40 @@ export interface TechDocsApi {
|
||||
export const techdocsApiRef = createApiRef<TechDocsApi>({
|
||||
id: 'plugin.techdocs.service',
|
||||
});
|
||||
|
||||
/**
|
||||
* The outcome of a docs sync operation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SyncResult = 'cached' | 'updated';
|
||||
|
||||
/**
|
||||
* API which talks to TechDocs storage to fetch files to render.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface TechDocsStorageApi {
|
||||
getApiOrigin(): Promise<string>;
|
||||
getStorageUrl(): Promise<string>;
|
||||
getBuilder(): Promise<string>;
|
||||
getEntityDocs(entityId: CompoundEntityRef, path: string): Promise<string>;
|
||||
syncEntityDocs(
|
||||
entityId: CompoundEntityRef,
|
||||
logHandler?: (line: string) => void,
|
||||
): Promise<SyncResult>;
|
||||
getBaseUrl(
|
||||
oldBaseUrl: string,
|
||||
entityId: CompoundEntityRef,
|
||||
path: string,
|
||||
): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility API reference for the {@link TechDocsStorageApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const techdocsStorageApiRef = createApiRef<TechDocsStorageApi>({
|
||||
id: 'plugin.techdocs.storageservice',
|
||||
});
|
||||
|
||||
@@ -26,8 +26,8 @@ export {
|
||||
TechDocsAddons,
|
||||
TECHDOCS_ADDONS_WRAPPER_KEY,
|
||||
} from './addons';
|
||||
export { techdocsApiRef } from './api';
|
||||
export type { TechDocsApi } from './api';
|
||||
export { techdocsApiRef, techdocsStorageApiRef } from './api';
|
||||
export type { SyncResult, TechDocsApi, TechDocsStorageApi } from './api';
|
||||
export {
|
||||
defaultTechDocsReaderPageValue,
|
||||
TechDocsReaderPageProvider,
|
||||
|
||||
@@ -191,7 +191,7 @@ export type ReaderState = {
|
||||
// @public
|
||||
export const Router: () => JSX.Element;
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export type SyncResult = 'cached' | 'updated';
|
||||
|
||||
// @public
|
||||
@@ -394,7 +394,7 @@ export type TechDocsSearchResultListItemProps = {
|
||||
title?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export interface TechDocsStorageApi {
|
||||
// (undocumented)
|
||||
getApiOrigin(): Promise<string>;
|
||||
@@ -417,7 +417,7 @@ export interface TechDocsStorageApi {
|
||||
): Promise<SyncResult>;
|
||||
}
|
||||
|
||||
// @public
|
||||
// @public @deprecated
|
||||
export const techdocsStorageApiRef: ApiRef<TechDocsStorageApi>;
|
||||
|
||||
// @public
|
||||
|
||||
@@ -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<TechDocsStorageApi>({
|
||||
id: 'plugin.techdocs.storageservice',
|
||||
@@ -44,6 +45,7 @@ export const techdocsApiRef = createApiRef<TechDocsApi>({
|
||||
* 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<string>;
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { techdocsApiRef, techdocsStorageApiRef } from './api';
|
||||
import {
|
||||
techdocsApiRef,
|
||||
techdocsStorageApiRef,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { TechDocsClient, TechDocsStorageClient } from './client';
|
||||
import {
|
||||
rootDocsRouteRef,
|
||||
|
||||
@@ -26,7 +26,7 @@ import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import { SidebarPinStateContext } from '@backstage/core-components';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
|
||||
import { techdocsStorageApiRef } from '../../../api';
|
||||
import { techdocsStorageApiRef } from '@backstage/plugin-techdocs-react';
|
||||
|
||||
import { useTechDocsReader } from './context';
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useApi } from '@backstage/core-plugin-api';
|
||||
import { useMemo, useReducer, useRef } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
|
||||
import { techdocsStorageApiRef } from '../../api';
|
||||
import { techdocsStorageApiRef } from '@backstage/plugin-techdocs-react';
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
||||
@@ -48,8 +48,9 @@ export const addGitFeedbackLink = (
|
||||
}
|
||||
|
||||
// topmost h1 only contains title for whole page
|
||||
const title = (dom.querySelector('article>h1') as HTMLElement).childNodes[0]
|
||||
.textContent;
|
||||
const title =
|
||||
(dom.querySelector('article>h1') as HTMLElement)?.childNodes[0]
|
||||
.textContent || '';
|
||||
const issueTitle = encodeURIComponent(`Documentation Feedback: ${title}`);
|
||||
const issueDesc = encodeURIComponent(
|
||||
`Page source:\n${sourceAnchor.href}\n\nFeedback:`,
|
||||
|
||||
@@ -259,7 +259,9 @@ const NO_WARNING_PACKAGES = [
|
||||
'plugins/search-common',
|
||||
'plugins/search-react',
|
||||
'plugins/techdocs',
|
||||
'plugins/techdocs-addons-test-utils',
|
||||
'plugins/techdocs-backend',
|
||||
'plugins/techdocs-module-addons-contrib',
|
||||
'plugins/techdocs-node',
|
||||
'plugins/techdocs-react',
|
||||
'plugins/tech-insights',
|
||||
|
||||
@@ -23576,6 +23576,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"
|
||||
|
||||
Reference in New Issue
Block a user