Skip to content

Commit

Permalink
simplify context switching
Browse files Browse the repository at this point in the history
  • Loading branch information
notanengineercom committed Oct 16, 2022
1 parent d4c3e07 commit a49c8bd
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 66 deletions.
115 changes: 54 additions & 61 deletions src/SubstituteNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { inspect, InspectOptions, types } from 'util'

import { SubstituteNodeBase } from './SubstituteNodeBase'
import { RecordedArguments } from './RecordedArguments'
import { ClearType as ClearTypeMap, PropertyType as PropertyTypeMap, isAssertionMethod, isSubstituteMethod, isSubstitutionMethod, textModifier } from './Utilities'
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 { ObjectSubstitute } from './Transformations'

const instance = Symbol('Substitute:Instance')
const clearTypeToFilterMap: Record<ClearType, FilterFunction<SubstituteNode>> = {
Expand All @@ -16,7 +17,7 @@ const clearTypeToFilterMap: Record<ClearType, FilterFunction<SubstituteNode>> =
type SpecialProperty = typeof instance | typeof inspect.custom | 'then'
type RootContext = { substituteMethodsEnabled: boolean }

export class SubstituteNode extends SubstituteNodeBase {
export class SubstituteNode extends SubstituteNodeBase implements ObjectSubstitute<unknown> {
private _proxy: SubstituteNode
private _rootContext: RootContext

Expand All @@ -25,7 +26,6 @@ export class SubstituteNode extends SubstituteNodeBase {
private _recordedArguments: RecordedArguments = RecordedArguments.none()

private _context: SubstituteContext = 'none'
private _disabledSubstituteMethods: boolean = false
private _retrySubstitutionExecutionAttempt: boolean = false

private constructor(key: PropertyKey, parent?: SubstituteNode) {
Expand All @@ -37,26 +37,28 @@ export class SubstituteNode extends SubstituteNodeBase {
{
get: function (target, property) {
if (target.isSpecialProperty(property)) return target.evaluateSpecialProperty(property)
if (target._retrySubstitutionExecutionAttempt) {
const result = target.attemptSubstitutionExecution()
target._retrySubstitutionExecutionAttempt = false
return result[property]
}
if (target._retrySubstitutionExecutionAttempt) return target.reattemptSubstitutionExecution()[property]
const newNode = SubstituteNode.createChild(property, target)
if (target.isRoot() && !target.rootContext.substituteMethodsEnabled) newNode.disableSubstituteMethods()
if (target.isAssertion) newNode.executeAssertion()
return newNode.read()
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.write(value)
newNode.handleSetter(value)
if (target.isAssertion) newNode.executeAssertion()
return true
},
apply: function (target, _thisArg, rawArguments) {
target.handleMethod(rawArguments)
if (target.hasContext) target.handleSpecialContext()
return (target.parent?.isAssertion ?? false) ? target.executeAssertion() : target.read()
if (target.hasDepthOfAtLeast(2)) {
if (isSubstitutionMethod(target.property)) return target.parent.assignContext(target.property)
if (target.parent.isAssertion) return target.executeAssertion()
}
return target.isAssertion ? target.proxy : target.attemptSubstitutionExecution()
}
}
)
Expand Down Expand Up @@ -112,34 +114,39 @@ export class SubstituteNode extends SubstituteNodeBase {
return this._recordedArguments
}

public get disabledSubstituteMethods(): boolean {
return this._disabledSubstituteMethods
public received(amount?: number): SubstituteNode {
this.handleMethod([amount])
return this.proxy
}

public assignContext(context: SubstituteContext): void {
this._context = context
public didNotReceive(): SubstituteNode {
this.handleMethod([0])
return this.proxy
}

public disableSubstituteMethods() {
this._disabledSubstituteMethods = true
public mimick() {
throw new Error('Mimick is not implemented yet')
}

public read(): SubstituteNode | void | never {
if ((this.parent?.isSubstitution ?? false) || this.context === 'clearSubstitute') return
if (this.isAssertion) return this.proxy
return this.attemptSubstitutionExecution()
public clearSubstitute(clearType: ClearType = ClearTypeMap.All): void {
this.handleMethod([clearType])
const filter = clearTypeToFilterMap[clearType]
this.recorder.clearRecords(filter)
}

public write(value: any) {
this._accessorType = 'set'
this._recordedArguments = RecordedArguments.from([value])
public [inspect.custom](...args: [_: number, options: InspectOptions]): string {
return types.isProxy(this) ? this[inspect.custom](...args) : this.printableForm(...args)
}

public clear() {
if (!this.recordedArguments.hasArguments()) throw new TypeError('No args')
const clearType: ClearType = this.recordedArguments.value[0] ?? ClearTypeMap.All
const filter = clearTypeToFilterMap[clearType]
this.recorder.clearRecords(filter)
private assignContext(context: SubstituteContext): void {
if (!isSubstituteMethod(context)) throw new Error(`Cannot assign context for property ${context.toString()}`)
this._context = context
}

private reattemptSubstitutionExecution(): SubstituteNode | any {
const result = this.attemptSubstitutionExecution()
this._retrySubstitutionExecutionAttempt = false
return result
}

private attemptSubstitutionExecution(): SubstituteNode | any {
Expand All @@ -149,7 +156,7 @@ export class SubstituteNode extends SubstituteNodeBase {
: this.proxy
}

public executeSubstitution(contextArguments: RecordedArguments) {
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')

Expand All @@ -175,8 +182,8 @@ export class SubstituteNode extends SubstituteNodeBase {
}
}

public executeAssertion(): void | never {
if (!this.isIntermediateNode()) throw new Error('Not possible')
private executeAssertion(): void | never {
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
Expand Down Expand Up @@ -208,22 +215,14 @@ export class SubstituteNode extends SubstituteNodeBase {
}
}

public handleMethod(rawArguments: any[]): void {
this._propertyType = PropertyTypeMap.Method
this._recordedArguments = RecordedArguments.from(rawArguments)
this.tryToAssignContext()
}

private tryToAssignContext() {
if (!isSubstituteMethod(this.property)) return
if (this.isIntermediateNode() && isSubstitutionMethod(this.property)) return this.parent.assignContext(this.property)
if (this.disabledSubstituteMethods) return
this.assignContext(this.property)
private handleSetter(value: any) {
this._accessorType = 'set'
this._recordedArguments = RecordedArguments.from([value])
}

private handleSpecialContext(): void {
if (this.context === 'clearSubstitute') return this.clear()
if (this.context === 'didNotReceive') this._recordedArguments = RecordedArguments.from([0])
private handleMethod(rawArguments: any[]): void {
this._propertyType = PropertyTypeMap.Method
this._recordedArguments = RecordedArguments.from(rawArguments)
}

private getMostSuitableSubstitution(): SubstituteNode | void {
Expand All @@ -233,7 +232,7 @@ export class SubstituteNode extends SubstituteNodeBase {
)
const potentialSuitableSubstitutionsSet = this.propertyType === PropertyTypeMap.Property && !this._retrySubstitutionExecutionAttempt ?
commonSuitableSubstitutionsSet.filter(node => node.propertyType === PropertyTypeMap.Method) :
[]
<this[]>[]

const strictSuitableSubstitutions = [...strictSuitableSubstitutionsSet]
const potentialSuitableSubstitutions = [...potentialSuitableSubstitutionsSet]
Expand All @@ -260,10 +259,6 @@ export class SubstituteNode extends SubstituteNodeBase {
}
}

public [inspect.custom](...args: [_: number, options: InspectOptions]): string {
return types.isProxy(this) ? this[inspect.custom](...args) : this.printableForm(...args)
}

private printableForm(_: number, options: InspectOptions): string {
return this.isRoot() ? this.printRootNode(options) : this.printNode(options)
}
Expand All @@ -277,14 +272,12 @@ export class SubstituteNode extends SubstituteNodeBase {
private printNode(options: InspectOptions): string {
const hasContext = this.hasContext
const args = inspect(this.recordedArguments, options)
const label = this.isSubstitution
? '=> '
: this.isAssertion
? `${this.child?.property.toString()}`
: ''
const s = hasContext
? ` ${label}${inspect(this.child?.recordedArguments, options)}`
: ''
const label = this.isSubstitution ?
'=> ' :
this.isAssertion ?
`${this.child?.property.toString()}` :
''
const s = hasContext ? `${label}${inspect(this.child?.recordedArguments, options)}` : ''

const printableNode = `${this.propertyType}<${this.property.toString()}>: ${args}${s}`
return hasContext ? textModifier.italic(printableNode) : printableNode
Expand Down
7 changes: 2 additions & 5 deletions src/SubstituteNodeBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export abstract class SubstituteNodeBase extends Function {
return typeof this._parent === 'undefined'
}

protected isIntermediateNode(): this is this & { parent: ThisType<SubstituteNodeBase> } {
return !this.isRoot()
protected hasDepthOfAtLeast<TDepth extends number>(depth: TDepth): this is (TDepth extends 0 ? this & { parent: undefined } : this & { parent: ThisType<SubstituteNodeBase> }) {
return this.depth >= depth
}

protected getAllSiblings(): RecordsSet<this> {
Expand All @@ -74,7 +74,4 @@ export abstract class SubstituteNodeBase extends Function {
protected hasChild(): this is this & { child: ThisType<SubstituteNodeBase> } {
return this.child instanceof SubstituteNodeBase
}

public abstract read(): void
public abstract write(value: any): void
}

0 comments on commit a49c8bd

Please sign in to comment.