1 | import _ from './wrap/lodash'
|
2 | import proxySafeCloneDeepWith from './wrap/proxy-safe-clone-deep-with'
|
3 | import callsStore from './store/calls'
|
4 | import store from './store'
|
5 | import stringifyArgs from './stringify/arguments'
|
6 | import stubbingsStore from './store/stubbings'
|
7 | import symbols from './symbols'
|
8 |
|
9 | export default function explain (testDouble) {
|
10 | if (_.isFunction(testDouble)) {
|
11 | return explainFunction(testDouble)
|
12 | } else if (_.isObject(testDouble)) {
|
13 | return explainObject(testDouble)
|
14 | } else {
|
15 | return explainNonTestDouble(testDouble)
|
16 | }
|
17 | }
|
18 |
|
19 | function explainObject (obj) {
|
20 | const { explanations, children } = explainChildren(obj)
|
21 |
|
22 | return {
|
23 | name: null,
|
24 | callCount: 0,
|
25 | calls: [],
|
26 | description: describeObject(explanations),
|
27 | children,
|
28 | isTestDouble: explanations.length > 0
|
29 | }
|
30 | }
|
31 |
|
32 | function explainChildren (thing) {
|
33 | const explanations = []
|
34 | const children = proxySafeCloneDeepWith(thing, (val, key, obj, stack) => {
|
35 | if (_.isFunction(val) && stack) {
|
36 | return _.tap(explainFunction(val), (explanation) => {
|
37 | if (explanation.isTestDouble) explanations.push(explanation)
|
38 | })
|
39 | }
|
40 | })
|
41 | return { explanations, children }
|
42 | }
|
43 |
|
44 | function describeObject (explanations) {
|
45 | const count = explanations.length
|
46 | if (count === 0) return 'This object contains no test doubles'
|
47 | return `This object contains ${count} test double function${count > 1 ? 's' : ''}: [${_.map(explanations, e =>
|
48 | `"${e.name}"`
|
49 | ).join(', ')}]`
|
50 | }
|
51 |
|
52 | function explainFunction (testDouble) {
|
53 | if (store.for(testDouble, false) == null) { return explainNonTestDouble(testDouble) }
|
54 | const calls = callsStore.for(testDouble)
|
55 | const stubs = stubbingsStore.for(testDouble)
|
56 | const { children } = explainChildren(testDouble)
|
57 |
|
58 | return {
|
59 | name: store.for(testDouble).name,
|
60 | callCount: calls.length,
|
61 | calls,
|
62 | description:
|
63 | testdoubleDescription(testDouble, stubs, calls) +
|
64 | stubbingDescription(stubs) +
|
65 | callDescription(calls),
|
66 | children,
|
67 | isTestDouble: true
|
68 | }
|
69 | }
|
70 |
|
71 | function explainNonTestDouble (thing) {
|
72 | return ({
|
73 | name: undefined,
|
74 | callCount: 0,
|
75 | calls: [],
|
76 | description: `This is not a test double${_.isFunction(thing) ? ' function' : ''}.`,
|
77 | isTestDouble: false
|
78 | })
|
79 | }
|
80 |
|
81 | function testdoubleDescription (testDouble, stubs, calls) {
|
82 | return `This test double ${stringifyName(testDouble)}has ${stubs.length} stubbings and ${calls.length} invocations.`
|
83 | }
|
84 |
|
85 | function stubbingDescription (stubs) {
|
86 | return stubs.length > 0
|
87 | ? _.reduce(stubs, (desc, stub) =>
|
88 | desc + `\n - when called with \`(${stringifyArgs(stub.args)})\`, then ${planFor(stub)} ${argsFor(stub)}.`
|
89 | , '\n\nStubbings:')
|
90 | : ''
|
91 | }
|
92 |
|
93 | function planFor (stub) {
|
94 | switch (stub.config.plan) {
|
95 | case 'thenCallback': return 'callback'
|
96 | case 'thenResolve': return 'resolve'
|
97 | case 'thenReject': return 'reject'
|
98 | default: return 'return'
|
99 | }
|
100 | }
|
101 |
|
102 | function argsFor (stub) {
|
103 | switch (stub.config.plan) {
|
104 | case 'thenCallback': return `\`(${stringifyArgs(stub.stubbedValues, ', ')})\``
|
105 | default: return stringifyArgs(stub.stubbedValues, ', then ', '`')
|
106 | }
|
107 | }
|
108 |
|
109 | function callDescription (calls) {
|
110 | return calls.length > 0
|
111 | ? _.reduce(calls, (desc, call) => {
|
112 | let argDescription
|
113 | if (call.cloneArgs !== symbols.uncloneable) {
|
114 | argDescription = `\`(${stringifyArgs(call.cloneArgs)})\`.`
|
115 | } else {
|
116 | argDescription = `\`(${stringifyArgs(call.args)})\` [Cloning argument values failed; displaying current references]`
|
117 | }
|
118 | return desc + `\n - called with ${argDescription}`
|
119 | }, '\n\nInvocations:')
|
120 | : ''
|
121 | }
|
122 |
|
123 | function stringifyName (testDouble) {
|
124 | const name = store.for(testDouble).name
|
125 | return name ? `\`${name}\` ` : ''
|
126 | }
|