Merge branch 'master' of https://github.com/backstage/backstage into techdocs/navigation-bugfix
This commit is contained in:
@@ -1,5 +1,76 @@
|
||||
# @backstage/plugin-techdocs
|
||||
|
||||
## 1.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 27740caa2d: Added experimental support for declarative integration via the `/alpha` subpath.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4918f65ab2: Create an experimental `TechDocsSearchResultItemExtension` for declarative integration with Backstage; it can be accessed via the `/alpha` import.
|
||||
- 3605370af6: Improved `DocsTable` to display pagination controls dynamically, appearing only when needed.
|
||||
- 0296f272b4: The `spec.lifecycle' field in entities will now always be rendered as a string.
|
||||
- 9a1fce352e: Updated dependency `@testing-library/jest-dom` to `^6.0.0`.
|
||||
- f95af4e540: Updated dependency `@testing-library/dom` to `^9.0.0`.
|
||||
- 9468a67b92: Added support for React 18. The new `createRoot` API from `react-dom/client` will now be used if present.
|
||||
- df449a7a31: Add kind column by default to TechDocsTable
|
||||
- Updated dependencies
|
||||
- @backstage/integration@1.7.1
|
||||
- @backstage/plugin-catalog-react@1.8.5
|
||||
- @backstage/frontend-plugin-api@0.2.0
|
||||
- @backstage/core-plugin-api@1.7.0
|
||||
- @backstage/core-components@0.13.6
|
||||
- @backstage/integration-react@1.1.20
|
||||
- @backstage/catalog-model@1.4.3
|
||||
- @backstage/errors@1.2.3
|
||||
- @backstage/plugin-search-react@1.7.1
|
||||
- @backstage/plugin-techdocs-react@1.1.12
|
||||
- @backstage/theme@0.4.3
|
||||
- @backstage/config@1.1.1
|
||||
- @backstage/plugin-search-common@1.2.7
|
||||
|
||||
## 1.7.1-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3605370af6: Improved `DocsTable` to display pagination controls dynamically, appearing only when needed.
|
||||
- Updated dependencies
|
||||
- @backstage/frontend-plugin-api@0.2.0-next.2
|
||||
- @backstage/integration-react@1.1.20-next.2
|
||||
- @backstage/core-components@0.13.6-next.2
|
||||
- @backstage/core-plugin-api@1.7.0-next.1
|
||||
- @backstage/catalog-model@1.4.3-next.0
|
||||
- @backstage/plugin-catalog-react@1.8.5-next.2
|
||||
- @backstage/integration@1.7.1-next.1
|
||||
- @backstage/errors@1.2.3-next.0
|
||||
- @backstage/plugin-search-react@1.7.1-next.2
|
||||
- @backstage/theme@0.4.3-next.0
|
||||
- @backstage/config@1.1.1-next.0
|
||||
- @backstage/plugin-search-common@1.2.7-next.0
|
||||
- @backstage/plugin-techdocs-react@1.1.12-next.2
|
||||
|
||||
## 1.7.1-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4918f65ab2: Create an experimental `TechDocsSearchResultItemExtension` for declarative integration with Backstage; it can be accessed via the `/alpha` import.
|
||||
- df449a7a31: Add kind column by default to TechDocsTable
|
||||
- Updated dependencies
|
||||
- @backstage/frontend-plugin-api@0.1.1-next.1
|
||||
- @backstage/core-components@0.13.6-next.1
|
||||
- @backstage/plugin-search-react@1.7.1-next.1
|
||||
- @backstage/integration-react@1.1.20-next.1
|
||||
- @backstage/plugin-catalog-react@1.8.5-next.1
|
||||
- @backstage/plugin-techdocs-react@1.1.12-next.1
|
||||
- @backstage/core-plugin-api@1.7.0-next.0
|
||||
- @backstage/config@1.1.0
|
||||
- @backstage/catalog-model@1.4.2
|
||||
- @backstage/errors@1.2.2
|
||||
- @backstage/integration@1.7.1-next.0
|
||||
- @backstage/theme@0.4.2
|
||||
- @backstage/plugin-search-common@1.2.6
|
||||
|
||||
## 1.7.1-next.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
## API Report File for "@backstage/plugin-techdocs"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
docRoot: RouteRef<{
|
||||
name: string;
|
||||
kind: string;
|
||||
namespace: string;
|
||||
}>;
|
||||
entityContent: RouteRef<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const TechDocsSearchResultListItemExtension: Extension<{
|
||||
lineClamp: number;
|
||||
noTrack: boolean;
|
||||
asListItem: boolean;
|
||||
asLink: boolean;
|
||||
title?: string | undefined;
|
||||
}>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -1,14 +1,27 @@
|
||||
{
|
||||
"name": "@backstage/plugin-techdocs",
|
||||
"description": "The Backstage plugin that renders technical documentation for your components",
|
||||
"version": "1.7.1-next.0",
|
||||
"version": "1.8.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"
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha.tsx",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"alpha": [
|
||||
"src/alpha.tsx"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"backstage": {
|
||||
"role": "frontend-plugin"
|
||||
@@ -39,6 +52,7 @@
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/integration-react": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
@@ -60,8 +74,8 @@
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0",
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -70,10 +84,9 @@
|
||||
"@backstage/dev-utils": "workspace:^",
|
||||
"@backstage/plugin-techdocs-module-addons-contrib": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/dom": "^8.0.0",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/react-hooks": "^8.0.0",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@types/dompurify": "^2.2.2",
|
||||
"@types/event-source-polyfill": "^1.0.0",
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2023 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 LibraryBooks from '@material-ui/icons/LibraryBooks';
|
||||
import {
|
||||
createPlugin,
|
||||
createSchemaFromZod,
|
||||
createApiExtension,
|
||||
createPageExtension,
|
||||
createNavItemExtension,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha';
|
||||
import {
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
|
||||
import {
|
||||
techdocsApiRef,
|
||||
techdocsStorageApiRef,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { TechDocsClient, TechDocsStorageClient } from './client';
|
||||
import {
|
||||
rootCatalogDocsRouteRef,
|
||||
rootDocsRouteRef,
|
||||
rootRouteRef,
|
||||
} from './routes';
|
||||
import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha';
|
||||
|
||||
/** @alpha */
|
||||
const techDocsStorage = createApiExtension({
|
||||
api: techdocsStorageApiRef,
|
||||
|
||||
factory() {
|
||||
return createApiFactory({
|
||||
api: techdocsStorageApiRef,
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
|
||||
new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
const techDocsClient = createApiExtension({
|
||||
api: techdocsApiRef,
|
||||
factory() {
|
||||
return createApiFactory({
|
||||
api: techdocsApiRef,
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, fetchApi }) =>
|
||||
new TechDocsClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
fetchApi,
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export const TechDocsSearchResultListItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'techdocs',
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
// TODO: Define how the icon can be configurable
|
||||
title: z.string().optional(),
|
||||
lineClamp: z.number().default(5),
|
||||
asLink: z.boolean().default(true),
|
||||
asListItem: z.boolean().default(true),
|
||||
noTrack: z.boolean().default(false),
|
||||
}),
|
||||
),
|
||||
predicate: result => result.type === 'techdocs',
|
||||
component: async ({ config }) => {
|
||||
const { TechDocsSearchResultListItem } = await import(
|
||||
'./search/components/TechDocsSearchResultListItem'
|
||||
);
|
||||
return props => <TechDocsSearchResultListItem {...props} {...config} />;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Responsible for rendering the provided router element
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
const TechDocsIndexPage = createPageExtension({
|
||||
id: 'plugin.techdocs.indexPage',
|
||||
defaultPath: '/docs',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
loader: () =>
|
||||
import('./home/components/TechDocsIndexPage').then(m => (
|
||||
<m.TechDocsIndexPage />
|
||||
)),
|
||||
});
|
||||
|
||||
/**
|
||||
* Component responsible for composing a TechDocs reader page experience
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
const TechDocsReaderPage = createPageExtension({
|
||||
id: 'plugin.techdocs.readerPage',
|
||||
defaultPath: '/docs/:namespace/:kind/:name',
|
||||
routeRef: convertLegacyRouteRef(rootDocsRouteRef),
|
||||
loader: () =>
|
||||
import('./reader/components/TechDocsReaderPage').then(m => (
|
||||
<m.TechDocsReaderPage />
|
||||
)),
|
||||
});
|
||||
|
||||
/**
|
||||
* Component responsible for rendering techdocs on entity pages
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
const TechDocsEntityContent = createEntityContentExtension({
|
||||
id: 'techdocs',
|
||||
defaultPath: 'docs',
|
||||
defaultTitle: 'TechDocs',
|
||||
loader: () => import('./Router').then(m => <m.EmbeddedDocsRouter />),
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
const TechDocsNavItem = createNavItemExtension({
|
||||
id: 'plugin.techdocs.nav.index',
|
||||
icon: LibraryBooks,
|
||||
title: 'Docs',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export default createPlugin({
|
||||
id: 'techdocs',
|
||||
extensions: [
|
||||
techDocsClient,
|
||||
techDocsStorage,
|
||||
TechDocsNavItem,
|
||||
TechDocsIndexPage,
|
||||
TechDocsReaderPage,
|
||||
TechDocsEntityContent,
|
||||
TechDocsSearchResultListItemExtension,
|
||||
],
|
||||
routes: {
|
||||
root: convertLegacyRouteRef(rootRouteRef),
|
||||
docRoot: convertLegacyRouteRef(rootDocsRouteRef),
|
||||
entityContent: convertLegacyRouteRef(rootCatalogDocsRouteRef),
|
||||
},
|
||||
});
|
||||
@@ -95,14 +95,17 @@ export const DocsTable = (props: DocsTableProps) => {
|
||||
actionFactories.createCopyDocsUrlAction(copyToClipboard),
|
||||
];
|
||||
|
||||
const pageSize = 20;
|
||||
const paging = documents && documents.length > pageSize;
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading || (documents && documents.length > 0) ? (
|
||||
<Table<DocsTableRow>
|
||||
isLoading={loading}
|
||||
options={{
|
||||
paging: true,
|
||||
pageSize: 20,
|
||||
paging,
|
||||
pageSize,
|
||||
search: true,
|
||||
actionsColumnIndex: -1,
|
||||
...options,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
TechDocsBuildLogs,
|
||||
TechDocsBuildLogsDrawerContent,
|
||||
} from './TechDocsBuildLogs';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
|
||||
// The <AutoSizer> inside <LogViewer> needs mocking to render in jsdom
|
||||
jest.mock('react-virtualized-auto-sizer', () => ({
|
||||
@@ -38,7 +39,7 @@ describe('<TechDocsBuildLogs />', () => {
|
||||
|
||||
it('should open drawer', async () => {
|
||||
const rendered = await renderInTestApp(<TechDocsBuildLogs buildLog={[]} />);
|
||||
rendered.getByText(/Show Build Logs/i).click();
|
||||
await userEvent.click(rendered.getByText(/Show Build Logs/i));
|
||||
expect(rendered.getByText(/Build Details/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+60
-71
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { act } from '@testing-library/react';
|
||||
import { scmIntegrationsApiRef } from '@backstage/integration-react';
|
||||
|
||||
import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
@@ -150,31 +149,25 @@ describe('<TechDocsReaderPage />', () => {
|
||||
});
|
||||
|
||||
it('should render a techdocs reader page with children', async () => {
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPage
|
||||
entityRef={{
|
||||
name: 'test-name',
|
||||
namespace: 'test-namespace',
|
||||
kind: 'test',
|
||||
}}
|
||||
>
|
||||
techdocs reader page
|
||||
</TechDocsReaderPage>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes,
|
||||
},
|
||||
);
|
||||
expect(
|
||||
rendered.container.querySelector('header'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
rendered.container.querySelector('article'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(rendered.getByText('techdocs reader page')).toBeInTheDocument();
|
||||
});
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPage
|
||||
entityRef={{
|
||||
name: 'test-name',
|
||||
namespace: 'test-namespace',
|
||||
kind: 'test',
|
||||
}}
|
||||
>
|
||||
techdocs reader page
|
||||
</TechDocsReaderPage>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes,
|
||||
},
|
||||
);
|
||||
expect(rendered.container.querySelector('header')).not.toBeInTheDocument();
|
||||
expect(rendered.container.querySelector('article')).not.toBeInTheDocument();
|
||||
expect(rendered.getByText('techdocs reader page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render techdocs reader page with addons', async () => {
|
||||
@@ -183,56 +176,52 @@ describe('<TechDocsReaderPage />', () => {
|
||||
const namespace = 'test-namespace';
|
||||
const kind = 'test';
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<FlatRoutes>
|
||||
<Route
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={<TechDocsReaderPage />}
|
||||
>
|
||||
<TechDocsAddons>
|
||||
<ReportIssue />
|
||||
</TechDocsAddons>
|
||||
</Route>
|
||||
</FlatRoutes>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes,
|
||||
routeEntries: ['/docs/test-namespace/test/test-name'],
|
||||
},
|
||||
);
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<FlatRoutes>
|
||||
<Route
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={<TechDocsReaderPage />}
|
||||
>
|
||||
<TechDocsAddons>
|
||||
<ReportIssue />
|
||||
</TechDocsAddons>
|
||||
</Route>
|
||||
</FlatRoutes>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes,
|
||||
routeEntries: ['/docs/test-namespace/test/test-name'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.getByText(`PageMock: ${namespace}#${kind}#${name}`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
rendered.getByText(`PageMock: ${namespace}#${kind}#${name}`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render techdocs reader page with addons and page', async () => {
|
||||
(Page as jest.Mock).mockImplementation(PageMock);
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<FlatRoutes>
|
||||
<Route
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={<TechDocsReaderPage />}
|
||||
>
|
||||
<p>the page</p>
|
||||
<TechDocsAddons>
|
||||
<ReportIssue />
|
||||
</TechDocsAddons>
|
||||
</Route>
|
||||
</FlatRoutes>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes,
|
||||
routeEntries: ['/docs/test-namespace/test/test-name'],
|
||||
},
|
||||
);
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<FlatRoutes>
|
||||
<Route
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={<TechDocsReaderPage />}
|
||||
>
|
||||
<p>the page</p>
|
||||
<TechDocsAddons>
|
||||
<ReportIssue />
|
||||
</TechDocsAddons>
|
||||
</Route>
|
||||
</FlatRoutes>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes,
|
||||
routeEntries: ['/docs/test-namespace/test/test-name'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.getByText('the page')).toBeInTheDocument();
|
||||
});
|
||||
expect(rendered.getByText('the page')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
|
||||
@@ -95,16 +95,13 @@ describe('context', () => {
|
||||
});
|
||||
|
||||
it('should return expected entity values', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
() => useEntityMetadata(),
|
||||
{ wrapper },
|
||||
);
|
||||
const { result } = renderHook(() => useEntityMetadata(), { wrapper });
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.value).toBeDefined();
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toMatchObject(mockEntityMetadata);
|
||||
await waitFor(() => {
|
||||
expect(result.current.value).toBeDefined();
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toMatchObject(mockEntityMetadata);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,16 +113,13 @@ describe('context', () => {
|
||||
});
|
||||
|
||||
it('should return expected techdocs metadata values', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
() => useTechDocsMetadata(),
|
||||
{ wrapper },
|
||||
);
|
||||
const { result } = renderHook(() => useTechDocsMetadata(), { wrapper });
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.value).toBeDefined();
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toMatchObject(mockTechDocsMetadata);
|
||||
await waitFor(() => {
|
||||
expect(result.current.value).toBeDefined();
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toMatchObject(mockTechDocsMetadata);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+34
-40
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
@@ -98,18 +98,16 @@ describe('<TechDocsReaderPageContent />', () => {
|
||||
useTechDocsReaderDom.mockReturnValue(document.createElement('html'));
|
||||
useReaderState.mockReturnValue({ state: 'cached' });
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageContent withSearch={false} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageContent withSearch={false} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rendered.getByTestId('techdocs-native-shadowroot'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rendered.getByTestId('techdocs-native-shadowroot'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,21 +116,19 @@ describe('<TechDocsReaderPageContent />', () => {
|
||||
useTechDocsReaderDom.mockReturnValue(document.createElement('html'));
|
||||
useReaderState.mockReturnValue({ state: 'cached' });
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageContent withSearch={false} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageContent withSearch={false} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rendered.queryByTestId('techdocs-native-shadowroot'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
rendered.getByText('ERROR 404: PAGE NOT FOUND'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rendered.queryByTestId('techdocs-native-shadowroot'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
rendered.getByText('ERROR 404: PAGE NOT FOUND'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,21 +138,19 @@ describe('<TechDocsReaderPageContent />', () => {
|
||||
useTechDocsReaderDom.mockReturnValue(undefined);
|
||||
useReaderState.mockReturnValue({ state: 'CONTENT_NOT_FOUND' });
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageContent withSearch={false} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageContent withSearch={false} />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rendered.queryByTestId('techdocs-native-shadowroot'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
rendered.getByText('ERROR 404: Documentation not found'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rendered.queryByTestId('techdocs-native-shadowroot'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
rendered.getByText('ERROR 404: Documentation not found'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+49
-85
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
@@ -80,112 +79,77 @@ describe('<TechDocsReaderPageHeader />', () => {
|
||||
getEntityMetadata.mockResolvedValue(mockEntityMetadata);
|
||||
getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.container.innerHTML).toContain('header');
|
||||
expect(rendered.container.innerHTML).toContain('header');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(rendered.getAllByText('test-site-name')).toHaveLength(2);
|
||||
});
|
||||
expect(rendered.getAllByText('test-site-name')).toHaveLength(2);
|
||||
expect(rendered.getByText('test-site-desc')).toBeDefined();
|
||||
|
||||
expect(rendered.getByText('test-site-desc')).toBeDefined();
|
||||
});
|
||||
expect(
|
||||
rendered.getByRole('link', { name: 'test:test-namespace/test-name' }),
|
||||
).toHaveAttribute('href', '/catalog/test-namespace/test/test-name');
|
||||
});
|
||||
|
||||
it('should render a techdocs page header even if metadata is not loaded', async () => {
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.container.innerHTML).toContain('header');
|
||||
});
|
||||
expect(rendered.container.innerHTML).toContain('header');
|
||||
});
|
||||
|
||||
it('should not render a techdocs page header if entity metadata is missing', async () => {
|
||||
getEntityMetadata.mockResolvedValue(undefined);
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(rendered.container.innerHTML).not.toContain('header');
|
||||
});
|
||||
});
|
||||
expect(rendered.container.innerHTML).not.toContain('header');
|
||||
});
|
||||
|
||||
it('should not render a techdocs page header if techdocs metadata is missing', async () => {
|
||||
getTechDocsMetadata.mockResolvedValue(undefined);
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(rendered.container.innerHTML).not.toContain('header');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a link back to the component page', async () => {
|
||||
getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata);
|
||||
|
||||
await act(async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<TechDocsReaderPageHeader />
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
'/docs': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
rendered.getByRole('link', { name: 'test:test-namespace/test-name' }),
|
||||
).toHaveAttribute('href', '/catalog/test-namespace/test/test-name');
|
||||
});
|
||||
});
|
||||
expect(rendered.container.innerHTML).not.toContain('header');
|
||||
});
|
||||
});
|
||||
|
||||
+3
-1
@@ -133,7 +133,9 @@ export const TechDocsReaderPageHeader = (
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{lifecycle ? <HeaderLabel label="Lifecycle" value={lifecycle} /> : null}
|
||||
{lifecycle ? (
|
||||
<HeaderLabel label="Lifecycle" value={String(lifecycle)} />
|
||||
) : null}
|
||||
{locationMetadata &&
|
||||
locationMetadata.type !== 'dir' &&
|
||||
locationMetadata.type !== 'file' ? (
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { techdocsStorageApiRef } from '../../api';
|
||||
import {
|
||||
@@ -284,24 +284,22 @@ describe('useReaderState', () => {
|
||||
return 'cached';
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForValueToChange } = await renderHook(
|
||||
() => useReaderState('Component', 'default', 'backstage', '/example'),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => useReaderState('Component', 'default', 'backstage', '/example'),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_FRESH',
|
||||
path: '/example',
|
||||
@@ -311,20 +309,20 @@ describe('useReaderState', () => {
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reload initially missing content', async () => {
|
||||
@@ -336,6 +334,7 @@ describe('useReaderState', () => {
|
||||
});
|
||||
techdocsStorageApi.syncEntityDocs.mockImplementation(
|
||||
async (_, logHandler) => {
|
||||
await 'a tick';
|
||||
logHandler?.call(this, 'Line 1');
|
||||
logHandler?.call(this, 'Line 2');
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
@@ -343,38 +342,39 @@ describe('useReaderState', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForValueToChange } = await renderHook(
|
||||
() => useReaderState('Component', 'default', 'backstage', '/example'),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => useReaderState('Component', 'default', 'backstage', '/example'),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForValueToChange(() => result.current.state, {
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'INITIAL_BUILD',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: 'NotFoundError: Page Not Found',
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: ['Line 1', 'Line 2'],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
},
|
||||
{
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
state: 'INITIAL_BUILD',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: 'NotFoundError: Page Not Found',
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: ['Line 1', 'Line 2'],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
@@ -384,9 +384,9 @@ describe('useReaderState', () => {
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_FRESH',
|
||||
path: '/example',
|
||||
@@ -396,22 +396,22 @@ describe('useReaderState', () => {
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledTimes(2);
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledTimes(1);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledTimes(2);
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledTimes(1);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle stale content', async () => {
|
||||
@@ -423,6 +423,7 @@ describe('useReaderState', () => {
|
||||
});
|
||||
techdocsStorageApi.syncEntityDocs.mockImplementation(
|
||||
async (_, logHandler) => {
|
||||
await 'a tick';
|
||||
logHandler?.call(this, 'Line 1');
|
||||
logHandler?.call(this, 'Line 2');
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
@@ -430,24 +431,23 @@ describe('useReaderState', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForValueToChange } = await renderHook(
|
||||
() => useReaderState('Component', 'default', 'backstage', '/example'),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => useReaderState('Component', 'default', 'backstage', '/example'),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
// the content is returned but the sync is in progress
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
// the content is returned but the sync is in progress
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_FRESH',
|
||||
path: '/example',
|
||||
@@ -457,9 +457,10 @@ describe('useReaderState', () => {
|
||||
buildLog: ['Line 1', 'Line 2'],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
// the sync takes longer than 1 seconds so the refreshing state starts
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
// the sync takes longer than 1 seconds so the refreshing state starts
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_STALE_REFRESHING',
|
||||
path: '/example',
|
||||
@@ -469,9 +470,10 @@ describe('useReaderState', () => {
|
||||
buildLog: ['Line 1', 'Line 2'],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
// the content is updated but not yet displayed
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
// the content is updated but not yet displayed
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_STALE_READY',
|
||||
path: '/example',
|
||||
@@ -481,12 +483,15 @@ describe('useReaderState', () => {
|
||||
buildLog: ['Line 1', 'Line 2'],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
// reload the content
|
||||
// reload the content
|
||||
await act(async () => {
|
||||
result.current.contentReload();
|
||||
});
|
||||
|
||||
// the new content refresh is triggered
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
// the new content refresh is triggered
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
@@ -496,35 +501,39 @@ describe('useReaderState', () => {
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
// the new content is loaded
|
||||
await waitForValueToChange(() => result.current.state, {
|
||||
timeout: 2000,
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_FRESH',
|
||||
path: '/example',
|
||||
content: 'my new content',
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledTimes(2);
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
// the new content is loaded
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_FRESH',
|
||||
path: '/example',
|
||||
content: 'my new content',
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
},
|
||||
{
|
||||
timeout: 2000,
|
||||
},
|
||||
);
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledTimes(2);
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle navigation', async () => {
|
||||
@@ -537,25 +546,24 @@ describe('useReaderState', () => {
|
||||
.mockRejectedValueOnce(new NotFoundError('Some error description'));
|
||||
techdocsStorageApi.syncEntityDocs.mockResolvedValue('cached');
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForValueToChange, rerender } = await renderHook(
|
||||
({ path }: { path: string }) =>
|
||||
useReaderState('Component', 'default', 'backstage', path),
|
||||
{ initialProps: { path: '/example' }, wrapper: Wrapper as any },
|
||||
);
|
||||
const { result, rerender } = renderHook(
|
||||
({ path }: { path: string }) =>
|
||||
useReaderState('Component', 'default', 'backstage', path),
|
||||
{ initialProps: { path: '/example' }, wrapper: Wrapper as any },
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
// show the content
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
// show the content
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_FRESH',
|
||||
path: '/example',
|
||||
@@ -565,11 +573,12 @@ describe('useReaderState', () => {
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
// navigate
|
||||
rerender({ path: '/new' });
|
||||
// navigate
|
||||
rerender({ path: '/new' });
|
||||
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
@@ -579,24 +588,29 @@ describe('useReaderState', () => {
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
await waitForValueToChange(() => result.current.state, {
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_FRESH',
|
||||
path: '/new',
|
||||
content: 'my new content',
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
},
|
||||
{
|
||||
timeout: 2000,
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_FRESH',
|
||||
path: '/new',
|
||||
content: 'my new content',
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// navigate
|
||||
rerender({ path: '/missing' });
|
||||
// navigate
|
||||
rerender({ path: '/missing' });
|
||||
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_NOT_FOUND',
|
||||
path: '/missing',
|
||||
@@ -606,24 +620,24 @@ describe('useReaderState', () => {
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/new',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/new',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle content error', async () => {
|
||||
@@ -632,24 +646,23 @@ describe('useReaderState', () => {
|
||||
);
|
||||
techdocsStorageApi.syncEntityDocs.mockResolvedValue('cached');
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForValueToChange } = await renderHook(
|
||||
() => useReaderState('Component', 'default', 'backstage', '/example'),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => useReaderState('Component', 'default', 'backstage', '/example'),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
state: 'CHECKING',
|
||||
path: '/example',
|
||||
content: undefined,
|
||||
contentErrorMessage: undefined,
|
||||
syncErrorMessage: undefined,
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
// the content loading threw an error
|
||||
await waitForValueToChange(() => result.current.state);
|
||||
// the content loading threw an error
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
state: 'CONTENT_NOT_FOUND',
|
||||
path: '/example',
|
||||
@@ -659,20 +672,20 @@ describe('useReaderState', () => {
|
||||
buildLog: [],
|
||||
contentReload: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
expect(techdocsStorageApi.getEntityDocs).toHaveBeenCalledWith(
|
||||
{ kind: 'Component', namespace: 'default', name: 'backstage' },
|
||||
'/example',
|
||||
);
|
||||
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'backstage',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
} from '@backstage/integration';
|
||||
import FeedbackOutlinedIcon from '@material-ui/icons/FeedbackOutlined';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { renderReactElement } from './renderReactElement';
|
||||
|
||||
// requires repo
|
||||
export const addGitFeedbackLink = (
|
||||
@@ -75,7 +75,7 @@ export const addGitFeedbackLink = (
|
||||
default:
|
||||
return dom;
|
||||
}
|
||||
ReactDOM.render(React.createElement(FeedbackOutlinedIcon), feedbackLink);
|
||||
renderReactElement(React.createElement(FeedbackOutlinedIcon), feedbackLink);
|
||||
feedbackLink.style.paddingLeft = '5px';
|
||||
feedbackLink.title = 'Leave feedback for this page';
|
||||
feedbackLink.id = 'git-feedback-link';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import type { Transformer } from './transformer';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { renderReactElement } from './renderReactElement';
|
||||
|
||||
export const addSidebarToggle = (): Transformer => {
|
||||
return dom => {
|
||||
@@ -33,7 +33,7 @@ export const addSidebarToggle = (): Transformer => {
|
||||
}
|
||||
|
||||
const toggleSidebar = mkdocsToggleSidebar.cloneNode() as HTMLLabelElement;
|
||||
ReactDOM.render(React.createElement(MenuIcon), toggleSidebar);
|
||||
renderReactElement(React.createElement(MenuIcon), toggleSidebar);
|
||||
toggleSidebar.id = 'toggle-sidebar';
|
||||
toggleSidebar.title = 'Toggle Sidebar';
|
||||
toggleSidebar.classList.add('md-content__button');
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { createTestShadowDom } from '../../test-utils';
|
||||
import { copyToClipboard } from './copyToClipboard';
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import useCopyToClipboard from 'react-use/lib/useCopyToClipboard';
|
||||
|
||||
const clipboardSpy = jest.fn();
|
||||
@@ -43,8 +43,11 @@ describe('copyToClipboard', () => {
|
||||
spy.mockReturnValue([{}, copy]);
|
||||
|
||||
const expectedClipboard = 'function foo() {return "bar";}';
|
||||
const shadowDom = await createTestShadowDom(
|
||||
`
|
||||
|
||||
let shadowDom: ShadowRoot;
|
||||
await act(async () => {
|
||||
shadowDom = await createTestShadowDom(
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
@@ -52,13 +55,20 @@ describe('copyToClipboard', () => {
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
{
|
||||
preTransformers: [],
|
||||
postTransformers: [copyToClipboard(lightTheme)],
|
||||
},
|
||||
);
|
||||
{
|
||||
preTransformers: [],
|
||||
postTransformers: [copyToClipboard(lightTheme)],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
shadowDom.querySelector('button')?.click();
|
||||
await waitFor(() => {
|
||||
expect(shadowDom.querySelector('button')).not.toBe(null);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
shadowDom.querySelector('button')!.click();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltip = document.querySelector('[role="tooltip"]');
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
import { renderReactElement } from './renderReactElement';
|
||||
import {
|
||||
withStyles,
|
||||
Theme,
|
||||
@@ -91,7 +91,7 @@ export const copyToClipboard = (theme: Theme): Transformer => {
|
||||
const text = code.textContent || '';
|
||||
const container = document.createElement('div');
|
||||
code?.parentElement?.prepend(container);
|
||||
ReactDom.render(
|
||||
renderReactElement(
|
||||
<ThemeProvider theme={theme}>
|
||||
<CopyToClipboardButton text={text} />
|
||||
</ThemeProvider>,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import React, { FC, PropsWithChildren } from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2023 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.
|
||||
*/
|
||||
|
||||
let ReactDOMPromise: Promise<
|
||||
typeof import('react-dom') | typeof import('react-dom/client')
|
||||
>;
|
||||
if (process.env.HAS_REACT_DOM_CLIENT) {
|
||||
ReactDOMPromise = import('react-dom/client');
|
||||
} else {
|
||||
ReactDOMPromise = import('react-dom');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function renderReactElement(element: JSX.Element, root: HTMLElement) {
|
||||
ReactDOMPromise.then(ReactDOM => {
|
||||
if ('createRoot' in ReactDOM) {
|
||||
ReactDOM.createRoot(root).render(element);
|
||||
} else {
|
||||
ReactDOM.render(element, root);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useStylesTransformer } from './transformer';
|
||||
|
||||
describe('Transformers > Styles', () => {
|
||||
|
||||
Reference in New Issue
Block a user