rtl 13 fixes for tests with type issues

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-10-13 09:21:51 +02:00
parent d60fbcb553
commit 0935b0d60f
33 changed files with 862 additions and 963 deletions
@@ -353,9 +353,7 @@ describe('v1 consumer', () => {
initialProps: {
routeRef: routeRef1 as AnyRouteRef,
},
wrapper: ({
children,
}: React.PropsWithChildren<{ routeRef: AnyRouteRef }>) => (
wrapper: ({ children }) => (
<RoutingProvider
routePaths={
new Map<RouteRef<any>, string>([
@@ -385,9 +385,7 @@ describe('v1 consumer', () => {
initialProps: {
routeRef: routeRef1 as AnyRouteRef,
},
wrapper: ({
children,
}: React.PropsWithChildren<{ routeRef: AnyRouteRef }>) => (
wrapper: ({ children }) => (
<RoutingProvider
routePaths={
new Map<RouteRef<any>, string>([
@@ -316,23 +316,23 @@ describe('useElementFilter', () => {
</MockComponent>
);
const { result } = renderHook(
props =>
useElementFilter(props.tree, elements =>
elements
.selectByComponentData({
key: WRAPPING_COMPONENT_KEY,
withStrictError: 'Could not find component',
})
.findComponentData({ key: INNER_COMPONENT_KEY }),
),
{
initialProps: { tree },
wrapper: Wrapper,
},
);
expect(result.error?.message).toEqual('Could not find component');
expect(() =>
renderHook(
props =>
useElementFilter(props.tree, elements =>
elements
.selectByComponentData({
key: WRAPPING_COMPONENT_KEY,
withStrictError: 'Could not find component',
})
.findComponentData({ key: INNER_COMPONENT_KEY }),
),
{
initialProps: { tree },
wrapper: Wrapper,
},
),
).toThrow('Could not find component');
});
it('should support fragments and text node iteration', () => {
@@ -20,7 +20,7 @@ import {
TestApiProvider,
withLogCollector,
} from '@backstage/test-utils';
import { renderHook } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react';
import { createTranslationRef, TranslationRef } from './TranslationRef';
import { useTranslationRef } from './useTranslationRef';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
@@ -91,14 +91,11 @@ describe('useTranslationRef', () => {
],
});
const { result, waitForNextUpdate } = renderHook(
() => useTranslationRef(plainRef),
{
wrapper: makeWrapper(translationApi),
},
);
const { result } = renderHook(() => useTranslationRef(plainRef), {
wrapper: makeWrapper(translationApi),
});
await waitForNextUpdate();
await act(async () => {});
const { t } = result.current;
@@ -123,12 +120,9 @@ describe('useTranslationRef', () => {
],
});
const { result, waitForNextUpdate } = renderHook(
() => useTranslationRef(plainRef),
{
wrapper: makeWrapper(translationApi),
},
);
const { result } = renderHook(() => useTranslationRef(plainRef), {
wrapper: makeWrapper(translationApi),
});
const { t } = result.current;
@@ -137,7 +131,7 @@ describe('useTranslationRef', () => {
languageApi.setLanguage('de');
await waitForNextUpdate();
await act(async () => {});
const { t: t2 } = result.current;
@@ -165,14 +159,11 @@ describe('useTranslationRef', () => {
languageApi,
});
const { result, waitForNextUpdate } = renderHook(
() => useTranslationRef(resourceRef),
{
wrapper: makeWrapper(translationApi),
},
);
const { result } = renderHook(() => useTranslationRef(resourceRef), {
wrapper: makeWrapper(translationApi),
});
await waitForNextUpdate();
await act(async () => {});
const { t } = result.current;
@@ -212,7 +203,7 @@ describe('useTranslationRef', () => {
});
const { error } = await withLogCollector(['error'], async () => {
await rendered2.waitForNextUpdate();
await act(rendered2.rerender);
});
const msg =
@@ -21,7 +21,7 @@ import {
RELATION_PART_OF,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { filter, keyBy } from 'lodash';
import { useEntityRelationGraph as useEntityRelationGraphMocked } from './useEntityRelationGraph';
import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges';
@@ -163,7 +163,7 @@ describe('useEntityRelationNodesAndEdges', () => {
});
test('should generate unidirectional graph with merged relations', async () => {
const { result, waitForValueToChange } = renderHook(() =>
const { result } = renderHook(() =>
useEntityRelationNodesAndEdges({
rootEntityRefs: ['b:d/c'],
unidirectional: true,
@@ -171,9 +171,9 @@ describe('useEntityRelationNodesAndEdges', () => {
}),
);
await waitForValueToChange(
() => result.current.nodes && result.current.edges,
);
await waitFor(() => {
expect(result.current.nodes && result.current.edges).toBeDefined();
});
const { nodes, edges, loading, error } = result.current;
@@ -236,7 +236,7 @@ describe('useEntityRelationNodesAndEdges', () => {
});
test('should generate unidirectional graph', async () => {
const { result, waitForValueToChange } = renderHook(() =>
const { result } = renderHook(() =>
useEntityRelationNodesAndEdges({
rootEntityRefs: ['b:d/c'],
unidirectional: true,
@@ -244,9 +244,9 @@ describe('useEntityRelationNodesAndEdges', () => {
}),
);
await waitForValueToChange(
() => result.current.nodes && result.current.edges,
);
await waitFor(() => {
expect(result.current.nodes && result.current.edges).toBeDefined();
});
const { nodes, edges, loading, error } = result.current;
@@ -309,7 +309,7 @@ describe('useEntityRelationNodesAndEdges', () => {
});
test('should generate bidirectional graph with merged relations', async () => {
const { result, waitForValueToChange } = renderHook(() =>
const { result } = renderHook(() =>
useEntityRelationNodesAndEdges({
rootEntityRefs: ['b:d/c'],
unidirectional: false,
@@ -317,9 +317,9 @@ describe('useEntityRelationNodesAndEdges', () => {
}),
);
await waitForValueToChange(
() => result.current.nodes && result.current.edges,
);
await waitFor(() => {
expect(result.current.nodes && result.current.edges).toBeDefined();
});
const { nodes, edges, loading, error } = result.current;
@@ -412,7 +412,7 @@ describe('useEntityRelationNodesAndEdges', () => {
});
test('should generate bidirectional graph with all relations', async () => {
const { result, waitForValueToChange } = renderHook(() =>
const { result } = renderHook(() =>
useEntityRelationNodesAndEdges({
rootEntityRefs: ['b:d/c'],
unidirectional: false,
@@ -420,9 +420,9 @@ describe('useEntityRelationNodesAndEdges', () => {
}),
);
await waitForValueToChange(
() => result.current.nodes && result.current.edges,
);
await waitFor(() => {
expect(result.current.nodes && result.current.edges).toBeDefined();
});
const { nodes, edges, loading, error } = result.current;
@@ -515,15 +515,15 @@ describe('useEntityRelationNodesAndEdges', () => {
});
test('should generate graph with multiple root nodes', async () => {
const { result, waitForValueToChange } = renderHook(() =>
const { result } = renderHook(() =>
useEntityRelationNodesAndEdges({
rootEntityRefs: ['b:d/c', 'b:d/c2'],
}),
);
await waitForValueToChange(
() => result.current.nodes && result.current.edges,
);
await waitFor(() => {
expect(result.current.nodes && result.current.edges).toBeDefined();
});
const { nodes, edges, loading, error } = result.current;
@@ -586,16 +586,16 @@ describe('useEntityRelationNodesAndEdges', () => {
});
test('should filter by relation', async () => {
const { result, waitForValueToChange } = renderHook(() =>
const { result } = renderHook(() =>
useEntityRelationNodesAndEdges({
rootEntityRefs: ['b:d/c'],
relations: [RELATION_OWNER_OF],
}),
);
await waitForValueToChange(
() => result.current.nodes && result.current.edges,
);
await waitFor(() => {
expect(result.current.nodes && result.current.edges).toBeDefined();
});
const { nodes, edges, loading, error } = result.current;
@@ -646,16 +646,16 @@ describe('useEntityRelationNodesAndEdges', () => {
});
test('should filter by kind', async () => {
const { result, waitForValueToChange } = renderHook(() =>
const { result } = renderHook(() =>
useEntityRelationNodesAndEdges({
rootEntityRefs: ['b:d/c'],
kinds: ['b'],
}),
);
await waitForValueToChange(
() => result.current.nodes && result.current.edges,
);
await waitFor(() => {
expect(result.current.nodes && result.current.edges).toBeDefined();
});
const { nodes, edges, loading, error } = result.current;
@@ -16,7 +16,7 @@
import { Entity } from '@backstage/catalog-model';
import { useApi as useApiMocked } from '@backstage/core-plugin-api';
import { act, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import { useEntityStore } from './useEntityStore';
jest.mock('@backstage/core-plugin-api');
@@ -65,7 +65,7 @@ describe('useEntityStore', () => {
catalogApi.getEntityByRef.mockResolvedValue(entity);
const { result, waitFor } = renderHook(() => useEntityStore());
const { result } = renderHook(() => useEntityStore());
act(() => {
result.current.requestEntities(['kind:namespace/name']);
@@ -85,7 +85,7 @@ describe('useEntityStore', () => {
const err = new Error('Hello World');
catalogApi.getEntityByRef.mockRejectedValue(err);
const { result, waitFor } = renderHook(() => useEntityStore());
const { result } = renderHook(() => useEntityStore());
act(() => {
result.current.requestEntities(['kind:namespace/name']);
@@ -134,7 +134,7 @@ describe('useEntityStore', () => {
catalogApi.getEntityByRef.mockResolvedValue(entity1);
const { result, waitFor } = renderHook(() => useEntityStore());
const { result } = renderHook(() => useEntityStore());
act(() => {
result.current.requestEntities(['kind:namespace/name1']);
@@ -189,7 +189,7 @@ describe('useEntityStore', () => {
catalogApi.getEntityByRef.mockResolvedValue(entity1);
const { result, waitFor } = renderHook(() => useEntityStore());
const { result } = renderHook(() => useEntityStore());
act(() => {
result.current.requestEntities(['kind:namespace/name1']);
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { useFacetsEntities } from './useFacetsEntities';
import { CatalogApi } from '@backstage/catalog-client';
@@ -50,9 +50,7 @@ describe('useFacetsEntities', () => {
},
});
const { result, waitFor } = renderHook(() =>
useFacetsEntities({ enabled: true }),
);
const { result } = renderHook(() => useFacetsEntities({ enabled: true }));
result.current[1]({ text: '' });
await waitFor(() => {
@@ -91,9 +89,7 @@ describe('useFacetsEntities', () => {
},
});
const { result, waitFor } = renderHook(() =>
useFacetsEntities({ enabled: true }),
);
const { result } = renderHook(() => useFacetsEntities({ enabled: true }));
result.current[1]({ text: '' });
await waitFor(() => {
@@ -147,9 +143,7 @@ describe('useFacetsEntities', () => {
},
});
const { result, waitFor } = renderHook(() =>
useFacetsEntities({ enabled: true }),
);
const { result } = renderHook(() => useFacetsEntities({ enabled: true }));
result.current[1]({ text: '' }, { limit: 2 });
await waitFor(() => {
@@ -261,9 +255,7 @@ describe('useFacetsEntities', () => {
},
});
const { result, waitFor } = renderHook(() =>
useFacetsEntities({ enabled: true }),
);
const { result } = renderHook(() => useFacetsEntities({ enabled: true }));
result.current[1]({ text: 'der ' });
await waitFor(() => {
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { CatalogApi } from '@backstage/catalog-client';
import { useQueryEntities } from './useQueryEntities';
@@ -62,7 +62,7 @@ describe('useQueryEntities', () => {
totalItems: 2,
});
const { result, waitFor } = renderHook(() => useQueryEntities());
const { result } = renderHook(() => useQueryEntities());
const [, fetch] = result.current!;
fetch({ text: 'text' });
@@ -17,12 +17,9 @@
import { CatalogApi, Location } from '@backstage/catalog-client';
import { Entity, ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model';
import { catalogApiRef } from '../../api';
import { act, renderHook, RenderHookResult } from '@testing-library/react';
import React, { ReactNode } from 'react';
import {
UseUnregisterEntityDialogState,
useUnregisterEntityDialogState,
} from './useUnregisterEntityDialogState';
import { act, renderHook } from '@testing-library/react';
import React from 'react';
import { useUnregisterEntityDialogState } from './useUnregisterEntityDialogState';
import { TestApiProvider } from '@backstage/test-utils';
function defer<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
@@ -81,26 +78,20 @@ describe('useUnregisterEntityDialogState', () => {
});
it('goes through the happy unregister path', async () => {
let rendered: RenderHookResult<
{ children?: ReactNode },
UseUnregisterEntityDialogState
>;
act(() => {
rendered = renderHook(() => useUnregisterEntityDialogState(entity), {
wrapper: Wrapper,
});
const rendered = renderHook(() => useUnregisterEntityDialogState(entity), {
wrapper: Wrapper,
});
expect(rendered!.result.current).toEqual({ type: 'loading' });
expect(rendered.result.current).toEqual({ type: 'loading' });
resolveLocation({ type: 'url', target: 'https://example.com', id: 'x' });
resolveColocatedEntities([entity]);
await act(async () => {
await rendered!.waitForNextUpdate();
await rendered.rerender();
});
expect(rendered!.result.current).toEqual({
expect(rendered.result.current).toEqual({
type: 'unregister',
location: 'url:https://example.com',
colocatedEntities: [{ kind: 'Component', namespace: 'ns', name: 'n' }],
@@ -113,23 +104,17 @@ describe('useUnregisterEntityDialogState', () => {
entity.metadata.annotations![ANNOTATION_ORIGIN_LOCATION] =
'bootstrap:bootstrap';
let rendered: RenderHookResult<
{ children?: ReactNode },
UseUnregisterEntityDialogState
>;
act(() => {
rendered = renderHook(() => useUnregisterEntityDialogState(entity), {
wrapper: Wrapper,
});
const rendered = renderHook(() => useUnregisterEntityDialogState(entity), {
wrapper: Wrapper,
});
resolveLocation({ type: 'bootstrap', target: 'bootstrap', id: 'x' });
resolveColocatedEntities([]);
await act(async () => {
await rendered!.waitForNextUpdate();
await rendered.rerender();
});
expect(rendered!.result.current).toEqual({
expect(rendered.result.current).toEqual({
type: 'bootstrap',
location: 'bootstrap:bootstrap',
deleteEntity: expect.any(Function),
@@ -139,46 +124,34 @@ describe('useUnregisterEntityDialogState', () => {
it('chooses only-delete when there was no location annotation', async () => {
delete entity.metadata.annotations![ANNOTATION_ORIGIN_LOCATION];
let rendered: RenderHookResult<
{ children?: ReactNode },
UseUnregisterEntityDialogState
>;
act(() => {
rendered = renderHook(() => useUnregisterEntityDialogState(entity), {
wrapper: Wrapper,
});
const rendered = renderHook(() => useUnregisterEntityDialogState(entity), {
wrapper: Wrapper,
});
resolveLocation(undefined);
resolveColocatedEntities([]);
await act(async () => {
await rendered!.waitForNextUpdate();
await rendered.rerender();
});
expect(rendered!.result.current).toEqual({
expect(rendered.result.current).toEqual({
type: 'only-delete',
deleteEntity: expect.any(Function),
});
});
it('chooses only-delete when the location could not be found', async () => {
let rendered: RenderHookResult<
{ children?: ReactNode },
UseUnregisterEntityDialogState
>;
act(() => {
rendered = renderHook(() => useUnregisterEntityDialogState(entity), {
wrapper: Wrapper,
});
const rendered = renderHook(() => useUnregisterEntityDialogState(entity), {
wrapper: Wrapper,
});
resolveLocation(undefined);
resolveColocatedEntities([]);
await act(async () => {
await rendered!.waitForNextUpdate();
await rendered.rerender();
});
expect(rendered!.result.current).toEqual({
expect(rendered.result.current).toEqual({
type: 'only-delete',
deleteEntity: expect.any(Function),
});
@@ -31,13 +31,13 @@ const entity = { metadata: { name: 'my-entity' }, kind: 'MyKind' } as Entity;
describe('useEntity', () => {
it('should throw if no entity is provided', async () => {
const { result } = renderHook(() => useEntity(), {
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
<EntityProvider children={children} />
),
});
expect(result.error?.message).toMatch(/entity has not been loaded/);
expect(() =>
renderHook(() => useEntity(), {
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
<EntityProvider children={children} />
),
}),
).toThrow(/entity has not been loaded/);
});
it('should provide an entity', async () => {
@@ -25,7 +25,7 @@ import {
storageApiRef,
} from '@backstage/core-plugin-api';
import { MockStorageApi, TestApiProvider } from '@backstage/test-utils';
import { act, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import qs from 'qs';
import React, { PropsWithChildren } from 'react';
import { MemoryRouter } from 'react-router-dom';
@@ -119,10 +119,14 @@ describe('<EntityListProvider />', () => {
});
it('resolves backend filters', async () => {
const { result, waitForValueToChange } = renderHook(() => useEntityList(), {
const { result } = renderHook(() => useEntityList(), {
wrapper,
});
await waitForValueToChange(() => result.current.backendEntities);
await waitFor(() => {
expect(result.current.backendEntities.length).toBeGreaterThan(0);
});
expect(result.current.backendEntities.length).toBe(2);
expect(mockCatalogApi.getEntities).toHaveBeenCalledWith({
filter: { kind: 'component' },
@@ -130,13 +134,16 @@ describe('<EntityListProvider />', () => {
});
it('resolves frontend filters', async () => {
const { result, waitFor } = renderHook(() => useEntityList(), {
const { result } = renderHook(() => useEntityList(), {
wrapper,
initialProps: {
userFilter: 'all',
},
});
await waitFor(() => !!result.current.entities.length);
await waitFor(() => {
expect(result.current.backendEntities.length).toBeGreaterThan(0);
});
expect(result.current.backendEntities.length).toBe(2);
act(() =>
@@ -160,13 +167,15 @@ describe('<EntityListProvider />', () => {
const query = qs.stringify({
filters: { kind: 'component', type: 'service' },
});
const { result, waitFor } = renderHook(() => useEntityList(), {
wrapper,
initialProps: {
location: `/catalog?${query}`,
},
const { result } = renderHook(() => useEntityList(), {
wrapper: ({ children }) =>
wrapper({ location: `/catalog?${query}`, children }),
});
await act(async () => {});
await waitFor(() => {
expect(result.current.queryParameters).toBeTruthy();
});
await act(() => waitFor(() => !!result.current.queryParameters));
expect(result.current.queryParameters).toEqual({
kind: 'component',
type: 'service',
@@ -174,7 +183,7 @@ describe('<EntityListProvider />', () => {
});
it('does not fetch when only frontend filters change', async () => {
const { result, waitFor } = renderHook(() => useEntityList(), {
const { result } = renderHook(() => useEntityList(), {
wrapper,
});
@@ -200,32 +209,34 @@ describe('<EntityListProvider />', () => {
});
it('debounces multiple filter changes', async () => {
const { result, waitForNextUpdate, waitForValueToChange } = renderHook(
() => useEntityList(),
{
wrapper,
},
);
await waitForValueToChange(() => result.current.backendEntities);
const { result } = renderHook(() => useEntityList(), {
wrapper,
});
await waitFor(() => {
expect(result.current.backendEntities.length).toBeGreaterThan(0);
});
expect(result.current.backendEntities.length).toBe(2);
expect(mockCatalogApi.getEntities).toHaveBeenCalledTimes(1);
act(() => {
await act(async () => {
result.current.updateFilters({ kind: new EntityKindFilter('component') });
result.current.updateFilters({ type: new EntityTypeFilter('service') });
});
await waitForNextUpdate();
expect(mockCatalogApi.getEntities).toHaveBeenCalledTimes(2);
await waitFor(() => {
expect(mockCatalogApi.getEntities).toHaveBeenCalledTimes(2);
});
});
it('returns an error on catalogApi failure', async () => {
const { result, waitForValueToChange, waitFor } = renderHook(
() => useEntityList(),
{
wrapper,
},
);
await waitForValueToChange(() => result.current.backendEntities);
const { result } = renderHook(() => useEntityList(), {
wrapper,
});
await waitFor(() => {
expect(result.current.backendEntities.length).toBeGreaterThan(0);
});
expect(result.current.backendEntities.length).toBe(2);
mockCatalogApi.getEntities = jest.fn().mockRejectedValue('error');
@@ -18,7 +18,7 @@ import { CatalogApi } from '@backstage/catalog-client';
import { ComponentEntity, RELATION_OWNED_BY } from '@backstage/catalog-model';
import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
import { TestApiProvider } from '@backstage/test-utils';
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import React from 'react';
import { catalogApiRef } from '../api';
import { useEntityOwnership } from './useEntityOwnership';
@@ -83,19 +83,15 @@ describe('useEntityOwnership', () => {
});
mockCatalogApi.getEntityByRef.mockResolvedValue(undefined);
const { result, waitForValueToChange } = renderHook(
() => useEntityOwnership(),
{
wrapper: Wrapper,
},
);
const { result } = renderHook(() => useEntityOwnership(), {
wrapper: Wrapper,
});
expect(result.current.loading).toBe(true);
expect(result.current.isOwnedEntity(ownedEntity)).toBe(false);
await waitForValueToChange(() => result.current.loading);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.loading).toBe(false);
expect(result.current.isOwnedEntity(ownedEntity)).toBe(true);
});
});
@@ -16,8 +16,8 @@
import { Entity } from '@backstage/catalog-model';
import { TestApiProvider } from '@backstage/test-utils';
import { WrapperComponent, renderHook } from '@testing-library/react';
import React, { PropsWithChildren } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import React, { ComponentType, PropsWithChildren } from 'react';
import { catalogApiRef } from '../api';
import { useRelatedEntities } from './useRelatedEntities';
@@ -50,7 +50,7 @@ describe('useRelatedEntities', () => {
getEntitiesByRefs: jest.fn(),
};
const wrapper: WrapperComponent<PropsWithChildren<{}>> = ({ children }) => {
const wrapper: ComponentType<PropsWithChildren<{}>> = ({ children }) => {
return (
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
{children}
@@ -70,7 +70,9 @@ describe('useRelatedEntities', () => {
expect(rendered.result.current).toEqual({ loading: true });
await rendered.waitForValueToChange(() => rendered.result.current.loading);
await waitFor(() => {
expect(rendered.result.current.loading).toBe(false);
});
expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledWith({
entityRefs: ['group:default/the-owners-1', 'group:default/the-owners-2'],
@@ -16,7 +16,7 @@
import { Entity } from '@backstage/catalog-model';
import { TestApiProvider } from '@backstage/test-utils';
import { act, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import React, { PropsWithChildren } from 'react';
import {
starredEntitiesApiRef,
@@ -56,12 +56,11 @@ describe('useStarredEntities', () => {
});
it('should return an empty set', async () => {
const { result, waitForNextUpdate } = renderHook(
() => useStarredEntities(),
{ wrapper },
);
const { result } = renderHook(() => useStarredEntities(), {
wrapper,
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current.starredEntities.size).toBe(0);
});
@@ -72,12 +71,11 @@ describe('useStarredEntities', () => {
mockApi.toggleStarred(id);
}
const { result, waitForNextUpdate } = renderHook(
() => useStarredEntities(),
{ wrapper },
);
const { result } = renderHook(() => useStarredEntities(), {
wrapper,
});
await waitForNextUpdate();
await act(async () => {});
for (const item of expectedIds) {
expect(result.current.starredEntities.has(item)).toBeTruthy();
@@ -85,12 +83,11 @@ describe('useStarredEntities', () => {
});
it('should listen to changes when the storage is set elsewhere', async () => {
const { result, waitForNextUpdate } = renderHook(
() => useStarredEntities(),
{ wrapper },
);
const { result } = renderHook(() => useStarredEntities(), {
wrapper,
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current.starredEntities.size).toBe(0);
expect(result.current.isStarredEntity(mockEntity)).toBeFalsy();
@@ -99,42 +96,37 @@ describe('useStarredEntities', () => {
// catch when the hook re-renders with the latest data
setTimeout(() => result.current.toggleStarredEntity(mockEntity), 1);
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.starredEntities.size).toBe(1);
});
expect(result.current.starredEntities.size).toBe(1);
expect(result.current.isStarredEntity(mockEntity)).toBeTruthy();
});
it('should write new entries to the local store when adding a toggling entity', async () => {
const { result, waitForNextUpdate } = renderHook(
() => useStarredEntities(),
{ wrapper },
);
act(() => {
result.current.toggleStarredEntity(mockEntity);
const { result } = renderHook(() => useStarredEntities(), {
wrapper,
});
await waitForNextUpdate();
await act(async () => {
result.current.toggleStarredEntity(mockEntity);
});
expect(result.current.isStarredEntity(mockEntity)).toBeTruthy();
expect(result.current.isStarredEntity(secondMockEntity)).toBeFalsy();
});
it('should remove an existing entity when toggling entries', async () => {
const { result, waitForNextUpdate } = renderHook(
() => useStarredEntities(),
{ wrapper },
);
const { result } = renderHook(() => useStarredEntities(), {
wrapper,
});
act(() => {
await act(async () => {
result.current.toggleStarredEntity(mockEntity);
result.current.toggleStarredEntity(secondMockEntity);
result.current.toggleStarredEntity(mockEntity);
});
await waitForNextUpdate();
expect(result.current.isStarredEntity(mockEntity)).toBeFalsy();
expect(result.current.isStarredEntity(secondMockEntity)).toBeTruthy();
});
@@ -20,7 +20,7 @@ import {
stringifyEntityRef,
} from '@backstage/catalog-model';
import { useApi } from '@backstage/core-plugin-api';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { starredEntitiesApiRef } from '../apis';
@@ -45,7 +45,7 @@ export function useStarredEntities(): {
const starredEntitiesApi = useApi(starredEntitiesApiRef);
const starredEntities = useObservable(
starredEntitiesApi.starredEntitie$(),
useMemo(() => starredEntitiesApi.starredEntitie$(), [starredEntitiesApi]),
new Set<string>(),
);
@@ -16,7 +16,7 @@
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
import { TestApiProvider } from '@backstage/test-utils';
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import React, { PropsWithChildren } from 'react';
import Observable from 'zen-observable';
import { StarredEntitiesApi, starredEntitiesApiRef } from '../apis';
@@ -83,18 +83,16 @@ describe('useStarredEntity', () => {
);
mockStarredEntitiesApi.toggleStarred.mockResolvedValue();
const { result, waitForNextUpdate } = renderHook(
() => useStarredEntity(entityOrRef),
{
wrapper,
},
);
const { result } = renderHook(() => useStarredEntity(entityOrRef), {
wrapper,
});
// the initial value will always be false because the observable triggers async
expect(result.current.isStarredEntity).toBe(false);
await waitForNextUpdate();
expect(result.current.isStarredEntity).toBe(true);
await waitFor(() => {
expect(result.current.isStarredEntity).toBe(true);
});
});
});
});
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { renderHook, act } from '@testing-library/react';
import { waitFor } from '@testing-library/react';
import {
mockCalverProject,
@@ -48,10 +47,9 @@ describe('useCreateReleaseCandidate', () => {
);
await act(async () => {
await waitFor(() => result.current.run());
await result.current.run();
});
expect(result.error).toEqual(undefined);
expect(result.current.responseSteps).toHaveLength(6);
});
@@ -67,7 +65,7 @@ describe('useCreateReleaseCandidate', () => {
);
await act(async () => {
await waitFor(() => result.current.run());
await result.current.run();
});
expect(result.current.responseSteps).toHaveLength(7);
@@ -15,7 +15,6 @@
*/
import { renderHook, act } from '@testing-library/react';
import { waitFor } from '@testing-library/react';
import {
mockBumpedTag,
@@ -50,10 +49,9 @@ describe('patch', () => {
);
await act(async () => {
await waitFor(() => result.current.run(mockSelectedPatchCommit));
await result.current.run(mockSelectedPatchCommit);
});
expect(result.error).toEqual(undefined);
expect(result.current.responseSteps).toHaveLength(18);
});
@@ -69,10 +67,9 @@ describe('patch', () => {
);
await act(async () => {
await waitFor(() => result.current.run(mockSelectedPatchCommit));
await result.current.run(mockSelectedPatchCommit);
});
expect(result.error).toEqual(undefined);
expect(result.current.responseSteps).toHaveLength(19);
expect(result.current).toMatchInlineSnapshot(`
{
@@ -15,8 +15,6 @@
*/
import { renderHook, act } from '@testing-library/react';
import { waitFor } from '@testing-library/react';
import {
mockCalverProject,
mockReleaseCandidateCalver,
@@ -50,10 +48,9 @@ describe('usePromoteRc', () => {
);
await act(async () => {
await waitFor(() => result.current.run());
await result.current.run();
});
expect(result.error).toEqual(undefined);
expect(result.current.responseSteps).toHaveLength(4);
});
@@ -67,7 +64,7 @@ describe('usePromoteRc', () => {
);
await act(async () => {
await waitFor(() => result.current.run());
await result.current.run();
});
expect(result.current.responseSteps).toHaveLength(5);
@@ -165,12 +165,8 @@ describe('useConsumerGroupOffsets', () => {
lifecycle: 'development',
},
};
const { result } = subject();
expect(() => result.current).toThrow();
expect(result.error).toStrictEqual(
new Error(
`Failed to parse kafka consumer group annotation: got "dev/another,consumer"`,
),
expect(() => subject()).toThrow(
`Failed to parse kafka consumer group annotation: got "dev/another,consumer"`,
);
});
});
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { when } from 'jest-when';
import React, { PropsWithChildren } from 'react';
import {
@@ -96,27 +96,28 @@ describe('useConsumerGroupOffsets', () => {
.mockResolvedValue(consumerGroupOffsets);
when(mockKafkaDashboardApi.getDashboardUrl).mockReturnValue({});
const { result, waitForNextUpdate } = subject();
await waitForNextUpdate();
const [tableProps] = result.current;
const { result } = subject();
expect(tableProps.consumerGroupsTopics).toStrictEqual([
{
clusterId: 'prod',
consumerGroup: consumerGroupOffsets.consumerId,
dashboardUrl: undefined,
topics: consumerGroupOffsets.offsets,
},
]);
await waitFor(() => {
expect(result.current[0].consumerGroupsTopics).toStrictEqual([
{
clusterId: 'prod',
consumerGroup: consumerGroupOffsets.consumerId,
dashboardUrl: undefined,
topics: consumerGroupOffsets.offsets,
},
]);
});
});
it('posts an error to the error api', async () => {
const error = new Error('error!');
mockKafkaApi.getConsumerGroupOffsets.mockRejectedValueOnce(error);
const { waitForNextUpdate } = subject();
await waitForNextUpdate();
subject();
expect(mockErrorApi.post).toHaveBeenCalledWith(error);
await waitFor(() => {
expect(mockErrorApi.post).toHaveBeenCalledWith(error);
});
});
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { useApi } from '@backstage/core-plugin-api';
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { useEvents } from './useEvents';
import { DateTime } from 'luxon';
@@ -49,7 +49,7 @@ describe('Events', () => {
mockGetEventsByInvolvedObjectName.mockResolvedValue(response),
});
const { result, waitForNextUpdate } = renderHook(() =>
const { result } = renderHook(() =>
useEvents({
involvedObjectName: 'some-objecgt',
namespace: 'some-namespace',
@@ -59,7 +59,9 @@ describe('Events', () => {
expect(result.current.loading).toEqual(true);
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.loading).toEqual(false);
});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
@@ -16,7 +16,7 @@
import { useCustomResources } from './useCustomResources';
import { Entity } from '@backstage/catalog-model';
import { renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import { useApi } from '@backstage/core-plugin-api';
import { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common';
import { generateAuth } from './auth';
@@ -97,13 +97,13 @@ describe('useCustomResources', () => {
getCustomObjectsByEntity:
mockGetCustomObjectsByEntity.mockResolvedValue(mockResponse),
});
const { result, waitForNextUpdate } = renderHook(() =>
const { result } = renderHook(() =>
useCustomResources(entity, customResourceMatchers),
);
expect(result.current.loading).toEqual(true);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
@@ -117,20 +117,20 @@ describe('useCustomResources', () => {
getCustomObjectsByEntity:
mockGetCustomObjectsByEntity.mockResolvedValue(mockResponse),
});
const { result, waitForNextUpdate } = renderHook(() =>
const { result } = renderHook(() =>
useCustomResources(entity, customResourceMatchers, 100),
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBeUndefined();
await waitForNextUpdate();
await waitFor(() => {
expectMocksCalledCorrectly(2);
});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
expect(result.current.kubernetesObjects).toStrictEqual(mockResponse);
expectMocksCalledCorrectly(2);
});
it('should return error when getObjectsByEntity throws', async () => {
mockGenerateAuth.mockResolvedValue(entityWithAuthToken.auth);
@@ -139,11 +139,11 @@ describe('useCustomResources', () => {
message: 'some error',
}),
});
const { result, waitForNextUpdate } = renderHook(() =>
const { result } = renderHook(() =>
useCustomResources(entity, customResourceMatchers),
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBe('some error');
expect(result.current.loading).toEqual(false);
@@ -162,17 +162,19 @@ describe('useCustomResources', () => {
mockGetCustomObjectsByEntity.mockResolvedValue(mockResponse),
});
const { result, waitForNextUpdate } = renderHook(() =>
const { result } = renderHook(() =>
useCustomResources(entity, customResourceMatchers, 100),
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBe('generateAuth failed');
expect(result.current.loading).toEqual(false);
expect(result.current.kubernetesObjects).toBeUndefined();
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.error).toBeUndefined();
});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
@@ -186,17 +188,19 @@ describe('useCustomResources', () => {
.mockResolvedValue(mockResponse),
});
const { result, waitForNextUpdate } = renderHook(() =>
const { result } = renderHook(() =>
useCustomResources(entity, customResourceMatchers, 100),
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBe('failed to fetch');
expect(result.current.loading).toEqual(false);
expect(result.current.kubernetesObjects).toBeUndefined();
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.error).toBeUndefined();
});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
@@ -212,17 +216,19 @@ describe('useCustomResources', () => {
mockGetCustomObjectsByEntity.mockResolvedValue(mockResponse),
});
const { result, waitForNextUpdate } = renderHook(() =>
const { result } = renderHook(() =>
useCustomResources(entity, customResourceMatchers, 100),
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
expect(result.current.kubernetesObjects).not.toBeUndefined();
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.error).toBeDefined();
});
expect(result.current.error).toBe('generateAuth failed');
expect(result.current.loading).toEqual(false);
@@ -236,17 +242,19 @@ describe('useCustomResources', () => {
.mockRejectedValue({ message: 'failed to fetch' }),
});
const { result, waitForNextUpdate } = renderHook(() =>
const { result } = renderHook(() =>
useCustomResources(entity, customResourceMatchers, 100),
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
expect(result.current.kubernetesObjects).not.toBeUndefined();
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.error).toBeDefined();
});
expect(result.current.error).toBe('failed to fetch');
expect(result.current.loading).toEqual(false);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { useApi } from '@backstage/core-plugin-api';
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { useIsPodExecTerminalSupported } from './useIsPodExecTerminalSupported';
@@ -68,15 +68,14 @@ describe('useIsClusterShellEnabled', () => {
async ({ testClusters, returnValue }) => {
clusters = testClusters;
const { result, waitForNextUpdate } = renderHook(() =>
useIsPodExecTerminalSupported(),
);
const { result } = renderHook(() => useIsPodExecTerminalSupported());
expect(result.current.loading).toEqual(true);
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.loading).toEqual(false);
});
expect(result.current.loading).toEqual(false);
expect(result.current.value).toBe(returnValue);
},
);
@@ -16,7 +16,7 @@
import { useKubernetesObjects } from './useKubernetesObjects';
import { Entity } from '@backstage/catalog-model';
import { renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import { useApi } from '@backstage/core-plugin-api';
import { generateAuth } from './auth';
@@ -87,13 +87,13 @@ describe('useKubernetesObjects', () => {
getObjectsByEntity:
mockGetObjectsByEntity.mockResolvedValue(mockResponse),
});
const { result, waitForNextUpdate } = renderHook(() =>
useKubernetesObjects(entity),
);
const { result } = renderHook(() => useKubernetesObjects(entity));
expect(result.current.loading).toEqual(true);
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.loading).toEqual(false);
});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
@@ -107,14 +107,13 @@ describe('useKubernetesObjects', () => {
getObjectsByEntity:
mockGetObjectsByEntity.mockResolvedValue(mockResponse),
});
const { result, waitForNextUpdate } = renderHook(() =>
useKubernetesObjects(entity, 100),
);
const { result } = renderHook(() => useKubernetesObjects(entity, 100));
await waitForNextUpdate();
expect(result.current.error).toBeUndefined();
await waitForNextUpdate();
await waitFor(() => {
expectMocksCalledCorrectly(2);
});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
@@ -129,11 +128,9 @@ describe('useKubernetesObjects', () => {
message: 'some error',
}),
});
const { result, waitForNextUpdate } = renderHook(() =>
useKubernetesObjects(entity),
);
const { result } = renderHook(() => useKubernetesObjects(entity));
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBe('some error');
expect(result.current.loading).toEqual(false);
@@ -152,17 +149,17 @@ describe('useKubernetesObjects', () => {
mockGetObjectsByEntity.mockResolvedValue(mockResponse),
});
const { result, waitForNextUpdate } = renderHook(() =>
useKubernetesObjects(entity, 100),
);
const { result } = renderHook(() => useKubernetesObjects(entity, 100));
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBe('generateAuth failed');
expect(result.current.loading).toEqual(false);
expect(result.current.kubernetesObjects).toBeUndefined();
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.error).toBeUndefined();
});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
@@ -176,17 +173,17 @@ describe('useKubernetesObjects', () => {
.mockResolvedValue(mockResponse),
});
const { result, waitForNextUpdate } = renderHook(() =>
useKubernetesObjects(entity, 100),
);
const { result } = renderHook(() => useKubernetesObjects(entity, 100));
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBe('failed to fetch');
expect(result.current.loading).toEqual(false);
expect(result.current.kubernetesObjects).toBeUndefined();
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.error).toBeUndefined();
});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
@@ -202,17 +199,17 @@ describe('useKubernetesObjects', () => {
mockGetObjectsByEntity.mockResolvedValue(mockResponse),
});
const { result, waitForNextUpdate } = renderHook(() =>
useKubernetesObjects(entity, 100),
);
const { result } = renderHook(() => useKubernetesObjects(entity, 100));
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
expect(result.current.kubernetesObjects).not.toBeUndefined();
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.error).toBeDefined();
});
expect(result.current.error).toBe('generateAuth failed');
expect(result.current.loading).toEqual(false);
@@ -226,17 +223,17 @@ describe('useKubernetesObjects', () => {
.mockRejectedValue({ message: 'failed to fetch' }),
});
const { result, waitForNextUpdate } = renderHook(() =>
useKubernetesObjects(entity, 100),
);
const { result } = renderHook(() => useKubernetesObjects(entity, 100));
await waitForNextUpdate();
await act(async () => {});
expect(result.current.error).toBeUndefined();
expect(result.current.loading).toEqual(false);
expect(result.current.kubernetesObjects).not.toBeUndefined();
await waitForNextUpdate();
await waitFor(() => {
expect(result.current.error).toBeDefined();
});
expect(result.current.error).toBe('failed to fetch');
expect(result.current.loading).toEqual(false);
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderHook } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react';
import React, { PropsWithChildren } from 'react';
import { WebsiteListResponse } from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from '../api';
@@ -77,8 +77,8 @@ describe('useWebsiteForEntity', () => {
});
it('returns the lighthouse information for the website url in annotations', async () => {
const { result, waitForNextUpdate } = subject();
await waitForNextUpdate();
const { result } = subject();
await act(async () => {});
expect(result.current?.value).toBe(website);
});
@@ -92,8 +92,8 @@ describe('useWebsiteForEntity', () => {
});
it('posts the error to the error api and returns the error to the caller', async () => {
const { result, waitForNextUpdate } = subject();
await waitForNextUpdate();
const { result } = subject();
await act(async () => {});
expect(result.current?.error).toBe(error);
expect(mockErrorApi.post).toHaveBeenCalledWith(error);
});
@@ -109,8 +109,8 @@ describe('useWebsiteForEntity', () => {
});
it('does not post the error to the error api and returns the error to the caller', async () => {
const { result, waitForNextUpdate } = subject();
await waitForNextUpdate();
const { result } = subject();
await act(async () => {});
expect(result.current?.error).toBe(error);
expect(mockErrorApi.post).not.toHaveBeenCalledWith(error);
});
@@ -16,7 +16,7 @@
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
import { useGetEntities } from './useGetEntities';
import { CatalogApi } from '@backstage/catalog-client';
import { renderHook } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react';
import { getEntityRelations } from '@backstage/plugin-catalog-react';
const givenParentGroup = 'team.squad1';
@@ -66,14 +66,11 @@ describe('useGetEntities', () => {
describe('given aggregated relationsType', () => {
const whenHookIsCalledWith = async (_entity: Entity) => {
const hook = renderHook(
({ entity }) => useGetEntities(entity, 'aggregated'),
{
initialProps: { entity: _entity },
},
);
renderHook(({ entity }) => useGetEntities(entity, 'aggregated'), {
initialProps: { entity: _entity },
});
await hook.waitForNextUpdate();
await act(async () => {});
};
beforeEach(() => {
@@ -207,14 +204,11 @@ describe('useGetEntities', () => {
describe('given direct relationsType', () => {
const whenHookIsCalledWith = async (_entity: Entity) => {
const hook = renderHook(
({ entity }) => useGetEntities(entity, 'direct'),
{
initialProps: { entity: _entity },
},
);
renderHook(({ entity }) => useGetEntities(entity, 'direct'), {
initialProps: { entity: _entity },
});
await hook.waitForNextUpdate();
await act(async () => {});
};
it('given group entity should return directly owned entities', async () => {
@@ -22,7 +22,7 @@ import {
} from '@backstage/core-plugin-api';
import { Playlist } from '@backstage/plugin-playlist-common';
import { TestApiProvider } from '@backstage/test-utils';
import { act, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import qs from 'qs';
import React, { PropsWithChildren } from 'react';
import { MemoryRouter } from 'react-router-dom';
@@ -114,23 +114,22 @@ describe('<PlaylistListProvider />', () => {
});
it('resolves backend filters', async () => {
const { result, waitForValueToChange } = renderHook(
() => usePlaylistList(),
{
wrapper,
},
);
await waitForValueToChange(() => result.current.backendPlaylists);
expect(result.current.backendPlaylists.length).toBe(2);
const { result } = renderHook(() => usePlaylistList(), {
wrapper,
});
await waitFor(() => {
expect(result.current.backendPlaylists.length).toBe(2);
});
expect(mockPlaylistApi.getAllPlaylists).toHaveBeenCalled();
});
it('resolves frontend filters', async () => {
const { result, waitFor } = renderHook(() => usePlaylistList(), {
const { result } = renderHook(() => usePlaylistList(), {
wrapper,
});
await waitFor(() => !!result.current.playlists.length);
expect(result.current.backendPlaylists.length).toBe(2);
await waitFor(() => {
expect(result.current.backendPlaylists.length).toBe(2);
});
act(() =>
result.current.updateFilters({
@@ -152,13 +151,15 @@ describe('<PlaylistListProvider />', () => {
const query = qs.stringify({
filters: { personal: 'all', owners: ['user:default/guest'] },
});
const { result, waitFor } = renderHook(() => usePlaylistList(), {
wrapper,
initialProps: {
location: `/playlist?${query}`,
},
const { result } = renderHook(() => usePlaylistList(), {
wrapper: ({ children }) =>
wrapper({ location: `/playlist?${query}`, children }),
});
await act(async () => {});
await waitFor(() => {
expect(result.current.queryParameters).toBeTruthy();
});
await waitFor(() => !!result.current.queryParameters);
expect(result.current.queryParameters).toEqual({
personal: 'all',
owners: ['user:default/guest'],
@@ -166,7 +167,7 @@ describe('<PlaylistListProvider />', () => {
});
it('does not fetch when only frontend filters change', async () => {
const { result, waitFor } = renderHook(() => usePlaylistList(), {
const { result } = renderHook(() => usePlaylistList(), {
wrapper,
});
@@ -191,7 +192,7 @@ describe('<PlaylistListProvider />', () => {
});
it('applies custom sorting', async () => {
const { result, waitFor } = renderHook(() => usePlaylistList(), {
const { result } = renderHook(() => usePlaylistList(), {
wrapper,
});
@@ -216,7 +217,7 @@ describe('<PlaylistListProvider />', () => {
it('returns an error on playlistApi failure', async () => {
mockPlaylistApi.getAllPlaylists = jest.fn().mockRejectedValue('error');
const { result, waitFor } = renderHook(() => usePlaylistList(), {
const { result } = renderHook(() => usePlaylistList(), {
wrapper,
});
await waitFor(() => {
@@ -16,7 +16,7 @@
import React from 'react';
import { ApiProvider } from '@backstage/core-app-api';
import { MockConfigApi, TestApiRegistry } from '@backstage/test-utils';
import { renderHook } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react';
import { searchApiRef } from '../../api';
import { SearchContextProvider, useSearch } from '../../context';
@@ -67,7 +67,7 @@ describe('SearchFilter.hooks', () => {
it('should set non-empty string value', async () => {
const expectedFilter = 'someField';
const expectedValue = 'someValue';
const { result, waitForNextUpdate } = renderHook(
const { result } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, expectedValue);
return useSearch();
@@ -77,7 +77,7 @@ describe('SearchFilter.hooks', () => {
},
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
@@ -85,7 +85,7 @@ describe('SearchFilter.hooks', () => {
it('should set non-empty array value', async () => {
const expectedFilter = 'someField';
const expectedValue = ['someValue', 'anotherValue'];
const { result, waitForNextUpdate } = renderHook(
const { result } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, expectedValue);
return useSearch();
@@ -95,7 +95,7 @@ describe('SearchFilter.hooks', () => {
},
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
@@ -103,24 +103,25 @@ describe('SearchFilter.hooks', () => {
it('should not set undefined value', async () => {
const expectedFilter = 'someField';
const expectedValue = 'notEmpty';
const { result, waitForNextUpdate } = renderHook(
const { result } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, undefined);
return useSearch();
},
{
wrapper,
initialProps: {
overrides: {
filters: {
[expectedFilter]: expectedValue,
wrapper: ({ children }) =>
wrapper({
children,
overrides: {
filters: {
[expectedFilter]: expectedValue,
},
},
},
},
}),
},
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
@@ -128,24 +129,25 @@ describe('SearchFilter.hooks', () => {
it('should not set null value', async () => {
const expectedFilter = 'someField';
const expectedValue = 'notEmpty';
const { result, waitForNextUpdate } = renderHook(
const { result } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, null);
return useSearch();
},
{
wrapper,
initialProps: {
overrides: {
filters: {
[expectedFilter]: expectedValue,
wrapper: ({ children }) =>
wrapper({
children,
overrides: {
filters: {
[expectedFilter]: expectedValue,
},
},
},
},
}),
},
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
@@ -153,24 +155,25 @@ describe('SearchFilter.hooks', () => {
it('should not set empty string value', async () => {
const expectedFilter = 'someField';
const expectedValue = 'notEmpty';
const { result, waitForNextUpdate } = renderHook(
const { result } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, '');
return useSearch();
},
{
wrapper,
initialProps: {
overrides: {
filters: {
[expectedFilter]: expectedValue,
wrapper: ({ children }) =>
wrapper({
children,
overrides: {
filters: {
[expectedFilter]: expectedValue,
},
},
},
},
}),
},
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
@@ -178,24 +181,25 @@ describe('SearchFilter.hooks', () => {
it('should not set empty array value', async () => {
const expectedFilter = 'someField';
const expectedValue = ['not', 'empty'];
const { result, waitForNextUpdate } = renderHook(
const { result } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, []);
return useSearch();
},
{
wrapper,
initialProps: {
overrides: {
filters: {
[expectedFilter]: expectedValue,
wrapper: ({ children }) =>
wrapper({
children,
overrides: {
filters: {
[expectedFilter]: expectedValue,
},
},
},
},
}),
},
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.filters[expectedFilter]).toEqual(expectedValue);
});
@@ -203,24 +207,25 @@ describe('SearchFilter.hooks', () => {
it('should not affect unrelated filters', async () => {
const expectedFilter = 'someField';
const expectedValue = 'someValue';
const { result, waitForNextUpdate } = renderHook(
const { result } = renderHook(
() => {
useDefaultFilterValue(expectedFilter, expectedValue);
return useSearch();
},
{
wrapper,
initialProps: {
overrides: {
filters: {
unrelatedField: 'unrelatedValue',
wrapper: ({ children }) =>
wrapper({
children,
overrides: {
filters: {
unrelatedField: 'unrelatedValue',
},
},
},
},
}),
},
);
await waitForNextUpdate();
await act(async () => {});
expect(result.current.filters.unrelatedField).toEqual('unrelatedValue');
});
@@ -240,14 +245,15 @@ describe('SearchFilter.hooks', () => {
it('should return resolved values of provided async function', async () => {
const expectedValues = ['value1', 'value2'];
const asyncFn = () => Promise.resolve(expectedValues);
const { result, waitForNextUpdate } = renderHook(() =>
const { result } = renderHook(() =>
useAsyncFilterValues(asyncFn, '', undefined, 1000),
);
expect(result.current.loading).toEqual(true);
jest.runAllTimers();
await waitForNextUpdate();
await act(async () => {
jest.runAllTimers();
});
expect(result.current.loading).toEqual(false);
expect(result.current.value).toEqual(expectedValues);
@@ -261,11 +267,15 @@ describe('SearchFilter.hooks', () => {
expect(asyncFn).not.toHaveBeenCalled();
// Advance timers by 600ms
jest.advanceTimersByTime(600);
await act(async () => {
jest.advanceTimersByTime(600);
});
expect(asyncFn).not.toHaveBeenCalled();
// Another 600ms to exceed the 1000ms debounce
jest.advanceTimersByTime(600);
await act(async () => {
jest.advanceTimersByTime(600);
});
expect(asyncFn).toHaveBeenCalled();
});
@@ -273,21 +283,23 @@ describe('SearchFilter.hooks', () => {
const asyncFn = jest
.fn()
.mockImplementation((x: string) => Promise.resolve([x]));
const { rerender, waitForNextUpdate } = renderHook(
const { rerender } = renderHook(
(props: { inputValue: string } = { inputValue: '' }) =>
useAsyncFilterValues(asyncFn, props.inputValue, undefined, 1000),
);
expect(asyncFn).not.toHaveBeenCalled();
jest.runAllTimers();
await waitForNextUpdate();
await act(async () => {
jest.runAllTimers();
});
expect(asyncFn).toHaveBeenCalledTimes(1);
expect(asyncFn).toHaveBeenCalledWith('');
// Re-render with different input value.
rerender({ inputValue: 'somethingElse' });
jest.runAllTimers();
await waitForNextUpdate();
await act(async () => {
jest.runAllTimers();
});
expect(asyncFn).toHaveBeenCalledTimes(2);
expect(asyncFn).toHaveBeenLastCalledWith('somethingElse');
});
@@ -295,15 +307,16 @@ describe('SearchFilter.hooks', () => {
it('should not call provided method more than once when re-rendered with same input', async () => {
const expectedValues = ['value1', 'value2'];
const asyncFn = jest.fn().mockResolvedValue(expectedValues);
const { rerender, waitForNextUpdate } = renderHook(
const { rerender } = renderHook(
(props: { inputValue: string } = { inputValue: '' }) =>
useAsyncFilterValues(asyncFn, props.inputValue, undefined, 1000),
);
expect(asyncFn).not.toHaveBeenCalled();
jest.runAllTimers();
await waitForNextUpdate();
await act(async () => {
jest.runAllTimers();
});
expect(asyncFn).toHaveBeenCalledTimes(1);
// Re-render multiple times with the same input.
@@ -15,8 +15,13 @@
*/
import { configApiRef } from '@backstage/core-plugin-api';
import { render, screen, waitFor } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react';
import {
render,
screen,
waitFor,
act,
renderHook,
} from '@testing-library/react';
import { MockConfigApi, TestApiProvider } from '@backstage/test-utils';
import React from 'react';
import {
@@ -70,10 +75,8 @@ describe('SearchContext', () => {
});
it('Throws error when no context is set', () => {
const { result } = renderHook(() => useSearch());
expect(result.error).toEqual(
Error('useSearch must be used within a SearchContextProvider'),
expect(() => renderHook(() => useSearch())).toThrow(
'useSearch must be used within a SearchContextProvider',
);
});
@@ -82,28 +85,22 @@ describe('SearchContext', () => {
expect(hook.result.current).toEqual(false);
const { result, waitForNextUpdate } = renderHook(
() => useSearchContextCheck(),
{
wrapper,
initialProps: {
initialState,
},
},
);
const { result } = renderHook(() => useSearchContextCheck(), {
wrapper: ({ children }) => wrapper({ children, initialState }),
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current).toEqual(true);
});
describe('Uses initial state values', () => {
it('Uses default initial state values', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
const { result } = renderHook(() => useSearch(), {
wrapper,
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current).toEqual(
expect.objectContaining({
@@ -117,34 +114,32 @@ describe('SearchContext', () => {
});
it('Uses provided initial state values', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState,
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) => wrapper({ children, initialState }),
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current).toEqual(expect.objectContaining(initialState));
});
it('Uses page limit provided via config api', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState,
config: {
search: {
query: {
pageLimit: 100,
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) =>
wrapper({
children,
initialState,
config: {
search: {
query: {
pageLimit: 100,
},
},
},
},
},
}),
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current).toEqual(
expect.objectContaining({ ...initialState, pageLimit: 100 }),
@@ -154,131 +149,122 @@ describe('SearchContext', () => {
describe('Resets cursor', () => {
it('When term is cleared', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState: {
...initialState,
term: 'first term',
pageCursor: 'SOMEPAGE',
},
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) =>
wrapper({
children,
initialState: {
...initialState,
term: 'first term',
pageCursor: 'SOMEPAGE',
},
}),
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current.term).toEqual('first term');
expect(result.current.pageCursor).toEqual('SOMEPAGE');
act(() => {
await act(async () => {
result.current.setTerm('');
});
await waitForNextUpdate();
expect(result.current.pageCursor).toBeUndefined();
});
it('When term is set (and different from previous)', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState: {
...initialState,
term: 'first term',
pageCursor: 'SOMEPAGE',
},
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) =>
wrapper({
children,
initialState: {
...initialState,
term: 'first term',
pageCursor: 'SOMEPAGE',
},
}),
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current.term).toEqual('first term');
expect(result.current.pageCursor).toEqual('SOMEPAGE');
act(() => {
await act(async () => {
result.current.setTerm('second term');
});
await waitForNextUpdate();
expect(result.current.pageCursor).toBeUndefined();
});
it('When filters are cleared', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState: {
...initialState,
term: 'first term',
filters: { foo: 'bar' },
pageCursor: 'SOMEPAGE',
},
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) =>
wrapper({
children,
initialState: {
...initialState,
term: 'first term',
filters: { foo: 'bar' },
pageCursor: 'SOMEPAGE',
},
}),
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current.filters).toEqual({ foo: 'bar' });
expect(result.current.pageCursor).toEqual('SOMEPAGE');
act(() => {
await act(async () => {
result.current.setFilters({});
});
await waitForNextUpdate();
expect(result.current.pageCursor).toBeUndefined();
});
it('When filters are set (and different from previous)', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState: {
...initialState,
term: 'first term',
filters: { foo: 'bar' },
pageCursor: 'SOMEPAGE',
},
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) =>
wrapper({
children,
initialState: {
...initialState,
term: 'first term',
filters: { foo: 'bar' },
pageCursor: 'SOMEPAGE',
},
}),
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current.filters).toEqual({ foo: 'bar' });
expect(result.current.pageCursor).toEqual('SOMEPAGE');
act(() => {
await act(async () => {
result.current.setFilters({ foo: 'test' });
});
await waitForNextUpdate();
expect(result.current.pageCursor).toBeUndefined();
});
});
describe('Performs search (and sets results)', () => {
it('When term is set', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState,
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) => wrapper({ children, initialState }),
});
await waitForNextUpdate();
await act(async () => {});
const term = 'term';
act(() => {
await act(async () => {
result.current.setTerm(term);
});
await waitForNextUpdate();
expect(searchApiMock.query).toHaveBeenLastCalledWith({
term,
types: ['*'],
@@ -287,23 +273,18 @@ describe('SearchContext', () => {
});
it('When types is set', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState,
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) => wrapper({ children, initialState }),
});
await waitForNextUpdate();
await act(async () => {});
const types = ['type'];
act(() => {
await act(async () => {
result.current.setTypes(types);
});
await waitForNextUpdate();
expect(searchApiMock.query).toHaveBeenLastCalledWith({
types,
term: '',
@@ -312,23 +293,18 @@ describe('SearchContext', () => {
});
it('When filters are set', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState,
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) => wrapper({ children, initialState }),
});
await waitForNextUpdate();
await act(async () => {});
const filters = { filter: 'filter' };
act(() => {
await act(async () => {
result.current.setFilters(filters);
});
await waitForNextUpdate();
expect(searchApiMock.query).toHaveBeenLastCalledWith({
filters,
term: '',
@@ -337,23 +313,18 @@ describe('SearchContext', () => {
});
it('When page limit is set', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState,
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) => wrapper({ children, initialState }),
});
await waitForNextUpdate();
await act(async () => {});
const pageLimit = 30;
act(() => {
await act(async () => {
result.current.setPageLimit(pageLimit);
});
await waitForNextUpdate();
expect(searchApiMock.query).toHaveBeenLastCalledWith({
pageLimit,
term: '',
@@ -363,23 +334,18 @@ describe('SearchContext', () => {
});
it('When page cursor is set', async () => {
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState,
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) => wrapper({ children, initialState }),
});
await waitForNextUpdate();
await act(async () => {});
const pageCursor = 'SOMEPAGE';
act(() => {
await act(async () => {
result.current.setPageCursor(pageCursor);
});
await waitForNextUpdate();
expect(searchApiMock.query).toHaveBeenLastCalledWith({
pageCursor,
term: '',
@@ -394,24 +360,19 @@ describe('SearchContext', () => {
nextPageCursor: 'NEXT',
});
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState,
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) => wrapper({ children, initialState }),
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current.fetchNextPage).toBeDefined();
expect(result.current.fetchPreviousPage).toBeUndefined();
act(() => {
await act(async () => {
result.current.fetchNextPage!();
});
await waitForNextUpdate();
expect(searchApiMock.query).toHaveBeenLastCalledWith({
term: '',
types: ['*'],
@@ -426,24 +387,19 @@ describe('SearchContext', () => {
previousPageCursor: 'PREVIOUS',
});
const { result, waitForNextUpdate } = renderHook(() => useSearch(), {
wrapper,
initialProps: {
initialState,
},
const { result } = renderHook(() => useSearch(), {
wrapper: ({ children }) => wrapper({ children, initialState }),
});
await waitForNextUpdate();
await act(async () => {});
expect(result.current.fetchNextPage).toBeUndefined();
expect(result.current.fetchPreviousPage).toBeDefined();
act(() => {
await act(async () => {
result.current.fetchPreviousPage!();
});
await waitForNextUpdate();
expect(searchApiMock.query).toHaveBeenLastCalledWith({
term: '',
types: ['*'],
+28 -35
View File
@@ -19,7 +19,11 @@ import { renderHook, act } from '@testing-library/react';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { MockAnalyticsApi, TestApiProvider } from '@backstage/test-utils';
import {
MockAnalyticsApi,
MockConfigApi,
TestApiProvider,
} from '@backstage/test-utils';
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
import {
analyticsApiRef,
@@ -30,6 +34,7 @@ import {
import { techdocsApiRef } from './api';
import { useTechDocsReaderPage, TechDocsReaderPageProvider } from './context';
import { TechDocsMetadata } from './types';
import { JsonObject } from '@backstage/config';
const mockShadowRoot = () => {
const div = document.createElement('div');
@@ -60,10 +65,6 @@ const techdocsApiMock = {
getTechDocsMetadata: jest.fn().mockResolvedValue(mockTechDocsMetadata),
};
const configApiMock = {
getOptionalBoolean: jest.fn().mockReturnValue(undefined),
};
const analyticsApiMock = new MockAnalyticsApi();
const wrapper = ({
@@ -72,16 +73,18 @@ const wrapper = ({
name: mockEntityMetadata.metadata.name,
namespace: mockEntityMetadata.metadata.namespace!!,
},
config,
children,
}: {
entityRef?: CompoundEntityRef;
config?: JsonObject;
children: React.ReactNode;
}) => (
<ThemeProvider theme={lightTheme}>
<TestApiProvider
apis={[
[analyticsApiRef, analyticsApiMock],
[configApiRef, configApiMock],
[configApiRef, new MockConfigApi(config ?? {})],
[techdocsApiRef, techdocsApiMock],
]}
>
@@ -98,47 +101,32 @@ describe('useTechDocsReaderPage', () => {
});
it('should set title', async () => {
const { result, waitForNextUpdate } = renderHook(
() => useTechDocsReaderPage(),
{ wrapper },
);
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
expect(result.current.title).toBe('');
act(() => result.current.setTitle('test site title'));
await waitForNextUpdate();
await act(async () => result.current.setTitle('test site title'));
expect(result.current.title).toBe('test site title');
});
it('should set subtitle', async () => {
const { result, waitForNextUpdate } = renderHook(
() => useTechDocsReaderPage(),
{ wrapper },
);
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
expect(result.current.subtitle).toBe('');
act(() => result.current.setSubtitle('test site subtitle'));
await waitForNextUpdate();
await act(async () => result.current.setSubtitle('test site subtitle'));
expect(result.current.subtitle).toBe('test site subtitle');
});
it('should set shadow root', async () => {
const { result, waitForNextUpdate } = renderHook(
() => useTechDocsReaderPage(),
{ wrapper },
);
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
// mock shadowroot
const shadowRoot = mockShadowRoot();
act(() => result.current.setShadowRoot(shadowRoot));
await waitForNextUpdate();
await act(async () => result.current.setShadowRoot(shadowRoot));
expect(result.current.shadowRoot?.innerHTML).toBe(
'<h1>Shadow DOM Mock</h1>',
@@ -152,30 +140,35 @@ describe('useTechDocsReaderPage', () => {
namespace: mockEntityMetadata.metadata.namespace?.toLocaleLowerCase(),
};
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
await act(async () => {});
expect(result.current.entityRef).toStrictEqual(lowercaseEntityRef);
});
it('entityRef is not modified when legacyUseCaseSensitiveTripletPaths is true', async () => {
configApiMock.getOptionalBoolean.mockReturnValueOnce(true);
const caseSensitiveEntityRef = {
kind: mockEntityMetadata.kind,
name: mockEntityMetadata.metadata.name,
namespace: mockEntityMetadata.metadata.namespace!!,
};
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
const { result } = renderHook(() => useTechDocsReaderPage(), {
wrapper: ({ children }) =>
wrapper({
children,
config: { techdocs: { legacyUseCaseSensitiveTripletPaths: true } },
}),
});
await act(async () => {});
expect(result.current.entityRef).toStrictEqual(caseSensitiveEntityRef);
});
it('entityRef provided as analytics context', async () => {
const { waitForNextUpdate } = renderHook(
() => useAnalytics().captureEvent('action', 'subject'),
{ wrapper },
);
await waitForNextUpdate();
renderHook(() => useAnalytics().captureEvent('action', 'subject'), {
wrapper,
});
await act(async () => {});
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
action: 'action',
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { renderHook } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react';
import { ThemeProvider } from '@material-ui/core';
@@ -95,12 +95,9 @@ describe('context', () => {
});
it('should return expected entity values', async () => {
const { result, waitForNextUpdate } = renderHook(
() => useEntityMetadata(),
{ wrapper },
);
const { result } = renderHook(() => useEntityMetadata(), { wrapper });
await waitForNextUpdate();
await act(async () => {});
expect(result.current.value).toBeDefined();
expect(result.current.error).toBeUndefined();
@@ -116,12 +113,9 @@ describe('context', () => {
});
it('should return expected techdocs metadata values', async () => {
const { result, waitForNextUpdate } = renderHook(
() => useTechDocsMetadata(),
{ wrapper },
);
const { result } = renderHook(() => useTechDocsMetadata(), { wrapper });
await waitForNextUpdate();
await act(async () => {});
expect(result.current.value).toBeDefined();
expect(result.current.error).toBeUndefined();
@@ -16,7 +16,7 @@
import { NotFoundError } from '@backstage/errors';
import { TestApiProvider } from '@backstage/test-utils';
import { act, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import React from 'react';
import { techdocsStorageApiRef } from '../../api';
import {
@@ -284,47 +284,45 @@ 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: 'CONTENT_FRESH',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
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(result.current).toEqual({
state: 'CHECKING',
path: '/example',
content: undefined,
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: [],
contentReload: expect.any(Function),
});
await act(async () => {});
expect(result.current).toEqual({
state: 'CONTENT_FRESH',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
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),
);
});
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,75 +342,73 @@ 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),
});
await waitForValueToChange(() => result.current.state, {
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);
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: 'CONTENT_FRESH',
path: '/example',
content: 'my 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).toHaveBeenCalledTimes(1);
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
{
kind: 'Component',
namespace: 'default',
name: 'backstage',
},
expect.any(Function),
);
expect(result.current).toEqual({
state: 'CHECKING',
path: '/example',
content: undefined,
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: [],
contentReload: expect.any(Function),
});
await waitFor(() => expect(result.current.state).toBe('INITIAL_BUILD'), {
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 waitFor(() => expect(result.current.state).toBe('CHECKING'));
expect(result.current).toEqual({
state: 'CHECKING',
path: '/example',
content: undefined,
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: [],
contentReload: expect.any(Function),
});
await waitFor(() => expect(result.current.state).toBe('CONTENT_FRESH'));
expect(result.current).toEqual({
state: 'CONTENT_FRESH',
path: '/example',
content: 'my 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).toHaveBeenCalledTimes(1);
expect(techdocsStorageApi.syncEntityDocs).toHaveBeenCalledWith(
{
kind: 'Component',
namespace: 'default',
name: 'backstage',
},
expect.any(Function),
);
});
it('should handle stale content', async () => {
@@ -423,6 +420,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,101 +428,105 @@ 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),
});
// the content is returned but the sync is in progress
await waitForValueToChange(() => result.current.state);
expect(result.current).toEqual({
state: 'CONTENT_FRESH',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
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);
expect(result.current).toEqual({
state: 'CONTENT_STALE_REFRESHING',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: ['Line 1', 'Line 2'],
contentReload: expect.any(Function),
});
// the content is updated but not yet displayed
await waitForValueToChange(() => result.current.state);
expect(result.current).toEqual({
state: 'CONTENT_STALE_READY',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: ['Line 1', 'Line 2'],
contentReload: expect.any(Function),
});
// reload the content
result.current.contentReload();
// the new content refresh is triggered
await waitForValueToChange(() => result.current.state);
expect(result.current).toEqual({
state: 'CHECKING',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
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),
);
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 waitFor(() => expect(result.current.state).toBe('CONTENT_FRESH'));
expect(result.current).toEqual({
state: 'CONTENT_FRESH',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: ['Line 1', 'Line 2'],
contentReload: expect.any(Function),
});
// the sync takes longer than 1 seconds so the refreshing state starts
await waitFor(() =>
expect(result.current.state).toBe('CONTENT_STALE_REFRESHING'),
);
expect(result.current).toEqual({
state: 'CONTENT_STALE_REFRESHING',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: ['Line 1', 'Line 2'],
contentReload: expect.any(Function),
});
// the content is updated but not yet displayed
await waitFor(() =>
expect(result.current.state).toBe('CONTENT_STALE_READY'),
);
expect(result.current).toEqual({
state: 'CONTENT_STALE_READY',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: ['Line 1', 'Line 2'],
contentReload: expect.any(Function),
});
// reload the content
await act(async () => {
result.current.contentReload();
});
// the new content refresh is triggered
await waitFor(() => expect(result.current.state).toBe('CHECKING'));
expect(result.current).toEqual({
state: 'CHECKING',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: [],
contentReload: expect.any(Function),
});
// the new content is loaded
await waitFor(() => expect(result.current.state).toBe('CONTENT_FRESH'), {
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),
);
});
it('should handle navigation', async () => {
@@ -537,93 +539,93 @@ 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),
});
// show the content
await waitForValueToChange(() => result.current.state);
expect(result.current).toEqual({
state: 'CONTENT_FRESH',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: [],
contentReload: expect.any(Function),
});
// navigate
rerender({ path: '/new' });
await waitForValueToChange(() => result.current.state);
expect(result.current).toEqual({
state: 'CHECKING',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: [],
contentReload: expect.any(Function),
});
await waitForValueToChange(() => result.current.state, {
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' });
await waitForValueToChange(() => result.current.state);
expect(result.current).toEqual({
state: 'CONTENT_NOT_FOUND',
path: '/missing',
content: undefined,
contentErrorMessage: 'NotFoundError: Some error description',
syncErrorMessage: undefined,
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(result.current).toEqual({
state: 'CHECKING',
path: '/example',
content: undefined,
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: [],
contentReload: expect.any(Function),
});
// show the content
await waitFor(() => expect(result.current.state).toBe('CONTENT_FRESH'));
expect(result.current).toEqual({
state: 'CONTENT_FRESH',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: [],
contentReload: expect.any(Function),
});
// navigate
rerender({ path: '/new' });
await waitFor(() => expect(result.current.state).toBe('CHECKING'));
expect(result.current).toEqual({
state: 'CHECKING',
path: '/example',
content: 'my content',
contentErrorMessage: undefined,
syncErrorMessage: undefined,
buildLog: [],
contentReload: expect.any(Function),
});
await waitFor(() => expect(result.current.state).toBe('CONTENT_FRESH'), {
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' });
await waitFor(() =>
expect(result.current.state).toBe('CONTENT_NOT_FOUND'),
);
expect(result.current).toEqual({
state: 'CONTENT_NOT_FOUND',
path: '/missing',
content: undefined,
contentErrorMessage: 'NotFoundError: Some error description',
syncErrorMessage: undefined,
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),
);
});
it('should handle content error', async () => {
@@ -632,47 +634,47 @@ 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),
});
// the content loading threw an error
await waitForValueToChange(() => result.current.state);
expect(result.current).toEqual({
state: 'CONTENT_NOT_FOUND',
path: '/example',
content: undefined,
contentErrorMessage: 'NotFoundError: Some error description',
syncErrorMessage: undefined,
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(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 waitFor(() =>
expect(result.current.state).toBe('CONTENT_NOT_FOUND'),
);
expect(result.current).toEqual({
state: 'CONTENT_NOT_FOUND',
path: '/example',
content: undefined,
contentErrorMessage: 'NotFoundError: Some error description',
syncErrorMessage: undefined,
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),
);
});
});
});