6289
expect(getVisibleChildren(container)).toEqual('Hi');
6290
});
6291
6292
+ // Regression: finishedTask aborting remaining fallback tasks from a
6293
+ // completed boundary could reenter itself via abortTaskSoft and fire
6294
+ // onAllReady twice (the inner call drained allPendingTasks to 0 and
6295
+ // called completeAll, then the outer call re-observed the same 0).
6296
+ it('only fires onAllReady once when a boundary with an instrumented sync-resolving thenable completes', async () => {
6297
+ // Mirrors Flight-client chunk behavior: the status-probe .then() in
6298
+ // trackUsedThenable stays pending, but the ping-attaching .then() in
6299
+ // renderNode's catch resolves synchronously. This reorders the work
6300
+ // queue so the fallback task is still in fallbackAbortableTasks when
6301
+ // the content task completes.
6302
+ function createDeferredSyncThenable(value) {
6303
+ let thenCallCount = 0;
6304
+ return {
6305
+ status: 'pending',
6306
+ value: undefined,
6307
+ then(resolve) {
6308
+ thenCallCount++;
6309
+ if (thenCallCount > 1) {
6310
+ this.status = 'fulfilled';
6311
+ this.value = value;
6312
+ resolve(value);
6313
+ }
6314
+ },
6315
+ };
6316
+ }
6317
+
6318
+ const thenable = createDeferredSyncThenable('hello');
6319
+ function AsyncContent() {
6320
+ return <Text text={use(thenable)} />;
6321
+ }
6322
+
6323
+ let allReadyCount = 0;
6324
+ await act(() => {
6325
+ const {pipe} = renderToPipeableStream(
6326
+ <Suspense fallback={<Text text="Loading..." />}>
6327
+ <AsyncContent />
6328
+ </Suspense>,
6329
+ {
6330
+ onAllReady() {
6331
+ allReadyCount++;
6332
+ },
6333
+ },
6334
+ );
6335
+ pipe(writable);
6336
+ });
6337
+
6338
+ expect(allReadyCount).toBe(1);
6339
+ expect(getVisibleChildren(container)).toEqual('hello');
6340
+ });
6341
+
6342
+ // Same bug, hit without any sync-thenable trickery: if the fallback
6343
+ // also suspends, its spawned sub-task lives in fallbackAbortableTasks
6344
+ // and can still be there when the content task completes first.
6345
+ it('only fires onAllReady once when both content and fallback suspend on real promises', async () => {
6346
+ let resolveContent;
6347
+ const contentPromise = new Promise(r => (resolveContent = r));
6348
+ // The fallback promise never resolves — the fallback-sub-task gets
6349
+ // soft-aborted when the content completes, so we never need it.
6350
+ const fallbackPromise = new Promise(() => {});
6351
+
6352
+ function AsyncContent() {
6353
+ return <Text text={use(contentPromise)} />;
6354
+ }
6355
+ function AsyncFallback() {
6356
+ return <Text text={use(fallbackPromise)} />;
6357
+ }
6358
+
6359
+ let allReadyCount = 0;
6360
+ await act(() => {
6361
+ const {pipe} = renderToPipeableStream(
6362
+ <Suspense fallback={<AsyncFallback />}>
6363
+ <AsyncContent />
6364
+ </Suspense>,
6365
+ {
6366
+ onAllReady() {
6367
+ allReadyCount++;
6368
+ },
6369
+ },
6370
+ );
6371
+ pipe(writable);
6372
+ });
6373
+
6374
+ // Resolving content alone is enough: the fallback-sub-task is still
6375
+ // in fallbackAbortableTasks when the content task completes, and
6376
+ // abortTaskSoft on it reenters finishedTask.
6377
+ await act(async () => {
6378
+ resolveContent('hello');
6379
+ await contentPromise;
6380
+ });
6381
+
6382
+ expect(allReadyCount).toBe(1);
6383
+ expect(getVisibleChildren(container)).toEqual('hello');
6384
+ });
6385
+
6386
it('promise as node', async () => {
6387
const promise = Promise.resolve('Hi');
6388
await act(async () => {