-
Notifications
You must be signed in to change notification settings - Fork 5k
Fix assertion failure constructing jest.fn() via Reflect.construct #32667
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,6 +86,7 @@ | |
| } | ||
|
|
||
| JSC_DECLARE_HOST_FUNCTION(jsMockFunctionCall); | ||
| JSC_DECLARE_HOST_FUNCTION(jsMockFunctionConstruct); | ||
| JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_protoImpl); | ||
| JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_mock); | ||
| JSC_DECLARE_HOST_FUNCTION(jsMockFunctionGetter_mockGetLastCall); | ||
|
|
@@ -462,7 +463,7 @@ | |
| } | ||
|
|
||
| JSMockFunction(JSC::VM& vm, JSC::Structure* structure, CallbackKind wrapKind) | ||
| : Base(vm, structure, jsMockFunctionCall, jsMockFunctionCall) | ||
| : Base(vm, structure, jsMockFunctionCall, jsMockFunctionConstruct) | ||
| { | ||
| initMock(); | ||
| } | ||
|
|
@@ -981,6 +982,28 @@ | |
| return JSValue::encode(jsUndefined()); | ||
| } | ||
|
|
||
| JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe)) | ||
| { | ||
| auto& vm = JSC::getVM(lexicalGlobalObject); | ||
| auto scope = DECLARE_THROW_SCOPE(vm); | ||
|
|
||
| JSObject* newTarget = asObject(callframe->newTarget()); | ||
| JSGlobalObject* functionGlobalObject = getFunctionRealm(lexicalGlobalObject, newTarget); | ||
| RETURN_IF_EXCEPTION(scope, {}); | ||
| Structure* structure = InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, functionGlobalObject->objectStructureForObjectConstructor()); | ||
| RETURN_IF_EXCEPTION(scope, {}); | ||
| JSObject* thisObject = constructEmptyObject(vm, structure); | ||
|
Check notice on line 995 in src/jsc/bindings/JSMockFunction.cpp
|
||
| callframe->setThisValue(thisObject); | ||
|
|
||
| EncodedJSValue encodedResult = jsMockFunctionCall(lexicalGlobalObject, callframe); | ||
| RETURN_IF_EXCEPTION(scope, {}); | ||
|
Comment on lines
+995
to
+999
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Record constructed mocks in The new construct path creates 🤖 Prompt for AI Agents |
||
|
|
||
| JSValue result = JSValue::decode(encodedResult); | ||
| if (result.isObject()) | ||
| return encodedResult; | ||
| return JSValue::encode(thisObject); | ||
| } | ||
|
|
||
| void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) | ||
| { | ||
| Base::finishCreation(vm); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -794,6 +794,28 @@ describe("mock()", () => { | |
|
|
||
| expect(bar()()).toBe(true); | ||
| }); | ||
|
|
||
| it("Reflect.construct returns an object when the implementation does not", () => { | ||
| const noImpl = jest.fn(); | ||
| expect(typeof Reflect.construct(noImpl, [])).toBe("object"); | ||
| expect(typeof new noImpl()).toBe("object"); | ||
|
|
||
| const primitiveImpl = jest.fn(() => 42); | ||
| expect(typeof Reflect.construct(primitiveImpl, [])).toBe("object"); | ||
| expect(typeof new primitiveImpl()).toBe("object"); | ||
|
|
||
| const returnsPrimitive = jest.fn().mockReturnValue(42); | ||
| expect(typeof Reflect.construct(returnsPrimitive, [])).toBe("object"); | ||
| expect(typeof new returnsPrimitive()).toBe("object"); | ||
|
Comment on lines
+800
to
+809
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert a non-null constructed object.
🤖 Prompt for AI Agents |
||
|
|
||
| const sentinel = {}; | ||
| const objectImpl = jest.fn(() => sentinel); | ||
| expect(Reflect.construct(objectImpl, [])).toBe(sentinel); | ||
| expect(new objectImpl()).toBe(sentinel); | ||
|
|
||
| expect(noImpl()).toBeUndefined(); | ||
| expect(primitiveImpl()).toBe(42); | ||
| }); | ||
| }); | ||
|
|
||
| describe("spyOn", () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟣 Note (pre-existing, not blocking):
JSMockFunctionnever installs an own.prototypeproperty, so whennewTargetis the mock itselfcreateSubclassStructurereadsfn.prototypeasundefinedand falls back toObject.prototype. The constructed instance therefore hasObject.getPrototypeOf(new fn()) === Object.prototype(andnew fn() instanceof fnthrows), diverging from Jest where mocks are real JS functions. A follow-up adding a per-instance.prototypeinJSMockFunction::create()would close the gap; the construct logic here would then work unchanged.Extended reasoning...
Summary
jsMockFunctionConstructderives the new instance's structure viaInternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, functionGlobalObject->objectStructureForObjectConstructor()). That helper readsnewTarget.prototypeand only falls back to the supplied base structure when the property is not an object.JSMockFunctionis anInternalFunctionand never defines an own.prototypedata property — there is noputDirect(vm.propertyNames->prototype, ...)anywhere in this file, no entry for it inJSMockFunctionPrototypeTableValues, andInternalFunction::finishCreationdoes not auto-create one (onlyJSFunctiondoes that lazily). So in the commonnew fn()case wherenewTarget === fn,fn.prototypeisundefined, the fallback path is taken, and the freshly allocatedthisObjecthas%Object.prototype%as its[[Prototype]].Step-by-step
const fn = jest.fn();— creates aJSMockFunction. No ownprototypeis installed, sofn.prototype === undefined.new fn()entersjsMockFunctionConstructwithnewTarget = fn.createSubclassStructurecallsnewTarget->get(globalObject, vm.propertyNames->prototype)→undefined. Since that's not an object, it returns the base structure (objectStructureForObjectConstructor()).constructEmptyObject(vm, structure)produces a plain object whose[[Prototype]]isObject.prototype.jsMockFunctionCallreturnsundefined,result.isObject()is false, andthisObjectis returned.Object.getPrototypeOf(new fn()) === Object.prototype, and(new fn()) instanceof fnthrowsTypeError: instanceof called on an object with an invalid prototype propertybecausefn.prototypeis not an object.In Jest, mock functions are ordinary JS functions, so each mock has its own
.prototypeobject,Object.getPrototypeOf(new fn()) === fn.prototype, and(new fn()) instanceof fn === true. The new test in this PR only assertstypeof === "object", so it does not exercise this divergence.Why this is pre-existing / a nit
The construct code added here is spec-correct:
OrdinaryCreateFromConstructormandates falling back to the realm's%Object.prototype%whennewTarget.prototypeis not an object. The root cause is the pre-existing lack of a per-instance.prototypeonJSMockFunction— e.g.({}) instanceof jest.fn()already threwTypeErrorbefore this PR, andjest.fn().prototypewas alreadyundefined. This PR simply makes the constructed instance observable for the first time (previously construction asserted/returnedundefined), so the prototype-chain divergence is now newly visible, but it was not introduced here. TheReflect.construct(fn, [], SomeClass)case wherenewTargetis a real class with a.prototypealready works correctly with this implementation.Suggested follow-up
In
JSMockFunction::create(), allocate a fresh prototype object and install it:Once that exists,
createSubclassStructurewill pick it up automatically andjsMockFunctionConstructneeds no changes. This is a Jest-compat improvement worth tracking separately; it should not block this crash fix.