Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1785777985382.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
__default__: patch
---

Harness tracks pending work while preserving native Promise instances so native modules return their settled values on Hermes.
123 changes: 123 additions & 0 deletions packages/runtime/src/__tests__/promise-tracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,49 @@ describe('promise tracker', () => {
expect(pending[0].stack).toContain('Promise created');
});

it('preserves native Promise instances created through the global constructor', () => {
const NativePromise = globalThis.Promise;
installPromiseTracker();

const promise = new Promise(() => undefined);

expect(Object.getPrototypeOf(promise)).toBe(NativePromise.prototype);
expect(promise).toBeInstanceOf(Promise);
expect(promise.constructor).toBe(Promise);
expect(Promise.resolve(promise)).toBe(promise);
expect(Object.keys(promise)).toEqual([]);
});

it('preserves custom Promise subclass behavior', async () => {
installPromiseTracker();

let usedCustomThen = false;
class CustomPromise<T> extends Promise<T> {
override then<TResult1 = T, TResult2 = never>(
onfulfilled?:
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| undefined
| null,
onrejected?:
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
| undefined
| null,
): Promise<TResult1 | TResult2> {
usedCustomThen = true;
return super.then(onfulfilled, onrejected);
}
}

const promise = new CustomPromise<number>((resolve) => resolve(55));

expect(Object.getPrototypeOf(promise)).toBe(CustomPromise.prototype);
expect(promise).toBeInstanceOf(CustomPromise);
expect(promise.constructor).toBe(CustomPromise);
expect(CustomPromise.resolve(promise)).toBe(promise);
await expect(promise.then((value) => value)).resolves.toBe(55);
expect(usedCustomThen).toBe(true);
});

it('removes promises when they resolve', async () => {
installPromiseTracker();

Expand All @@ -43,6 +86,86 @@ describe('promise tracker', () => {
expect(getPendingPromises()).toHaveLength(0);
});

it('preserves native Promise static method behavior', async () => {
const NativePromise = globalThis.Promise;
installPromiseTracker();

const resolvedValue = Promise.resolve(55);
const resolvedUndefined = Promise.resolve();
const rejected = Promise.reject('failed');
const all = Promise.all([55]);
const allSettled = Promise.allSettled([55]);
const any = Promise.any([55]);
const race = Promise.race([55]);
const nativeWithResolvers: unknown = Reflect.get(
NativePromise,
'withResolvers',
);
const trackedWithResolvers: unknown = Reflect.get(
Promise,
'withResolvers',
);
const withResolvers =
typeof nativeWithResolvers === 'function' &&
typeof trackedWithResolvers === 'function'
? (Reflect.apply(trackedWithResolvers, Promise, []) as {
promise: Promise<number>;
resolve: (value: number) => void;
})
: null;
const nativeTry: unknown = Reflect.get(NativePromise, 'try');
const trackedTry: unknown = Reflect.get(Promise, 'try');
const tried =
typeof nativeTry === 'function' && typeof trackedTry === 'function'
? (Reflect.apply(trackedTry, Promise, [() => 55]) as Promise<number>)
: null;
const rejectedAssertion = expect(rejected).rejects.toBe('failed');

for (const promise of [
resolvedValue,
resolvedUndefined,
rejected,
all,
allSettled,
any,
race,
...(withResolvers ? [withResolvers.promise] : []),
...(tried ? [tried] : []),
]) {
expect(Object.getPrototypeOf(promise)).toBe(NativePromise.prototype);
}

withResolvers?.resolve(55);

await expect(resolvedValue).resolves.toBe(55);
await expect(resolvedUndefined).resolves.toBeUndefined();
await rejectedAssertion;
await expect(all).resolves.toEqual([55]);
await expect(allSettled).resolves.toEqual([
{ status: 'fulfilled', value: 55 },
]);
await expect(any).resolves.toBe(55);
await expect(race).resolves.toBe(55);
if (withResolvers) {
await expect(withResolvers.promise).resolves.toBe(55);
}
if (tried) {
await expect(tried).resolves.toBe(55);
}
});

it('resolves values when Promise.resolve is invoked without a receiver', async () => {
installPromiseTracker();

const resolve = Promise.resolve;

await expect(Reflect.apply(resolve, undefined, [55])).resolves.toBe(55);
await expect(Reflect.apply(resolve, undefined, [])).resolves.toBeUndefined();
await expect(
Reflect.apply(resolve, undefined, [undefined]),
).resolves.toBeUndefined();
});

it('keeps promises pending while their resolved thenable is pending', () => {
installPromiseTracker();

Expand Down
Loading
Loading