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