From b508e58efd4333747baf385489572c82077a4b98 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 1 Mar 2025 22:07:18 +0100 Subject: [PATCH] plugins/app: added tests for DialogDisplay + fix component rendering Signed-off-by: Patrik Oldsberg --- .../app/src/extensions/DialogDisplay.test.tsx | 175 ++++++++++++++++++ plugins/app/src/extensions/DialogDisplay.tsx | 12 +- 2 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 plugins/app/src/extensions/DialogDisplay.test.tsx diff --git a/plugins/app/src/extensions/DialogDisplay.test.tsx b/plugins/app/src/extensions/DialogDisplay.test.tsx new file mode 100644 index 0000000000..8bcf86817c --- /dev/null +++ b/plugins/app/src/extensions/DialogDisplay.test.tsx @@ -0,0 +1,175 @@ +/* + * Copyright 2025 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 { renderInTestApp } from '@backstage/frontend-test-utils'; +import React, { act, useEffect } from 'react'; +import { + AppRootElementBlueprint, + DialogApi, + DialogApiDialog, + dialogApiRef, +} from '@backstage/frontend-plugin-api'; +import { createDeferred } from '@backstage/types'; +import userEvent from '@testing-library/user-event'; + +async function withDialogApi( + callback: (dialogApi: DialogApi) => Promise, +) { + const deferred = createDeferred(); + await renderInTestApp(
, { + extensions: [ + AppRootElementBlueprint.makeWithOverrides({ + name: 'derp', + factory(originalFactory, { apis }) { + function TestComponent() { + useEffect(() => { + deferred.resolve(apis.get(dialogApiRef)!); + }, []); + return
; + } + return originalFactory({ element: }); + }, + }), + ], + }); + return await callback(await deferred); +} + +describe('DialogDisplay', () => { + function AutoDialog({ + dialog, + result, + }: { + dialog: DialogApiDialog; + result?: string; + }) { + useEffect(() => { + if (result) { + dialog.close(result); + } + }, [dialog, result]); + return
; + } + + it('should render a simple dialog', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.show(
Test
)); + dialog.close('test'); + return dialog.result(); + }); + expect(result).toBe('test'); + }); + + it('should allow dialog to be updated', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.show(AutoDialog)); + + setTimeout(async () => { + await act(async () => { + dialog.update(props => ); + }); + }, 100); + + return dialog.result(); + }); + + expect(result).toBe('test2'); + }); + + it('should allow dialog to be closed by pressing escape', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.show(AutoDialog)); + + setTimeout(async () => { + await userEvent.keyboard('{Escape}'); + }, 100); + + return dialog.result(); + }); + + expect(result).toBe(undefined); + }); + + it('should allow a stack of dialogs', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog1 = await act(() => dialogApi.show(AutoDialog)); + const dialog2 = await act(() => dialogApi.show(AutoDialog)); + const dialog3 = await act(() => dialogApi.show(AutoDialog)); + + setTimeout(async () => { + await act(async () => { + dialog3.close('test3'); + dialog1.close('test1'); + dialog2.close('test2'); + }); + }, 100); + + return Promise.all([ + dialog1.result(), + dialog2.result(), + dialog3.result(), + ]); + }); + + expect(result).toEqual(['test1', 'test2', 'test3']); + }); + + it('should only cancel one dialog at a time', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog1 = await act(() => dialogApi.show(AutoDialog)); + const dialog2 = await act(() => dialogApi.show(AutoDialog)); + const dialog3 = await act(() => dialogApi.show(AutoDialog)); + + setTimeout(async () => { + await userEvent.keyboard('{Escape}'); + + await act(async () => { + dialog1.close('test1'); + dialog2.close('test2'); + dialog3.close('test3'); + }); + }, 100); + + return Promise.all([ + dialog1.result(), + dialog2.result(), + dialog3.result(), + ]); + }); + + expect(result).toEqual(['test1', 'test2', undefined]); + }); + + it('should not allow modal dialog to be closed by pressing escape', async () => { + const result = await withDialogApi(async dialogApi => { + const dialog = await act(() => dialogApi.showModal(AutoDialog)); + + setTimeout(async () => { + await userEvent.keyboard('{Escape}'); + + setTimeout(async () => { + await act(async () => { + dialog.close('test'); + }); + }, 100); + }, 100); + + return dialog.result(); + }); + + expect(result).toBe('test'); + }); +}); diff --git a/plugins/app/src/extensions/DialogDisplay.tsx b/plugins/app/src/extensions/DialogDisplay.tsx index 7381523e0f..fef3fa61ff 100644 --- a/plugins/app/src/extensions/DialogDisplay.tsx +++ b/plugins/app/src/extensions/DialogDisplay.tsx @@ -63,11 +63,13 @@ function DialogDisplay({ deferred.resolve(result); setDialogs(ds => ds.filter(d => d.dialog.id !== id)); }, - update(elementOrComponent) { + update(ElementOrComponent) { const element = - typeof elementOrComponent === 'function' - ? elementOrComponent({ dialog }) - : elementOrComponent; + typeof ElementOrComponent === 'function' ? ( + + ) : ( + ElementOrComponent + ); setDialogs(ds => ds.map(d => (d.dialog.id === id ? { dialog, element } : d)), ); @@ -76,7 +78,7 @@ function DialogDisplay({ return deferred; }, }; - const element = options.component({ dialog }); + const element = ; setDialogs(ds => [...ds, { dialog, element }]); return dialog; });