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