|
2 | 2 | import { Component, inject, NgModule, NO_ERRORS_SCHEMA, ViewContainerRef } from '@angular/core'; |
3 | 3 | import { TestBed, waitForAsync } from '@angular/core/testing'; |
4 | 4 | import { FrameService, ModalDialogParams, ModalDialogService, NativeScriptCommonModule, NSLocationStrategy, Outlet } from '@nativescript/angular'; |
5 | | -import { Frame, isIOS } from '@nativescript/core'; |
| 5 | +import { Application, View } from '@nativescript/core'; |
6 | 6 |
|
7 | 7 | import { FakeFrameService } from './ns-location-strategy.spec'; |
8 | | -const CLOSE_WAIT = isIOS ? 1000 : 0; |
| 8 | + |
| 9 | +/** |
| 10 | + * Resolves once `condition` is truthy, polling on each frame. Unlike a fixed delay this resolves |
| 11 | + * as soon as the awaited state is reached (e.g. a modal finishing its animated dismissal), with a |
| 12 | + * bounded safety timeout so a stuck condition can't hang the suite. |
| 13 | + */ |
| 14 | +function waitUntil(condition: () => boolean, timeout = 5000): Promise<void> { |
| 15 | + return new Promise((resolve, reject) => { |
| 16 | + const start = Date.now(); |
| 17 | + const check = () => { |
| 18 | + if (condition()) { |
| 19 | + resolve(); |
| 20 | + } else if (Date.now() - start > timeout) { |
| 21 | + reject(new Error('Timed out waiting for condition')); |
| 22 | + } else { |
| 23 | + setTimeout(check, 16); |
| 24 | + } |
| 25 | + }; |
| 26 | + check(); |
| 27 | + }); |
| 28 | +} |
9 | 29 |
|
10 | 30 | @Component({ |
11 | 31 | selector: 'modal-comp', |
@@ -75,17 +95,22 @@ describe('modal-dialog', () => { |
75 | 95 | // done() |
76 | 96 | // }); |
77 | 97 |
|
78 | | - afterEach((done) => { |
79 | | - const page = Frame.topmost().currentPage; |
80 | | - if (page && page.modal) { |
81 | | - console.log('Warning: closing a leftover modal page!'); |
82 | | - page.modal.closeModal(); |
83 | | - } |
84 | | - if (CLOSE_WAIT > 0) { |
85 | | - setTimeout(done, CLOSE_WAIT); |
86 | | - } else { |
87 | | - done(); |
88 | | - } |
| 98 | + afterEach(async () => { |
| 99 | + // Close any modal still presented (via core's global registry) and wait until it has actually |
| 100 | + // finished dismissing before the next test runs. |
| 101 | + // |
| 102 | + // Note: `closeModal()` removes the modal from `_rootModalViews` *synchronously*, before the |
| 103 | + // animated dismissal starts, so the registry being empty does NOT mean the modal is gone. On |
| 104 | + // iOS the parent keeps a `presentedViewController` until the dismiss animation completes — and |
| 105 | + // that's exactly what makes the next `showModal` fail with "already presenting" — so wait on it. |
| 106 | + const open = ((Application.getRootView()?._getRootModalViews() ?? []) as View[]).slice(); |
| 107 | + // Capture parents before closing: `closeModal()` nulls `_modalParent` synchronously. |
| 108 | + const parents = open |
| 109 | + .map((modal) => (modal as { _modalParent?: View })._modalParent) |
| 110 | + .filter((parent): parent is View => !!parent); |
| 111 | + open.forEach((modal) => modal.closeModal()); |
| 112 | + const isPresenting = (parent: View) => !!(parent as { viewController?: { presentedViewController?: unknown } }).viewController?.presentedViewController; |
| 113 | + await waitUntil(() => parents.every((parent) => !isPresenting(parent))).catch(() => undefined); |
89 | 114 | }); |
90 | 115 |
|
91 | 116 | it('showModal does not throws when there is no viewContainer provided', waitForAsync(async () => { |
|
0 commit comments