Skip to content

Commit

Permalink
fix: progress on symbols
Browse files Browse the repository at this point in the history
  • Loading branch information
ffMathy committed Mar 10, 2024
1 parent 2e93eef commit cdd3d04
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 61 deletions.
32 changes: 16 additions & 16 deletions spec/regression/didNotReceive.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'ava'
import { Substitute, Arg } from '../../src'
import { Substitute, Arg, didNotReceive, received } from '../../src'
import { SubstituteException } from '../../src/SubstituteException'

interface Calculator {
Expand All @@ -12,41 +12,41 @@ interface Calculator {
test('not calling a method correctly asserts the call count', t => {
const calculator = Substitute.for<Calculator>()

calculator.didNotReceive().add(1, 1)
t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException })
t.throws(() => calculator.received().add(Arg.all()), { instanceOf: SubstituteException })
calculator[didNotReceive]().add(1, 1)
t.throws(() => calculator[received]().add(1, 1), { instanceOf: SubstituteException })
t.throws(() => calculator[received]().add(Arg.all()), { instanceOf: SubstituteException })
})

test('not getting a property correctly asserts the call count', t => {
const calculator = Substitute.for<Calculator>()

calculator.didNotReceive().isEnabled
t.throws(() => calculator.received(1).isEnabled, { instanceOf: SubstituteException })
t.throws(() => calculator.received().isEnabled, { instanceOf: SubstituteException })
calculator[didNotReceive]().isEnabled
t.throws(() => calculator[received](1).isEnabled, { instanceOf: SubstituteException })
t.throws(() => calculator[received]().isEnabled, { instanceOf: SubstituteException })
})

test('not setting a property correctly asserts the call count', t => {
const calculator = Substitute.for<Calculator>()

calculator.didNotReceive().isEnabled = true
t.throws(() => calculator.received(1).isEnabled = true, { instanceOf: SubstituteException })
t.throws(() => calculator.received().isEnabled = true, { instanceOf: SubstituteException })
calculator[didNotReceive]().isEnabled = true
t.throws(() => calculator[received](1).isEnabled = true, { instanceOf: SubstituteException })
t.throws(() => calculator[received]().isEnabled = true, { instanceOf: SubstituteException })
})

test('not calling a method with mock correctly asserts the call count', t => {
const calculator = Substitute.for<Calculator>()
calculator.add(1, 1).returns(2)

calculator.didNotReceive().add(1, 1)
t.throws(() => calculator.received(1).add(1, 1), { instanceOf: SubstituteException })
t.throws(() => calculator.received().add(Arg.all()), { instanceOf: SubstituteException })
calculator[didNotReceive]().add(1, 1)
t.throws(() => calculator[received](1).add(1, 1), { instanceOf: SubstituteException })
t.throws(() => calculator[received]().add(Arg.all()), { instanceOf: SubstituteException })
})

test('not getting a property with mock correctly asserts the call count', t => {
const calculator = Substitute.for<Calculator>()
calculator.isEnabled.returns(true)

calculator.didNotReceive().isEnabled
t.throws(() => calculator.received(1).isEnabled, { instanceOf: SubstituteException })
t.throws(() => calculator.received().isEnabled, { instanceOf: SubstituteException })
calculator[didNotReceive]().isEnabled
t.throws(() => calculator[received](1).isEnabled, { instanceOf: SubstituteException })
t.throws(() => calculator[received]().isEnabled, { instanceOf: SubstituteException })
})
2 changes: 0 additions & 2 deletions spec/regression/returns.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ test('returns a primitive value for method with specific arguments', t => {

calculator.add(1, 1).returns(2)

calculator.clearSubstitute

t.is(2, calculator.add(1, 1))
t.is(2, calculator.add(1, 1))
t.true(types.isProxy(noResult))
Expand Down
1 change: 1 addition & 0 deletions src/Substitute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DisabledSubstituteObject, ObjectSubstitute } from './Transformations'
import { SubstituteNode } from './SubstituteNode'

export type SubstituteOf<T> = ObjectSubstitute<T> & T

type InstantiableSubstitute<T extends SubstituteOf<unknown>> = T & { [SubstituteNode.instance]: SubstituteNode }

export class Substitute {
Expand Down
82 changes: 56 additions & 26 deletions src/SubstituteNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@ import { SubstituteNodeBase } from './SubstituteNodeBase'
import { RecordedArguments } from './RecordedArguments'
import { ClearType as ClearTypeMap, PropertyType as PropertyTypeMap, isAssertionMethod, isSubstituteMethod, isSubstitutionMethod, textModifier, isConfigurationMethod } from './Utilities'
import { SubstituteException } from './SubstituteException'
import type { FilterFunction, SubstituteContext, SubstitutionMethod, ClearType, PropertyType } from './Types'
import type { FilterFunction, SubstituteContext, SubstitutionMethod, PropertyType } from './Types'
import type { ObjectSubstitute } from './Transformations'
import { didNotReceive, mimick, mimicks, received, rejects, resolves, returns, throws } from './Symbols'
import { didNotReceive, mimick, mimicks, received, rejects, resolves, returns, throws, clearReceivedCalls } from './Transformations'

const instance = Symbol('Substitute:Instance')
const clearTypeToFilterMap: Record<ClearType, FilterFunction<SubstituteNode>> = {
all: () => true,
receivedCalls: node => !node.hasContext,
substituteValues: node => node.isSubstitution
}

type SpecialProperty = typeof instance | typeof inspect.custom | 'then'
type RootContext = { substituteMethodsEnabled: boolean }
Expand All @@ -31,35 +26,54 @@ export class SubstituteNode extends SubstituteNodeBase implements ObjectSubstitu

private constructor(key: PropertyKey, parent?: SubstituteNode) {
super(key, parent)
if (this.isRoot()) this._rootContext = { substituteMethodsEnabled: true }
else this._rootContext = this.root.rootContext
if (this.isRoot()) {
this._rootContext = { substituteMethodsEnabled: true }
}
else {
this._rootContext = this.root.rootContext
}

this._proxy = new Proxy(
this,
{
get: function (target, property) {
if (target.isSpecialProperty(property)) return target.evaluateSpecialProperty(property)
if (target._retrySubstitutionExecutionAttempt) return target.reattemptSubstitutionExecution()[property]
if (target.isSpecialProperty(property))
return target.evaluateSpecialProperty(property)

if (target._retrySubstitutionExecutionAttempt)
return target.reattemptSubstitutionExecution()[property]

const newNode = SubstituteNode.createChild(property, target)
if (target.isAssertion) newNode.executeAssertion()
if (target.isAssertion)
newNode.executeAssertion()

if (target.isRoot() && target.rootContext.substituteMethodsEnabled && (isAssertionMethod(property) || isConfigurationMethod(property))) {
newNode.assignContext(property)
return newNode[property].bind(newNode)
}

return newNode.attemptSubstitutionExecution()
},
set: function (target, property, value) {
const newNode = SubstituteNode.createChild(property, target)
newNode.handleSetter(value)
if (target.isAssertion) newNode.executeAssertion()
if (target.isAssertion)
newNode.executeAssertion()

return true
},
apply: function (target, _thisArg, rawArguments) {
target.handleMethod(rawArguments)
if (target.hasDepthOfAtLeast(2)) {
if (isSubstitutionMethod(target.property)) return target.parent.assignContext(target.property)
if (target.parent.isAssertion) return target.executeAssertion()
if (isSubstitutionMethod(target.property))
return target.parent.assignContext(target.property)

if (target.parent.isAssertion)
return target.executeAssertion()
}
return target.isAssertion ? target.proxy : target.attemptSubstitutionExecution()
return target.isAssertion ?
target.proxy :
target.attemptSubstitutionExecution()
}
}
)
Expand Down Expand Up @@ -129,9 +143,10 @@ export class SubstituteNode extends SubstituteNodeBase implements ObjectSubstitu
throw new Error('Mimick is not implemented yet')
}

public clearSubstitute(clearType: ClearType = ClearTypeMap.All): void {
this.handleMethod([clearType])
const filter = clearTypeToFilterMap[clearType]
public [clearReceivedCalls](): void {
this.handleMethod([])

const filter = (node: SubstituteNode) => !node.hasContext
this.recorder.clearRecords(filter)
}

Expand All @@ -140,7 +155,9 @@ export class SubstituteNode extends SubstituteNodeBase implements ObjectSubstitu
}

private assignContext(context: SubstituteContext): void {
if (!isSubstituteMethod(context)) throw new Error(`Cannot assign context for property ${context.toString()}`)
if (!isSubstituteMethod(context))
throw new Error(`Cannot assign context for property ${context.toString()}`)

this._context = context
}

Expand All @@ -158,8 +175,11 @@ export class SubstituteNode extends SubstituteNodeBase implements ObjectSubstitu
}

private executeSubstitution(contextArguments: RecordedArguments) {
if (!this.hasChild()) throw new TypeError('Substitue node has no child')
if (!this.child.recordedArguments.hasArguments()) throw new TypeError('Child args')
if (!this.hasChild())
throw new TypeError('Substitue node has no child')

if (!this.child.recordedArguments.hasArguments())
throw new TypeError('Child args')

const substitutionMethod = this.context as SubstitutionMethod
const substitutionValue = this.child.recordedArguments.value.length > 1
Expand All @@ -168,10 +188,16 @@ export class SubstituteNode extends SubstituteNodeBase implements ObjectSubstitu
switch (substitutionMethod) {
case throws:
throw substitutionValue

case mimicks:
if (this.propertyType === PropertyTypeMap.Property) return substitutionValue()
if (!contextArguments.hasArguments()) throw new TypeError('Context arguments cannot be undefined')
if (this.propertyType === PropertyTypeMap.Property)
return substitutionValue()

if (!contextArguments.hasArguments())
throw new TypeError('Context arguments cannot be undefined')

return substitutionValue(...contextArguments.value)

case resolves:
return Promise.resolve(substitutionValue)
case rejects:
Expand All @@ -184,8 +210,12 @@ export class SubstituteNode extends SubstituteNodeBase implements ObjectSubstitu
}

private executeAssertion(): void | never {
if (!this.hasDepthOfAtLeast(2)) throw new Error('Not possible')
if (!this.parent.recordedArguments.hasArguments()) throw new TypeError('Parent args')
if (!this.hasDepthOfAtLeast(2))
throw new Error('Not possible')

if (!this.parent.recordedArguments.hasArguments())
throw new TypeError('Parent args')

const expectedCount = this.parent.recordedArguments.value[0] ?? undefined
const finiteExpectation = expectedCount !== undefined
if (finiteExpectation && (!Number.isInteger(expectedCount) || expectedCount < 0)) throw new Error('Expected count has to be a positive integer')
Expand Down
11 changes: 0 additions & 11 deletions src/Symbols.ts

This file was deleted.

18 changes: 14 additions & 4 deletions src/Transformations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { AllArguments } from './Arguments';
import { clearReceivedCalls, didNotReceive, mimick, received } from './Symbols';
import type { ClearType, FirstLevelMethod } from './Types';
import type { FirstLevelMethod } from './Types';

type FunctionSubstituteWithOverloads<TFunc, Terminating = false> =
TFunc extends {
Expand Down Expand Up @@ -91,11 +90,22 @@ type ObjectSubstituteTransformation<K, T = OmitProxyMethods<K>> = {
[P in keyof T]: TryToExpandNonArgumentedFunctionSubstitute<T, P> & TryToExpandArgumentedFunctionSubstitute<T, P> & TryToExpandPropertySubstitute<T, P>;
}

export const received = Symbol('received');
export const didNotReceive = Symbol('didNotReceive');
export const mimick = Symbol('mimick');
export const clearReceivedCalls = Symbol('clearReceivedCalls');

export const mimicks = Symbol('mimicks');
export const throws = Symbol('throws');
export const returns = Symbol('returns');
export const resolves = Symbol('resolves');
export const rejects = Symbol('rejects');

export type OmitProxyMethods<T> = Omit<T, FirstLevelMethod>;
export type ObjectSubstitute<T> = ObjectSubstituteTransformation<T> & {
[received](amount?: number): TerminatingObject<T>;
[didNotReceive](): TerminatingObject<T>;
[mimick](instance: OmitProxyMethods<T>): void;
[clearReceivedCalls](clearType?: ClearType): void;
[clearReceivedCalls](): void;
}
export type DisabledSubstituteObject<T> = T extends ObjectSubstitute<infer K> ? K : never;
export type DisabledSubstituteObject<T> = T extends ObjectSubstitute<infer K> ? K : never;
2 changes: 1 addition & 1 deletion src/Types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { clearReceivedCalls, didNotReceive, mimick, mimicks, received, rejects, resolves, returns, throws } from "./Symbols"
import type { clearReceivedCalls, didNotReceive, mimick, mimicks, received, rejects, resolves, returns, throws } from "./Transformations"

export type PropertyType = 'method' | 'property'
export type AssertionMethod = typeof received | typeof didNotReceive
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { Substitute, SubstituteOf } from './Substitute'
export { Arg } from './Arguments'
export { Substitute, SubstituteOf }
export { ClearType } from './Utilities'
export { clearReceivedCalls, didNotReceive, mimick, received } from './Symbols'
export { clearReceivedCalls, didNotReceive, mimick, received } from './Transformations'

export default Substitute

0 comments on commit cdd3d04

Please sign in to comment.