UNPKG

5.68 kBJavaScriptView Raw
1'use strict';
2
3function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
4
5var tap = _interopDefault(require('zora-tap-reporter'));
6var deepEqual = _interopDefault(require('deep-equal'));
7
8const getAssertionLocation = () => {
9 const err = new Error();
10 const stack = (err.stack || '').split('\n');
11 return (stack[3] || '').trim().replace(/^at/i, '');
12};
13const assertMethodHook = fn => function (...args) {
14 const assertResult = fn(...args);
15
16 if (assertResult.pass === false) {
17 assertResult.at = getAssertionLocation();
18 }
19
20 this.collect(assertResult);
21 return assertResult;
22};
23
24const Assertion = {
25 ok: assertMethodHook((val, message = 'should be truthy') => ({
26 pass: Boolean(val),
27 actual: val,
28 expected: true,
29 message,
30 operator: 'ok'
31 })),
32 deepEqual: assertMethodHook((actual, expected, message = 'should be equivalent') => ({
33 pass: deepEqual(actual, expected),
34 actual,
35 expected,
36 message,
37 operator: 'deepEqual'
38 })),
39 equal: assertMethodHook((actual, expected, message = 'should be equal') => ({
40 pass: actual === expected,
41 actual,
42 expected,
43 message,
44 operator: 'equal'
45 })),
46 notOk: assertMethodHook((val, message = 'should not be truthy') => ({
47 pass: !val,
48 expected: false,
49 actual: val,
50 message,
51 operator: 'notOk'
52 })),
53 notDeepEqual: assertMethodHook((actual, expected, message = 'should not be equivalent') => ({
54 pass: !deepEqual(actual, expected),
55 actual,
56 expected,
57 message,
58 operator: 'notDeepEqual'
59 })),
60 notEqual: assertMethodHook((actual, expected, message = 'should not be equal') => ({
61 pass: actual !== expected,
62 actual,
63 expected,
64 message,
65 operator: 'notEqual'
66 })),
67 throws: assertMethodHook((func, expected, message) => {
68 let caught;
69 let pass;
70 let actual;
71 if (typeof expected === 'string') {
72 [expected, message] = [message, expected];
73 }
74 try {
75 func();
76 } catch (err) {
77 caught = {error: err};
78 }
79 pass = caught !== undefined;
80 actual = caught && caught.error;
81 if (expected instanceof RegExp) {
82 pass = expected.test(actual) || expected.test(actual && actual.message);
83 expected = String(expected);
84 } else if (typeof expected === 'function' && caught) {
85 pass = actual instanceof expected;
86 actual = actual.constructor;
87 }
88 return {
89 pass,
90 expected,
91 actual,
92 operator: 'throws',
93 message: message || 'should throw'
94 };
95 }),
96 doesNotThrow: assertMethodHook((func, expected, message) => {
97 let caught;
98 if (typeof expected === 'string') {
99 [expected, message] = [message, expected];
100 }
101 try {
102 func();
103 } catch (err) {
104 caught = {error: err};
105 }
106 return {
107 pass: caught === undefined,
108 expected: 'no thrown error',
109 actual: caught && caught.error,
110 operator: 'doesNotThrow',
111 message: message || 'should not throw'
112 };
113 }),
114 fail: assertMethodHook((message = 'fail called') => ({
115 pass: false,
116 actual: 'fail called',
117 expected: 'fail not called',
118 message,
119 operator: 'fail'
120 }))
121};
122
123var assert = collect => Object.create(Assertion, {collect: {value: collect}});
124
125const noop = () => {};
126
127const skip = description => test('SKIPPED - ' + description, noop);
128
129const Test = {
130 async run() {
131 const collect = assertion => this.items.push(assertion);
132 const start = Date.now();
133 await Promise.resolve(this.spec(assert(collect)));
134 const executionTime = Date.now() - start;
135 return Object.assign(this, {
136 executionTime
137 });
138 },
139 skip() {
140 return skip(this.description);
141 }
142};
143
144function test(description, spec, {only = false} = {}) {
145 return Object.create(Test, {
146 items: {value: []},
147 only: {value: only},
148 spec: {value: spec},
149 description: {value: description}
150 });
151}
152
153// Force to resolve on next tick so consumer can do something with previous iteration result
154const onNextTick = val => new Promise(resolve => setTimeout(() => resolve(val), 0));
155
156const PlanProto = {
157 [Symbol.iterator]() {
158 return this.items[Symbol.iterator]();
159 },
160 test(description, spec, opts) {
161 if (!spec && description.test) {
162 // If it is a plan
163 this.items.push(...description);
164 } else {
165 this.items.push(test(description, spec, opts));
166 }
167 return this;
168 },
169 only(description, spec) {
170 return this.test(description, spec, {only: true});
171 },
172 skip(description, spec) {
173 if (!spec && description.test) {
174 // If it is a plan we skip the whole plan
175 for (const t of description) {
176 this.items.push(t.skip());
177 }
178 } else {
179 this.items.push(skip(description));
180 }
181 return this;
182 }
183};
184
185const runnify = fn => async function (sink = tap()) {
186 const sinkIterator = typeof sink[Symbol.iterator] === 'function' ?
187 sink[Symbol.iterator]() :
188 sink(); // Backward compatibility
189 sinkIterator.next();
190 try {
191 const hasOnly = this.items.some(t => t.only);
192 const tests = hasOnly ? this.items.map(t => t.only ? t : t.skip()) : this.items;
193 await fn(tests, sinkIterator);
194 } catch (err) {
195 sinkIterator.throw(err);
196 } finally {
197 sinkIterator.return();
198 }
199};
200
201function factory({sequence = false} = {sequence: false}) {
202 /* eslint-disable no-await-in-loop */
203 const exec = sequence === true ? async (tests, sinkIterator) => {
204 for (const t of tests) {
205 const result = await onNextTick(t.run());
206 sinkIterator.next(result);
207 }
208 } : async (tests, sinkIterator) => {
209 const runningTests = tests.map(t => t.run());
210 for (const r of runningTests) {
211 const executedTest = await onNextTick(r);
212 sinkIterator.next(executedTest);
213 }
214 };
215 /* eslint-enable no-await-in-loop */
216
217 return Object.assign(Object.create(PlanProto, {
218 items: {value: []}, length: {
219 get() {
220 return this.items.length;
221 }
222 }
223 }), {
224 run: runnify(exec)
225 });
226}
227
228module.exports = factory;