Skip to content

Commit b83e315

Browse files
committed
test: properly handle race conditions in modal tests
1 parent 4383dc8 commit b83e315

1 file changed

Lines changed: 38 additions & 13 deletions

File tree

apps/nativescript-demo-ng/src/tests/modal-dialog.spec.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,30 @@
22
import { Component, inject, NgModule, NO_ERRORS_SCHEMA, ViewContainerRef } from '@angular/core';
33
import { TestBed, waitForAsync } from '@angular/core/testing';
44
import { FrameService, ModalDialogParams, ModalDialogService, NativeScriptCommonModule, NSLocationStrategy, Outlet } from '@nativescript/angular';
5-
import { Frame, isIOS } from '@nativescript/core';
5+
import { Application, View } from '@nativescript/core';
66

77
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+
}
929

1030
@Component({
1131
selector: 'modal-comp',
@@ -75,17 +95,22 @@ describe('modal-dialog', () => {
7595
// done()
7696
// });
7797

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);
89114
});
90115

91116
it('showModal does not throws when there is no viewContainer provided', waitForAsync(async () => {

0 commit comments

Comments
 (0)