diff --git a/code/addons/interactions/package.json b/code/addons/interactions/package.json index d8757d87f486..bc3392c1902e 100644 --- a/code/addons/interactions/package.json +++ b/code/addons/interactions/package.json @@ -82,7 +82,6 @@ "@storybook/preview-api": "7.2.0-alpha.0", "@storybook/theming": "7.2.0-alpha.0", "@storybook/types": "7.2.0-alpha.0", - "jest-mock": "^29.5.0", "polished": "^4.2.2", "ts-dedent": "^2.2.0" }, diff --git a/code/addons/interactions/src/jest-mock/index.ts b/code/addons/interactions/src/jest-mock/index.ts new file mode 100644 index 000000000000..5b6cc6512a52 --- /dev/null +++ b/code/addons/interactions/src/jest-mock/index.ts @@ -0,0 +1,1377 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/ban-types, func-names, @typescript-eslint/no-shadow, no-param-reassign, no-restricted-syntax, no-return-assign, no-inner-declarations, consistent-return, no-plusplus, no-underscore-dangle, prefer-rest-params */ + +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * We copied it because because of the dist of `jest-mock` package is CJS and can't be tree-shaken effectively, + * This causes node dependencies to leak into the browser bundle. + * @see: https://github.com/storybookjs/storybook/issues/23570 + * + * this code is copied from https://github.com/jestjs/jest/blob/main/packages/jest-util/src/isPromise.ts + * this code is copied from https://github.com/jestjs/jest/blob/main/packages/jest-mock/src/index.ts + * + * I made changes to the source files: + * 1. remove all `export` keywords, except for `class ModuleMocker` + * 2. remove unused variables/interfaces/types + */ + +function isPromise(candidate: unknown): candidate is PromiseLike { + return ( + candidate != null && + (typeof candidate === 'object' || typeof candidate === 'function') && + typeof (candidate as any).then === 'function' + ); +} + +type MockMetadataType = + | 'object' + | 'array' + | 'regexp' + | 'function' + | 'constant' + | 'collection' + | 'null' + | 'undefined'; + +type MockMetadata = { + ref?: number; + members?: Record>; + mockImpl?: T; + name?: string; + refID?: number; + type?: MetadataType; + value?: T; + length?: number; +}; + +type ClassLike = { new (...args: any): any }; +type FunctionLike = (...args: any) => any; + +type ConstructorLikeKeys = keyof { + [K in keyof T as Required[K] extends ClassLike ? K : never]: T[K]; +}; + +type MethodLikeKeys = keyof { + [K in keyof T as Required[K] extends FunctionLike ? K : never]: T[K]; +}; + +type PropertyLikeKeys = Exclude | MethodLikeKeys>; + +type MockedClass = MockInstance< + (...args: ConstructorParameters) => Mocked> +> & + MockedObject; + +type MockedFunction = MockInstance & MockedObject; + +type MockedFunctionShallow = MockInstance & T; + +type MockedObject = { + [K in keyof T]: T[K] extends ClassLike + ? MockedClass + : T[K] extends FunctionLike + ? MockedFunction + : T[K] extends object + ? MockedObject + : T[K]; +} & T; + +type MockedObjectShallow = { + [K in keyof T]: T[K] extends ClassLike + ? MockedClass + : T[K] extends FunctionLike + ? MockedFunctionShallow + : T[K]; +} & T; + +type Mocked = T extends ClassLike + ? MockedClass + : T extends FunctionLike + ? MockedFunction + : T extends object + ? MockedObject + : T; + +type MockedShallow = T extends ClassLike + ? MockedClass + : T extends FunctionLike + ? MockedFunctionShallow + : T extends object + ? MockedObjectShallow + : T; + +type UnknownFunction = (...args: Array) => unknown; +type UnknownClass = { new (...args: Array): unknown }; + +type SpiedClass = MockInstance< + (...args: ConstructorParameters) => InstanceType +>; + +type SpiedFunction = MockInstance< + (...args: Parameters) => ReturnType +>; + +type SpiedGetter = MockInstance<() => T>; + +type SpiedSetter = MockInstance<(arg: T) => void>; + +type Spied = T extends ClassLike + ? SpiedClass + : T extends FunctionLike + ? SpiedFunction + : never; + +/** + * All what the internal typings need is to be sure that we have any-function. + * `FunctionLike` type ensures that and helps to constrain the type as well. + * The default of `UnknownFunction` makes sure that `any`s do not leak to the + * user side. For instance, calling `fn()` without implementation will return + * a mock of `(...args: Array) => unknown` type. If implementation + * is provided, its typings are inferred correctly. + */ +interface Mock extends Function, MockInstance { + new (...args: Parameters): ReturnType; + (...args: Parameters): ReturnType; +} + +type ResolveType = ReturnType extends PromiseLike ? U : never; + +type RejectType = ReturnType extends PromiseLike ? unknown : never; + +interface MockInstance { + _isMockFunction: true; + _protoImpl: Function; + getMockImplementation(): T | undefined; + getMockName(): string; + mock: MockFunctionState; + mockClear(): this; + mockReset(): this; + mockRestore(): void; + mockImplementation(fn: T): this; + mockImplementationOnce(fn: T): this; + withImplementation(fn: T, callback: () => Promise): Promise; + withImplementation(fn: T, callback: () => void): void; + mockName(name: string): this; + mockReturnThis(): this; + mockReturnValue(value: ReturnType): this; + mockReturnValueOnce(value: ReturnType): this; + mockResolvedValue(value: ResolveType): this; + mockResolvedValueOnce(value: ResolveType): this; + mockRejectedValue(value: RejectType): this; + mockRejectedValueOnce(value: RejectType): this; +} + +interface Replaced { + /** + * Restore property to its original value known at the time of mocking. + */ + restore(): void; + /** + * Change the value of the property. + */ + replaceValue(value: T): this; +} + +type ReplacedPropertyRestorer = { + (): void; + object: T; + property: K; + replaced: Replaced; +}; + +type MockFunctionResultIncomplete = { + type: 'incomplete'; + /** + * Result of a single call to a mock function that has not yet completed. + * This occurs if you test the result from within the mock function itself, + * or from within a function that was called by the mock. + */ + value: undefined; +}; +type MockFunctionResultReturn = { + type: 'return'; + /** + * Result of a single call to a mock function that returned. + */ + value: ReturnType; +}; +type MockFunctionResultThrow = { + type: 'throw'; + /** + * Result of a single call to a mock function that threw. + */ + value: unknown; +}; + +type MockFunctionResult = + | MockFunctionResultIncomplete + | MockFunctionResultReturn + | MockFunctionResultThrow; + +type MockFunctionState = { + /** + * List of the call arguments of all calls that have been made to the mock. + */ + calls: Array>; + /** + * List of all the object instances that have been instantiated from the mock. + */ + instances: Array>; + /** + * List of all the function contexts that have been applied to calls to the mock. + */ + contexts: Array>; + /** + * List of the call order indexes of the mock. Jest is indexing the order of + * invocations of all mocks in a test file. The index is starting with `1`. + */ + invocationCallOrder: Array; + /** + * List of the call arguments of the last call that was made to the mock. + * If the function was not called, it will return `undefined`. + */ + lastCall?: Parameters; + /** + * List of the results of all calls that have been made to the mock. + */ + results: Array>; +}; + +type MockFunctionConfig = { + mockImpl: Function | undefined; + mockName: string; + specificMockImpls: Array; +}; + +type SpyState = { reset?: () => void; restore: () => void }; + +const MOCK_CONSTRUCTOR_NAME = 'mockConstructor'; + +const FUNCTION_NAME_RESERVED_PATTERN = /[\s!-/:-@[-`{-~]/; +const FUNCTION_NAME_RESERVED_REPLACE = new RegExp(FUNCTION_NAME_RESERVED_PATTERN.source, 'g'); + +const RESERVED_KEYWORDS = new Set([ + 'arguments', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'enum', + 'eval', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'implements', + 'import', + 'in', + 'instanceof', + 'interface', + 'let', + 'new', + 'null', + 'package', + 'private', + 'protected', + 'public', + 'return', + 'static', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', +]); + +function matchArity(fn: Function, length: number): Function { + let mockConstructor; + + switch (length) { + case 1: + mockConstructor = function (this: unknown, _a: unknown) { + return fn.apply(this, arguments); + }; + break; + case 2: + mockConstructor = function (this: unknown, _a: unknown, _b: unknown) { + return fn.apply(this, arguments); + }; + break; + case 3: + mockConstructor = function (this: unknown, _a: unknown, _b: unknown, _c: unknown) { + return fn.apply(this, arguments); + }; + break; + case 4: + mockConstructor = function ( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown + ) { + return fn.apply(this, arguments); + }; + break; + case 5: + mockConstructor = function ( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + _e: unknown + ) { + return fn.apply(this, arguments); + }; + break; + case 6: + mockConstructor = function ( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + _e: unknown, + _f: unknown + ) { + return fn.apply(this, arguments); + }; + break; + case 7: + mockConstructor = function ( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + _e: unknown, + _f: unknown, + _g: unknown + ) { + return fn.apply(this, arguments); + }; + break; + case 8: + mockConstructor = function ( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + _e: unknown, + _f: unknown, + _g: unknown, + _h: unknown + ) { + return fn.apply(this, arguments); + }; + break; + case 9: + mockConstructor = function ( + this: unknown, + _a: unknown, + _b: unknown, + _c: unknown, + _d: unknown, + _e: unknown, + _f: unknown, + _g: unknown, + _h: unknown, + _i: unknown + ) { + return fn.apply(this, arguments); + }; + break; + default: + mockConstructor = function (this: unknown) { + return fn.apply(this, arguments); + }; + break; + } + + return mockConstructor; +} + +function getObjectType(value: unknown): string { + return Object.prototype.toString.apply(value).slice(8, -1); +} + +function getType(ref?: unknown): MockMetadataType | null { + const typeName = getObjectType(ref); + if ( + typeName === 'Function' || + typeName === 'AsyncFunction' || + typeName === 'GeneratorFunction' || + typeName === 'AsyncGeneratorFunction' + ) { + return 'function'; + } + if (Array.isArray(ref)) { + return 'array'; + } + if (typeName === 'Object' || typeName === 'Module') { + return 'object'; + } + if ( + typeName === 'Number' || + typeName === 'String' || + typeName === 'Boolean' || + typeName === 'Symbol' + ) { + return 'constant'; + } + if (typeName === 'Map' || typeName === 'WeakMap' || typeName === 'Set') { + return 'collection'; + } + if (typeName === 'RegExp') { + return 'regexp'; + } + if (ref === undefined) { + return 'undefined'; + } + if (ref === null) { + return 'null'; + } + return null; +} + +function isReadonlyProp(object: unknown, prop: string): boolean { + if ( + prop === 'arguments' || + prop === 'caller' || + prop === 'callee' || + prop === 'name' || + prop === 'length' + ) { + const typeName = getObjectType(object); + return ( + typeName === 'Function' || + typeName === 'AsyncFunction' || + typeName === 'GeneratorFunction' || + typeName === 'AsyncGeneratorFunction' + ); + } + + if (prop === 'source' || prop === 'global' || prop === 'ignoreCase' || prop === 'multiline') { + return getObjectType(object) === 'RegExp'; + } + + return false; +} + +export class ModuleMocker { + private readonly _environmentGlobal: typeof globalThis; + + private _mockState: WeakMap; + + private _mockConfigRegistry: WeakMap; + + private _spyState: Set; + + private _invocationCallCounter: number; + + /** + * @see README.md + * @param global Global object of the test environment, used to create + * mocks + */ + constructor(global: typeof globalThis) { + this._environmentGlobal = global; + this._mockState = new WeakMap(); + this._mockConfigRegistry = new WeakMap(); + this._spyState = new Set(); + this._invocationCallCounter = 1; + } + + private _getSlots(object?: Record): Array { + if (!object) { + return []; + } + + const slots = new Set(); + const EnvObjectProto = this._environmentGlobal.Object.prototype; + const EnvFunctionProto = this._environmentGlobal.Function.prototype; + const EnvRegExpProto = this._environmentGlobal.RegExp.prototype; + + // Also check the builtins in the current context as they leak through + // core node modules. + const ObjectProto = Object.prototype; + const FunctionProto = Function.prototype; + const RegExpProto = RegExp.prototype; + + // Properties of Object.prototype, Function.prototype and RegExp.prototype + // are never reported as slots + while ( + object != null && + object !== EnvObjectProto && + object !== EnvFunctionProto && + object !== EnvRegExpProto && + object !== ObjectProto && + object !== FunctionProto && + object !== RegExpProto + ) { + const ownNames = Object.getOwnPropertyNames(object); + + for (let i = 0; i < ownNames.length; i++) { + const prop = ownNames[i]; + + if (!isReadonlyProp(object, prop)) { + const propDesc = Object.getOwnPropertyDescriptor(object, prop); + if ((propDesc !== undefined && !propDesc.get) || object.__esModule) { + slots.add(prop); + } + } + } + + object = Object.getPrototypeOf(object); + } + + return Array.from(slots); + } + + private _ensureMockConfig(f: Mock): MockFunctionConfig { + let config = this._mockConfigRegistry.get(f); + if (!config) { + config = this._defaultMockConfig(); + this._mockConfigRegistry.set(f, config); + } + return config; + } + + private _ensureMockState(f: Mock): MockFunctionState { + let state = this._mockState.get(f); + if (!state) { + state = this._defaultMockState(); + this._mockState.set(f, state); + } + if (state.calls.length > 0) { + state.lastCall = state.calls[state.calls.length - 1]; + } + return state; + } + + private _defaultMockConfig(): MockFunctionConfig { + return { + mockImpl: undefined, + mockName: 'jest.fn()', + specificMockImpls: [], + }; + } + + private _defaultMockState(): MockFunctionState { + return { + calls: [], + contexts: [], + instances: [], + invocationCallOrder: [], + results: [], + }; + } + + private _makeComponent>( + metadata: MockMetadata, + spyState?: SpyState + ): T; + private _makeComponent>( + metadata: MockMetadata, + spyState?: SpyState + ): T; + private _makeComponent( + metadata: MockMetadata, + spyState?: SpyState + ): T; + private _makeComponent( + metadata: MockMetadata, + spyState?: SpyState + ): T; + private _makeComponent( + metadata: MockMetadata, + spyState?: SpyState + ): Mock; + private _makeComponent( + metadata: MockMetadata, + spyState?: SpyState + ): Record | Array | RegExp | T | Mock | undefined { + if (metadata.type === 'object') { + return new this._environmentGlobal.Object(); + } + if (metadata.type === 'array') { + return new this._environmentGlobal.Array(); + } + if (metadata.type === 'regexp') { + return new this._environmentGlobal.RegExp(''); + } + if ( + metadata.type === 'constant' || + metadata.type === 'collection' || + metadata.type === 'null' || + metadata.type === 'undefined' + ) { + return metadata.value; + } + if (metadata.type === 'function') { + const prototype = + (metadata.members && metadata.members.prototype && metadata.members.prototype.members) || + {}; + const prototypeSlots = this._getSlots(prototype); + // eslint-disable-next-line @typescript-eslint/no-this-alias + const mocker = this; + const mockConstructor = matchArity(function (this: ReturnType, ...args: Parameters) { + const mockState = mocker._ensureMockState(f); + const mockConfig = mocker._ensureMockConfig(f); + mockState.instances.push(this); + mockState.contexts.push(this); + mockState.calls.push(args); + // Create and record an "incomplete" mock result immediately upon + // calling rather than waiting for the mock to return. This avoids + // issues caused by recursion where results can be recorded in the + // wrong order. + const mockResult: MockFunctionResult = { + type: 'incomplete', + value: undefined, + }; + mockState.results.push(mockResult); + mockState.invocationCallOrder.push(mocker._invocationCallCounter++); + + // Will be set to the return value of the mock if an error is not thrown + let finalReturnValue; + // Will be set to the error that is thrown by the mock (if it throws) + let thrownError; + // Will be set to true if the mock throws an error. The presence of a + // value in `thrownError` is not a 100% reliable indicator because a + // function could throw a value of undefined. + let callDidThrowError = false; + + try { + // The bulk of the implementation is wrapped in an immediately + // executed arrow function so the return value of the mock function + // can be easily captured and recorded, despite the many separate + // return points within the logic. + finalReturnValue = (() => { + if (this instanceof f) { + // This is probably being called as a constructor + prototypeSlots.forEach((slot) => { + // Copy prototype methods to the instance to make + // it easier to interact with mock instance call and + // return values + if (prototype[slot].type === 'function') { + // @ts-expect-error no index signature + const protoImpl = this[slot]; + // @ts-expect-error no index signature + this[slot] = mocker.generateFromMetadata(prototype[slot]); + // @ts-expect-error no index signature + this[slot]._protoImpl = protoImpl; + } + }); + + // Run the mock constructor implementation + const mockImpl = mockConfig.specificMockImpls.length + ? mockConfig.specificMockImpls.shift() + : mockConfig.mockImpl; + return mockImpl && mockImpl.apply(this, arguments); + } + + // If mockImplementationOnce()/mockImplementation() is last set, + // implementation use the mock + let specificMockImpl = mockConfig.specificMockImpls.shift(); + if (specificMockImpl === undefined) { + specificMockImpl = mockConfig.mockImpl; + } + if (specificMockImpl) { + return specificMockImpl.apply(this, arguments); + } + // Otherwise use prototype implementation + if (f._protoImpl) { + return f._protoImpl.apply(this, arguments); + } + + return undefined; + })(); + } catch (error) { + // Store the thrown error so we can record it, then re-throw it. + thrownError = error; + callDidThrowError = true; + throw error; + } finally { + // Record the result of the function. + // NOTE: Intentionally NOT pushing/indexing into the array of mock + // results here to avoid corrupting results data if mockClear() + // is called during the execution of the mock. + // @ts-expect-error reassigning 'incomplete' + mockResult.type = callDidThrowError ? 'throw' : 'return'; + mockResult.value = callDidThrowError ? thrownError : finalReturnValue; + } + + return finalReturnValue; + }, metadata.length || 0); + + const f = this._createMockFunction(metadata, mockConstructor) as Mock; + f._isMockFunction = true; + f.getMockImplementation = () => this._ensureMockConfig(f).mockImpl as T; + + if (spyState != null) { + this._spyState.add(spyState); + } + + this._mockState.set(f, this._defaultMockState()); + this._mockConfigRegistry.set(f, this._defaultMockConfig()); + + Object.defineProperty(f, 'mock', { + configurable: false, + enumerable: true, + get: () => this._ensureMockState(f), + set: (val) => this._mockState.set(f, val), + }); + + f.mockClear = () => { + this._mockState.delete(f); + return f; + }; + + f.mockReset = () => { + f.mockClear(); + this._mockConfigRegistry.delete(f); + + if (spyState != null) { + spyState.reset?.(); + } + + return f; + }; + + f.mockRestore = () => { + f.mockClear(); + this._mockConfigRegistry.delete(f); + + if (spyState != null) { + spyState.restore(); + this._spyState.delete(spyState); + } + }; + + f.mockReturnValueOnce = (value: ReturnType) => + // next function call will return this value or default return value + f.mockImplementationOnce(() => value); + + f.mockResolvedValueOnce = (value: ResolveType) => + f.mockImplementationOnce(() => this._environmentGlobal.Promise.resolve(value)); + + f.mockRejectedValueOnce = (value: unknown) => + f.mockImplementationOnce(() => this._environmentGlobal.Promise.reject(value)); + + f.mockReturnValue = (value: ReturnType) => + // next function call will return specified return value or this one + f.mockImplementation(() => value); + + f.mockResolvedValue = (value: ResolveType) => + f.mockImplementation(() => this._environmentGlobal.Promise.resolve(value)); + + f.mockRejectedValue = (value: unknown) => + f.mockImplementation(() => this._environmentGlobal.Promise.reject(value)); + + f.mockImplementationOnce = (fn: T) => { + // next function call will use this mock implementation return value + // or default mock implementation return value + const mockConfig = this._ensureMockConfig(f); + mockConfig.specificMockImpls.push(fn); + return f; + }; + + f.withImplementation = withImplementation.bind(this); + + function withImplementation(fn: T, callback: () => void): void; + function withImplementation(fn: T, callback: () => Promise): Promise; + function withImplementation( + this: ModuleMocker, + fn: T, + callback: (() => void) | (() => Promise) + ): void | Promise { + // Remember previous mock implementation, then set new one + const mockConfig = this._ensureMockConfig(f); + const previousImplementation = mockConfig.mockImpl; + const previousSpecificImplementations = mockConfig.specificMockImpls; + mockConfig.mockImpl = fn; + mockConfig.specificMockImpls = []; + + const returnedValue = callback(); + + if (isPromise(returnedValue)) { + return returnedValue.then(() => { + mockConfig.mockImpl = previousImplementation; + mockConfig.specificMockImpls = previousSpecificImplementations; + }); + } + mockConfig.mockImpl = previousImplementation; + mockConfig.specificMockImpls = previousSpecificImplementations; + } + + f.mockImplementation = (fn: T) => { + // next function call will use mock implementation return value + const mockConfig = this._ensureMockConfig(f); + mockConfig.mockImpl = fn; + return f; + }; + + f.mockReturnThis = () => + f.mockImplementation(function (this: ReturnType) { + return this; + }); + + f.mockName = (name: string) => { + if (name) { + const mockConfig = this._ensureMockConfig(f); + mockConfig.mockName = name; + } + return f; + }; + + f.getMockName = () => { + const mockConfig = this._ensureMockConfig(f); + return mockConfig.mockName || 'jest.fn()'; + }; + + if (metadata.mockImpl) { + f.mockImplementation(metadata.mockImpl); + } + + return f; + } + const unknownType = metadata.type || 'undefined type'; + throw new Error(`Unrecognized type ${unknownType}`); + } + + private _createMockFunction( + metadata: MockMetadata, + mockConstructor: Function + ): Function { + let { name } = metadata; + if (!name) { + return mockConstructor; + } + + // Preserve `name` property of mocked function. + const boundFunctionPrefix = 'bound '; + let bindCall = ''; + // if-do-while for perf reasons. The common case is for the if to fail. + if (name.startsWith(boundFunctionPrefix)) { + do { + name = name.substring(boundFunctionPrefix.length); + // Call bind() just to alter the function name. + bindCall = '.bind(null)'; + } while (name && name.startsWith(boundFunctionPrefix)); + } + + // Special case functions named `mockConstructor` to guard for infinite loops + if (name === MOCK_CONSTRUCTOR_NAME) { + return mockConstructor; + } + + if ( + // It's a syntax error to define functions with a reserved keyword as name + RESERVED_KEYWORDS.has(name) || + // It's also a syntax error to define functions with a name that starts with a number + /^\d/.test(name) + ) { + name = `$${name}`; + } + + // It's also a syntax error to define a function with a reserved character + // as part of it's name. + if (FUNCTION_NAME_RESERVED_PATTERN.test(name)) { + name = name.replace(FUNCTION_NAME_RESERVED_REPLACE, '$'); + } + + const body = + `return function ${name}() {` + + ` return ${MOCK_CONSTRUCTOR_NAME}.apply(this,arguments);` + + `}${bindCall}`; + const createConstructor = new this._environmentGlobal.Function(MOCK_CONSTRUCTOR_NAME, body); + + return createConstructor(mockConstructor); + } + + private _generateMock( + metadata: MockMetadata, + callbacks: Array, + refs: Record | Array | RegExp | T | Mock | undefined> + ): Mocked { + // metadata not compatible but it's the same type, maybe problem with + // overloading of _makeComponent and not _generateMock? + // @ts-expect-error - unsure why TSC complains here? + const mock = this._makeComponent(metadata); + if (metadata.refID != null) { + refs[metadata.refID] = mock; + } + + this._getSlots(metadata.members).forEach((slot) => { + const slotMetadata = (metadata.members && metadata.members[slot]) || {}; + if (slotMetadata.ref != null) { + callbacks.push( + (function (ref) { + // @ts-expect-error (is any) + return () => (mock[slot] = refs[ref]); + })(slotMetadata.ref) + ); + } else { + // @ts-expect-error (is any) + mock[slot] = this._generateMock(slotMetadata, callbacks, refs); + } + }); + + if ( + metadata.type !== 'undefined' && + metadata.type !== 'null' && + // @ts-expect-error (is any) + mock.prototype && + // @ts-expect-error (is any) + typeof mock.prototype === 'object' + ) { + // @ts-expect-error (is any) + mock.prototype.constructor = mock; + } + + return mock as Mocked; + } + + /** + * Check whether the given property of an object has been already replaced. + */ + private _findReplacedProperty( + object: T, + propertyKey: K + ): ReplacedPropertyRestorer | undefined { + for (const { restore } of this._spyState) { + if ( + 'object' in restore && + 'property' in restore && + restore.object === object && + restore.property === propertyKey + ) { + return restore as ReplacedPropertyRestorer; + } + } + } + + /** + * @see README.md + * @param metadata Metadata for the mock in the schema returned by the + * getMetadata method of this module. + */ + generateFromMetadata(metadata: MockMetadata): Mocked { + const callbacks: Array = []; + const refs = {}; + const mock = this._generateMock(metadata, callbacks, refs); + callbacks.forEach((setter) => setter()); + return mock; + } + + /** + * @see README.md + * @param component The component for which to retrieve metadata. + */ + getMetadata(component: T, _refs?: Map): MockMetadata | null { + const refs = _refs || new Map(); + const ref = refs.get(component); + if (ref != null) { + return { ref }; + } + + const type = getType(component); + if (!type) { + return null; + } + + const metadata: MockMetadata = { type }; + if (type === 'constant' || type === 'collection' || type === 'undefined' || type === 'null') { + metadata.value = component; + return metadata; + } + if (type === 'function') { + // @ts-expect-error component is a function so it has a name, but not + // necessarily a string: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name#function_names_in_classes + const componentName = component.name; + if (typeof componentName === 'string') { + metadata.name = componentName; + } + if (this.isMockFunction(component)) { + metadata.mockImpl = component.getMockImplementation() as T; + } + } + + metadata.refID = refs.size; + refs.set(component, metadata.refID); + + let members: Record> | null = null; + // Leave arrays alone + if (type !== 'array') { + this._getSlots(component).forEach((slot) => { + if (type === 'function' && this.isMockFunction(component) && slot.match(/^mock/)) { + return; + } + // @ts-expect-error no index signature + const slotMetadata = this.getMetadata(component[slot], refs); + if (slotMetadata) { + if (!members) { + members = {}; + } + members[slot] = slotMetadata; + } + }); + } + + if (members) { + metadata.members = members; + } + + return metadata; + } + + isMockFunction( + fn: MockInstance + ): fn is MockInstance; + isMockFunction

, R>(fn: (...args: P) => R): fn is Mock<(...args: P) => R>; + isMockFunction(fn: unknown): fn is Mock; + isMockFunction(fn: unknown): fn is Mock { + return fn != null && (fn as Mock)._isMockFunction === true; + } + + fn(implementation?: T): Mock { + const length = implementation ? implementation.length : 0; + const fn = this._makeComponent({ + length, + type: 'function', + }); + if (implementation) { + fn.mockImplementation(implementation); + } + return fn; + } + + private _attachMockImplementation(mock: Mock, original: T) { + mock.mockImplementation(function (this: unknown) { + return original.apply(this, arguments); + }); + } + + spyOn< + T extends object, + K extends PropertyLikeKeys, + V extends Required[K], + A extends 'get' | 'set' + >( + object: T, + methodKey: K, + accessType: A + ): A extends 'get' ? SpiedGetter : A extends 'set' ? SpiedSetter : never; + + spyOn< + T extends object, + K extends ConstructorLikeKeys | MethodLikeKeys, + V extends Required[K] + >(object: T, methodKey: K): V extends ClassLike | FunctionLike ? Spied : never; + + spyOn(object: T, methodKey: keyof T, accessType?: 'get' | 'set'): MockInstance { + if (object == null || (typeof object !== 'object' && typeof object !== 'function')) { + throw new Error(`Cannot use spyOn on a primitive value; ${this._typeOf(object)} given`); + } + + if (methodKey == null) { + throw new Error('No property name supplied'); + } + + if (accessType) { + return this._spyOnProperty(object, methodKey, accessType); + } + + const original = object[methodKey]; + + if (!original) { + throw new Error(`Property \`${String(methodKey)}\` does not exist in the provided object`); + } + + if (!this.isMockFunction(original)) { + if (typeof original !== 'function') { + throw new Error( + `Cannot spy on the \`${String( + methodKey + )}\` property because it is not a function; ${this._typeOf(original)} given instead.${ + typeof original !== 'object' + ? ` If you are trying to mock a property, use \`jest.replaceProperty(object, '${String( + methodKey + )}', value)\` instead.` + : '' + }` + ); + } + + const isMethodOwner = Object.prototype.hasOwnProperty.call(object, methodKey); + + let descriptor = Object.getOwnPropertyDescriptor(object, methodKey); + let proto = Object.getPrototypeOf(object); + + while (!descriptor && proto !== null) { + descriptor = Object.getOwnPropertyDescriptor(proto, methodKey); + proto = Object.getPrototypeOf(proto); + } + + let mock: Mock; + + if (descriptor && descriptor.get) { + const originalGet = descriptor.get; + mock = this._makeComponent( + { type: 'function' }, + { + reset: () => { + this._attachMockImplementation(mock, original); + }, + restore: () => { + descriptor!.get = originalGet; + Object.defineProperty(object, methodKey, descriptor!); + }, + } + ); + descriptor.get = () => mock; + Object.defineProperty(object, methodKey, descriptor); + } else { + mock = this._makeComponent( + { type: 'function' }, + { + reset: () => { + this._attachMockImplementation(mock, original); + }, + restore: () => { + if (isMethodOwner) { + object[methodKey] = original; + } else { + delete object[methodKey]; + } + }, + } + ); + // @ts-expect-error: overriding original method with a mock + object[methodKey] = mock; + } + + this._attachMockImplementation(mock, original); + } + + return object[methodKey] as Mock; + } + + private _spyOnProperty( + object: T, + propertyKey: keyof T, + accessType: 'get' | 'set' + ): MockInstance { + let descriptor = Object.getOwnPropertyDescriptor(object, propertyKey); + let proto = Object.getPrototypeOf(object); + + while (!descriptor && proto !== null) { + descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey); + proto = Object.getPrototypeOf(proto); + } + + if (!descriptor) { + throw new Error(`Property \`${String(propertyKey)}\` does not exist in the provided object`); + } + + if (!descriptor.configurable) { + throw new Error(`Property \`${String(propertyKey)}\` is not declared configurable`); + } + + if (!descriptor[accessType]) { + throw new Error( + `Property \`${String(propertyKey)}\` does not have access type ${accessType}` + ); + } + + const original = descriptor[accessType]; + + if (!this.isMockFunction(original)) { + if (typeof original !== 'function') { + throw new Error( + `Cannot spy on the ${String( + propertyKey + )} property because it is not a function; ${this._typeOf(original)} given instead.${ + typeof original !== 'object' + ? ` If you are trying to mock a property, use \`jest.replaceProperty(object, '${String( + propertyKey + )}', value)\` instead.` + : '' + }` + ); + } + + descriptor[accessType] = this._makeComponent( + { type: 'function' }, + { + reset: () => { + this._attachMockImplementation(descriptor![accessType] as Mock, original); + }, + restore: () => { + // @ts-expect-error: overriding original method with a mock + descriptor![accessType] = original; + Object.defineProperty(object, propertyKey, descriptor!); + }, + } + ); + + this._attachMockImplementation(descriptor[accessType] as Mock, original); + } + + Object.defineProperty(object, propertyKey, descriptor); + return descriptor[accessType] as Mock; + } + + replaceProperty( + object: T, + propertyKey: K, + value: T[K] + ): Replaced { + if (object == null || (typeof object !== 'object' && typeof object !== 'function')) { + throw new Error( + `Cannot use replaceProperty on a primitive value; ${this._typeOf(object)} given` + ); + } + + if (propertyKey == null) { + throw new Error('No property name supplied'); + } + + let descriptor = Object.getOwnPropertyDescriptor(object, propertyKey); + let proto = Object.getPrototypeOf(object); + while (!descriptor && proto !== null) { + descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey); + proto = Object.getPrototypeOf(proto); + } + if (!descriptor) { + throw new Error(`Property \`${String(propertyKey)}\` does not exist in the provided object`); + } + if (!descriptor.configurable) { + throw new Error(`Property \`${String(propertyKey)}\` is not declared configurable`); + } + + if (descriptor.get !== undefined) { + throw new Error( + `Cannot replace the \`${String( + propertyKey + )}\` property because it has a getter. Use \`jest.spyOn(object, '${String( + propertyKey + )}', 'get').mockReturnValue(value)\` instead.` + ); + } + + if (descriptor.set !== undefined) { + throw new Error( + `Cannot replace the \`${String( + propertyKey + )}\` property because it has a setter. Use \`jest.spyOn(object, '${String( + propertyKey + )}', 'set').mockReturnValue(value)\` instead.` + ); + } + + if (typeof descriptor.value === 'function') { + throw new Error( + `Cannot replace the \`${String( + propertyKey + )}\` property because it is a function. Use \`jest.spyOn(object, '${String( + propertyKey + )}')\` instead.` + ); + } + + const existingRestore = this._findReplacedProperty(object, propertyKey); + + if (existingRestore) { + return existingRestore.replaced.replaceValue(value); + } + + const isPropertyOwner = Object.prototype.hasOwnProperty.call(object, propertyKey); + const originalValue = descriptor.value; + + const restore: ReplacedPropertyRestorer = () => { + if (isPropertyOwner) { + object[propertyKey] = originalValue; + } else { + delete object[propertyKey]; + } + }; + + const replaced: Replaced = { + replaceValue: (value) => { + object[propertyKey] = value; + + return replaced; + }, + + restore: () => { + restore(); + + this._spyState.delete({ restore }); + }, + }; + + restore.object = object; + restore.property = propertyKey; + restore.replaced = replaced; + + this._spyState.add({ restore }); + + return replaced.replaceValue(value); + } + + clearAllMocks(): void { + this._mockState = new WeakMap(); + } + + resetAllMocks(): void { + this.clearAllMocks(); + this._mockConfigRegistry = new WeakMap(); + this._spyState.forEach((spyState) => spyState.reset?.()); + } + + restoreAllMocks(): void { + this.clearAllMocks(); + this._mockConfigRegistry = new WeakMap(); + this._spyState.forEach((spyState) => spyState.restore()); + this._spyState = new Set(); + } + + private _typeOf(value: unknown): string { + return value == null ? `${value}` : typeof value; + } + + mocked(source: T, options?: { shallow: false }): Mocked; + mocked(source: T, options: { shallow: true }): MockedShallow; + mocked( + source: T, + _options?: { shallow: boolean } + ): Mocked | MockedShallow { + return source as Mocked | MockedShallow; + } +} diff --git a/code/addons/interactions/src/preview.ts b/code/addons/interactions/src/preview.ts index 972126421393..c8cc53991c4b 100644 --- a/code/addons/interactions/src/preview.ts +++ b/code/addons/interactions/src/preview.ts @@ -11,7 +11,7 @@ import type { StepLabel, } from '@storybook/types'; import { instrument } from '@storybook/instrumenter'; -import { ModuleMocker } from 'jest-mock'; +import { ModuleMocker } from './jest-mock'; const JestMock = new ModuleMocker(global); const fn = JestMock.fn.bind(JestMock); diff --git a/code/yarn.lock b/code/yarn.lock index 54fd489d64c5..64685b68b568 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5761,7 +5761,6 @@ __metadata: "@storybook/types": 7.2.0-alpha.0 "@types/node": ^16.0.0 formik: ^2.2.9 - jest-mock: ^29.5.0 polished: ^4.2.2 ts-dedent: ^2.2.0 typescript: ~4.9.3 @@ -19797,7 +19796,7 @@ __metadata: languageName: node linkType: hard -"jest-mock@npm:^29.5.0, jest-mock@npm:^29.6.1": +"jest-mock@npm:^29.6.1": version: 29.6.1 resolution: "jest-mock@npm:29.6.1" dependencies: