Merge branch 'master' of https://github.com/backstage/backstage into techdocs/navigation-bugfix
This commit is contained in:
@@ -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