Merge pull request #18919 from backstage/fix-entity-switch-unmounting-children
Fix entity switch unmounting children
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Fixed an issue causing `EntitySwitch` to unmount its children once entity refresh was invoked
|
||||
@@ -407,6 +407,7 @@ unbreak
|
||||
Unconference
|
||||
unicode
|
||||
unmanaged
|
||||
unmount
|
||||
unregister
|
||||
unregistering
|
||||
unregistration
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
AsyncEntityProvider,
|
||||
EntityProvider,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { isKind } from './conditions';
|
||||
import { EntitySwitch } from './EntitySwitch';
|
||||
import { featureFlagsApiRef } from '@backstage/core-plugin-api';
|
||||
@@ -35,6 +35,80 @@ const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
);
|
||||
|
||||
describe('EntitySwitch', () => {
|
||||
it('should render only the first match', () => {
|
||||
const content = (
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isKind('component')} children="A" />
|
||||
<EntitySwitch.Case if={isKind('component')} children="B" />
|
||||
<EntitySwitch.Case children="C" />
|
||||
</EntitySwitch>
|
||||
);
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<EntityProvider
|
||||
entity={{ metadata: { name: 'mock' }, kind: 'component' } as Entity}
|
||||
>
|
||||
{content}
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('A')).toBeInTheDocument();
|
||||
expect(screen.queryByText('B')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('C')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the fallback if no cases are matching', () => {
|
||||
const content = (
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isKind('system')} children="A" />
|
||||
<EntitySwitch.Case if={isKind('api')} children="B" />
|
||||
<EntitySwitch.Case children="C" />
|
||||
</EntitySwitch>
|
||||
);
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<EntityProvider
|
||||
entity={{ metadata: { name: 'mock' }, kind: 'component' } as Entity}
|
||||
>
|
||||
{content}
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('A')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('B')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('C')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render only the first fallback in case no cases are matching', () => {
|
||||
const content = (
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isKind('system')} children="A" />
|
||||
<EntitySwitch.Case if={isKind('api')} children="B" />
|
||||
<EntitySwitch.Case children="C" />
|
||||
<EntitySwitch.Case children="D" />
|
||||
</EntitySwitch>
|
||||
);
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<EntityProvider
|
||||
entity={{ metadata: { name: 'mock' }, kind: 'component' } as Entity}
|
||||
>
|
||||
{content}
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('A')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('B')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('C')).toBeInTheDocument();
|
||||
expect(screen.queryByText('D')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should switch child when entity switches', () => {
|
||||
const content = (
|
||||
<EntitySwitch>
|
||||
@@ -96,7 +170,7 @@ describe('EntitySwitch', () => {
|
||||
|
||||
expect(screen.queryByText('A')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('B')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('C')).toBeInTheDocument();
|
||||
expect(screen.queryByText('C')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should switch child when filters switch', () => {
|
||||
@@ -209,6 +283,97 @@ describe('EntitySwitch', () => {
|
||||
expect(screen.queryByText('B')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display the children in case entity is available and loading is true', () => {
|
||||
const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity;
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<AsyncEntityProvider entity={entity} loading>
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case
|
||||
if={isKind('component')}
|
||||
children={<p>Component</p>}
|
||||
/>
|
||||
<EntitySwitch.Case if={isKind('system')} children={<p>System</p>} />
|
||||
</EntitySwitch>
|
||||
</AsyncEntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Component')).toBeInTheDocument();
|
||||
expect(screen.queryByText('System')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it(`shouldn't unmount the children on entity refresh`, () => {
|
||||
const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity;
|
||||
|
||||
let mountsCount = 0;
|
||||
function Component() {
|
||||
useEffect(() => {
|
||||
++mountsCount;
|
||||
}, []);
|
||||
|
||||
return <p>Component</p>;
|
||||
}
|
||||
|
||||
const rendered = render(
|
||||
<Wrapper>
|
||||
<AsyncEntityProvider entity={entity} loading={false}>
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case
|
||||
if={isKind('component')}
|
||||
children={<Component />}
|
||||
/>
|
||||
<EntitySwitch.Case if={isKind('system')} children={<p>System</p>} />
|
||||
</EntitySwitch>
|
||||
</AsyncEntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Component')).toBeInTheDocument();
|
||||
expect(screen.queryByText('System')).not.toBeInTheDocument();
|
||||
|
||||
expect(mountsCount).toBe(1);
|
||||
|
||||
rendered.rerender(
|
||||
<Wrapper>
|
||||
<AsyncEntityProvider entity={entity} loading>
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case
|
||||
if={isKind('component')}
|
||||
children={<Component />}
|
||||
/>
|
||||
<EntitySwitch.Case if={isKind('system')} children={<p>System</p>} />
|
||||
</EntitySwitch>
|
||||
</AsyncEntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Component')).toBeInTheDocument();
|
||||
expect(screen.queryByText('System')).not.toBeInTheDocument();
|
||||
|
||||
expect(mountsCount).toBe(1);
|
||||
|
||||
rendered.rerender(
|
||||
<Wrapper>
|
||||
<AsyncEntityProvider entity={entity} loading={false}>
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case
|
||||
if={isKind('component')}
|
||||
children={<Component />}
|
||||
/>
|
||||
<EntitySwitch.Case if={isKind('system')} children={<p>System</p>} />
|
||||
</EntitySwitch>
|
||||
</AsyncEntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Component')).toBeInTheDocument();
|
||||
expect(screen.queryByText('System')).not.toBeInTheDocument();
|
||||
|
||||
expect(mountsCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should switch with async condition that is true', async () => {
|
||||
const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity;
|
||||
|
||||
@@ -292,10 +457,10 @@ describe('EntitySwitch', () => {
|
||||
expect(screen.queryByText('A')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should switch with sync condition that throws', async () => {
|
||||
it('should switch with async condition that throws', async () => {
|
||||
const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity;
|
||||
|
||||
const shouldRender = () => Promise.reject();
|
||||
const shouldRender = jest.fn().mockRejectedValue(undefined);
|
||||
render(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
@@ -308,7 +473,9 @@ describe('EntitySwitch', () => {
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText('C')).resolves.toBeInTheDocument();
|
||||
await waitFor(() => expect(shouldRender).toHaveBeenCalled());
|
||||
|
||||
expect(screen.getByText('C')).toBeInTheDocument();
|
||||
expect(screen.queryByText('A')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('B')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -64,8 +64,9 @@ export interface EntitySwitchProps {
|
||||
|
||||
/** @public */
|
||||
export const EntitySwitch = (props: EntitySwitchProps) => {
|
||||
const { entity, loading } = useAsyncEntity();
|
||||
const { entity } = useAsyncEntity();
|
||||
const apis = useApiHolder();
|
||||
|
||||
const results = useElementFilter(
|
||||
props.children,
|
||||
collection =>
|
||||
@@ -76,23 +77,14 @@ export const EntitySwitch = (props: EntitySwitchProps) => {
|
||||
})
|
||||
.getElements()
|
||||
.flatMap<SwitchCaseResult>((element: ReactElement) => {
|
||||
// Nothing is rendered while loading
|
||||
if (loading) {
|
||||
// If the entity is missing or there is an error, render nothing
|
||||
if (!entity) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { if: condition, children: elementsChildren } =
|
||||
element.props as EntitySwitchCase;
|
||||
|
||||
// If the entity is missing or there is an error, render the default page
|
||||
if (!entity) {
|
||||
return [
|
||||
{
|
||||
if: condition === undefined,
|
||||
children: elementsChildren,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
if: condition?.(entity, { apis }),
|
||||
@@ -100,7 +92,7 @@ export const EntitySwitch = (props: EntitySwitchProps) => {
|
||||
},
|
||||
];
|
||||
}),
|
||||
[apis, entity, loading],
|
||||
[apis, entity],
|
||||
);
|
||||
|
||||
const hasAsyncCases = results.some(
|
||||
|
||||
Reference in New Issue
Block a user