From 641a0685149c9f6aa12ab454bda2c305992236de Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 20 Mar 2024 13:42:24 +0100 Subject: [PATCH] feat: create app mode provider Signed-off-by: Camila Belo --- packages/app/package.json | 3 +- packages/app/src/App.tsx | 7 +- packages/app/src/AppMode.test.tsx | 148 ++++++++++++++++++++++++++++++ packages/app/src/AppMode.tsx | 55 +++++++++++ yarn.lock | 1 + 5 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 packages/app/src/AppMode.test.tsx create mode 100644 packages/app/src/AppMode.tsx diff --git a/packages/app/package.json b/packages/app/package.json index 9401ca89be..79101dff70 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -142,7 +142,8 @@ "@types/react": "*", "@types/react-dom": "*", "@types/zen-observable": "^0.8.0", - "cross-env": "^7.0.0" + "cross-env": "^7.0.0", + "msw": "^1.0.0" }, "bundled": true } diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 3d8bd45e5a..06851d6ac7 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -108,6 +108,7 @@ import { DevToolsPage } from '@backstage/plugin-devtools'; import { customDevToolsPage } from './components/devtools/CustomDevToolsPage'; import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities'; import { NotificationsPage } from '@backstage/plugin-notifications'; +import { AppMode } from './AppMode'; const app = createApp({ apis, @@ -282,8 +283,10 @@ export default app.createRoot( - - {routes} + + + {routes} + , ); diff --git a/packages/app/src/AppMode.test.tsx b/packages/app/src/AppMode.test.tsx new file mode 100644 index 0000000000..b110944e1e --- /dev/null +++ b/packages/app/src/AppMode.test.tsx @@ -0,0 +1,148 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { + MockConfigApi, + TestApiProvider, + renderInTestApp, + setupRequestMockHandlers, +} from '@backstage/test-utils'; +import { AppMode } from './AppMode'; +import { + configApiRef, + discoveryApiRef, + fetchApiRef, +} from '@backstage/core-plugin-api'; + +describe('AppMode', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + const configApiMock = new MockConfigApi({ + backend: { + baseUrl: 'http://localhost:7000', + }, + }); + + const fetchApiMock = { + fetch: jest.fn(), + }; + + const discoveryApiMock = { + getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7000/app'), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the progress component while loading', async () => { + fetchApiMock.fetch.mockReturnValueOnce(new Promise(() => {})); + await renderInTestApp( + + Test content + , + ); + expect(screen.getByTestId('progress')).toBeVisible(); + expect(screen.queryByText('Test Content')).not.toBeInTheDocument(); + }); + + it('should render the children even when there is an error', async () => { + const error = new Error('Failed to fetch'); + fetchApiMock.fetch.mockRejectedValueOnce(error); + await renderInTestApp( + + Test content + , + ); + await waitFor(() => + expect(screen.getByText('Test content')).toBeInTheDocument(), + ); + expect(screen.queryByTestId('progress')).not.toBeInTheDocument(); + expect(screen.queryByText('Failed to fetch')).not.toBeInTheDocument(); + }); + + it('should render the children even when the public index is not available', async () => { + fetchApiMock.fetch.mockResolvedValueOnce({ ok: false }); + await renderInTestApp( + + Test content + , + ); + await waitFor(() => + expect(screen.getByText('Test content')).toBeInTheDocument(), + ); + expect(screen.queryByTestId('progress')).not.toBeInTheDocument(); + }); + + it('should render the children also when the public index is available', async () => { + fetchApiMock.fetch.mockResolvedValueOnce({ ok: true }); + await renderInTestApp(Test content); + await waitFor(() => + expect(screen.getByText('Test content')).toBeInTheDocument(), + ); + expect(screen.queryByTestId('progress')).not.toBeInTheDocument(); + }); + + it('should render the children wrapped in the CookieAuthRefreshProvider', async () => { + worker.use( + rest.get('http://localhost:7000/public/index.html', (_, res, ctx) => { + return res(ctx.status(200)); + }), + rest.get( + 'http://localhost:7000/app/.backstage/v1-cookie', + (_, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ expiresAt: Date.now() + 10 * 60 * 1000 }), + ); + }, + ), + ); + await renderInTestApp( + + Test content + , + ); + await waitFor(() => + expect(screen.getByText('Test content')).toBeInTheDocument(), + ); + }); +}); diff --git a/packages/app/src/AppMode.tsx b/packages/app/src/AppMode.tsx new file mode 100644 index 0000000000..2b11064c65 --- /dev/null +++ b/packages/app/src/AppMode.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactNode } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { + configApiRef, + fetchApiRef, + useApi, + useApp, +} from '@backstage/core-plugin-api'; +import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react'; + +export function AppMode(props: { children: ReactNode }): JSX.Element { + const { children } = props; + const fetchApi = useApi(fetchApiRef); + const configApi = useApi(configApiRef); + const Components = useApp().getComponents(); + + const { loading, error, value } = useAsync(async () => { + const baseUrl = configApi.getString('backend.baseUrl'); + const response = await fetchApi.fetch(`${baseUrl}/public/index.html`); + return response.ok; + }, [fetchApi, configApi]); + + if (loading) { + return ; + } + + // Request failed, or the public index is not available + if (error || !value) { + return <>{children}; + } + + // The public index is available + // That means the app is running in public experimental mode + return ( + + {children} + + ); +} diff --git a/yarn.lock b/yarn.lock index ce9665c22d..ffc26aff2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27526,6 +27526,7 @@ __metadata: "@vitejs/plugin-react": ^4.0.4 cross-env: ^7.0.0 history: ^5.0.0 + msw: ^1.0.0 react: ^18.0.2 react-dom: ^18.0.2 react-router: ^6.3.0