Skip to content

Commit 42a137d

Browse files
Add clear substitute (#233)
resolves #46 * implement clearSubstitute * add clearSubstitute spec
1 parent 636f3c5 commit 42a137d

File tree

5 files changed

+99
-6
lines changed

5 files changed

+99
-6
lines changed

spec/ClearSubstitute.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import test from 'ava'
2+
3+
import { Substitute, SubstituteOf } from '../src'
4+
import { SubstituteBase } from '../src/SubstituteBase'
5+
import { SubstituteNode } from '../src/SubstituteNode'
6+
7+
interface Calculator {
8+
add(a: number, b: number): number
9+
subtract(a: number, b: number): number
10+
divide(a: number, b: number): number
11+
isEnabled: boolean
12+
}
13+
14+
type InstanceReturningSubstitute<T> = SubstituteOf<T> & {
15+
[SubstituteBase.instance]: Substitute
16+
}
17+
18+
test('clears everything on a substitute', t => {
19+
const calculator = Substitute.for<Calculator>() as InstanceReturningSubstitute<Calculator>
20+
calculator.add(1, 1)
21+
calculator.received().add(1, 1)
22+
calculator.clearSubstitute()
23+
24+
t.is(calculator[Substitute.instance].recorder.records.size, 0)
25+
t.is(calculator[Substitute.instance].recorder.indexedRecords.size, 0)
26+
27+
t.throws(() => calculator.received().add(1, 1))
28+
29+
// explicitly using 'all'
30+
calculator.add(1, 1)
31+
calculator.received().add(1, 1)
32+
calculator.clearSubstitute('all')
33+
34+
t.is(calculator[Substitute.instance].recorder.records.size, 0)
35+
t.is(calculator[Substitute.instance].recorder.indexedRecords.size, 0)
36+
37+
t.throws(() => calculator.received().add(1, 1))
38+
})
39+
40+
test('clears received calls on a substitute', t => {
41+
const calculator = Substitute.for<Calculator>() as InstanceReturningSubstitute<Calculator>
42+
calculator.add(1, 1)
43+
calculator.add(1, 1).returns(2)
44+
calculator.clearSubstitute('receivedCalls')
45+
46+
t.is(calculator[Substitute.instance].recorder.records.size, 2)
47+
t.is(calculator[Substitute.instance].recorder.indexedRecords.size, 2)
48+
49+
t.throws(() => calculator.received().add(1, 1))
50+
t.is(calculator.add(1, 1), 2)
51+
})
52+
53+
test('clears return values on a substitute', t => {
54+
const calculator = Substitute.for<Calculator>() as InstanceReturningSubstitute<Calculator>
55+
calculator.add(1, 1)
56+
calculator.add(1, 1).returns(2)
57+
calculator.clearSubstitute('substituteValues')
58+
59+
t.is(calculator[Substitute.instance].recorder.records.size, 2)
60+
t.is(calculator[Substitute.instance].recorder.indexedRecords.size, 2)
61+
62+
t.notThrows(() => calculator.received().add(1, 1))
63+
// @ts-expect-error
64+
t.true(calculator.add(1, 1)[SubstituteBase.instance] instanceof SubstituteNode)
65+
})

src/Recorder.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ export class Recorder {
3535
return siblingNodes.filter(siblingNode => siblingNode !== node)
3636
}
3737

38+
public clearRecords(filterFunction: (node: SubstituteNodeBase) => boolean) {
39+
const recordsToRemove = this.records.filter(filterFunction)
40+
for (const record of recordsToRemove) {
41+
const indexedRecord = this.indexedRecords.get(record.key)
42+
indexedRecord.delete(record)
43+
if (indexedRecord.size === 0) this.indexedRecords.delete(record.key)
44+
this.records.delete(record)
45+
}
46+
}
47+
3848
public [inspect.custom](_: number, options: InspectOptions): string {
3949
const entries = [...this.indexedRecords.entries()]
4050
return entries.map(

src/SubstituteNode.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { inspect, InspectOptions } from 'util'
22

3-
import { PropertyType, isSubstitutionMethod, isAssertionMethod, AssertionMethod, SubstitutionMethod, textModifier, isSubstituteMethod } from './Utilities'
3+
import { PropertyType, isSubstitutionMethod, isAssertionMethod, AssertionMethod, SubstitutionMethod, textModifier, ConfigurationMethod, isSubstituteMethod } from './Utilities'
44
import { SubstituteException } from './SubstituteException'
55
import { RecordedArguments } from './RecordedArguments'
66
import { SubstituteNodeBase } from './SubstituteNodeBase'
77
import { SubstituteBase } from './SubstituteBase'
88
import { createSubstituteProxy } from './SubstituteProxy'
9+
import { ClearType } from './Transformations'
910

10-
type SubstituteContext = SubstitutionMethod | AssertionMethod | 'none'
11+
type SubstituteContext = SubstitutionMethod | AssertionMethod | ConfigurationMethod | 'none'
12+
const clearTypeToFilterMap: Record<ClearType, (node: SubstituteNode) => boolean> = {
13+
all: () => true,
14+
receivedCalls: node => !node.hasContext,
15+
substituteValues: node => node.isSubstitution
16+
}
1117

1218
export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
1319
private _proxy: SubstituteNode
@@ -31,6 +37,7 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
3137
},
3238
apply: (node, _, rawArguments) => {
3339
node.handleMethod(rawArguments)
40+
if (node.context === 'clearSubstitute') return node.clear()
3441
return node.parent?.isAssertion ?? false ? node.executeAssertion() : node.read()
3542
}
3643
}
@@ -100,6 +107,12 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
100107
this._recordedArguments = RecordedArguments.from([value])
101108
}
102109

110+
public clear() {
111+
const clearType: ClearType = this.recordedArguments.value[0] ?? 'all'
112+
const filter = clearTypeToFilterMap[clearType] as (node: SubstituteNodeBase) => boolean
113+
this.root.recorder.clearRecords(filter)
114+
}
115+
103116
public executeSubstitution(contextArguments: RecordedArguments) {
104117
const substitutionMethod = this.context as SubstitutionMethod
105118
const substitutionValue = this.child.recordedArguments.value.length > 1

src/Transformations.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AllArguments } from "./Arguments";
1+
import { AllArguments } from './Arguments';
22

33
type FunctionSubstituteWithOverloads<TFunc, Terminating = false> =
44
TFunc extends {
@@ -70,6 +70,7 @@ export type ObjectSubstitute<T extends Object, K extends Object = T> = ObjectSub
7070
received(amount?: number): TerminatingObject<K>;
7171
didNotReceive(): TerminatingObject<K>;
7272
mimick(instance: T): void;
73+
clearSubstitute(clearType?: ClearType): void;
7374
}
7475

7576
type TerminatingFunction<TArguments extends any[]> = ((...args: TArguments) => void) & ((arg: AllArguments<TArguments>) => void)
@@ -92,5 +93,6 @@ type ObjectSubstituteTransformation<T extends Object> = {
9293

9394
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
9495

95-
export type OmitProxyMethods<T extends any> = Omit<T, 'mimick' | 'received' | 'didNotReceive'>;
96+
export type ClearType = 'all' | 'receivedCalls' | 'substituteValues';
97+
export type OmitProxyMethods<T extends any> = Omit<T, 'mimick' | 'received' | 'didNotReceive' | 'clearSubstitute'>;
9698
export type DisabledSubstituteObject<T> = T extends ObjectSubstitute<OmitProxyMethods<infer K>, infer K> ? K : never;

src/Utilities.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ export type AssertionMethod = 'received' | 'didNotReceive'
1010
export const isAssertionMethod = (property: PropertyKey): property is AssertionMethod =>
1111
property === 'received' || property === 'didNotReceive'
1212

13+
export type ConfigurationMethod = 'clearSubstitute'
14+
export const isConfigurationMethod = (property: PropertyKey): property is ConfigurationMethod => property === 'clearSubstitute'
15+
1316
export type SubstitutionMethod = 'mimicks' | 'throws' | 'returns' | 'resolves' | 'rejects'
1417
export const isSubstitutionMethod = (property: PropertyKey): property is SubstitutionMethod =>
1518
property === 'mimicks' || property === 'returns' || property === 'throws' || property === 'resolves' || property === 'rejects'
1619

17-
export const isSubstituteMethod = (property: PropertyKey): property is SubstitutionMethod | AssertionMethod =>
18-
isSubstitutionMethod(property) || isAssertionMethod(property)
20+
export const isSubstituteMethod = (property: PropertyKey): property is SubstitutionMethod | ConfigurationMethod | AssertionMethod =>
21+
isSubstitutionMethod(property) || isConfigurationMethod(property) || isAssertionMethod(property)
1922

2023
export const stringifyArguments = (args: RecordedArguments) => textModifier.faint(
2124
args.hasNoArguments

0 commit comments

Comments
 (0)