|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 "use strict"; |
|
5 |
|
6 const { setTimeout } = require('sdk/timers'); |
|
7 const utils = require('sdk/lang/functional'); |
|
8 const { invoke, defer, partial, compose, memoize, once, is, isnt, |
|
9 delay, wrap, curry, chainable, field, query, isInstance, debounce, throttle |
|
10 } = utils; |
|
11 const { LoaderWithHookedConsole } = require('sdk/test/loader'); |
|
12 |
|
13 exports['test forwardApply'] = function(assert) { |
|
14 function sum(b, c) { return this.a + b + c; } |
|
15 assert.equal(invoke(sum, [2, 3], { a: 1 }), 6, |
|
16 'passed arguments and pseoude-variable are used'); |
|
17 |
|
18 assert.equal(invoke(sum.bind({ a: 2 }), [2, 3], { a: 1 }), 7, |
|
19 'bounded `this` pseoudo variable is used'); |
|
20 }; |
|
21 |
|
22 exports['test deferred function'] = function(assert, done) { |
|
23 let nextTurn = false; |
|
24 function sum(b, c) { |
|
25 assert.ok(nextTurn, 'enqueued is called in next turn of event loop'); |
|
26 assert.equal(this.a + b + c, 6, |
|
27 'passed arguments an pseoude-variable are used'); |
|
28 done(); |
|
29 } |
|
30 |
|
31 let fixture = { a: 1, method: defer(sum) }; |
|
32 fixture.method(2, 3); |
|
33 nextTurn = true; |
|
34 }; |
|
35 |
|
36 exports['test partial function'] = function(assert) { |
|
37 function sum(b, c) { return this.a + b + c; } |
|
38 |
|
39 let foo = { a : 5 }; |
|
40 |
|
41 foo.sum7 = partial(sum, 7); |
|
42 foo.sum8and4 = partial(sum, 8, 4); |
|
43 |
|
44 assert.equal(foo.sum7(2), 14, 'partial one arguments works'); |
|
45 |
|
46 assert.equal(foo.sum8and4(), 17, 'partial both arguments works'); |
|
47 }; |
|
48 |
|
49 exports["test curry defined numeber of arguments"] = function(assert) { |
|
50 var sum = curry(function(a, b, c) { |
|
51 return a + b + c; |
|
52 }); |
|
53 |
|
54 assert.equal(sum(2, 2, 1), 5, "sum(2, 2, 1) => 5"); |
|
55 assert.equal(sum(2, 4)(1), 7, "sum(2, 4)(1) => 7"); |
|
56 assert.equal(sum(2)(4, 2), 8, "sum(2)(4, 2) => 8"); |
|
57 assert.equal(sum(2)(4)(3), 9, "sum(2)(4)(3) => 9"); |
|
58 }; |
|
59 |
|
60 exports['test compose'] = function(assert) { |
|
61 let greet = function(name) { return 'hi: ' + name; }; |
|
62 let exclaim = function(sentence) { return sentence + '!'; }; |
|
63 |
|
64 assert.equal(compose(exclaim, greet)('moe'), 'hi: moe!', |
|
65 'can compose a function that takes another'); |
|
66 |
|
67 assert.equal(compose(greet, exclaim)('moe'), 'hi: moe!', |
|
68 'in this case, the functions are also commutative'); |
|
69 |
|
70 let target = { |
|
71 name: 'Joe', |
|
72 greet: compose(function exclaim(sentence) { |
|
73 return sentence + '!'; |
|
74 }, function(title) { |
|
75 return 'hi : ' + title + ' ' + this.name; |
|
76 }) |
|
77 }; |
|
78 |
|
79 assert.equal(target.greet('Mr'), 'hi : Mr Joe!', |
|
80 'this can be passed in'); |
|
81 assert.equal(target.greet.call({ name: 'Alex' }, 'Dr'), 'hi : Dr Alex!', |
|
82 'this can be applied'); |
|
83 |
|
84 let single = compose(function(value) { |
|
85 return value + ':suffix'; |
|
86 }); |
|
87 |
|
88 assert.equal(single('text'), 'text:suffix', 'works with single function'); |
|
89 |
|
90 let identity = compose(); |
|
91 assert.equal(identity('bla'), 'bla', 'works with zero functions'); |
|
92 }; |
|
93 |
|
94 exports['test wrap'] = function(assert) { |
|
95 let greet = function(name) { return 'hi: ' + name; }; |
|
96 let backwards = wrap(greet, function(f, name) { |
|
97 return f(name) + ' ' + name.split('').reverse().join(''); |
|
98 }); |
|
99 |
|
100 assert.equal(backwards('moe'), 'hi: moe eom', |
|
101 'wrapped the saluation function'); |
|
102 |
|
103 let inner = function () { return 'Hello '; }; |
|
104 let target = { |
|
105 name: 'Matteo', |
|
106 hi: wrap(inner, function(f) { return f() + this.name; }) |
|
107 }; |
|
108 |
|
109 assert.equal(target.hi(), 'Hello Matteo', 'works with this'); |
|
110 |
|
111 function noop() { } |
|
112 let wrapped = wrap(noop, function(f) { |
|
113 return Array.slice(arguments); |
|
114 }); |
|
115 |
|
116 let actual = wrapped([ 'whats', 'your' ], 'vector', 'victor'); |
|
117 assert.deepEqual(actual, [ noop, ['whats', 'your'], 'vector', 'victor' ], |
|
118 'works with fancy stuff'); |
|
119 }; |
|
120 |
|
121 exports['test memoize'] = function(assert) { |
|
122 const fib = n => n < 2 ? n : fib(n - 1) + fib(n - 2); |
|
123 let fibnitro = memoize(fib); |
|
124 |
|
125 assert.equal(fib(10), 55, |
|
126 'a memoized version of fibonacci produces identical results'); |
|
127 assert.equal(fibnitro(10), 55, |
|
128 'a memoized version of fibonacci produces identical results'); |
|
129 |
|
130 function o(key, value) { return value; } |
|
131 let oo = memoize(o), v1 = {}, v2 = {}; |
|
132 |
|
133 |
|
134 assert.equal(oo(1, v1), v1, 'returns value back'); |
|
135 assert.equal(oo(1, v2), v1, 'memoized by a first argument'); |
|
136 assert.equal(oo(2, v2), v2, 'returns back value if not memoized'); |
|
137 assert.equal(oo(2), v2, 'memoized new value'); |
|
138 assert.notEqual(oo(1), oo(2), 'values do not override'); |
|
139 assert.equal(o(3, v2), oo(2, 3), 'returns same value as un-memoized'); |
|
140 |
|
141 let get = memoize(function(attribute) { return this[attribute]; }); |
|
142 let target = { name: 'Bob', get: get }; |
|
143 |
|
144 assert.equal(target.get('name'), 'Bob', 'has correct `this`'); |
|
145 assert.equal(target.get.call({ name: 'Jack' }, 'name'), 'Bob', |
|
146 'name is memoized'); |
|
147 assert.equal(get('name'), 'Bob', 'once memoized can be called without this'); |
|
148 }; |
|
149 |
|
150 exports['test delay'] = function(assert, done) { |
|
151 let delayed = false; |
|
152 delay(function() { |
|
153 assert.ok(delayed, 'delayed the function'); |
|
154 done(); |
|
155 }, 1); |
|
156 delayed = true; |
|
157 }; |
|
158 |
|
159 exports['test delay with this'] = function(assert, done) { |
|
160 let context = {}; |
|
161 delay.call(context, function(name) { |
|
162 assert.equal(this, context, 'this was passed in'); |
|
163 assert.equal(name, 'Tom', 'argument was passed in'); |
|
164 done(); |
|
165 }, 10, 'Tom'); |
|
166 }; |
|
167 |
|
168 exports['test once'] = function(assert) { |
|
169 let n = 0; |
|
170 let increment = once(function() { n ++; }); |
|
171 |
|
172 increment(); |
|
173 increment(); |
|
174 |
|
175 assert.equal(n, 1, 'only incremented once'); |
|
176 |
|
177 let target = { |
|
178 state: 0, |
|
179 update: once(function() { |
|
180 return this.state ++; |
|
181 }) |
|
182 }; |
|
183 |
|
184 target.update(); |
|
185 target.update(); |
|
186 |
|
187 assert.equal(target.state, 1, 'this was passed in and called only once'); |
|
188 }; |
|
189 |
|
190 exports['test once with argument'] = function(assert) { |
|
191 let n = 0; |
|
192 let increment = once(a => n++); |
|
193 |
|
194 increment(); |
|
195 increment('foo'); |
|
196 |
|
197 assert.equal(n, 1, 'only incremented once'); |
|
198 |
|
199 increment(); |
|
200 increment('foo'); |
|
201 |
|
202 assert.equal(n, 1, 'only incremented once'); |
|
203 }; |
|
204 |
|
205 exports['test complement'] = assert => { |
|
206 let { complement } = require("sdk/lang/functional"); |
|
207 |
|
208 let isOdd = x => Boolean(x % 2); |
|
209 |
|
210 assert.equal(isOdd(1), true); |
|
211 assert.equal(isOdd(2), false); |
|
212 |
|
213 let isEven = complement(isOdd); |
|
214 |
|
215 assert.equal(isEven(1), false); |
|
216 assert.equal(isEven(2), true); |
|
217 |
|
218 let foo = {}; |
|
219 let isFoo = function() { return this === foo; }; |
|
220 let insntFoo = complement(isFoo); |
|
221 |
|
222 assert.equal(insntFoo.call(foo), false); |
|
223 assert.equal(insntFoo.call({}), true); |
|
224 }; |
|
225 |
|
226 exports['test constant'] = assert => { |
|
227 let { constant } = require("sdk/lang/functional"); |
|
228 |
|
229 let one = constant(1); |
|
230 |
|
231 assert.equal(one(1), 1); |
|
232 assert.equal(one(2), 1); |
|
233 }; |
|
234 |
|
235 exports['test apply'] = assert => { |
|
236 let { apply } = require("sdk/lang/functional"); |
|
237 |
|
238 let dashify = (...args) => args.join("-"); |
|
239 |
|
240 assert.equal(apply(dashify, 1, [2, 3]), "1-2-3"); |
|
241 assert.equal(apply(dashify, "a"), "a"); |
|
242 assert.equal(apply(dashify, ["a", "b"]), "a-b"); |
|
243 assert.equal(apply(dashify, ["a", "b"], "c"), "a,b-c"); |
|
244 assert.equal(apply(dashify, [1, 2], [3, 4]), "1,2-3-4"); |
|
245 }; |
|
246 |
|
247 exports['test flip'] = assert => { |
|
248 let { flip } = require("sdk/lang/functional"); |
|
249 |
|
250 let append = (left, right) => left + " " + right; |
|
251 let prepend = flip(append); |
|
252 |
|
253 assert.equal(append("hello", "world"), "hello world"); |
|
254 assert.equal(prepend("hello", "world"), "world hello"); |
|
255 |
|
256 let wrap = function(left, right) { |
|
257 return left + " " + this + " " + right; |
|
258 }; |
|
259 let invertWrap = flip(wrap); |
|
260 |
|
261 assert.equal(wrap.call("@", "hello", "world"), "hello @ world"); |
|
262 assert.equal(invertWrap.call("@", "hello", "world"), "world @ hello"); |
|
263 |
|
264 let reverse = flip((...args) => args); |
|
265 |
|
266 assert.deepEqual(reverse(1, 2, 3, 4), [4, 3, 2, 1]); |
|
267 assert.deepEqual(reverse(1), [1]); |
|
268 assert.deepEqual(reverse(), []); |
|
269 |
|
270 // currying still works |
|
271 let prependr = curry(prepend); |
|
272 |
|
273 assert.equal(prependr("hello", "world"), "world hello"); |
|
274 assert.equal(prependr("hello")("world"), "world hello"); |
|
275 }; |
|
276 |
|
277 exports["test when"] = assert => { |
|
278 let { when } = require("sdk/lang/functional"); |
|
279 |
|
280 let areNums = (...xs) => xs.every(x => typeof(x) === "number"); |
|
281 |
|
282 let sum = when(areNums, (...xs) => xs.reduce((y, x) => x + y, 0)); |
|
283 |
|
284 assert.equal(sum(1, 2, 3), 6); |
|
285 assert.equal(sum(1, 2, "3"), undefined); |
|
286 |
|
287 let multiply = when(areNums, |
|
288 (...xs) => xs.reduce((y, x) => x * y, 1), |
|
289 (...xs) => xs); |
|
290 |
|
291 assert.equal(multiply(2), 2); |
|
292 assert.equal(multiply(2, 3), 6); |
|
293 assert.deepEqual(multiply(2, "4"), [2, "4"]); |
|
294 |
|
295 function Point(x, y) { |
|
296 this.x = x; |
|
297 this.y = y; |
|
298 } |
|
299 |
|
300 let isPoint = x => x instanceof Point; |
|
301 |
|
302 let inc = when(isPoint, ({x, y}) => new Point(x + 1, y + 1)); |
|
303 |
|
304 assert.equal(inc({}), undefined); |
|
305 assert.deepEqual(inc(new Point(0, 0)), { x: 1, y: 1 }); |
|
306 |
|
307 let axis = when(isPoint, |
|
308 ({ x, y }) => [x, y], |
|
309 _ => [0, 0]); |
|
310 |
|
311 assert.deepEqual(axis(new Point(1, 4)), [1, 4]); |
|
312 assert.deepEqual(axis({ foo: "bar" }), [0, 0]); |
|
313 }; |
|
314 |
|
315 exports["test chainable"] = function(assert) { |
|
316 let Player = function () { this.volume = 5; }; |
|
317 Player.prototype = { |
|
318 setBand: chainable(function (band) { return (this.band = band); }), |
|
319 incVolume: chainable(function () { return this.volume++; }) |
|
320 }; |
|
321 let player = new Player(); |
|
322 player |
|
323 .setBand('Animals As Leaders') |
|
324 .incVolume().incVolume().incVolume().incVolume().incVolume().incVolume(); |
|
325 |
|
326 assert.equal(player.band, 'Animals As Leaders', 'passes arguments into chained'); |
|
327 assert.equal(player.volume, 11, 'accepts no arguments in chain'); |
|
328 }; |
|
329 |
|
330 exports["test field"] = assert => { |
|
331 let Num = field("constructor", 0); |
|
332 assert.equal(Num.name, Number.name); |
|
333 assert.ok(typeof(Num), "function"); |
|
334 |
|
335 let x = field("x"); |
|
336 |
|
337 [ |
|
338 [field("foo", { foo: 1 }), 1], |
|
339 [field("foo")({ foo: 1 }), 1], |
|
340 [field("bar", {}), undefined], |
|
341 [field("bar")({}), undefined], |
|
342 [field("hey", undefined), undefined], |
|
343 [field("hey")(undefined), undefined], |
|
344 [field("how", null), null], |
|
345 [field("how")(null), null], |
|
346 [x(1), undefined], |
|
347 [x(undefined), undefined], |
|
348 [x(null), null], |
|
349 [x({ x: 1 }), 1], |
|
350 [x({ x: 2 }), 2], |
|
351 ].forEach(([actual, expected]) => assert.equal(actual, expected)); |
|
352 }; |
|
353 |
|
354 exports["test query"] = assert => { |
|
355 let Num = query("constructor", 0); |
|
356 assert.equal(Num.name, Number.name); |
|
357 assert.ok(typeof(Num), "function"); |
|
358 |
|
359 let x = query("x"); |
|
360 let xy = query("x.y"); |
|
361 |
|
362 [ |
|
363 [query("foo", { foo: 1 }), 1], |
|
364 [query("foo")({ foo: 1 }), 1], |
|
365 [query("foo.bar", { foo: { bar: 2 } }), 2], |
|
366 [query("foo.bar")({ foo: { bar: 2 } }), 2], |
|
367 [query("foo.bar", { foo: 1 }), undefined], |
|
368 [query("foo.bar")({ foo: 1 }), undefined], |
|
369 [x(1), undefined], |
|
370 [x(undefined), undefined], |
|
371 [x(null), null], |
|
372 [x({ x: 1 }), 1], |
|
373 [x({ x: 2 }), 2], |
|
374 [xy(1), undefined], |
|
375 [xy(undefined), undefined], |
|
376 [xy(null), null], |
|
377 [xy({ x: 1 }), undefined], |
|
378 [xy({ x: 2 }), undefined], |
|
379 [xy({ x: { y: 1 } }), 1], |
|
380 [xy({ x: { y: 2 } }), 2] |
|
381 ].forEach(([actual, expected]) => assert.equal(actual, expected)); |
|
382 }; |
|
383 |
|
384 exports["test isInstance"] = assert => { |
|
385 function X() {} |
|
386 function Y() {} |
|
387 let isX = isInstance(X); |
|
388 |
|
389 [ |
|
390 isInstance(X, new X()), |
|
391 isInstance(X)(new X()), |
|
392 !isInstance(X, new Y()), |
|
393 !isInstance(X)(new Y()), |
|
394 isX(new X()), |
|
395 !isX(new Y()) |
|
396 ].forEach(x => assert.ok(x)); |
|
397 }; |
|
398 |
|
399 exports["test is"] = assert => { |
|
400 |
|
401 assert.deepEqual([ 1, 0, 1, 0, 1 ].map(is(1)), |
|
402 [ true, false, true, false, true ], |
|
403 "is can be partially applied"); |
|
404 |
|
405 assert.ok(is(1, 1)); |
|
406 assert.ok(!is({}, {})); |
|
407 assert.ok(is()(1)()(1), "is is curried"); |
|
408 assert.ok(!is()(1)()(2)); |
|
409 }; |
|
410 |
|
411 exports["test isnt"] = assert => { |
|
412 |
|
413 assert.deepEqual([ 1, 0, 1, 0, 1 ].map(isnt(0)), |
|
414 [ true, false, true, false, true ], |
|
415 "is can be partially applied"); |
|
416 |
|
417 assert.ok(!isnt(1, 1)); |
|
418 assert.ok(isnt({}, {})); |
|
419 assert.ok(!isnt()(1)()(1)); |
|
420 assert.ok(isnt()(1)()(2)); |
|
421 }; |
|
422 |
|
423 exports["test debounce"] = (assert, done) => { |
|
424 let counter = 0; |
|
425 let fn = debounce(() => counter++, 100); |
|
426 |
|
427 new Array(10).join(0).split("").forEach(fn); |
|
428 |
|
429 assert.equal(counter, 0, "debounce does not fire immediately"); |
|
430 setTimeout(() => { |
|
431 assert.equal(counter, 1, "function called after wait time"); |
|
432 fn(); |
|
433 setTimeout(() => { |
|
434 assert.equal(counter, 2, "function able to be called again after wait"); |
|
435 done(); |
|
436 }, 150); |
|
437 }, 200); |
|
438 }; |
|
439 |
|
440 exports["test throttle"] = (assert, done) => { |
|
441 let called = 0; |
|
442 let attempt = 0; |
|
443 let atleast100ms = false; |
|
444 let throttledFn = throttle(() => { |
|
445 called++; |
|
446 if (called === 2) { |
|
447 assert.equal(attempt, 10, "called twice, but attempted 10 times"); |
|
448 fn(); |
|
449 } |
|
450 if (called === 3) { |
|
451 assert.ok(atleast100ms, "atleast 100ms have passed"); |
|
452 assert.equal(attempt, 11, "called third, waits for delay to happen"); |
|
453 done(); |
|
454 } |
|
455 }, 200); |
|
456 let fn = () => ++attempt && throttledFn(); |
|
457 |
|
458 setTimeout(() => atleast100ms = true, 100); |
|
459 |
|
460 new Array(11).join(0).split("").forEach(fn); |
|
461 }; |
|
462 |
|
463 require('test').run(exports); |