Skip to content

Commit

Permalink
Added spread support to MockProxy (deephaven#1809)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmingles committed Feb 15, 2024
1 parent d61d350 commit bf1ed5c
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 4 deletions.
54 changes: 54 additions & 0 deletions packages/utils/src/MockProxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,58 @@ describe('createMockProxy', () => {
});
expect(mock.testMethod).toBeInstanceOf(jest.fn().constructor);
});

it.each([undefined, 'some label'])('should be spreadable: %s', label => {
const overrides = {
name: 'mock.name',
age: 42,
};

const mock = createMockProxy<{
name: string;
age: number;
testMethod: () => void;
}>(overrides, { label });

expect({ ...mock }).toEqual({
[MockProxySymbol.labelSymbol]: label ?? 'Mock Proxy',
name: 'mock.name',
age: 42,
});
});

it.each([undefined, true, false])(
'should include accessed auto proxy props if includeAutoProxiesInOwnKeys is true: %s',
includeAutoProxiesInOwnKeys => {
const overrides = {
name: 'mock.name',
age: 42,
};

const mock = createMockProxy<{
name: string;
age: number;
testMethod: () => void;
}>(overrides, { includeAutoProxiesInOwnKeys });

const expectedBase = {
[MockProxySymbol.labelSymbol]: 'Mock Proxy',
name: 'mock.name',
age: 42,
};

expect({ ...mock }).toEqual(expectedBase);

mock.testMethod();

if (includeAutoProxiesInOwnKeys === true) {
expect({ ...mock }).toEqual({
...expectedBase,
testMethod: expect.any(Function),
});
} else {
expect({ ...mock }).toEqual(expectedBase);
}
}
);
});
51 changes: 47 additions & 4 deletions packages/utils/src/MockProxy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const labelSymbol: unique symbol = Symbol('mockProxyType');
const defaultPropsSymbol: unique symbol = Symbol('mockProxyDefaultProps');
const overridesSymbol: unique symbol = Symbol('mockProxyOverrides');
const proxiesSymbol: unique symbol = Symbol('mockProxyProxies');

export const MockProxySymbol = {
labelSymbol,
defaultProps: defaultPropsSymbol,
overrides: overridesSymbol,
proxies: proxiesSymbol,
Expand All @@ -29,32 +31,53 @@ const mockProxyDefaultProps = {
* The proxy target contains state + configuration for the proxy
*/
export interface MockProxyTarget<T> {
[MockProxySymbol.labelSymbol]: string;
[MockProxySymbol.defaultProps]: typeof mockProxyDefaultProps;
[MockProxySymbol.overrides]: Partial<T>;
[MockProxySymbol.proxies]: Record<keyof T, jest.Mock>;
}

export interface MockProxyConfig {
// Optional label to be assigned to the proxy object's
// `MockProxySymbol.labelSymbol` property.
label?: string;

// `ownKeys` has no way to know all of the potential auto proxy keys, but it
// can know auto proxies that have been called / cached. If this flag is true,
// include those in the `ownKeys` result. This is mostly useful for spread
// operations. Alternatively, the `overrides` are can explicitly include any
// proxies to be included in the `ownKeys` result without setting this flag.
// e.g. createMockProxy({ someMethod: jest.fn() }) would include `someMethod`.
includeAutoProxiesInOwnKeys?: boolean;
}

/**
* Creates a mock object for a type `T` using a Proxy object. Each prop can
* optionally be set via the constructor. Any prop that is not set will be set
* to a jest.fn() instance on first access with the exeption of "then" which
* will not be automatically proxied.
* @param overrides Optional props to explicitly set on the Proxy.
* @returns
* @param config Optional configuration for the proxy.
* @returns A mock Proxy object for type `T`.
*/
export default function createMockProxy<T>(
overrides: Partial<T> = {}
overrides: Partial<T> = {},
{
label = 'Mock Proxy',
includeAutoProxiesInOwnKeys = false,
}: MockProxyConfig = {}
): T & MockProxyTarget<T> {
const targetDef: MockProxyTarget<T> = {
[MockProxySymbol.labelSymbol]: label,
[MockProxySymbol.defaultProps]: mockProxyDefaultProps,
[MockProxySymbol.overrides]: overrides,
[MockProxySymbol.proxies]: {} as Record<keyof T, jest.Mock>,
};

return new Proxy(targetDef, {
get(target, name) {
if (name === Symbol.toStringTag) {
return 'Mock Proxy';
if (name === Symbol.toStringTag || name === MockProxySymbol.labelSymbol) {
return targetDef[MockProxySymbol.labelSymbol];
}

// Reserved attributes for the proxy
Expand Down Expand Up @@ -92,5 +115,25 @@ export default function createMockProxy<T>(
has(target, name) {
return name in target[MockProxySymbol.overrides];
},
// Needed to support the spread (...) operator
getOwnPropertyDescriptor(_target, _prop) {
return { configurable: true, enumerable: true };
},
// Needed to support the spread (...) operator
ownKeys(target) {
const autoProxyKeys = includeAutoProxiesInOwnKeys
? Reflect.ownKeys(target[MockProxySymbol.proxies])
: [];

const overridesKeys = Reflect.ownKeys(target[MockProxySymbol.overrides]);

return [
...new Set<string | symbol>([
MockProxySymbol.labelSymbol,
...autoProxyKeys,
...overridesKeys,
]),
];
},
}) as T & typeof targetDef;
}

0 comments on commit bf1ed5c

Please sign in to comment.