/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import React from 'react'; import { describe, expect, it, beforeEach, vi } from 'vitest'; import { render, act, waitFor, screen } from '@testing-library/react'; import { FlowEngine } from '../../flowEngine'; import { FlowEngineProvider } from '../../provider'; import { FlowViewer } from '../FlowView'; import { usePage, GLOBAL_EMBED_CONTAINER_ID } from '../usePage'; import { App, ConfigProvider } from 'antd'; describe('FlowViewer zIndex with usePage', () => { let engine: FlowEngine; function TestApp({ onReady }: { onReady: (getViewer: () => FlowViewer) => void }) { const [page, pageHolder] = usePage() as [{ open: (config: any, flowContext: any) => any }, React.ReactNode]; React.useEffect(() => { const wrappedTypes = { embed: { open: (props: any, ctx: any) => { return page.open(props, ctx); }, }, drawer: { open: () => Promise.resolve() }, dialog: { open: () => Promise.resolve() }, popover: { open: () => Promise.resolve() }, }; engine.context.defineProperty('themeToken', { value: { zIndexPopupBase: 1000 }, }); const getViewer = () => new FlowViewer(engine.context, wrappedTypes); onReady(getViewer); }, [page, onReady]); return <>{pageHolder}; } const Wrapper: React.FC<{ onReady: (getViewer: () => FlowViewer) => void }> = ({ onReady }) => ( ); beforeEach(() => { engine = new FlowEngine(); }); it('open page1 -> open page2 -> close page2 -> open page3: internal zIndex counter should be correct', async () => { let getViewer: () => FlowViewer; const { unmount } = render( { getViewer = fn; }} />, ); await waitFor(() => expect(getViewer).toBeDefined()); // Record initial zIndex const initialZIndex = getViewer().getNextZIndex(); // Open page1 let page1: any; await act(async () => { page1 = getViewer().embed({ content:
Page 1
}); }); await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument()); // After opening page1, zIndex counter should increment const afterPage1ZIndex = getViewer().getNextZIndex(); expect(afterPage1ZIndex).toBe(initialZIndex + 1); // Open page2 let page2: any; await act(async () => { page2 = getViewer().embed({ content:
Page 2
}); }); await waitFor(() => expect(screen.getByTestId('page2')).toBeInTheDocument()); // After opening page2, zIndex counter should increment again const afterPage2ZIndex = getViewer().getNextZIndex(); expect(afterPage2ZIndex).toBe(initialZIndex + 2); // Close page2 await act(async () => { page2.close(); }); await waitFor(() => expect(screen.queryByTestId('page2')).not.toBeInTheDocument()); // After closing page2, zIndex counter should decrement back const afterClosePage2ZIndex = getViewer().getNextZIndex(); expect(afterClosePage2ZIndex).toBe(initialZIndex + 1); // Open page3 let page3: any; await act(async () => { page3 = getViewer().embed({ content:
Page 3
}); }); await waitFor(() => expect(screen.getByTestId('page3')).toBeInTheDocument()); // After opening page3, zIndex should be same as after page2 was opened (counter reused) const afterPage3ZIndex = getViewer().getNextZIndex(); expect(afterPage3ZIndex).toBe(afterPage2ZIndex); // Cleanup await act(async () => { page3.close(); page1.close(); }); unmount(); }); it('replaces previous embed view when using global #nocobase-embed-container target', async () => { let api: { open: (config: any, flowContext: any) => any } | undefined; function TestApp({ onReady }: { onReady: (page: any) => void }) { const [page, pageHolder] = usePage() as [{ open: (config: any, flowContext: any) => any }, React.ReactNode]; React.useEffect(() => { onReady(page); }, [page, onReady]); return <>{pageHolder}; } const Wrapper: React.FC<{ onReady: (page: any) => void }> = ({ onReady }) => ( ); const target = document.createElement('div'); target.id = GLOBAL_EMBED_CONTAINER_ID; document.body.appendChild(target); const { unmount } = render( { api = page; }} />, ); await waitFor(() => expect(api).toBeDefined()); const pageApi = api as NonNullable; await act(async () => { pageApi.open({ target, content:
Page 1
}, engine.context); }); await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument()); // Opening page2 into the global embed container should destroy page1 (replace behavior). await act(async () => { pageApi.open({ target, content:
Page 2
}, engine.context); }); await waitFor(() => expect(screen.getByTestId('page2')).toBeInTheDocument()); expect(screen.queryByTestId('page1')).not.toBeInTheDocument(); unmount(); document.body.removeChild(target); }); it('keeps active global embed view when replacement beforeClose blocks closing', async () => { let getViewer: () => FlowViewer; const beforeClose = vi.fn().mockResolvedValue(false); const target = document.createElement('div'); target.id = GLOBAL_EMBED_CONTAINER_ID; document.body.appendChild(target); const { unmount } = render( { getViewer = fn; }} />, ); await waitFor(() => expect(getViewer).toBeDefined()); const initialZIndex = getViewer().getNextZIndex(); let page1: any; await act(async () => { page1 = getViewer().embed({ target, content: (currentPage) => { currentPage.beforeClose = beforeClose; return
Page 1
; }, }); }); await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument()); expect(getViewer().getNextZIndex()).toBe(initialZIndex + 1); await act(async () => { const page2 = getViewer().embed({ target, content:
Page 2
}); await page2; }); expect(beforeClose).toHaveBeenCalledTimes(1); expect(screen.getByTestId('page1')).toBeInTheDocument(); expect(screen.queryByTestId('page2')).not.toBeInTheDocument(); expect(getViewer().getNextZIndex()).toBe(initialZIndex + 1); await act(async () => { page1.destroy(); }); unmount(); document.body.removeChild(target); }); it('opens the replacement view after async beforeClose allows global embed replacement', async () => { let getViewer: () => FlowViewer; const beforeClose = vi.fn().mockResolvedValue(true); const target = document.createElement('div'); target.id = GLOBAL_EMBED_CONTAINER_ID; document.body.appendChild(target); const { unmount } = render( { getViewer = fn; }} />, ); await waitFor(() => expect(getViewer).toBeDefined()); await act(async () => { getViewer().embed({ target, content: (currentPage) => { currentPage.beforeClose = beforeClose; return
Page 1
; }, }); }); await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument()); await act(async () => { getViewer().embed({ target, content:
Page 2
}); }); await waitFor(() => expect(screen.getByTestId('page2')).toBeInTheDocument()); expect(beforeClose).toHaveBeenCalledTimes(1); expect(screen.queryByTestId('page1')).not.toBeInTheDocument(); unmount(); document.body.removeChild(target); }); it('runs a pending close only once and allows retry when beforeClose rejects', async () => { let getViewer: () => FlowViewer; let resolveFirstClose: (value: boolean) => void; const beforeClose = vi .fn() .mockImplementationOnce(() => new Promise((resolve) => (resolveFirstClose = resolve))) .mockResolvedValueOnce(true); const { unmount } = render( { getViewer = fn; }} />, ); await waitFor(() => expect(getViewer).toBeDefined()); let page: any; await act(async () => { page = getViewer().embed({ content: (currentPage) => { currentPage.beforeClose = beforeClose; return
Draft editor
; }, }); }); await waitFor(() => expect(screen.getByTestId('draft-editor')).toBeInTheDocument()); const firstClose = page.close(); const secondClose = page.close(); expect(firstClose).toBe(secondClose); expect(beforeClose).toHaveBeenCalledTimes(1); await act(async () => { resolveFirstClose(false); await firstClose; }); expect(screen.getByTestId('draft-editor')).toBeInTheDocument(); await act(async () => { await page.close(); }); expect(beforeClose).toHaveBeenCalledTimes(2); await waitFor(() => expect(screen.queryByTestId('draft-editor')).not.toBeInTheDocument()); unmount(); }); it('keeps only the latest pending global embed replacement', async () => { let getViewer: () => FlowViewer; let resolveBeforeClose: (value: boolean) => void; const beforeClose = vi.fn(() => new Promise((resolve) => (resolveBeforeClose = resolve))); const target = document.createElement('div'); target.id = GLOBAL_EMBED_CONTAINER_ID; document.body.appendChild(target); const { unmount } = render( { getViewer = fn; }} />, ); await waitFor(() => expect(getViewer).toBeDefined()); const initialZIndex = getViewer().getNextZIndex(); await act(async () => { getViewer().embed({ target, content: (currentPage) => { currentPage.beforeClose = beforeClose; return
Page 1
; }, }); }); await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument()); const page2 = getViewer().embed({ target, content:
Page 2
}); const page3 = getViewer().embed({ target, content:
Page 3
}); await act(async () => { resolveBeforeClose(true); await page2; }); expect(beforeClose).toHaveBeenCalledTimes(1); expect(screen.queryByTestId('page1')).not.toBeInTheDocument(); expect(screen.queryByTestId('page2')).not.toBeInTheDocument(); await waitFor(() => expect(screen.getByTestId('page3')).toBeInTheDocument()); expect(getViewer().getNextZIndex()).toBe(initialZIndex + 1); unmount(); document.body.removeChild(target); }); it('keeps the embed close button usable after beforeClose blocks closing', async () => { let getViewer: () => FlowViewer; const beforeClose = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true); const { unmount } = render( { getViewer = fn; }} />, ); await waitFor(() => expect(getViewer).toBeDefined()); await act(async () => { getViewer().embed({ title: 'Draft editor', content: (currentPage) => { currentPage.beforeClose = beforeClose; return
Draft editor
; }, }); }); await waitFor(() => expect(screen.getByTestId('draft-editor')).toBeInTheDocument()); const closeButton = screen.getByRole('button'); await act(async () => { closeButton.click(); }); expect(beforeClose).toHaveBeenCalledTimes(1); expect(screen.getByTestId('draft-editor')).toBeInTheDocument(); await act(async () => { closeButton.click(); }); expect(beforeClose).toHaveBeenCalledTimes(2); await waitFor(() => expect(screen.queryByTestId('draft-editor')).not.toBeInTheDocument()); unmount(); }); });