@nocobase/flow-engine 2.1.0-alpha.35 → 2.1.0-alpha.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import React from 'react';
11
- import { describe, expect, it, beforeEach } from 'vitest';
11
+ import { describe, expect, it, beforeEach, vi } from 'vitest';
12
12
  import { render, act, waitFor, screen } from '@testing-library/react';
13
13
  import { FlowEngine } from '../../flowEngine';
14
14
  import { FlowEngineProvider } from '../../provider';
@@ -167,15 +167,16 @@ describe('FlowViewer zIndex with usePage', () => {
167
167
  );
168
168
 
169
169
  await waitFor(() => expect(api).toBeDefined());
170
+ const pageApi = api as NonNullable<typeof api>;
170
171
 
171
172
  await act(async () => {
172
- api!.open({ target, content: <div data-testid="page1">Page 1</div> }, engine.context);
173
+ pageApi.open({ target, content: <div data-testid="page1">Page 1</div> }, engine.context);
173
174
  });
174
175
  await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
175
176
 
176
177
  // Opening page2 into the global embed container should destroy page1 (replace behavior).
177
178
  await act(async () => {
178
- api!.open({ target, content: <div data-testid="page2">Page 2</div> }, engine.context);
179
+ pageApi.open({ target, content: <div data-testid="page2">Page 2</div> }, engine.context);
179
180
  });
180
181
  await waitFor(() => expect(screen.getByTestId('page2')).toBeInTheDocument());
181
182
  expect(screen.queryByTestId('page1')).not.toBeInTheDocument();
@@ -183,4 +184,243 @@ describe('FlowViewer zIndex with usePage', () => {
183
184
  unmount();
184
185
  document.body.removeChild(target);
185
186
  });
187
+
188
+ it('keeps active global embed view when replacement beforeClose blocks closing', async () => {
189
+ let getViewer: () => FlowViewer;
190
+ const beforeClose = vi.fn().mockResolvedValue(false);
191
+
192
+ const target = document.createElement('div');
193
+ target.id = GLOBAL_EMBED_CONTAINER_ID;
194
+ document.body.appendChild(target);
195
+
196
+ const { unmount } = render(
197
+ <Wrapper
198
+ onReady={(fn) => {
199
+ getViewer = fn;
200
+ }}
201
+ />,
202
+ );
203
+
204
+ await waitFor(() => expect(getViewer).toBeDefined());
205
+ const initialZIndex = getViewer().getNextZIndex();
206
+
207
+ let page1: any;
208
+ await act(async () => {
209
+ page1 = getViewer().embed({
210
+ target,
211
+ content: (currentPage) => {
212
+ currentPage.beforeClose = beforeClose;
213
+ return <div data-testid="page1">Page 1</div>;
214
+ },
215
+ });
216
+ });
217
+
218
+ await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
219
+ expect(getViewer().getNextZIndex()).toBe(initialZIndex + 1);
220
+
221
+ await act(async () => {
222
+ const page2 = getViewer().embed({ target, content: <div data-testid="page2">Page 2</div> });
223
+ await page2;
224
+ });
225
+
226
+ expect(beforeClose).toHaveBeenCalledTimes(1);
227
+ expect(screen.getByTestId('page1')).toBeInTheDocument();
228
+ expect(screen.queryByTestId('page2')).not.toBeInTheDocument();
229
+ expect(getViewer().getNextZIndex()).toBe(initialZIndex + 1);
230
+
231
+ await act(async () => {
232
+ page1.destroy();
233
+ });
234
+
235
+ unmount();
236
+ document.body.removeChild(target);
237
+ });
238
+
239
+ it('opens the replacement view after async beforeClose allows global embed replacement', async () => {
240
+ let getViewer: () => FlowViewer;
241
+ const beforeClose = vi.fn().mockResolvedValue(true);
242
+
243
+ const target = document.createElement('div');
244
+ target.id = GLOBAL_EMBED_CONTAINER_ID;
245
+ document.body.appendChild(target);
246
+
247
+ const { unmount } = render(
248
+ <Wrapper
249
+ onReady={(fn) => {
250
+ getViewer = fn;
251
+ }}
252
+ />,
253
+ );
254
+
255
+ await waitFor(() => expect(getViewer).toBeDefined());
256
+
257
+ await act(async () => {
258
+ getViewer().embed({
259
+ target,
260
+ content: (currentPage) => {
261
+ currentPage.beforeClose = beforeClose;
262
+ return <div data-testid="page1">Page 1</div>;
263
+ },
264
+ });
265
+ });
266
+
267
+ await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
268
+
269
+ await act(async () => {
270
+ getViewer().embed({ target, content: <div data-testid="page2">Page 2</div> });
271
+ });
272
+
273
+ await waitFor(() => expect(screen.getByTestId('page2')).toBeInTheDocument());
274
+ expect(beforeClose).toHaveBeenCalledTimes(1);
275
+ expect(screen.queryByTestId('page1')).not.toBeInTheDocument();
276
+
277
+ unmount();
278
+ document.body.removeChild(target);
279
+ });
280
+
281
+ it('runs a pending close only once and allows retry when beforeClose rejects', async () => {
282
+ let getViewer: () => FlowViewer;
283
+ let resolveFirstClose: (value: boolean) => void;
284
+ const beforeClose = vi
285
+ .fn()
286
+ .mockImplementationOnce(() => new Promise<boolean>((resolve) => (resolveFirstClose = resolve)))
287
+ .mockResolvedValueOnce(true);
288
+
289
+ const { unmount } = render(
290
+ <Wrapper
291
+ onReady={(fn) => {
292
+ getViewer = fn;
293
+ }}
294
+ />,
295
+ );
296
+
297
+ await waitFor(() => expect(getViewer).toBeDefined());
298
+
299
+ let page: any;
300
+ await act(async () => {
301
+ page = getViewer().embed({
302
+ content: (currentPage) => {
303
+ currentPage.beforeClose = beforeClose;
304
+ return <div data-testid="draft-editor">Draft editor</div>;
305
+ },
306
+ });
307
+ });
308
+
309
+ await waitFor(() => expect(screen.getByTestId('draft-editor')).toBeInTheDocument());
310
+
311
+ const firstClose = page.close();
312
+ const secondClose = page.close();
313
+ expect(firstClose).toBe(secondClose);
314
+ expect(beforeClose).toHaveBeenCalledTimes(1);
315
+
316
+ await act(async () => {
317
+ resolveFirstClose(false);
318
+ await firstClose;
319
+ });
320
+
321
+ expect(screen.getByTestId('draft-editor')).toBeInTheDocument();
322
+
323
+ await act(async () => {
324
+ await page.close();
325
+ });
326
+
327
+ expect(beforeClose).toHaveBeenCalledTimes(2);
328
+ await waitFor(() => expect(screen.queryByTestId('draft-editor')).not.toBeInTheDocument());
329
+
330
+ unmount();
331
+ });
332
+
333
+ it('keeps only the latest pending global embed replacement', async () => {
334
+ let getViewer: () => FlowViewer;
335
+ let resolveBeforeClose: (value: boolean) => void;
336
+ const beforeClose = vi.fn(() => new Promise<boolean>((resolve) => (resolveBeforeClose = resolve)));
337
+
338
+ const target = document.createElement('div');
339
+ target.id = GLOBAL_EMBED_CONTAINER_ID;
340
+ document.body.appendChild(target);
341
+
342
+ const { unmount } = render(
343
+ <Wrapper
344
+ onReady={(fn) => {
345
+ getViewer = fn;
346
+ }}
347
+ />,
348
+ );
349
+
350
+ await waitFor(() => expect(getViewer).toBeDefined());
351
+ const initialZIndex = getViewer().getNextZIndex();
352
+
353
+ await act(async () => {
354
+ getViewer().embed({
355
+ target,
356
+ content: (currentPage) => {
357
+ currentPage.beforeClose = beforeClose;
358
+ return <div data-testid="page1">Page 1</div>;
359
+ },
360
+ });
361
+ });
362
+
363
+ await waitFor(() => expect(screen.getByTestId('page1')).toBeInTheDocument());
364
+
365
+ const page2 = getViewer().embed({ target, content: <div data-testid="page2">Page 2</div> });
366
+ const page3 = getViewer().embed({ target, content: <div data-testid="page3">Page 3</div> });
367
+
368
+ await act(async () => {
369
+ resolveBeforeClose(true);
370
+ await page2;
371
+ });
372
+
373
+ expect(beforeClose).toHaveBeenCalledTimes(1);
374
+ expect(screen.queryByTestId('page1')).not.toBeInTheDocument();
375
+ expect(screen.queryByTestId('page2')).not.toBeInTheDocument();
376
+ await waitFor(() => expect(screen.getByTestId('page3')).toBeInTheDocument());
377
+ expect(getViewer().getNextZIndex()).toBe(initialZIndex + 1);
378
+
379
+ unmount();
380
+ document.body.removeChild(target);
381
+ });
382
+
383
+ it('keeps the embed close button usable after beforeClose blocks closing', async () => {
384
+ let getViewer: () => FlowViewer;
385
+ const beforeClose = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true);
386
+
387
+ const { unmount } = render(
388
+ <Wrapper
389
+ onReady={(fn) => {
390
+ getViewer = fn;
391
+ }}
392
+ />,
393
+ );
394
+
395
+ await waitFor(() => expect(getViewer).toBeDefined());
396
+
397
+ await act(async () => {
398
+ getViewer().embed({
399
+ title: 'Draft editor',
400
+ content: (currentPage) => {
401
+ currentPage.beforeClose = beforeClose;
402
+ return <div data-testid="draft-editor">Draft editor</div>;
403
+ },
404
+ });
405
+ });
406
+
407
+ await waitFor(() => expect(screen.getByTestId('draft-editor')).toBeInTheDocument());
408
+ const closeButton = screen.getByRole('button');
409
+
410
+ await act(async () => {
411
+ closeButton.click();
412
+ });
413
+
414
+ expect(beforeClose).toHaveBeenCalledTimes(1);
415
+ expect(screen.getByTestId('draft-editor')).toBeInTheDocument();
416
+
417
+ await act(async () => {
418
+ closeButton.click();
419
+ });
420
+
421
+ expect(beforeClose).toHaveBeenCalledTimes(2);
422
+ await waitFor(() => expect(screen.queryByTestId('draft-editor')).not.toBeInTheDocument());
423
+
424
+ unmount();
425
+ });
186
426
  });