Skip to content

Commit

Permalink
fix(Canvas): Avoid flickering in @patternfly/react-topology
Browse files Browse the repository at this point in the history
When rendering the graph, we can see some flickering in a few
situations:

1. The app is loaded for the first time or refreshed
2. Switching between no routes in the canvas and a adding a route
3. Navigating to a pages and go backto the design page
4. Disabling and enabling steps in the canvas
5. Switching the layout between horizontal and vertical

The fix for those situations is:
1. Adding an `initialized` state to
  • Loading branch information
lordrip committed Sep 26, 2024
1 parent e03c0c9 commit ed422f6
Show file tree
Hide file tree
Showing 12 changed files with 2,180 additions and 1,122 deletions.
10 changes: 10 additions & 0 deletions packages/ui/src/components/Visualization/Canvas/Canvas.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,13 @@
border: none;
}
}

.canvas-empty-state {
position: absolute;
height: 100%;
width: 100%;
}

.hidden {
visibility: hidden;
}
241 changes: 179 additions & 62 deletions packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,52 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { VisualizationProvider } from '@patternfly/react-topology';
import { act, fireEvent, render, RenderResult, screen, waitFor } from '@testing-library/react';
import { CamelRouteResource, KameletResource } from '../../../models/camel';
import { CamelRouteVisualEntity } from '../../../models/visualization/flows';
import { ActionConfirmationModalContextProvider } from '../../../providers';
import { CatalogModalContext } from '../../../providers/catalog-modal.provider';
import { VisibleFLowsContextResult } from '../../../providers/visible-flows.provider';
import { TestProvidersWrapper } from '../../../stubs';
import { camelRouteJson } from '../../../stubs/camel-route';
import { kameletJson } from '../../../stubs/kamelet-route';
import { Canvas } from './Canvas';
import { ActionConfirmationModalContextProvider } from '../../../providers';
import { ControllerService } from './controller.service';

describe('Canvas', () => {
const entity = new CamelRouteVisualEntity(camelRouteJson);
const entity2 = { ...entity, id: 'route-9999' } as CamelRouteVisualEntity;

beforeEach(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});

it('should render correctly', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: { visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult,
});
const result = render(
<Provider>
<Canvas entities={[entity]} />
</Provider>,
);

await waitFor(async () => expect(result.container.querySelector('#fit-to-screen')).toBeInTheDocument());
expect(result.container).toMatchSnapshot();
let result: RenderResult | undefined;

await act(async () => {
result = render(
<Provider>
<VisualizationProvider controller={ControllerService.createController()}>
<Canvas entities={[entity]} />
</VisualizationProvider>
</Provider>,
);
});

await act(async () => {
await jest.runAllTimersAsync();
});

await waitFor(async () => expect(screen.getByText('Reset View')).toBeInTheDocument());
expect(result?.asFragment()).toMatchSnapshot();
});

it('should render correctly with more routes ', async () => {
Expand All @@ -34,14 +55,25 @@ describe('Canvas', () => {
visibleFlows: { ['route-8888']: true, ['route-9999']: false },
} as unknown as VisibleFLowsContextResult,
});
const result = render(
<Provider>
<Canvas entities={[entity, entity2]} />
</Provider>,
);

await waitFor(async () => expect(result.container.querySelector('#fit-to-screen')).toBeInTheDocument());
expect(result.container).toMatchSnapshot();
let result: RenderResult | undefined;

await act(async () => {
result = render(
<Provider>
<VisualizationProvider controller={ControllerService.createController()}>
<Canvas entities={[entity, entity2]} />
</VisualizationProvider>
</Provider>,
);
});

await act(async () => {
await jest.runAllTimersAsync();
});

await waitFor(async () => expect(screen.getByText('Reset View')).toBeInTheDocument());
expect(result?.asFragment()).toMatchSnapshot();
});

it('should be able to delete the routes', async () => {
Expand All @@ -55,16 +87,31 @@ describe('Canvas', () => {
visibleFlows: { ['route-8888']: true },
} as unknown as VisibleFLowsContextResult,
});
const wrapper = render(
<ActionConfirmationModalContextProvider>
<Provider>
<Canvas entities={routeEntities} />
</Provider>
</ActionConfirmationModalContextProvider>,
);

let result: RenderResult | undefined;

await act(async () => {
result = render(
<ActionConfirmationModalContextProvider>
<Provider>
<VisualizationProvider controller={ControllerService.createController()}>
<Canvas entities={routeEntities} />
</VisualizationProvider>
</Provider>
</ActionConfirmationModalContextProvider>,
);
});

await act(async () => {
await jest.runAllTimersAsync();
});

const route = result?.getByText('route-8888');
if (!route) {
fail('Route not found');
}

// Right click anywhere on the container label
const route = wrapper.getByText('route-8888');
await act(async () => {
fireEvent.contextMenu(route);
});
Expand Down Expand Up @@ -102,17 +149,30 @@ describe('Canvas', () => {
} as unknown as VisibleFLowsContextResult,
});

const wrapper = render(
<ActionConfirmationModalContextProvider>
<Provider>
<Canvas entities={kameletEntities} />
</Provider>
</ActionConfirmationModalContextProvider>,
);
let result: RenderResult | undefined;

await act(async () => {
result = render(
<ActionConfirmationModalContextProvider>
<Provider>
<VisualizationProvider controller={ControllerService.createController()}>
<Canvas entities={kameletEntities} />
</VisualizationProvider>
</Provider>
</ActionConfirmationModalContextProvider>,
);
});

await act(async () => {
await jest.runAllTimersAsync();
});

const kamelet = result?.getByText('user-source');
if (!kamelet) {
fail('Kamelet not found');
}

// Right click anywhere on the container label
const kamelet = wrapper.getByText('user-source');
// const route = document.querySelectorAll('.pf-topology__group');
await act(async () => {
fireEvent.contextMenu(kamelet);
});
Expand All @@ -137,51 +197,108 @@ describe('Canvas', () => {
expect(removeSpy).toHaveBeenCalled();
});

it('should render the Catalog button if `CatalogModalContext` is provided', async () => {
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: { visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult,
describe('Catalog button', () => {
it('should be present if `CatalogModalContext` is provided', async () => {
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: { visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult,
});

let result: RenderResult | undefined;

await act(async () => {
result = render(
<CatalogModalContext.Provider value={{ getNewComponent: jest.fn(), setIsModalOpen: jest.fn() }}>
<Provider>
<VisualizationProvider controller={ControllerService.createController()}>
<Canvas entities={[entity]} />
</VisualizationProvider>
</Provider>
</CatalogModalContext.Provider>,
);
});

await act(async () => {
await jest.runAllTimersAsync();
});

await waitFor(async () => expect(screen.getByText('Open Catalog')).toBeInTheDocument());
expect(result?.asFragment()).toMatchSnapshot();
});

it('should NOT be present if `CatalogModalContext` is NOT provided', async () => {
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: { visibleFlows: { ['route-8888']: true } } as unknown as VisibleFLowsContextResult,
});

let result: RenderResult | undefined;

await act(async () => {
result = render(
<Provider>
<VisualizationProvider controller={ControllerService.createController()}>
<Canvas entities={[entity]} />
</VisualizationProvider>
</Provider>,
);
});

await act(async () => {
await jest.runAllTimersAsync();
});

await waitFor(async () => expect(screen.queryByText('Open Catalog')).not.toBeInTheDocument());
expect(result?.asFragment()).toMatchSnapshot();
});
const result = render(
<CatalogModalContext.Provider value={{ getNewComponent: jest.fn(), setIsModalOpen: jest.fn() }}>
<Provider>
<Canvas entities={[entity]} />
</Provider>
</CatalogModalContext.Provider>,
);

await waitFor(async () =>
expect(result.container.querySelector('#topology-control-bar-catalog-button')).toBeInTheDocument(),
);
expect(result.container).toMatchSnapshot();
});

describe('Empty state', () => {
it('should render empty state when there is no visual entity', async () => {
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: { visibleFlows: {} } as unknown as VisibleFLowsContextResult,
});
const result = render(
<Provider>
<Canvas entities={[]} />
</Provider>,
);

await waitFor(async () => expect(result.getByTestId('visualization-empty-state')).toBeInTheDocument());
expect(result.container).toMatchSnapshot();
let result: RenderResult | undefined;

await act(async () => {
result = render(
<Provider>
<VisualizationProvider controller={ControllerService.createController()}>
<Canvas entities={[]} />
</VisualizationProvider>
</Provider>,
);
});

await act(async () => {
await jest.runAllTimersAsync();
});

await waitFor(async () => expect(screen.getByTestId('visualization-empty-state')).toBeInTheDocument());
expect(result?.asFragment()).toMatchSnapshot();
});

it('should render empty state when there is no visible flows', async () => {
const { Provider } = TestProvidersWrapper({
visibleFlowsContext: { visibleFlows: { ['route-8888']: false } } as unknown as VisibleFLowsContextResult,
});
const result = render(
<Provider>
<Canvas entities={[entity]} />
</Provider>,
);
let result: RenderResult | undefined;

await act(async () => {
result = render(
<Provider>
<VisualizationProvider controller={ControllerService.createController()}>
<Canvas entities={[entity]} />
</VisualizationProvider>
</Provider>,
);
});

await act(async () => {
await jest.runAllTimersAsync();
});

await waitFor(async () => expect(result.getByTestId('visualization-empty-state')).toBeInTheDocument());
expect(result.container).toMatchSnapshot();
await waitFor(async () => expect(screen.getByTestId('visualization-empty-state')).toBeInTheDocument());
expect(result?.container).toMatchSnapshot();
});
});
});
Loading

0 comments on commit ed422f6

Please sign in to comment.