From 4a6a41190264175f530b87a00d12d26875181498 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Tue, 11 Apr 2017 10:48:45 -0400 Subject: [PATCH 01/56] add observer --- karma.conf.js | 13 +++++---- package.json | 7 +++-- src/flux/observer.ts | 11 +++++++ test/bootstrap.ts | 5 ++++ karma.entry.ts => test/karma.entry.ts | 6 ++-- test/{ => unit}/bridge.ts | 4 +-- test/{ => unit}/capacitor.ts | 2 +- test/unit/observer.ts | 19 +++++++++++++ test/{ => unit}/pager.ts | 4 +-- test/{ => unit}/query.ts | 6 ++-- typings.json | 1 + yarn.lock | 41 +++++++++++++++++++++------ 12 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 src/flux/observer.ts create mode 100644 test/bootstrap.ts rename karma.entry.ts => test/karma.entry.ts (74%) rename test/{ => unit}/bridge.ts (98%) rename test/{ => unit}/capacitor.ts (99%) create mode 100644 test/unit/observer.ts rename test/{ => unit}/pager.ts (99%) rename test/{ => unit}/query.ts (97%) diff --git a/karma.conf.js b/karma.conf.js index bfeda05..12c0eb1 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -3,7 +3,7 @@ const webpackConfig = require('./webpack.config'); // eslint-disable-next-line no-process-env const isCi = process.env.NODE_ENV === 'ci'; -function reporters () { +function reporters() { const coverageReporters = [{ type: 'json', subdir: '.', @@ -15,19 +15,22 @@ function reporters () { }); } -module.exports = function (config) { +module.exports = function(config) { config.set({ basePath: '', - frameworks: ['mocha', 'chai', 'source-map-support', 'sinon'], - files: ['./karma.entry.ts'], + frameworks: ['mocha', 'sinon-chai', 'source-map-support'], + files: ['./test/karma.entry.ts'], preprocessors: { - './karma.entry.ts': ['webpack'] + './test/karma.entry.ts': ['webpack'] }, webpack: webpackConfig, webpackServer: { noInfo: true, stats: 'errors-only' }, + client: { + captureConsole: true + }, coverageReporter: { dir: 'coverage', reporters: reporters() diff --git a/package.json b/package.json index e9db3bb..69e4bb9 100644 --- a/package.json +++ b/package.json @@ -49,12 +49,11 @@ "gb-license-check": "^1.0.1", "istanbul": "^0.4.5", "karma": "^0.13.22", - "karma-chai": "^0.1.0", "karma-coverage": "^1.1.1", "karma-mocha": "^1.0.1", "karma-mocha-reporter": "^2.0.4", "karma-phantomjs-launcher": "^1.0.0", - "karma-sinon": "^1.0.5", + "karma-sinon-chai": "^1.3.1", "karma-source-map-support": "^1.2.0", "karma-webpack": "^1.7.0", "mocha": "^2.2.5", @@ -64,6 +63,7 @@ "remap-istanbul": "^0.6.4", "rimraf": "^2.5.4", "sinon": "^1.17.6", + "sinon-chai": "^2.9.0", "sourcemap-istanbul-instrumenter-loader": "^0.2.0", "tslint-eslint-rules": "^1.3.0", "tslint-loader": "^2.1.5", @@ -89,6 +89,7 @@ "eventemitter3": "^1.2.0", "filter-object": "^2.1.0", "lodash.range": "^3.2.0", - "qs": "^6.1.0" + "qs": "^6.1.0", + "redux": "^3.6.0" } } diff --git a/src/flux/observer.ts b/src/flux/observer.ts new file mode 100644 index 0000000..29047ee --- /dev/null +++ b/src/flux/observer.ts @@ -0,0 +1,11 @@ +import * as redux from 'redux'; + +export default function observe(store: redux.Store) { + let prevState; + + return () => { + const state = store.getState(); + + prevState = state; + }; +} diff --git a/test/bootstrap.ts b/test/bootstrap.ts new file mode 100644 index 0000000..b412c01 --- /dev/null +++ b/test/bootstrap.ts @@ -0,0 +1,5 @@ +import '../src/polyfills'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; + +chai.use(sinonChai); diff --git a/karma.entry.ts b/test/karma.entry.ts similarity index 74% rename from karma.entry.ts rename to test/karma.entry.ts index 9b5f449..38f227c 100644 --- a/karma.entry.ts +++ b/test/karma.entry.ts @@ -1,7 +1,7 @@ -import './src/polyfills'; +import './bootstrap'; -const coreContext = (<{ context?: Function }>require).context('./src', true, /\.ts/); +const coreContext = (<{ context?: Function }>require).context('../src', true, /\.ts/); coreContext.keys().forEach(coreContext); -const testContext = (<{ context?: Function }>require).context('./test', true, /\.ts/); +const testContext = (<{ context?: Function }>require).context('./unit', true, /\.ts/); testContext.keys().forEach(testContext); diff --git a/test/bridge.ts b/test/unit/bridge.ts similarity index 98% rename from test/bridge.ts rename to test/unit/bridge.ts index 1e96030..5fc88fc 100644 --- a/test/bridge.ts +++ b/test/unit/bridge.ts @@ -1,5 +1,5 @@ -import { BrowserBridge, CloudBridge } from '../src/core/bridge'; -import { Query } from '../src/core/query'; +import { BrowserBridge, CloudBridge } from '../../src/core/bridge'; +import { Query } from '../../src/core/query'; import { expect } from 'chai'; import * as mock from 'xhr-mock'; diff --git a/test/capacitor.ts b/test/unit/capacitor.ts similarity index 99% rename from test/capacitor.ts rename to test/unit/capacitor.ts index a74ff63..1fa8db9 100644 --- a/test/capacitor.ts +++ b/test/unit/capacitor.ts @@ -1,4 +1,4 @@ -import { Events, FluxCapacitor, Results, SelectedValueRefinement, Sort } from '../src/index'; +import { Events, FluxCapacitor, Results, SelectedValueRefinement, Sort } from '../../src/index'; import { expect } from 'chai'; import * as mock from 'xhr-mock'; diff --git a/test/unit/observer.ts b/test/unit/observer.ts new file mode 100644 index 0000000..32552b5 --- /dev/null +++ b/test/unit/observer.ts @@ -0,0 +1,19 @@ +import observe from '../../src/flux/observer'; +import { expect } from 'chai'; + +describe.only('observer', () => { + it('should return a function', () => { + const observer = observe({}); + + expect(observer).to.be.a('function'); + }); + + it('should call store.getState()', () => { + const getState = sinon.spy(); + const observer = observe({ getState }); + + observer(); + + expect(getState).to.be.called; + }); +}); diff --git a/test/pager.ts b/test/unit/pager.ts similarity index 99% rename from test/pager.ts rename to test/unit/pager.ts index c8801f0..e7f9fdf 100644 --- a/test/pager.ts +++ b/test/unit/pager.ts @@ -1,5 +1,5 @@ -import { Pager } from '../src/flux/pager'; -import { Events, FluxCapacitor, Query } from '../src/index'; +import { Pager } from '../../src/flux/pager'; +import { Events, FluxCapacitor, Query } from '../../src/index'; import { expect } from 'chai'; describe('Pager', function() { diff --git a/test/query.ts b/test/unit/query.ts similarity index 97% rename from test/query.ts rename to test/unit/query.ts index e655b0d..95a6ba8 100644 --- a/test/query.ts +++ b/test/unit/query.ts @@ -1,6 +1,6 @@ -import { Query } from '../src/core/query'; -import { SelectedValueRefinement } from '../src/models/request'; -import { COMBINED_REFINEMENTS, COMPLEX_REQUEST, CUSTOM_PARAMS_FROM_STRING } from './fixtures'; +import { Query } from '../../src/core/query'; +import { SelectedValueRefinement } from '../../src/models/request'; +import { COMBINED_REFINEMENTS, COMPLEX_REQUEST, CUSTOM_PARAMS_FROM_STRING } from '../fixtures'; import { expect } from 'chai'; describe('Query', function() { diff --git a/typings.json b/typings.json index ca0d9d9..93bae08 100644 --- a/typings.json +++ b/typings.json @@ -11,6 +11,7 @@ "sinon": "registry:dt/sinon#1.16.1+20161208163514" }, "devDependencies": { + "sinon-chai": "registry:dt/sinon-chai#2.7.0+20160628004037", "xhr-mock": "registry:npm/xhr-mock#1.6.0+20160610174323" } } diff --git a/yarn.lock b/yarn.lock index 312a662..8c34331 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1905,10 +1905,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.3.6" -karma-chai@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/karma-chai/-/karma-chai-0.1.0.tgz#bee5ad40400517811ae34bb945f762909108b79a" - karma-coverage@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-1.1.1.tgz#5aff8b39cf6994dc22de4c84362c76001b637cf6" @@ -1938,9 +1934,11 @@ karma-phantomjs-launcher@^1.0.0: lodash "^4.0.1" phantomjs-prebuilt "^2.1.7" -karma-sinon@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/karma-sinon/-/karma-sinon-1.0.5.tgz#4e3443f2830fdecff624d3747163f1217daa2a9a" +karma-sinon-chai@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/karma-sinon-chai/-/karma-sinon-chai-1.3.1.tgz#4633419494d9e2d848787dd76053031859f5b7f5" + dependencies: + lolex "^1.6.0" karma-source-map-support@^1.2.0: version "1.2.0" @@ -2072,6 +2070,10 @@ lockfile@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.3.tgz#2638fc39a0331e9cac1a04b71799931c9c50df79" +lodash-es@^4.2.1: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" + lodash._basecopy@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" @@ -2163,7 +2165,7 @@ lodash@^3.10.0, lodash@^3.8.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.1, lodash@^4.13.1, lodash@^4.16.3: +lodash@^4.0.1, lodash@^4.13.1, lodash@^4.16.3, lodash@^4.2.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -2189,11 +2191,15 @@ lolex@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" +lolex@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.1.0: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -2970,6 +2976,15 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +redux@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d" + dependencies: + lodash "^4.2.1" + lodash-es "^4.2.1" + loose-envify "^1.1.0" + symbol-observable "^1.0.2" + regex-cache@^0.4.2: version "0.4.3" resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" @@ -3160,6 +3175,10 @@ signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +sinon-chai@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.9.0.tgz#34d820042bc9661a14527130d401eb462c49bb84" + sinon@^1.17.6: version "1.17.7" resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf" @@ -3408,6 +3427,10 @@ supports-color@^3.1.0: dependencies: has-flag "^1.0.0" +symbol-observable@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" + tapable@^0.1.8, tapable@~0.1.8: version "0.1.10" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" From 09aad85c4387b3d7d728489650b60743ad7d2b7e Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Tue, 11 Apr 2017 12:07:13 -0400 Subject: [PATCH 02/56] Add observers --- src/flux/observer.ts | 39 +++++++++++++++++++++++++++++++++++++-- test/unit/observer.ts | 2 +- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/flux/observer.ts b/src/flux/observer.ts index 29047ee..a06f5f5 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -1,11 +1,46 @@ +import { Request } from '../models/request'; +import { Events, FluxCapacitor } from './capacitor'; import * as redux from 'redux'; export default function observe(store: redux.Store) { - let prevState; + let oldState; return () => { const state = store.getState(); - prevState = state; + oldState = state; }; } + +export type Observer = (oldState: any, newState: any) => void + +export type ObserverMap = { [key: string]: Observer | ObserverMap } + +// tslint:disable-next-line max-line-length +export function resolveObserver(oldState: any = {}, newState: any = {}, observers: Observer & ObserverMap) { + if (oldState !== newState) { + if (typeof observers === 'function') { + + } + } + + return null; +} + +export const observers = (flux: FluxCapacitor) => ({ + request: { + query: (oldQuery, newQuery) => { + if (oldQuery !== newQuery) { + + flux.emit(Events.QUERY_CHANGED, newQuery); + } + }, + refinements: (oldRefinements, newRefinements) => { + if (oldRefinements !== newRefinements) { + + flux.emit(Events.REFINEMENTS_CHANGED, newRefinements); + } + } + }, + response: Object.assign(() => { }, {}) +}); diff --git a/test/unit/observer.ts b/test/unit/observer.ts index 32552b5..80b4b06 100644 --- a/test/unit/observer.ts +++ b/test/unit/observer.ts @@ -1,4 +1,4 @@ -import observe from '../../src/flux/observer'; +import observe, { observers } from '../../src/flux/observer'; import { expect } from 'chai'; describe.only('observer', () => { From 18b81415f2ffb95020107657272c3205098ed66f Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Wed, 12 Apr 2017 11:20:23 -0400 Subject: [PATCH 03/56] observer tests --- src/flux/capacitor.ts | 3 ++ src/flux/observer.ts | 67 +++++++++++++-------------- test/unit/observer.ts | 102 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 129 insertions(+), 43 deletions(-) diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index f47b6a0..9adea15 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -4,6 +4,7 @@ import { SelectedRangeRefinement, SelectedValueRefinement, Sort } from '../model import { Navigation, RefinementResults, Results } from '../models/response'; import { Pager } from './pager'; import * as EventEmitter from 'eventemitter3'; +import * as redux from 'redux'; import filterObject = require('filter-object'); export namespace Events { @@ -38,6 +39,8 @@ export interface FluxBridgeConfig { export class FluxCapacitor extends EventEmitter { + store: redux.Store; + query: Query; bridge: BrowserBridge; results: Results; diff --git a/src/flux/observer.ts b/src/flux/observer.ts index a06f5f5..d0cfe49 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -1,46 +1,47 @@ -import { Request } from '../models/request'; import { Events, FluxCapacitor } from './capacitor'; -import * as redux from 'redux'; -export default function observe(store: redux.Store) { - let oldState; +type Observer = (oldState: any, newState: any) => void - return () => { - const state = store.getState(); +namespace Observer { + export type Map = { [key: string]: Observer | Map } + export type Node = Map | Observer | (Observer & Map); - oldState = state; - }; -} + export function listen(flux: FluxCapacitor) { + let oldState; -export type Observer = (oldState: any, newState: any) => void + return () => { + const state = flux.store.getState(); -export type ObserverMap = { [key: string]: Observer | ObserverMap } + Observer.resolve(oldState, state, Observer.create(flux)); -// tslint:disable-next-line max-line-length -export function resolveObserver(oldState: any = {}, newState: any = {}, observers: Observer & ObserverMap) { - if (oldState !== newState) { - if (typeof observers === 'function') { + oldState = state; + }; + } + export function resolve(oldState: any, newState: any, observers: Node) { + if (oldState !== newState) { + if (typeof observers === 'function') { + observers(oldState, newState); + } + + Object.keys(observers) + .forEach((key) => Observer.resolve((oldState || {})[key], (newState || {})[key], observers[key])); } } - return null; + export function create(flux: FluxCapacitor) { + return { + request: { + query: (oldQuery, newQuery) => { + flux.emit(Events.QUERY_CHANGED, newQuery); + }, + refinements: (oldRefinements, newRefinements) => { + flux.emit(Events.REFINEMENTS_CHANGED, newRefinements); + } + }, + response: Object.assign(() => { let a = 'a'; }, {}) + }; + } } -export const observers = (flux: FluxCapacitor) => ({ - request: { - query: (oldQuery, newQuery) => { - if (oldQuery !== newQuery) { - - flux.emit(Events.QUERY_CHANGED, newQuery); - } - }, - refinements: (oldRefinements, newRefinements) => { - if (oldRefinements !== newRefinements) { - - flux.emit(Events.REFINEMENTS_CHANGED, newRefinements); - } - } - }, - response: Object.assign(() => { }, {}) -}); +export default Observer; diff --git a/test/unit/observer.ts b/test/unit/observer.ts index 80b4b06..668b475 100644 --- a/test/unit/observer.ts +++ b/test/unit/observer.ts @@ -1,19 +1,101 @@ -import observe, { observers } from '../../src/flux/observer'; +import Observer from '../../src/flux/observer'; import { expect } from 'chai'; -describe.only('observer', () => { - it('should return a function', () => { - const observer = observe({}); +describe.only('Observer', () => { + describe('listen()', () => { + let sandbox: Sinon.SinonSandbox; - expect(observer).to.be.a('function'); + beforeEach(() => sandbox = sinon.sandbox.create()); + afterEach(() => sandbox.restore()); + + it('should return a function', () => { + const observer = Observer.listen({}); + + expect(observer).to.be.a('function'); + }); + + it('should call store.getState()', () => { + const getState = sinon.spy(); + const observer = Observer.listen({ store: { getState } }); + + observer(); + + expect(getState).to.be.called; + }); + + it('should call Observer.resolve()', () => { + const newState = { a: 'b' }; + const flux: any = { store: { getState: () => newState } }; + const resolve = sandbox.stub(Observer, 'resolve'); + const create = sandbox.stub(Observer, 'create'); + const observer = Observer.listen(flux); + + observer(); + + expect(resolve).to.be.calledWith(undefined, newState); + expect(create).to.be.calledWith(flux); + }); }); - it('should call store.getState()', () => { - const getState = sinon.spy(); - const observer = observe({ getState }); + describe('resolve()', () => { + it('should not call the observer if no changes', () => { + const observer = sinon.spy(); + + Observer.resolve(undefined, undefined, observer); + + expect(observer).to.not.be.called; + }); + + it('should not call the observer if not a function', () => { + expect(() => Observer.resolve(1, 2, {})).to.not.throw(); + }); + + it('should call the observer with the updated node', () => { + const observer = sinon.spy(); + + Observer.resolve(1, 2, (...args) => observer(...args)); + + expect(observer).to.be.calledWith(1, 2); + }); + + it('should call resolve() on subtrees', () => { + const observer1 = sinon.spy(); + const observer2 = sinon.spy(); + const observer3 = sinon.spy(); + const observer4 = sinon.spy(); + const observers = Object.assign((...args) => observer1(...args), { + a: Object.assign((...args) => observer2(...args), { + x: (...args) => observer3(...args) + }), + b: (...args) => observer4(...args) + }); + const oldState = { a: { x: 1 } }; + const newState = { b: 2 }; + + Observer.resolve(oldState, newState, observers); + + expect(observer1).to.be.calledWith(oldState, newState); + expect(observer2).to.be.calledWith({ x: 1 }, undefined); + expect(observer3).to.be.calledWith(1, undefined); + expect(observer4).to.be.calledWith(undefined, 2); + }); + + it('should not call resolve() on equal subtrees', () => { + const observer1 = sinon.spy(); + const observer2 = sinon.spy(); + const observer3 = sinon.spy(); + const observers = Object.assign((...args) => observer1(...args), { + a: (...args) => observer2(...args), + b: (...args) => observer3(...args) + }); + const oldState = {}; + const newState = {}; - observer(); + Observer.resolve(oldState, newState, observers); - expect(getState).to.be.called; + expect(observer1).to.be.calledWith(oldState, newState); + expect(observer2).to.not.be.called; + expect(observer3).to.not.be.called; + }); }); }); From 07e301be29c1fff891f1698fe2d18214f43744a9 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Wed, 12 Apr 2017 11:36:37 -0400 Subject: [PATCH 04/56] add create() tests --- test/unit/observer.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/observer.ts b/test/unit/observer.ts index 668b475..8bc316f 100644 --- a/test/unit/observer.ts +++ b/test/unit/observer.ts @@ -98,4 +98,16 @@ describe.only('Observer', () => { expect(observer3).to.not.be.called; }); }); + + describe('create()', () => { + it('should return an observer tree', () => { + const observers = Observer.create({}); + + expect(observers).to.be.an('object'); + expect(observers.request).to.be.an('object'); + expect(observers.request.query).to.be.a('function'); + expect(observers.request.refinements).to.be.a('function'); + expect(observers.response).to.be.a('function'); + }); + }); }); From 55288c329ccc3fecbae1e6c581f272bea918312e Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Wed, 12 Apr 2017 11:47:40 -0400 Subject: [PATCH 05/56] inline --- src/flux/observer.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/flux/observer.ts b/src/flux/observer.ts index d0cfe49..57142ba 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -1,6 +1,6 @@ import { Events, FluxCapacitor } from './capacitor'; -type Observer = (oldState: any, newState: any) => void +type Observer = (oldState: any, newState: any) => void; namespace Observer { export type Map = { [key: string]: Observer | Map } @@ -32,12 +32,8 @@ namespace Observer { export function create(flux: FluxCapacitor) { return { request: { - query: (oldQuery, newQuery) => { - flux.emit(Events.QUERY_CHANGED, newQuery); - }, - refinements: (oldRefinements, newRefinements) => { - flux.emit(Events.REFINEMENTS_CHANGED, newRefinements); - } + query: (oldQuery, newQuery) => flux.emit(Events.QUERY_CHANGED, newQuery), + refinements: (oldRefinements, newRefinements) => flux.emit(Events.REFINEMENTS_CHANGED, newRefinements) }, response: Object.assign(() => { let a = 'a'; }, {}) }; From be02b77d362af91a8e51e4fcaa0e04ff4ca36d62 Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Wed, 12 Apr 2017 12:11:41 -0400 Subject: [PATCH 06/56] Add response --- src/flux/observer.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/flux/observer.ts b/src/flux/observer.ts index 57142ba..007e6b5 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -35,7 +35,13 @@ namespace Observer { query: (oldQuery, newQuery) => flux.emit(Events.QUERY_CHANGED, newQuery), refinements: (oldRefinements, newRefinements) => flux.emit(Events.REFINEMENTS_CHANGED, newRefinements) }, - response: Object.assign(() => { let a = 'a'; }, {}) + response: Object.assign((oldResponse, newResponse) => { + if (newResponse.redirect) { + flux.emit(Events.REDIRECT, newResponse.redirect); + } else { + flux.emit(Events.RESULTS, newResponse); + } + }, {}) }; } } From a6ac65b91daec0322d918815fffb809e0c354887 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Wed, 12 Apr 2017 15:43:35 -0400 Subject: [PATCH 07/56] complete observer? --- src/flux/observer.ts | 38 +++++++++++++++++++++++++--------- src/flux/reducer.ts | 3 +++ src/flux/store.ts | 30 +++++++++++++++++++++++++++ src/models/request.ts | 46 +++++++++++++++++++++--------------------- src/models/response.ts | 3 +++ test/unit/observer.ts | 10 +++++---- 6 files changed, 93 insertions(+), 37 deletions(-) create mode 100644 src/flux/reducer.ts create mode 100644 src/flux/store.ts diff --git a/src/flux/observer.ts b/src/flux/observer.ts index 007e6b5..eff3492 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -1,5 +1,7 @@ import { Events, FluxCapacitor } from './capacitor'; +export const DETAIL_QUERY_INDICATOR = 'gbiDetailQuery'; + type Observer = (oldState: any, newState: any) => void; namespace Observer { @@ -31,17 +33,33 @@ namespace Observer { export function create(flux: FluxCapacitor) { return { - request: { - query: (oldQuery, newQuery) => flux.emit(Events.QUERY_CHANGED, newQuery), - refinements: (oldRefinements, newRefinements) => flux.emit(Events.REFINEMENTS_CHANGED, newRefinements) - }, - response: Object.assign((oldResponse, newResponse) => { - if (newResponse.redirect) { - flux.emit(Events.REDIRECT, newResponse.redirect); - } else { - flux.emit(Events.RESULTS, newResponse); + data: { + search: { + request: Object.assign((_, newRequest) => flux.emit(Events.SEARCH, newRequest), { + // NOTE: can ONLY be used to switch the "active" page in gb-paging + skip: (_, newPageNumber) => flux.emit(Events.PAGE_CHANGED, newPageNumber), + collection: (_, newCollection) => flux.emit(Events.COLLECTION_CHANGED, newCollection), + query: (_, newQuery) => flux.emit(Events.QUERY_CHANGED, newQuery), + // TODO: emitted value will break current implementations + refinements: (_, newRefinements) => flux.emit(Events.REFINEMENTS_CHANGED, newRefinements), + sort: (_, newSort) => flux.emit(Events.SORT, newSort) + }), + response: Object.assign((_, newResponse) => { + if (newResponse.redirect) { + flux.emit(Events.REDIRECT, newResponse.redirect); + } else { + // NOTE: REFINEMENT_RESULTS is no longer, should check RESULTS + flux.emit(Events.RESULTS, newResponse); + + const isDetailQuery = (newResponse.originalQuery.customUrlParams || []) + .find(({ key }) => key === DETAIL_QUERY_INDICATOR); + if (isDetailQuery) { + flux.emit(Events.DETAILS, newResponse.records[0]); + } + } + }) } - }, {}) + } }; } } diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts new file mode 100644 index 0000000..e800863 --- /dev/null +++ b/src/flux/reducer.ts @@ -0,0 +1,3 @@ +import * as redux from 'redux'; + +export default redux.combineReducers({}); diff --git a/src/flux/store.ts b/src/flux/store.ts new file mode 100644 index 0000000..75dbfd4 --- /dev/null +++ b/src/flux/store.ts @@ -0,0 +1,30 @@ +import { Request } from '../models/request'; +import { Results } from '../models/response'; +import reducer from './reducer'; +import * as redux from 'redux'; + +namespace Store { + + export interface State { + data?: { + search?: { + req?: Request; + res?: Results; + }; + // sayt?: {}; + // recommendations?: {}; + // shoppingCart?: {}; + }; + ui?: { + [tagName: string]: { + [tagId: number]: any; + }; + }; + } + + export function create() { + return redux.createStore(reducer); + } +} + +export default Store; diff --git a/src/models/request.ts b/src/models/request.ts index b93dac0..eb3a972 100644 --- a/src/models/request.ts +++ b/src/models/request.ts @@ -2,38 +2,38 @@ import { RangeRefinement, Refinement, ValueRefinement } from './response'; export type SortOrder = 'Ascending' | 'Descending'; -export class Request { +export interface Request { // query parameters - query: string; - refinements: SelectedRefinement[]; + query?: string; + refinements?: SelectedRefinement[]; // query configuration - fields: string[]; - orFields: string[]; - includedNavigations: string[]; - excludedNavigations: string[]; - sort: Sort[]; - customUrlParams: CustomUrlParam[]; - restrictNavigation: RestrictNavigation; - biasing: Biasing; - matchStrategy: MatchStrategy; + fields?: string[]; + orFields?: string[]; + includedNavigations?: string[]; + excludedNavigations?: string[]; + sort?: Sort[]; + customUrlParams?: CustomUrlParam[]; + restrictNavigation?: RestrictNavigation; + biasing?: Biasing; + matchStrategy?: MatchStrategy; // configuration - userId: string; - language: string; - collection: string; - area: string; - biasingProfile: string; + userId?: string; + language?: string; + collection?: string; + area?: string; + biasingProfile?: string; // paging - skip: number; - pageSize: number; + skip?: number; + pageSize?: number; // format - returnBinary: boolean; - pruneRefinements: boolean; - disableAutocorrection: boolean; - wildcardSearchEnabled: boolean; + returnBinary?: boolean; + pruneRefinements?: boolean; + disableAutocorrection?: boolean; + wildcardSearchEnabled?: boolean; } export interface Sort { diff --git a/src/models/response.ts b/src/models/response.ts index ab4475d..bcc900c 100644 --- a/src/models/response.ts +++ b/src/models/response.ts @@ -1,3 +1,5 @@ +import { Request } from './request'; + export type RefinementType = 'Value' | 'Range'; export type SortType = 'Count_Ascending' | 'Count_Descending' | 'Value_Ascending' | 'Value_Descending'; @@ -6,6 +8,7 @@ export interface Results { query: string; originalQuery: string; correctedQuery: string; + originalRequest: Request; area: string; biasingProfile: string; diff --git a/test/unit/observer.ts b/test/unit/observer.ts index 8bc316f..9450697 100644 --- a/test/unit/observer.ts +++ b/test/unit/observer.ts @@ -104,10 +104,12 @@ describe.only('Observer', () => { const observers = Observer.create({}); expect(observers).to.be.an('object'); - expect(observers.request).to.be.an('object'); - expect(observers.request.query).to.be.a('function'); - expect(observers.request.refinements).to.be.a('function'); - expect(observers.response).to.be.a('function'); + expect(observers.data).to.be.an('object'); + expect(observers.data.search).to.be.an('object'); + expect(observers.data.search.request).to.be.a('function'); + expect(observers.data.search.request.query).to.be.a('function'); + expect(observers.data.search.request.refinements).to.be.a('function'); + expect(observers.data.search.response).to.be.a('function'); }); }); }); From f38ed879f0ea2b3ca34cbc02816ea6f269458ce4 Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Wed, 12 Apr 2017 16:24:32 -0400 Subject: [PATCH 08/56] Add tests for emits --- test/unit/observer.ts | 66 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/test/unit/observer.ts b/test/unit/observer.ts index 9450697..ab8a9c7 100644 --- a/test/unit/observer.ts +++ b/test/unit/observer.ts @@ -1,4 +1,5 @@ -import Observer from '../../src/flux/observer'; +import { Events } from '../../src/flux/capacitor'; +import Observer, { DETAIL_QUERY_INDICATOR } from '../../src/flux/observer'; import { expect } from 'chai'; describe.only('Observer', () => { @@ -111,5 +112,68 @@ describe.only('Observer', () => { expect(observers.data.search.request.refinements).to.be.a('function'); expect(observers.data.search.response).to.be.a('function'); }); + + describe('search', () => { + let emit; + let observers; + + beforeEach(() => { + emit = sinon.spy(); + observers = Observer.create({ emit }); + }); + + describe('request', () => { + it('should emit search event', () => { + observers.data.search.request(undefined, { a: 'b' }); + expect(emit).to.be.calledWith(Events.SEARCH, { a: 'b' }); + }); + + it('should emit PAGE_CHANGED event', () => { + observers.data.search.request.skip(undefined, 23); + expect(emit).to.be.calledWith(Events.PAGE_CHANGED, 23); + }); + + it('should emit COLLECTION_CHANGED event', () => { + observers.data.search.request.collection(undefined, 'somestring'); + expect(emit).to.be.calledWith(Events.COLLECTION_CHANGED, 'somestring'); + }); + + it('should emit QUERY_CHANGED event', () => { + observers.data.search.request.query(undefined, 'tomatoes'); + expect(emit).to.be.calledWith(Events.QUERY_CHANGED, 'tomatoes'); + }); + + it('should emit REFINEMENTS_CHANGED event', () => { + observers.data.search.request.refinements(undefined, [{ c: 'd' }]); + expect(emit).to.be.calledWith(Events.REFINEMENTS_CHANGED, [{ c: 'd' }]); + }); + + it('should emit SORT event', () => { + observers.data.search.request.sort(undefined, [{ e: 'f' }]); + expect(emit).to.be.calledWith(Events.SORT, [{ e: 'f' }]); + }); + }); + + describe('response', () => { + it('should emit REDIRECT event', () => { + observers.data.search.response(undefined, { redirect: '/toys.html' }); + expect(emit).to.be.calledWith(Events.REDIRECT, '/toys.html'); + }); + + it('should emit RESULTS event', () => { + observers.data.search.response(undefined, { g: 'h', originalQuery: {} }); + expect(emit).to.be.calledWith(Events.RESULTS, { g: 'h', originalQuery: {} }); + }); + + it('should emit DETAILS event', () => { + observers.data.search.response(undefined, { + originalQuery: { customUrlParams: [{ key: DETAIL_QUERY_INDICATOR, value: 'yo' }] }, + records: [{ i: 'j' }] + }); + expect(emit).to.be.calledWith(Events.DETAILS, { i: 'j' }); + }); + }); + }); + }); }); From 1344f61c25528e51a2263c79e8fe36ac7d248ca3 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Mon, 17 Apr 2017 17:11:32 -0400 Subject: [PATCH 09/56] rename events (#58) --- package.json | 12 +-- src/flux/actions.ts | 1 + src/flux/capacitor.ts | 15 ++++ src/flux/observer.ts | 59 ++++++++++--- src/flux/reducer.ts | 22 ++++- src/utils/index.ts | 1 + test/unit/_suite.ts | 21 +++++ test/unit/{ => core}/bridge.ts | 10 +-- test/unit/{ => core}/query.ts | 10 +-- test/unit/{ => flux}/capacitor.ts | 8 +- test/unit/{ => flux}/observer.ts | 52 +++++++----- test/unit/{ => flux}/pager.ts | 8 +- tslint.json | 3 - typings.json | 1 - yarn.lock | 137 ++++++++++++++++++++++-------- 15 files changed, 261 insertions(+), 99 deletions(-) create mode 100644 src/flux/actions.ts create mode 100644 src/utils/index.ts create mode 100644 test/unit/_suite.ts rename test/unit/{ => core}/bridge.ts (96%) rename test/unit/{ => core}/query.ts (96%) rename test/unit/{ => flux}/capacitor.ts (99%) rename test/unit/{ => flux}/observer.ts (76%) rename test/unit/{ => flux}/pager.ts (98%) diff --git a/package.json b/package.json index 69e4bb9..fe2d1d0 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "postinstall": "[ ! -d src ] || typings install", "clean": "rimraf docs coverage dist node_modules typings", "pretest": "rimraf coverage", + "prepush": "npm test && npm run license", "test": "NODE_ENV=test karma start", "test+coverage": "NODE_ENV=test karma start && npm run coverage && npm run coverage:codacy", "posttest": "npm run coverage", @@ -23,10 +24,6 @@ "docs": "rimraf docs && typedoc --options ./typedoc.json ./src ./custom_typings ./typings", "license": "node licenseChecker.js" }, - "pre-commit": [ - "test", - "license" - ], "repository": { "type": "git", "url": "git+https://github.com/groupby/api-javascript.git" @@ -47,6 +44,7 @@ "chai": "^3.2.0", "codacy-coverage": "^2.0.0", "gb-license-check": "^1.0.1", + "husky": "^0.13.3", "istanbul": "^0.4.5", "karma": "^0.13.22", "karma-coverage": "^1.1.1", @@ -57,6 +55,7 @@ "karma-source-map-support": "^1.2.0", "karma-webpack": "^1.7.0", "mocha": "^2.2.5", + "mocha-suite": "^1.0.8", "object-assign": "^4.1.0", "phantomjs-prebuilt": "^2.1.7", "pre-commit": "^1.1.3", @@ -65,8 +64,9 @@ "sinon": "^1.17.6", "sinon-chai": "^2.9.0", "sourcemap-istanbul-instrumenter-loader": "^0.2.0", - "tslint-eslint-rules": "^1.3.0", - "tslint-loader": "^2.1.5", + "tslint": "^5.1.0", + "tslint-eslint-rules": "^4.0.0", + "tslint-loader": "^3.5.2", "typedoc": "^0.4.5", "typescript": "2.0.2", "typings": "^1.3.3", diff --git a/src/flux/actions.ts b/src/flux/actions.ts new file mode 100644 index 0000000..288387c --- /dev/null +++ b/src/flux/actions.ts @@ -0,0 +1 @@ +export const UPDATE_SEARCH_REQUEST = 'search:req:update'; diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index 9adea15..33a9447 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -21,6 +21,21 @@ export namespace Events { export const REWRITE_QUERY = 'rewrite_query'; export const SEARCH = 'search'; export const SORT = 'sort'; + + // request + export const SEARCH_REQ_UPDATED = 'search:req_updated'; + export const SEARCH_COLLECTION_UPDATED = 'search:req:collection_updated'; + export const SEARCH_PAGE_UPDATED = 'search:req:page_updated'; + export const SEARCH_QUERY_UPDATED = 'search:req:query_updated'; + export const SEARCH_REFINEMENTS_UPDATED = 'search:req:refinements_updated'; + export const SEARCH_SORT_UPDATED = 'search:req:sort_updated'; + + // response + export const SEARCH_RES_UPDATED = 'search:res_updated'; + + // "global" + export const SEARCH_REDIRECT = 'search:res:redirect'; + export const SEARCH_DETAILS = 'search:res:details'; } export { Pager }; diff --git a/src/flux/observer.ts b/src/flux/observer.ts index eff3492..375a8f5 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -1,3 +1,4 @@ +import { rayify } from '../utils'; import { Events, FluxCapacitor } from './capacitor'; export const DETAIL_QUERY_INDICATOR = 'gbiDetailQuery'; @@ -32,29 +33,63 @@ namespace Observer { } export function create(flux: FluxCapacitor) { + function emit(events: string | string[], data: any) { + rayify(events).forEach((event) => flux.emit(event, data)); + } + return { data: { search: { - request: Object.assign((_, newRequest) => flux.emit(Events.SEARCH, newRequest), { - // NOTE: can ONLY be used to switch the "active" page in gb-paging - skip: (_, newPageNumber) => flux.emit(Events.PAGE_CHANGED, newPageNumber), - collection: (_, newCollection) => flux.emit(Events.COLLECTION_CHANGED, newCollection), - query: (_, newQuery) => flux.emit(Events.QUERY_CHANGED, newQuery), - // TODO: emitted value will break current implementations - refinements: (_, newRefinements) => flux.emit(Events.REFINEMENTS_CHANGED, newRefinements), - sort: (_, newSort) => flux.emit(Events.SORT, newSort) - }), + request: Object.assign((_, newRequest) => emit([ + Events.SEARCH_REQ_UPDATED, + Events.SEARCH + ], newRequest), { + // NOTE: can ONLY be used to switch the "active" page in gb-paging + skip: (_, newPageNumber) => emit([ + Events.SEARCH_PAGE_UPDATED, + Events.PAGE_CHANGED + ], newPageNumber), + collection: (_, newCollection) => emit([ + Events.SEARCH_COLLECTION_UPDATED, + Events.COLLECTION_CHANGED + ], newCollection), + query: (_, newQuery) => emit([ + Events.SEARCH_QUERY_UPDATED, + Events.QUERY_CHANGED, + Events.REWRITE_QUERY + ], newQuery), + // TODO: emitted value will break current implementations + refinements: (_, newRefinements) => emit([ + Events.SEARCH_REFINEMENTS_UPDATED, + Events.REFINEMENTS_CHANGED + ], newRefinements), + sort: (_, newSort) => emit([ + Events.SEARCH_SORT_UPDATED, + Events.SORT + ], newSort) + }), response: Object.assign((_, newResponse) => { if (newResponse.redirect) { - flux.emit(Events.REDIRECT, newResponse.redirect); + emit([ + Events.SEARCH_REDIRECT, + Events.REDIRECT + ], newResponse.redirect); } else { // NOTE: REFINEMENT_RESULTS is no longer, should check RESULTS - flux.emit(Events.RESULTS, newResponse); + emit([ + Events.SEARCH_RES_UPDATED, + Events.RESULTS, + Events.RESET + ], newResponse); + // NOTE: make sure to add the indicator when making the request const isDetailQuery = (newResponse.originalQuery.customUrlParams || []) .find(({ key }) => key === DETAIL_QUERY_INDICATOR); if (isDetailQuery) { - flux.emit(Events.DETAILS, newResponse.records[0]); + emit([ + Events.SEARCH_DETAILS, + Events.DETAILS + ], newResponse.records[0]); } } }) diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts index e800863..0ffda69 100644 --- a/src/flux/reducer.ts +++ b/src/flux/reducer.ts @@ -1,3 +1,23 @@ +import { UPDATE_SEARCH_REQUEST } from './actions'; import * as redux from 'redux'; -export default redux.combineReducers({}); +export namespace Search { + export const request = (state, action) => { + + switch (action.type) { + case UPDATE_SEARCH_REQUEST: + break; + default: return state; + } + }; + export const response = (state, action) => state; +} + +export default redux.combineReducers({ + data: redux.combineReducers({ + search: redux.combineReducers({ + req: Search.request, + res: Search.response + }) + }) +}); diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..f2af439 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export const rayify = (arr: any | any[]): any[] => Array.isArray(arr) ? arr : [arr]; diff --git a/test/unit/_suite.ts b/test/unit/_suite.ts new file mode 100644 index 0000000..606121f --- /dev/null +++ b/test/unit/_suite.ts @@ -0,0 +1,21 @@ +import { expect } from 'chai'; +import * as suite from 'mocha-suite'; + +export default <(description: string, tests: (utils: Utils) => void) => void>suite((tests) => { + let sandbox: Sinon.SinonSandbox; + + beforeEach(() => sandbox = sinon.sandbox.create()); + afterEach(() => sandbox.restore()); + + tests({ + expect, + spy: (...args) => (sandbox.spy)(...args), + stub: (...args) => (sandbox.stub)(...args) + }); +}); + +export interface Utils { + expect: Chai.ExpectStatic; + spy: Sinon.SinonSpyStatic; + stub: Sinon.SinonSpyStatic; +} diff --git a/test/unit/bridge.ts b/test/unit/core/bridge.ts similarity index 96% rename from test/unit/bridge.ts rename to test/unit/core/bridge.ts index 5fc88fc..f108e1c 100644 --- a/test/unit/bridge.ts +++ b/test/unit/core/bridge.ts @@ -1,12 +1,12 @@ -import { BrowserBridge, CloudBridge } from '../../src/core/bridge'; -import { Query } from '../../src/core/query'; -import { expect } from 'chai'; +import { BrowserBridge, CloudBridge } from '../../../src/core/bridge'; +import { Query } from '../../../src/core/query'; +import suite from '../_suite'; import * as mock from 'xhr-mock'; const CLIENT_KEY = 'XXX-XXX-XXX-XXX'; const CUSTOMER_ID = 'services'; -describe('Bridge', () => { +suite('Bridge', ({ expect, spy }) => { let bridge; let query; @@ -171,7 +171,7 @@ describe('Bridge', () => { }); it('should invoke any configured errorHandler on error and allow downstream promise catching', (done) => { - const errorHandler = bridge.errorHandler = sinon.spy(); + const errorHandler = bridge.errorHandler = spy(); mock.post(`https://${CUSTOMER_ID}.groupbycloud.com:443/api/v1/search`, (req, res) => { return res.status(400).body('error'); }); diff --git a/test/unit/query.ts b/test/unit/core/query.ts similarity index 96% rename from test/unit/query.ts rename to test/unit/core/query.ts index 95a6ba8..3b4ec7a 100644 --- a/test/unit/query.ts +++ b/test/unit/core/query.ts @@ -1,9 +1,9 @@ -import { Query } from '../../src/core/query'; -import { SelectedValueRefinement } from '../../src/models/request'; -import { COMBINED_REFINEMENTS, COMPLEX_REQUEST, CUSTOM_PARAMS_FROM_STRING } from '../fixtures'; -import { expect } from 'chai'; +import { Query } from '../../../src/core/query'; +import { SelectedValueRefinement } from '../../../src/models/request'; +import { COMBINED_REFINEMENTS, COMPLEX_REQUEST, CUSTOM_PARAMS_FROM_STRING } from '../../fixtures'; +import suite from '../_suite'; -describe('Query', function() { +suite('Query', ({ expect }) => { let query: Query; beforeEach(() => { diff --git a/test/unit/capacitor.ts b/test/unit/flux/capacitor.ts similarity index 99% rename from test/unit/capacitor.ts rename to test/unit/flux/capacitor.ts index 1fa8db9..b119684 100644 --- a/test/unit/capacitor.ts +++ b/test/unit/flux/capacitor.ts @@ -1,5 +1,5 @@ -import { Events, FluxCapacitor, Results, SelectedValueRefinement, Sort } from '../../src/index'; -import { expect } from 'chai'; +import { Events, FluxCapacitor, Results, SelectedValueRefinement, Sort } from '../../../src/index'; +import suite from '../_suite'; import * as mock from 'xhr-mock'; const CUSTOMER_ID = 'services'; @@ -9,7 +9,7 @@ const SELECTED_REFINEMENT: SelectedValueRefinement = { type: 'Value', navigation const REFINEMENT_RESULT = { availableNavigation: 'a', selectedNavigation: 'b' }; const DETAILS_RESULT = { records: [{}] }; -describe('FluxCapacitor', function() { +suite('FluxCapacitor', ({ expect, spy }) => { let flux: FluxCapacitor; beforeEach(() => { @@ -81,7 +81,7 @@ describe('FluxCapacitor', function() { }); it('should set configured errorHandler on bridge', () => { - const errorHandler = sinon.spy(); + const errorHandler = spy(); flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { errorHandler } }); const error: any = { a: 'b' }; diff --git a/test/unit/observer.ts b/test/unit/flux/observer.ts similarity index 76% rename from test/unit/observer.ts rename to test/unit/flux/observer.ts index ab8a9c7..a769237 100644 --- a/test/unit/observer.ts +++ b/test/unit/flux/observer.ts @@ -1,14 +1,9 @@ -import { Events } from '../../src/flux/capacitor'; -import Observer, { DETAIL_QUERY_INDICATOR } from '../../src/flux/observer'; -import { expect } from 'chai'; +import { Events } from '../../../src/flux/capacitor'; +import Observer, { DETAIL_QUERY_INDICATOR } from '../../../src/flux/observer'; +import suite from '../_suite'; -describe.only('Observer', () => { +suite('Observer', ({ expect, spy, stub }) => { describe('listen()', () => { - let sandbox: Sinon.SinonSandbox; - - beforeEach(() => sandbox = sinon.sandbox.create()); - afterEach(() => sandbox.restore()); - it('should return a function', () => { const observer = Observer.listen({}); @@ -16,7 +11,7 @@ describe.only('Observer', () => { }); it('should call store.getState()', () => { - const getState = sinon.spy(); + const getState = spy(); const observer = Observer.listen({ store: { getState } }); observer(); @@ -27,8 +22,8 @@ describe.only('Observer', () => { it('should call Observer.resolve()', () => { const newState = { a: 'b' }; const flux: any = { store: { getState: () => newState } }; - const resolve = sandbox.stub(Observer, 'resolve'); - const create = sandbox.stub(Observer, 'create'); + const resolve = stub(Observer, 'resolve'); + const create = stub(Observer, 'create'); const observer = Observer.listen(flux); observer(); @@ -40,7 +35,7 @@ describe.only('Observer', () => { describe('resolve()', () => { it('should not call the observer if no changes', () => { - const observer = sinon.spy(); + const observer = spy(); Observer.resolve(undefined, undefined, observer); @@ -52,7 +47,7 @@ describe.only('Observer', () => { }); it('should call the observer with the updated node', () => { - const observer = sinon.spy(); + const observer = spy(); Observer.resolve(1, 2, (...args) => observer(...args)); @@ -60,10 +55,10 @@ describe.only('Observer', () => { }); it('should call resolve() on subtrees', () => { - const observer1 = sinon.spy(); - const observer2 = sinon.spy(); - const observer3 = sinon.spy(); - const observer4 = sinon.spy(); + const observer1 = spy(); + const observer2 = spy(); + const observer3 = spy(); + const observer4 = spy(); const observers = Object.assign((...args) => observer1(...args), { a: Object.assign((...args) => observer2(...args), { x: (...args) => observer3(...args) @@ -82,9 +77,9 @@ describe.only('Observer', () => { }); it('should not call resolve() on equal subtrees', () => { - const observer1 = sinon.spy(); - const observer2 = sinon.spy(); - const observer3 = sinon.spy(); + const observer1 = spy(); + const observer2 = spy(); + const observer3 = spy(); const observers = Object.assign((...args) => observer1(...args), { a: (...args) => observer2(...args), b: (...args) => observer3(...args) @@ -118,38 +113,45 @@ describe.only('Observer', () => { let observers; beforeEach(() => { - emit = sinon.spy(); + emit = spy(); observers = Observer.create({ emit }); }); describe('request', () => { it('should emit search event', () => { observers.data.search.request(undefined, { a: 'b' }); + expect(emit).to.be.calledWith(Events.SEARCH_REQ_UPDATED, { a: 'b' }); expect(emit).to.be.calledWith(Events.SEARCH, { a: 'b' }); }); it('should emit PAGE_CHANGED event', () => { observers.data.search.request.skip(undefined, 23); + expect(emit).to.be.calledWith(Events.SEARCH_PAGE_UPDATED, 23); expect(emit).to.be.calledWith(Events.PAGE_CHANGED, 23); }); it('should emit COLLECTION_CHANGED event', () => { observers.data.search.request.collection(undefined, 'somestring'); + expect(emit).to.be.calledWith(Events.SEARCH_COLLECTION_UPDATED, 'somestring'); expect(emit).to.be.calledWith(Events.COLLECTION_CHANGED, 'somestring'); }); it('should emit QUERY_CHANGED event', () => { observers.data.search.request.query(undefined, 'tomatoes'); + expect(emit).to.be.calledWith(Events.SEARCH_QUERY_UPDATED, 'tomatoes'); expect(emit).to.be.calledWith(Events.QUERY_CHANGED, 'tomatoes'); + expect(emit).to.be.calledWith(Events.REWRITE_QUERY, 'tomatoes'); }); it('should emit REFINEMENTS_CHANGED event', () => { observers.data.search.request.refinements(undefined, [{ c: 'd' }]); + expect(emit).to.be.calledWith(Events.SEARCH_REFINEMENTS_UPDATED, [{ c: 'd' }]); expect(emit).to.be.calledWith(Events.REFINEMENTS_CHANGED, [{ c: 'd' }]); }); - it('should emit SORT event', () => { + it('should emit SORT and SORT_CHANGED event', () => { observers.data.search.request.sort(undefined, [{ e: 'f' }]); + expect(emit).to.be.calledWith(Events.SEARCH_SORT_UPDATED, [{ e: 'f' }]); expect(emit).to.be.calledWith(Events.SORT, [{ e: 'f' }]); }); }); @@ -157,12 +159,15 @@ describe.only('Observer', () => { describe('response', () => { it('should emit REDIRECT event', () => { observers.data.search.response(undefined, { redirect: '/toys.html' }); + expect(emit).to.be.calledWith(Events.SEARCH_REDIRECT, '/toys.html'); expect(emit).to.be.calledWith(Events.REDIRECT, '/toys.html'); }); it('should emit RESULTS event', () => { observers.data.search.response(undefined, { g: 'h', originalQuery: {} }); + expect(emit).to.be.calledWith(Events.SEARCH_RES_UPDATED, { g: 'h', originalQuery: {} }); expect(emit).to.be.calledWith(Events.RESULTS, { g: 'h', originalQuery: {} }); + expect(emit).to.be.calledWith(Events.RESET, { g: 'h', originalQuery: {} }); }); it('should emit DETAILS event', () => { @@ -170,6 +175,7 @@ describe.only('Observer', () => { originalQuery: { customUrlParams: [{ key: DETAIL_QUERY_INDICATOR, value: 'yo' }] }, records: [{ i: 'j' }] }); + expect(emit).to.be.calledWith(Events.SEARCH_DETAILS, { i: 'j' }); expect(emit).to.be.calledWith(Events.DETAILS, { i: 'j' }); }); }); diff --git a/test/unit/pager.ts b/test/unit/flux/pager.ts similarity index 98% rename from test/unit/pager.ts rename to test/unit/flux/pager.ts index e7f9fdf..6f397a7 100644 --- a/test/unit/pager.ts +++ b/test/unit/flux/pager.ts @@ -1,8 +1,8 @@ -import { Pager } from '../../src/flux/pager'; -import { Events, FluxCapacitor, Query } from '../../src/index'; -import { expect } from 'chai'; +import { Pager } from '../../../src/flux/pager'; +import { Events, FluxCapacitor, Query } from '../../../src/index'; +import suite from '../_suite'; -describe('Pager', function() { +suite('Pager', ({ expect }) => { function flux(opts: { start: number, total?: number, pageSize?: number } | number, search?: Function): FluxCapacitor { const recordStart = typeof opts === 'number' ? opts : opts.start; const totalRecordCount = typeof opts === 'object' && Number(opts.total) >= 0 ? opts.total : 30; diff --git a/tslint.json b/tslint.json index 786b379..f924893 100644 --- a/tslint.json +++ b/tslint.json @@ -21,16 +21,13 @@ }], "forin": true, "label-position": true, - "label-undefined": true, "no-arg": true, "no-console": [true, "log"], "no-construct": true, "no-shadowed-variable": true, "no-switch-case-fall-through": true, - "no-unreachable": true, "no-unsafe-finally": true, "no-unused-expression": true, - "no-unused-new": true, "no-unused-variable": [true], "no-use-before-declare": true, "no-var-keyword": true, diff --git a/typings.json b/typings.json index 93bae08..f947b70 100644 --- a/typings.json +++ b/typings.json @@ -7,7 +7,6 @@ }, "globalDevDependencies": { "chai": "registry:dt/chai#3.4.0+20160601211834", - "mocha": "github:DefinitelyTyped/DefinitelyTyped/mocha/mocha.d.ts#855569dbc86b619aa3bbfab98f3ff2e6b55b5fcd", "sinon": "registry:dt/sinon#1.16.1+20161208163514" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 8c34331..92b8ae7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,6 +18,10 @@ version "1.2.0" resolved "https://registry.yarnpkg.com/@types/eventemitter3/-/eventemitter3-1.2.0.tgz#4ebc1b9c6e92417c8e5c49204bb48189f7463dca" +"@types/mocha@^2.2.40": + version "2.2.40" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.40.tgz#9811dd800ece544cd84b5b859917bf584a150c4c" + "@types/node@^7.0.4": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.4.tgz#9aabc135979ded383325749f508894c662948c8b" @@ -245,6 +249,14 @@ axios@^0.12.0: dependencies: follow-redirects "0.0.7" +babel-code-frame@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" @@ -445,7 +457,7 @@ chai@^3.2.0: deep-eql "^0.1.3" type-detect "^1.0.0" -chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1: +chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -480,6 +492,10 @@ chokidar@^1.0.0, chokidar@^1.4.1: optionalDependencies: fsevents "^1.0.0" +ci-info@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.0.0.tgz#dc5285f2b4e251821683681c381c3388f46ec534" + cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" @@ -790,9 +806,9 @@ diff@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" -diff@^2.2.1: - version "2.2.3" - resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" +diff@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" doctrine@^0.7.2: version "0.7.2" @@ -1133,6 +1149,10 @@ finalhandler@0.5.0: statuses "~1.3.0" unpipe "~1.0.0" +find-parent-dir@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -1317,7 +1337,7 @@ glob@^6.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5: +glob@^7.0.0, glob@^7.0.5, glob@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -1534,6 +1554,15 @@ https-proxy-agent@^1.0.0: debug "2" extend "3" +husky@^0.13.3: + version "0.13.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-0.13.3.tgz#bc2066080badc8b8fe3516e881f5bc68a57052ff" + dependencies: + chalk "^1.1.3" + find-parent-dir "^0.3.0" + is-ci "^1.0.9" + normalize-path "^1.0.0" + iconv-lite@0.4.15: version "0.4.15" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" @@ -1620,6 +1649,12 @@ is-callable@^1.1.1, is-callable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" +is-ci@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" + dependencies: + ci-info "^1.0.0" + is-date-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" @@ -2057,7 +2092,7 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -loader-utils@0.x.x, loader-utils@^0.2.11, loader-utils@^0.2.5, loader-utils@^0.2.6, loader-utils@^0.2.7: +loader-utils@0.x.x, loader-utils@^0.2.11, loader-utils@^0.2.5, loader-utils@^0.2.6: version "0.2.16" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" dependencies: @@ -2066,6 +2101,14 @@ loader-utils@0.x.x, loader-utils@^0.2.11, loader-utils@^0.2.5, loader-utils@^0.2 json5 "^0.5.0" object-assign "^4.0.1" +loader-utils@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + lockfile@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.3.tgz#2638fc39a0331e9cac1a04b71799931c9c50df79" @@ -2367,6 +2410,12 @@ mkdirp@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" +mocha-suite@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/mocha-suite/-/mocha-suite-1.0.8.tgz#130b9c9faf6f92b4ff1cf72cc6e523c09ef6b8d3" + dependencies: + "@types/mocha" "^2.2.40" + mocha@^2.2.5: version "2.5.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" @@ -2481,6 +2530,10 @@ normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package- semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" + normalize-path@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" @@ -2506,7 +2559,7 @@ object-assign@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" -object-assign@4.x.x, object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@4.x.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -2666,6 +2719,10 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -3080,10 +3137,16 @@ requires-port@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" -resolve@1.1.x, resolve@^1.1.6, resolve@^1.1.7: +resolve@1.1.x, resolve@^1.1.6: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" +resolve@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.2.tgz#1f0442c9e0cbb8136e87b9305f932f46c7f28235" + dependencies: + path-parse "^1.0.5" + restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" @@ -3317,7 +3380,7 @@ spdx-license-ids@^1.0.2: version "1.2.2" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" -sprintf-js@^1.0.3, sprintf-js@~1.0.2: +sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -3405,7 +3468,7 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -strip-json-comments@^1.0.2, strip-json-comments@~1.0.4: +strip-json-comments@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" @@ -3539,34 +3602,45 @@ trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" -tslint-eslint-rules@^1.3.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-1.6.1.tgz#39e92f31956ad2a66c0061c351fa96c0808ae0f8" +tslib@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.6.0.tgz#cf36c93e02ae86a20fc131eae511162b869a6652" + +tslint-eslint-rules@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-4.0.0.tgz#4e0e59ecd5701c9a48c66ed47bdcafb1c635d27b" dependencies: doctrine "^0.7.2" - tslint "^3.15.1" + tslib "^1.0.0" + tsutils "^1.4.0" -tslint-loader@^2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/tslint-loader/-/tslint-loader-2.1.5.tgz#77abdfd9bf13d7133a6efa4447a1690783c4bb49" +tslint-loader@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/tslint-loader/-/tslint-loader-3.5.2.tgz#ef2f512c05a96fb1a22d94c32d22fb7975ef9917" dependencies: - loader-utils "^0.2.7" + loader-utils "^1.0.2" mkdirp "^0.5.1" - object-assign "^4.0.1" + object-assign "^4.1.1" rimraf "^2.4.4" - strip-json-comments "^1.0.2" + semver "^5.3.0" -tslint@^3.15.1: - version "3.15.1" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-3.15.1.tgz#da165ca93d8fdc2c086b51165ee1bacb48c98ea5" +tslint@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.1.0.tgz#51a47baeeb58956fcd617bd2cf00e2ef0eea2ed9" dependencies: + babel-code-frame "^6.22.0" colors "^1.1.2" - diff "^2.2.1" + diff "^3.2.0" findup-sync "~0.3.0" - glob "^7.0.3" + glob "^7.1.1" optimist "~0.6.0" - resolve "^1.1.7" - underscore.string "^3.3.4" + resolve "^1.3.2" + semver "^5.3.0" + tsutils "^1.4.0" + +tsutils@^1.4.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.6.0.tgz#1fd7fac2a61369ed99cd3997f0fbb437128850f2" tty-browserify@0.0.0: version "0.0.0" @@ -3719,13 +3793,6 @@ unc-path-regex@^0.1.0: version "0.1.2" resolved "http://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" -underscore.string@^3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.4.tgz#2c2a3f9f83e64762fdc45e6ceac65142864213db" - dependencies: - sprintf-js "^1.0.3" - util-deprecate "^1.0.2" - unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -3766,7 +3833,7 @@ useragent@^2.1.6: dependencies: lru-cache "2.2.x" -util-deprecate@^1.0.2, util-deprecate@~1.0.1: +util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From cf4f573a799f4188a06fc6f3ecc59042637989b6 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Tue, 18 Apr 2017 19:41:38 -0400 Subject: [PATCH 10/56] new store structure --- src/flux/capacitor.ts | 6 +-- src/flux/store.ts | 115 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 110 insertions(+), 11 deletions(-) diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index 33a9447..67a7733 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -1,11 +1,11 @@ +import * as EventEmitter from 'eventemitter3'; +import * as redux from 'redux'; +import filterObject = require('filter-object'); import { BrowserBridge } from '../core/bridge'; import { Query, QueryConfiguration } from '../core/query'; import { SelectedRangeRefinement, SelectedValueRefinement, Sort } from '../models/request'; import { Navigation, RefinementResults, Results } from '../models/response'; import { Pager } from './pager'; -import * as EventEmitter from 'eventemitter3'; -import * as redux from 'redux'; -import filterObject = require('filter-object'); export namespace Events { export const COLLECTION_CHANGED = 'collection_changed'; diff --git a/src/flux/store.ts b/src/flux/store.ts index 75dbfd4..062fadd 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -1,19 +1,25 @@ +import * as redux from 'redux'; import { Request } from '../models/request'; import { Results } from '../models/response'; import reducer from './reducer'; -import * as redux from 'redux'; namespace Store { export interface State { data?: { - search?: { - req?: Request; - res?: Results; - }; - // sayt?: {}; - // recommendations?: {}; - // shoppingCart?: {}; + query: Query; + filter: Filter; + sort: Sort[]; + products: Product[]; + collection: Indexed; + navigation: Indexed; + + autocomplete: Autocomplete; + + redirect?: string; + + errors: string[]; // convert from single string? + warnings: string[]; }; ui?: { [tagName: string]: { @@ -22,6 +28,99 @@ namespace Store { }; } + export interface Query { + original: string; + corrected?: string; + related: Query.Related[]; + didYouMean: Query.DidYouMean[]; + rewrites: string[]; + } + + export namespace Query { + export interface Related { + value: string; + url: string; + } + + export interface DidYouMean { + value: string; + url: string; + } + } + + export interface Collection { + name: string; + total: number; + selected?: boolean; + } + + export interface Sort { + field: string; + descending?: boolean; + } + + export interface Product { + id: string; + [key: string]: any; + } + + export type Filter = { field: string; } & ( + { + range: false; + refinements: ValueRefinement[]; + } | { + range: true; + refinements: RangeRefinement[]; + } + ); + + export type Navigation = { + field: string; + label: string; + sort?: Sort; + or?: boolean; + } & ( + { + range: false; + refinements: ValueRefinement[]; + } | { + range: true; + refinements: RangeRefinement[]; + } + ); + + export interface BaseRefinement { + total: number; + selected?: boolean; + } + + export interface ValueRefinement extends BaseRefinement { + value: string; + } + + export interface RangeRefinement extends BaseRefinement { + low: number; + high: number; + } + + export interface Autocomplete { + query: string[]; + categories: Autocomplete.Category[]; + products: any[]; + } + + export namespace Autocomplete { + export interface Category { + field: string[]; + values: string[]; + } + } + + export interface Indexed { + byId: { [key: string]: T }; + allIds: string[]; + } + export function create() { return redux.createStore(reducer); } From 60d2aabc3edcd4038239e618aa5a5b58322ac7c4 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Wed, 19 Apr 2017 10:44:03 -0400 Subject: [PATCH 11/56] observers --- src/flux/capacitor.ts | 15 +++++ src/flux/observer.ts | 16 +++++- src/flux/store.ts | 124 +++++++++++++++++++++++------------------- 3 files changed, 97 insertions(+), 58 deletions(-) diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index 67a7733..e704b57 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -22,6 +22,21 @@ export namespace Events { export const SEARCH = 'search'; export const SORT = 'sort'; + export const QUERY_UPDATED = 'query_updated'; // mixed + export const ORIGINAL_QUERY_UPDATED = 'original_query_updated'; // pre + export const CORRECTED_QUERY_UPDATED = 'corrected_query_updated'; // post + export const RELATED_QUERIES_UPDATED = 'related_queries_updated'; // post + export const DID_YOU_MEANS_UPDATED = 'did_you_means_updated'; // post + export const QUERY_REWRITES_UPDATED = 'query_rewrites_updated'; // post + + export const FILTER_UPDATED = 'filter_updated'; // mixed + + export const SORT_UPDATED = 'sort_updated'; // mixed + + export const PRODUCTS_UPDATED = 'products_updated'; // mixed + + // post-request + // request export const SEARCH_REQ_UPDATED = 'search:req_updated'; export const SEARCH_COLLECTION_UPDATED = 'search:req:collection_updated'; diff --git a/src/flux/observer.ts b/src/flux/observer.ts index 375a8f5..e5e9f23 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -6,7 +6,7 @@ export const DETAIL_QUERY_INDICATOR = 'gbiDetailQuery'; type Observer = (oldState: any, newState: any) => void; namespace Observer { - export type Map = { [key: string]: Observer | Map } + export type Map = { [key: string]: Observer | Map }; export type Node = Map | Observer | (Observer & Map); export function listen(flux: FluxCapacitor) { @@ -39,6 +39,20 @@ namespace Observer { return { data: { + query: Object.assign((_, newQuery) => emit(Events.QUERY_UPDATED, newQuery), { + original: (_, newOriginal) => emit(Events.ORIGINAL_QUERY_UPDATED, newOriginal), + corrected: (_, newCorrected) => emit(Events.CORRECTED_QUERY_UPDATED, newCorrected), + related: (_, newRelated) => emit(Events.RELATED_QUERIES_UPDATED, newRelated), + didYouMeans: (_, newDidYouMeans) => emit(Events.DID_YOU_MEANS_UPDATED, newDidYouMeans), + rewrites: (_, newRewrites) => emit(Events.QUERY_REWRITES_UPDATED, newRewrites), + }), + + filter: (_, newFilter) => emit(Events.FILTER_UPDATED, newFilter), + + sort: (_, newSort) => emit(Events.SORT_UPDATED, newSort), + + products: (_, newProduct) => emit(Events.PRODUCTS_UPDATED, newProduct), + search: { request: Object.assign((_, newRequest) => emit([ Events.SEARCH_REQ_UPDATED, diff --git a/src/flux/store.ts b/src/flux/store.ts index 062fadd..a928a9c 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -7,19 +7,19 @@ namespace Store { export interface State { data?: { - query: Query; - filter: Filter; - sort: Sort[]; - products: Product[]; - collection: Indexed; - navigation: Indexed; + query: Query; // mixed + filter: Filter; // mixed + sort: Sort[]; // pre + products: Product[]; // post + collection: Indexed; // mixed + navigation: Indexed; // mixed - autocomplete: Autocomplete; + autocomplete: Autocomplete; // mixed - redirect?: string; + redirect?: string; // post - errors: string[]; // convert from single string? - warnings: string[]; + errors: string[]; // post + warnings: string[]; // post }; ui?: { [tagName: string]: { @@ -29,29 +29,29 @@ namespace Store { } export interface Query { - original: string; - corrected?: string; - related: Query.Related[]; - didYouMean: Query.DidYouMean[]; - rewrites: string[]; + original: string; // pre + corrected?: string; // post + related: Query.Related[]; // post + didYouMean: Query.DidYouMean[]; // post + rewrites: string[]; // post } export namespace Query { export interface Related { - value: string; - url: string; + value: string; // post + url: string; // post (generated) } export interface DidYouMean { - value: string; - url: string; + value: string; // post + url: string; // post (generated) } } export interface Collection { - name: string; - total: number; - selected?: boolean; + name: string; // static + total: number; // post + selected?: boolean; // pre } export interface Sort { @@ -60,59 +60,69 @@ namespace Store { } export interface Product { - id: string; - [key: string]: any; + id: string; // post + [key: string]: any; // post } - export type Filter = { field: string; } & ( - { - range: false; - refinements: ValueRefinement[]; - } | { - range: true; - refinements: RangeRefinement[]; - } - ); + export type Filter = ValueFilter | RangeFilter; - export type Navigation = { - field: string; - label: string; - sort?: Sort; - or?: boolean; - } & ( - { - range: false; - refinements: ValueRefinement[]; - } | { - range: true; - refinements: RangeRefinement[]; - } - ); + export interface BaseFilter { + field: string; // static + } + + export interface ValueFilter extends BaseFilter { + range?: false; // post + refinements: ValueRefinement[]; // post + } + + export interface RangeFilter extends BaseFilter { + range: true; // post + refinements: RangeRefinement[]; // post + } + + export type Navigation = ValueNavigation | RangeNavigation; + + export interface BaseNavigation { + field: string; // post + label: string; // post + or?: boolean; // post + sort?: Sort; // post + } + + export interface ValueNavigation extends BaseNavigation { + range?: false; // post + refinements: ValueRefinement[]; // post + } + + export interface RangeNavigation extends BaseNavigation { + range: true; // post + refinements: RangeRefinement[]; // post + } export interface BaseRefinement { - total: number; - selected?: boolean; + total: number; // post + selected?: boolean; // pre } export interface ValueRefinement extends BaseRefinement { - value: string; + value: string; // post } export interface RangeRefinement extends BaseRefinement { - low: number; - high: number; + low: number; // post + high: number; // post } export interface Autocomplete { - query: string[]; - categories: Autocomplete.Category[]; - products: any[]; + query: string[]; // post + categories: Indexed; // static & post + products: any[]; // post } export namespace Autocomplete { export interface Category { - field: string[]; - values: string[]; + field: string; // static + values: string[]; // post } } From 83efa1f0b6ff88020fa7f548c5480b1ca74010f5 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Wed, 19 Apr 2017 13:38:41 -0400 Subject: [PATCH 12/56] events --- src/flux/capacitor.ts | 36 +++----------- src/flux/observer.ts | 112 ++++++++++++++---------------------------- src/flux/reducer.ts | 2 +- src/flux/store.ts | 29 +++-------- 4 files changed, 53 insertions(+), 126 deletions(-) diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index e704b57..f8544d0 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -8,20 +8,6 @@ import { Navigation, RefinementResults, Results } from '../models/response'; import { Pager } from './pager'; export namespace Events { - export const COLLECTION_CHANGED = 'collection_changed'; - export const DETAILS = 'details'; - export const ERROR_BRIDGE = 'error:bridge'; - export const PAGE_CHANGED = 'page_changed'; - export const QUERY_CHANGED = 'query_changed'; - export const REDIRECT = 'redirect'; - export const REFINEMENT_RESULTS = 'refinement_results'; - export const REFINEMENTS_CHANGED = 'refinements_changed'; - export const RESET = 'reset'; - export const RESULTS = 'results'; - export const REWRITE_QUERY = 'rewrite_query'; - export const SEARCH = 'search'; - export const SORT = 'sort'; - export const QUERY_UPDATED = 'query_updated'; // mixed export const ORIGINAL_QUERY_UPDATED = 'original_query_updated'; // pre export const CORRECTED_QUERY_UPDATED = 'corrected_query_updated'; // post @@ -29,28 +15,20 @@ export namespace Events { export const DID_YOU_MEANS_UPDATED = 'did_you_means_updated'; // post export const QUERY_REWRITES_UPDATED = 'query_rewrites_updated'; // post - export const FILTER_UPDATED = 'filter_updated'; // mixed - export const SORT_UPDATED = 'sort_updated'; // mixed export const PRODUCTS_UPDATED = 'products_updated'; // mixed - // post-request + export const COLLECTIONS_UPDATED = 'collections_updated'; // mixed + export const COLLECTION_UPDATED = 'collection_updated'; // post - // request - export const SEARCH_REQ_UPDATED = 'search:req_updated'; - export const SEARCH_COLLECTION_UPDATED = 'search:req:collection_updated'; - export const SEARCH_PAGE_UPDATED = 'search:req:page_updated'; - export const SEARCH_QUERY_UPDATED = 'search:req:query_updated'; - export const SEARCH_REFINEMENTS_UPDATED = 'search:req:refinements_updated'; - export const SEARCH_SORT_UPDATED = 'search:req:sort_updated'; + export const NAVIGATIONS_UPDATED = 'navigations_updated'; // post - // response - export const SEARCH_RES_UPDATED = 'search:res_updated'; + export const AUTOCOMPLETE_QUERIES_UPDATED = 'autocomplete_queries_updated'; + export const AUTOCOMPLETE_CATEGORIES_UPDATED = 'autocomplete_categories_updated'; + export const AUTOCOMPLETE_PRODUCTS_UPDATED = 'autocomplete_products_updated'; - // "global" - export const SEARCH_REDIRECT = 'search:res:redirect'; - export const SEARCH_DETAILS = 'search:res:details'; + export const REDIRECT = 'redirect'; } export { Pager }; diff --git a/src/flux/observer.ts b/src/flux/observer.ts index e5e9f23..0b4918e 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -2,11 +2,12 @@ import { rayify } from '../utils'; import { Events, FluxCapacitor } from './capacitor'; export const DETAIL_QUERY_INDICATOR = 'gbiDetailQuery'; +export const INDEXED = Symbol(); type Observer = (oldState: any, newState: any) => void; namespace Observer { - export type Map = { [key: string]: Observer | Map }; + export type Map = { [key: string]: Observer | Map, indexed?: Observer }; export type Node = Map | Observer | (Observer & Map); export function listen(flux: FluxCapacitor) { @@ -23,91 +24,54 @@ namespace Observer { export function resolve(oldState: any, newState: any, observers: Node) { if (oldState !== newState) { + let observerResult; if (typeof observers === 'function') { - observers(oldState, newState); + observerResult = observers(oldState, newState); } - Object.keys(observers) - .forEach((key) => Observer.resolve((oldState || {})[key], (newState || {})[key], observers[key])); + if (oldState.allIds === newState.allIds && INDEXED in observers) { + Object.keys(newState.allIds) + .forEach((key) => observers.indexed(oldState.byId[key], newState.byId[key])) + } else { + Object.keys(observers) + .forEach((key) => Observer.resolve((oldState || {})[key], (newState || {})[key], observers[key])); + } } } export function create(flux: FluxCapacitor) { - function emit(events: string | string[], data: any) { - rayify(events).forEach((event) => flux.emit(event, data)); - } + const emit = (event: string) => (_, newValue) => flux.emit(event, newValue); + const indexed = (event: string, field: string, prefix: string) => + Object.assign(emit(event), { + INDEXED, + indexed: (_, newById) => Object.keys(newById) + }); return { data: { - query: Object.assign((_, newQuery) => emit(Events.QUERY_UPDATED, newQuery), { - original: (_, newOriginal) => emit(Events.ORIGINAL_QUERY_UPDATED, newOriginal), - corrected: (_, newCorrected) => emit(Events.CORRECTED_QUERY_UPDATED, newCorrected), - related: (_, newRelated) => emit(Events.RELATED_QUERIES_UPDATED, newRelated), - didYouMeans: (_, newDidYouMeans) => emit(Events.DID_YOU_MEANS_UPDATED, newDidYouMeans), - rewrites: (_, newRewrites) => emit(Events.QUERY_REWRITES_UPDATED, newRewrites), + query: Object.assign(emit(Events.QUERY_UPDATED), { + original: emit(Events.ORIGINAL_QUERY_UPDATED), + corrected: emit(Events.CORRECTED_QUERY_UPDATED), + related: emit(Events.RELATED_QUERIES_UPDATED), + didYouMeans: emit(Events.DID_YOU_MEANS_UPDATED), + rewrites: emit(Events.QUERY_REWRITES_UPDATED), }), - filter: (_, newFilter) => emit(Events.FILTER_UPDATED, newFilter), - - sort: (_, newSort) => emit(Events.SORT_UPDATED, newSort), - - products: (_, newProduct) => emit(Events.PRODUCTS_UPDATED, newProduct), - - search: { - request: Object.assign((_, newRequest) => emit([ - Events.SEARCH_REQ_UPDATED, - Events.SEARCH - ], newRequest), { - // NOTE: can ONLY be used to switch the "active" page in gb-paging - skip: (_, newPageNumber) => emit([ - Events.SEARCH_PAGE_UPDATED, - Events.PAGE_CHANGED - ], newPageNumber), - collection: (_, newCollection) => emit([ - Events.SEARCH_COLLECTION_UPDATED, - Events.COLLECTION_CHANGED - ], newCollection), - query: (_, newQuery) => emit([ - Events.SEARCH_QUERY_UPDATED, - Events.QUERY_CHANGED, - Events.REWRITE_QUERY - ], newQuery), - // TODO: emitted value will break current implementations - refinements: (_, newRefinements) => emit([ - Events.SEARCH_REFINEMENTS_UPDATED, - Events.REFINEMENTS_CHANGED - ], newRefinements), - sort: (_, newSort) => emit([ - Events.SEARCH_SORT_UPDATED, - Events.SORT - ], newSort) - }), - response: Object.assign((_, newResponse) => { - if (newResponse.redirect) { - emit([ - Events.SEARCH_REDIRECT, - Events.REDIRECT - ], newResponse.redirect); - } else { - // NOTE: REFINEMENT_RESULTS is no longer, should check RESULTS - emit([ - Events.SEARCH_RES_UPDATED, - Events.RESULTS, - Events.RESET - ], newResponse); - - // NOTE: make sure to add the indicator when making the request - const isDetailQuery = (newResponse.originalQuery.customUrlParams || []) - .find(({ key }) => key === DETAIL_QUERY_INDICATOR); - if (isDetailQuery) { - emit([ - Events.SEARCH_DETAILS, - Events.DETAILS - ], newResponse.records[0]); - } - } - }) - } + sorts: emit(Events.SORT_UPDATED), // come up with approach for indexed + + products: emit(Events.PRODUCTS_UPDATED), + + collections: emit(Events.COLLECTIONS_UPDATED), + + navigations: emit(Events.NAVIGATIONS_UPDATED), + + autocomplete: { + queries: emit(Events.AUTOCOMPLETE_QUERIES_UPDATED), + categories: emit(Events.AUTOCOMPLETE_CATEGORIES_UPDATED), + products: emit(Events.AUTOCOMPLETE_PRODUCTS_UPDATED) + }, + + reditect: emit(Events.REDIRECT), } }; } diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts index 0ffda69..0509d0d 100644 --- a/src/flux/reducer.ts +++ b/src/flux/reducer.ts @@ -1,5 +1,5 @@ -import { UPDATE_SEARCH_REQUEST } from './actions'; import * as redux from 'redux'; +import { UPDATE_SEARCH_REQUEST } from './actions'; export namespace Search { export const request = (state, action) => { diff --git a/src/flux/store.ts b/src/flux/store.ts index a928a9c..7dc8d70 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -8,11 +8,11 @@ namespace Store { export interface State { data?: { query: Query; // mixed - filter: Filter; // mixed - sort: Sort[]; // pre - products: Product[]; // post - collection: Indexed; // mixed - navigation: Indexed; // mixed + + sorts: Indexed; // pre + products: Indexed; // post + collections: Indexed; // mixed + navigations: Indexed; // mixed autocomplete: Autocomplete; // mixed @@ -50,6 +50,7 @@ namespace Store { export interface Collection { name: string; // static + label: string; // static total: number; // post selected?: boolean; // pre } @@ -64,22 +65,6 @@ namespace Store { [key: string]: any; // post } - export type Filter = ValueFilter | RangeFilter; - - export interface BaseFilter { - field: string; // static - } - - export interface ValueFilter extends BaseFilter { - range?: false; // post - refinements: ValueRefinement[]; // post - } - - export interface RangeFilter extends BaseFilter { - range: true; // post - refinements: RangeRefinement[]; // post - } - export type Navigation = ValueNavigation | RangeNavigation; export interface BaseNavigation { @@ -114,7 +99,7 @@ namespace Store { } export interface Autocomplete { - query: string[]; // post + queries: string[]; // post categories: Indexed; // static & post products: any[]; // post } From 0ab10f277b4005c21f51c6b234f42fad534481e0 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Thu, 20 Apr 2017 15:31:42 -0400 Subject: [PATCH 13/56] add actions --- package.json | 3 ++- src/flux/actions.ts | 36 +++++++++++++++++++++++++++++++++++- src/flux/capacitor.ts | 38 +++++++++++++++++++++++--------------- src/flux/observer.ts | 43 +++++++++++++++++++++++++++---------------- src/flux/store.ts | 8 +++++++- yarn.lock | 8 ++++++-- 6 files changed, 100 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index fe2d1d0..08c6e34 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "filter-object": "^2.1.0", "lodash.range": "^3.2.0", "qs": "^6.1.0", - "redux": "^3.6.0" + "redux": "^3.6.0", + "redux-thunk": "^2.2.0" } } diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 288387c..ff2d9a1 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -1 +1,35 @@ -export const UPDATE_SEARCH_REQUEST = 'search:req:update'; +import { Dispatch } from 'redux'; +import 'redux-thunk'; +import { Request } from '../models/request'; +import Store from './store'; + +export const UPDATE_SEARCH_REQUEST = 'UPDATE_SEARCH_REQUEST'; +export const UPDATE_QUERY = 'UPDATE_QUERY'; +export const ADD_REFINEMENT = 'ADD_REFINEMENT'; +export const REMOVE_REFINEMENT = 'REMOVE_REFINEMENT'; + +export const conditional = (condition: boolean, action: () => Promise) => + condition ? action() : Promise.resolve(); + +export function search(request: Request) { + return (dispatch: Dispatch, getState: () => Store.State) => { + const data = getState().data; + + // conditional(data.query.original !== request.query, () => updateQuery(request.query)) + // .then(() => conditional()) + // + // return dispatch({ + // type: UPDATE_SEARCH_REQUEST, + // request + // }); + }; +} + +export const updateQuery = (query: string) => + (dispatch: Dispatch) => dispatch({ type: UPDATE_QUERY, query }); + +export const addRefinement = (refinement: Store.Refinement) => + (dispatch: Dispatch) => dispatch, any>({ type: ADD_REFINEMENT, refinement }); + +export const removeRefinement = (refinement: Store.Refinement) => + (dispatch: Dispatch) => dispatch({ type: REMOVE_REFINEMENT, refinement }); diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index f8544d0..c2dfa65 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -15,17 +15,21 @@ export namespace Events { export const DID_YOU_MEANS_UPDATED = 'did_you_means_updated'; // post export const QUERY_REWRITES_UPDATED = 'query_rewrites_updated'; // post + export const SORTS_UPDATED = 'sorts_updated'; // mixed export const SORT_UPDATED = 'sort_updated'; // mixed export const PRODUCTS_UPDATED = 'products_updated'; // mixed + export const PRODUCT_UPDATED = 'product_updated'; // mixed export const COLLECTIONS_UPDATED = 'collections_updated'; // mixed export const COLLECTION_UPDATED = 'collection_updated'; // post export const NAVIGATIONS_UPDATED = 'navigations_updated'; // post + export const NAVIGATION_UPDATED = 'navigation_updated'; // post export const AUTOCOMPLETE_QUERIES_UPDATED = 'autocomplete_queries_updated'; export const AUTOCOMPLETE_CATEGORIES_UPDATED = 'autocomplete_categories_updated'; + export const AUTOCOMPLETE_CATEGORY_UPDATED = 'autocomplete_category_updated'; export const AUTOCOMPLETE_PRODUCTS_UPDATED = 'autocomplete_products_updated'; export const REDIRECT = 'redirect'; @@ -70,24 +74,28 @@ export class FluxCapacitor extends EventEmitter { this.page = new Pager(this); } - search(originalQuery: string = this.originalQuery): Promise { - this.query.withQuery(originalQuery); - this.emit(Events.SEARCH, this.query.raw); - return this.bridge.search(this.query) - .then((results) => { - const oldQuery = this.originalQuery; - Object.assign(this, { results, originalQuery }); - - if (results.redirect) { - this.emit(Events.REDIRECT, results.redirect); - } - this.emit(Events.RESULTS, results); - this.emitQueryChanged(oldQuery, originalQuery); + search(query: string = this.originalQuery) { - return results; - }); } + // search(originalQuery: string = this.originalQuery): Promise { + // this.query.withQuery(originalQuery); + // this.emit(Events.SEARCH, this.query.raw); + // return this.bridge.search(this.query) + // .then((results) => { + // const oldQuery = this.originalQuery; + // Object.assign(this, { results, originalQuery }); + // + // if (results.redirect) { + // this.emit(Events.REDIRECT, results.redirect); + // } + // this.emit(Events.RESULTS, results); + // this.emitQueryChanged(oldQuery, originalQuery); + // + // return results; + // }); + // } + refinements(navigationName: string): Promise { return this.bridge.refinements(this.query, navigationName) .then((results) => { diff --git a/src/flux/observer.ts b/src/flux/observer.ts index 0b4918e..8276924 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -7,7 +7,7 @@ export const INDEXED = Symbol(); type Observer = (oldState: any, newState: any) => void; namespace Observer { - export type Map = { [key: string]: Observer | Map, indexed?: Observer }; + export type Map = { [key: string]: Observer | Map }; export type Node = Map | Observer | (Observer & Map); export function listen(flux: FluxCapacitor) { @@ -22,29 +22,40 @@ namespace Observer { }; } - export function resolve(oldState: any, newState: any, observers: Node) { + export function shouldObserve(oldState: any, newState: any, observer: Node): observer is Observer { + // double check this logic + return typeof observer === 'function' + && !(INDEXED in observer && oldState.allIds === newState.allIds); + } + + export function resolve(oldState: any, newState: any, observer: Node) { if (oldState !== newState) { - let observerResult; - if (typeof observers === 'function') { - observerResult = observers(oldState, newState); + if (Observer.shouldObserve(oldState, newState, observer)) { + observer(oldState, newState); } - if (oldState.allIds === newState.allIds && INDEXED in observers) { + if (INDEXED in observer && oldState.allIds === newState.allIds) { Object.keys(newState.allIds) - .forEach((key) => observers.indexed(oldState.byId[key], newState.byId[key])) + .forEach((key) => Observer.resolveIndexed(oldState.byId[key], newState.byId[key], observer['indexed'])); } else { - Object.keys(observers) - .forEach((key) => Observer.resolve((oldState || {})[key], (newState || {})[key], observers[key])); + Object.keys(observer) + .forEach((key) => Observer.resolve((oldState || {})[key], (newState || {})[key], observer[key])); } } } + export function resolveIndexed(oldState: any, newState: any, observer: Observer) { + if (oldState !== newState) { + observer(oldState, newState); + } + } + export function create(flux: FluxCapacitor) { const emit = (event: string) => (_, newValue) => flux.emit(event, newValue); - const indexed = (event: string, field: string, prefix: string) => + const indexed = (event: string, prefix: string, field: string) => Object.assign(emit(event), { INDEXED, - indexed: (_, newById) => Object.keys(newById) + indexed: (_, newIndexed) => flux.emit(`${prefix}:${newIndexed[field]}`, newIndexed) }); return { @@ -57,17 +68,17 @@ namespace Observer { rewrites: emit(Events.QUERY_REWRITES_UPDATED), }), - sorts: emit(Events.SORT_UPDATED), // come up with approach for indexed + sorts: indexed(Events.SORTS_UPDATED, Events.SORT_UPDATED, 'field'), - products: emit(Events.PRODUCTS_UPDATED), + products: indexed(Events.PRODUCTS_UPDATED, Events.PRODUCT_UPDATED, 'id'), - collections: emit(Events.COLLECTIONS_UPDATED), + collections: indexed(Events.COLLECTIONS_UPDATED, Events.COLLECTION_UPDATED, 'name'), - navigations: emit(Events.NAVIGATIONS_UPDATED), + navigations: indexed(Events.NAVIGATIONS_UPDATED, Events.NAVIGATION_UPDATED, 'field'), autocomplete: { queries: emit(Events.AUTOCOMPLETE_QUERIES_UPDATED), - categories: emit(Events.AUTOCOMPLETE_CATEGORIES_UPDATED), + categories: indexed(Events.AUTOCOMPLETE_CATEGORIES_UPDATED, Events.AUTOCOMPLETE_CATEGORY_UPDATED, 'field'), products: emit(Events.AUTOCOMPLETE_PRODUCTS_UPDATED) }, diff --git a/src/flux/store.ts b/src/flux/store.ts index 7dc8d70..39bcca7 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -1,4 +1,5 @@ import * as redux from 'redux'; +import thunk from 'redux-thunk'; import { Request } from '../models/request'; import { Results } from '../models/response'; import reducer from './reducer'; @@ -89,6 +90,8 @@ namespace Store { selected?: boolean; // pre } + export type Refinement = ValueRefinement | RangeRefinement; + export interface ValueRefinement extends BaseRefinement { value: string; // post } @@ -117,7 +120,10 @@ namespace Store { } export function create() { - return redux.createStore(reducer); + return redux.createStore( + reducer, + redux.applyMiddleware(thunk) + ); } } diff --git a/yarn.lock b/yarn.lock index 92b8ae7..16058db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3033,6 +3033,10 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +redux-thunk@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5" + redux@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d" @@ -3137,11 +3141,11 @@ requires-port@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" -resolve@1.1.x, resolve@^1.1.6: +resolve@1.1.x: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.2.tgz#1f0442c9e0cbb8136e87b9305f932f46c7f28235" dependencies: From 6d47fcc8e064e2c13da9605a067a9b9268c06d02 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Thu, 20 Apr 2017 16:45:52 -0400 Subject: [PATCH 14/56] more action tests --- package.json | 17 +- src/flux/actions.ts | 60 +++-- src/polyfills.ts | 1 + test/unit/_suite.ts | 9 +- test/unit/flux/actions.ts | 60 +++++ yarn.lock | 528 ++++++++++++++++++++++---------------- 6 files changed, 417 insertions(+), 258 deletions(-) create mode 100644 test/unit/flux/actions.ts diff --git a/package.json b/package.json index 08c6e34..8fc0461 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ }, "homepage": "https://github.com/groupby/api-javascript#readme", "devDependencies": { - "@types/node": "^7.0.4", - "awesome-typescript-loader": "^2.2.4", + "@types/node": "^7.0.13", + "awesome-typescript-loader": "^3.1.2", "chai": "^3.2.0", "codacy-coverage": "^2.0.0", "gb-license-check": "^1.0.1", @@ -67,18 +67,18 @@ "tslint": "^5.1.0", "tslint-eslint-rules": "^4.0.0", "tslint-loader": "^3.5.2", - "typedoc": "^0.4.5", - "typescript": "2.0.2", - "typings": "^1.3.3", + "typedoc": "^0.5.10", + "typescript": "^2.2.2", + "typings": "^2.1.1", "webpack": "^1.13.2", "xhr-mock": "^1.6.0" }, "dependencies": { "@types/axios": "^0.9.35", "@types/clone": "^0.1.30", - "@types/deep-equal": "^0.0.30", - "@types/eventemitter3": "^1.2.0", - "@types/qs": "^6.2.30", + "@types/deep-equal": "^1.0.0", + "@types/eventemitter3": "^2.0.2", + "@types/qs": "^6.2.31", "array.prototype.find": "^2.0.0", "array.prototype.findindex": "^2.0.0", "axios": "^0.12.0", @@ -86,6 +86,7 @@ "deep-equal": "^1.0.1", "es6-object-assign": "^1.0.3", "es6-promise": "^3.3.1", + "es6-symbol": "^3.1.1", "eventemitter3": "^1.2.0", "filter-object": "^2.1.0", "lodash.range": "^3.2.0", diff --git a/src/flux/actions.ts b/src/flux/actions.ts index ff2d9a1..4135d10 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -1,35 +1,39 @@ import { Dispatch } from 'redux'; -import 'redux-thunk'; import { Request } from '../models/request'; import Store from './store'; -export const UPDATE_SEARCH_REQUEST = 'UPDATE_SEARCH_REQUEST'; -export const UPDATE_QUERY = 'UPDATE_QUERY'; -export const ADD_REFINEMENT = 'ADD_REFINEMENT'; -export const REMOVE_REFINEMENT = 'REMOVE_REFINEMENT'; - -export const conditional = (condition: boolean, action: () => Promise) => - condition ? action() : Promise.resolve(); - -export function search(request: Request) { - return (dispatch: Dispatch, getState: () => Store.State) => { - const data = getState().data; - - // conditional(data.query.original !== request.query, () => updateQuery(request.query)) - // .then(() => conditional()) - // - // return dispatch({ - // type: UPDATE_SEARCH_REQUEST, - // request - // }); - }; -} +namespace Actions { + export const UPDATE_SEARCH_REQUEST = 'UPDATE_SEARCH_REQUEST'; + export const UPDATE_QUERY = 'UPDATE_QUERY'; + export const ADD_REFINEMENT = 'ADD_REFINEMENT'; + export const REMOVE_REFINEMENT = 'REMOVE_REFINEMENT'; + + export function search(request: Request) { + return (dispatch: Dispatch, getState: () => Store.State) => { + const data = getState().data; + + conditional(data.query.original !== request.query, () => updateQuery(request.query)) + .then(() => conditional()); + + return dispatch({ + type: UPDATE_SEARCH_REQUEST, + request + }); + }; + } + + export const updateQuery = (query: string) => Actions.thunk(UPDATE_QUERY, { query }); -export const updateQuery = (query: string) => - (dispatch: Dispatch) => dispatch({ type: UPDATE_QUERY, query }); + export const addRefinement = (refinement: Store.Refinement) => Actions.thunk(ADD_REFINEMENT, { refinement }); -export const addRefinement = (refinement: Store.Refinement) => - (dispatch: Dispatch) => dispatch, any>({ type: ADD_REFINEMENT, refinement }); + export const removeRefinement = (refinement: Store.Refinement) => Actions.thunk(REMOVE_REFINEMENT, { refinement }); + + // utilities + + export const conditional = (condition: boolean, action: Function) => + condition ? action() : Promise.resolve(); + + export const thunk = (type: string, data: any) => (dispatch) => dispatch({ type, ...data }); +} -export const removeRefinement = (refinement: Store.Refinement) => - (dispatch: Dispatch) => dispatch({ type: REMOVE_REFINEMENT, refinement }); +export default Actions; diff --git a/src/polyfills.ts b/src/polyfills.ts index 9e24282..baa69a2 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,3 +1,4 @@ +import 'es6-symbol/implement'; require('array.prototype.find').shim(); require('array.prototype.findindex').shim(); require('es6-object-assign').polyfill(); diff --git a/test/unit/_suite.ts b/test/unit/_suite.ts index 606121f..449eee6 100644 --- a/test/unit/_suite.ts +++ b/test/unit/_suite.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import * as suite from 'mocha-suite'; -export default <(description: string, tests: (utils: Utils) => void) => void>suite((tests) => { +export default suite((tests) => { let sandbox: Sinon.SinonSandbox; beforeEach(() => sandbox = sinon.sandbox.create()); @@ -19,3 +19,10 @@ export interface Utils { spy: Sinon.SinonSpyStatic; stub: Sinon.SinonSpyStatic; } + +export type Suite = UtilsSuite & { + only: UtilsSuite; + skip: UtilsSuite; +}; + +export type UtilsSuite = (description: string, tests: (utils: Utils) => void) => void; diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts new file mode 100644 index 0000000..b8bd8c3 --- /dev/null +++ b/test/unit/flux/actions.ts @@ -0,0 +1,60 @@ +import Actions from '../../../src/flux/actions'; +import suite from '../_suite'; + +suite.only('actions', ({ expect, stub }) => { + describe('conditional()', () => { + it('should return result of action', () => { + const obj = { a: 'b' }; + + const result = Actions.conditional(true, () => obj); + + expect(result).to.eq(obj); + }); + + it('should return a resolved Promise', () => { + const obj = { a: 'b' }; + + const result = Actions.conditional(false, () => obj); + + expect(result).to.not.eq(obj); + expect(result).to.be.an.instanceof(Promise); + }); + }); + + describe('thunk()', () => { + + }); + + describe('updateQuery()', () => { + it('should create an UPDATE_QUERY action', () => { + const query = 'red apple'; + const thunk = stub(Actions, 'thunk'); + + Actions.updateQuery(query); + + expect(thunk).to.be.calledWith(Actions.UPDATE_QUERY, { query }); + }); + }); + + describe('addRefinement()', () => { + it('should create an ADD_REFINEMENT action', () => { + const refinement: any = { a: 'b' }; + const thunk = stub(Actions, 'thunk'); + + Actions.addRefinement(refinement); + + expect(thunk).to.be.calledWith(Actions.ADD_REFINEMENT, { refinement }); + }); + }); + + describe('removeRefinement()', () => { + it('should create a REMOVE_REFINEMENT action', () => { + const refinement: any = { a: 'b' }; + const thunk = stub(Actions, 'thunk'); + + Actions.removeRefinement(refinement); + + expect(thunk).to.be.calledWith(Actions.REMOVE_REFINEMENT, { refinement }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 16058db..b072a4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,25 +10,59 @@ version "0.1.30" resolved "https://registry.yarnpkg.com/@types/clone/-/clone-0.1.30.tgz#e7365648c1b42136a59c7d5040637b3b5c83b614" -"@types/deep-equal@^0.0.30": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-0.0.30.tgz#c14df825c9201a1804980e7ebc3ef30e137bb64c" +"@types/deep-equal@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.0.tgz#9ebeaa73d1fc4791f038a5f1440e0449ea968495" -"@types/eventemitter3@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/eventemitter3/-/eventemitter3-1.2.0.tgz#4ebc1b9c6e92417c8e5c49204bb48189f7463dca" +"@types/eventemitter3@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/eventemitter3/-/eventemitter3-2.0.2.tgz#94b57c2568c4f09479d64812f625317b12a6edd0" + dependencies: + eventemitter3 "*" + +"@types/fs-extra@0.0.33": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-0.0.33.tgz#a8719c417b080c012d3497b28e228ac09745fdf2" + dependencies: + "@types/node" "*" + +"@types/handlebars@^4.0.31": + version "4.0.32" + resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.32.tgz#637e8d945a9354aab47df7125005490fe9f8e592" + +"@types/highlight.js@^9.1.8": + version "9.1.9" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.1.9.tgz#ed6336955eaf233b75eb7923b9b1f373d045ef01" + +"@types/lodash@^4.14.37": + version "4.14.62" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.62.tgz#8674f9861582148a60b7a89cb260f11378d11683" + +"@types/marked@0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.0.28.tgz#44ba754e9fa51432583e8eb30a7c4dd249b52faa" + +"@types/minimatch@^2.0.29": + version "2.0.29" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" "@types/mocha@^2.2.40": version "2.2.40" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.40.tgz#9811dd800ece544cd84b5b859917bf584a150c4c" -"@types/node@^7.0.4": - version "7.0.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.4.tgz#9aabc135979ded383325749f508894c662948c8b" +"@types/node@*", "@types/node@^7.0.13": + version "7.0.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.13.tgz#1b0a53fe9ef9c3a5d061b126cc9b915bca43a3f5" -"@types/qs@^6.2.30": - version "6.2.30" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.2.30.tgz#139e247511ee3b9be539cdd553c8fdb33c1f624e" +"@types/qs@^6.2.31": + version "6.2.31" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.2.31.tgz#7d929bd877f9cd3ece6415c602b7cf9b077133f1" + +"@types/shelljs@^0.3.32": + version "0.3.33" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.3.33.tgz#df613bddb88225ed09ce5c835f620dcaaf155e6b" + dependencies: + "@types/node" "*" abbrev@1, abbrev@1.0.x: version "1.0.9" @@ -224,16 +258,17 @@ asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" -awesome-typescript-loader@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/awesome-typescript-loader/-/awesome-typescript-loader-2.2.4.tgz#4185d60c035c25515f9c2a747fa5f69b2a001e9e" +awesome-typescript-loader@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/awesome-typescript-loader/-/awesome-typescript-loader-3.1.2.tgz#3df192b91a6285f795ca65e63aad114fbb44f710" dependencies: colors "^1.1.2" - enhanced-resolve "^2.2.2" - loader-utils "^0.2.6" - lodash "^4.13.1" - object-assign "^4.1.0" - source-map-support "^0.4.0" + enhanced-resolve "^3.1.0" + loader-utils "^1.0.2" + lodash "^4.17.4" + mkdirp "^0.5.1" + object-assign "^4.1.1" + source-map-support "^0.4.11" aws-sign2@~0.6.0: version "0.6.0" @@ -344,18 +379,16 @@ boom@2.x.x: dependencies: hoek "2.x.x" -boxen@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-0.6.0.tgz#8364d4248ac34ff0ef1b2f2bf49a6c60ce0d81b6" +boxen@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.0.0.tgz#b2694baf1f605f708ff0177c12193b22f29aaaab" dependencies: ansi-align "^1.1.0" - camelcase "^2.1.0" + camelcase "^4.0.0" chalk "^1.1.1" cli-boxes "^1.0.0" - filled-array "^1.0.0" - object-assign "^4.0.1" - repeating "^2.0.0" - string-width "^1.0.1" + string-width "^2.0.0" + term-size "^0.1.0" widest-line "^1.0.0" brace-expansion@^1.0.0: @@ -430,10 +463,14 @@ camelcase@^1.0.2: version "1.2.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" -camelcase@^2.0.0, camelcase@^2.1.0: +camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" +camelcase@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + capture-stack-trace@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" @@ -506,12 +543,12 @@ cli-cursor@^1.0.2: dependencies: restore-cursor "^1.0.1" -cli-truncate@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" +cli-truncate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-1.0.0.tgz#21eb91f47b3f6560f004db77a769b4668d9c5518" dependencies: slice-ansi "0.0.4" - string-width "^1.0.1" + string-width "^2.0.0" cliui@^2.1.0: version "2.1.0" @@ -603,19 +640,16 @@ concat-stream@1.5.0, concat-stream@^1.4.7: readable-stream "~2.0.0" typedarray "~0.0.5" -configstore@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-2.1.0.tgz#737a3a7036e9886102aa6099e47bb33ab1aba1a1" +configstore@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.0.0.tgz#e1b8669c1803ccc50b545e92f8e6e79aa80e0196" dependencies: - dot-prop "^3.0.0" + dot-prop "^4.1.0" graceful-fs "^4.1.2" mkdirp "^0.5.0" - object-assign "^4.0.1" - os-tmpdir "^1.0.0" - osenv "^0.1.0" - uuid "^2.0.1" + unique-string "^1.0.0" write-file-atomic "^1.1.2" - xdg-basedir "^2.0.0" + xdg-basedir "^3.0.0" connect@^3.3.5: version "3.5.0" @@ -656,12 +690,19 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" -create-error-class@^3.0.1: +create-error-class@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" dependencies: capture-stack-trace "^1.0.0" +cross-spawn-async@^2.1.1: + version "2.2.5" + resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc" + dependencies: + lru-cache "^4.0.0" + which "^1.2.8" + cross-spawn@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.0.1.tgz#a3bbb302db2297cbea3c04edf36941f4613aa399" @@ -685,6 +726,10 @@ crypto-browserify@3.3.0: ripemd160 "0.2.0" sha.js "2.2.6" +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -695,6 +740,12 @@ custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -785,11 +836,9 @@ depd@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - dependencies: - repeating "^2.0.0" +detect-indent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" dezalgo@^1.0.0: version "1.0.3" @@ -834,9 +883,9 @@ domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" -dot-prop@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" +dot-prop@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.1.1.tgz#a8493f0b7b5eeec82525b5c7587fa7de7ca859c1" dependencies: is-obj "^1.0.0" @@ -846,11 +895,9 @@ duplexer2@0.0.2: dependencies: readable-stream "~1.1.9" -duplexer2@^0.1.4: +duplexer3@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" - dependencies: - readable-stream "^2.0.2" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" ecc-jsbn@~0.1.1: version "0.1.1" @@ -909,14 +956,14 @@ engine.io@1.8.2: engine.io-parser "1.3.2" ws "1.1.1" -enhanced-resolve@^2.2.2: - version "2.3.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-2.3.0.tgz#a115c32504b6302e85a76269d7a57ccdd962e359" +enhanced-resolve@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz#9f4b626f577245edcf4b2ad83d86e17f4f421dec" dependencies: graceful-fs "^4.1.2" - memory-fs "^0.3.0" + memory-fs "^0.4.0" object-assign "^4.0.1" - tapable "^0.2.3" + tapable "^0.2.5" enhanced-resolve@~0.9.0: version "0.9.1" @@ -959,6 +1006,21 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.1" +es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.15" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.15.tgz#c330a5934c1ee21284a7c081a86e5fd937c91ea6" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + +es6-iterator@2: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-symbol "^3.1" + es6-object-assign@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.0.3.tgz#40a192e0fda5ee44ee8cf6f5b5d9b47cd0f69b14" @@ -971,6 +1033,13 @@ es6-promise@~4.0.3: version "4.0.5" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42" +es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -1006,7 +1075,7 @@ esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" -eventemitter3@1.x.x, eventemitter3@^1.2.0: +eventemitter3@*, eventemitter3@1.x.x, eventemitter3@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" @@ -1014,6 +1083,17 @@ events@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" +execa@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.4.0.tgz#4eb6467a36a095fabb2970ff9d5e3fb7bce6ebc3" + dependencies: + cross-spawn-async "^2.1.1" + is-stream "^1.1.0" + npm-run-path "^1.0.0" + object-assign "^4.0.1" + path-key "^1.0.0" + strip-eof "^1.0.0" + exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" @@ -1112,10 +1192,6 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" -filled-array@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84" - filter-keys@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/filter-keys/-/filter-keys-1.1.0.tgz#e3851541c924695646f8c1fc4dcac91193b2e77b" @@ -1205,15 +1281,12 @@ formatio@1.1.1: dependencies: samsam "~1.1" -fs-extra@^0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" +fs-extra@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-2.1.2.tgz#046c70163cef9aad46b0e4a7fa467fb22d71de35" dependencies: graceful-fs "^4.1.2" jsonfile "^2.1.0" - klaw "^1.0.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" fs-extra@~1.0.0: version "1.0.0" @@ -1291,6 +1364,10 @@ get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + getpass@^0.1.1: version "0.1.6" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" @@ -1361,27 +1438,23 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" -got@^5.0.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35" +got@^6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" dependencies: - create-error-class "^3.0.1" - duplexer2 "^0.1.4" + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" is-redirect "^1.0.0" is-retry-allowed "^1.0.0" is-stream "^1.0.0" lowercase-keys "^1.0.0" - node-status-codes "^1.0.0" - object-assign "^4.0.1" - parse-json "^2.1.0" - pinkie-promise "^2.0.0" - read-all-stream "^3.0.0" - readable-stream "^2.0.5" - timed-out "^3.0.0" - unzip-response "^1.0.2" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" url-parse-lax "^1.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -1689,6 +1762,10 @@ is-fullwidth-code-point@^1.0.0: dependencies: number-is-nan "^1.0.0" +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + is-glob@^2.0.0, is-glob@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" @@ -1765,7 +1842,7 @@ is-retry-allowed@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" -is-stream@^1.0.0, is-stream@^1.0.1: +is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -1932,6 +2009,20 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" +jspm-config@^0.3.0: + version "0.3.4" + resolved "https://registry.yarnpkg.com/jspm-config/-/jspm-config-0.3.4.tgz#44c26902e4ae8ece2366cedc9ff16b10a5f391c6" + dependencies: + any-promise "^1.3.0" + graceful-fs "^4.1.4" + make-error-cause "^1.2.1" + object.pick "^1.1.2" + parse-json "^2.2.0" + strip-bom "^3.0.0" + thenify "^3.2.0" + throat "^3.0.0" + xtend "^4.0.1" + jsprim@^1.2.2: version "1.3.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" @@ -2041,19 +2132,19 @@ klaw@^1.0.0: optionalDependencies: graceful-fs "^4.1.9" -latest-version@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-2.0.0.tgz#56f8d6139620847b8017f8f1f4d78e211324168b" +latest-version@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" dependencies: - package-json "^2.0.0" + package-json "^4.0.0" lazy-cache@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" -lazy-req@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/lazy-req/-/lazy-req-1.1.0.tgz#bdaebead30f8d824039ce0ce149d4daa07ba1fac" +lazy-req@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lazy-req/-/lazy-req-2.0.0.tgz#c9450a363ecdda2e6f0c70132ad4f37f8f06f2b4" lcov-parse@0.x: version "0.0.10" @@ -2092,7 +2183,7 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -loader-utils@0.x.x, loader-utils@^0.2.11, loader-utils@^0.2.5, loader-utils@^0.2.6: +loader-utils@0.x.x, loader-utils@^0.2.11, loader-utils@^0.2.5: version "0.2.16" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" dependencies: @@ -2208,7 +2299,7 @@ lodash@^3.10.0, lodash@^3.8.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.1, lodash@^4.13.1, lodash@^4.16.3, lodash@^4.2.1: +lodash@^4.0.1, lodash@^4.13.1, lodash@^4.16.3, lodash@^4.17.4, lodash@^4.2.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -2263,7 +2354,7 @@ lru-cache@2, lru-cache@2.2.x: version "2.2.4" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" -lru-cache@^4.0.1: +lru-cache@^4.0.0, lru-cache@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" dependencies: @@ -2296,16 +2387,16 @@ memory-fs@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" -memory-fs@^0.3.0, memory-fs@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.3.0.tgz#7bcc6b629e3a43e871d7e29aca6ae8a7f15cbb20" +memory-fs@^0.4.0, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" dependencies: errno "^0.1.3" readable-stream "^2.0.1" -memory-fs@~0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" +memory-fs@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.3.0.tgz#7bcc6b629e3a43e871d7e29aca6ae8a7f15cbb20" dependencies: errno "^0.1.3" readable-stream "^2.0.1" @@ -2499,10 +2590,6 @@ node-pre-gyp@^0.6.29: tar "~2.2.1" tar-pack "~3.3.0" -node-status-codes@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" - nopt@3.x, nopt@~3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -2538,6 +2625,12 @@ normalize-path@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" +npm-run-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-1.0.0.tgz#f5c32bf595fe81ae927daec52e82f8b000ac3c8f" + dependencies: + path-key "^1.0.0" + npmlog@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f" @@ -2582,7 +2675,7 @@ object.omit@^2.0.0: for-own "^0.1.4" is-extendable "^0.1.1" -object.pick@^1.1.1: +object.pick@^1.1.1, object.pick@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.2.0.tgz#b5392bee9782da6d9fb7d6afaf539779f1234c2b" dependencies: @@ -2636,30 +2729,15 @@ os-browserify@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - os-shim@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" -os-tmpdir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - -osenv@^0.1.0: - version "0.1.4" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -package-json@^2.0.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb" +package-json@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" dependencies: - got "^5.0.0" + got "^6.7.1" registry-auth-token "^3.0.1" registry-url "^3.0.3" semver "^5.1.0" @@ -2677,7 +2755,7 @@ parse-glob@^3.0.4: is-extglob "^1.0.0" is-glob "^2.0.0" -parse-json@^2.1.0, parse-json@^2.2.0: +parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" dependencies: @@ -2719,6 +2797,10 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" +path-key@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-1.0.0.tgz#5d53d578019646c0d68800db4e146e6bdc2ac7af" + path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" @@ -2789,18 +2871,14 @@ popsicle-status@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/popsicle-status/-/popsicle-status-2.0.0.tgz#54e12722376efba0a353abdf53cbf1ce0e852efa" -popsicle@^8.0.2: - version "8.2.0" - resolved "https://registry.yarnpkg.com/popsicle/-/popsicle-8.2.0.tgz#ff4401005cab43a9418a91410611c00197712d21" +popsicle@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/popsicle/-/popsicle-9.1.0.tgz#4f900f38d57a574ec170eda40496e364082bff66" dependencies: - any-promise "^1.3.0" - arrify "^1.0.0" concat-stream "^1.4.7" form-data "^2.0.0" make-error-cause "^1.2.1" - throwback "^1.1.0" tough-cookie "^2.0.0" - xtend "^4.0.0" pre-commit@^1.1.3: version "1.2.2" @@ -2838,11 +2916,9 @@ progress@^1.1.8, progress@~1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" -promise-finally@^2.0.1, promise-finally@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/promise-finally/-/promise-finally-2.2.1.tgz#22616c4ba902916e988bd46c54d7caa08910cd77" - dependencies: - any-promise "^1.3.0" +promise-finally@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/promise-finally/-/promise-finally-3.0.0.tgz#ddd5d0f895432b1206ceb8da1275064d18e7aa23" prr@~0.0.0: version "0.0.0" @@ -2904,13 +2980,6 @@ rc@^1.0.1, rc@^1.1.5, rc@^1.1.6, rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~1.0.4" -read-all-stream@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" - dependencies: - pinkie-promise "^2.0.0" - readable-stream "^2.0.0" - read-installed@~4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067" @@ -2949,7 +3018,7 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.1.0: +"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.1.0: version "2.2.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" dependencies: @@ -3164,7 +3233,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.3.3, rimraf@^2.4.4, rimraf@^2.5.4, rimraf@~2.5.1, rimraf@~2.5.4: +rimraf@2, rimraf@^2.3.3, rimraf@^2.4.4, rimraf@^2.5.4, rimraf@~2.5.1, rimraf@~2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: @@ -3174,6 +3243,10 @@ ripemd160@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-0.2.0.tgz#2bf198bde167cacfa51c0a928e84b68bbe171fce" +safe-buffer@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" + samsam@1.1.2, samsam@~1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" @@ -3323,7 +3396,7 @@ source-list-map@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" -source-map-support@^0.4.0, source-map-support@^0.4.1: +source-map-support@^0.4.1, source-map-support@^0.4.11: version "0.4.11" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.11.tgz#647f939978b38535909530885303daf23279f322" dependencies: @@ -3440,6 +3513,13 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +string-width@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^3.0.0" + string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -3466,6 +3546,14 @@ strip-bom@^2.0.0: dependencies: is-utf8 "^0.2.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -3502,7 +3590,7 @@ tapable@^0.1.8, tapable@~0.1.8: version "0.1.10" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" -tapable@^0.2.3: +tapable@^0.2.5: version "0.2.6" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d" @@ -3527,7 +3615,13 @@ tar@~2.2.1: fstream "^1.0.2" inherits "2" -thenify@^3.1.0: +term-size@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-0.1.1.tgz#87360b96396cab5760963714cda0d0cbeecad9ca" + dependencies: + execa "^0.4.0" + +thenify@^3.1.0, thenify@^3.2.0: version "3.2.1" resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.2.1.tgz#251fd1c80aff6e5cf57cb179ab1fcb724269bd11" dependencies: @@ -3548,19 +3642,13 @@ through2@2.0.1, through2@^2.0.0: readable-stream "~2.0.0" xtend "~4.0.0" -throwback@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/throwback/-/throwback-1.1.1.tgz#f007e7c17604a6d16d7a07c41aa0e8fedc6184a4" - dependencies: - any-promise "^1.3.0" - time-stamp@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.0.1.tgz#9f4bd23559c9365966f3302dbba2b07c6b99b151" -timed-out@^3.0.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217" +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" timers-browserify@^2.0.2: version "2.0.2" @@ -3683,15 +3771,22 @@ typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typedoc-default-themes@^0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.4.2.tgz#640b854fd7ef19e6774496ea7741ec31a0dcaddc" - -typedoc@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.4.5.tgz#d12c8edc6bb97be7199c56dade65e0cc12d12e8b" - dependencies: - fs-extra "^0.30.0" +typedoc-default-themes@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.4.4.tgz#abe997dcf17462b627438bc63b65c50d363c252f" + +typedoc@^0.5.10: + version "0.5.10" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.5.10.tgz#4bd60c53c423811931fc519ffb36e58338824335" + dependencies: + "@types/fs-extra" "0.0.33" + "@types/handlebars" "^4.0.31" + "@types/highlight.js" "^9.1.8" + "@types/lodash" "^4.14.37" + "@types/marked" "0.0.28" + "@types/minimatch" "^2.0.29" + "@types/shelljs" "^0.3.32" + fs-extra "^2.0.0" handlebars "4.0.5" highlight.js "^9.0.0" lodash "^4.13.1" @@ -3699,76 +3794,67 @@ typedoc@^0.4.5: minimatch "^3.0.0" progress "^1.1.8" shelljs "^0.7.0" - typedoc-default-themes "^0.4.0" - typescript "1.8.10" - -typescript@1.8.10: - version "1.8.10" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-1.8.10.tgz#b475d6e0dff0bf50f296e5ca6ef9fbb5c7320f1e" + typedoc-default-themes "^0.4.2" + typescript "2.2.2" -typescript@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.0.2.tgz#a63f848074498a4dfa7f293afda0835d501d9540" - -typescript@^2.0.3: - version "2.1.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.1.5.tgz#6fe9479e00e01855247cea216e7561bafcdbcd4a" +typescript@2.2.2, typescript@^2.1.4, typescript@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.2.tgz#606022508479b55ffa368b58fee963a03dfd7b0c" -typings-core@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/typings-core/-/typings-core-1.6.1.tgz#ce4b2931ea2f19bb8f3dacbec69983ac4e964a37" +typings-core@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/typings-core/-/typings-core-2.3.3.tgz#09ec54cd5b11dd5f1ef2fc0ab31d37002ca2b5ad" dependencies: - any-promise "^1.3.0" array-uniq "^1.0.2" - configstore "^2.0.0" + configstore "^3.0.0" debug "^2.2.0" - detect-indent "^4.0.0" + detect-indent "^5.0.0" graceful-fs "^4.1.2" has "^1.0.1" invariant "^2.2.0" is-absolute "^0.2.3" + jspm-config "^0.3.0" listify "^1.0.0" lockfile "^1.0.1" make-error-cause "^1.2.1" mkdirp "^0.5.1" object.pick "^1.1.1" parse-json "^2.2.0" - popsicle "^8.0.2" + popsicle "^9.0.0" popsicle-proxy-agent "^3.0.0" popsicle-retry "^3.2.0" popsicle-rewrite "^1.0.0" popsicle-status "^2.0.0" - promise-finally "^2.0.1" + promise-finally "^3.0.0" rc "^1.1.5" rimraf "^2.4.4" sort-keys "^1.0.0" string-template "^1.0.0" - strip-bom "^2.0.0" + strip-bom "^3.0.0" thenify "^3.1.0" throat "^3.0.0" touch "^1.0.0" - typescript "^2.0.3" + typescript "^2.1.4" xtend "^4.0.0" zip-object "^0.1.0" -typings@^1.3.3: - version "1.5.0" - resolved "https://registry.yarnpkg.com/typings/-/typings-1.5.0.tgz#b9d236cf1d37460854f8c671ea495d9405b8103f" +typings@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/typings/-/typings-2.1.1.tgz#bacc69d255970a478e09f76c7f689975d535a78a" dependencies: - any-promise "^1.3.0" archy "^1.0.0" bluebird "^3.1.1" chalk "^1.0.0" - cli-truncate "^0.2.1" + cli-truncate "^1.0.0" columnify "^1.5.2" elegant-spinner "^1.0.1" has-unicode "^2.0.1" listify "^1.0.0" log-update "^1.0.2" minimist "^1.2.0" - promise-finally "^2.2.1" - typings-core "^1.6.1" - update-notifier "^1.0.0" + promise-finally "^3.0.0" + typings-core "^2.3.3" + update-notifier "^2.0.0" wordwrap "^1.0.0" xtend "^4.0.1" @@ -3797,26 +3883,32 @@ unc-path-regex@^0.1.0: version "0.1.2" resolved "http://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + dependencies: + crypto-random-string "^1.0.0" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" -unzip-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" -update-notifier@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-1.0.3.tgz#8f92c515482bd6831b7c93013e70f87552c7cf5a" +update-notifier@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.1.0.tgz#ec0c1e53536b76647a24b77cb83966d9315123d9" dependencies: - boxen "^0.6.0" + boxen "^1.0.0" chalk "^1.0.0" - configstore "^2.0.0" + configstore "^3.0.0" is-npm "^1.0.0" - latest-version "^2.0.0" - lazy-req "^1.1.0" + latest-version "^3.0.0" + lazy-req "^2.0.0" semver-diff "^2.0.0" - xdg-basedir "^2.0.0" + xdg-basedir "^3.0.0" url-parse-lax@^1.0.0: version "1.0.0" @@ -3855,10 +3947,6 @@ utils-merge@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" -uuid@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" - uuid@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" @@ -3944,7 +4032,7 @@ webpack@^1.13.2: watchpack "^0.2.1" webpack-core "~0.6.9" -which@1.2.x, which@^1.1.1, which@^1.2.9, which@~1.2.10: +which@1.2.x, which@^1.1.1, which@^1.2.8, which@^1.2.9, which@~1.2.10: version "1.2.12" resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" dependencies: @@ -4001,11 +4089,9 @@ wtf-8@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a" -xdg-basedir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" - dependencies: - os-homedir "^1.0.0" +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" xhr-mock@^1.6.0: version "1.7.0" From 0da955306fc147620bf7ba96a7bb4ab9a6ef4b4f Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Thu, 20 Apr 2017 16:47:53 -0400 Subject: [PATCH 15/56] Add reducers --- src/flux/reducer.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts index 0509d0d..741ecc6 100644 --- a/src/flux/reducer.ts +++ b/src/flux/reducer.ts @@ -1,23 +1,18 @@ +import * as actions from './actions'; import * as redux from 'redux'; -import { UPDATE_SEARCH_REQUEST } from './actions'; -export namespace Search { - export const request = (state, action) => { - - switch (action.type) { - case UPDATE_SEARCH_REQUEST: - break; - default: return state; - } - }; - export const response = (state, action) => state; +export function updateQuery(state, action) { + switch (action) { + case actions.UPDATE_QUERY: + return { ...state, query: action.query } + break; + default: + return state; + } } export default redux.combineReducers({ data: redux.combineReducers({ - search: redux.combineReducers({ - req: Search.request, - res: Search.response - }) + query: updateQuery() }) }); From 91fe7ba13593e071c885ac69787284ed65d82d14 Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Thu, 20 Apr 2017 17:22:28 -0400 Subject: [PATCH 16/56] Add boilerplate for reducers --- src/flux/reducer.ts | 92 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts index 741ecc6..0336707 100644 --- a/src/flux/reducer.ts +++ b/src/flux/reducer.ts @@ -1,11 +1,83 @@ -import * as actions from './actions'; import * as redux from 'redux'; +import Actions from './actions'; +import Store from './store'; -export function updateQuery(state, action) { +export function updateQuery(state: Store.Query, action) { switch (action) { - case actions.UPDATE_QUERY: - return { ...state, query: action.query } - break; + case Actions.UPDATE_QUERY: + return { ...state, query: action.query }; + default: + return state; + } +} + +export function updateNavigations(state: Store.Navigation, action) { + switch (action) { + // case Actions.SELECT_REFINEMENT: + // return { ...state, query: action.navigationId }; + default: + return state; + } +} + +export function updateSort(state: Store.Sort, action) { + switch (action) { + // case Actions.UPDATE_QUERY: + // return { ...state, query: action.query }; + default: + return state; + } +} + +export function updateProducts(state: Store.Product, action) { + switch (action) { + // case Actions.UPDATE_QUERY: + // return { ...state, query: action.query }; + default: + return state; + } +} + +export function updateCollections(state: Store.Collection, action) { + switch (action) { + // case Actions.UPDATE_QUERY: + // return { ...state, query: action.query }; + default: + return state; + } +} + +export function updateAutocomplete(state: Store.Autocomplete, action) { + switch (action) { + // case Actions.UPDATE_QUERY: + // return { ...state, query: action.query }; + default: + return state; + } +} + +export function updateRedirect(state, action) { + switch (action) { + // case Actions.UPDATE_QUERY: + // return { ...state, query: action.query }; + default: + return state; + } +} + +export function updateErrors(state, action) { + switch (action) { + // case Actions.UPDATE_QUERY: + // return { ...state, query: action.query }; + default: + return state; + } +} + +export function updateWarnings(state, action) { + switch (action) { + // case Actions.UPDATE_QUERY: + // return { ...state, query: action.query }; default: return state; } @@ -13,6 +85,14 @@ export function updateQuery(state, action) { export default redux.combineReducers({ data: redux.combineReducers({ - query: updateQuery() + query: updateQuery, + sort: updateSort, + products: updateProducts, + collections: updateCollections, + navigations: updateNavigations, + autocomplete: updateAutocomplete, + redirect: updateRedirect, + errors: updateErrors, + warnings: updateWarnings }) }); From b5dcd2816d4e3076a1c465dd5aaf45129b0e687b Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Fri, 21 Apr 2017 07:41:23 -0400 Subject: [PATCH 17/56] more types --- src/flux/actions.ts | 20 +++++++--- src/flux/store.ts | 78 +++++++++++++++++++++++++++++++++++++++ test/unit/flux/actions.ts | 55 +++++++++++++++++++++------ 3 files changed, 136 insertions(+), 17 deletions(-) diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 4135d10..c891730 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -5,15 +5,17 @@ import Store from './store'; namespace Actions { export const UPDATE_SEARCH_REQUEST = 'UPDATE_SEARCH_REQUEST'; export const UPDATE_QUERY = 'UPDATE_QUERY'; - export const ADD_REFINEMENT = 'ADD_REFINEMENT'; - export const REMOVE_REFINEMENT = 'REMOVE_REFINEMENT'; + export const SELECT_REFINEMENT = 'SELECT_REFINEMENT'; + export const DESELECT_REFINEMENT = 'DESELECT_REFINEMENT'; + export const SELECT_COLLECTION = 'SELECT_COLLECTION'; + export const DESELECT_COLLECTION = 'DESELECT_COLLECTION'; export function search(request: Request) { return (dispatch: Dispatch, getState: () => Store.State) => { const data = getState().data; - conditional(data.query.original !== request.query, () => updateQuery(request.query)) - .then(() => conditional()); + // conditional(data.query.original !== request.query, () => updateQuery(request.query)) + // .then(() => conditional()); return dispatch({ type: UPDATE_SEARCH_REQUEST, @@ -24,9 +26,15 @@ namespace Actions { export const updateQuery = (query: string) => Actions.thunk(UPDATE_QUERY, { query }); - export const addRefinement = (refinement: Store.Refinement) => Actions.thunk(ADD_REFINEMENT, { refinement }); + export const selectRefinement = (data: { navigationId: string, refinementIndex: number }) => + Actions.thunk(SELECT_REFINEMENT, data); - export const removeRefinement = (refinement: Store.Refinement) => Actions.thunk(REMOVE_REFINEMENT, { refinement }); + export const deselectRefinement = (data: { navigationId: string, refinementIndex: number }) => + Actions.thunk(DESELECT_REFINEMENT, data); + + export const selectCollection = (data: { collectionId: string }) => Actions.thunk(SELECT_COLLECTION, data); + + export const deselectCollection = (data: { collectionId: string }) => Actions.thunk(DESELECT_COLLECTION, data); // utilities diff --git a/src/flux/store.ts b/src/flux/store.ts index 39bcca7..ccde797 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -17,6 +17,12 @@ namespace Store { autocomplete: Autocomplete; // mixed + page: Page; // mixed + + template: Template; // post + + details: Details; // post + redirect?: string; // post errors: string[]; // post @@ -61,6 +67,78 @@ namespace Store { descending?: boolean; } + export interface Page { + /** + * number of products per page + */ + size: number; // pre + + /** + * current page number + */ + current: number; // post + + /** + * number of next page + */ + previous: number; // post + /** + * number of previous page + */ + next: number; // post + /** + * number of first page + */ + first: 1; // static + /** + * number of last page + */ + last: number; // post + + /** + * start of displayed products + */ + fromResult: number; // post + /** + * end of displayed products + */ + toResult: number; // post + + /** + * displayed number range (in ) + */ + range: number[]; // post + } + + export interface Template { + name: string; + rule: string; + zones: { + [zoneName: string]: Zone; + }; + } + + export type Zone = ContentZone | RichContentZone | RecordZone; + + export interface ContentZone { + type: 'content'; + content: string; + } + + export interface RichContentZone { + type: 'rich_content'; + content: string; + } + + export interface RecordZone { + type: 'record'; + products: Product[]; + } + + export interface Details { + product: Product; + } + export interface Product { id: string; // post [key: string]: any; // post diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts index b8bd8c3..e6323ae 100644 --- a/test/unit/flux/actions.ts +++ b/test/unit/flux/actions.ts @@ -1,7 +1,7 @@ import Actions from '../../../src/flux/actions'; import suite from '../_suite'; -suite.only('actions', ({ expect, stub }) => { +suite.only('actions', ({ expect, spy, stub }) => { describe('conditional()', () => { it('should return result of action', () => { const obj = { a: 'b' }; @@ -22,7 +22,18 @@ suite.only('actions', ({ expect, stub }) => { }); describe('thunk()', () => { + it('should return a constructed thunk', () => { + const dispatch = spy(); + const type = 'MY_ACTION'; + const thunk = Actions.thunk(type, { a: 'b' }); + + expect(thunk).to.be.a('function'); + + thunk(dispatch); + + expect(dispatch).to.be.calledWith({ type, a: 'b' }); + }); }); describe('updateQuery()', () => { @@ -36,25 +47,47 @@ suite.only('actions', ({ expect, stub }) => { }); }); - describe('addRefinement()', () => { - it('should create an ADD_REFINEMENT action', () => { - const refinement: any = { a: 'b' }; + describe('selectRefinement()', () => { + it('should create a SELECT_REFINEMENT action', () => { + const refinement: any = { id: 1 }; + const thunk = stub(Actions, 'thunk'); + + Actions.selectRefinement(refinement); + + expect(thunk).to.be.calledWith(Actions.SELECT_REFINEMENT, refinement); + }); + }); + + describe('deselectRefinement()', () => { + it('should create a DESELECT_REFINEMENT action', () => { + const refinement: any = { id: 1 }; + const thunk = stub(Actions, 'thunk'); + + Actions.deselectRefinement(refinement); + + expect(thunk).to.be.calledWith(Actions.DESELECT_REFINEMENT, refinement); + }); + }); + + describe('selectCollection()', () => { + it('should create a SELECT_COLLECTION action', () => { + const collection: any = { id: 1 }; const thunk = stub(Actions, 'thunk'); - Actions.addRefinement(refinement); + Actions.selectCollection(collection); - expect(thunk).to.be.calledWith(Actions.ADD_REFINEMENT, { refinement }); + expect(thunk).to.be.calledWith(Actions.SELECT_COLLECTION, collection); }); }); - describe('removeRefinement()', () => { - it('should create a REMOVE_REFINEMENT action', () => { - const refinement: any = { a: 'b' }; + describe('deselectCollection()', () => { + it('should create a DESELECT_COLLECTION action', () => { + const collection: any = { id: 1 }; const thunk = stub(Actions, 'thunk'); - Actions.removeRefinement(refinement); + Actions.deselectCollection(collection); - expect(thunk).to.be.calledWith(Actions.REMOVE_REFINEMENT, { refinement }); + expect(thunk).to.be.calledWith(Actions.DESELECT_COLLECTION, collection); }); }); }); From 680ba550ba1861ffced60695c655ad7fccefdf69 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Fri, 21 Apr 2017 08:07:08 -0400 Subject: [PATCH 18/56] update to webpack2 --- package.json | 18 +- webpack.config.js | 59 +-- yarn.lock | 959 ++++++++++++++++++++++++++++++---------------- 3 files changed, 673 insertions(+), 363 deletions(-) diff --git a/package.json b/package.json index 8fc0461..1d3308b 100644 --- a/package.json +++ b/package.json @@ -42,26 +42,26 @@ "@types/node": "^7.0.13", "awesome-typescript-loader": "^3.1.2", "chai": "^3.2.0", - "codacy-coverage": "^2.0.0", + "codacy-coverage": "^2.0.2", "gb-license-check": "^1.0.1", "husky": "^0.13.3", "istanbul": "^0.4.5", - "karma": "^0.13.22", + "karma": "^1.6.0", "karma-coverage": "^1.1.1", "karma-mocha": "^1.0.1", - "karma-mocha-reporter": "^2.0.4", - "karma-phantomjs-launcher": "^1.0.0", + "karma-mocha-reporter": "^2.2.3", + "karma-phantomjs-launcher": "^1.0.4", "karma-sinon-chai": "^1.3.1", "karma-source-map-support": "^1.2.0", - "karma-webpack": "^1.7.0", - "mocha": "^2.2.5", + "karma-webpack": "^2.0.3", + "mocha": "^3.2.0", "mocha-suite": "^1.0.8", "object-assign": "^4.1.0", "phantomjs-prebuilt": "^2.1.7", "pre-commit": "^1.1.3", - "remap-istanbul": "^0.6.4", + "remap-istanbul": "^0.9.5", "rimraf": "^2.5.4", - "sinon": "^1.17.6", + "sinon": "^2.1.0", "sinon-chai": "^2.9.0", "sourcemap-istanbul-instrumenter-loader": "^0.2.0", "tslint": "^5.1.0", @@ -70,7 +70,7 @@ "typedoc": "^0.5.10", "typescript": "^2.2.2", "typings": "^2.1.1", - "webpack": "^1.13.2", + "webpack": "^2.4.1", "xhr-mock": "^1.6.0" }, "dependencies": { diff --git a/webpack.config.js b/webpack.config.js index 8ae9ee7..86a7651 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,5 @@ +const path = require('path'); + // eslint-disable-next-line no-process-env const isCi = process.env.NODE_ENV === 'ci'; @@ -5,35 +7,40 @@ module.exports = { devtool: 'inline-source-map', resolve: { - extensions: ['', '.ts', '.js'], - modulesDirectories: ['node_modules', 'src'] + extensions: ['.ts', '.js'], + modules: [ + 'node_modules', + path.resolve(__dirname, 'src') + ] }, module: { - preLoaders: isCi ? [] : [{ - test: /\.ts$/, - loader: 'tslint' - }], - - postLoaders: isCi ? [] : [{ - test: /\.ts$/, - loader: 'sourcemap-istanbul-instrumenter', - exclude: [ - /node_modules/, - /test/, - /karma\.entry\.ts$/ - ] - }], - - loaders: [{ - test: /\.ts$/, - exclude: /node_modules/, - loader: 'awesome-typescript', - query: { - inlineSourceMap: true, - sourceMap: false - } - }] + rules: + (isCi ? [{ + test: /\.ts$/, + enforce: 'pre', + loader: 'tslint-loader', + options: { + typeCheck: true + } + }, { + test: /\.ts$/, + exclude: [ + path.resolve(__dirname, 'node_modules'), + path.resolve(__dirname, 'test'), + path.resolve(__dirname, 'test/karma.entry.ts') + ], + loader: 'sourcemap-istanbul-instrumenter-loader' + }] : []) + .concat({ + test: /\.ts$/, + exclude: path.resolve(__dirname, 'node_modules'), + loader: 'awesome-typescript-loader', + options: { + inlineSourceMap: true, + sourceMap: true + } + }) } }; diff --git a/yarn.lock b/yarn.lock index b072a4f..dfc6934 100644 --- a/yarn.lock +++ b/yarn.lock @@ -75,9 +75,19 @@ accepts@1.3.3: mime-types "~2.1.11" negotiator "0.6.1" -acorn@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" +acorn-dynamic-import@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" + dependencies: + acorn "^4.0.3" + +acorn@^4.0.3: + version "4.0.11" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0" + +acorn@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d" after@0.8.2: version "0.8.2" @@ -90,6 +100,17 @@ agent-base@2: extend "~3.0.0" semver "~5.0.1" +ajv-keywords@^1.1.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + +ajv@^4.7.0: + version "4.11.7" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.7.tgz#8655a5d86d0824985cc471a1d913fb6729a0ec48" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" @@ -98,7 +119,7 @@ align-text@^0.1.1, align-text@^0.1.3: longest "^1.0.1" repeat-string "^1.5.2" -amdefine@1.0.0, amdefine@>=0.0.4: +amdefine@>=0.0.4, amdefine@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.0.tgz#fd17474700cb5cc9c2b709f0be9d23ce3c198c33" @@ -216,6 +237,14 @@ asap@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" +asn1.js@^4.0.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -242,18 +271,20 @@ async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" -async@1.x, async@^1.3.0, async@^1.4.0: +async@1.x, async@^1.4.0: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^0.9.0, async@~0.9.0: +async@^2.1.2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.3.0.tgz#1013d1051047dd320fe24e494d5c66ecaf6147d9" + dependencies: + lodash "^4.14.0" + +async@~0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" -async@~0.2.6: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -312,10 +343,6 @@ base64id@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" -batch@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.5.3.tgz#3f3414f380321743bfc1042f9a83ff1d5824d464" - bcrypt-pbkdf@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz#3ca76b85241c7170bf7d9703e7b9aa74630040d4" @@ -350,26 +377,30 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@^2.3, bluebird@^2.9.27, bluebird@^2.9.x: +bluebird@^2.3, bluebird@^2.9.x: version "2.11.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" -bluebird@^3.1.1: +bluebird@^3.1.1, bluebird@^3.3.0: version "3.4.7" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" -body-parser@^1.12.4: - version "1.16.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.16.0.tgz#924a5e472c6229fb9d69b85a20d5f2532dec788b" +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.6" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" + +body-parser@^1.16.1: + version "1.17.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.1.tgz#75b3bc98ddd6e7e0d8ffe750dfaca5c66993fa47" dependencies: bytes "2.4.0" content-type "~1.0.2" - debug "2.6.0" + debug "2.6.1" depd "~1.1.0" - http-errors "~1.5.1" + http-errors "~1.6.1" iconv-lite "0.4.15" on-finished "~2.3.0" - qs "6.2.1" + qs "6.4.0" raw-body "~2.2.0" type-is "~1.6.14" @@ -412,11 +443,58 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" -browserify-aes@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-0.4.0.tgz#067149b668df31c4b58533e02d01e806d8608e2c" +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browser-stdout@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a" + dependencies: + buffer-xor "^1.0.2" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + inherits "^2.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" inherits "^2.0.1" + parse-asn1 "^5.0.0" browserify-zlib@^0.1.4: version "0.1.4" @@ -428,7 +506,11 @@ buffer-shims@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" -buffer@^4.9.0: +buffer-xor@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: version "4.9.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" dependencies: @@ -467,6 +549,10 @@ camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + camelcase@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -514,7 +600,7 @@ chalk@~0.5.1: strip-ansi "^0.3.0" supports-color "^0.2.0" -chokidar@^1.0.0, chokidar@^1.4.1: +chokidar@^1.4.1, chokidar@^1.4.3: version "1.6.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" dependencies: @@ -533,6 +619,12 @@ ci-info@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.0.0.tgz#dc5285f2b4e251821683681c381c3388f46ec534" +cipher-base@^1.0.0, cipher-base@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07" + dependencies: + inherits "^2.0.1" + cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" @@ -558,6 +650,14 @@ cliui@^2.1.0: right-align "^0.1.1" wordwrap "0.0.2" +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + clone-stats@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" @@ -566,14 +666,19 @@ clone@^1.0.0, clone@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" -codacy-coverage@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/codacy-coverage/-/codacy-coverage-2.0.1.tgz#5ac7a0892031030dc35299a2cce7895b3763c28d" +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +codacy-coverage@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/codacy-coverage/-/codacy-coverage-2.0.2.tgz#394f2f3c0e2b8ee924281e633df51e29b94dd8d9" dependencies: bluebird "^2.9.x" commander "^2.x" joi "^6.4.x" lcov-parse "0.x" + lodash "^4.17.4" log-driver "^1.x" request-promise "^0.x" @@ -592,21 +697,19 @@ columnify@^1.5.2: strip-ansi "^3.0.0" wcwidth "^1.0.0" +combine-lists@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6" + dependencies: + lodash "^4.5.0" + combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" dependencies: delayed-stream "~1.0.0" -commander@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" - -commander@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" - -commander@^2.9.0, commander@^2.x: +commander@2.9.0, commander@^2.9.0, commander@^2.x: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" dependencies: @@ -651,12 +754,12 @@ configstore@^3.0.0: write-file-atomic "^1.1.2" xdg-basedir "^3.0.0" -connect@^3.3.5: - version "3.5.0" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.5.0.tgz#b357525a0b4c1f50599cd983e1d9efeea9677198" +connect@^3.6.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.1.tgz#b7760693a74f0454face1d9378edb3f885b43227" dependencies: - debug "~2.2.0" - finalhandler "0.5.0" + debug "2.6.3" + finalhandler "1.0.1" parseurl "~1.3.1" utils-merge "1.0.0" @@ -682,7 +785,7 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" -core-js@^2.1.0: +core-js@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" @@ -690,12 +793,35 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + create-error-class@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" dependencies: capture-stack-trace "^1.0.0" +create-hash@^1.1.0, create-hash@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.2.tgz#51210062d7bb7479f6c65bb41a92208b1d61abad" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^1.0.0" + sha.js "^2.3.6" + +create-hmac@^1.1.0, create-hmac@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.4.tgz#d3fb4ba253eb8b3f56e39ea2fbcb8af747bd3170" + dependencies: + create-hash "^1.1.0" + inherits "^2.0.1" + cross-spawn-async@^2.1.1: version "2.2.5" resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc" @@ -717,14 +843,20 @@ cryptiles@2.x.x: dependencies: boom "2.x.x" -crypto-browserify@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.3.0.tgz#b9fc75bb4a0ed61dcf1cd5dae96eb30c9c3e506c" +crypto-browserify@^3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522" dependencies: - browserify-aes "0.4.0" - pbkdf2-compat "2.0.1" - ripemd160 "0.2.0" - sha.js "2.2.6" + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" crypto-random-string@^1.0.0: version "1.0.0" @@ -767,13 +899,7 @@ debug@0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" -debug@2, debug@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" - dependencies: - ms "0.7.2" - -debug@2.2.0, debug@^2.2.0, debug@~2.2.0: +debug@2, debug@2.2.0, debug@^2.2.0, debug@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" dependencies: @@ -785,11 +911,23 @@ debug@2.3.3: dependencies: ms "0.7.2" +debug@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351" + dependencies: + ms "0.7.2" + +debug@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" + dependencies: + ms "0.7.2" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" -decamelize@^1.0.0, decamelize@^1.1.2: +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -832,10 +970,17 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" -depd@~1.1.0: +depd@1.1.0, depd@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" @@ -855,10 +1000,18 @@ diff@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" -diff@^3.2.0: +diff@^3.1.0, diff@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + doctrine@^0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523" @@ -913,13 +1066,29 @@ elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" -engine.io-client@1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.2.tgz#c38767547f2a7d184f5752f6f0ad501006703766" +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +engine.io-client@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.3.tgz#1798ed93451246453d4c6f635d7a201fe940d5ab" dependencies: component-emitter "1.2.1" component-inherit "0.0.3" @@ -930,7 +1099,7 @@ engine.io-client@1.8.2: parsejson "0.0.3" parseqs "0.0.5" parseuri "0.0.5" - ws "1.1.1" + ws "1.1.2" xmlhttprequest-ssl "1.5.3" yeast "0.1.2" @@ -945,18 +1114,18 @@ engine.io-parser@1.3.2: has-binary "0.1.7" wtf-8 "1.0.0" -engine.io@1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.2.tgz#6b59be730b348c0125b0a4589de1c355abcf7a7e" +engine.io@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.3.tgz#8de7f97895d20d39b85f88eeee777b2bd42b13d4" dependencies: accepts "1.3.3" base64id "1.0.0" cookie "0.3.1" debug "2.3.3" engine.io-parser "1.3.2" - ws "1.1.1" + ws "1.1.2" -enhanced-resolve@^3.1.0: +enhanced-resolve@^3.0.0, enhanced-resolve@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz#9f4b626f577245edcf4b2ad83d86e17f4f421dec" dependencies: @@ -965,14 +1134,6 @@ enhanced-resolve@^3.1.0: object-assign "^4.0.1" tapable "^0.2.5" -enhanced-resolve@~0.9.0: - version "0.9.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz#4d6e689b3725f86090927ccc86cd9f1635b89e2e" - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.2.0" - tapable "^0.1.8" - ent@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" @@ -1044,7 +1205,11 @@ escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" -escape-string-regexp@1.0.2, escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2: +escape-string-regexp@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" @@ -1083,6 +1248,12 @@ events@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" +evp_bytestokey@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz#497b66ad9fef65cd7c08a6180824ba1476b66e53" + dependencies: + create-hash "^1.1.1" + execa@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.4.0.tgz#4eb6467a36a095fabb2970ff9d5e3fb7bce6ebc3" @@ -1175,13 +1346,6 @@ filename-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" -fileset@0.2.x: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fileset/-/fileset-0.2.1.tgz#588ef8973c6623b2a76df465105696b96aac8067" - dependencies: - glob "5.x" - minimatch "2.x" - fill-range@^2.1.0: version "2.2.3" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" @@ -1215,14 +1379,16 @@ filter-values@^0.4.0: for-own "^0.1.3" is-match "^0.4.0" -finalhandler@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7" +finalhandler@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.1.tgz#bcd15d1689c0e5ed729b6f7f541a6df984117db8" dependencies: - debug "~2.2.0" + debug "2.6.3" + encodeurl "~1.0.1" escape-html "~1.0.3" on-finished "~2.3.0" - statuses "~1.3.0" + parseurl "~1.3.1" + statuses "~1.3.1" unpipe "~1.0.0" find-parent-dir@^0.3.0: @@ -1275,11 +1441,11 @@ form-data@^2.0.0, form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" -formatio@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9" +formatio@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" dependencies: - samsam "~1.1" + samsam "1.x" fs-extra@^2.0.0: version "2.1.2" @@ -1360,6 +1526,10 @@ generate-object-property@^1.1.0: dependencies: is-property "^1.0.0" +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -1387,14 +1557,18 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob@3.2.11: - version "3.2.11" - resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" +glob@7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" inherits "2" - minimatch "0.3" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" -glob@5.x, glob@^5.0.15, glob@~5.0.0: +glob@^5.0.15, glob@~5.0.0: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" dependencies: @@ -1556,6 +1730,12 @@ has@^1.0.1: dependencies: function-bind "^1.0.2" +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.0.3.tgz#1332ff00156c0a0ffdd8236013d07b77a0451573" + dependencies: + inherits "^2.0.1" + hasha@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" @@ -1576,6 +1756,14 @@ highlight.js@^9.0.0: version "9.9.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.9.0.tgz#b9995dcfdc2773e307a34f0460d92b9a474782c0" +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" @@ -1584,12 +1772,13 @@ hosted-git-info@^2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b" -http-errors@~1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" +http-errors@~1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" dependencies: + depd "1.1.0" inherits "2.0.3" - setprototypeof "1.0.2" + setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" http-proxy-agent@^1.0.0: @@ -1677,10 +1866,6 @@ ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" -interpret@^0.6.4: - version "0.6.6" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-0.6.6.tgz#fecd7a18e7ce5ca6abfb953e1f86213a49f1625b" - interpret@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" @@ -1691,6 +1876,10 @@ invariant@^2.2.0: dependencies: loose-envify "^1.0.0" +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + is-absolute@^0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb" @@ -1898,26 +2087,7 @@ isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" -istanbul@0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.3.tgz#5b714ee0ae493ac5ef204b99f3872bceef73d53a" - dependencies: - abbrev "1.0.x" - async "1.x" - escodegen "1.8.x" - esprima "2.7.x" - fileset "0.2.x" - handlebars "^4.0.1" - js-yaml "3.x" - mkdirp "0.5.x" - nopt "3.x" - once "1.x" - resolve "1.1.x" - supports-color "^3.1.0" - which "^1.1.1" - wordwrap "^1.0.0" - -istanbul@0.x.x, istanbul@^0.4.0, istanbul@^0.4.5: +istanbul@0.4.5, istanbul@0.x.x, istanbul@^0.4.0, istanbul@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" dependencies: @@ -1936,13 +2106,6 @@ istanbul@0.x.x, istanbul@^0.4.0, istanbul@^0.4.5: which "^1.1.1" wordwrap "^1.0.0" -jade@0.26.3: - version "0.26.3" - resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" - dependencies: - commander "0.6.1" - mkdirp "0.3.0" - jju@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jju/-/jju-1.3.0.tgz#dadd9ef01924bc728b03f2f7979bdbd62f7a2aaa" @@ -1977,6 +2140,10 @@ jsbn@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" +json-loader@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" + json-parse-helpfulerror@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz#13f14ce02eed4e981297b64eb9e3b932e2dd13dc" @@ -1987,6 +2154,12 @@ json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -1995,7 +2168,7 @@ json3@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" -json5@^0.5.0: +json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -2005,6 +2178,10 @@ jsonfile@^2.1.0: optionalDependencies: graceful-fs "^4.1.6" +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" @@ -2041,9 +2218,9 @@ karma-coverage@^1.1.1: minimatch "^3.0.0" source-map "^0.5.1" -karma-mocha-reporter@^2.0.4: - version "2.2.2" - resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.2.tgz#876de9a287244e54a608591732a98e66611f6abe" +karma-mocha-reporter@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.3.tgz#04fdda45a1d9697a73871c7472223c581701ab20" dependencies: chalk "1.1.3" @@ -2053,9 +2230,9 @@ karma-mocha@^1.0.1: dependencies: minimist "1.2.0" -karma-phantomjs-launcher@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.2.tgz#19e1041498fd75563ed86730a22c1fe579fa8fb1" +karma-phantomjs-launcher@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz#d23ca34801bda9863ad318e3bb4bd4062b13acd2" dependencies: lodash "^4.0.1" phantomjs-prebuilt "^2.1.7" @@ -2072,9 +2249,9 @@ karma-source-map-support@^1.2.0: dependencies: source-map-support "^0.4.1" -karma-webpack@^1.7.0: - version "1.8.1" - resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-1.8.1.tgz#39d5fd2edeea3cc3ef5b405989b37d5b0e6a3b4e" +karma-webpack@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.3.tgz#39cebf5ca2580139b27f9ae69b78816b9c82fae6" dependencies: async "~0.9.0" loader-utils "^0.2.5" @@ -2082,33 +2259,37 @@ karma-webpack@^1.7.0: source-map "^0.1.41" webpack-dev-middleware "^1.0.11" -karma@^0.13.22: - version "0.13.22" - resolved "https://registry.yarnpkg.com/karma/-/karma-0.13.22.tgz#07750b1bd063d7e7e7b91bcd2e6354d8f2aa8744" +karma@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/karma/-/karma-1.6.0.tgz#0e871d4527d5eac56c41d181f03c5c0a7e6dbf3e" dependencies: - batch "^0.5.3" - bluebird "^2.9.27" - body-parser "^1.12.4" + bluebird "^3.3.0" + body-parser "^1.16.1" chokidar "^1.4.1" colors "^1.1.0" - connect "^3.3.5" - core-js "^2.1.0" + combine-lists "^1.0.0" + connect "^3.6.0" + core-js "^2.2.0" di "^0.0.1" dom-serialize "^2.2.0" expand-braces "^0.1.1" - glob "^7.0.0" + glob "^7.1.1" graceful-fs "^4.1.2" http-proxy "^1.13.0" isbinaryfile "^3.0.0" lodash "^3.8.0" log4js "^0.6.31" mime "^1.3.4" - minimatch "^3.0.0" + minimatch "^3.0.2" optimist "^0.6.1" - rimraf "^2.3.3" - socket.io "^1.4.5" + qjobs "^1.1.4" + range-parser "^1.2.0" + rimraf "^2.6.0" + safe-buffer "^5.0.1" + socket.io "1.7.3" source-map "^0.5.3" - useragent "^2.1.6" + tmp "0.0.31" + useragent "^2.1.12" kew@~0.7.0: version "0.7.0" @@ -2146,6 +2327,12 @@ lazy-req@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lazy-req/-/lazy-req-2.0.0.tgz#c9450a363ecdda2e6f0c70132ad4f37f8f06f2b4" +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + lcov-parse@0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-0.0.10.tgz#1b0b8ff9ac9c7889250582b70b71315d9da6d9a3" @@ -2183,7 +2370,11 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -loader-utils@0.x.x, loader-utils@^0.2.11, loader-utils@^0.2.5: +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@0.x.x, loader-utils@^0.2.16, loader-utils@^0.2.5: version "0.2.16" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" dependencies: @@ -2208,10 +2399,21 @@ lodash-es@^4.2.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + lodash._basecopy@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" +lodash._basecreate@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" + lodash._basetostring@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" @@ -2244,6 +2446,14 @@ lodash._root@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" +lodash.create@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" + dependencies: + lodash._baseassign "^3.0.0" + lodash._basecreate "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash.escape@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" @@ -2299,7 +2509,7 @@ lodash@^3.10.0, lodash@^3.8.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.1, lodash@^4.13.1, lodash@^4.16.3, lodash@^4.17.4, lodash@^4.2.1: +lodash@^4.0.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.3, lodash@^4.17.4, lodash@^4.2.1, lodash@^4.5.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -2321,10 +2531,6 @@ log4js@^0.6.31: readable-stream "~1.0.2" semver "~4.3.3" -lolex@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" - lolex@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" @@ -2350,7 +2556,7 @@ lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" -lru-cache@2, lru-cache@2.2.x: +lru-cache@2.2.x: version "2.2.4" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" @@ -2383,10 +2589,6 @@ media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" -memory-fs@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" - memory-fs@^0.4.0, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -2394,13 +2596,6 @@ memory-fs@^0.4.0, memory-fs@~0.4.1: errno "^0.1.3" readable-stream "^2.0.1" -memory-fs@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.3.0.tgz#7bcc6b629e3a43e871d7e29aca6ae8a7f15cbb20" - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - meow@^3.3.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -2434,6 +2629,13 @@ micromatch@^2.1.5, micromatch@^2.2.0, micromatch@^2.3.7: parse-glob "^3.0.4" regex-cache "^0.4.2" +miller-rabin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + mime-db@~1.26.0: version "1.26.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" @@ -2454,25 +2656,20 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" -minimatch@0.3: - version "0.3.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" - dependencies: - lru-cache "2" - sigmund "~1.0.0" +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2: +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" dependencies: brace-expansion "^1.0.0" -minimatch@2.x: - version "2.0.10" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" - dependencies: - brace-expansion "^1.0.0" - minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -2481,10 +2678,6 @@ minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -mkdirp@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" - mkdirp@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" @@ -2507,20 +2700,21 @@ mocha-suite@^1.0.8: dependencies: "@types/mocha" "^2.2.40" -mocha@^2.2.5: - version "2.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" +mocha@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.2.0.tgz#7dc4f45e5088075171a68896814e6ae9eb7a85e3" dependencies: - commander "2.3.0" + browser-stdout "1.3.0" + commander "2.9.0" debug "2.2.0" diff "1.4.0" - escape-string-regexp "1.0.2" - glob "3.2.11" + escape-string-regexp "1.0.5" + glob "7.0.5" growl "1.9.2" - jade "0.26.3" + json3 "3.3.2" + lodash.create "3.1.1" mkdirp "0.5.1" - supports-color "1.2.0" - to-iso-string "0.0.2" + supports-color "3.1.2" moment@2.x.x: version "2.17.1" @@ -2544,20 +2738,24 @@ nan@^2.3.0: version "2.5.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" +native-promise-only@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" + negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" -node-libs-browser@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-0.7.0.tgz#3e272c0819e308935e26674408d7af0e1491b83b" +node-libs-browser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646" dependencies: assert "^1.1.1" browserify-zlib "^0.1.4" - buffer "^4.9.0" + buffer "^4.3.0" console-browserify "^1.1.0" constants-browserify "^1.0.0" - crypto-browserify "3.3.0" + crypto-browserify "^3.11.0" domain-browser "^1.1.1" events "^1.0.0" https-browserify "0.0.1" @@ -2729,10 +2927,20 @@ os-browserify@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + os-shim@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" +os-tmpdir@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + package-json@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" @@ -2746,6 +2954,16 @@ pako@~0.2.0: version "0.2.9" resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -2805,6 +3023,12 @@ path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -2813,9 +3037,11 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -pbkdf2-compat@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz#b6e0c8fa99494d94e0511575802a59a5c142f288" +pbkdf2@^3.0.3: + version "3.0.9" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.9.tgz#f2c4b25a600058b3c3773c086c37dbbee1ffe693" + dependencies: + create-hmac "^1.1.2" pend@~1.2.0: version "1.2.0" @@ -2928,6 +3154,16 @@ pseudomap@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" @@ -2936,9 +3172,13 @@ punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -qs@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625" +qjobs@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" + +qs@6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" qs@^6.1.0, qs@~6.3.0: version "6.3.0" @@ -2959,7 +3199,11 @@ randomatic@^1.1.3: is-number "^2.0.2" kind-of "^3.0.2" -range-parser@^1.0.3: +randombytes@^2.0.0, randombytes@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.3.tgz#674c99760901c3c4112771a31e521dc349cc09ec" + +range-parser@^1.0.3, range-parser@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" @@ -3018,9 +3262,9 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.1.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" +"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.1.0, readable-stream@~2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" dependencies: buffer-shims "^1.0.0" core-util-is "~1.0.0" @@ -3059,18 +3303,6 @@ readable-stream@~1.1.9: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@~2.1.4: - version "2.1.5" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" - dependencies: - buffer-shims "^1.0.0" - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" @@ -3134,13 +3366,14 @@ registry-url@^3.0.3: dependencies: rc "^1.0.1" -remap-istanbul@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/remap-istanbul/-/remap-istanbul-0.6.4.tgz#ac551eff1aa641504b4f318d0303dda61e3bb695" +remap-istanbul@^0.9.5: + version "0.9.5" + resolved "https://registry.yarnpkg.com/remap-istanbul/-/remap-istanbul-0.9.5.tgz#a18617b1f31eec5a7dbee77538298b775606aaa8" dependencies: - amdefine "1.0.0" + amdefine "^1.0.0" gulp-util "3.0.7" - istanbul "0.4.3" + istanbul "0.4.5" + minimatch "^3.0.3" source-map ">=0.5.6" through2 "2.0.1" @@ -3206,6 +3439,14 @@ request@^2.34, request@^2.79.0, request@~2.79.0: tunnel-agent "~0.4.1" uuid "^3.0.0" +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + requires-port@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -3233,23 +3474,29 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.3.3, rimraf@^2.4.4, rimraf@^2.5.4, rimraf@~2.5.1, rimraf@~2.5.4: +rimraf@2, rimraf@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +rimraf@^2.4.4, rimraf@^2.5.4, rimraf@~2.5.1, rimraf@~2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: glob "^7.0.5" -ripemd160@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-0.2.0.tgz#2bf198bde167cacfa51c0a928e84b68bbe171fce" +ripemd160@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e" safe-buffer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" -samsam@1.1.2, samsam@~1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" +samsam@1.x, samsam@^1.1.3: + version "1.2.1" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.2.1.tgz#edd39093a3184370cb859243b2bdf255e7d8ea67" semver-diff@^2.0.0: version "2.1.0" @@ -3269,7 +3516,7 @@ semver@~5.0.1: version "5.0.3" resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" -set-blocking@~2.0.0: +set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -3281,13 +3528,15 @@ setimmediate@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" -setprototypeof@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" -sha.js@2.2.6: - version "2.2.6" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" +sha.js@^2.3.6: + version "2.4.8" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" + dependencies: + inherits "^2.0.1" shebang-command@^1.2.0: version "1.2.0" @@ -3307,10 +3556,6 @@ shelljs@^0.7.0: interpret "^1.0.0" rechoir "^0.6.2" -sigmund@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -3319,14 +3564,18 @@ sinon-chai@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.9.0.tgz#34d820042bc9661a14527130d401eb462c49bb84" -sinon@^1.17.6: - version "1.17.7" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf" +sinon@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.1.0.tgz#e057a9d2bf1b32f5d6dd62628ca9ee3961b0cafb" dependencies: - formatio "1.1.1" - lolex "1.3.2" - samsam "1.1.2" - util ">=0.10.3 <1" + diff "^3.1.0" + formatio "1.2.0" + lolex "^1.6.0" + native-promise-only "^0.8.1" + path-to-regexp "^1.7.0" + samsam "^1.1.3" + text-encoding "0.6.4" + type-detect "^4.0.0" slice-ansi@0.0.4: version "0.0.4" @@ -3349,15 +3598,15 @@ socket.io-adapter@0.5.0: debug "2.3.3" socket.io-parser "2.3.1" -socket.io-client@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.2.tgz#39fdb0c3dd450e321b7e40cfd83612ec533dd644" +socket.io-client@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.3.tgz#b30e86aa10d5ef3546601c09cde4765e381da377" dependencies: backo2 "1.0.2" component-bind "1.0.0" component-emitter "1.2.1" debug "2.3.3" - engine.io-client "1.8.2" + engine.io-client "1.8.3" has-binary "0.1.7" indexof "0.0.1" object-component "0.0.3" @@ -3374,16 +3623,16 @@ socket.io-parser@2.3.1: isarray "0.0.1" json3 "3.3.2" -socket.io@^1.4.5: - version "1.7.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.2.tgz#83bbbdf2e79263b378900da403e7843e05dc3b71" +socket.io@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.3.tgz#b8af9caba00949e568e369f1327ea9be9ea2461b" dependencies: debug "2.3.3" - engine.io "1.8.2" + engine.io "1.8.3" has-binary "0.1.7" object-assign "4.1.0" socket.io-adapter "0.5.0" - socket.io-client "1.7.2" + socket.io-client "1.7.3" socket.io-parser "2.3.1" sort-keys@^1.0.0: @@ -3392,9 +3641,9 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" -source-list-map@~0.1.7: - version "0.1.8" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" +source-list-map@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-1.1.1.tgz#1a33ac210ca144d1e561f906ebccab5669ff4cb4" source-map-support@^0.4.1, source-map-support@^0.4.11: version "0.4.11" @@ -3402,7 +3651,7 @@ source-map-support@^0.4.1, source-map-support@^0.4.11: dependencies: source-map "^0.5.3" -source-map@>=0.5.6, source-map@^0.5.1, source-map@^0.5.3, source-map@~0.5.1: +source-map@>=0.5.6, source-map@^0.5.1, source-map@^0.5.3, source-map@~0.5.1, source-map@~0.5.3: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" @@ -3412,7 +3661,7 @@ source-map@^0.1.41: dependencies: amdefine ">=0.0.4" -source-map@^0.4.4, source-map@~0.4.1: +source-map@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" dependencies: @@ -3476,7 +3725,7 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" -"statuses@>= 1.3.1 < 2", statuses@~1.3.0: +"statuses@>= 1.3.1 < 2", statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" @@ -3505,7 +3754,7 @@ string-template@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" -string-width@^1.0.1: +string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" dependencies: @@ -3564,9 +3813,11 @@ strip-json-comments@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" -supports-color@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" +supports-color@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" + dependencies: + has-flag "^1.0.0" supports-color@^0.2.0: version "0.2.0" @@ -3586,11 +3837,7 @@ symbol-observable@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" -tapable@^0.1.8, tapable@~0.1.8: - version "0.1.10" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" - -tapable@^0.2.5: +tapable@^0.2.5, tapable@~0.2.5: version "0.2.6" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d" @@ -3621,6 +3868,10 @@ term-size@^0.1.0: dependencies: execa "^0.4.0" +text-encoding@0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" + thenify@^3.1.0, thenify@^3.2.0: version "3.2.1" resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.2.1.tgz#251fd1c80aff6e5cf57cb179ab1fcb724269bd11" @@ -3656,6 +3907,12 @@ timers-browserify@^2.0.2: dependencies: setimmediate "^1.0.4" +tmp@0.0.31, tmp@0.0.x: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" + dependencies: + os-tmpdir "~1.0.1" + to-array@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" @@ -3664,10 +3921,6 @@ to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" -to-iso-string@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" - topo@1.x.x: version "1.1.0" resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5" @@ -3760,6 +4013,10 @@ type-detect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" +type-detect@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea" + type-is@~1.6.14: version "1.6.14" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" @@ -3858,14 +4115,14 @@ typings@^2.1.1: wordwrap "^1.0.0" xtend "^4.0.1" -uglify-js@^2.6, uglify-js@~2.7.3: - version "2.7.5" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8" +uglify-js@^2.6, uglify-js@^2.8.5: + version "2.8.22" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.22.tgz#d54934778a8da14903fa29a326fb24c0ab51a1a0" dependencies: - async "~0.2.6" source-map "~0.5.1" - uglify-to-browserify "~1.0.0" yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" uglify-to-browserify@~1.0.0: version "1.0.2" @@ -3923,11 +4180,12 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -useragent@^2.1.6: - version "2.1.11" - resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.1.11.tgz#6a026e6a6c619b46ca7a0b2fdef6c1ac3da8ca29" +useragent@^2.1.12: + version "2.1.13" + resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.1.13.tgz#bba43e8aa24d5ceb83c2937473e102e21df74c10" dependencies: lru-cache "2.2.x" + tmp "0.0.x" util-deprecate@~1.0.1: version "1.0.2" @@ -3937,7 +4195,7 @@ util-extend@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" -util@0.10.3, "util@>=0.10.3 <1", util@^0.10.3: +util@0.10.3, util@^0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" dependencies: @@ -3982,12 +4240,12 @@ void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" -watchpack@^0.2.1: - version "0.2.9" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-0.2.9.tgz#62eaa4ab5e5ba35fdfc018275626e3c0f5e3fb0b" +watchpack@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87" dependencies: - async "^0.9.0" - chokidar "^1.0.0" + async "^2.1.2" + chokidar "^1.4.3" graceful-fs "^4.1.2" wcwidth@^1.0.0: @@ -3996,13 +4254,6 @@ wcwidth@^1.0.0: dependencies: defaults "^1.0.3" -webpack-core@~0.6.9: - version "0.6.9" - resolved "https://registry.yarnpkg.com/webpack-core/-/webpack-core-0.6.9.tgz#fc571588c8558da77be9efb6debdc5a3b172bdc2" - dependencies: - source-list-map "~0.1.7" - source-map "~0.4.1" - webpack-dev-middleware@^1.0.11: version "1.9.0" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.9.0.tgz#a1c67a3dfd8a5c5d62740aa0babe61758b4c84aa" @@ -4012,25 +4263,42 @@ webpack-dev-middleware@^1.0.11: path-is-absolute "^1.0.0" range-parser "^1.0.3" -webpack@^1.13.2: - version "1.14.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-1.14.0.tgz#54f1ffb92051a328a5b2057d6ae33c289462c823" +webpack-sources@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.2.3.tgz#17c62bfaf13c707f9d02c479e0dcdde8380697fb" dependencies: - acorn "^3.0.0" - async "^1.3.0" - clone "^1.0.2" - enhanced-resolve "~0.9.0" - interpret "^0.6.4" - loader-utils "^0.2.11" - memory-fs "~0.3.0" + source-list-map "^1.1.1" + source-map "~0.5.3" + +webpack@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.4.1.tgz#15a91dbe34966d8a4b99c7d656efd92a2e5a6f6a" + dependencies: + acorn "^5.0.0" + acorn-dynamic-import "^2.0.0" + ajv "^4.7.0" + ajv-keywords "^1.1.1" + async "^2.1.2" + enhanced-resolve "^3.0.0" + interpret "^1.0.0" + json-loader "^0.5.4" + json5 "^0.5.1" + loader-runner "^2.3.0" + loader-utils "^0.2.16" + memory-fs "~0.4.1" mkdirp "~0.5.0" - node-libs-browser "^0.7.0" - optimist "~0.6.0" + node-libs-browser "^2.0.0" + source-map "^0.5.3" supports-color "^3.1.0" - tapable "~0.1.8" - uglify-js "~2.7.3" - watchpack "^0.2.1" - webpack-core "~0.6.9" + tapable "~0.2.5" + uglify-js "^2.8.5" + watchpack "^1.3.1" + webpack-sources "^0.2.3" + yargs "^6.0.0" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" which@1.2.x, which@^1.1.1, which@^1.2.8, which@^1.2.9, which@~1.2.10: version "1.2.12" @@ -4066,6 +4334,13 @@ wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -4078,9 +4353,9 @@ write-file-atomic@^1.1.2: imurmurhash "^0.1.4" slide "^1.1.5" -ws@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018" +ws@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.2.tgz#8a244fa052401e08c9886cf44a85189e1fd4067f" dependencies: options ">=0.0.5" ultron "1.0.x" @@ -4107,10 +4382,38 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + yallist@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.0.0.tgz#306c543835f09ee1a4cb23b7bce9ab341c91cdd4" +yargs-parser@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" + dependencies: + camelcase "^3.0.0" + +yargs@^6.0.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^4.2.0" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" From 4395eb1d8f2353a9c5b70f8f43ca1bd217a0a6aa Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Fri, 21 Apr 2017 13:20:10 -0400 Subject: [PATCH 19/56] more actions --- package.json | 4 +- src/flux/actions.ts | 75 +++++++++++++++++--------- src/flux/capacitor.ts | 48 +++++++++-------- src/flux/observer.ts | 19 +++++-- src/flux/store.ts | 44 ++++++++++----- src/flux/utils.ts | 1 + test/unit/flux/actions.ts | 103 ++++++++++++++++++------------------ test/unit/flux/capacitor.ts | 2 +- test/unit/flux/utils.ts | 19 +++++++ tsconfig.json | 3 +- webpack.config.js | 2 +- yarn.lock | 77 ++++++--------------------- 12 files changed, 216 insertions(+), 181 deletions(-) create mode 100644 src/flux/utils.ts create mode 100644 test/unit/flux/utils.ts diff --git a/package.json b/package.json index 1d3308b..b037a79 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "mocha-suite": "^1.0.8", "object-assign": "^4.1.0", "phantomjs-prebuilt": "^2.1.7", - "pre-commit": "^1.1.3", "remap-istanbul": "^0.9.5", "rimraf": "^2.5.4", "sinon": "^2.1.0", @@ -77,7 +76,6 @@ "@types/axios": "^0.9.35", "@types/clone": "^0.1.30", "@types/deep-equal": "^1.0.0", - "@types/eventemitter3": "^2.0.2", "@types/qs": "^6.2.31", "array.prototype.find": "^2.0.0", "array.prototype.findindex": "^2.0.0", @@ -87,7 +85,7 @@ "es6-object-assign": "^1.0.3", "es6-promise": "^3.3.1", "es6-symbol": "^3.1.1", - "eventemitter3": "^1.2.0", + "eventemitter3": "^2.0.3", "filter-object": "^2.1.0", "lodash.range": "^3.2.0", "qs": "^6.1.0", diff --git a/src/flux/actions.ts b/src/flux/actions.ts index c891730..0ffef5b 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -1,47 +1,70 @@ import { Dispatch } from 'redux'; import { Request } from '../models/request'; import Store from './store'; +import { thunk } from './utils'; namespace Actions { - export const UPDATE_SEARCH_REQUEST = 'UPDATE_SEARCH_REQUEST'; - export const UPDATE_QUERY = 'UPDATE_QUERY'; - export const SELECT_REFINEMENT = 'SELECT_REFINEMENT'; + export const UPDATE_SEARCH = 'UPDATE_SEARCH'; export const DESELECT_REFINEMENT = 'DESELECT_REFINEMENT'; export const SELECT_COLLECTION = 'SELECT_COLLECTION'; - export const DESELECT_COLLECTION = 'DESELECT_COLLECTION'; + export const UPDATE_SORTS = 'UPDATE_SORTS'; + export const UPDATE_AUTOCOMPLETE_QUERY = 'UPDATE_AUTOCOMPLETE_QUERY'; + export const UPDATE_PAGE_SIZE = 'UPDATE_PAGE_SIZE'; + export const UPDATE_CURRENT_PAGE = 'UPDATE_CURRENT_PAGE'; + export const UPDATE_DETAILS_ID = 'UPDATE_DETAILS_ID'; - export function search(request: Request) { - return (dispatch: Dispatch, getState: () => Store.State) => { - const data = getState().data; + export const updateSearch = (search: Actions.Search) => + thunk(UPDATE_SEARCH, search); - // conditional(data.query.original !== request.query, () => updateQuery(request.query)) - // .then(() => conditional()); + export const deselectRefinement = (navigationId: string, index: number) => + thunk(DESELECT_REFINEMENT, { navigationId, index }); - return dispatch({ - type: UPDATE_SEARCH_REQUEST, - request - }); - }; - } + export const selectCollection = (id: string) => + thunk(SELECT_COLLECTION, { id }); + + export const updateSorts = (sorts: Store.Sort[]) => + thunk(UPDATE_SORTS, { sorts }); + + export const updateAutocompleteQuery = (query: string) => + thunk(UPDATE_AUTOCOMPLETE_QUERY, { query }); - export const updateQuery = (query: string) => Actions.thunk(UPDATE_QUERY, { query }); + export const updatePageSize = (size: number) => + thunk(UPDATE_PAGE_SIZE, { size }); - export const selectRefinement = (data: { navigationId: string, refinementIndex: number }) => - Actions.thunk(SELECT_REFINEMENT, data); + export const updateCurrentPage = (page: number) => + thunk(UPDATE_CURRENT_PAGE, { page }); - export const deselectRefinement = (data: { navigationId: string, refinementIndex: number }) => - Actions.thunk(DESELECT_REFINEMENT, data); + export const updateDetailsId = (id: string) => + thunk(UPDATE_DETAILS_ID, { id }); - export const selectCollection = (data: { collectionId: string }) => Actions.thunk(SELECT_COLLECTION, data); + export interface Search { + query?: string; + refinements?: Search.Refinement[]; - export const deselectCollection = (data: { collectionId: string }) => Actions.thunk(DESELECT_COLLECTION, data); + /** + * only for refinements + * if true, replace refinements with the provided ones + * if false, add the provided refinements + */ + clear?: boolean; + } + + export namespace Search { + export type Refinement = ValueRefinement | RangeRefinement; - // utilities + export interface BaseRefinement { + field: string; + } - export const conditional = (condition: boolean, action: Function) => - condition ? action() : Promise.resolve(); + export interface ValueRefinement extends BaseRefinement { + value: string; + } - export const thunk = (type: string, data: any) => (dispatch) => dispatch({ type, ...data }); + export interface RangeRefinement extends BaseRefinement { + low?: number; + high?: number; + } + } } export default Actions; diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index c2dfa65..b942936 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -1,4 +1,4 @@ -import * as EventEmitter from 'eventemitter3'; +import { EventEmitter } from 'eventemitter3'; import * as redux from 'redux'; import filterObject = require('filter-object'); import { BrowserBridge } from '../core/bridge'; @@ -27,11 +27,21 @@ export namespace Events { export const NAVIGATIONS_UPDATED = 'navigations_updated'; // post export const NAVIGATION_UPDATED = 'navigation_updated'; // post - export const AUTOCOMPLETE_QUERIES_UPDATED = 'autocomplete_queries_updated'; + export const AUTOCOMPLETE_QUERY_UPDATED = 'autocomplete_query_updated'; + export const AUTOCOMPLETE_SUGGESTIONS_UPDATED = 'autocomplete_suggestions_updated'; export const AUTOCOMPLETE_CATEGORIES_UPDATED = 'autocomplete_categories_updated'; export const AUTOCOMPLETE_CATEGORY_UPDATED = 'autocomplete_category_updated'; export const AUTOCOMPLETE_PRODUCTS_UPDATED = 'autocomplete_products_updated'; + export const TEMPLATE_UPDATED = 'template_updated'; + + export const DETAILS_ID_UPDATED = 'details_id_updated'; // pre + export const DETAILS_PRODUCT_UPDATED = 'details_product_updated'; // post + + export const PAGE_SIZE_UPDATED = 'page_size_updated'; // pre + export const CURRENT_PAGE_UPDATED = 'current_page_updated'; // pre + export const PAGE_UPDATED = 'page_updated'; // post + export const REDIRECT = 'redirect'; } @@ -74,28 +84,24 @@ export class FluxCapacitor extends EventEmitter { this.page = new Pager(this); } - search(query: string = this.originalQuery) { + search(originalQuery: string = this.originalQuery): Promise { + this.query.withQuery(originalQuery); + this.emit(Events.SEARCH, this.query.raw); + return this.bridge.search(this.query) + .then((results) => { + const oldQuery = this.originalQuery; + Object.assign(this, { results, originalQuery }); + + if (results.redirect) { + this.emit(Events.REDIRECT, results.redirect); + } + this.emit(Events.RESULTS, results); + this.emitQueryChanged(oldQuery, originalQuery); + return results; + }); } - // search(originalQuery: string = this.originalQuery): Promise { - // this.query.withQuery(originalQuery); - // this.emit(Events.SEARCH, this.query.raw); - // return this.bridge.search(this.query) - // .then((results) => { - // const oldQuery = this.originalQuery; - // Object.assign(this, { results, originalQuery }); - // - // if (results.redirect) { - // this.emit(Events.REDIRECT, results.redirect); - // } - // this.emit(Events.RESULTS, results); - // this.emitQueryChanged(oldQuery, originalQuery); - // - // return results; - // }); - // } - refinements(navigationName: string): Promise { return this.bridge.refinements(this.query, navigationName) .then((results) => { diff --git a/src/flux/observer.ts b/src/flux/observer.ts index 8276924..0019237 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -34,7 +34,7 @@ namespace Observer { observer(oldState, newState); } - if (INDEXED in observer && oldState.allIds === newState.allIds) { + if (INDEXED in observer && 'allIds' in newState && oldState.allIds === newState.allIds) { Object.keys(newState.allIds) .forEach((key) => Observer.resolveIndexed(oldState.byId[key], newState.byId[key], observer['indexed'])); } else { @@ -68,7 +68,7 @@ namespace Observer { rewrites: emit(Events.QUERY_REWRITES_UPDATED), }), - sorts: indexed(Events.SORTS_UPDATED, Events.SORT_UPDATED, 'field'), + sorts: emit(Events.SORTS_UPDATED), products: indexed(Events.PRODUCTS_UPDATED, Events.PRODUCT_UPDATED, 'id'), @@ -77,11 +77,24 @@ namespace Observer { navigations: indexed(Events.NAVIGATIONS_UPDATED, Events.NAVIGATION_UPDATED, 'field'), autocomplete: { - queries: emit(Events.AUTOCOMPLETE_QUERIES_UPDATED), + query: emit(Events.AUTOCOMPLETE_QUERY_UPDATED), + suggestions: emit(Events.AUTOCOMPLETE_SUGGESTIONS_UPDATED), categories: indexed(Events.AUTOCOMPLETE_CATEGORIES_UPDATED, Events.AUTOCOMPLETE_CATEGORY_UPDATED, 'field'), products: emit(Events.AUTOCOMPLETE_PRODUCTS_UPDATED) }, + page: Object.assign(emit(Events.PAGE_UPDATED), { + size: emit(Events.PAGE_SIZE_UPDATED), + current: emit(Events.CURRENT_PAGE_UPDATED), + }), + + template: emit(Events.TEMPLATE_UPDATED), + + details: { + id: emit(Events.DETAILS_ID_UPDATED), + product: emit(Events.DETAILS_PRODUCT_UPDATED) + }, + reditect: emit(Events.REDIRECT), } }; diff --git a/src/flux/store.ts b/src/flux/store.ts index ccde797..7c2e959 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -10,9 +10,9 @@ namespace Store { data?: { query: Query; // mixed - sorts: Indexed; // pre + sorts: Sort[]; // pre products: Indexed; // post - collections: Indexed; // mixed + collections: Indexed.Selectable; // mixed navigations: Indexed; // mixed autocomplete: Autocomplete; // mixed @@ -21,7 +21,7 @@ namespace Store { template: Template; // post - details: Details; // post + details: Details; // mixed redirect?: string; // post @@ -59,7 +59,6 @@ namespace Store { name: string; // static label: string; // static total: number; // post - selected?: boolean; // pre } export interface Sort { @@ -76,7 +75,7 @@ namespace Store { /** * current page number */ - current: number; // post + current: number; // pre /** * number of next page @@ -98,11 +97,11 @@ namespace Store { /** * start of displayed products */ - fromResult: number; // post + from: number; // post /** * end of displayed products */ - toResult: number; // post + to: number; // post /** * displayed number range (in ) @@ -120,23 +119,28 @@ namespace Store { export type Zone = ContentZone | RichContentZone | RecordZone; - export interface ContentZone { + export interface BaseZone { + name: string; + } + + export interface ContentZone extends BaseZone { type: 'content'; content: string; } - export interface RichContentZone { + export interface RichContentZone extends BaseZone { type: 'rich_content'; content: string; } - export interface RecordZone { + export interface RecordZone extends BaseZone { type: 'record'; products: Product[]; } export interface Details { - product: Product; + id: string; // pre + product: Product; // post } export interface Product { @@ -151,6 +155,7 @@ namespace Store { label: string; // post or?: boolean; // post sort?: Sort; // post + selected: number[]; // pre } export interface ValueNavigation extends BaseNavigation { @@ -165,7 +170,6 @@ namespace Store { export interface BaseRefinement { total: number; // post - selected?: boolean; // pre } export type Refinement = ValueRefinement | RangeRefinement; @@ -180,12 +184,18 @@ namespace Store { } export interface Autocomplete { - queries: string[]; // post + query: string; // pre + suggestions: Autocomplete.Suggestion[]; // post categories: Indexed; // static & post - products: any[]; // post + products: Product[]; // post } export namespace Autocomplete { + export interface Suggestion { + value: string; + selected?: boolean; + } + export interface Category { field: string; // static values: string[]; // post @@ -197,6 +207,12 @@ namespace Store { allIds: string[]; } + export namespace Indexed { + export interface Selectable extends Indexed { + selected: string; + } + } + export function create() { return redux.createStore( reducer, diff --git a/src/flux/utils.ts b/src/flux/utils.ts new file mode 100644 index 0000000..050c74e --- /dev/null +++ b/src/flux/utils.ts @@ -0,0 +1 @@ +export const thunk = (type: string, data: any) => (dispatch) => dispatch({ type, ...data }); diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts index e6323ae..92e0ebd 100644 --- a/test/unit/flux/actions.ts +++ b/test/unit/flux/actions.ts @@ -1,93 +1,94 @@ import Actions from '../../../src/flux/actions'; +import * as utils from '../../../src/flux/utils'; import suite from '../_suite'; -suite.only('actions', ({ expect, spy, stub }) => { - describe('conditional()', () => { - it('should return result of action', () => { - const obj = { a: 'b' }; +suite('actions', ({ expect, spy, stub }) => { + describe('updateSearch()', () => { + it('should create an UPDATE_SEARCH action', () => { + const data: any = { a: 'b' }; + const thunk = stub(utils, 'thunk'); - const result = Actions.conditional(true, () => obj); + Actions.updateSearch(data); - expect(result).to.eq(obj); + expect(thunk).to.be.calledWith(Actions.UPDATE_SEARCH, data); }); + }); - it('should return a resolved Promise', () => { - const obj = { a: 'b' }; + describe('deselectRefinement()', () => { + it('should create a DESELECT_REFINEMENT action', () => { + const navigationId = 'brand'; + const index = 3; + const thunk = stub(utils, 'thunk'); - const result = Actions.conditional(false, () => obj); + Actions.deselectRefinement(navigationId, index); - expect(result).to.not.eq(obj); - expect(result).to.be.an.instanceof(Promise); + expect(thunk).to.be.calledWith(Actions.DESELECT_REFINEMENT, { navigationId, index }); }); }); - describe('thunk()', () => { - it('should return a constructed thunk', () => { - const dispatch = spy(); - const type = 'MY_ACTION'; - - const thunk = Actions.thunk(type, { a: 'b' }); - - expect(thunk).to.be.a('function'); + describe('selectCollection()', () => { + it('should create a SELECT_COLLECTION action', () => { + const id = 'products'; + const thunk = stub(utils, 'thunk'); - thunk(dispatch); + Actions.selectCollection(id); - expect(dispatch).to.be.calledWith({ type, a: 'b' }); + expect(thunk).to.be.calledWith(Actions.SELECT_COLLECTION, { id }); }); }); - describe('updateQuery()', () => { - it('should create an UPDATE_QUERY action', () => { - const query = 'red apple'; - const thunk = stub(Actions, 'thunk'); + describe('updateAutocompleteQuery()', () => { + it('should create an UPDATE_AUTOCOMPLETE_QUERY action', () => { + const query = 'William Shake'; + const thunk = stub(utils, 'thunk'); - Actions.updateQuery(query); + Actions.updateAutocompleteQuery(query); - expect(thunk).to.be.calledWith(Actions.UPDATE_QUERY, { query }); + expect(thunk).to.be.calledWith(Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }); }); }); - describe('selectRefinement()', () => { - it('should create a SELECT_REFINEMENT action', () => { - const refinement: any = { id: 1 }; - const thunk = stub(Actions, 'thunk'); + describe('updateSorts()', () => { + it('should create an UPDATE_SORTS action', () => { + const sorts: any[] = [{ a: 'b' }]; + const thunk = stub(utils, 'thunk'); - Actions.selectRefinement(refinement); + Actions.updateSorts(sorts); - expect(thunk).to.be.calledWith(Actions.SELECT_REFINEMENT, refinement); + expect(thunk).to.be.calledWith(Actions.UPDATE_SORTS, { sorts }); }); }); - describe('deselectRefinement()', () => { - it('should create a DESELECT_REFINEMENT action', () => { - const refinement: any = { id: 1 }; - const thunk = stub(Actions, 'thunk'); + describe('updatePageSize()', () => { + it('should create an UPDATE_PAGE_SIZE action', () => { + const size = 34; + const thunk = stub(utils, 'thunk'); - Actions.deselectRefinement(refinement); + Actions.updatePageSize(size); - expect(thunk).to.be.calledWith(Actions.DESELECT_REFINEMENT, refinement); + expect(thunk).to.be.calledWith(Actions.UPDATE_PAGE_SIZE, { size }); }); }); - describe('selectCollection()', () => { - it('should create a SELECT_COLLECTION action', () => { - const collection: any = { id: 1 }; - const thunk = stub(Actions, 'thunk'); + describe('updateCurrentPage()', () => { + it('should create an UPDATE_CURRENT_PAGE action', () => { + const page = 4; + const thunk = stub(utils, 'thunk'); - Actions.selectCollection(collection); + Actions.updateCurrentPage(page); - expect(thunk).to.be.calledWith(Actions.SELECT_COLLECTION, collection); + expect(thunk).to.be.calledWith(Actions.UPDATE_CURRENT_PAGE, { page }); }); }); - describe('deselectCollection()', () => { - it('should create a DESELECT_COLLECTION action', () => { - const collection: any = { id: 1 }; - const thunk = stub(Actions, 'thunk'); + describe('updateDetailsId()', () => { + it('should create an UPDATE_CURRENT_PAGE action', () => { + const id = '123'; + const thunk = stub(utils, 'thunk'); - Actions.deselectCollection(collection); + Actions.updateDetailsId(id); - expect(thunk).to.be.calledWith(Actions.DESELECT_COLLECTION, collection); + expect(thunk).to.be.calledWith(Actions.UPDATE_DETAILS_ID, { id }); }); }); }); diff --git a/test/unit/flux/capacitor.ts b/test/unit/flux/capacitor.ts index b119684..c256ad4 100644 --- a/test/unit/flux/capacitor.ts +++ b/test/unit/flux/capacitor.ts @@ -9,7 +9,7 @@ const SELECTED_REFINEMENT: SelectedValueRefinement = { type: 'Value', navigation const REFINEMENT_RESULT = { availableNavigation: 'a', selectedNavigation: 'b' }; const DETAILS_RESULT = { records: [{}] }; -suite('FluxCapacitor', ({ expect, spy }) => { +suite.skip('FluxCapacitor', ({ expect, spy }) => { let flux: FluxCapacitor; beforeEach(() => { diff --git a/test/unit/flux/utils.ts b/test/unit/flux/utils.ts new file mode 100644 index 0000000..739e291 --- /dev/null +++ b/test/unit/flux/utils.ts @@ -0,0 +1,19 @@ +import * as utils from '../../../src/flux/utils'; +import suite from '../_suite'; + +suite('utils', ({ expect, spy }) => { + describe('thunk()', () => { + it('should return a constructed thunk', () => { + const dispatch = spy(); + const type = 'MY_ACTION'; + + const thunk = utils.thunk(type, { a: 'b' }); + + expect(thunk).to.be.a('function'); + + thunk(dispatch); + + expect(dispatch).to.be.calledWith({ type, a: 'b' }); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index d85ca19..dbb9a51 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "lib": [ "ES5", - "ES6" + "ES6", + "ES2016.Array.Include" ], "module": "commonjs", "moduleResolution": "node", diff --git a/webpack.config.js b/webpack.config.js index 86a7651..1cdaef1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -22,7 +22,7 @@ module.exports = { enforce: 'pre', loader: 'tslint-loader', options: { - typeCheck: true + // typeCheck: true } }, { test: /\.ts$/, diff --git a/yarn.lock b/yarn.lock index dfc6934..c36962a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,12 +14,6 @@ version "1.0.0" resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.0.tgz#9ebeaa73d1fc4791f038a5f1440e0449ea968495" -"@types/eventemitter3@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/eventemitter3/-/eventemitter3-2.0.2.tgz#94b57c2568c4f09479d64812f625317b12a6edd0" - dependencies: - eventemitter3 "*" - "@types/fs-extra@0.0.33": version "0.0.33" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-0.0.33.tgz#a8719c417b080c012d3497b28e228ac09745fdf2" @@ -829,14 +823,6 @@ cross-spawn-async@^2.1.1: lru-cache "^4.0.0" which "^1.2.8" -cross-spawn@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.0.1.tgz#a3bbb302db2297cbea3c04edf36941f4613aa399" - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -899,7 +885,13 @@ debug@0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" -debug@2, debug@2.2.0, debug@^2.2.0, debug@~2.2.0: +debug@2, debug@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" + dependencies: + ms "0.7.2" + +debug@2.2.0, debug@^2.2.0, debug@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" dependencies: @@ -917,12 +909,6 @@ debug@2.6.1: dependencies: ms "0.7.2" -debug@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" - dependencies: - ms "0.7.2" - debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -1205,14 +1191,10 @@ escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" -escape-string-regexp@1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" - escodegen@1.8.x: version "1.8.1" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" @@ -1240,10 +1222,14 @@ esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" -eventemitter3@*, eventemitter3@1.x.x, eventemitter3@^1.2.0: +eventemitter3@1.x.x: version "1.2.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" +eventemitter3@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" + events@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -2560,7 +2546,7 @@ lru-cache@2.2.x: version "2.2.4" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" -lru-cache@^4.0.0, lru-cache@^4.0.1: +lru-cache@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" dependencies: @@ -2933,10 +2919,6 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" -os-shim@^0.1.2: - version "0.1.3" - resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" - os-tmpdir@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -3106,14 +3088,6 @@ popsicle@^9.0.0: make-error-cause "^1.2.1" tough-cookie "^2.0.0" -pre-commit@^1.1.3: - version "1.2.2" - resolved "https://registry.yarnpkg.com/pre-commit/-/pre-commit-1.2.2.tgz#dbcee0ee9de7235e57f79c56d7ce94641a69eec6" - dependencies: - cross-spawn "^5.0.1" - spawn-sync "^1.0.15" - which "1.2.x" - prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -3474,13 +3448,13 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.6.0: +rimraf@2, rimraf@^2.4.4, rimraf@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: glob "^7.0.5" -rimraf@^2.4.4, rimraf@^2.5.4, rimraf@~2.5.1, rimraf@~2.5.4: +rimraf@^2.5.4, rimraf@~2.5.1, rimraf@~2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: @@ -3538,16 +3512,6 @@ sha.js@^2.3.6: dependencies: inherits "^2.0.1" -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - shelljs@^0.7.0: version "0.7.6" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad" @@ -3685,13 +3649,6 @@ sparkles@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" -spawn-sync@^1.0.15: - version "1.0.15" - resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" - dependencies: - concat-stream "^1.4.7" - os-shim "^0.1.2" - spdx-correct@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" @@ -4300,7 +4257,7 @@ which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" -which@1.2.x, which@^1.1.1, which@^1.2.8, which@^1.2.9, which@~1.2.10: +which@^1.1.1, which@^1.2.8, which@~1.2.10: version "1.2.12" resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" dependencies: From 7866aa1be09acb3f3eee3fbb90087c0dfc486433 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Fri, 21 Apr 2017 15:07:35 -0400 Subject: [PATCH 20/56] remove unnecessary events --- src/flux/capacitor.ts | 28 +++++++++++++++++----------- src/flux/observer.ts | 32 +++++++++++++++++++------------- src/flux/store.ts | 11 +++-------- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index b942936..206d6c8 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -8,6 +8,7 @@ import { Navigation, RefinementResults, Results } from '../models/response'; import { Pager } from './pager'; export namespace Events { + // query events export const QUERY_UPDATED = 'query_updated'; // mixed export const ORIGINAL_QUERY_UPDATED = 'original_query_updated'; // pre export const CORRECTED_QUERY_UPDATED = 'corrected_query_updated'; // post @@ -15,33 +16,38 @@ export namespace Events { export const DID_YOU_MEANS_UPDATED = 'did_you_means_updated'; // post export const QUERY_REWRITES_UPDATED = 'query_rewrites_updated'; // post + // sort events export const SORTS_UPDATED = 'sorts_updated'; // mixed - export const SORT_UPDATED = 'sort_updated'; // mixed + // product events export const PRODUCTS_UPDATED = 'products_updated'; // mixed - export const PRODUCT_UPDATED = 'product_updated'; // mixed - export const COLLECTIONS_UPDATED = 'collections_updated'; // mixed + // collection events export const COLLECTION_UPDATED = 'collection_updated'; // post + export const SELECTED_COLLECTION_UPDATED = 'selected_collection_updated'; // post + // navigation events export const NAVIGATIONS_UPDATED = 'navigations_updated'; // post - export const NAVIGATION_UPDATED = 'navigation_updated'; // post + export const SELECTED_REFINEMENTS_UPDATED = 'selected_refinements_updated'; // post - export const AUTOCOMPLETE_QUERY_UPDATED = 'autocomplete_query_updated'; - export const AUTOCOMPLETE_SUGGESTIONS_UPDATED = 'autocomplete_suggestions_updated'; - export const AUTOCOMPLETE_CATEGORIES_UPDATED = 'autocomplete_categories_updated'; - export const AUTOCOMPLETE_CATEGORY_UPDATED = 'autocomplete_category_updated'; - export const AUTOCOMPLETE_PRODUCTS_UPDATED = 'autocomplete_products_updated'; + // autocomplete events + export const AUTOCOMPLETE_UPDATED = 'autocomplete_updated'; // post + export const AUTOCOMPLETE_QUERY_UPDATED = 'autocomplete_query_updated'; // pre + export const AUTOCOMPLETE_PRODUCTS_UPDATED = 'autocomplete_products_updated'; // post - export const TEMPLATE_UPDATED = 'template_updated'; + // template events + export const TEMPLATE_UPDATED = 'template_updated'; // post + // details events export const DETAILS_ID_UPDATED = 'details_id_updated'; // pre export const DETAILS_PRODUCT_UPDATED = 'details_product_updated'; // post + // page events + export const PAGE_UPDATED = 'page_updated'; // post export const PAGE_SIZE_UPDATED = 'page_size_updated'; // pre export const CURRENT_PAGE_UPDATED = 'current_page_updated'; // pre - export const PAGE_UPDATED = 'page_updated'; // post + // redirect event export const REDIRECT = 'redirect'; } diff --git a/src/flux/observer.ts b/src/flux/observer.ts index 0019237..2a00c86 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -36,11 +36,11 @@ namespace Observer { if (INDEXED in observer && 'allIds' in newState && oldState.allIds === newState.allIds) { Object.keys(newState.allIds) - .forEach((key) => Observer.resolveIndexed(oldState.byId[key], newState.byId[key], observer['indexed'])); - } else { - Object.keys(observer) - .forEach((key) => Observer.resolve((oldState || {})[key], (newState || {})[key], observer[key])); + .forEach((key) => Observer.resolveIndexed(oldState.byId[key], newState.byId[key], observer[INDEXED])); } + + Object.keys(observer) + .forEach((key) => Observer.resolve((oldState || {})[key], (newState || {})[key], observer[key])); } } @@ -54,8 +54,7 @@ namespace Observer { const emit = (event: string) => (_, newValue) => flux.emit(event, newValue); const indexed = (event: string, prefix: string, field: string) => Object.assign(emit(event), { - INDEXED, - indexed: (_, newIndexed) => flux.emit(`${prefix}:${newIndexed[field]}`, newIndexed) + INDEXED: (_, newIndexed) => flux.emit(`${prefix}:${newIndexed[field]}`, newIndexed) }); return { @@ -70,18 +69,25 @@ namespace Observer { sorts: emit(Events.SORTS_UPDATED), - products: indexed(Events.PRODUCTS_UPDATED, Events.PRODUCT_UPDATED, 'id'), + products: emit(Events.PRODUCTS_UPDATED), - collections: indexed(Events.COLLECTIONS_UPDATED, Events.COLLECTION_UPDATED, 'name'), + collections: { + INDEXED: (_, newIndexed) => flux.emit(`${Events.COLLECTION_UPDATED}:${newIndexed.name}`, newIndexed), + selected: emit(Events.SELECTED_COLLECTION_UPDATED) + }, - navigations: indexed(Events.NAVIGATIONS_UPDATED, Events.NAVIGATION_UPDATED, 'field'), + navigations: Object.assign(emit(Events.NAVIGATIONS_UPDATED), { + INDEXED: (oldNavigation, newNavigation) => { + if (oldNavigation.selected !== newNavigation.selected) { + flux.emit(`${Events.SELECTED_REFINEMENTS_UPDATED}:${newNavigation.field}`, newNavigation.selected); + } + } + }), - autocomplete: { + autocomplete: Object.assign(emit(Events.AUTOCOMPLETE_UPDATED), { query: emit(Events.AUTOCOMPLETE_QUERY_UPDATED), - suggestions: emit(Events.AUTOCOMPLETE_SUGGESTIONS_UPDATED), - categories: indexed(Events.AUTOCOMPLETE_CATEGORIES_UPDATED, Events.AUTOCOMPLETE_CATEGORY_UPDATED, 'field'), products: emit(Events.AUTOCOMPLETE_PRODUCTS_UPDATED) - }, + }), page: Object.assign(emit(Events.PAGE_UPDATED), { size: emit(Events.PAGE_SIZE_UPDATED), diff --git a/src/flux/store.ts b/src/flux/store.ts index 7c2e959..d901c19 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -11,7 +11,7 @@ namespace Store { query: Query; // mixed sorts: Sort[]; // pre - products: Indexed; // post + products: Product[]; // post collections: Indexed.Selectable; // mixed navigations: Indexed; // mixed @@ -185,17 +185,12 @@ namespace Store { export interface Autocomplete { query: string; // pre - suggestions: Autocomplete.Suggestion[]; // post - categories: Indexed; // static & post + suggestions: string[]; // post + categories: Autocomplete.Category[]; // static & post products: Product[]; // post } export namespace Autocomplete { - export interface Suggestion { - value: string; - selected?: boolean; - } - export interface Category { field: string; // static values: string[]; // post From b901e1b4425dcd6ac1271fefa5b9ea34aac55d38 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Fri, 21 Apr 2017 15:09:28 -0400 Subject: [PATCH 21/56] actions --- src/flux/actions.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 0ffef5b..9753dc1 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -5,6 +5,7 @@ import { thunk } from './utils'; namespace Actions { export const UPDATE_SEARCH = 'UPDATE_SEARCH'; + export const SELECT_REFINEMENT = 'SELECT_REFINEMENT'; export const DESELECT_REFINEMENT = 'DESELECT_REFINEMENT'; export const SELECT_COLLECTION = 'SELECT_COLLECTION'; export const UPDATE_SORTS = 'UPDATE_SORTS'; @@ -16,6 +17,9 @@ namespace Actions { export const updateSearch = (search: Actions.Search) => thunk(UPDATE_SEARCH, search); + export const selectRefinement = (navigationId: string, index: number) => + thunk(SELECT_REFINEMENT, { navigationId, index }); + export const deselectRefinement = (navigationId: string, index: number) => thunk(DESELECT_REFINEMENT, { navigationId, index }); From 419648271eed090f3e13464518f8136c02543a48 Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Fri, 21 Apr 2017 16:51:42 -0400 Subject: [PATCH 22/56] Update search --- src/flux/reducer.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts index 0336707..cca9b2f 100644 --- a/src/flux/reducer.ts +++ b/src/flux/reducer.ts @@ -3,11 +3,8 @@ import Actions from './actions'; import Store from './store'; export function updateQuery(state: Store.Query, action) { - switch (action) { - case Actions.UPDATE_QUERY: - return { ...state, query: action.query }; - default: - return state; + if (action === Actions.UPDATE_SEARCH) { + return { ...state, query: action.query }; } } From 14daa19e706821fbf6d71c4de4a8fdeee7a0e111 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Mon, 24 Apr 2017 09:30:06 -0400 Subject: [PATCH 23/56] update linting --- .gitignore | 6 +- src/flux/capacitor.ts | 22 ++- test/bootstrap.ts | 2 +- test/fixtures.ts | 120 +++++++------- test/karma.entry.ts | 1 + test/tslint.json | 6 + test/unit/_suite.ts | 2 +- test/unit/core/bridge.ts | 18 +-- test/unit/core/query.ts | 54 +++---- test/unit/flux/capacitor.ts | 14 +- test/unit/flux/observer.ts | 311 +++++++++++++++++++----------------- test/unit/flux/pager.ts | 4 +- tslint.json | 74 +-------- 13 files changed, 301 insertions(+), 333 deletions(-) create mode 100644 test/tslint.json diff --git a/.gitignore b/.gitignore index fd4dd01..522438e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,10 @@ tmp node_modules npm-debug.log typings -src/**/*.js* -test/**/*.js* +src/**/*.js +src/**/*.js.map +test/**/*.js +test/**/*.js.map coverage dist docs diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index 206d6c8..eeee690 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -80,10 +80,14 @@ export class FluxCapacitor extends EventEmitter { const bridgeConfig: FluxBridgeConfig = config.bridge || {}; this.bridge = new BrowserBridge(endpoint, bridgeConfig.https, bridgeConfig); - if (bridgeConfig.headers) this.bridge.headers = bridgeConfig.headers; + if (bridgeConfig.headers) { + this.bridge.headers = bridgeConfig.headers; + } this.bridge.errorHandler = (err) => { this.emit(Events.ERROR_BRIDGE, err); - if (bridgeConfig.errorHandler) bridgeConfig.errorHandler(err); + if (bridgeConfig.errorHandler) { + bridgeConfig.errorHandler(err); + } }; this.query = new Query().withConfiguration(filterObject(config, ['*', '!{bridge}']), mask); @@ -162,13 +166,17 @@ export class FluxCapacitor extends EventEmitter { refine(refinement: FluxRefinement, config: RefinementConfig = { reset: true }): Promise { this.query.withSelectedRefinements(refinement); - if (config.skipSearch) return Promise.resolve(this.navigationInfo); + if (config.skipSearch) { + return Promise.resolve(this.navigationInfo); + } return this.doRefinement(config); } unrefine(refinement: FluxRefinement, config: RefinementConfig = { reset: true }): Promise { this.query.withoutSelectedRefinements(refinement); - if (config.skipSearch) return Promise.resolve(this.navigationInfo); + if (config.skipSearch) { + return Promise.resolve(this.navigationInfo); + } return this.doRefinement(config); } @@ -178,7 +186,9 @@ export class FluxCapacitor extends EventEmitter { .withSelectedRefinements({ type: 'Value', navigationName, value: id }) .withPageSize(1)) .then((res) => { - if (res.records.length) this.emit(Events.DETAILS, res.records[0]); + if (res.records.length) { + this.emit(Events.DETAILS, res.records[0]); + } return res; }); } @@ -215,7 +225,7 @@ export class FluxCapacitor extends EventEmitter { private get navigationInfo(): NavigationInfo { return { available: this.results.availableNavigation, - selected: this.results.selectedNavigation + selected: this.results.selectedNavigation, }; } } diff --git a/test/bootstrap.ts b/test/bootstrap.ts index b412c01..92cd316 100644 --- a/test/bootstrap.ts +++ b/test/bootstrap.ts @@ -1,5 +1,5 @@ -import '../src/polyfills'; import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; +import '../src/polyfills'; chai.use(sinonChai); diff --git a/test/fixtures.ts b/test/fixtures.ts index cb67e95..806cab9 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -1,127 +1,127 @@ -export let COMPLEX_REQUEST = { - query: 'complex', - sort: [ - { field: 'price', order: 'Ascending' }, - { field: 'boost', order: 'Descending' } - ], - fields: ['title', 'description'], - orFields: ['brand', 'colour'], +export const COMPLEX_REQUEST = { + area: 'Development', + biasing: { + augmentBiases: true, + biases: [{ name: 'popularity', strength: 'Strong_Decrease' }], + }, + biasingProfile: 'boost top brands', + collection: 'dev', customUrlParams: [ { key: 'banner', value: 'nike_landing' }, - { key: 'style', value: 'branded' } + { key: 'style', value: 'branded' }, ], - includedNavigations: ['brand', 'size'], + disableAutocorrection: true, excludedNavigations: ['_meta', 'originalPrice'], - wildcardSearchEnabled: true, - pruneRefinements: false, - userId: '13afasd', + fields: ['title', 'description'], + includedNavigations: ['brand', 'size'], language: 'en', - collection: 'dev', - area: 'Development', - biasingProfile: 'boost top brands', + matchStrategy: { + rules: [{ terms: 5, termsGreaterThan: 7 }], + }, + orFields: ['brand', 'colour'], pageSize: 300, - skip: 40, + pruneRefinements: false, + query: 'complex', restrictNavigation: { + count: 10, name: 'brand', - count: 10 - }, - matchStrategy: { - rules: [{ terms: 5, termsGreaterThan: 7 }] }, - biasing: { - augmentBiases: true, - biases: [{ name: 'popularity', strength: 'Strong_Decrease' }] - }, - disableAutocorrection: true, - returnBinary: false -}; - -export let BULK_REQUEST = { - query: 'bulk', + returnBinary: false, + skip: 40, sort: [ { field: 'price', order: 'Ascending' }, - { field: 'boost', order: 'Descending' } + { field: 'boost', order: 'Descending' }, ], - fields: ['title', 'description'], - orFields: ['brand', 'colour'], + userId: '13afasd', + wildcardSearchEnabled: true, +}; + +export const BULK_REQUEST = { customUrlParams: [ { key: 'banner', value: 'nike_landing' }, - { key: 'style', value: 'branded' } + { key: 'style', value: 'branded' }, ], - includedNavigations: ['brand', 'size'], excludedNavigations: ['_meta', 'originalPrice'], + fields: ['title', 'description'], + includedNavigations: ['brand', 'size'], + orFields: ['brand', 'colour'], + pruneRefinements: true, + query: 'bulk', + sort: [ + { field: 'price', order: 'Ascending' }, + { field: 'boost', order: 'Descending' }, + ], wildcardSearchEnabled: false, - pruneRefinements: true }; -export let COMBINED_REFINEMENTS = [ +export const COMBINED_REFINEMENTS = [ { + exclude: false, + high: 13, + low: 1, navigationName: 'size', type: 'Range', - low: 1, - high: 13, - exclude: false }, { + exclude: true, navigationName: 'brand', type: 'Value', value: 'Nike', - exclude: true }, { navigationName: 'material', type: 'Value', - value: 'wool' + value: 'wool', }, { + exclude: false, + high: 2009, + low: 2000, navigationName: 'year', type: 'Range', - low: 2000, - high: 2009, - exclude: false }, { + high: 2011, + low: 2010, navigationName: 'year', type: 'Range', - low: 2010, - high: 2011 }, { + exclude: true, navigationName: 'rating', type: 'Value', value: '****', - exclude: true }, { - navigationName: 'price', - low: 122, + exclude: false, high: 413, + low: 122, + navigationName: 'price', type: 'Range', - exclude: false }, { navigationName: 'rating', type: 'Value', - value: '***' + value: '***', }, { + high: 44, + low: 31, navigationName: 'price', type: 'Range', - low: 31, - high: 44 }, { + high: 100, + low: 89, navigationName: 'price', type: 'Range', - low: 89, - high: 100 - } + }, ]; -export let CUSTOM_PARAMS_FROM_STRING = [ +export const CUSTOM_PARAMS_FROM_STRING = [ { key: 'banner', value: 'nike_landing' }, { key: 'style', value: 'branded' }, { key: 'defaults', value: '' }, { key: 'others', value: '' }, - { key: 'something', value: 'as_well' } + { key: 'something', value: 'as_well' }, ]; diff --git a/test/karma.entry.ts b/test/karma.entry.ts index 38f227c..5827c77 100644 --- a/test/karma.entry.ts +++ b/test/karma.entry.ts @@ -1,3 +1,4 @@ +// tslint:disable ban-types import './bootstrap'; const coreContext = (<{ context?: Function }>require).context('../src', true, /\.ts/); diff --git a/test/tslint.json b/test/tslint.json new file mode 100644 index 0000000..293e06c --- /dev/null +++ b/test/tslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../tslint.json", + "rules": { + "no-unused-expression": [false] + } +} diff --git a/test/unit/_suite.ts b/test/unit/_suite.ts index 449eee6..63c2991 100644 --- a/test/unit/_suite.ts +++ b/test/unit/_suite.ts @@ -10,7 +10,7 @@ export default suite((tests) => { tests({ expect, spy: (...args) => (sandbox.spy)(...args), - stub: (...args) => (sandbox.stub)(...args) + stub: (...args) => (sandbox.stub)(...args), }); }); diff --git a/test/unit/core/bridge.ts b/test/unit/core/bridge.ts index f108e1c..298cef0 100644 --- a/test/unit/core/bridge.ts +++ b/test/unit/core/bridge.ts @@ -1,7 +1,7 @@ +import * as mock from 'xhr-mock'; import { BrowserBridge, CloudBridge } from '../../../src/core/bridge'; import { Query } from '../../../src/core/query'; import suite from '../_suite'; -import * as mock from 'xhr-mock'; const CLIENT_KEY = 'XXX-XXX-XXX-XXX'; const CUSTOMER_ID = 'services'; @@ -27,17 +27,13 @@ suite('Bridge', ({ expect, spy }) => { }); it('should have default values', () => { - expect(bridge.config).to.eql({ - timeout: 1500 - }); + expect(bridge.config).to.eql({ timeout: 1500 }); }); it('should accept configuration', () => { bridge = new CloudBridge(CLIENT_KEY, CUSTOMER_ID, { timeout: 4000 }); - expect(bridge.config).to.eql({ - timeout: 4000 - }); + expect(bridge.config).to.eql({ timeout: 4000 }); }); it('should handle invalid query types', (done) => { @@ -95,15 +91,15 @@ suite('Bridge', ({ expect, spy }) => { }); it('should send a search query and return a promise', (done) => { - mock.post(`https://${CUSTOMER_ID}.groupbycloud.com:443/api/v1/search?size=20&syle=branded&other=`, (req, res) => { + mock.post(`https://${CUSTOMER_ID}.groupbycloud.com:443/api/v1/search?other=&size=20&syle=branded`, (req, res) => { return res.status(200).body('success'); }); query = new Query('skirts') .withQueryParams({ + other: '', size: 20, syle: 'branded', - other: '' }); bridge.search(query) @@ -206,7 +202,7 @@ suite('Bridge', ({ expect, spy }) => { describe('refinements()', () => { it('should send requests to the CORS supported refinements endpoint', (done) => { mock.post(`http://${CUSTOMER_ID}-cors.groupbycloud.com/api/v1/search/refinements`, (req, res) => { - expect(JSON.parse(req['_body'])).to.contain.all.keys('originalQuery', 'navigationName'); + expect(JSON.parse((req)._body)).to.contain.all.keys('originalQuery', 'navigationName'); return res.status(200).body('success'); }); @@ -234,7 +230,7 @@ suite('Bridge', ({ expect, spy }) => { it('should include headers', (done) => { const headers = { a: 'b' }; mock.post(`http://${CUSTOMER_ID}-cors.groupbycloud.com/api/v1/search`, (req, res) => { - expect(req['_headers']).to.include.keys('a'); + expect((req)._headers).to.include.keys('a'); return res.status(200).body('success'); }); diff --git a/test/unit/core/query.ts b/test/unit/core/query.ts index 3b4ec7a..837f42e 100644 --- a/test/unit/core/query.ts +++ b/test/unit/core/query.ts @@ -21,20 +21,20 @@ suite('Query', ({ expect }) => { it('should build a simple request with defaults', () => { const request = query.build(); expect(request).to.eql({ + pruneRefinements: true, query: 'test', wildcardSearchEnabled: false, - pruneRefinements: true }); }); it('should build a complex request', () => { const request = new Query('complex') .withConfiguration({ - userId: '13afasd', - language: 'en', - collection: 'dev', area: 'Development', - biasingProfile: 'boost top brands' + biasingProfile: 'boost top brands', + collection: 'dev', + language: 'en', + userId: '13afasd', }) .withCustomUrlParams([{ key: 'banner', value: 'nike_landing' }, { key: 'style', value: 'branded' }]) .withFields('title', 'description') @@ -43,21 +43,21 @@ suite('Query', ({ expect }) => { .withExcludedNavigations('_meta', 'originalPrice') .withQueryParams({ attrs: 'size,brand', - id: '' + id: '', }) .withSorts({ field: 'price', order: 'Ascending' }, { field: 'boost', order: 'Descending' }) .withPageSize(300) .skip(40) .restrictNavigation({ + count: 10, name: 'brand', - count: 10 }) .withMatchStrategy({ - rules: [{ terms: 5, termsGreaterThan: 7 }] + rules: [{ terms: 5, termsGreaterThan: 7 }], }) .withBiasing({ augmentBiases: true, - biases: [{ name: 'popularity', strength: 'Strong_Decrease' }] + biases: [{ name: 'popularity', strength: 'Strong_Decrease' }], }) .enableWildcardSearch() .disableAutocorrection() @@ -71,22 +71,22 @@ suite('Query', ({ expect }) => { describe('withConfiguration() behaviour', () => { it('should allow all properties through', () => { const request = query.withConfiguration({ + properties: 'are these', some: 'invalid', - properties: 'are these' }).build(); expect(request).to.eql({ - query: 'test', - wildcardSearchEnabled: false, + properties: 'are these', pruneRefinements: true, + query: 'test', some: 'invalid', - properties: 'are these' + wildcardSearchEnabled: false, }); }); it('should allow a custom mask', () => { const request = query.withConfiguration({ + properties: 'are these', some: 'invalid', - properties: 'are these' }, '{query,other}').build(); expect(request).to.not.have.keys('some', 'properties'); }); @@ -96,40 +96,40 @@ suite('Query', ({ expect }) => { const request = new Query('refinements') .withSelectedRefinements( { + exclude: false, + high: 13, + low: 1, navigationName: 'size', type: 'Range', - low: 1, - high: 13, - exclude: false }, { + exclude: true, navigationName: 'brand', type: 'Value', value: 'Nike', - exclude: true }) .withRefinements('material', { type: 'Value', - value: 'wool' + value: 'wool', }) .withRefinements('year', { - type: 'Range', - low: 2000, + exclude: false, high: 2009, - exclude: false - }, { + low: 2000, type: 'Range', + }, { + high: 2011, low: 2010, - high: 2011 + type: 'Range', }) .withNavigations({ name: 'rating', - refinements: [{ type: 'Value', value: '***' }] + refinements: [{ type: 'Value', value: '***' }], }, { name: 'price', refinements: [ { type: 'Range', low: 31, high: 44 }, - { type: 'Range', low: 89, high: 100 } - ] + { type: 'Range', low: 89, high: 100 }, + ], }) .refineByValue('rating', '****', true) .refineByRange('price', 122, 413) diff --git a/test/unit/flux/capacitor.ts b/test/unit/flux/capacitor.ts index c256ad4..83c858b 100644 --- a/test/unit/flux/capacitor.ts +++ b/test/unit/flux/capacitor.ts @@ -1,6 +1,6 @@ +import * as mock from 'xhr-mock'; import { Events, FluxCapacitor, Results, SelectedValueRefinement, Sort } from '../../../src/index'; import suite from '../_suite'; -import * as mock from 'xhr-mock'; const CUSTOMER_ID = 'services'; const SEARCH_URL = `http://${CUSTOMER_ID}-cors.groupbycloud.com/api/v1/search`; @@ -47,8 +47,8 @@ suite.skip('FluxCapacitor', ({ expect, spy }) => { b: 'Ascending', bridge: { headers: { c: 'd' }, - https: true - } + https: true, + }, }); expect(flux.query.raw).to.not.contain.keys('bridge'); @@ -477,7 +477,9 @@ suite.skip('FluxCapacitor', ({ expect, spy }) => { let count = 0; const checkComplete = () => { - if (++count === 2) done(); // tslint:disable-line:no-constant-condition + if (++count === 2) { + done(); + } }; flux.on(Events.RESET, checkComplete); flux.on(Events.PAGE_CHANGED, checkComplete); @@ -535,7 +537,7 @@ suite.skip('FluxCapacitor', ({ expect, spy }) => { const sorts: Sort[] = [ { field: 'price', order: 'Descending' }, { field: 'other', order: 'Ascending' }, - { field: 'type', order: 'Descending' } + { field: 'type', order: 'Descending' }, ]; flux.query.withSorts(...sorts); mock.post(SEARCH_URL, (req, res) => { @@ -582,8 +584,8 @@ suite.skip('FluxCapacitor', ({ expect, spy }) => { flux.query.withConfiguration({ area: 'nonProd', collection: 'offbrand', + fields: ['title', 'price'], language: 'zh', - fields: ['title', 'price'] }); mock.post(SEARCH_URL, (req, res) => { const body = JSON.parse(req.body()); diff --git a/test/unit/flux/observer.ts b/test/unit/flux/observer.ts index a769237..bb3801a 100644 --- a/test/unit/flux/observer.ts +++ b/test/unit/flux/observer.ts @@ -33,153 +33,168 @@ suite('Observer', ({ expect, spy, stub }) => { }); }); - describe('resolve()', () => { - it('should not call the observer if no changes', () => { - const observer = spy(); + // describe.skip('resolve()', () => { + // it('should not call the observer if no changes', () => { + // const observer = spy(); + // + // Observer.resolve(undefined, undefined, observer); + // + // expect(observer).to.not.be.called; + // }); + // + // it('should not call the observer if not a function', () => { + // expect(() => Observer.resolve(1, 2, {})).to.not.throw(); + // }); + // + // it('should call the observer with the updated node', () => { + // const observer = spy(); + // + // Observer.resolve(1, 2, (...args) => observer(...args)); + // + // expect(observer).to.be.calledWith(1, 2); + // }); + // + // it('should call resolve() on subtrees', () => { + // const observer1 = spy(); + // const observer2 = spy(); + // const observer3 = spy(); + // const observer4 = spy(); + // const observers = Object.assign((...args) => observer1(...args), { + // a: Object.assign((...args) => observer2(...args), { + // x: (...args) => observer3(...args) + // }), + // b: (...args) => observer4(...args) + // }); + // const oldState = { a: { x: 1 } }; + // const newState = { b: 2 }; + // + // Observer.resolve(oldState, newState, observers); + // + // expect(observer1).to.be.calledWith(oldState, newState); + // expect(observer2).to.be.calledWith({ x: 1 }, undefined); + // expect(observer3).to.be.calledWith(1, undefined); + // expect(observer4).to.be.calledWith(undefined, 2); + // }); + // + // it('should not call resolve() on equal subtrees', () => { + // const observer1 = spy(); + // const observer2 = spy(); + // const observer3 = spy(); + // const observers = Object.assign((...args) => observer1(...args), { + // a: (...args) => observer2(...args), + // b: (...args) => observer3(...args) + // }); + // const oldState = {}; + // const newState = {}; + // + // Observer.resolve(oldState, newState, observers); + // + // expect(observer1).to.be.calledWith(oldState, newState); + // expect(observer2).to.not.be.called; + // expect(observer3).to.not.be.called; + // }); + // }); + + // describe('create()', () => { + // it('should return an observer tree', () => { + // const observers = Observer.create({}); + // + // expect(observers).to.be.an('object'); + // expect(observers.data).to.be.an('object'); + // expect(observers.data.search).to.be.an('object'); + // expect(observers.data.search.request).to.be.a('function'); + // expect(observers.data.search.request.query).to.be.a('function'); + // expect(observers.data.search.request.refinements).to.be.a('function'); + // expect(observers.data.search.response).to.be.a('function'); + // }); + // + // describe('data', () => { + // let emit; + // let observers; + // + // beforeEach(() => { + // emit = spy(); + // observers = Observer.create({ emit }); + // }); + // + // describe('query', () => { + // observers.data.query(undefined, { a: 'b' }); + // + // expect(emit).to.be.calledWith(Events.QUERY_UPDATED, { a: 'b' }); + // }); + // }); + + // describe('search', () => { + // let emit; + // let observers; + // + // beforeEach(() => { + // emit = spy(); + // observers = Observer.create({ emit }); + // }); + // + // describe('request', () => { + // it('should emit search event', () => { + // observers.data.search.request(undefined, { a: 'b' }); + // expect(emit).to.be.calledWith(Events.SEARCH_REQ_UPDATED, { a: 'b' }); + // expect(emit).to.be.calledWith(Events.SEARCH, { a: 'b' }); + // }); + // + // it('should emit PAGE_CHANGED event', () => { + // observers.data.search.request.skip(undefined, 23); + // expect(emit).to.be.calledWith(Events.SEARCH_PAGE_UPDATED, 23); + // expect(emit).to.be.calledWith(Events.PAGE_CHANGED, 23); + // }); + // + // it('should emit COLLECTION_CHANGED event', () => { + // observers.data.search.request.collection(undefined, 'somestring'); + // expect(emit).to.be.calledWith(Events.SEARCH_COLLECTION_UPDATED, 'somestring'); + // expect(emit).to.be.calledWith(Events.COLLECTION_CHANGED, 'somestring'); + // }); + // + // it('should emit QUERY_CHANGED event', () => { + // observers.data.search.request.query(undefined, 'tomatoes'); + // expect(emit).to.be.calledWith(Events.SEARCH_QUERY_UPDATED, 'tomatoes'); + // expect(emit).to.be.calledWith(Events.QUERY_CHANGED, 'tomatoes'); + // expect(emit).to.be.calledWith(Events.REWRITE_QUERY, 'tomatoes'); + // }); + // + // it('should emit REFINEMENTS_CHANGED event', () => { + // observers.data.search.request.refinements(undefined, [{ c: 'd' }]); + // expect(emit).to.be.calledWith(Events.SEARCH_REFINEMENTS_UPDATED, [{ c: 'd' }]); + // expect(emit).to.be.calledWith(Events.REFINEMENTS_CHANGED, [{ c: 'd' }]); + // }); + // + // it('should emit SORT and SORT_CHANGED event', () => { + // observers.data.search.request.sort(undefined, [{ e: 'f' }]); + // expect(emit).to.be.calledWith(Events.SEARCH_SORT_UPDATED, [{ e: 'f' }]); + // expect(emit).to.be.calledWith(Events.SORT, [{ e: 'f' }]); + // }); + // }); + // + // describe('response', () => { + // it('should emit REDIRECT event', () => { + // observers.data.search.response(undefined, { redirect: '/toys.html' }); + // expect(emit).to.be.calledWith(Events.SEARCH_REDIRECT, '/toys.html'); + // expect(emit).to.be.calledWith(Events.REDIRECT, '/toys.html'); + // }); + // + // it('should emit RESULTS event', () => { + // observers.data.search.response(undefined, { g: 'h', originalQuery: {} }); + // expect(emit).to.be.calledWith(Events.SEARCH_RES_UPDATED, { g: 'h', originalQuery: {} }); + // expect(emit).to.be.calledWith(Events.RESULTS, { g: 'h', originalQuery: {} }); + // expect(emit).to.be.calledWith(Events.RESET, { g: 'h', originalQuery: {} }); + // }); + // + // it('should emit DETAILS event', () => { + // observers.data.search.response(undefined, { + // originalQuery: { customUrlParams: [{ key: DETAIL_QUERY_INDICATOR, value: 'yo' }] }, + // records: [{ i: 'j' }] + // }); + // expect(emit).to.be.calledWith(Events.SEARCH_DETAILS, { i: 'j' }); + // expect(emit).to.be.calledWith(Events.DETAILS, { i: 'j' }); + // }); + // }); + // }); - Observer.resolve(undefined, undefined, observer); - - expect(observer).to.not.be.called; - }); - - it('should not call the observer if not a function', () => { - expect(() => Observer.resolve(1, 2, {})).to.not.throw(); - }); - - it('should call the observer with the updated node', () => { - const observer = spy(); - - Observer.resolve(1, 2, (...args) => observer(...args)); - - expect(observer).to.be.calledWith(1, 2); - }); - - it('should call resolve() on subtrees', () => { - const observer1 = spy(); - const observer2 = spy(); - const observer3 = spy(); - const observer4 = spy(); - const observers = Object.assign((...args) => observer1(...args), { - a: Object.assign((...args) => observer2(...args), { - x: (...args) => observer3(...args) - }), - b: (...args) => observer4(...args) - }); - const oldState = { a: { x: 1 } }; - const newState = { b: 2 }; - - Observer.resolve(oldState, newState, observers); - - expect(observer1).to.be.calledWith(oldState, newState); - expect(observer2).to.be.calledWith({ x: 1 }, undefined); - expect(observer3).to.be.calledWith(1, undefined); - expect(observer4).to.be.calledWith(undefined, 2); - }); - - it('should not call resolve() on equal subtrees', () => { - const observer1 = spy(); - const observer2 = spy(); - const observer3 = spy(); - const observers = Object.assign((...args) => observer1(...args), { - a: (...args) => observer2(...args), - b: (...args) => observer3(...args) - }); - const oldState = {}; - const newState = {}; - - Observer.resolve(oldState, newState, observers); - - expect(observer1).to.be.calledWith(oldState, newState); - expect(observer2).to.not.be.called; - expect(observer3).to.not.be.called; - }); - }); - - describe('create()', () => { - it('should return an observer tree', () => { - const observers = Observer.create({}); - - expect(observers).to.be.an('object'); - expect(observers.data).to.be.an('object'); - expect(observers.data.search).to.be.an('object'); - expect(observers.data.search.request).to.be.a('function'); - expect(observers.data.search.request.query).to.be.a('function'); - expect(observers.data.search.request.refinements).to.be.a('function'); - expect(observers.data.search.response).to.be.a('function'); - }); - - describe('search', () => { - let emit; - let observers; - - beforeEach(() => { - emit = spy(); - observers = Observer.create({ emit }); - }); - - describe('request', () => { - it('should emit search event', () => { - observers.data.search.request(undefined, { a: 'b' }); - expect(emit).to.be.calledWith(Events.SEARCH_REQ_UPDATED, { a: 'b' }); - expect(emit).to.be.calledWith(Events.SEARCH, { a: 'b' }); - }); - - it('should emit PAGE_CHANGED event', () => { - observers.data.search.request.skip(undefined, 23); - expect(emit).to.be.calledWith(Events.SEARCH_PAGE_UPDATED, 23); - expect(emit).to.be.calledWith(Events.PAGE_CHANGED, 23); - }); - - it('should emit COLLECTION_CHANGED event', () => { - observers.data.search.request.collection(undefined, 'somestring'); - expect(emit).to.be.calledWith(Events.SEARCH_COLLECTION_UPDATED, 'somestring'); - expect(emit).to.be.calledWith(Events.COLLECTION_CHANGED, 'somestring'); - }); - - it('should emit QUERY_CHANGED event', () => { - observers.data.search.request.query(undefined, 'tomatoes'); - expect(emit).to.be.calledWith(Events.SEARCH_QUERY_UPDATED, 'tomatoes'); - expect(emit).to.be.calledWith(Events.QUERY_CHANGED, 'tomatoes'); - expect(emit).to.be.calledWith(Events.REWRITE_QUERY, 'tomatoes'); - }); - - it('should emit REFINEMENTS_CHANGED event', () => { - observers.data.search.request.refinements(undefined, [{ c: 'd' }]); - expect(emit).to.be.calledWith(Events.SEARCH_REFINEMENTS_UPDATED, [{ c: 'd' }]); - expect(emit).to.be.calledWith(Events.REFINEMENTS_CHANGED, [{ c: 'd' }]); - }); - - it('should emit SORT and SORT_CHANGED event', () => { - observers.data.search.request.sort(undefined, [{ e: 'f' }]); - expect(emit).to.be.calledWith(Events.SEARCH_SORT_UPDATED, [{ e: 'f' }]); - expect(emit).to.be.calledWith(Events.SORT, [{ e: 'f' }]); - }); - }); - - describe('response', () => { - it('should emit REDIRECT event', () => { - observers.data.search.response(undefined, { redirect: '/toys.html' }); - expect(emit).to.be.calledWith(Events.SEARCH_REDIRECT, '/toys.html'); - expect(emit).to.be.calledWith(Events.REDIRECT, '/toys.html'); - }); - - it('should emit RESULTS event', () => { - observers.data.search.response(undefined, { g: 'h', originalQuery: {} }); - expect(emit).to.be.calledWith(Events.SEARCH_RES_UPDATED, { g: 'h', originalQuery: {} }); - expect(emit).to.be.calledWith(Events.RESULTS, { g: 'h', originalQuery: {} }); - expect(emit).to.be.calledWith(Events.RESET, { g: 'h', originalQuery: {} }); - }); - - it('should emit DETAILS event', () => { - observers.data.search.response(undefined, { - originalQuery: { customUrlParams: [{ key: DETAIL_QUERY_INDICATOR, value: 'yo' }] }, - records: [{ i: 'j' }] - }); - expect(emit).to.be.calledWith(Events.SEARCH_DETAILS, { i: 'j' }); - expect(emit).to.be.calledWith(Events.DETAILS, { i: 'j' }); - }); - }); - }); - - }); }); diff --git a/test/unit/flux/pager.ts b/test/unit/flux/pager.ts index 6f397a7..dfe663e 100644 --- a/test/unit/flux/pager.ts +++ b/test/unit/flux/pager.ts @@ -8,12 +8,12 @@ suite('Pager', ({ expect }) => { const totalRecordCount = typeof opts === 'object' && Number(opts.total) >= 0 ? opts.total : 30; const pageSize = typeof opts === 'object' && Number(opts.pageSize) >= 0 ? opts.pageSize : 10; return { + emit: (event: string) => null, query: new Query() .skip(recordStart) .withPageSize(pageSize), results: { totalRecordCount }, - emit: (event: string) => null, - search + search, }; } diff --git a/tslint.json b/tslint.json index f924893..a245a1a 100644 --- a/tslint.json +++ b/tslint.json @@ -1,77 +1,13 @@ { + "extends": "tslint:latest", "rulesDirectory": "node_modules/tslint-eslint-rules/dist/rules", "rules": { - "typedef": [true, "parameter", "property-declaration", "member-variable-declaration"], - "member-ordering": [true, { - "order": "instance-sandwich" - }], - "no-internal-module": true, - "typedef-whitespace": [true, { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - }, { - "call-signature": "space", - "index-signature": "space", - "parameter": "space", - "property-declaration": "space", - "variable-declaration": "space" - }], - "forin": true, - "label-position": true, - "no-arg": true, - "no-console": [true, "log"], - "no-construct": true, - "no-shadowed-variable": true, - "no-switch-case-fall-through": true, - "no-unsafe-finally": true, - "no-unused-expression": true, - "no-unused-variable": [true], - "no-use-before-declare": true, - "no-var-keyword": true, - "triple-equals": [true, "allow-null-check"], - "use-isnan": true, - "indent": [true, "spaces"], - "linebreak-style": [true, "LF"], - "max-line-length": [true, 120], - "no-trailing-whitespace": true, - "arrow-parens": true, - "class-name": true, - "comment-format": [true, "check-space", "check-lowercase"], "interface-name": [true, "never-prefix"], - "jsdoc-format": true, - "new-parens": true, - "no-consecutive-blank-lines": true, - "object-literal-key-quotes": [true, "as-needed"], - "object-literal-shorthand": true, - "one-line": [true, "check-catch", "check-finally", "check-else", "check-open-brace", "check-whitespace"], - "one-variable-per-declaration": [true, "ignore-for-loop"], - "ordered-imports": [true, { - "import-sources-order": "lowercase-last", - "named-imports-order": "lowercase-first" - }], "quotemark": [true, "single", "avoid-escape"], "semicolon": [true, "always"], - "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], - "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-moduler", "check-type"], - "no-constant-condition": true, - "no-duplicate-case": true, - "no-empty": true, - "no-empty-character-class": true, - "no-extra-boolean-cast": true, - "no-extra-semi": true, - "no-inner-declarations": [true, "both"], - "no-invalid-regexp": true, - "no-irregular-whitespace": true, - "no-regex-spaces": true, - "no-sparse-arrays": true, - "no-unexpected-multiline": true, - "valid-typeof": true, - "block-spacing": [true, "always"], - "brace-style": [true, "1tbs", { - "allowSingleLine": true - }] + "no-angle-bracket-type-assertion": [false, "always"], + "whitespace": [false, "typecast"], + "no-namespace": false, + "member-access": [true, "no-public"] } } From 130e96fdca976e5e16bf54d5cdb7d01e03e17f1d Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Mon, 24 Apr 2017 09:35:38 -0400 Subject: [PATCH 24/56] all actions tests --- src/flux/actions.ts | 6 +++--- src/flux/utils.ts | 2 ++ test/unit/flux/actions.ts | 20 ++++++++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 9753dc1..e358f9a 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -1,7 +1,7 @@ import { Dispatch } from 'redux'; import { Request } from '../models/request'; import Store from './store'; -import { thunk } from './utils'; +import { rayify, thunk } from './utils'; namespace Actions { export const UPDATE_SEARCH = 'UPDATE_SEARCH'; @@ -26,8 +26,8 @@ namespace Actions { export const selectCollection = (id: string) => thunk(SELECT_COLLECTION, { id }); - export const updateSorts = (sorts: Store.Sort[]) => - thunk(UPDATE_SORTS, { sorts }); + export const updateSorts = (sorts: Store.Sort | Store.Sort[]) => + thunk(UPDATE_SORTS, { sorts: rayify(sorts) }); export const updateAutocompleteQuery = (query: string) => thunk(UPDATE_AUTOCOMPLETE_QUERY, { query }); diff --git a/src/flux/utils.ts b/src/flux/utils.ts index 050c74e..8e80644 100644 --- a/src/flux/utils.ts +++ b/src/flux/utils.ts @@ -1 +1,3 @@ export const thunk = (type: string, data: any) => (dispatch) => dispatch({ type, ...data }); + +export const rayify = (arr: T | T[]): T[] => Array.isArray(arr) ? arr : [arr]; diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts index 92e0ebd..7363046 100644 --- a/test/unit/flux/actions.ts +++ b/test/unit/flux/actions.ts @@ -14,6 +14,18 @@ suite('actions', ({ expect, spy, stub }) => { }); }); + describe('selectRefinement()', () => { + it('should create a SELECT_REFINEMENT action', () => { + const navigationId = 'brand'; + const index = 3; + const thunk = stub(utils, 'thunk'); + + Actions.selectRefinement(navigationId, index); + + expect(thunk).to.be.calledWith(Actions.SELECT_REFINEMENT, { navigationId, index }); + }); + }); + describe('deselectRefinement()', () => { it('should create a DESELECT_REFINEMENT action', () => { const navigationId = 'brand'; @@ -49,13 +61,13 @@ suite('actions', ({ expect, spy, stub }) => { }); describe('updateSorts()', () => { - it('should create an UPDATE_SORTS action', () => { - const sorts: any[] = [{ a: 'b' }]; + it('should create a UPDATE_SORTS action', () => { + const sort = { field: 'price' }; const thunk = stub(utils, 'thunk'); - Actions.updateSorts(sorts); + Actions.updateSorts(sort); - expect(thunk).to.be.calledWith(Actions.UPDATE_SORTS, { sorts }); + expect(thunk).to.be.calledWith(Actions.UPDATE_SORTS, { sorts: [sort] }); }); }); From d86e7e1fe67fa2dc390e677ff30316cdd3459aa5 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Mon, 24 Apr 2017 10:15:56 -0400 Subject: [PATCH 25/56] more observer tests --- src/flux/observer.ts | 58 +++++------ test/unit/flux/observer.ts | 200 ++++++++++++++++++++----------------- 2 files changed, 137 insertions(+), 121 deletions(-) diff --git a/src/flux/observer.ts b/src/flux/observer.ts index 2a00c86..6a2492e 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -7,7 +7,7 @@ export const INDEXED = Symbol(); type Observer = (oldState: any, newState: any) => void; namespace Observer { - export type Map = { [key: string]: Observer | Map }; + export interface Map { [key: string]: Observer | Map; } export type Node = Map | Observer | (Observer & Map); export function listen(flux: FluxCapacitor) { @@ -52,57 +52,53 @@ namespace Observer { export function create(flux: FluxCapacitor) { const emit = (event: string) => (_, newValue) => flux.emit(event, newValue); - const indexed = (event: string, prefix: string, field: string) => - Object.assign(emit(event), { - INDEXED: (_, newIndexed) => flux.emit(`${prefix}:${newIndexed[field]}`, newIndexed) - }); return { data: { - query: Object.assign(emit(Events.QUERY_UPDATED), { - original: emit(Events.ORIGINAL_QUERY_UPDATED), - corrected: emit(Events.CORRECTED_QUERY_UPDATED), - related: emit(Events.RELATED_QUERIES_UPDATED), - didYouMeans: emit(Events.DID_YOU_MEANS_UPDATED), - rewrites: emit(Events.QUERY_REWRITES_UPDATED), + autocomplete: Object.assign(emit(Events.AUTOCOMPLETE_UPDATED), { + products: emit(Events.AUTOCOMPLETE_PRODUCTS_UPDATED), + query: emit(Events.AUTOCOMPLETE_QUERY_UPDATED), }), - sorts: emit(Events.SORTS_UPDATED), - - products: emit(Events.PRODUCTS_UPDATED), - collections: { - INDEXED: (_, newIndexed) => flux.emit(`${Events.COLLECTION_UPDATED}:${newIndexed.name}`, newIndexed), - selected: emit(Events.SELECTED_COLLECTION_UPDATED) + [INDEXED]: (_, newIndexed) => flux.emit(`${Events.COLLECTION_UPDATED}:${newIndexed.name}`, newIndexed), + selected: emit(Events.SELECTED_COLLECTION_UPDATED), + }, + + details: { + id: emit(Events.DETAILS_ID_UPDATED), + product: emit(Events.DETAILS_PRODUCT_UPDATED), }, navigations: Object.assign(emit(Events.NAVIGATIONS_UPDATED), { - INDEXED: (oldNavigation, newNavigation) => { + [INDEXED]: (oldNavigation, newNavigation) => { if (oldNavigation.selected !== newNavigation.selected) { flux.emit(`${Events.SELECTED_REFINEMENTS_UPDATED}:${newNavigation.field}`, newNavigation.selected); } - } - }), - - autocomplete: Object.assign(emit(Events.AUTOCOMPLETE_UPDATED), { - query: emit(Events.AUTOCOMPLETE_QUERY_UPDATED), - products: emit(Events.AUTOCOMPLETE_PRODUCTS_UPDATED) + }, }), page: Object.assign(emit(Events.PAGE_UPDATED), { - size: emit(Events.PAGE_SIZE_UPDATED), current: emit(Events.CURRENT_PAGE_UPDATED), + size: emit(Events.PAGE_SIZE_UPDATED), }), - template: emit(Events.TEMPLATE_UPDATED), + products: emit(Events.PRODUCTS_UPDATED), - details: { - id: emit(Events.DETAILS_ID_UPDATED), - product: emit(Events.DETAILS_PRODUCT_UPDATED) - }, + query: Object.assign(emit(Events.QUERY_UPDATED), { + corrected: emit(Events.CORRECTED_QUERY_UPDATED), + didYouMeans: emit(Events.DID_YOU_MEANS_UPDATED), + original: emit(Events.ORIGINAL_QUERY_UPDATED), + related: emit(Events.RELATED_QUERIES_UPDATED), + rewrites: emit(Events.QUERY_REWRITES_UPDATED), + }), reditect: emit(Events.REDIRECT), - } + + sorts: emit(Events.SORTS_UPDATED), + + template: emit(Events.TEMPLATE_UPDATED), + }, }; } } diff --git a/test/unit/flux/observer.ts b/test/unit/flux/observer.ts index bb3801a..4749689 100644 --- a/test/unit/flux/observer.ts +++ b/test/unit/flux/observer.ts @@ -1,5 +1,5 @@ import { Events } from '../../../src/flux/capacitor'; -import Observer, { DETAIL_QUERY_INDICATOR } from '../../../src/flux/observer'; +import Observer, { DETAIL_QUERY_INDICATOR, INDEXED } from '../../../src/flux/observer'; import suite from '../_suite'; suite('Observer', ({ expect, spy, stub }) => { @@ -33,96 +33,116 @@ suite('Observer', ({ expect, spy, stub }) => { }); }); - // describe.skip('resolve()', () => { - // it('should not call the observer if no changes', () => { - // const observer = spy(); - // - // Observer.resolve(undefined, undefined, observer); - // - // expect(observer).to.not.be.called; - // }); - // - // it('should not call the observer if not a function', () => { - // expect(() => Observer.resolve(1, 2, {})).to.not.throw(); - // }); - // - // it('should call the observer with the updated node', () => { - // const observer = spy(); - // - // Observer.resolve(1, 2, (...args) => observer(...args)); - // - // expect(observer).to.be.calledWith(1, 2); - // }); - // - // it('should call resolve() on subtrees', () => { - // const observer1 = spy(); - // const observer2 = spy(); - // const observer3 = spy(); - // const observer4 = spy(); - // const observers = Object.assign((...args) => observer1(...args), { - // a: Object.assign((...args) => observer2(...args), { - // x: (...args) => observer3(...args) - // }), - // b: (...args) => observer4(...args) - // }); - // const oldState = { a: { x: 1 } }; - // const newState = { b: 2 }; - // - // Observer.resolve(oldState, newState, observers); - // - // expect(observer1).to.be.calledWith(oldState, newState); - // expect(observer2).to.be.calledWith({ x: 1 }, undefined); - // expect(observer3).to.be.calledWith(1, undefined); - // expect(observer4).to.be.calledWith(undefined, 2); - // }); - // - // it('should not call resolve() on equal subtrees', () => { - // const observer1 = spy(); - // const observer2 = spy(); - // const observer3 = spy(); - // const observers = Object.assign((...args) => observer1(...args), { - // a: (...args) => observer2(...args), - // b: (...args) => observer3(...args) - // }); - // const oldState = {}; - // const newState = {}; - // - // Observer.resolve(oldState, newState, observers); - // - // expect(observer1).to.be.calledWith(oldState, newState); - // expect(observer2).to.not.be.called; - // expect(observer3).to.not.be.called; - // }); - // }); + describe.skip('resolve()', () => { + it('should not call the observer if no changes', () => { + const observer = spy(); - // describe('create()', () => { - // it('should return an observer tree', () => { - // const observers = Observer.create({}); - // - // expect(observers).to.be.an('object'); - // expect(observers.data).to.be.an('object'); - // expect(observers.data.search).to.be.an('object'); - // expect(observers.data.search.request).to.be.a('function'); - // expect(observers.data.search.request.query).to.be.a('function'); - // expect(observers.data.search.request.refinements).to.be.a('function'); - // expect(observers.data.search.response).to.be.a('function'); - // }); - // - // describe('data', () => { - // let emit; - // let observers; - // - // beforeEach(() => { - // emit = spy(); - // observers = Observer.create({ emit }); - // }); - // - // describe('query', () => { - // observers.data.query(undefined, { a: 'b' }); - // - // expect(emit).to.be.calledWith(Events.QUERY_UPDATED, { a: 'b' }); - // }); - // }); + Observer.resolve(undefined, undefined, observer); + + expect(observer).to.not.be.called; + }); + + it('should not call the observer if not a function', () => { + expect(() => Observer.resolve(1, 2, {})).to.not.throw(); + }); + + it('should call the observer with the updated node', () => { + const observer = spy(); + + Observer.resolve(1, 2, (...args) => observer(...args)); + + expect(observer).to.be.calledWith(1, 2); + }); + + it('should call resolve() on subtrees', () => { + const observer1 = spy(); + const observer2 = spy(); + const observer3 = spy(); + const observer4 = spy(); + const observers = Object.assign((...args) => observer1(...args), { + a: Object.assign((...args) => observer2(...args), { + x: (...args) => observer3(...args), + }), + b: (...args) => observer4(...args), + }); + const oldState = { a: { x: 1 } }; + const newState = { b: 2 }; + + Observer.resolve(oldState, newState, observers); + + expect(observer1).to.be.calledWith(oldState, newState); + expect(observer2).to.be.calledWith({ x: 1 }, undefined); + expect(observer3).to.be.calledWith(1, undefined); + expect(observer4).to.be.calledWith(undefined, 2); + }); + + it('should not call resolve() on equal subtrees', () => { + const observer1 = spy(); + const observer2 = spy(); + const observer3 = spy(); + const observers = Object.assign((...args) => observer1(...args), { + a: (...args) => observer2(...args), + b: (...args) => observer3(...args), + }); + const oldState = {}; + const newState = {}; + + Observer.resolve(oldState, newState, observers); + + expect(observer1).to.be.calledWith(oldState, newState); + expect(observer2).to.not.be.called; + expect(observer3).to.not.be.called; + }); + }); + + describe('create()', () => { + it('should return an observer tree', () => { + const observers = Observer.create({}); + + expect(observers).to.be.an('object'); + expect(observers.data).to.be.an('object'); + expect(observers.data.autocomplete).to.be.a('function'); + expect(observers.data.autocomplete.products).to.be.a('function'); + expect(observers.data.autocomplete.query).to.be.a('function'); + expect(observers.data.collections).to.be.an('object'); + expect(observers.data.collections[INDEXED]).to.be.a('function'); + expect(observers.data.collections.selected).to.be.a('function'); + expect(observers.data.details).to.be.an('object'); + expect(observers.data.details.id).to.be.a('function'); + expect(observers.data.details.product).to.be.a('function'); + expect(observers.data.navigations).to.be.a('function'); + // expect(observers.data.navigations[INDEXED]).to.be.a('function'); + expect(observers.data.page).to.be.a('function'); + expect(observers.data.page.current).to.be.a('function'); + expect(observers.data.page.size).to.be.a('function'); + expect(observers.data.products).to.be.a('function'); + expect(observers.data.query).to.be.a('function'); + expect(observers.data.query.corrected).to.be.a('function'); + expect(observers.data.query.didYouMeans).to.be.a('function'); + expect(observers.data.query.original).to.be.a('function'); + expect(observers.data.query.related).to.be.a('function'); + expect(observers.data.query.rewrites).to.be.a('function'); + expect(observers.data.reditect).to.be.a('function'); + expect(observers.data.sorts).to.be.a('function'); + expect(observers.data.template).to.be.a('function'); + }); + + // describe('data', () => { + // let emit; + // let observers; + // + // beforeEach(() => { + // emit = spy(); + // observers = Observer.create({ emit }); + // }); + // + // describe('query', () => { + // observers.data.query(undefined, { a: 'b' }); + // + // expect(emit).to.be.calledWith(Events.QUERY_UPDATED, { a: 'b' }); + // }); + // }); + }); // describe('search', () => { // let emit; From 0b76c099ea2f1fad79cf2621bd672eeb8d066fe6 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Mon, 24 Apr 2017 10:39:10 -0400 Subject: [PATCH 26/56] more observer tests --- test/unit/flux/observer.ts | 73 +++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/test/unit/flux/observer.ts b/test/unit/flux/observer.ts index 4749689..328ac5a 100644 --- a/test/unit/flux/observer.ts +++ b/test/unit/flux/observer.ts @@ -33,7 +33,7 @@ suite('Observer', ({ expect, spy, stub }) => { }); }); - describe.skip('resolve()', () => { + describe('resolve()', () => { it('should not call the observer if no changes', () => { const observer = spy(); @@ -42,11 +42,11 @@ suite('Observer', ({ expect, spy, stub }) => { expect(observer).to.not.be.called; }); - it('should not call the observer if not a function', () => { + it.skip('should not call the observer if not a function', () => { expect(() => Observer.resolve(1, 2, {})).to.not.throw(); }); - it('should call the observer with the updated node', () => { + it.skip('should call the observer with the updated node', () => { const observer = spy(); Observer.resolve(1, 2, (...args) => observer(...args)); @@ -54,7 +54,7 @@ suite('Observer', ({ expect, spy, stub }) => { expect(observer).to.be.calledWith(1, 2); }); - it('should call resolve() on subtrees', () => { + it.skip('should call resolve() on subtrees', () => { const observer1 = spy(); const observer2 = spy(); const observer3 = spy(); @@ -76,7 +76,7 @@ suite('Observer', ({ expect, spy, stub }) => { expect(observer4).to.be.calledWith(undefined, 2); }); - it('should not call resolve() on equal subtrees', () => { + it.skip('should not call resolve() on equal subtrees', () => { const observer1 = spy(); const observer2 = spy(); const observer3 = spy(); @@ -127,21 +127,53 @@ suite('Observer', ({ expect, spy, stub }) => { expect(observers.data.template).to.be.a('function'); }); - // describe('data', () => { - // let emit; - // let observers; - // - // beforeEach(() => { - // emit = spy(); - // observers = Observer.create({ emit }); - // }); - // - // describe('query', () => { - // observers.data.query(undefined, { a: 'b' }); - // - // expect(emit).to.be.calledWith(Events.QUERY_UPDATED, { a: 'b' }); - // }); - // }); + describe('data', () => { + let emit; + let observers; + + beforeEach(() => { + emit = spy(); + observers = Observer.create({ emit }); + }); + + describe('query', () => { + it('should observe emit QUERY_UPDATED event', () => { + observers.data.query(undefined, { a: 'b' }); + + expect(emit).to.be.calledWith(Events.QUERY_UPDATED, { a: 'b' }); + }); + + it('should observe emit CORRECTED_QUERY_UPDATED event', () => { + observers.data.query.corrected(undefined, { a: 'b' }); + + expect(emit).to.be.calledWith(Events.CORRECTED_QUERY_UPDATED, { a: 'b' }); + }); + + it('should observe emit DID_YOU_MEANS_UPDATED event', () => { + observers.data.query.didYouMeans(undefined, { a: 'b' }); + + expect(emit).to.be.calledWith(Events.DID_YOU_MEANS_UPDATED, { a: 'b' }); + }); + + it('should observe emit ORIGINAL_QUERY_UPDATED event', () => { + observers.data.query.original(undefined, { a: 'b' }); + + expect(emit).to.be.calledWith(Events.ORIGINAL_QUERY_UPDATED, { a: 'b' }); + }); + + it('should observe emit RELATED_QUERIES_UPDATED event', () => { + observers.data.query.related(undefined, { a: 'b' }); + + expect(emit).to.be.calledWith(Events.RELATED_QUERIES_UPDATED, { a: 'b' }); + }); + + it('should observe emit QUERY_REWRITES_UPDATED event', () => { + observers.data.query.rewrites(undefined, { a: 'b' }); + + expect(emit).to.be.calledWith(Events.QUERY_REWRITES_UPDATED, { a: 'b' }); + }); + }); + }); }); // describe('search', () => { @@ -216,5 +248,4 @@ suite('Observer', ({ expect, spy, stub }) => { // }); // }); // }); - }); From 5ce978143ac006ed68137f3caa90206055bb08c0 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Mon, 24 Apr 2017 10:53:55 -0400 Subject: [PATCH 27/56] observer tests --- test/unit/flux/observer.ts | 218 +++++++++++++++++++++---------------- 1 file changed, 127 insertions(+), 91 deletions(-) diff --git a/test/unit/flux/observer.ts b/test/unit/flux/observer.ts index 328ac5a..1c88655 100644 --- a/test/unit/flux/observer.ts +++ b/test/unit/flux/observer.ts @@ -128,6 +128,7 @@ suite('Observer', ({ expect, spy, stub }) => { }); describe('data', () => { + const OBJ = { a: 'b' }; let emit; let observers; @@ -136,116 +137,151 @@ suite('Observer', ({ expect, spy, stub }) => { observers = Observer.create({ emit }); }); + describe('autocomplete', () => { + it('should emit AUTOCOMPLETE_UPDATED event', () => { + observers.data.autocomplete(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.AUTOCOMPLETE_UPDATED, OBJ); + }); + + it('should emit AUTOCOMPLETE_PRODUCTS_UPDATED event', () => { + observers.data.autocomplete.products(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.AUTOCOMPLETE_PRODUCTS_UPDATED, OBJ); + }); + + it('should emit AUTOCOMPLETE_QUERY_UPDATED event', () => { + observers.data.autocomplete.query(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.AUTOCOMPLETE_QUERY_UPDATED, OBJ); + }); + }); + + describe('collections', () => { + it.skip('should emit COLLECTION_UPDATED event', () => { + observers.data.collections.bydId.brand(undefined, OBJ); + + expect(emit).to.be.calledWith(`${Events.COLLECTION_UPDATED}:brand`, OBJ); + }); + + it('should emit SELECTED_COLLECTION_UPDATED event', () => { + observers.data.collections.selected(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.SELECTED_COLLECTION_UPDATED, OBJ); + }); + }); + + describe('details', () => { + it('should emit DETAILS_ID_UPDATED event', () => { + observers.data.details.id(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.DETAILS_ID_UPDATED, OBJ); + }); + + it('should emit DETAILS_PRODUCT_UPDATED event', () => { + observers.data.details.product(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.DETAILS_PRODUCT_UPDATED, OBJ); + }); + }); + + describe('navigations', () => { + it.skip('should emit SELECTED_REFINEMENTS_UPDATED event', () => { + observers.data.navigations.bydId.brand.selected(undefined, OBJ); + + expect(emit).to.be.calledWith(`${Events.SELECTED_REFINEMENTS_UPDATED}:brand`, OBJ); + }); + }); + + describe('page', () => { + it('should emit PAGE_UPDATED event', () => { + observers.data.page(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.PAGE_UPDATED, OBJ); + }); + + it('should emit CURRENT_PAGE_UPDATED event', () => { + observers.data.page.current(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.CURRENT_PAGE_UPDATED, OBJ); + }); + + it('should emit PAGE_SIZE_UPDATED event', () => { + observers.data.page.size(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.PAGE_SIZE_UPDATED, OBJ); + }); + }); + + describe('products', () => { + it('should emit PRODUCTS_UPDATED event', () => { + observers.data.products(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.PRODUCTS_UPDATED, OBJ); + }); + }); + describe('query', () => { - it('should observe emit QUERY_UPDATED event', () => { - observers.data.query(undefined, { a: 'b' }); + it('should emit QUERY_UPDATED event', () => { + observers.data.query(undefined, OBJ); - expect(emit).to.be.calledWith(Events.QUERY_UPDATED, { a: 'b' }); + expect(emit).to.be.calledWith(Events.QUERY_UPDATED, OBJ); }); - it('should observe emit CORRECTED_QUERY_UPDATED event', () => { - observers.data.query.corrected(undefined, { a: 'b' }); + it('should emit CORRECTED_QUERY_UPDATED event', () => { + observers.data.query.corrected(undefined, OBJ); - expect(emit).to.be.calledWith(Events.CORRECTED_QUERY_UPDATED, { a: 'b' }); + expect(emit).to.be.calledWith(Events.CORRECTED_QUERY_UPDATED, OBJ); }); - it('should observe emit DID_YOU_MEANS_UPDATED event', () => { - observers.data.query.didYouMeans(undefined, { a: 'b' }); + it('should emit DID_YOU_MEANS_UPDATED event', () => { + observers.data.query.didYouMeans(undefined, OBJ); - expect(emit).to.be.calledWith(Events.DID_YOU_MEANS_UPDATED, { a: 'b' }); + expect(emit).to.be.calledWith(Events.DID_YOU_MEANS_UPDATED, OBJ); }); - it('should observe emit ORIGINAL_QUERY_UPDATED event', () => { - observers.data.query.original(undefined, { a: 'b' }); + it('should emit ORIGINAL_QUERY_UPDATED event', () => { + observers.data.query.original(undefined, OBJ); - expect(emit).to.be.calledWith(Events.ORIGINAL_QUERY_UPDATED, { a: 'b' }); + expect(emit).to.be.calledWith(Events.ORIGINAL_QUERY_UPDATED, OBJ); }); - it('should observe emit RELATED_QUERIES_UPDATED event', () => { - observers.data.query.related(undefined, { a: 'b' }); + it('should emit RELATED_QUERIES_UPDATED event', () => { + observers.data.query.related(undefined, OBJ); - expect(emit).to.be.calledWith(Events.RELATED_QUERIES_UPDATED, { a: 'b' }); + expect(emit).to.be.calledWith(Events.RELATED_QUERIES_UPDATED, OBJ); }); - it('should observe emit QUERY_REWRITES_UPDATED event', () => { - observers.data.query.rewrites(undefined, { a: 'b' }); + it('should emit QUERY_REWRITES_UPDATED event', () => { + observers.data.query.rewrites(undefined, OBJ); - expect(emit).to.be.calledWith(Events.QUERY_REWRITES_UPDATED, { a: 'b' }); + expect(emit).to.be.calledWith(Events.QUERY_REWRITES_UPDATED, OBJ); + }); + }); + + describe('reditect', () => { + it('should emit REDIRECT event', () => { + observers.data.reditect(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.REDIRECT, OBJ); + }); + }); + + describe('sorts', () => { + it('should emit SORTS_UPDATED event', () => { + observers.data.sorts(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.SORTS_UPDATED, OBJ); + }); + }); + + describe('template', () => { + it('should emit TEMPLATE_UPDATED event', () => { + observers.data.template(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.TEMPLATE_UPDATED, OBJ); }); }); }); }); - - // describe('search', () => { - // let emit; - // let observers; - // - // beforeEach(() => { - // emit = spy(); - // observers = Observer.create({ emit }); - // }); - // - // describe('request', () => { - // it('should emit search event', () => { - // observers.data.search.request(undefined, { a: 'b' }); - // expect(emit).to.be.calledWith(Events.SEARCH_REQ_UPDATED, { a: 'b' }); - // expect(emit).to.be.calledWith(Events.SEARCH, { a: 'b' }); - // }); - // - // it('should emit PAGE_CHANGED event', () => { - // observers.data.search.request.skip(undefined, 23); - // expect(emit).to.be.calledWith(Events.SEARCH_PAGE_UPDATED, 23); - // expect(emit).to.be.calledWith(Events.PAGE_CHANGED, 23); - // }); - // - // it('should emit COLLECTION_CHANGED event', () => { - // observers.data.search.request.collection(undefined, 'somestring'); - // expect(emit).to.be.calledWith(Events.SEARCH_COLLECTION_UPDATED, 'somestring'); - // expect(emit).to.be.calledWith(Events.COLLECTION_CHANGED, 'somestring'); - // }); - // - // it('should emit QUERY_CHANGED event', () => { - // observers.data.search.request.query(undefined, 'tomatoes'); - // expect(emit).to.be.calledWith(Events.SEARCH_QUERY_UPDATED, 'tomatoes'); - // expect(emit).to.be.calledWith(Events.QUERY_CHANGED, 'tomatoes'); - // expect(emit).to.be.calledWith(Events.REWRITE_QUERY, 'tomatoes'); - // }); - // - // it('should emit REFINEMENTS_CHANGED event', () => { - // observers.data.search.request.refinements(undefined, [{ c: 'd' }]); - // expect(emit).to.be.calledWith(Events.SEARCH_REFINEMENTS_UPDATED, [{ c: 'd' }]); - // expect(emit).to.be.calledWith(Events.REFINEMENTS_CHANGED, [{ c: 'd' }]); - // }); - // - // it('should emit SORT and SORT_CHANGED event', () => { - // observers.data.search.request.sort(undefined, [{ e: 'f' }]); - // expect(emit).to.be.calledWith(Events.SEARCH_SORT_UPDATED, [{ e: 'f' }]); - // expect(emit).to.be.calledWith(Events.SORT, [{ e: 'f' }]); - // }); - // }); - // - // describe('response', () => { - // it('should emit REDIRECT event', () => { - // observers.data.search.response(undefined, { redirect: '/toys.html' }); - // expect(emit).to.be.calledWith(Events.SEARCH_REDIRECT, '/toys.html'); - // expect(emit).to.be.calledWith(Events.REDIRECT, '/toys.html'); - // }); - // - // it('should emit RESULTS event', () => { - // observers.data.search.response(undefined, { g: 'h', originalQuery: {} }); - // expect(emit).to.be.calledWith(Events.SEARCH_RES_UPDATED, { g: 'h', originalQuery: {} }); - // expect(emit).to.be.calledWith(Events.RESULTS, { g: 'h', originalQuery: {} }); - // expect(emit).to.be.calledWith(Events.RESET, { g: 'h', originalQuery: {} }); - // }); - // - // it('should emit DETAILS event', () => { - // observers.data.search.response(undefined, { - // originalQuery: { customUrlParams: [{ key: DETAIL_QUERY_INDICATOR, value: 'yo' }] }, - // records: [{ i: 'j' }] - // }); - // expect(emit).to.be.calledWith(Events.SEARCH_DETAILS, { i: 'j' }); - // expect(emit).to.be.calledWith(Events.DETAILS, { i: 'j' }); - // }); - // }); - // }); }); From 6165fc63e6dba182b1618680aaf72dd15887a8a6 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Mon, 24 Apr 2017 11:35:23 -0400 Subject: [PATCH 28/56] capacitor v2 --- src/flux/capacitor.ts | 210 +++--- src/flux/store.ts | 2 +- test/unit/flux/capacitor.ts | 1242 ++++++++++++++++++----------------- 3 files changed, 737 insertions(+), 717 deletions(-) diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index eeee690..a17b598 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -5,7 +5,9 @@ import { BrowserBridge } from '../core/bridge'; import { Query, QueryConfiguration } from '../core/query'; import { SelectedRangeRefinement, SelectedValueRefinement, Sort } from '../models/request'; import { Navigation, RefinementResults, Results } from '../models/response'; +import Actions from './actions'; import { Pager } from './pager'; +import Store from './store'; export namespace Events { // query events @@ -67,7 +69,7 @@ export interface FluxBridgeConfig { export class FluxCapacitor extends EventEmitter { - store: redux.Store; + store: redux.Store = Store.create(); query: Query; bridge: BrowserBridge; @@ -94,22 +96,26 @@ export class FluxCapacitor extends EventEmitter { this.page = new Pager(this); } - search(originalQuery: string = this.originalQuery): Promise { - this.query.withQuery(originalQuery); - this.emit(Events.SEARCH, this.query.raw); - return this.bridge.search(this.query) - .then((results) => { - const oldQuery = this.originalQuery; - Object.assign(this, { results, originalQuery }); - - if (results.redirect) { - this.emit(Events.REDIRECT, results.redirect); - } - this.emit(Events.RESULTS, results); - this.emitQueryChanged(oldQuery, originalQuery); - - return results; - }); + // search(originalQuery: string = this.originalQuery): Promise { + // this.query.withQuery(originalQuery); + // this.emit(Events.SEARCH, this.query.raw); + // return this.bridge.search(this.query) + // .then((results) => { + // const oldQuery = this.originalQuery; + // Object.assign(this, { results, originalQuery }); + // + // if (results.redirect) { + // this.emit(Events.REDIRECT, results.redirect); + // } + // this.emit(Events.RESULTS, results); + // this.emitQueryChanged(oldQuery, originalQuery); + // + // return results; + // }); + // } + + searchV2(query: string = this.originalQuery) { + this.store.dispatch(Actions.updateSearch({ query })); } refinements(navigationName: string): Promise { @@ -120,64 +126,72 @@ export class FluxCapacitor extends EventEmitter { }); } - rewrite(query: string, config: RewriteConfig = {}): Promise { - let search: Promise; - if (config.skipSearch) { - this.emitQueryChanged(this.originalQuery, query); - search = Promise.resolve(this.query.withQuery(this.originalQuery = query)); - } else { - search = this.search(query); - } - return search.then(() => this.emit(Events.REWRITE_QUERY, query)) - .then(() => query); - } - resetRecall() { this.query = new Query().withConfiguration(this.filteredRequest); } - reset(query: string = this.originalQuery): Promise { - this.resetRecall(); - this.emit(Events.PAGE_CHANGED, { pageNumber: 1 }); - return this.search(query) - .then((res) => this.emit(Events.RESET, res)) - .then(() => query); + // reset(query: string = this.originalQuery): Promise { + // this.resetRecall(); + // this.emit(Events.PAGE_CHANGED, { pageNumber: 1 }); + // return this.search(query) + // .then((res) => this.emit(Events.RESET, res)) + // .then(() => query); + // } + + reset(query: string = null) { + this.store.dispatch(Actions.updateSearch({ query, refinements: [], clear: true })); } - resize(pageSize: number, resetOffset?: boolean): Promise { - this.query.withPageSize(pageSize); - if (resetOffset) { - return this.page.switchPage(1); - } else { - const total = this.page.restrictTotalRecords(this.page.fromResult, pageSize); - const page = this.page.getPage(total); - return this.page.switchPage(page); - } + // resize(pageSize: number, resetOffset?: boolean): Promise { + // this.query.withPageSize(pageSize); + // if (resetOffset) { + // return this.page.switchPage(1); + // } else { + // const total = this.page.restrictTotalRecords(this.page.fromResult, pageSize); + // const page = this.page.getPage(total); + // return this.page.switchPage(page); + // } + // } + + resize(pageSize: number) { + this.store.dispatch(Actions.updatePageSize(pageSize)); } - sort(sort: Sort, clearSorts: Sort[] = [sort]): Promise { - this.query.withoutSorts(...clearSorts).withSorts(sort); - return this.page.reset() - .then((res) => { - this.emit(Events.SORT, this.query.raw.sort); - return res; - }); + // sort(sort: Sort, clearSorts: Sort[] = [sort]): Promise { + // this.query.withoutSorts(...clearSorts).withSorts(sort); + // return this.page.reset() + // .then((res) => { + // this.emit(Events.SORT, this.query.raw.sort); + // return res; + // }); + // } + + sort(sort: Sort | Sort[]) { + this.store.dispatch(Actions.updateSorts(sort)); } - refine(refinement: FluxRefinement, config: RefinementConfig = { reset: true }): Promise { - this.query.withSelectedRefinements(refinement); - if (config.skipSearch) { - return Promise.resolve(this.navigationInfo); - } - return this.doRefinement(config); + // refine(refinement: FluxRefinement, config: RefinementConfig = { reset: true }): Promise { + // this.query.withSelectedRefinements(refinement); + // if (config.skipSearch) { + // return Promise.resolve(this.navigationInfo); + // } + // return this.doRefinement(config); + // } + + refine(navigationName: string, index: number) { + this.store.dispatch(Actions.selectRefinement(navigationName, index)); } - unrefine(refinement: FluxRefinement, config: RefinementConfig = { reset: true }): Promise { - this.query.withoutSelectedRefinements(refinement); - if (config.skipSearch) { - return Promise.resolve(this.navigationInfo); - } - return this.doRefinement(config); + // unrefine(refinement: FluxRefinement, config: RefinementConfig = { reset: true }): Promise { + // this.query.withoutSelectedRefinements(refinement); + // if (config.skipSearch) { + // return Promise.resolve(this.navigationInfo); + // } + // return this.doRefinement(config); + // } + + unrefine(navigationName: string, index: number) { + this.store.dispatch(Actions.deselectRefinement(navigationName, index)); } details(id: string, navigationName: string = 'id'): Promise { @@ -193,41 +207,45 @@ export class FluxCapacitor extends EventEmitter { }); } - switchCollection(collection: string): Promise { - this.query.withConfiguration({ collection, refinements: [], sort: [], skip: 0 }); - return this.search() - .then((res) => { - this.emit(Events.COLLECTION_CHANGED, collection); - return res; - }); - } - - private emitQueryChanged(oldQuery: string, newQuery: string) { - if (oldQuery.toLowerCase() !== newQuery.toLowerCase()) { - this.emit(Events.QUERY_CHANGED, newQuery); - } - } - - private get filteredRequest() { - return filterObject(this.query.raw, '!{query,refinements,skip}'); - } - - private resetPaging(reset: boolean): Promise { - return reset ? this.page.reset() : this.search(); + // switchCollection(collection: string): Promise { + // this.query.withConfiguration({ collection, refinements: [], sort: [], skip: 0 }); + // return this.search() + // .then((res) => { + // this.emit(Events.COLLECTION_CHANGED, collection); + // return res; + // }); + // } + + switchCollection(collection: string) { + this.store.dispatch(Actions.selectCollection(collection)); } - private doRefinement({ reset }: RefinementConfig): Promise { - return this.resetPaging(reset) - .then(() => this.emit(Events.REFINEMENTS_CHANGED, this.navigationInfo)) - .then(() => this.navigationInfo); - } - - private get navigationInfo(): NavigationInfo { - return { - available: this.results.availableNavigation, - selected: this.results.selectedNavigation, - }; - } + // private emitQueryChanged(oldQuery: string, newQuery: string) { + // if (oldQuery.toLowerCase() !== newQuery.toLowerCase()) { + // this.emit(Events.QUERY_CHANGED, newQuery); + // } + // } + // + // private get filteredRequest() { + // return filterObject(this.query.raw, '!{query,refinements,skip}'); + // } + // + // private resetPaging(reset: boolean): Promise { + // return reset ? this.page.reset() : this.search(); + // } + // + // private doRefinement({ reset }: RefinementConfig): Promise { + // return this.resetPaging(reset) + // .then(() => this.emit(Events.REFINEMENTS_CHANGED, this.navigationInfo)) + // .then(() => this.navigationInfo); + // } + // + // private get navigationInfo(): NavigationInfo { + // return { + // available: this.results.availableNavigation, + // selected: this.results.selectedNavigation, + // }; + // } } export interface NavigationInfo { diff --git a/src/flux/store.ts b/src/flux/store.ts index d901c19..be250d6 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -211,7 +211,7 @@ namespace Store { export function create() { return redux.createStore( reducer, - redux.applyMiddleware(thunk) + redux.applyMiddleware(thunk), ); } } diff --git a/test/unit/flux/capacitor.ts b/test/unit/flux/capacitor.ts index 83c858b..a5f5845 100644 --- a/test/unit/flux/capacitor.ts +++ b/test/unit/flux/capacitor.ts @@ -9,7 +9,7 @@ const SELECTED_REFINEMENT: SelectedValueRefinement = { type: 'Value', navigation const REFINEMENT_RESULT = { availableNavigation: 'a', selectedNavigation: 'b' }; const DETAILS_RESULT = { records: [{}] }; -suite.skip('FluxCapacitor', ({ expect, spy }) => { +suite('FluxCapacitor', ({ expect, spy }) => { let flux: FluxCapacitor; beforeEach(() => { @@ -22,655 +22,657 @@ suite.skip('FluxCapacitor', ({ expect, spy }) => { flux = null; }); - it('should be defined', () => { - expect(flux).to.be.ok; - expect(flux.bridge).to.be.ok; - expect(flux.query).to.be.ok; - expect(flux.results).to.not.be.ok; - }); - - it('should accept a mask for configuration', () => { - const config: any = { a: 'something', b: 'Ascending' }; - - flux = new FluxCapacitor(CUSTOMER_ID, config); - - expect(flux.query.raw).to.contain.keys('a', 'b'); - - flux = new FluxCapacitor(CUSTOMER_ID, config, '{refinements,area}'); - - expect(flux.query.raw).to.not.contain.keys('a', 'b'); - }); - - it('should strip fields from configuration', () => { - flux = new FluxCapacitor(CUSTOMER_ID, { - a: 'something', - b: 'Ascending', - bridge: { - headers: { c: 'd' }, - https: true, - }, + describe('constructor()', () => { + it('should be defined', () => { + expect(flux).to.be.ok; + expect(flux.bridge).to.be.ok; + expect(flux.query).to.be.ok; + expect(flux.results).to.not.be.ok; }); - expect(flux.query.raw).to.not.contain.keys('bridge'); - }); - - it('should set headers on bridge', () => { - const headers = { c: 'd' }; - flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { headers } }); + it('should accept a mask for configuration', () => { + const config: any = { a: 'something', b: 'Ascending' }; - expect(flux.bridge.headers).to.eq(headers); - }); + flux = new FluxCapacitor(CUSTOMER_ID, config); - it('should set HTTPS on bridge', () => { - flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { https: true } }); + expect(flux.query.raw).to.contain.keys('a', 'b'); - expect(flux.bridge.baseUrl).to.eq('https://services-cors.groupbycloud.com:443/api/v1'); - }); + flux = new FluxCapacitor(CUSTOMER_ID, config, '{refinements,area}'); - it('should add default event listener', (done) => { - const error: any = { a: 'b' }; - flux = new FluxCapacitor(CUSTOMER_ID); - flux.on(Events.ERROR_BRIDGE, (err) => { - expect(err).to.eq(error); - done(); + expect(flux.query.raw).to.not.contain.keys('a', 'b'); }); - expect(flux.bridge.errorHandler).to.be.a('function'); - - flux.bridge.errorHandler(error); - }); - - it('should set configured errorHandler on bridge', () => { - const errorHandler = spy(); - flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { errorHandler } }); - const error: any = { a: 'b' }; - - flux.bridge.errorHandler(error); - - expect(errorHandler.calledWith(error)).to.be.true; - }); - - it('should not override default errorHandler on bridge', (done) => { - flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { errorHandler: () => null } }); - flux.on(Events.ERROR_BRIDGE, () => done()); - - flux.bridge.errorHandler({}); - }); - - describe('search()', () => { - it('should make a search request', (done) => { - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).query).to.eq('testing'); - done(); + it('should strip fields from configuration', () => { + flux = new FluxCapacitor(CUSTOMER_ID, { + a: 'something', + b: 'Ascending', + bridge: { + headers: { c: 'd' }, + https: true, + }, }); - flux.search('testing'); + expect(flux.query.raw).to.not.contain.keys('bridge'); }); - describe('events', () => { - it('should emit a search event before searching', (done) => { - flux.bridge.search = (): any => expect.fail(); - flux.on(Events.SEARCH, (query) => { - expect(query.query).to.eq('danish'); - done(); - }); - - flux.search('danish'); - }); - - it('should emit a results event', (done) => { - flux.bridge.search = (): any => Promise.resolve('ok'); - // mock.post(SEARCH_URL, (req, res) => res.body('ok')); - flux.on(Events.RESULTS, () => done()); - - flux.search(''); - }); + it('should set headers on bridge', () => { + const headers = { c: 'd' }; + flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { headers } }); - it('should not emit a redirect event', (done) => { - flux.bridge.search = (): any => ({ then: (cb) => cb({}) }); - flux.on(Events.REDIRECT, () => expect.fail()); - - flux.search(''); - done(); - }); - - it('should emit a redirect event', (done) => { - const redirect = 'something.html'; - flux.bridge.search = (): any => ({ then: (cb) => cb({ redirect }) }); - flux.on(Events.REDIRECT, (url) => expect(url).to.eq(redirect)); - - flux.search(''); - done(); - }); - - it('should not emit a query_changed event on subsequent equivalent requests', (done) => { - flux.bridge.search = (): any => Promise.resolve('ok'); - - flux.search('apple') - .then(() => flux.on(Events.QUERY_CHANGED, () => expect.fail())) - .then(() => flux.search('apple')) - .then(() => done()); - }); - - it('should emit a query_changed event on changing the query', (done) => { - flux.bridge.search = (): any => Promise.resolve('ok'); - - flux.search('shoes') - .then(() => flux.on(Events.QUERY_CHANGED, (query) => { - expect(query).to.eq('other'); - done(); - })) - .then(() => flux.search('other')); - }); - - it('should emit a query_changed with case insensitivity', (done) => { - flux.bridge.search = (): any => Promise.resolve('ok'); - - flux.search('apple') - .then(() => flux.on(Events.QUERY_CHANGED, () => expect.fail())) - .then(() => flux.search('ApPle')) - .then(() => done()); - }); - }); - }); - - describe('refinements()', () => { - it('should make a refinements request', (done) => { - mock.post(REFINEMENTS_URL, (req, res) => { - expect(JSON.parse(req.body()).navigationName).to.eq('brand'); - done(); - }); - - flux.refinements('brand'); + expect(flux.bridge.headers).to.eq(headers); }); - describe('events', () => { - it('should emit a refinement_results event', (done) => { - mock.post(REFINEMENTS_URL, (req, res) => res.body('ok')); - flux.on(Events.REFINEMENT_RESULTS, () => done()); + it('should set HTTPS on bridge', () => { + flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { https: true } }); - flux.refinements(''); - }); + expect(flux.bridge.baseUrl).to.eq('https://services-cors.groupbycloud.com:443/api/v1'); }); - }); - describe('refine()', () => { - it('should make a request on refinement', (done) => { - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).refinements.length).to.eq(1); + it('should add default event listener', (done) => { + const error: any = { a: 'b' }; + flux = new FluxCapacitor(CUSTOMER_ID); + flux.on(Events.ERROR_BRIDGE, (err) => { + expect(err).to.eq(error); done(); }); - flux.refine(SELECTED_REFINEMENT); - }); - - it('should reset paging on refinement', (done) => { - flux.query.skip(20); - mock.post(SEARCH_URL, (req, res) => { - expect(flux.query.build().skip).to.eq(0); - done(); - }); + expect(flux.bridge.errorHandler).to.be.a('function'); - flux.refine(SELECTED_REFINEMENT); + flux.bridge.errorHandler(error); }); - it('should skip reset paging on refinement', (done) => { - flux.query.skip(20); - mock.post(SEARCH_URL, (req, res) => res.body('ok')); + it('should set configured errorHandler on bridge', () => { + const errorHandler = spy(); + flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { errorHandler } }); + const error: any = { a: 'b' }; - flux.refine(SELECTED_REFINEMENT, { reset: false }) - .then(() => { - expect(flux.query.build().skip).to.eq(20); - done(); - }); - }); - }); - - describe('events', () => { - it('should emit refinements_changed event on refinement', (done) => { - mock.post(SEARCH_URL, (req, res) => res.body(JSON.stringify(REFINEMENT_RESULT))); - flux.on(Events.REFINEMENTS_CHANGED, (data) => { - expect(data.available).to.eq('a'); - expect(data.selected).to.eq('b'); - done(); - }); - - flux.refine(SELECTED_REFINEMENT); - }); - }); - - describe('unrefine()', () => { - it('should make a request on un-refinement', (done) => { - flux.query.withSelectedRefinements(SELECTED_REFINEMENT); - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).refinements).to.not.be.ok; - done(); - }); - - flux.unrefine(SELECTED_REFINEMENT); - }); - - it('should un-refine with deep equality', (done) => { - flux.query.withSelectedRefinements(SELECTED_REFINEMENT); - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).refinements).to.not.be.ok; - done(); - }); - - // intentionally not using SELECTED_REFINEMENT - flux.unrefine({ type: 'Value', navigationName: 'brand', value: 'DeWalt' }); - }); - - it('should reset paging on un-refinement', (done) => { - flux.query.skip(20); - flux.query.withSelectedRefinements(SELECTED_REFINEMENT); - mock.post(SEARCH_URL, (req, res) => res.body('ok')); - - flux.unrefine(SELECTED_REFINEMENT) - .then(() => { - expect(flux.query.build().skip).to.eq(0); - done(); - }); - }); - - describe('events', () => { - it('should emit refinements_changed event on un-refinement', (done) => { - flux.query.withSelectedRefinements(SELECTED_REFINEMENT); - mock.post(SEARCH_URL, (req, res) => res.body(JSON.stringify(REFINEMENT_RESULT))); - flux.on(Events.REFINEMENTS_CHANGED, (data) => { - expect(data.available).to.eq('a'); - expect(data.selected).to.eq('b'); - done(); - }); - - flux.unrefine(SELECTED_REFINEMENT); - }); - }); - }); - - describe('paging behaviour', () => { - beforeEach(() => { - flux.query.skip(20); - flux.results = { totalRecordCount: 300 }; - }); + flux.bridge.errorHandler(error); - it('should reset paging', (done) => { - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).skip).to.eq(0); - done(); - }); - - flux.page.reset(); - }); - - it('should page forward', (done) => { - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).skip).to.eq(30); - done(); - }); - - flux.page.next(); + expect(errorHandler.calledWith(error)).to.be.true; }); - it('should page backward', (done) => { - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).skip).to.eq(10); - done(); - }); - flux.page.prev(); - }); - - it('should advance to last page', (done) => { - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).skip).to.eq(290); - done(); - }); + it('should not override default errorHandler on bridge', (done) => { + flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { errorHandler: () => null } }); + flux.on(Events.ERROR_BRIDGE, () => done()); - flux.page.last(); + flux.bridge.errorHandler({}); }); }); - describe('resizing behaviour', () => { - it('should resize the page and keep skip', (done) => { - flux.query.withPageSize(10); - flux.query.skip(20); - flux.page.pageExists = () => true; - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).skip).to.eq(20); - expect(JSON.parse(req.body()).pageSize).to.eq(20); - done(); - }); - - flux.resize(20, false); - }); - - it('should resize the page and keep skip on the same page', (done) => { - flux.query.withPageSize(10); - flux.query.skip(30); - flux.page.pageExists = () => true; - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).skip).to.eq(20); - expect(JSON.parse(req.body()).pageSize).to.eq(20); - done(); - }); - - flux.resize(20, false); - }); - - it('should resize the page and bring skip to 0', (done) => { - flux.query.withPageSize(10); - flux.query.skip(20); - flux.page.pageExists = () => true; - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).skip).to.eq(0); - expect(JSON.parse(req.body()).pageSize).to.eq(30); - done(); - }); - - flux.resize(30, true); - }); - - it('should resize from smaller to larger and keep skip when total near max', (done) => { - flux.query.withPageSize(12); - flux.query.skip(9984); - flux.page.pageExists = () => true; - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).skip).to.eq(9960); - expect(JSON.parse(req.body()).pageSize).to.eq(24); - done(); - }); - - flux.resize(24, false); - }); - - it('should resize from larger to smaller and keep skip when total near max', (done) => { - flux.query.withPageSize(50); - flux.query.skip(9950); - flux.page.pageExists = () => true; - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).skip).to.eq(9947); - expect(JSON.parse(req.body()).pageSize).to.eq(49); - done(); - }); - - flux.resize(49, false); - }); - }); - - describe('rewrite()', () => { - it('should rewrite the query', (done) => { - const newQuery = 'montana'; - flux.query.withQuery('alabama'); - flux.search = (query): any => Promise.resolve(expect(query).to.eq(newQuery)); - - flux.rewrite(newQuery) - .then(() => done()); - }); - - it('should rewrite the query but not perform a search', () => { - const newQuery = 'montana'; - flux.query.withQuery('alabama'); - flux.search = (query): any => expect.fail(); - - flux.rewrite(newQuery, { skipSearch: true }); - - expect(flux.query.raw.query).to.eq(newQuery); - }); - - it('should emit events on search', (done) => { - const newQuery = 'montana'; - flux.query.withQuery('alabama'); - - flux.search = (query): any => ({ then: (cb) => cb() }); - flux.emit = (event, data): any => { - expect(data).to.eq(newQuery); - done(); - }; - - flux.rewrite(newQuery); - }); - - it('should emit events when not searching', () => { - const newQuery = 'montana'; - flux.query.withQuery('alabama'); - flux.emit = (event, data): any => { - switch (event) { - case Events.REWRITE_QUERY: - return expect(data).to.eq(newQuery); - case Events.QUERY_CHANGED: - break; - default: - expect.fail(); - } - }; - - flux.rewrite(newQuery, { skipSearch: true }); - }); - }); - - describe('reset behaviour', () => { - it('should reset the query', (done) => { - flux.query.withQuery('alabama'); - flux.resetRecall = () => null; - mock.post(SEARCH_URL, (req, res) => { - const body = JSON.parse(req.body()); - expect(body.query).to.eq(''); - done(); - }); - - flux.reset(); - }); - - it('should accept a new query on reset', (done) => { - flux.query.withQuery('alabama'); - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).query).to.eq('texas'); - done(); - }); - - flux.reset('texas'); - }); - - describe('events', () => { - it('should emit events', (done) => { - mock.post(SEARCH_URL, (req, res) => res.body('ok')); - - let count = 0; - const checkComplete = () => { - if (++count === 2) { - done(); - } - }; - flux.on(Events.RESET, checkComplete); - flux.on(Events.PAGE_CHANGED, checkComplete); - flux.reset(); - }); - }); - }); - - describe('sort()', () => { - it('should reset paging but not refinements', (done) => { - const refinement: SelectedValueRefinement = { navigationName: 'brand', type: 'Value', value: 'DeWalt' }; - flux.query.skip(30) - .withSelectedRefinements(refinement); - mock.post(SEARCH_URL, (req, res) => { - const body = JSON.parse(req.body()); - expect(body.skip).to.eq(0); - expect(body.sort).to.eql([{ field: 'price', order: 'Ascending' }]); - expect(body.refinements).to.eql([refinement]); - done(); - }); - - flux.sort({ field: 'price', order: 'Ascending' }); - }); - - it('should add sorts', (done) => { - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).sort).to.eql([{ field: 'price', order: 'Ascending' }]); - done(); - }); - - flux.sort({ field: 'price', order: 'Ascending' }); - }); - - it('should add more sorts', (done) => { - flux.query.withSorts({ field: 'title', order: 'Descending' }); - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).sort.length).to.eq(2); - done(); - }); - - flux.sort({ field: 'price', order: 'Ascending' }); - }); - - it('should remove sorts', (done) => { - flux.query.withSorts({ field: 'price', order: 'Descending' }); - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).sort).to.eql([{ field: 'price', order: 'Ascending' }]); - done(); - }); - - flux.sort({ field: 'price', order: 'Ascending' }); - }); - - it('should remove all sorts', (done) => { - const sorts: Sort[] = [ - { field: 'price', order: 'Descending' }, - { field: 'other', order: 'Ascending' }, - { field: 'type', order: 'Descending' }, - ]; - flux.query.withSorts(...sorts); - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).sort).to.eql([{ field: 'price', order: 'Ascending' }]); - done(); - }); - - flux.sort({ field: 'price', order: 'Ascending' }, sorts); - }); - - it('should emit sort event', (done) => { - const sort: any = { field: 'price', order: 'Ascending' }; - mock.post(SEARCH_URL, (req, res) => res.body('ok')); - flux.on(Events.SORT, (newSort) => { - expect(newSort).to.be.ok; - done(); - }); - - flux.sort(sort); - }); - }); - - describe('details()', () => { - it('should refine by id', (done) => { - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).refinements).to.eql([{ navigationName: 'id', type: 'Value', value: '14830' }]); - done(); - }); - - flux.details('14830'); - }); - - it('should refine by specified field', (done) => { - const navigationName = 'variants.id'; - mock.post(SEARCH_URL, (req, res) => { - expect(JSON.parse(req.body()).refinements).to.eql([{ navigationName, type: 'Value', value: '14830' }]); - done(); - }); - - flux.details('14830', navigationName); - }); - - it('should persist area, collection, language, fields', (done) => { - flux.query.withConfiguration({ - area: 'nonProd', - collection: 'offbrand', - fields: ['title', 'price'], - language: 'zh', - }); - mock.post(SEARCH_URL, (req, res) => { - const body = JSON.parse(req.body()); - expect(body.area).to.eq('nonProd'); - expect(body.collection).to.eq('offbrand'); - expect(body.language).to.eq('zh'); - expect(body.pageSize).to.eq(1); - expect(body.fields).to.eql(['title', 'price']); - done(); - }); - - flux.details('14830'); - }); - - it('should emit details event', (done) => { - mock.post(SEARCH_URL, (req, res) => res.body(JSON.stringify(DETAILS_RESULT))); - flux.on(Events.DETAILS, (data) => { - expect(data).to.be.ok; - done(); - }); - - flux.details('14830'); - }); - }); - - describe('switchCollection()', () => { - it('should switch collection', (done) => { - const collection = 'other'; - mock.post(SEARCH_URL, (req, res) => res.body('ok')); - flux.query.withConfiguration({ collection: 'something' }); - - flux.switchCollection(collection) - .then(() => { - expect(flux.query.raw.collection).to.eq(collection); - done(); - }); - }); - - it('should reset paging, sort and refinements on switch collection', (done) => { - const collection = 'other'; - mock.post(SEARCH_URL, (req, res) => res.body('ok')); - - flux.query.withConfiguration({ collection: 'something' }) - .withSelectedRefinements({ navigationName: 'brand', type: 'Value', value: 'Nike' }) - .withSorts({ field: 'price', order: 'Descending' }) - .skip(30); - - flux.switchCollection(collection) - .then(() => { - const rawQuery = flux.query.raw; - expect(rawQuery.collection).to.eq(collection); - expect(rawQuery.skip).to.eq(0); - expect(rawQuery.sort).to.be.empty; - expect(rawQuery.refinements).to.be.empty; - done(); - }); - }); - - it('should emit collection_changed event', (done) => { - const collection = 'support'; - mock.post(SEARCH_URL, (req, res) => res.body('ok')); - flux.on(Events.COLLECTION_CHANGED, (coll) => { - expect(coll).to.eq(collection); - done(); - }); - - flux.switchCollection(collection); - }); - }); - - it('should reset recall', () => { - flux.query - .withQuery('alabama') - .withPageSize(20) - .skip(34) - .withSelectedRefinements({ navigationName: 'a', value: 'b', type: 'Value' }) - .withOrFields('boots', 'hats'); - - flux.resetRecall(); - - const request = flux.query.raw; - expect(request.pageSize).to.be.ok; - expect(request.orFields).to.be.ok; - expect(request.refinements).to.eql([]); - expect(request.skip).to.not.be.ok; - expect(request.query).to.eq(''); - }); + // describe('search()', () => { + // it('should make a search request', (done) => { + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).query).to.eq('testing'); + // done(); + // }); + // + // flux.search('testing'); + // }); + // + // describe('events', () => { + // it('should emit a search event before searching', (done) => { + // flux.bridge.search = (): any => expect.fail(); + // flux.on(Events.SEARCH, (query) => { + // expect(query.query).to.eq('danish'); + // done(); + // }); + // + // flux.search('danish'); + // }); + // + // it('should emit a results event', (done) => { + // flux.bridge.search = (): any => Promise.resolve('ok'); + // // mock.post(SEARCH_URL, (req, res) => res.body('ok')); + // flux.on(Events.RESULTS, () => done()); + // + // flux.search(''); + // }); + // + // it('should not emit a redirect event', (done) => { + // flux.bridge.search = (): any => ({ then: (cb) => cb({}) }); + // flux.on(Events.REDIRECT, () => expect.fail()); + // + // flux.search(''); + // done(); + // }); + // + // it('should emit a redirect event', (done) => { + // const redirect = 'something.html'; + // flux.bridge.search = (): any => ({ then: (cb) => cb({ redirect }) }); + // flux.on(Events.REDIRECT, (url) => expect(url).to.eq(redirect)); + // + // flux.search(''); + // done(); + // }); + // + // it('should not emit a query_changed event on subsequent equivalent requests', (done) => { + // flux.bridge.search = (): any => Promise.resolve('ok'); + // + // flux.search('apple') + // .then(() => flux.on(Events.QUERY_CHANGED, () => expect.fail())) + // .then(() => flux.search('apple')) + // .then(() => done()); + // }); + // + // it('should emit a query_changed event on changing the query', (done) => { + // flux.bridge.search = (): any => Promise.resolve('ok'); + // + // flux.search('shoes') + // .then(() => flux.on(Events.QUERY_CHANGED, (query) => { + // expect(query).to.eq('other'); + // done(); + // })) + // .then(() => flux.search('other')); + // }); + // + // it('should emit a query_changed with case insensitivity', (done) => { + // flux.bridge.search = (): any => Promise.resolve('ok'); + // + // flux.search('apple') + // .then(() => flux.on(Events.QUERY_CHANGED, () => expect.fail())) + // .then(() => flux.search('ApPle')) + // .then(() => done()); + // }); + // }); + // }); + // + // describe('refinements()', () => { + // it('should make a refinements request', (done) => { + // mock.post(REFINEMENTS_URL, (req, res) => { + // expect(JSON.parse(req.body()).navigationName).to.eq('brand'); + // done(); + // }); + // + // flux.refinements('brand'); + // }); + // + // describe('events', () => { + // it('should emit a refinement_results event', (done) => { + // mock.post(REFINEMENTS_URL, (req, res) => res.body('ok')); + // flux.on(Events.REFINEMENT_RESULTS, () => done()); + // + // flux.refinements(''); + // }); + // }); + // }); + // + // describe('refine()', () => { + // it('should make a request on refinement', (done) => { + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).refinements.length).to.eq(1); + // done(); + // }); + // + // flux.refine(SELECTED_REFINEMENT); + // }); + // + // it('should reset paging on refinement', (done) => { + // flux.query.skip(20); + // mock.post(SEARCH_URL, (req, res) => { + // expect(flux.query.build().skip).to.eq(0); + // done(); + // }); + // + // flux.refine(SELECTED_REFINEMENT); + // }); + // + // it('should skip reset paging on refinement', (done) => { + // flux.query.skip(20); + // mock.post(SEARCH_URL, (req, res) => res.body('ok')); + // + // flux.refine(SELECTED_REFINEMENT, { reset: false }) + // .then(() => { + // expect(flux.query.build().skip).to.eq(20); + // done(); + // }); + // }); + // }); + // + // describe('events', () => { + // it('should emit refinements_changed event on refinement', (done) => { + // mock.post(SEARCH_URL, (req, res) => res.body(JSON.stringify(REFINEMENT_RESULT))); + // flux.on(Events.REFINEMENTS_CHANGED, (data) => { + // expect(data.available).to.eq('a'); + // expect(data.selected).to.eq('b'); + // done(); + // }); + // + // flux.refine(SELECTED_REFINEMENT); + // }); + // }); + // + // describe('unrefine()', () => { + // it('should make a request on un-refinement', (done) => { + // flux.query.withSelectedRefinements(SELECTED_REFINEMENT); + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).refinements).to.not.be.ok; + // done(); + // }); + // + // flux.unrefine(SELECTED_REFINEMENT); + // }); + // + // it('should un-refine with deep equality', (done) => { + // flux.query.withSelectedRefinements(SELECTED_REFINEMENT); + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).refinements).to.not.be.ok; + // done(); + // }); + // + // // intentionally not using SELECTED_REFINEMENT + // flux.unrefine({ type: 'Value', navigationName: 'brand', value: 'DeWalt' }); + // }); + // + // it('should reset paging on un-refinement', (done) => { + // flux.query.skip(20); + // flux.query.withSelectedRefinements(SELECTED_REFINEMENT); + // mock.post(SEARCH_URL, (req, res) => res.body('ok')); + // + // flux.unrefine(SELECTED_REFINEMENT) + // .then(() => { + // expect(flux.query.build().skip).to.eq(0); + // done(); + // }); + // }); + // + // describe('events', () => { + // it('should emit refinements_changed event on un-refinement', (done) => { + // flux.query.withSelectedRefinements(SELECTED_REFINEMENT); + // mock.post(SEARCH_URL, (req, res) => res.body(JSON.stringify(REFINEMENT_RESULT))); + // flux.on(Events.REFINEMENTS_CHANGED, (data) => { + // expect(data.available).to.eq('a'); + // expect(data.selected).to.eq('b'); + // done(); + // }); + // + // flux.unrefine(SELECTED_REFINEMENT); + // }); + // }); + // }); + // + // describe('paging behaviour', () => { + // beforeEach(() => { + // flux.query.skip(20); + // flux.results = { totalRecordCount: 300 }; + // }); + // + // it('should reset paging', (done) => { + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).skip).to.eq(0); + // done(); + // }); + // + // flux.page.reset(); + // }); + // + // it('should page forward', (done) => { + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).skip).to.eq(30); + // done(); + // }); + // + // flux.page.next(); + // }); + // + // it('should page backward', (done) => { + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).skip).to.eq(10); + // done(); + // }); + // flux.page.prev(); + // }); + // + // it('should advance to last page', (done) => { + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).skip).to.eq(290); + // done(); + // }); + // + // flux.page.last(); + // }); + // }); + // + // describe('resizing behaviour', () => { + // it('should resize the page and keep skip', (done) => { + // flux.query.withPageSize(10); + // flux.query.skip(20); + // flux.page.pageExists = () => true; + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).skip).to.eq(20); + // expect(JSON.parse(req.body()).pageSize).to.eq(20); + // done(); + // }); + // + // flux.resize(20, false); + // }); + // + // it('should resize the page and keep skip on the same page', (done) => { + // flux.query.withPageSize(10); + // flux.query.skip(30); + // flux.page.pageExists = () => true; + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).skip).to.eq(20); + // expect(JSON.parse(req.body()).pageSize).to.eq(20); + // done(); + // }); + // + // flux.resize(20, false); + // }); + // + // it('should resize the page and bring skip to 0', (done) => { + // flux.query.withPageSize(10); + // flux.query.skip(20); + // flux.page.pageExists = () => true; + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).skip).to.eq(0); + // expect(JSON.parse(req.body()).pageSize).to.eq(30); + // done(); + // }); + // + // flux.resize(30, true); + // }); + // + // it('should resize from smaller to larger and keep skip when total near max', (done) => { + // flux.query.withPageSize(12); + // flux.query.skip(9984); + // flux.page.pageExists = () => true; + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).skip).to.eq(9960); + // expect(JSON.parse(req.body()).pageSize).to.eq(24); + // done(); + // }); + // + // flux.resize(24, false); + // }); + // + // it('should resize from larger to smaller and keep skip when total near max', (done) => { + // flux.query.withPageSize(50); + // flux.query.skip(9950); + // flux.page.pageExists = () => true; + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).skip).to.eq(9947); + // expect(JSON.parse(req.body()).pageSize).to.eq(49); + // done(); + // }); + // + // flux.resize(49, false); + // }); + // }); + // + // describe('rewrite()', () => { + // it('should rewrite the query', (done) => { + // const newQuery = 'montana'; + // flux.query.withQuery('alabama'); + // flux.search = (query): any => Promise.resolve(expect(query).to.eq(newQuery)); + // + // flux.rewrite(newQuery) + // .then(() => done()); + // }); + // + // it('should rewrite the query but not perform a search', () => { + // const newQuery = 'montana'; + // flux.query.withQuery('alabama'); + // flux.search = (query): any => expect.fail(); + // + // flux.rewrite(newQuery, { skipSearch: true }); + // + // expect(flux.query.raw.query).to.eq(newQuery); + // }); + // + // it('should emit events on search', (done) => { + // const newQuery = 'montana'; + // flux.query.withQuery('alabama'); + // + // flux.search = (query): any => ({ then: (cb) => cb() }); + // flux.emit = (event, data): any => { + // expect(data).to.eq(newQuery); + // done(); + // }; + // + // flux.rewrite(newQuery); + // }); + // + // it('should emit events when not searching', () => { + // const newQuery = 'montana'; + // flux.query.withQuery('alabama'); + // flux.emit = (event, data): any => { + // switch (event) { + // case Events.REWRITE_QUERY: + // return expect(data).to.eq(newQuery); + // case Events.QUERY_CHANGED: + // break; + // default: + // expect.fail(); + // } + // }; + // + // flux.rewrite(newQuery, { skipSearch: true }); + // }); + // }); + // + // describe('reset behaviour', () => { + // it('should reset the query', (done) => { + // flux.query.withQuery('alabama'); + // flux.resetRecall = () => null; + // mock.post(SEARCH_URL, (req, res) => { + // const body = JSON.parse(req.body()); + // expect(body.query).to.eq(''); + // done(); + // }); + // + // flux.reset(); + // }); + // + // it('should accept a new query on reset', (done) => { + // flux.query.withQuery('alabama'); + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).query).to.eq('texas'); + // done(); + // }); + // + // flux.reset('texas'); + // }); + // + // describe('events', () => { + // it('should emit events', (done) => { + // mock.post(SEARCH_URL, (req, res) => res.body('ok')); + // + // let count = 0; + // const checkComplete = () => { + // if (++count === 2) { + // done(); + // } + // }; + // flux.on(Events.RESET, checkComplete); + // flux.on(Events.PAGE_CHANGED, checkComplete); + // flux.reset(); + // }); + // }); + // }); + // + // describe('sort()', () => { + // it('should reset paging but not refinements', (done) => { + // const refinement: SelectedValueRefinement = { navigationName: 'brand', type: 'Value', value: 'DeWalt' }; + // flux.query.skip(30) + // .withSelectedRefinements(refinement); + // mock.post(SEARCH_URL, (req, res) => { + // const body = JSON.parse(req.body()); + // expect(body.skip).to.eq(0); + // expect(body.sort).to.eql([{ field: 'price', order: 'Ascending' }]); + // expect(body.refinements).to.eql([refinement]); + // done(); + // }); + // + // flux.sort({ field: 'price', order: 'Ascending' }); + // }); + // + // it('should add sorts', (done) => { + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).sort).to.eql([{ field: 'price', order: 'Ascending' }]); + // done(); + // }); + // + // flux.sort({ field: 'price', order: 'Ascending' }); + // }); + // + // it('should add more sorts', (done) => { + // flux.query.withSorts({ field: 'title', order: 'Descending' }); + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).sort.length).to.eq(2); + // done(); + // }); + // + // flux.sort({ field: 'price', order: 'Ascending' }); + // }); + // + // it('should remove sorts', (done) => { + // flux.query.withSorts({ field: 'price', order: 'Descending' }); + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).sort).to.eql([{ field: 'price', order: 'Ascending' }]); + // done(); + // }); + // + // flux.sort({ field: 'price', order: 'Ascending' }); + // }); + // + // it('should remove all sorts', (done) => { + // const sorts: Sort[] = [ + // { field: 'price', order: 'Descending' }, + // { field: 'other', order: 'Ascending' }, + // { field: 'type', order: 'Descending' }, + // ]; + // flux.query.withSorts(...sorts); + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).sort).to.eql([{ field: 'price', order: 'Ascending' }]); + // done(); + // }); + // + // flux.sort({ field: 'price', order: 'Ascending' }, sorts); + // }); + // + // it('should emit sort event', (done) => { + // const sort: any = { field: 'price', order: 'Ascending' }; + // mock.post(SEARCH_URL, (req, res) => res.body('ok')); + // flux.on(Events.SORT, (newSort) => { + // expect(newSort).to.be.ok; + // done(); + // }); + // + // flux.sort(sort); + // }); + // }); + // + // describe('details()', () => { + // it('should refine by id', (done) => { + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).refinements).to.eql([{ navigationName: 'id', type: 'Value', value: '14830' }]); + // done(); + // }); + // + // flux.details('14830'); + // }); + // + // it('should refine by specified field', (done) => { + // const navigationName = 'variants.id'; + // mock.post(SEARCH_URL, (req, res) => { + // expect(JSON.parse(req.body()).refinements).to.eql([{ navigationName, type: 'Value', value: '14830' }]); + // done(); + // }); + // + // flux.details('14830', navigationName); + // }); + // + // it('should persist area, collection, language, fields', (done) => { + // flux.query.withConfiguration({ + // area: 'nonProd', + // collection: 'offbrand', + // fields: ['title', 'price'], + // language: 'zh', + // }); + // mock.post(SEARCH_URL, (req, res) => { + // const body = JSON.parse(req.body()); + // expect(body.area).to.eq('nonProd'); + // expect(body.collection).to.eq('offbrand'); + // expect(body.language).to.eq('zh'); + // expect(body.pageSize).to.eq(1); + // expect(body.fields).to.eql(['title', 'price']); + // done(); + // }); + // + // flux.details('14830'); + // }); + // + // it('should emit details event', (done) => { + // mock.post(SEARCH_URL, (req, res) => res.body(JSON.stringify(DETAILS_RESULT))); + // flux.on(Events.DETAILS, (data) => { + // expect(data).to.be.ok; + // done(); + // }); + // + // flux.details('14830'); + // }); + // }); + // + // describe('switchCollection()', () => { + // it('should switch collection', (done) => { + // const collection = 'other'; + // mock.post(SEARCH_URL, (req, res) => res.body('ok')); + // flux.query.withConfiguration({ collection: 'something' }); + // + // flux.switchCollection(collection) + // .then(() => { + // expect(flux.query.raw.collection).to.eq(collection); + // done(); + // }); + // }); + // + // it('should reset paging, sort and refinements on switch collection', (done) => { + // const collection = 'other'; + // mock.post(SEARCH_URL, (req, res) => res.body('ok')); + // + // flux.query.withConfiguration({ collection: 'something' }) + // .withSelectedRefinements({ navigationName: 'brand', type: 'Value', value: 'Nike' }) + // .withSorts({ field: 'price', order: 'Descending' }) + // .skip(30); + // + // flux.switchCollection(collection) + // .then(() => { + // const rawQuery = flux.query.raw; + // expect(rawQuery.collection).to.eq(collection); + // expect(rawQuery.skip).to.eq(0); + // expect(rawQuery.sort).to.be.empty; + // expect(rawQuery.refinements).to.be.empty; + // done(); + // }); + // }); + // + // it('should emit collection_changed event', (done) => { + // const collection = 'support'; + // mock.post(SEARCH_URL, (req, res) => res.body('ok')); + // flux.on(Events.COLLECTION_CHANGED, (coll) => { + // expect(coll).to.eq(collection); + // done(); + // }); + // + // flux.switchCollection(collection); + // }); + // }); + // + // it('should reset recall', () => { + // flux.query + // .withQuery('alabama') + // .withPageSize(20) + // .skip(34) + // .withSelectedRefinements({ navigationName: 'a', value: 'b', type: 'Value' }) + // .withOrFields('boots', 'hats'); + // + // flux.resetRecall(); + // + // const request = flux.query.raw; + // expect(request.pageSize).to.be.ok; + // expect(request.orFields).to.be.ok; + // expect(request.refinements).to.eql([]); + // expect(request.skip).to.not.be.ok; + // expect(request.query).to.eq(''); + // }); }); From 35436bc3123ec8aeebbed63a1fb3fb025676aaaf Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Mon, 24 Apr 2017 11:51:03 -0400 Subject: [PATCH 29/56] Add general reducer logic --- src/flux/reducer.ts | 81 +++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts index cca9b2f..b8f76fe 100644 --- a/src/flux/reducer.ts +++ b/src/flux/reducer.ts @@ -2,52 +2,68 @@ import * as redux from 'redux'; import Actions from './actions'; import Store from './store'; -export function updateQuery(state: Store.Query, action) { - if (action === Actions.UPDATE_SEARCH) { - return { ...state, query: action.query }; +export function updateAutocomplete(state: Store.Autocomplete, action) { + switch (action) { + case Actions.UPDATE_AUTOCOMPLETE_QUERY: + return { ...state, query: action.query }; + default: + return state; } } -export function updateNavigations(state: Store.Navigation, action) { +export function updateCollections(state: Store.Indexed.Selectable, action) { switch (action) { - // case Actions.SELECT_REFINEMENT: - // return { ...state, query: action.navigationId }; + case Actions.SELECT_COLLECTION: + return { ...state, selected: action.id }; default: return state; } } -export function updateSort(state: Store.Sort, action) { +export function updateErrors(state, action) { switch (action) { - // case Actions.UPDATE_QUERY: - // return { ...state, query: action.query }; + // case Actions.UPDATE_ERRORS: + // return { ...state }; default: return state; } } -export function updateProducts(state: Store.Product, action) { +export function updateNavigations(state: Store.Navigation, action) { switch (action) { - // case Actions.UPDATE_QUERY: - // return { ...state, query: action.query }; + // case Actions.UPDATE_NAVIGATIONS: + // return { ...state }; default: return state; } } -export function updateCollections(state: Store.Collection, action) { +export function updatePage(state: Store.Page, action) { switch (action) { - // case Actions.UPDATE_QUERY: - // return { ...state, query: action.query }; + case Actions.UPDATE_SEARCH: + return { ...state, current: 1 }; + case Actions.UPDATE_CURRENT_PAGE: + return { ...state, current: action.page }; + case Actions.UPDATE_PAGE_SIZE: + return { ...state, size: action.size }; default: return state; } } -export function updateAutocomplete(state: Store.Autocomplete, action) { +export function updateProducts(state: Store.Product, action) { switch (action) { - // case Actions.UPDATE_QUERY: - // return { ...state, query: action.query }; + // case Actions.UPDATE_PRODUCTS: + // return { ...state }; + default: + return state; + } +} + +export function updateQuery(state: Store.Query, action) { + switch (action) { + case Actions.UPDATE_SEARCH: + return { ...state, original: action.query }; default: return state; } @@ -55,17 +71,17 @@ export function updateAutocomplete(state: Store.Autocomplete, action) { export function updateRedirect(state, action) { switch (action) { - // case Actions.UPDATE_QUERY: - // return { ...state, query: action.query }; + // case Actions.UPDATE_REDIRECT: + // return { ...state }; default: return state; } } -export function updateErrors(state, action) { +export function updateSorts(state: Store.Sort, action) { switch (action) { - // case Actions.UPDATE_QUERY: - // return { ...state, query: action.query }; + case Actions.UPDATE_SORTS: + return { ...state, field: action.field, descending: !!action.descending }; default: return state; } @@ -73,8 +89,8 @@ export function updateErrors(state, action) { export function updateWarnings(state, action) { switch (action) { - // case Actions.UPDATE_QUERY: - // return { ...state, query: action.query }; + // case Actions.UPDATE_WARNINGS: + // return { ...state }; default: return state; } @@ -82,14 +98,15 @@ export function updateWarnings(state, action) { export default redux.combineReducers({ data: redux.combineReducers({ - query: updateQuery, - sort: updateSort, - products: updateProducts, + autocomplete: updateAutocomplete, collections: updateCollections, + errors: updateErrors, navigations: updateNavigations, - autocomplete: updateAutocomplete, + page: updatePage, + products: updateProducts, + query: updateQuery, redirect: updateRedirect, - errors: updateErrors, - warnings: updateWarnings - }) + sorts: updateSorts, + warnings: updateWarnings, + }), }); From 22bef407c0996085b8cd1d3240cf99373b33dc5d Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Mon, 24 Apr 2017 12:09:46 -0400 Subject: [PATCH 30/56] More reducer logic --- src/flux/reducer.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts index b8f76fe..7531713 100644 --- a/src/flux/reducer.ts +++ b/src/flux/reducer.ts @@ -29,10 +29,25 @@ export function updateErrors(state, action) { } } -export function updateNavigations(state: Store.Navigation, action) { +export function updateNavigations(state: Store.Indexed, action) { switch (action) { - // case Actions.UPDATE_NAVIGATIONS: - // return { ...state }; + case Actions.UPDATE_SEARCH: + case Actions.SELECT_REFINEMENT: + return { ...state, + byId: { + [action.navigationId]: { + selected: state.byId[action.navigationId].selected.concat(action.index), + }, + }, + }; + case Actions.DESELECT_REFINEMENT: + return { ...state, + byId: { + [action.navigationId]: { + selected: state.byId[action.navigationId].selected.filter((index) => index !== action.index), + }, + }, + }; default: return state; } From c202ad1ff5c44575224bbc1109fb8dbfd49fb12a Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Mon, 24 Apr 2017 13:50:34 -0400 Subject: [PATCH 31/56] capacitor equivalencies --- src/flux/capacitor.ts | 38 +++++------ src/flux/store.ts | 1 + test/unit/_suite.ts | 2 +- test/unit/flux/capacitor.ts | 127 +++++++++++++++++++++++++++++++++++- 4 files changed, 144 insertions(+), 24 deletions(-) diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index a17b598..66236aa 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -6,6 +6,7 @@ import { Query, QueryConfiguration } from '../core/query'; import { SelectedRangeRefinement, SelectedValueRefinement, Sort } from '../models/request'; import { Navigation, RefinementResults, Results } from '../models/response'; import Actions from './actions'; +import Observer from './observer'; import { Pager } from './pager'; import Store from './store'; @@ -51,6 +52,9 @@ export namespace Events { // redirect event export const REDIRECT = 'redirect'; + + // error events + export const ERROR_BRIDGE = 'error:bridge'; } export { Pager }; @@ -75,11 +79,13 @@ export class FluxCapacitor extends EventEmitter { bridge: BrowserBridge; results: Results; page: Pager; - private originalQuery: string = ''; + originalQuery: string = ''; constructor(endpoint: string, config: FluxConfiguration = {}, mask?: string) { super(); + this.store.subscribe(Observer.listen(this)); + const bridgeConfig: FluxBridgeConfig = config.bridge || {}; this.bridge = new BrowserBridge(endpoint, bridgeConfig.https, bridgeConfig); if (bridgeConfig.headers) { @@ -114,10 +120,11 @@ export class FluxCapacitor extends EventEmitter { // }); // } - searchV2(query: string = this.originalQuery) { + search(query: string = this.originalQuery) { this.store.dispatch(Actions.updateSearch({ query })); } + // TODO: update to store implementation refinements(navigationName: string): Promise { return this.bridge.refinements(this.query, navigationName) .then((results) => { @@ -126,9 +133,9 @@ export class FluxCapacitor extends EventEmitter { }); } - resetRecall() { - this.query = new Query().withConfiguration(this.filteredRequest); - } + // resetRecall() { + // this.query = new Query().withConfiguration(this.filteredRequest); + // } // reset(query: string = this.originalQuery): Promise { // this.resetRecall(); @@ -138,8 +145,8 @@ export class FluxCapacitor extends EventEmitter { // .then(() => query); // } - reset(query: string = null) { - this.store.dispatch(Actions.updateSearch({ query, refinements: [], clear: true })); + reset(query: string = null, refinements: any[] = []) { + this.store.dispatch(Actions.updateSearch({ query, refinements, clear: true })); } // resize(pageSize: number, resetOffset?: boolean): Promise { @@ -166,7 +173,7 @@ export class FluxCapacitor extends EventEmitter { // }); // } - sort(sort: Sort | Sort[]) { + sort(sort: Store.Sort | Store.Sort[]) { this.store.dispatch(Actions.updateSorts(sort)); } @@ -194,6 +201,7 @@ export class FluxCapacitor extends EventEmitter { this.store.dispatch(Actions.deselectRefinement(navigationName, index)); } + // TODO: update to store implementation details(id: string, navigationName: string = 'id'): Promise { return this.bridge.search(new Query() .withConfiguration(this.query.raw, '{area,collection,language,fields}') @@ -247,17 +255,3 @@ export class FluxCapacitor extends EventEmitter { // }; // } } - -export interface NavigationInfo { - available: Navigation[]; - selected: Navigation[]; -} - -export interface RefinementConfig { - reset?: boolean; - skipSearch?: boolean; -} - -export interface RewriteConfig { - skipSearch?: boolean; -} diff --git a/src/flux/store.ts b/src/flux/store.ts index be250d6..5398725 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -211,6 +211,7 @@ namespace Store { export function create() { return redux.createStore( reducer, + {}, redux.applyMiddleware(thunk), ); } diff --git a/test/unit/_suite.ts b/test/unit/_suite.ts index 63c2991..dfc530c 100644 --- a/test/unit/_suite.ts +++ b/test/unit/_suite.ts @@ -17,7 +17,7 @@ export default suite((tests) => { export interface Utils { expect: Chai.ExpectStatic; spy: Sinon.SinonSpyStatic; - stub: Sinon.SinonSpyStatic; + stub: Sinon.SinonStubStatic; } export type Suite = UtilsSuite & { diff --git a/test/unit/flux/capacitor.ts b/test/unit/flux/capacitor.ts index a5f5845..3512810 100644 --- a/test/unit/flux/capacitor.ts +++ b/test/unit/flux/capacitor.ts @@ -1,4 +1,7 @@ import * as mock from 'xhr-mock'; +import Actions from '../../../src/flux/actions'; +import Observer from '../../../src/flux/observer'; +import Store from '../../../src/flux/store'; import { Events, FluxCapacitor, Results, SelectedValueRefinement, Sort } from '../../../src/index'; import suite from '../_suite'; @@ -9,11 +12,18 @@ const SELECTED_REFINEMENT: SelectedValueRefinement = { type: 'Value', navigation const REFINEMENT_RESULT = { availableNavigation: 'a', selectedNavigation: 'b' }; const DETAILS_RESULT = { records: [{}] }; -suite('FluxCapacitor', ({ expect, spy }) => { +suite('FluxCapacitor', ({ expect, spy, stub }) => { + const LISTENER = () => null; + let create: Sinon.SinonStub; + let listen: Sinon.SinonStub; + let subscribe: Sinon.SinonSpy; let flux: FluxCapacitor; beforeEach(() => { mock.setup(); + subscribe = spy(); + create = stub(Store, 'create').returns({ subscribe }); + listen = stub(Observer, 'listen').returns(LISTENER); flux = new FluxCapacitor(CUSTOMER_ID); }); @@ -30,6 +40,12 @@ suite('FluxCapacitor', ({ expect, spy }) => { expect(flux.results).to.not.be.ok; }); + it('should set up store and observer', () => { + expect(listen).to.be.calledWith(flux); + expect(subscribe).to.be.calledWith(LISTENER); + expect(create).to.be.called; + }); + it('should accept a mask for configuration', () => { const config: any = { a: 'something', b: 'Ascending' }; @@ -99,6 +115,115 @@ suite('FluxCapacitor', ({ expect, spy }) => { }); }); + describe('actions', () => { + let dispatch: Sinon.SinonSpy; + + beforeEach(() => { + dispatch = spy(); + flux.store = { dispatch }; + }); + + describe('search()', () => { + it('should dispatch updateSearch()', () => { + const query = 'half moon'; + const updateSearch = stub(Actions, 'updateSearch'); + + flux.search(query); + + expect(updateSearch).to.be.calledWith({ query }); + }); + + it('should fallback to previous query', () => { + const query = flux.originalQuery = 'half moon'; + const updateSearch = stub(Actions, 'updateSearch'); + + flux.search(); + + expect(updateSearch).to.be.calledWith({ query }); + }); + }); + + describe('reset()', () => { + it('should dispatch updateSearch()', () => { + const query = 'half moon'; + const refinements = [{ a: 'b' }, { c: 'd' }]; + const updateSearch = stub(Actions, 'updateSearch'); + + flux.reset(query, refinements); + + expect(updateSearch).to.be.calledWith({ query, refinements, clear: true }); + }); + + it('should fallback to null query and empty refinements', () => { + const updateSearch = stub(Actions, 'updateSearch'); + + flux.reset(); + + expect(updateSearch).to.be.calledWith({ query: null, refinements: [], clear: true }); + }); + }); + + describe('resize()', () => { + it('should dispatch updatePageSize()', () => { + const updatePageSize = stub(Actions, 'updatePageSize'); + + flux.resize(24); + + expect(updatePageSize).to.be.calledWith(24); + }); + }); + + describe('sort()', () => { + it('should dispatch updateSorts()', () => { + const sort = { field: 'price', descending: true }; + const updateSorts = stub(Actions, 'updateSorts'); + + flux.sort(sort); + + expect(updateSorts).to.be.calledWith(sort); + }); + + it('should accept multiple sorts', () => { + const sorts = [{ field: 'price', descending: true }, { field: 'popularity' }]; + const updateSorts = stub(Actions, 'updateSorts'); + + flux.sort(sorts); + + expect(updateSorts).to.be.calledWith(sorts); + }); + }); + + describe('refine()', () => { + it('should dispatch selectRefinement()', () => { + const selectRefinement = stub(Actions, 'selectRefinement'); + + flux.refine('brand', 3); + + expect(selectRefinement).to.be.calledWith('brand', 3); + }); + }); + + describe('unrefine()', () => { + it('should dispatch deselectRefinement()', () => { + const deselectRefinement = stub(Actions, 'deselectRefinement'); + + flux.unrefine('brand', 3); + + expect(deselectRefinement).to.be.calledWith('brand', 3); + }); + }); + + describe('switchCollection()', () => { + it('should dispatch selectCollection()', () => { + const selectCollection = stub(Actions, 'selectCollection'); + + flux.switchCollection('products'); + + expect(selectCollection).to.be.calledWith('products'); + }); + }); + }); + // describe('search()', () => { // it('should make a search request', (done) => { // mock.post(SEARCH_URL, (req, res) => { From 44d922c2fcd1d4aeb7ec645733aecc2abd1a253c Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Mon, 24 Apr 2017 14:02:32 -0400 Subject: [PATCH 32/56] add more property to navigations --- src/flux/capacitor.ts | 14 ++------------ src/flux/store.ts | 1 + test/unit/flux/capacitor.ts | 11 +++++++++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index 66236aa..aaa6bb1 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -201,18 +201,8 @@ export class FluxCapacitor extends EventEmitter { this.store.dispatch(Actions.deselectRefinement(navigationName, index)); } - // TODO: update to store implementation - details(id: string, navigationName: string = 'id'): Promise { - return this.bridge.search(new Query() - .withConfiguration(this.query.raw, '{area,collection,language,fields}') - .withSelectedRefinements({ type: 'Value', navigationName, value: id }) - .withPageSize(1)) - .then((res) => { - if (res.records.length) { - this.emit(Events.DETAILS, res.records[0]); - } - return res; - }); + details(id: string) { + this.store.dispatch(Actions.updateDetailsId(id)); } // switchCollection(collection: string): Promise { diff --git a/src/flux/store.ts b/src/flux/store.ts index 5398725..4f0df25 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -154,6 +154,7 @@ namespace Store { field: string; // post label: string; // post or?: boolean; // post + more?: boolean; // post sort?: Sort; // post selected: number[]; // pre } diff --git a/test/unit/flux/capacitor.ts b/test/unit/flux/capacitor.ts index 3512810..35e3718 100644 --- a/test/unit/flux/capacitor.ts +++ b/test/unit/flux/capacitor.ts @@ -213,6 +213,17 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { }); }); + describe('details()', () => { + it('should dispatch updateDetailsId()', () => { + const id = '123123'; + const updateDetailsId = stub(Actions, 'updateDetailsId'); + + flux.details(id); + + expect(updateDetailsId).to.be.calledWith(id); + }); + }); + describe('switchCollection()', () => { it('should dispatch selectCollection()', () => { const selectCollection = stub(Actions, 'selectCollection'); From 393295e0294170ccf20c6df77e9eb05854a1e027 Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Mon, 24 Apr 2017 17:14:23 -0400 Subject: [PATCH 33/56] reduce --- src/flux/actions.ts | 4 ++- src/flux/reducer.ts | 87 ++++++++++++++++++++++++++++++++++++++------- src/flux/store.ts | 8 ++++- 3 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src/flux/actions.ts b/src/flux/actions.ts index e358f9a..919aa58 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -43,7 +43,9 @@ namespace Actions { export interface Search { query?: string; - refinements?: Search.Refinement[]; + // refinements?: Search.Refinement[]; + navigationId?: string; + index?: number; /** * only for refinements diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts index 7531713..8ce9ed2 100644 --- a/src/flux/reducer.ts +++ b/src/flux/reducer.ts @@ -20,6 +20,15 @@ export function updateCollections(state: Store.Indexed.Selectable, action) { + const navigationId = action.navigationId; + const refinementIndex = action.index; switch (action) { case Actions.UPDATE_SEARCH: + // TODO: add case for clear + if (action.clear) { + const byIds = state.allIds.reduce( + (newById, index) => Object.assign(newById, { [index]: { ...state.byId[index], selected: [] } }), { }, + ); + if (!(navigationId && action.index)) { + return { + ...state, + byId: byIds, + }; + } else { + return { + ...state, + byId: { + ...byIds, + [navigationId]: { + ...state.byId[refinementIndex], + // TODO: maybe check if already there + selected: refinementIndex, + }, + }, + }; + } + } case Actions.SELECT_REFINEMENT: - return { ...state, - byId: { - [action.navigationId]: { - selected: state.byId[action.navigationId].selected.concat(action.index), - }, - }, - }; + if (navigationId && refinementIndex) { + return { + ...state, + byId: { + ...state.byId, + [navigationId]: { + ...state.byId[navigationId], + // TODO: maybe check if already there + selected: state.byId[navigationId].selected.concat(refinementIndex), + }, + }, + }; + } else { + return state; + } case Actions.DESELECT_REFINEMENT: - return { ...state, + return { + ...state, byId: { - [action.navigationId]: { - selected: state.byId[action.navigationId].selected.filter((index) => index !== action.index), + ...state.byId, + [navigationId]: { + ...state.byId[navigationId], + selected: state.byId[navigationId].selected.filter((index) => index !== refinementIndex), }, }, }; @@ -56,11 +102,15 @@ export function updateNavigations(state: Store.Indexed, action export function updatePage(state: Store.Page, action) { switch (action) { case Actions.UPDATE_SEARCH: + case Actions.UPDATE_SORTS: + case Actions.SELECT_COLLECTION: + case Actions.SELECT_REFINEMENT: + case Actions.DESELECT_REFINEMENT: return { ...state, current: 1 }; case Actions.UPDATE_CURRENT_PAGE: return { ...state, current: action.page }; case Actions.UPDATE_PAGE_SIZE: - return { ...state, size: action.size }; + return { ...state, current: 1, size: action.size }; default: return state; } @@ -93,10 +143,19 @@ export function updateRedirect(state, action) { } } -export function updateSorts(state: Store.Sort, action) { +export function updateSorts(state: Store.Indexed.Selectable, action) { switch (action) { case Actions.UPDATE_SORTS: - return { ...state, field: action.field, descending: !!action.descending }; + return { ...state, selected: action.label }; + default: + return state; + } +} + +export function updateTemplate(state, action) { + switch (action) { + // case Actions.UPDATE_TEMPLATE: + // return { ...state }; default: return state; } @@ -115,6 +174,7 @@ export default redux.combineReducers({ data: redux.combineReducers({ autocomplete: updateAutocomplete, collections: updateCollections, + details: updateDetails, errors: updateErrors, navigations: updateNavigations, page: updatePage, @@ -122,6 +182,7 @@ export default redux.combineReducers({ query: updateQuery, redirect: updateRedirect, sorts: updateSorts, + template: updateTemplate, warnings: updateWarnings, }), }); diff --git a/src/flux/store.ts b/src/flux/store.ts index 4f0df25..e6a550d 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -10,7 +10,7 @@ namespace Store { data?: { query: Query; // mixed - sorts: Sort[]; // pre + sorts: Indexed.Selectable; // pre products: Product[]; // post collections: Indexed.Selectable; // mixed navigations: Indexed; // mixed @@ -66,6 +66,12 @@ namespace Store { descending?: boolean; } + export namespace Sort { + export interface Labelled extends Sort { + label: string; + } + } + export interface Page { /** * number of products per page From cd5fee0c3852aec032ecf8ae61eddb4c3ecbeee3 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Tue, 25 Apr 2017 10:25:17 -0400 Subject: [PATCH 34/56] more actions --- src/flux/actions.ts | 212 +++++++++++++++++++++++++--------- src/flux/adapters/request.ts | 5 + src/flux/adapters/response.ts | 118 +++++++++++++++++++ src/flux/capacitor.ts | 142 +++-------------------- src/flux/observer.ts | 1 + src/flux/pager.ts | 111 ++++++++---------- src/flux/reducer.ts | 72 ++++++------ src/flux/selectors.ts | 36 ++++++ src/flux/store.ts | 67 ++++++----- src/flux/utils.ts | 11 ++ src/models/request.ts | 15 ++- src/models/response.ts | 26 ++++- test/tslint.json | 3 +- test/unit/flux/actions.ts | 142 +++++++++++++---------- test/unit/flux/capacitor.ts | 28 ++--- 15 files changed, 602 insertions(+), 387 deletions(-) create mode 100644 src/flux/adapters/request.ts create mode 100644 src/flux/adapters/response.ts create mode 100644 src/flux/selectors.ts diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 919aa58..728513b 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -1,76 +1,184 @@ import { Dispatch } from 'redux'; +import { BrowserBridge } from '../core/bridge'; import { Request } from '../models/request'; +import { RefinementResults, Results } from '../models/response'; +import ResponseAdapter from './adapters/response'; +import Selectors from './selectors'; import Store from './store'; -import { rayify, thunk } from './utils'; +import { conditional, LinkMapper, rayify, thunk } from './utils'; + +class Actions { + private linkMapper: (value: string) => Store.Linkable; + + constructor(private bridge: BrowserBridge, paths: Paths) { + this.linkMapper = LinkMapper(paths.search); + } + + // request action creators + updateSearch = (search: Search) => + thunk(Actions.UPDATE_SEARCH, search) + + selectRefinement = (navigationId: string, index: number) => + conditional((state) => Selectors.isRefinementDeselected(state, navigationId, index), + Actions.SELECT_REFINEMENT, { navigationId, index }) + + deselectRefinement = (navigationId: string, index: number) => + conditional((state) => Selectors.isRefinementSelected(state, navigationId, index), + Actions.DESELECT_REFINEMENT, { navigationId, index }) + + selectCollection = (id: string) => + conditional((state) => state.data.collections.selected !== id, + Actions.SELECT_COLLECTION, { id }) + + updateSorts = (id: string) => + conditional((state) => state.data.sorts.selected !== id, + Actions.UPDATE_SORTS, { id }) + + updateAutocompleteQuery = (query: string) => + conditional((state) => state.data.autocomplete.query !== query, + Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }) + + updatePageSize = (size: number) => + conditional((state) => state.data.page.size !== size, + Actions.UPDATE_PAGE_SIZE, { size }) + + updateCurrentPage = (page: number) => + conditional((state) => state.data.page.current !== page, + Actions.UPDATE_CURRENT_PAGE, { page }) + + updateDetailsId = (id: string) => + thunk(Actions.UPDATE_DETAILS_ID, { id }) + + fetchMoreRefinements = (navigationId: string) => + (dispatch: Dispatch, getStore: () => Store.State) => { + const state = getStore(); + if (Selectors.hasMoreRefinements(state, navigationId)) { + this.bridge.refinements(Selectors.searchRequest(state), navigationId) + .then(({ navigation: { name, refinements } }) => + dispatch(this.addMoreRefinements(name, refinements.map(ResponseAdapter.extractRefinement)))); + } + } + + // response action creators + addMoreRefinements = (navigationId: string, refinements: any) => + thunk(Actions.ADD_MORE_REFINEMENTS, { navigationId, refinements }) + + updateSearchResponse = (results: Results) => + (dispatch: Dispatch, getStore: () => Store.State) => { + const state = getStore(); + dispatch(this.updateQuery(ResponseAdapter.extractQuery(results, this.linkMapper))); + dispatch(this.updateProducts(results.records.map((product) => product.allMeta))); + dispatch(this.updateCollectionCount(state.data.collections.selected, results.totalRecordCount)); + // tslint:disable-next-line max-line-length + dispatch(this.updateNavigations(ResponseAdapter.combineNavigations(results.availableNavigation, results.selectedNavigation))); + dispatch(this.updatePage(ResponseAdapter.extractPage(state, results))); + dispatch(this.updateTemplate(ResponseAdapter.extractTemplate(results.template))); + dispatch(this.updateRedirect(results.redirect)); + } + + updateQuery = (query: Query) => + thunk(Actions.UPDATE_QUERY, query) + + updateProducts = (products: Store.Product[]) => + thunk(Actions.UPDATE_PRODUCTS, { products }) + + updateCollectionCount = (collection: string, count: number) => + thunk(Actions.UPDATE_COLLECTION_COUNT, { collection, count }) + + updateNavigations = (navigations: Store.Navigation[]) => + thunk(Actions.UPDATE_NAVIGATIONS, { navigations }) + + updatePage = (page: Page) => + thunk(Actions.UPDATE_PAGE, page) + + updateTemplate = (template: Store.Template) => + thunk(Actions.UPDATE_TEMPLATE, { template }) + + updateRedirect = (redirect: string) => + thunk(Actions.UPDATE_REDIRECT, { redirect }) + + updateAutocompleteSuggestions = (suggestions: string[], category: Store.Autocomplete.Category) => + thunk(Actions.UPDATE_AUTOCOMPLETE_SUGGESTIONS, { suggestions, category }) + + updateDetailsProduct = (product: Store.Product) => + thunk(Actions.UPDATE_DETAILS_PRODUCT, { product }) +} namespace Actions { + // request actions + export const UPDATE_AUTOCOMPLETE_QUERY = 'UPDATE_AUTOCOMPLETE_QUERY'; + export const UPDATE_DETAILS_ID = 'UPDATE_DETAILS_ID'; export const UPDATE_SEARCH = 'UPDATE_SEARCH'; export const SELECT_REFINEMENT = 'SELECT_REFINEMENT'; export const DESELECT_REFINEMENT = 'DESELECT_REFINEMENT'; export const SELECT_COLLECTION = 'SELECT_COLLECTION'; export const UPDATE_SORTS = 'UPDATE_SORTS'; - export const UPDATE_AUTOCOMPLETE_QUERY = 'UPDATE_AUTOCOMPLETE_QUERY'; export const UPDATE_PAGE_SIZE = 'UPDATE_PAGE_SIZE'; export const UPDATE_CURRENT_PAGE = 'UPDATE_CURRENT_PAGE'; - export const UPDATE_DETAILS_ID = 'UPDATE_DETAILS_ID'; - - export const updateSearch = (search: Actions.Search) => - thunk(UPDATE_SEARCH, search); - - export const selectRefinement = (navigationId: string, index: number) => - thunk(SELECT_REFINEMENT, { navigationId, index }); - - export const deselectRefinement = (navigationId: string, index: number) => - thunk(DESELECT_REFINEMENT, { navigationId, index }); - - export const selectCollection = (id: string) => - thunk(SELECT_COLLECTION, { id }); - export const updateSorts = (sorts: Store.Sort | Store.Sort[]) => - thunk(UPDATE_SORTS, { sorts: rayify(sorts) }); - - export const updateAutocompleteQuery = (query: string) => - thunk(UPDATE_AUTOCOMPLETE_QUERY, { query }); + // response actions + export const ADD_MORE_REFINEMENTS = 'ADD_MORE_REFINEMENTS'; + export const UPDATE_AUTOCOMPLETE_SUGGESTIONS = 'UPDATE_AUTOCOMPLETE_SUGGESTIONS'; + export const UPDATE_DETAILS_PRODUCT = 'UPDATE_DETAILS_PRODUCT'; + export const UPDATE_QUERY = 'UPDATE_AUTOCOMPLETE_SUGGESTIONS'; + export const UPDATE_PRODUCTS = 'UPDATE_PRODUCTS'; + export const UPDATE_COLLECTION_COUNT = 'UPDATE_COLLECTION_COUNT'; + export const UPDATE_NAVIGATIONS = 'UPDATE_NAVIGATIONS'; + export const UPDATE_PAGE = 'UPDATE_PAGE'; + export const UPDATE_TEMPLATE = 'UPDATE_TEMPLATE'; + export const UPDATE_REDIRECT = 'UPDATE_TEMPLATE'; +} - export const updatePageSize = (size: number) => - thunk(UPDATE_PAGE_SIZE, { size }); +export default Actions; - export const updateCurrentPage = (page: number) => - thunk(UPDATE_CURRENT_PAGE, { page }); +export interface Query { + corrected?: string; + related: Store.Query.Related[]; + didYouMean: Store.Query.DidYouMean[]; + rewrites: string[]; +} - export const updateDetailsId = (id: string) => - thunk(UPDATE_DETAILS_ID, { id }); +export interface Search { + query?: string; + navigationId?: string; + index?: number; + + /** + * only for refinements + * if true, replace refinements with the provided ones + * if false, add the provided refinements + */ + clear?: boolean; +} - export interface Search { - query?: string; - // refinements?: Search.Refinement[]; - navigationId?: string; - index?: number; +export namespace Search { + export type Refinement = ValueRefinement | RangeRefinement; - /** - * only for refinements - * if true, replace refinements with the provided ones - * if false, add the provided refinements - */ - clear?: boolean; + export interface BaseRefinement { + field: string; } - export namespace Search { - export type Refinement = ValueRefinement | RangeRefinement; - - export interface BaseRefinement { - field: string; - } - - export interface ValueRefinement extends BaseRefinement { - value: string; - } + export interface ValueRefinement extends BaseRefinement { + value: string; + } - export interface RangeRefinement extends BaseRefinement { - low?: number; - high?: number; - } + export interface RangeRefinement extends BaseRefinement { + low?: number; + high?: number; } } -export default Actions; +export interface Page { + previous: number; + next: number; + last: number; + from: number; + to: number; + total: number; + range: number[]; +} + +export interface Paths { + search: string; + // details: string; +} diff --git a/src/flux/adapters/request.ts b/src/flux/adapters/request.ts new file mode 100644 index 0000000..fd740e2 --- /dev/null +++ b/src/flux/adapters/request.ts @@ -0,0 +1,5 @@ +namespace Request { + +} + +export default Request; diff --git a/src/flux/adapters/response.ts b/src/flux/adapters/response.ts new file mode 100644 index 0000000..1726e33 --- /dev/null +++ b/src/flux/adapters/response.ts @@ -0,0 +1,118 @@ +import { + Navigation, + PageInfo, + RangeRefinement, + Results, + SortType, + Template, + ValueRefinement, + Zone, +} from '../../models/response'; +import { Page, Query } from '../actions'; +import { Pager } from '../pager'; +import Store from '../store'; + +namespace Response { + + export const extractQuery = (results: Results, linkMapper: (value: string) => Store.Linkable): Query => ({ + corrected: results.correctedQuery, + didYouMean: results.didYouMean.map(linkMapper), + related: results.relatedQueries.map(linkMapper), + rewrites: results.rewrites, + }); + + export const extractRefinement = ({ type, value, low, high, count: total }: RangeRefinement & ValueRefinement): + Store.ValueRefinement | Store.RangeRefinement => + type === 'Value' ? { value, total } : { low, high, total }; + + export const extractNavigationSort = (sort: SortType): Store.Sort => { + switch (sort) { + case 'Count_Ascending': return { field: 'count' }; + case 'Count_Descending': return { field: 'count', descending: true }; + case 'Value_Ascending': return { field: 'value' }; + case 'Value_Descending': return { field: 'value', descending: true }; + } + }; + + export const extractNavigation = (navigation: Navigation): Store.Navigation => ({ + field: navigation.name, + label: navigation.displayName, + more: navigation.moreRefinements, + or: navigation.or, + range: !!navigation.range, + refinements: navigation.refinements.map(Response.extractRefinement), + selected: [], + sort: navigation.sort && Response.extractNavigationSort(navigation.sort), + }); + + // tslint:disable-next-line max-line-length + export const refinementsMatch = (lhs: Store.RangeRefinement & Store.ValueRefinement, rhs: RangeRefinement & ValueRefinement) => { + if (rhs.type === 'Value') { + return lhs.value === rhs.value; + } else { + return lhs.low === rhs.low && lhs.high === rhs.high; + } + }; + + export const appendSelectedRefinements = (available: Store.Navigation, selected: Navigation) => { + available.selected = selected.refinements.reduce((indices, refinement) => { + // tslint:disable-next-line max-line-length + const index = (available.refinements.findIndex)((availableRef) => refinementsMatch(availableRef, refinement)); + if (index > -1) { + indices.push(index); + } + return indices; + }, []); + }; + + export const combineNavigations = (available: Navigation[], selected: Navigation[]): Store.Navigation[] => { + const navigations = available.reduce((map, navigation) => + Object.assign(map, { [navigation.name]: Response.extractNavigation(navigation) }), {}); + + selected.forEach((selectedNav) => { + const availableNav = navigations[selectedNav.name]; + + if (availableNav) { + Response.appendSelectedRefinements(availableNav, selectedNav); + } else { + const navigation = Response.extractNavigation(selectedNav); + navigation.selected = Object.keys(Array(selectedNav.refinements.length)); + navigations[selectedNav.name] = navigation; + } + }); + + return Object.keys(navigations).reduce((navs, key) => navs.concat(navigations[key]), []); + }; + + export const extractZone = (zone: Zone): Store.Zone => { + switch (zone.type) { + case 'Content': return { + content: zone.content, + name: zone.name, + type: Store.Zone.Type.CONTENT, + }; + case 'Rich_Content': return { + content: zone.content, + name: zone.name, + type: Store.Zone.Type.RICH_CONTENT, + }; + case 'Records': return { + name: zone.name, + products: zone.records.map((record) => record.allMeta), + type: Store.Zone.Type.RECORD, + }; + } + }; + + export const extractTemplate = (template: Template): Store.Template => ({ + name: template.name, + rule: template.ruleName, + zones: Object.keys(template.zones).reduce((zones, key) => + Object.assign(zones, { [key]: Response.extractZone(template.zones[key]) }), {}), + }); + + export const extractPage = (store: Store.State, results: Results): Page => + new Pager(store, results).build(); +} + +export default Response; diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index aaa6bb1..3d1b6c3 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -3,9 +3,9 @@ import * as redux from 'redux'; import filterObject = require('filter-object'); import { BrowserBridge } from '../core/bridge'; import { Query, QueryConfiguration } from '../core/query'; -import { SelectedRangeRefinement, SelectedValueRefinement, Sort } from '../models/request'; +import { RangeRefinement, Sort, ValueRefinement } from '../models/request'; import { Navigation, RefinementResults, Results } from '../models/response'; -import Actions from './actions'; +import ActionPack from './actions'; import Observer from './observer'; import { Pager } from './pager'; import Store from './store'; @@ -47,6 +47,7 @@ export namespace Events { // page events export const PAGE_UPDATED = 'page_updated'; // post + export const PAGE_TOTAL_UPDATED = 'page_total_updated'; // post export const PAGE_SIZE_UPDATED = 'page_size_updated'; // pre export const CURRENT_PAGE_UPDATED = 'current_page_updated'; // pre @@ -58,7 +59,7 @@ export namespace Events { } export { Pager }; -export type FluxRefinement = SelectedValueRefinement | SelectedRangeRefinement; +export type FluxRefinement = ValueRefinement | RangeRefinement; export interface FluxConfiguration extends QueryConfiguration { bridge?: FluxBridgeConfig; @@ -74,11 +75,11 @@ export interface FluxBridgeConfig { export class FluxCapacitor extends EventEmitter { store: redux.Store = Store.create(); + actions: ActionPack; query: Query; bridge: BrowserBridge; results: Results; - page: Pager; originalQuery: string = ''; constructor(endpoint: string, config: FluxConfiguration = {}, mask?: string) { @@ -97,151 +98,44 @@ export class FluxCapacitor extends EventEmitter { bridgeConfig.errorHandler(err); } }; + this.actions = new ActionPack(this.bridge, { search: '/search' }); this.query = new Query().withConfiguration(filterObject(config, ['*', '!{bridge}']), mask); - this.page = new Pager(this); } - // search(originalQuery: string = this.originalQuery): Promise { - // this.query.withQuery(originalQuery); - // this.emit(Events.SEARCH, this.query.raw); - // return this.bridge.search(this.query) - // .then((results) => { - // const oldQuery = this.originalQuery; - // Object.assign(this, { results, originalQuery }); - // - // if (results.redirect) { - // this.emit(Events.REDIRECT, results.redirect); - // } - // this.emit(Events.RESULTS, results); - // this.emitQueryChanged(oldQuery, originalQuery); - // - // return results; - // }); - // } - search(query: string = this.originalQuery) { - this.store.dispatch(Actions.updateSearch({ query })); + this.store.dispatch(this.actions.updateSearch({ query })); } - // TODO: update to store implementation - refinements(navigationName: string): Promise { - return this.bridge.refinements(this.query, navigationName) - .then((results) => { - this.emit(Events.REFINEMENT_RESULTS, results); - return results; - }); + refinements(navigationName: string) { + this.store.dispatch(this.actions.fetchMoreRefinements(navigationName)); } - // resetRecall() { - // this.query = new Query().withConfiguration(this.filteredRequest); - // } - - // reset(query: string = this.originalQuery): Promise { - // this.resetRecall(); - // this.emit(Events.PAGE_CHANGED, { pageNumber: 1 }); - // return this.search(query) - // .then((res) => this.emit(Events.RESET, res)) - // .then(() => query); - // } - - reset(query: string = null, refinements: any[] = []) { - this.store.dispatch(Actions.updateSearch({ query, refinements, clear: true })); + reset(query: string = null, { field: navigationId, index }: { field: string, index: number } = {}) { + this.store.dispatch(this.actions.updateSearch({ query, navigationId, index, clear: true })); } - // resize(pageSize: number, resetOffset?: boolean): Promise { - // this.query.withPageSize(pageSize); - // if (resetOffset) { - // return this.page.switchPage(1); - // } else { - // const total = this.page.restrictTotalRecords(this.page.fromResult, pageSize); - // const page = this.page.getPage(total); - // return this.page.switchPage(page); - // } - // } - resize(pageSize: number) { - this.store.dispatch(Actions.updatePageSize(pageSize)); + this.store.dispatch(this.actions.updatePageSize(pageSize)); } - // sort(sort: Sort, clearSorts: Sort[] = [sort]): Promise { - // this.query.withoutSorts(...clearSorts).withSorts(sort); - // return this.page.reset() - // .then((res) => { - // this.emit(Events.SORT, this.query.raw.sort); - // return res; - // }); - // } - - sort(sort: Store.Sort | Store.Sort[]) { - this.store.dispatch(Actions.updateSorts(sort)); + sort(label: string) { + this.store.dispatch(this.actions.updateSorts(label)); } - // refine(refinement: FluxRefinement, config: RefinementConfig = { reset: true }): Promise { - // this.query.withSelectedRefinements(refinement); - // if (config.skipSearch) { - // return Promise.resolve(this.navigationInfo); - // } - // return this.doRefinement(config); - // } - refine(navigationName: string, index: number) { - this.store.dispatch(Actions.selectRefinement(navigationName, index)); + this.store.dispatch(this.actions.selectRefinement(navigationName, index)); } - // unrefine(refinement: FluxRefinement, config: RefinementConfig = { reset: true }): Promise { - // this.query.withoutSelectedRefinements(refinement); - // if (config.skipSearch) { - // return Promise.resolve(this.navigationInfo); - // } - // return this.doRefinement(config); - // } - unrefine(navigationName: string, index: number) { - this.store.dispatch(Actions.deselectRefinement(navigationName, index)); + this.store.dispatch(this.actions.deselectRefinement(navigationName, index)); } details(id: string) { - this.store.dispatch(Actions.updateDetailsId(id)); + this.store.dispatch(this.actions.updateDetailsId(id)); } - // switchCollection(collection: string): Promise { - // this.query.withConfiguration({ collection, refinements: [], sort: [], skip: 0 }); - // return this.search() - // .then((res) => { - // this.emit(Events.COLLECTION_CHANGED, collection); - // return res; - // }); - // } - switchCollection(collection: string) { - this.store.dispatch(Actions.selectCollection(collection)); + this.store.dispatch(this.actions.selectCollection(collection)); } - - // private emitQueryChanged(oldQuery: string, newQuery: string) { - // if (oldQuery.toLowerCase() !== newQuery.toLowerCase()) { - // this.emit(Events.QUERY_CHANGED, newQuery); - // } - // } - // - // private get filteredRequest() { - // return filterObject(this.query.raw, '!{query,refinements,skip}'); - // } - // - // private resetPaging(reset: boolean): Promise { - // return reset ? this.page.reset() : this.search(); - // } - // - // private doRefinement({ reset }: RefinementConfig): Promise { - // return this.resetPaging(reset) - // .then(() => this.emit(Events.REFINEMENTS_CHANGED, this.navigationInfo)) - // .then(() => this.navigationInfo); - // } - // - // private get navigationInfo(): NavigationInfo { - // return { - // available: this.results.availableNavigation, - // selected: this.results.selectedNavigation, - // }; - // } } diff --git a/src/flux/observer.ts b/src/flux/observer.ts index 6a2492e..77d3423 100644 --- a/src/flux/observer.ts +++ b/src/flux/observer.ts @@ -81,6 +81,7 @@ namespace Observer { page: Object.assign(emit(Events.PAGE_UPDATED), { current: emit(Events.CURRENT_PAGE_UPDATED), size: emit(Events.PAGE_SIZE_UPDATED), + total: emit(Events.PAGE_TOTAL_UPDATED), }), products: emit(Events.PRODUCTS_UPDATED), diff --git a/src/flux/pager.ts b/src/flux/pager.ts index f45e3bb..d72413e 100644 --- a/src/flux/pager.ts +++ b/src/flux/pager.ts @@ -1,83 +1,74 @@ import { Results } from '../models/response'; +import { Page } from './actions'; import { Events, FluxCapacitor } from './capacitor'; +import Store from './store'; import range = require('lodash.range'); const MAX_RECORDS = 10000; export class Pager { - constructor(private flux: FluxCapacitor) { } + constructor(private state: Store.State, private results: Results) { } - next(): Promise { - return this.switchPage(this.nextPage); + previousPage(currentPage: number) { + return currentPage > 1 ? currentPage - 1 : null; } - prev(): Promise { - return this.switchPage(this.previousPage); + nextPage(currentPage: number, finalPage: number) { + return (currentPage + 1 <= finalPage) ? currentPage + 1 : null; } - last(): Promise { - return this.switchPage(this.finalPage); + finalPage(pageSize: number, totalRecords: number) { + return Math.max(this.getPage(this.restrictTotalRecords(totalRecords, pageSize), pageSize), 1); } - reset(): Promise { - return this.switchPage(this.firstPage); + fromResult(currentPage: number, pageSize: number) { + return currentPage * pageSize + 1; + // TODO move the default value into reducer setup + // return this.flux.query.build().skip + 1 || 1; } - get currentPage(): number { - return this.getPage(this.fromResult); - } - - get previousPage(): number | null { - return (this.currentPage - 1 >= this.firstPage) ? this.currentPage - 1 : null; - } - - get nextPage(): number | null { - return (this.currentPage + 1 <= this.finalPage) ? this.currentPage + 1 : null; - } - - get firstPage(): number { - return 1; - } - - get finalPage(): number { - return Math.max(this.getPage(this.restrictTotalRecords(this.totalRecords, this.pageSize)), 1); - } - - get fromResult(): number { - return this.flux.query.build().skip + 1 || 1; - } - - get toResult(): number { - if ((this.currentPage * this.pageSize) > this.totalRecords) { - return ((this.currentPage - 1) * this.pageSize) + (this.totalRecords % this.currentPage); + toResult(currentPage: number, pageSize: number, totalRecords: number) { + if ((currentPage * pageSize) > totalRecords) { + return ((currentPage - 1) * pageSize) + (totalRecords % currentPage); } else { - return this.currentPage * this.pageSize; + return currentPage * pageSize; } } get totalRecords(): number { - return this.flux.results ? this.flux.results.totalRecordCount : 0; + return this.results.totalRecordCount; } - pageExists(page: number): boolean { - return page <= this.finalPage && page >= this.firstPage; + get pageSize(): number { + // TODO move this default into the reducer setup + return this.state.data.page.size || 10; } - pageNumbers(limit: number = 5): number[] { - return range(1, Math.min(this.finalPage + 1, limit + 1)) - .map(this.transformPages(limit)); + get currentPage(): number { + return this.state.data.page.current; + } + + build(): Page { + const pageSize = this.pageSize; + const currentPage = this.state.data.page.current; + const totalRecords = this.totalRecords; + const finalPage = this.finalPage(pageSize, totalRecords); + + return { + from: this.fromResult(currentPage, pageSize), + last: finalPage, + next: this.nextPage(currentPage, finalPage), + previous: this.previousPage(currentPage), + range: this.pageNumbers(currentPage, finalPage, this.state.data.page.limit), + to: this.toResult(currentPage, pageSize, totalRecords), + total: this.totalRecords, + }; } - switchPage(page: number): Promise { - if (this.pageExists(page)) { - const skip = (page - 1) * this.pageSize; - this.flux.query.skip(skip); - this.flux.emit(Events.PAGE_CHANGED, { pageNumber: page }); - return this.flux.search(); - } else { - return Promise.reject(new Error(`page ${page} does not exist`)); - } + pageNumbers(currentPage: number, finalPage: number, limit: number): number[] { + return range(1, Math.min(finalPage + 1, limit + 1)) + .map(this.transformPages(currentPage, finalPage, limit)); } restrictTotalRecords(total: number, pageSize: number): number { @@ -94,28 +85,24 @@ export class Pager { } } - getPage(record: number): number { + getPage(record: number, pageSize: number): number { return Math.ceil(record / this.pageSize); } - private transformPages(limit: number): (value: number) => number { + transformPages(currentPage: number, finalPage: number, limit: number): (value: number) => number { const border = Math.ceil(limit / 2); return (value: number): number => { // account for 0-indexed pages - if (this.currentPage <= border || limit > this.finalPage) { + if (currentPage <= border || limit > finalPage) { // pages start at beginning return value; - } else if (this.currentPage > this.finalPage - border) { + } else if (currentPage > finalPage - border) { // pages start and end in the middle - return value + this.finalPage - limit; + return value + finalPage - limit; } else { // pages end at last page - return value + this.currentPage - border; + return value + currentPage - border; } }; } - - private get pageSize(): number { - return this.flux.query.build().pageSize || 10; - } } diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts index 8ce9ed2..4dc1d77 100644 --- a/src/flux/reducer.ts +++ b/src/flux/reducer.ts @@ -48,54 +48,54 @@ export function updateNavigations(state: Store.Indexed, action const byIds = state.allIds.reduce( (newById, index) => Object.assign(newById, { [index]: { ...state.byId[index], selected: [] } }), { }, ); - if (!(navigationId && action.index)) { - return { + if (!(navigationId && action.index)) { + return { ...state, - byId: byIds, - }; - } else { - return { + byId: byIds, + }; + } else { + return { ...state, - byId: { + byId: { ...byIds, - [navigationId]: { + [navigationId]: { ...state.byId[refinementIndex], - // TODO: maybe check if already there - selected: refinementIndex, - }, - }, - }; - } - } + // TODO: maybe check if already there + selected: refinementIndex, + }, + }, + }; + } +} case Actions.SELECT_REFINEMENT: - if (navigationId && refinementIndex) { - return { +if (navigationId && refinementIndex) { + return { ...state, - byId: { + byId: { ...state.byId, - [navigationId]: { + [navigationId]: { ...state.byId[navigationId], - // TODO: maybe check if already there - selected: state.byId[navigationId].selected.concat(refinementIndex), - }, - }, - }; - } else { - return state; - } + // TODO: maybe check if already there + selected: state.byId[navigationId].selected.concat(refinementIndex), + }, + }, + }; +} else { + return state; +} case Actions.DESELECT_REFINEMENT: - return { +return { ...state, - byId: { + byId: { ...state.byId, - [navigationId]: { + [navigationId]: { ...state.byId[navigationId], - selected: state.byId[navigationId].selected.filter((index) => index !== refinementIndex), - }, - }, - }; + selected: state.byId[navigationId].selected.filter((index) => index !== refinementIndex), + }, + }, +}; default: - return state; +return state; } } @@ -146,7 +146,7 @@ export function updateRedirect(state, action) { export function updateSorts(state: Store.Indexed.Selectable, action) { switch (action) { case Actions.UPDATE_SORTS: - return { ...state, selected: action.label }; + return { ...state, selected: action.id }; default: return state; } diff --git a/src/flux/selectors.ts b/src/flux/selectors.ts new file mode 100644 index 0000000..78de07c --- /dev/null +++ b/src/flux/selectors.ts @@ -0,0 +1,36 @@ +import { Request } from '../models/request'; +import Store from './store'; + +namespace Selectors { + + export const searchRequest = (store: Store.State): Request => ({ + query: store.data.query.original, + refinements: store.data.navigations.allIds.map((id) => store.data.navigations.byId[id]) + .reduce((allRefinements, navigation) => + (navigation.refinements).reduce((refinements, { field, type, low, high, value }) => + refinements.concat(navigation.range + ? { navigationName: field, high, low, type: 'Range' } + : { navigationName: field, type: 'Value', value }), []), []), + // sort: store.data.sorts.allIds.map((id) => store.data.sorts.byId[id]), + }); + + export const navigation = (state: Store.State, navigationId: string) => + state.data.navigations.byId[navigationId]; + + export const isRefinementDeselected = (state: Store.State, navigationId: string, index: number) => { + const nav = navigation(state, navigationId); + return nav && !nav.selected.includes(index); + }; + + export const isRefinementSelected = (state: Store.State, navigationId: string, index: number) => { + const nav = navigation(state, navigationId); + return nav && nav.selected.includes(index); + }; + + export const hasMoreRefinements = (state: Store.State, navigationId: string) => { + const nav = navigation(state, navigationId); + return nav && nav.more; + }; +} + +export default Selectors; diff --git a/src/flux/store.ts b/src/flux/store.ts index e6a550d..75fbac9 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -44,15 +44,8 @@ namespace Store { } export namespace Query { - export interface Related { - value: string; // post - url: string; // post (generated) - } - - export interface DidYouMean { - value: string; // post - url: string; // post (generated) - } + export type Related = Linkable; + export type DidYouMean = Linkable; } export interface Collection { @@ -83,6 +76,15 @@ namespace Store { */ current: number; // pre + /** + * number of first page + */ + first: 1; // static + /** + * maximum number of page numbers to display + */ + limit: number; // static + /** * number of next page */ @@ -91,10 +93,6 @@ namespace Store { * number of previous page */ next: number; // post - /** - * number of first page - */ - first: 1; // static /** * number of last page */ @@ -109,6 +107,11 @@ namespace Store { */ to: number; // post + /** + * the total number of products returned by this search + */ + total: number; // post + /** * displayed number range (in ) */ @@ -125,8 +128,19 @@ namespace Store { export type Zone = ContentZone | RichContentZone | RecordZone; + export namespace Zone { + export type Type = 'content' | 'rich_content' | 'record'; + + export namespace Type { + export const CONTENT = 'content'; + export const RICH_CONTENT = 'rich_content'; + export const RECORD = 'record'; + } + } + export interface BaseZone { name: string; + type: Zone.Type; } export interface ContentZone extends BaseZone { @@ -154,25 +168,15 @@ namespace Store { [key: string]: any; // post } - export type Navigation = ValueNavigation | RangeNavigation; - - export interface BaseNavigation { + export interface Navigation { field: string; // post label: string; // post - or?: boolean; // post more?: boolean; // post - sort?: Sort; // post + range?: boolean; // post + or?: boolean; // post selected: number[]; // pre - } - - export interface ValueNavigation extends BaseNavigation { - range?: false; // post - refinements: ValueRefinement[]; // post - } - - export interface RangeNavigation extends BaseNavigation { - range: true; // post - refinements: RangeRefinement[]; // post + refinements: Array; // post + sort?: Sort; // post } export interface BaseRefinement { @@ -193,7 +197,7 @@ namespace Store { export interface Autocomplete { query: string; // pre suggestions: string[]; // post - categories: Autocomplete.Category[]; // static & post + category: Autocomplete.Category; // static & post products: Product[]; // post } @@ -215,6 +219,11 @@ namespace Store { } } + export interface Linkable { + value: string; // post + url: string; // post (generated) + } + export function create() { return redux.createStore( reducer, diff --git a/src/flux/utils.ts b/src/flux/utils.ts index 8e80644..2f19181 100644 --- a/src/flux/utils.ts +++ b/src/flux/utils.ts @@ -1,3 +1,14 @@ +import Store from './store'; + export const thunk = (type: string, data: any) => (dispatch) => dispatch({ type, ...data }); +export const conditional = (predicate: (state: Store.State) => boolean, type: string, data: any) => + (dispatch, getStore) => { + if (predicate(getStore())) { + dispatch({ type, ...data }); + } +}; + export const rayify = (arr: T | T[]): T[] => Array.isArray(arr) ? arr : [arr]; + +export const LinkMapper = (baseUrl: string) => (value: string) => ({ value, url: `${baseUrl}/${value}` }); diff --git a/src/models/request.ts b/src/models/request.ts index eb3a972..ca24c7a 100644 --- a/src/models/request.ts +++ b/src/models/request.ts @@ -1,11 +1,11 @@ -import { RangeRefinement, Refinement, ValueRefinement } from './response'; +import { RefinementType } from './response'; export type SortOrder = 'Ascending' | 'Descending'; export interface Request { // query parameters query?: string; - refinements?: SelectedRefinement[]; + refinements?: Refinement[]; // query configuration fields?: string[]; @@ -46,14 +46,19 @@ export interface CustomUrlParam { value: string; } -export interface SelectedRefinement extends Refinement { +export interface Refinement { + type: RefinementType; navigationName: string; + exclude?: boolean; } -export interface SelectedRangeRefinement extends SelectedRefinement, RangeRefinement { +export interface RangeRefinement extends Refinement { + low?: number; + high?: number; } -export interface SelectedValueRefinement extends SelectedRefinement, ValueRefinement { +export interface ValueRefinement extends Refinement { + value: string; } export interface RestrictNavigation { diff --git a/src/models/response.ts b/src/models/response.ts index bcc900c..8a425dc 100644 --- a/src/models/response.ts +++ b/src/models/response.ts @@ -37,6 +37,27 @@ export interface Template { zones: any; } +export type Zone = ContentZone | RichContentZone | RecordZone; + +export interface BaseZone { + name: string; +} + +export interface ContentZone extends BaseZone { + type: 'Content'; + content: string; +} + +export interface RichContentZone extends BaseZone { + type: 'Rich_Content'; + content: string; +} + +export interface RecordZone extends BaseZone { + type: 'Records'; + records: Record[]; +} + export interface PageInfo { recordStart: number; recordEnd: number; @@ -53,6 +74,7 @@ export interface Record { id: string; url: string; title: string; + collection: string; snippet?: string; allMeta: any; } @@ -63,14 +85,16 @@ export interface Navigation { type: RefinementType; range?: boolean; or?: boolean; + moreRefinements?: boolean; ignored?: boolean; sort?: SortType; - refinements: Array; + refinements: Array; metadata: any[]; } export interface Refinement { exclude?: boolean; + count: number; type: RefinementType; } diff --git a/test/tslint.json b/test/tslint.json index 293e06c..64f25d6 100644 --- a/test/tslint.json +++ b/test/tslint.json @@ -1,6 +1,7 @@ { "extends": "../tslint.json", "rules": { - "no-unused-expression": [false] + "no-unused-expression": false, + "no-string-literal": false } } diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts index 7363046..6b71255 100644 --- a/test/unit/flux/actions.ts +++ b/test/unit/flux/actions.ts @@ -2,105 +2,119 @@ import Actions from '../../../src/flux/actions'; import * as utils from '../../../src/flux/utils'; import suite from '../_suite'; -suite('actions', ({ expect, spy, stub }) => { - describe('updateSearch()', () => { - it('should create an UPDATE_SEARCH action', () => { - const data: any = { a: 'b' }; - const thunk = stub(utils, 'thunk'); +suite('Actions', ({ expect, spy, stub }) => { + let actions: Actions; + const bridge: any = { a: 'b' }; - Actions.updateSearch(data); + beforeEach(() => actions = new Actions(bridge, { search: '/search' })); - expect(thunk).to.be.calledWith(Actions.UPDATE_SEARCH, data); + describe('constructor()', () => { + it('should set properties', () => { + expect(actions['bridge']).to.be.a('function'); + expect(actions['linkMapper']).to.be.a('function'); }); }); - describe('selectRefinement()', () => { - it('should create a SELECT_REFINEMENT action', () => { - const navigationId = 'brand'; - const index = 3; - const thunk = stub(utils, 'thunk'); + describe('request action creators', () => { + describe('updateSearch()', () => { + it('should create an UPDATE_SEARCH action', () => { + const data: any = { a: 'b' }; + const thunk = stub(utils, 'thunk'); - Actions.selectRefinement(navigationId, index); + actions.updateSearch(data); - expect(thunk).to.be.calledWith(Actions.SELECT_REFINEMENT, { navigationId, index }); + expect(thunk).to.be.calledWith(Actions.UPDATE_SEARCH, data); + }); }); - }); - describe('deselectRefinement()', () => { - it('should create a DESELECT_REFINEMENT action', () => { - const navigationId = 'brand'; - const index = 3; - const thunk = stub(utils, 'thunk'); + describe('selectRefinement()', () => { + it('should create a SELECT_REFINEMENT action', () => { + const navigationId = 'brand'; + const index = 3; + const thunk = stub(utils, 'thunk'); - Actions.deselectRefinement(navigationId, index); + actions.selectRefinement(navigationId, index); - expect(thunk).to.be.calledWith(Actions.DESELECT_REFINEMENT, { navigationId, index }); + expect(thunk).to.be.calledWith(Actions.SELECT_REFINEMENT, { navigationId, index }); + }); }); - }); - describe('selectCollection()', () => { - it('should create a SELECT_COLLECTION action', () => { - const id = 'products'; - const thunk = stub(utils, 'thunk'); + describe('deselectRefinement()', () => { + it('should create a DESELECT_REFINEMENT action', () => { + const navigationId = 'brand'; + const index = 3; + const thunk = stub(utils, 'thunk'); - Actions.selectCollection(id); + actions.deselectRefinement(navigationId, index); - expect(thunk).to.be.calledWith(Actions.SELECT_COLLECTION, { id }); + expect(thunk).to.be.calledWith(Actions.DESELECT_REFINEMENT, { navigationId, index }); + }); }); - }); - describe('updateAutocompleteQuery()', () => { - it('should create an UPDATE_AUTOCOMPLETE_QUERY action', () => { - const query = 'William Shake'; - const thunk = stub(utils, 'thunk'); + describe('selectCollection()', () => { + it('should create a SELECT_COLLECTION action', () => { + const id = 'products'; + const thunk = stub(utils, 'thunk'); - Actions.updateAutocompleteQuery(query); + actions.selectCollection(id); - expect(thunk).to.be.calledWith(Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }); + expect(thunk).to.be.calledWith(Actions.SELECT_COLLECTION, { id }); + }); }); - }); - describe('updateSorts()', () => { - it('should create a UPDATE_SORTS action', () => { - const sort = { field: 'price' }; - const thunk = stub(utils, 'thunk'); + describe('updateAutocompleteQuery()', () => { + it('should create an UPDATE_AUTOCOMPLETE_QUERY action', () => { + const query = 'William Shake'; + const thunk = stub(utils, 'thunk'); - Actions.updateSorts(sort); + actions.updateAutocompleteQuery(query); - expect(thunk).to.be.calledWith(Actions.UPDATE_SORTS, { sorts: [sort] }); + expect(thunk).to.be.calledWith(Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }); + }); }); - }); - describe('updatePageSize()', () => { - it('should create an UPDATE_PAGE_SIZE action', () => { - const size = 34; - const thunk = stub(utils, 'thunk'); + describe('updateSorts()', () => { + it('should create a UPDATE_SORTS action', () => { + const sort = { field: 'price' }; + const thunk = stub(utils, 'thunk'); - Actions.updatePageSize(size); + actions.updateSorts(sort); - expect(thunk).to.be.calledWith(Actions.UPDATE_PAGE_SIZE, { size }); + expect(thunk).to.be.calledWith(Actions.UPDATE_SORTS, { sorts: [sort] }); + }); }); - }); - describe('updateCurrentPage()', () => { - it('should create an UPDATE_CURRENT_PAGE action', () => { - const page = 4; - const thunk = stub(utils, 'thunk'); + describe('updatePageSize()', () => { + it('should create an UPDATE_PAGE_SIZE action', () => { + const size = 34; + const thunk = stub(utils, 'thunk'); - Actions.updateCurrentPage(page); + actions.updatePageSize(size); - expect(thunk).to.be.calledWith(Actions.UPDATE_CURRENT_PAGE, { page }); + expect(thunk).to.be.calledWith(Actions.UPDATE_PAGE_SIZE, { size }); + }); + }); + + describe('updateCurrentPage()', () => { + it('should create an UPDATE_CURRENT_PAGE action', () => { + const page = 4; + const thunk = stub(utils, 'thunk'); + + actions.updateCurrentPage(page); + + expect(thunk).to.be.calledWith(Actions.UPDATE_CURRENT_PAGE, { page }); + }); }); - }); - describe('updateDetailsId()', () => { - it('should create an UPDATE_CURRENT_PAGE action', () => { - const id = '123'; - const thunk = stub(utils, 'thunk'); + describe('updateDetailsId()', () => { + it('should create an UPDATE_CURRENT_PAGE action', () => { + const id = '123'; + const thunk = stub(utils, 'thunk'); - Actions.updateDetailsId(id); + actions.updateDetailsId(id); - expect(thunk).to.be.calledWith(Actions.UPDATE_DETAILS_ID, { id }); + expect(thunk).to.be.calledWith(Actions.UPDATE_DETAILS_ID, { id }); + }); }); }); }); diff --git a/test/unit/flux/capacitor.ts b/test/unit/flux/capacitor.ts index 35e3718..ec78f1a 100644 --- a/test/unit/flux/capacitor.ts +++ b/test/unit/flux/capacitor.ts @@ -126,7 +126,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { describe('search()', () => { it('should dispatch updateSearch()', () => { const query = 'half moon'; - const updateSearch = stub(Actions, 'updateSearch'); + const updateSearch = stub(flux.actions, 'updateSearch'); flux.search(query); @@ -135,7 +135,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { it('should fallback to previous query', () => { const query = flux.originalQuery = 'half moon'; - const updateSearch = stub(Actions, 'updateSearch'); + const updateSearch = stub(flux.actions, 'updateSearch'); flux.search(); @@ -146,16 +146,18 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { describe('reset()', () => { it('should dispatch updateSearch()', () => { const query = 'half moon'; + const field = 'brand'; + const index = 8; const refinements = [{ a: 'b' }, { c: 'd' }]; - const updateSearch = stub(Actions, 'updateSearch'); + const updateSearch = stub(flux.actions, 'updateSearch'); - flux.reset(query, refinements); + flux.reset(query, { field, index }); - expect(updateSearch).to.be.calledWith({ query, refinements, clear: true }); + expect(updateSearch).to.be.calledWith({ query, field, index, clear: true }); }); it('should fallback to null query and empty refinements', () => { - const updateSearch = stub(Actions, 'updateSearch'); + const updateSearch = stub(flux.actions, 'updateSearch'); flux.reset(); @@ -165,7 +167,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { describe('resize()', () => { it('should dispatch updatePageSize()', () => { - const updatePageSize = stub(Actions, 'updatePageSize'); + const updatePageSize = stub(flux.actions, 'updatePageSize'); flux.resize(24); @@ -176,7 +178,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { describe('sort()', () => { it('should dispatch updateSorts()', () => { const sort = { field: 'price', descending: true }; - const updateSorts = stub(Actions, 'updateSorts'); + const updateSorts = stub(flux.actions, 'updateSorts'); flux.sort(sort); @@ -185,7 +187,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { it('should accept multiple sorts', () => { const sorts = [{ field: 'price', descending: true }, { field: 'popularity' }]; - const updateSorts = stub(Actions, 'updateSorts'); + const updateSorts = stub(flux.actions, 'updateSorts'); flux.sort(sorts); @@ -195,7 +197,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { describe('refine()', () => { it('should dispatch selectRefinement()', () => { - const selectRefinement = stub(Actions, 'selectRefinement'); + const selectRefinement = stub(flux.actions, 'selectRefinement'); flux.refine('brand', 3); @@ -205,7 +207,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { describe('unrefine()', () => { it('should dispatch deselectRefinement()', () => { - const deselectRefinement = stub(Actions, 'deselectRefinement'); + const deselectRefinement = stub(flux.actions, 'deselectRefinement'); flux.unrefine('brand', 3); @@ -216,7 +218,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { describe('details()', () => { it('should dispatch updateDetailsId()', () => { const id = '123123'; - const updateDetailsId = stub(Actions, 'updateDetailsId'); + const updateDetailsId = stub(flux.actions, 'updateDetailsId'); flux.details(id); @@ -226,7 +228,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { describe('switchCollection()', () => { it('should dispatch selectCollection()', () => { - const selectCollection = stub(Actions, 'selectCollection'); + const selectCollection = stub(flux.actions, 'selectCollection'); flux.switchCollection('products'); From 5b46abdba2549a4c3b09a3549f7ae95375c2b7b3 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Tue, 25 Apr 2017 10:50:18 -0400 Subject: [PATCH 35/56] mooooore --- src/flux/actions.ts | 80 ++++++++++++++++++++++----------------------- src/flux/pager.ts | 22 +++---------- 2 files changed, 45 insertions(+), 57 deletions(-) diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 728513b..25d19f9 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -55,53 +55,53 @@ class Actions { if (Selectors.hasMoreRefinements(state, navigationId)) { this.bridge.refinements(Selectors.searchRequest(state), navigationId) .then(({ navigation: { name, refinements } }) => - dispatch(this.addMoreRefinements(name, refinements.map(ResponseAdapter.extractRefinement)))); + dispatch(this.receiveMoreRefinements(name, refinements.map(ResponseAdapter.extractRefinement)))); } } // response action creators - addMoreRefinements = (navigationId: string, refinements: any) => - thunk(Actions.ADD_MORE_REFINEMENTS, { navigationId, refinements }) - - updateSearchResponse = (results: Results) => + receiveSearchResponse = (results: Results) => (dispatch: Dispatch, getStore: () => Store.State) => { const state = getStore(); - dispatch(this.updateQuery(ResponseAdapter.extractQuery(results, this.linkMapper))); - dispatch(this.updateProducts(results.records.map((product) => product.allMeta))); - dispatch(this.updateCollectionCount(state.data.collections.selected, results.totalRecordCount)); + dispatch(this.receiveRedirect(results.redirect)); + dispatch(this.receiveQuery(ResponseAdapter.extractQuery(results, this.linkMapper))); + dispatch(this.receiveProducts(results.records.map((product) => product.allMeta))); // tslint:disable-next-line max-line-length - dispatch(this.updateNavigations(ResponseAdapter.combineNavigations(results.availableNavigation, results.selectedNavigation))); - dispatch(this.updatePage(ResponseAdapter.extractPage(state, results))); - dispatch(this.updateTemplate(ResponseAdapter.extractTemplate(results.template))); - dispatch(this.updateRedirect(results.redirect)); + dispatch(this.receiveNavigations(ResponseAdapter.combineNavigations(results.availableNavigation, results.selectedNavigation))); + dispatch(this.receivePage(ResponseAdapter.extractPage(state, results))); + dispatch(this.receiveTemplate(ResponseAdapter.extractTemplate(results.template))); + dispatch(this.receiveCollectionCount(state.data.collections.selected, results.totalRecordCount)); } - updateQuery = (query: Query) => - thunk(Actions.UPDATE_QUERY, query) + receiveQuery = (query: Query) => + thunk(Actions.RECEIVE_QUERY, query) + + receiveProducts = (products: Store.Product[]) => + thunk(Actions.RECEIVE_PRODUCTS, { products }) - updateProducts = (products: Store.Product[]) => - thunk(Actions.UPDATE_PRODUCTS, { products }) + receiveCollectionCount = (collection: string, count: number) => + thunk(Actions.RECEIVE_COLLECTION_COUNT, { collection, count }) - updateCollectionCount = (collection: string, count: number) => - thunk(Actions.UPDATE_COLLECTION_COUNT, { collection, count }) + receiveNavigations = (navigations: Store.Navigation[]) => + thunk(Actions.RECEIVE_NAVIGATIONS, { navigations }) - updateNavigations = (navigations: Store.Navigation[]) => - thunk(Actions.UPDATE_NAVIGATIONS, { navigations }) + receivePage = (page: Page) => + thunk(Actions.RECEIVE_PAGE, page) - updatePage = (page: Page) => - thunk(Actions.UPDATE_PAGE, page) + receiveTemplate = (template: Store.Template) => + thunk(Actions.RECEIVE_TEMPLATE, { template }) - updateTemplate = (template: Store.Template) => - thunk(Actions.UPDATE_TEMPLATE, { template }) + receiveRedirect = (redirect: string) => + thunk(Actions.RECEIVE_REDIRECT, { redirect }) - updateRedirect = (redirect: string) => - thunk(Actions.UPDATE_REDIRECT, { redirect }) + receiveMoreRefinements = (navigationId: string, refinements: any) => + thunk(Actions.RECEIVE_MORE_REFINEMENTS, { navigationId, refinements }) - updateAutocompleteSuggestions = (suggestions: string[], category: Store.Autocomplete.Category) => - thunk(Actions.UPDATE_AUTOCOMPLETE_SUGGESTIONS, { suggestions, category }) + receiveAutocompleteSuggestions = (suggestions: string[], category: Store.Autocomplete.Category) => + thunk(Actions.RECEIVE_AUTOCOMPLETE_SUGGESTIONS, { suggestions, category }) - updateDetailsProduct = (product: Store.Product) => - thunk(Actions.UPDATE_DETAILS_PRODUCT, { product }) + receiveDetailsProduct = (product: Store.Product) => + thunk(Actions.RECEIVE_DETAILS_PRODUCT, { product }) } namespace Actions { @@ -117,16 +117,16 @@ namespace Actions { export const UPDATE_CURRENT_PAGE = 'UPDATE_CURRENT_PAGE'; // response actions - export const ADD_MORE_REFINEMENTS = 'ADD_MORE_REFINEMENTS'; - export const UPDATE_AUTOCOMPLETE_SUGGESTIONS = 'UPDATE_AUTOCOMPLETE_SUGGESTIONS'; - export const UPDATE_DETAILS_PRODUCT = 'UPDATE_DETAILS_PRODUCT'; - export const UPDATE_QUERY = 'UPDATE_AUTOCOMPLETE_SUGGESTIONS'; - export const UPDATE_PRODUCTS = 'UPDATE_PRODUCTS'; - export const UPDATE_COLLECTION_COUNT = 'UPDATE_COLLECTION_COUNT'; - export const UPDATE_NAVIGATIONS = 'UPDATE_NAVIGATIONS'; - export const UPDATE_PAGE = 'UPDATE_PAGE'; - export const UPDATE_TEMPLATE = 'UPDATE_TEMPLATE'; - export const UPDATE_REDIRECT = 'UPDATE_TEMPLATE'; + export const RECEIVE_MORE_REFINEMENTS = 'RECEIVE_MORE_REFINEMENTS'; + export const RECEIVE_AUTOCOMPLETE_SUGGESTIONS = 'RECEIVE_AUTOCOMPLETE_SUGGESTIONS'; + export const RECEIVE_DETAILS_PRODUCT = 'RECEIVE_DETAILS_PRODUCT'; + export const RECEIVE_QUERY = 'RECEIVE_QUERY'; + export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'; + export const RECEIVE_COLLECTION_COUNT = 'RECEIVE_COLLECTION_COUNT'; + export const RECEIVE_NAVIGATIONS = 'RECEIVE_NAVIGATIONS'; + export const RECEIVE_PAGE = 'RECEIVE_PAGE'; + export const RECEIVE_TEMPLATE = 'RECEIVE_TEMPLATE'; + export const RECEIVE_REDIRECT = 'RECEIVE_REDIRECT'; } export default Actions; diff --git a/src/flux/pager.ts b/src/flux/pager.ts index d72413e..6f123b7 100644 --- a/src/flux/pager.ts +++ b/src/flux/pager.ts @@ -36,23 +36,11 @@ export class Pager { } } - get totalRecords(): number { - return this.results.totalRecordCount; - } - - get pageSize(): number { - // TODO move this default into the reducer setup - return this.state.data.page.size || 10; - } - - get currentPage(): number { - return this.state.data.page.current; - } - build(): Page { - const pageSize = this.pageSize; + // TODO move this default into the reducer setup + const pageSize = this.state.data.page.size || 10; const currentPage = this.state.data.page.current; - const totalRecords = this.totalRecords; + const totalRecords = this.results.totalRecordCount; const finalPage = this.finalPage(pageSize, totalRecords); return { @@ -62,7 +50,7 @@ export class Pager { previous: this.previousPage(currentPage), range: this.pageNumbers(currentPage, finalPage, this.state.data.page.limit), to: this.toResult(currentPage, pageSize, totalRecords), - total: this.totalRecords, + total: totalRecords, }; } @@ -86,7 +74,7 @@ export class Pager { } getPage(record: number, pageSize: number): number { - return Math.ceil(record / this.pageSize); + return Math.ceil(record / pageSize); } transformPages(currentPage: number, finalPage: number, limit: number): (value: number) => number { From 7c7dc5f6499edda7e39bd1aacb50a6ed0c23e46b Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Tue, 25 Apr 2017 11:29:20 -0400 Subject: [PATCH 36/56] Add files --- src/flux/actions.ts | 4 +- src/flux/reducer.ts | 188 ------------------------------ src/flux/reducers/autocomplete.ts | 20 ++++ src/flux/reducers/collections.ts | 11 ++ src/flux/reducers/details.ts | 11 ++ src/flux/reducers/errors.ts | 11 ++ src/flux/reducers/index.ts | 33 ++++++ src/flux/reducers/navigations.ts | 63 ++++++++++ src/flux/reducers/page.ts | 19 +++ src/flux/reducers/products.ts | 11 ++ src/flux/reducers/query.ts | 11 ++ src/flux/reducers/redirect.ts | 11 ++ src/flux/reducers/sorts.ts | 11 ++ src/flux/reducers/template.ts | 11 ++ src/flux/reducers/warnings.ts | 11 ++ 15 files changed, 236 insertions(+), 190 deletions(-) delete mode 100644 src/flux/reducer.ts create mode 100644 src/flux/reducers/autocomplete.ts create mode 100644 src/flux/reducers/collections.ts create mode 100644 src/flux/reducers/details.ts create mode 100644 src/flux/reducers/errors.ts create mode 100644 src/flux/reducers/index.ts create mode 100644 src/flux/reducers/navigations.ts create mode 100644 src/flux/reducers/page.ts create mode 100644 src/flux/reducers/products.ts create mode 100644 src/flux/reducers/query.ts create mode 100644 src/flux/reducers/redirect.ts create mode 100644 src/flux/reducers/sorts.ts create mode 100644 src/flux/reducers/template.ts create mode 100644 src/flux/reducers/warnings.ts diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 25d19f9..1f6471c 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -97,8 +97,8 @@ class Actions { receiveMoreRefinements = (navigationId: string, refinements: any) => thunk(Actions.RECEIVE_MORE_REFINEMENTS, { navigationId, refinements }) - receiveAutocompleteSuggestions = (suggestions: string[], category: Store.Autocomplete.Category) => - thunk(Actions.RECEIVE_AUTOCOMPLETE_SUGGESTIONS, { suggestions, category }) + receiveAutocompleteSuggestions = (suggestions: string[], categoryValues: string[]) => + thunk(Actions.RECEIVE_AUTOCOMPLETE_SUGGESTIONS, { suggestions, categoryValues }) receiveDetailsProduct = (product: Store.Product) => thunk(Actions.RECEIVE_DETAILS_PRODUCT, { product }) diff --git a/src/flux/reducer.ts b/src/flux/reducer.ts deleted file mode 100644 index 4dc1d77..0000000 --- a/src/flux/reducer.ts +++ /dev/null @@ -1,188 +0,0 @@ -import * as redux from 'redux'; -import Actions from './actions'; -import Store from './store'; - -export function updateAutocomplete(state: Store.Autocomplete, action) { - switch (action) { - case Actions.UPDATE_AUTOCOMPLETE_QUERY: - return { ...state, query: action.query }; - default: - return state; - } -} - -export function updateCollections(state: Store.Indexed.Selectable, action) { - switch (action) { - case Actions.SELECT_COLLECTION: - return { ...state, selected: action.id }; - default: - return state; - } -} - -export function updateDetails(state: Store.Details, action) { - switch (action) { - case Actions.UPDATE_DETAILS_ID: - return { ...state, id: action.id }; - default: - return state; - } -} - -export function updateErrors(state, action) { - switch (action) { - // case Actions.UPDATE_ERRORS: - // return { ...state }; - default: - return state; - } -} - -export function updateNavigations(state: Store.Indexed, action) { - const navigationId = action.navigationId; - const refinementIndex = action.index; - switch (action) { - case Actions.UPDATE_SEARCH: - // TODO: add case for clear - if (action.clear) { - const byIds = state.allIds.reduce( - (newById, index) => Object.assign(newById, { [index]: { ...state.byId[index], selected: [] } }), { }, - ); - if (!(navigationId && action.index)) { - return { - ...state, - byId: byIds, - }; - } else { - return { - ...state, - byId: { - ...byIds, - [navigationId]: { - ...state.byId[refinementIndex], - // TODO: maybe check if already there - selected: refinementIndex, - }, - }, - }; - } -} - case Actions.SELECT_REFINEMENT: -if (navigationId && refinementIndex) { - return { - ...state, - byId: { - ...state.byId, - [navigationId]: { - ...state.byId[navigationId], - // TODO: maybe check if already there - selected: state.byId[navigationId].selected.concat(refinementIndex), - }, - }, - }; -} else { - return state; -} - case Actions.DESELECT_REFINEMENT: -return { - ...state, - byId: { - ...state.byId, - [navigationId]: { - ...state.byId[navigationId], - selected: state.byId[navigationId].selected.filter((index) => index !== refinementIndex), - }, - }, -}; - default: -return state; - } -} - -export function updatePage(state: Store.Page, action) { - switch (action) { - case Actions.UPDATE_SEARCH: - case Actions.UPDATE_SORTS: - case Actions.SELECT_COLLECTION: - case Actions.SELECT_REFINEMENT: - case Actions.DESELECT_REFINEMENT: - return { ...state, current: 1 }; - case Actions.UPDATE_CURRENT_PAGE: - return { ...state, current: action.page }; - case Actions.UPDATE_PAGE_SIZE: - return { ...state, current: 1, size: action.size }; - default: - return state; - } -} - -export function updateProducts(state: Store.Product, action) { - switch (action) { - // case Actions.UPDATE_PRODUCTS: - // return { ...state }; - default: - return state; - } -} - -export function updateQuery(state: Store.Query, action) { - switch (action) { - case Actions.UPDATE_SEARCH: - return { ...state, original: action.query }; - default: - return state; - } -} - -export function updateRedirect(state, action) { - switch (action) { - // case Actions.UPDATE_REDIRECT: - // return { ...state }; - default: - return state; - } -} - -export function updateSorts(state: Store.Indexed.Selectable, action) { - switch (action) { - case Actions.UPDATE_SORTS: - return { ...state, selected: action.id }; - default: - return state; - } -} - -export function updateTemplate(state, action) { - switch (action) { - // case Actions.UPDATE_TEMPLATE: - // return { ...state }; - default: - return state; - } -} - -export function updateWarnings(state, action) { - switch (action) { - // case Actions.UPDATE_WARNINGS: - // return { ...state }; - default: - return state; - } -} - -export default redux.combineReducers({ - data: redux.combineReducers({ - autocomplete: updateAutocomplete, - collections: updateCollections, - details: updateDetails, - errors: updateErrors, - navigations: updateNavigations, - page: updatePage, - products: updateProducts, - query: updateQuery, - redirect: updateRedirect, - sorts: updateSorts, - template: updateTemplate, - warnings: updateWarnings, - }), -}); diff --git a/src/flux/reducers/autocomplete.ts b/src/flux/reducers/autocomplete.ts new file mode 100644 index 0000000..3d9e08d --- /dev/null +++ b/src/flux/reducers/autocomplete.ts @@ -0,0 +1,20 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateAutocomplete(state: Store.Autocomplete, action) { + switch (action) { + case Actions.UPDATE_AUTOCOMPLETE_QUERY: + return { ...state, query: action.query }; + case Actions.RECEIVE_AUTOCOMPLETE_SUGGESTIONS: + return { + ...state, + category: { + ...state.category, + values: action.categoryValues, + }, + suggestions: action.suggestions, + }; + default: + return state; + } +} diff --git a/src/flux/reducers/collections.ts b/src/flux/reducers/collections.ts new file mode 100644 index 0000000..aff055c --- /dev/null +++ b/src/flux/reducers/collections.ts @@ -0,0 +1,11 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateCollections(state: Store.Indexed.Selectable, action) { + switch (action) { + case Actions.SELECT_COLLECTION: + return { ...state, selected: action.id }; + default: + return state; + } +} diff --git a/src/flux/reducers/details.ts b/src/flux/reducers/details.ts new file mode 100644 index 0000000..936886e --- /dev/null +++ b/src/flux/reducers/details.ts @@ -0,0 +1,11 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateDetails(state: Store.Details, action) { + switch (action) { + case Actions.UPDATE_DETAILS_ID: + return { ...state, id: action.id }; + default: + return state; + } +} diff --git a/src/flux/reducers/errors.ts b/src/flux/reducers/errors.ts new file mode 100644 index 0000000..6089314 --- /dev/null +++ b/src/flux/reducers/errors.ts @@ -0,0 +1,11 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateErrors(state, action) { + switch (action) { + // case Actions.UPDATE_ERRORS: + // return { ...state }; + default: + return state; + } +} diff --git a/src/flux/reducers/index.ts b/src/flux/reducers/index.ts new file mode 100644 index 0000000..239c999 --- /dev/null +++ b/src/flux/reducers/index.ts @@ -0,0 +1,33 @@ +import * as redux from 'redux'; +import Actions from '../actions'; +import Store from '../store'; + +import autocomplete from './autocomplete'; +import collections from './collections'; +import details from './details'; +import errors from './errors'; +import navigations from './navigations'; +import page from './page'; +import products from './products'; +import query from './query'; +import redirect from './redirect'; +import sorts from './sorts'; +import template from './template'; +import warnings from './warnings'; + +export default redux.combineReducers({ + data: redux.combineReducers({ + autocomplete, + collections, + details, + errors, + navigations, + page, + products, + query, + redirect, + sorts, + template, + warnings, + }), +}); diff --git a/src/flux/reducers/navigations.ts b/src/flux/reducers/navigations.ts new file mode 100644 index 0000000..aeb4781 --- /dev/null +++ b/src/flux/reducers/navigations.ts @@ -0,0 +1,63 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateNavigations(state: Store.Indexed, action) { + const navigationId = action.navigationId; + const refinementIndex = action.index; + switch (action) { + case Actions.UPDATE_SEARCH: + // TODO: add case for clear + if (action.clear) { + const byIds = state.allIds.reduce( + (newById, index) => Object.assign(newById, { [index]: { ...state.byId[index], selected: [] } }), { }, + ); + if (!(navigationId && action.index)) { + return { + ...state, + byId: byIds, + }; + } else { + return { + ...state, + byId: { + ...byIds, + [navigationId]: { + ...state.byId[refinementIndex], + // TODO: maybe check if already there + selected: refinementIndex, + }, + }, + }; + } + } + case Actions.SELECT_REFINEMENT: + if (navigationId && refinementIndex) { + return { + ...state, + byId: { + ...state.byId, + [navigationId]: { + ...state.byId[navigationId], + // TODO: maybe check if already there + selected: state.byId[navigationId].selected.concat(refinementIndex), + }, + }, + }; + } else { + return state; + } + case Actions.DESELECT_REFINEMENT: + return { + ...state, + byId: { + ...state.byId, + [navigationId]: { + ...state.byId[navigationId], + selected: state.byId[navigationId].selected.filter((index) => index !== refinementIndex), + }, + }, + }; + default: + return state; + } +} diff --git a/src/flux/reducers/page.ts b/src/flux/reducers/page.ts new file mode 100644 index 0000000..595e16e --- /dev/null +++ b/src/flux/reducers/page.ts @@ -0,0 +1,19 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updatePage(state: Store.Page, action) { + switch (action) { + case Actions.UPDATE_SEARCH: + case Actions.UPDATE_SORTS: + case Actions.SELECT_COLLECTION: + case Actions.SELECT_REFINEMENT: + case Actions.DESELECT_REFINEMENT: + return { ...state, current: 1 }; + case Actions.UPDATE_CURRENT_PAGE: + return { ...state, current: action.page }; + case Actions.UPDATE_PAGE_SIZE: + return { ...state, current: 1, size: action.size }; + default: + return state; + } +} diff --git a/src/flux/reducers/products.ts b/src/flux/reducers/products.ts new file mode 100644 index 0000000..df42703 --- /dev/null +++ b/src/flux/reducers/products.ts @@ -0,0 +1,11 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateProducts(state: Store.Product, action) { + switch (action) { + // case Actions.UPDATE_PRODUCTS: + // return { ...state }; + default: + return state; + } +} diff --git a/src/flux/reducers/query.ts b/src/flux/reducers/query.ts new file mode 100644 index 0000000..d7ea3fc --- /dev/null +++ b/src/flux/reducers/query.ts @@ -0,0 +1,11 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateQuery(state: Store.Query, action) { + switch (action) { + case Actions.UPDATE_SEARCH: + return { ...state, original: action.query }; + default: + return state; + } +} diff --git a/src/flux/reducers/redirect.ts b/src/flux/reducers/redirect.ts new file mode 100644 index 0000000..e0b9f0a --- /dev/null +++ b/src/flux/reducers/redirect.ts @@ -0,0 +1,11 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateRedirect(state, action) { + switch (action) { + // case Actions.UPDATE_REDIRECT: + // return { ...state }; + default: + return state; + } +} diff --git a/src/flux/reducers/sorts.ts b/src/flux/reducers/sorts.ts new file mode 100644 index 0000000..118cdcc --- /dev/null +++ b/src/flux/reducers/sorts.ts @@ -0,0 +1,11 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateSorts(state: Store.Indexed.Selectable, action) { + switch (action) { + case Actions.UPDATE_SORTS: + return { ...state, selected: action.id }; + default: + return state; + } +} diff --git a/src/flux/reducers/template.ts b/src/flux/reducers/template.ts new file mode 100644 index 0000000..433f2d1 --- /dev/null +++ b/src/flux/reducers/template.ts @@ -0,0 +1,11 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateTemplate(state, action) { + switch (action) { + // case Actions.UPDATE_TEMPLATE: + // return { ...state }; + default: + return state; + } +} diff --git a/src/flux/reducers/warnings.ts b/src/flux/reducers/warnings.ts new file mode 100644 index 0000000..a874f8f --- /dev/null +++ b/src/flux/reducers/warnings.ts @@ -0,0 +1,11 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateWarnings(state, action) { + switch (action) { + // case Actions.UPDATE_WARNINGS: + // return { ...state }; + default: + return state; + } +} From 0c07fbee3b0404d70d523c58955b79d28738aa61 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Tue, 25 Apr 2017 11:29:53 -0400 Subject: [PATCH 37/56] more action testst --- test/unit/flux/actions.ts | 62 +++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts index 6b71255..5e9d2b6 100644 --- a/test/unit/flux/actions.ts +++ b/test/unit/flux/actions.ts @@ -1,8 +1,10 @@ +import { BrowserBridge } from '../../../src/core/bridge'; import Actions from '../../../src/flux/actions'; +import Selectors from '../../../src/flux/selectors'; import * as utils from '../../../src/flux/utils'; import suite from '../_suite'; -suite('Actions', ({ expect, spy, stub }) => { +suite.only('Actions', ({ expect, spy, stub }) => { let actions: Actions; const bridge: any = { a: 'b' }; @@ -10,7 +12,7 @@ suite('Actions', ({ expect, spy, stub }) => { describe('constructor()', () => { it('should set properties', () => { - expect(actions['bridge']).to.be.a('function'); + expect(actions['bridge']).to.eq(bridge); expect(actions['linkMapper']).to.be.a('function'); }); }); @@ -31,11 +33,16 @@ suite('Actions', ({ expect, spy, stub }) => { it('should create a SELECT_REFINEMENT action', () => { const navigationId = 'brand'; const index = 3; - const thunk = stub(utils, 'thunk'); + const state = { a: 'b' }; + const conditional = stub(utils, 'conditional'); + const isRefinementDeselected = stub(Selectors, 'isRefinementDeselected'); actions.selectRefinement(navigationId, index); - expect(thunk).to.be.calledWith(Actions.SELECT_REFINEMENT, { navigationId, index }); + expect(conditional).to.be.calledWith(sinon.match((predicate) => { + predicate(state); + return expect(isRefinementDeselected).to.be.calledWith(state, navigationId, index); + }), Actions.SELECT_REFINEMENT, { navigationId, index }); }); }); @@ -43,66 +50,81 @@ suite('Actions', ({ expect, spy, stub }) => { it('should create a DESELECT_REFINEMENT action', () => { const navigationId = 'brand'; const index = 3; - const thunk = stub(utils, 'thunk'); + const state = { a: 'b' }; + const conditional = stub(utils, 'conditional'); + const isRefinementSelected = stub(Selectors, 'isRefinementSelected'); actions.deselectRefinement(navigationId, index); - expect(thunk).to.be.calledWith(Actions.DESELECT_REFINEMENT, { navigationId, index }); + expect(conditional).to.be.calledWith(sinon.match((predicate) => { + predicate(state); + return expect(isRefinementSelected).to.be.calledWith(state, navigationId, index); + }), Actions.DESELECT_REFINEMENT, { navigationId, index }); }); }); describe('selectCollection()', () => { it('should create a SELECT_COLLECTION action', () => { const id = 'products'; - const thunk = stub(utils, 'thunk'); + const conditional = stub(utils, 'conditional'); actions.selectCollection(id); - expect(thunk).to.be.calledWith(Actions.SELECT_COLLECTION, { id }); + expect(conditional).to.be.calledWith(sinon.match((predicate) => + predicate({ data: { collections: { selected: 'tutorials' } } })), + Actions.SELECT_COLLECTION, { id }); }); }); describe('updateAutocompleteQuery()', () => { it('should create an UPDATE_AUTOCOMPLETE_QUERY action', () => { const query = 'William Shake'; - const thunk = stub(utils, 'thunk'); + const conditional = stub(utils, 'conditional'); actions.updateAutocompleteQuery(query); - expect(thunk).to.be.calledWith(Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }); + expect(conditional).to.be.calledWith(sinon.match((predicate) => + predicate({ data: { autocomplete: { query: 'Fred Flinsto' } } })), + Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }); }); }); describe('updateSorts()', () => { it('should create a UPDATE_SORTS action', () => { - const sort = { field: 'price' }; - const thunk = stub(utils, 'thunk'); + const id = 'Price Ascending'; + const conditional = stub(utils, 'conditional'); - actions.updateSorts(sort); + actions.updateSorts(id); - expect(thunk).to.be.calledWith(Actions.UPDATE_SORTS, { sorts: [sort] }); + expect(conditional).to.be.calledWith(sinon.match((predicate) => + predicate({ data: { sorts: { selected: 'Price Descending' } } })), + Actions.UPDATE_SORTS, { id }); }); }); describe('updatePageSize()', () => { it('should create an UPDATE_PAGE_SIZE action', () => { const size = 34; - const thunk = stub(utils, 'thunk'); + const conditional = stub(utils, 'conditional'); actions.updatePageSize(size); - expect(thunk).to.be.calledWith(Actions.UPDATE_PAGE_SIZE, { size }); + expect(conditional).to.be.calledWith(sinon.match((predicate) => + predicate({ data: { page: { size: 20 } } })), + Actions.UPDATE_PAGE_SIZE, { size }); }); }); describe('updateCurrentPage()', () => { it('should create an UPDATE_CURRENT_PAGE action', () => { const page = 4; - const thunk = stub(utils, 'thunk'); + const conditional = stub(utils, 'conditional'); actions.updateCurrentPage(page); - expect(thunk).to.be.calledWith(Actions.UPDATE_CURRENT_PAGE, { page }); + expect(conditional).to.be.calledWith(sinon.match((predicate) => + predicate({ data: { page: { current: 3 } } })), + Actions.UPDATE_CURRENT_PAGE, { page }); }); }); @@ -117,4 +139,8 @@ suite('Actions', ({ expect, spy, stub }) => { }); }); }); + + describe('response action creators', () => { + + }); }); From 015bd01e03a68752d3e89ba26c2b369844820781 Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Tue, 25 Apr 2017 12:00:51 -0400 Subject: [PATCH 38/56] Add reducers and record-count as global --- src/flux/actions.ts | 3 +++ src/flux/reducers/collections.ts | 12 ++++++++++++ src/flux/reducers/details.ts | 2 ++ src/flux/reducers/index.ts | 2 ++ src/flux/reducers/page.ts | 10 ++++++++++ src/flux/reducers/query.ts | 8 ++++++++ src/flux/reducers/record-count.ts | 11 +++++++++++ src/flux/store.ts | 1 + 8 files changed, 49 insertions(+) create mode 100644 src/flux/reducers/record-count.ts diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 1f6471c..a8415fd 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -117,12 +117,15 @@ namespace Actions { export const UPDATE_CURRENT_PAGE = 'UPDATE_CURRENT_PAGE'; // response actions + // TODO export const RECEIVE_MORE_REFINEMENTS = 'RECEIVE_MORE_REFINEMENTS'; export const RECEIVE_AUTOCOMPLETE_SUGGESTIONS = 'RECEIVE_AUTOCOMPLETE_SUGGESTIONS'; export const RECEIVE_DETAILS_PRODUCT = 'RECEIVE_DETAILS_PRODUCT'; export const RECEIVE_QUERY = 'RECEIVE_QUERY'; + // TODO export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'; export const RECEIVE_COLLECTION_COUNT = 'RECEIVE_COLLECTION_COUNT'; + // TODO export const RECEIVE_NAVIGATIONS = 'RECEIVE_NAVIGATIONS'; export const RECEIVE_PAGE = 'RECEIVE_PAGE'; export const RECEIVE_TEMPLATE = 'RECEIVE_TEMPLATE'; diff --git a/src/flux/reducers/collections.ts b/src/flux/reducers/collections.ts index aff055c..9f0ad2e 100644 --- a/src/flux/reducers/collections.ts +++ b/src/flux/reducers/collections.ts @@ -5,6 +5,18 @@ export default function updateCollections(state: Store.Indexed.Selectable; // pre products: Product[]; // post collections: Indexed.Selectable; // mixed From 856300f3accf9da6a28092067857c6614c0ceb9c Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Tue, 25 Apr 2017 12:10:49 -0400 Subject: [PATCH 39/56] More receiving reducer cases --- src/flux/actions.ts | 1 - src/flux/reducers/page.ts | 8 ++++---- src/flux/reducers/record-count.ts | 2 +- src/flux/reducers/redirect.ts | 4 ++-- src/flux/reducers/template.ts | 6 +++--- src/flux/store.ts | 5 ----- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/flux/actions.ts b/src/flux/actions.ts index a8415fd..eef1994 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -177,7 +177,6 @@ export interface Page { last: number; from: number; to: number; - total: number; range: number[]; } diff --git a/src/flux/reducers/page.ts b/src/flux/reducers/page.ts index dbd6c92..8c43136 100644 --- a/src/flux/reducers/page.ts +++ b/src/flux/reducers/page.ts @@ -16,12 +16,12 @@ export default function updatePage(state: Store.Page, action) { case Actions.RECEIVE_PAGE: return { ...state, - previous: action.previous, - next: action.next, - last: action.last, from: action.from, + last: action.last, + next: action.next, + previous: action.previous, + range: action.range, to: action.to, - }; default: return state; diff --git a/src/flux/reducers/record-count.ts b/src/flux/reducers/record-count.ts index f474cb0..53cc3c9 100644 --- a/src/flux/reducers/record-count.ts +++ b/src/flux/reducers/record-count.ts @@ -4,7 +4,7 @@ import Store from '../store'; export default function updateRecordCount(state: Store.Page, action) { switch (action) { case Actions.RECEIVE_PRODUCTS: - return { ...state, recordCount: action.recordCount }; + return action.recordCount; default: return state; } diff --git a/src/flux/reducers/redirect.ts b/src/flux/reducers/redirect.ts index e0b9f0a..08c8566 100644 --- a/src/flux/reducers/redirect.ts +++ b/src/flux/reducers/redirect.ts @@ -3,8 +3,8 @@ import Store from '../store'; export default function updateRedirect(state, action) { switch (action) { - // case Actions.UPDATE_REDIRECT: - // return { ...state }; + case Actions.RECEIVE_REDIRECT: + return action.redirect; default: return state; } diff --git a/src/flux/reducers/template.ts b/src/flux/reducers/template.ts index 433f2d1..1b9eabf 100644 --- a/src/flux/reducers/template.ts +++ b/src/flux/reducers/template.ts @@ -1,10 +1,10 @@ import Actions from '../actions'; import Store from '../store'; -export default function updateTemplate(state, action) { +export default function updateTemplate(state: Store.Template, action) { switch (action) { - // case Actions.UPDATE_TEMPLATE: - // return { ...state }; + case Actions.RECEIVE_TEMPLATE: + return action.template; default: return state; } diff --git a/src/flux/store.ts b/src/flux/store.ts index f1542a9..4f73fde 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -108,11 +108,6 @@ namespace Store { */ to: number; // post - /** - * the total number of products returned by this search - */ - total: number; // post - /** * displayed number range (in ) */ From b156e0f538c518eb826900b671ef155d0ff1d4ef Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Tue, 25 Apr 2017 12:32:22 -0400 Subject: [PATCH 40/56] more action tests --- src/flux/actions.ts | 33 ++++--- src/flux/store.ts | 2 +- test/unit/flux/actions.ts | 201 +++++++++++++++++++++++++++++++++++--- 3 files changed, 206 insertions(+), 30 deletions(-) diff --git a/src/flux/actions.ts b/src/flux/actions.ts index eef1994..7b9d011 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -14,6 +14,17 @@ class Actions { this.linkMapper = LinkMapper(paths.search); } + // fetch action creators + fetchMoreRefinements = (navigationId: string) => + (dispatch: Dispatch, getStore: () => Store.State) => { + const state = getStore(); + if (Selectors.hasMoreRefinements(state, navigationId)) { + this.bridge.refinements(Selectors.searchRequest(state), navigationId) + .then(({ navigation: { name, refinements } }) => + dispatch(this.receiveMoreRefinements(name, refinements.map(ResponseAdapter.extractRefinement)))); + } + } + // request action creators updateSearch = (search: Search) => thunk(Actions.UPDATE_SEARCH, search) @@ -34,10 +45,6 @@ class Actions { conditional((state) => state.data.sorts.selected !== id, Actions.UPDATE_SORTS, { id }) - updateAutocompleteQuery = (query: string) => - conditional((state) => state.data.autocomplete.query !== query, - Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }) - updatePageSize = (size: number) => conditional((state) => state.data.page.size !== size, Actions.UPDATE_PAGE_SIZE, { size }) @@ -49,15 +56,9 @@ class Actions { updateDetailsId = (id: string) => thunk(Actions.UPDATE_DETAILS_ID, { id }) - fetchMoreRefinements = (navigationId: string) => - (dispatch: Dispatch, getStore: () => Store.State) => { - const state = getStore(); - if (Selectors.hasMoreRefinements(state, navigationId)) { - this.bridge.refinements(Selectors.searchRequest(state), navigationId) - .then(({ navigation: { name, refinements } }) => - dispatch(this.receiveMoreRefinements(name, refinements.map(ResponseAdapter.extractRefinement)))); - } - } + updateAutocompleteQuery = (query: string) => + conditional((state) => state.data.autocomplete.query !== query, + Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }) // response action creators receiveSearchResponse = (results: Results) => @@ -65,7 +66,7 @@ class Actions { const state = getStore(); dispatch(this.receiveRedirect(results.redirect)); dispatch(this.receiveQuery(ResponseAdapter.extractQuery(results, this.linkMapper))); - dispatch(this.receiveProducts(results.records.map((product) => product.allMeta))); + dispatch(this.receiveProducts(results.records.map((product) => product.allMeta), results.totalRecordCount)); // tslint:disable-next-line max-line-length dispatch(this.receiveNavigations(ResponseAdapter.combineNavigations(results.availableNavigation, results.selectedNavigation))); dispatch(this.receivePage(ResponseAdapter.extractPage(state, results))); @@ -76,8 +77,8 @@ class Actions { receiveQuery = (query: Query) => thunk(Actions.RECEIVE_QUERY, query) - receiveProducts = (products: Store.Product[]) => - thunk(Actions.RECEIVE_PRODUCTS, { products }) + receiveProducts = (products: Store.Product[], recordCount: number) => + thunk(Actions.RECEIVE_PRODUCTS, { products, recordCount }) receiveCollectionCount = (collection: string, count: number) => thunk(Actions.RECEIVE_COLLECTION_COUNT, { collection, count }) diff --git a/src/flux/store.ts b/src/flux/store.ts index 4f73fde..cabd1da 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -2,7 +2,7 @@ import * as redux from 'redux'; import thunk from 'redux-thunk'; import { Request } from '../models/request'; import { Results } from '../models/response'; -import reducer from './reducer'; +import reducer from './reducers'; namespace Store { diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts index 5e9d2b6..16655f3 100644 --- a/test/unit/flux/actions.ts +++ b/test/unit/flux/actions.ts @@ -1,5 +1,6 @@ import { BrowserBridge } from '../../../src/core/bridge'; import Actions from '../../../src/flux/actions'; +import ResponseAdapter from '../../../src/flux/adapters/response'; import Selectors from '../../../src/flux/selectors'; import * as utils from '../../../src/flux/utils'; import suite from '../_suite'; @@ -76,19 +77,6 @@ suite.only('Actions', ({ expect, spy, stub }) => { }); }); - describe('updateAutocompleteQuery()', () => { - it('should create an UPDATE_AUTOCOMPLETE_QUERY action', () => { - const query = 'William Shake'; - const conditional = stub(utils, 'conditional'); - - actions.updateAutocompleteQuery(query); - - expect(conditional).to.be.calledWith(sinon.match((predicate) => - predicate({ data: { autocomplete: { query: 'Fred Flinsto' } } })), - Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }); - }); - }); - describe('updateSorts()', () => { it('should create a UPDATE_SORTS action', () => { const id = 'Price Ascending'; @@ -138,9 +126,196 @@ suite.only('Actions', ({ expect, spy, stub }) => { expect(thunk).to.be.calledWith(Actions.UPDATE_DETAILS_ID, { id }); }); }); + + describe('updateAutocompleteQuery()', () => { + it('should create an UPDATE_AUTOCOMPLETE_QUERY action', () => { + const query = 'William Shake'; + const conditional = stub(utils, 'conditional'); + + actions.updateAutocompleteQuery(query); + + expect(conditional).to.be.calledWith(sinon.match((predicate) => + predicate({ data: { autocomplete: { query: 'Fred Flinsto' } } })), + Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }); + }); + }); }); describe('response action creators', () => { + describe('receiveSearchResponse()', () => { + it('should return a thunk', () => { + const results: any = {}; + + const thunk = actions.receiveSearchResponse(results); + + expect(thunk).to.be.a('function'); + }); + + it('should dispatch actions', () => { + const receiveRedirectAction = () => null; + const receiveQueryAction = () => null; + const receiveProductsAction = () => null; + const receiveNavigationsAction = () => null; + const receivePageAction = () => null; + const receiveTemplateAction = () => null; + const receiveCollectionCountAction = () => null; + const linkMapper = actions['linkMapper'] = () => null; + const results: any = { + availableNavigation: ['d', 'e'], + records: [ + { allMeta: { u: 'v' } }, + { allMeta: { w: 'x' } }, + ], + redirect: 'page.html', + selectedNavigation: ['b', 'c'], + template: { m: 'n' }, + totalRecordCount: 41, + }; + const query: any = { y: 'z' }; + const navigations: any[] = ['a', 'b']; + const page: any = { p: 'q' }; + const template: any = { c: 'd' }; + const state: any = { data: { collections: { selected: 'products' } } }; + const getStore = () => state; + const dispatch = spy(); + const extractQuery = stub(ResponseAdapter, 'extractQuery').returns(query); + const combineNavigations = stub(ResponseAdapter, 'combineNavigations').returns(navigations); + const extractPage = stub(ResponseAdapter, 'extractPage').returns(page); + const extractTemplate = stub(ResponseAdapter, 'extractTemplate').returns(template); + const receiveRedirect = stub(actions, 'receiveRedirect').returns(receiveRedirectAction); + const receiveQuery = stub(actions, 'receiveQuery').returns(receiveQueryAction); + const receiveProducts = stub(actions, 'receiveProducts').returns(receiveProductsAction); + const receiveNavigations = stub(actions, 'receiveNavigations').returns(receiveNavigationsAction); + const receivePage = stub(actions, 'receivePage').returns(receivePageAction); + const receiveTemplate = stub(actions, 'receiveTemplate').returns(receiveTemplateAction); + const receiveCollectionCount = stub(actions, 'receiveCollectionCount').returns(receiveCollectionCountAction); + const thunk = actions.receiveSearchResponse(results); + + thunk(dispatch, getStore); + + expect(receiveRedirect).to.be.calledWith(results.redirect); + expect(dispatch).to.be.calledWith(receiveRedirectAction); + expect(receiveQuery).to.be.calledWith(query); + expect(extractQuery).to.be.calledWith(results, linkMapper); + expect(dispatch).to.be.calledWith(receiveQueryAction); + expect(receiveProducts).to.be.calledWith([{ u: 'v' }, { w: 'x' }], results.totalRecordCount); + expect(dispatch).to.be.calledWith(receiveProductsAction); + expect(receiveNavigations).to.be.calledWith(navigations); + expect(combineNavigations).to.be.calledWith(results.availableNavigation, results.selectedNavigation); + expect(dispatch).to.be.calledWith(receiveNavigationsAction); + expect(receivePage).to.be.calledWith(page); + expect(extractPage).to.be.calledWith(state, results); + expect(dispatch).to.be.calledWith(receivePageAction); + expect(receiveTemplate).to.be.calledWith(template); + expect(extractTemplate).to.be.calledWith(results.template); + expect(dispatch).to.be.calledWith(receiveTemplateAction); + expect(receiveCollectionCount).to.be.calledWith(state.data.collections.selected, results.totalRecordCount); + expect(dispatch).to.be.calledWith(receiveCollectionCountAction); + }); + }); + + describe('receiveQuery()', () => { + it('should create a RECEIVE_QUERY action', () => { + const query: any = { a: 'b' }; + const thunk = stub(utils, 'thunk'); + + actions.receiveQuery(query); + + expect(thunk).to.be.calledWith(Actions.RECEIVE_QUERY, query); + }); + }); + + describe('receiveProducts()', () => { + it('should create a RECEIVE_PRODUCTS action', () => { + const products: any = ['a', 'b']; + const recordCount = 10; + const thunk = stub(utils, 'thunk'); + + actions.receiveProducts(products, recordCount); + + expect(thunk).to.be.calledWith(Actions.RECEIVE_PRODUCTS, { products, recordCount }); + }); + }); + describe('receiveCollectionCount()', () => { + it('should create a RECEIVE_NAVIGATIONS action', () => { + const collection = 'products'; + const count = 10; + const thunk = stub(utils, 'thunk'); + + actions.receiveCollectionCount(collection, count); + + expect(thunk).to.be.calledWith(Actions.RECEIVE_COLLECTION_COUNT, { collection, count }); + }); + }); + + describe('receiveNavigations()', () => { + it('should create a RECEIVE_NAVIGATIONS action', () => { + const navigations: any[] = ['a', 'b']; + const thunk = stub(utils, 'thunk'); + + actions.receiveNavigations(navigations); + + expect(thunk).to.be.calledWith(Actions.RECEIVE_NAVIGATIONS, { navigations }); + }); + }); + + describe('receivePage()', () => { + it('should create a RECEIVE_PAGE action', () => { + const page: any = { a: 'b' }; + const thunk = stub(utils, 'thunk'); + + actions.receivePage(page); + + expect(thunk).to.be.calledWith(Actions.RECEIVE_PAGE, page); + }); + }); + + describe('receiveTemplate()', () => { + it('should create a RECEIVE_PAGE action', () => { + const template: any = { a: 'b' }; + const thunk = stub(utils, 'thunk'); + + actions.receiveTemplate(template); + + expect(thunk).to.be.calledWith(Actions.RECEIVE_TEMPLATE, { template }); + }); + }); + + describe('receiveRedirect()', () => { + it('should create a RECEIVE_PAGE action', () => { + const redirect = 'page.html'; + const thunk = stub(utils, 'thunk'); + + actions.receiveRedirect(redirect); + + expect(thunk).to.be.calledWith(Actions.RECEIVE_REDIRECT, { redirect }); + }); + }); + + describe('receiveMoreRefinements()', () => { + it('should create a RECEIVE_MORE_REFINEMENTS action', () => { + const navigationId = 'brand'; + const refinements: any[] = ['a', 'b']; + const thunk = stub(utils, 'thunk'); + + actions.receiveMoreRefinements(navigationId, refinements); + + expect(thunk).to.be.calledWith(Actions.RECEIVE_MORE_REFINEMENTS, { navigationId, refinements }); + }); + }); + + describe('receiveAutocompleteSuggestions()', () => { + it('should create a RECEIVE_AUTOCOMPLETE_SUGGESTIONS action', () => { + const navigationId = 'brand'; + const suggestions = ['a', 'b']; + const categoryValues = ['c', 'd']; + const thunk = stub(utils, 'thunk'); + + actions.receiveAutocompleteSuggestions(suggestions, categoryValues); + + expect(thunk).to.be.calledWith(Actions.RECEIVE_AUTOCOMPLETE_SUGGESTIONS, { suggestions, categoryValues }); + }); + }); }); }); From 550fa5f27ae1244ee93a965f60dcd41d8b0a5550 Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Tue, 25 Apr 2017 14:51:43 -0400 Subject: [PATCH 41/56] Add tests and action.type --- src/flux/reducers/autocomplete.ts | 2 +- src/flux/reducers/collections.ts | 2 +- src/flux/reducers/details.ts | 2 +- src/flux/reducers/errors.ts | 2 +- src/flux/reducers/navigations.ts | 2 +- src/flux/reducers/page.ts | 2 +- src/flux/reducers/products.ts | 2 +- src/flux/reducers/query.ts | 2 +- src/flux/reducers/record-count.ts | 2 +- src/flux/reducers/redirect.ts | 2 +- src/flux/reducers/sorts.ts | 2 +- src/flux/reducers/template.ts | 2 +- src/flux/reducers/warnings.ts | 2 +- test/tslint.json | 3 +- test/unit/flux/actions.ts | 2 +- test/unit/flux/reducers/autocomplete.ts | 74 ++++++++++++++++ test/unit/flux/reducers/collections.ts | 110 ++++++++++++++++++++++++ 17 files changed, 200 insertions(+), 15 deletions(-) create mode 100644 test/unit/flux/reducers/autocomplete.ts create mode 100644 test/unit/flux/reducers/collections.ts diff --git a/src/flux/reducers/autocomplete.ts b/src/flux/reducers/autocomplete.ts index 3d9e08d..2d8203a 100644 --- a/src/flux/reducers/autocomplete.ts +++ b/src/flux/reducers/autocomplete.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateAutocomplete(state: Store.Autocomplete, action) { - switch (action) { + switch (action.type) { case Actions.UPDATE_AUTOCOMPLETE_QUERY: return { ...state, query: action.query }; case Actions.RECEIVE_AUTOCOMPLETE_SUGGESTIONS: diff --git a/src/flux/reducers/collections.ts b/src/flux/reducers/collections.ts index 9f0ad2e..c38561b 100644 --- a/src/flux/reducers/collections.ts +++ b/src/flux/reducers/collections.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateCollections(state: Store.Indexed.Selectable, action) { - switch (action) { + switch (action.type) { case Actions.SELECT_COLLECTION: return { ...state, selected: action.id }; case Actions.RECEIVE_COLLECTION_COUNT: diff --git a/src/flux/reducers/details.ts b/src/flux/reducers/details.ts index b398104..bf424c7 100644 --- a/src/flux/reducers/details.ts +++ b/src/flux/reducers/details.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateDetails(state: Store.Details, action) { - switch (action) { + switch (action.type) { case Actions.UPDATE_DETAILS_ID: return { ...state, id: action.id }; case Actions.RECEIVE_DETAILS_PRODUCT: diff --git a/src/flux/reducers/errors.ts b/src/flux/reducers/errors.ts index 6089314..32acb5f 100644 --- a/src/flux/reducers/errors.ts +++ b/src/flux/reducers/errors.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateErrors(state, action) { - switch (action) { + switch (action.type) { // case Actions.UPDATE_ERRORS: // return { ...state }; default: diff --git a/src/flux/reducers/navigations.ts b/src/flux/reducers/navigations.ts index aeb4781..d25c8a2 100644 --- a/src/flux/reducers/navigations.ts +++ b/src/flux/reducers/navigations.ts @@ -4,7 +4,7 @@ import Store from '../store'; export default function updateNavigations(state: Store.Indexed, action) { const navigationId = action.navigationId; const refinementIndex = action.index; - switch (action) { + switch (action.type) { case Actions.UPDATE_SEARCH: // TODO: add case for clear if (action.clear) { diff --git a/src/flux/reducers/page.ts b/src/flux/reducers/page.ts index 8c43136..8fb18da 100644 --- a/src/flux/reducers/page.ts +++ b/src/flux/reducers/page.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updatePage(state: Store.Page, action) { - switch (action) { + switch (action.type) { case Actions.UPDATE_SEARCH: case Actions.UPDATE_SORTS: case Actions.SELECT_COLLECTION: diff --git a/src/flux/reducers/products.ts b/src/flux/reducers/products.ts index df42703..145453b 100644 --- a/src/flux/reducers/products.ts +++ b/src/flux/reducers/products.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateProducts(state: Store.Product, action) { - switch (action) { + switch (action.type) { // case Actions.UPDATE_PRODUCTS: // return { ...state }; default: diff --git a/src/flux/reducers/query.ts b/src/flux/reducers/query.ts index 7bcf8a1..1f64b38 100644 --- a/src/flux/reducers/query.ts +++ b/src/flux/reducers/query.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateQuery(state: Store.Query, action) { - switch (action) { + switch (action.type) { case Actions.UPDATE_SEARCH: return { ...state, original: action.query }; case Actions.RECEIVE_QUERY: diff --git a/src/flux/reducers/record-count.ts b/src/flux/reducers/record-count.ts index 53cc3c9..0fa8cd3 100644 --- a/src/flux/reducers/record-count.ts +++ b/src/flux/reducers/record-count.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateRecordCount(state: Store.Page, action) { - switch (action) { + switch (action.type) { case Actions.RECEIVE_PRODUCTS: return action.recordCount; default: diff --git a/src/flux/reducers/redirect.ts b/src/flux/reducers/redirect.ts index 08c8566..ca36c05 100644 --- a/src/flux/reducers/redirect.ts +++ b/src/flux/reducers/redirect.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateRedirect(state, action) { - switch (action) { + switch (action.type) { case Actions.RECEIVE_REDIRECT: return action.redirect; default: diff --git a/src/flux/reducers/sorts.ts b/src/flux/reducers/sorts.ts index 118cdcc..38fedcc 100644 --- a/src/flux/reducers/sorts.ts +++ b/src/flux/reducers/sorts.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateSorts(state: Store.Indexed.Selectable, action) { - switch (action) { + switch (action.type) { case Actions.UPDATE_SORTS: return { ...state, selected: action.id }; default: diff --git a/src/flux/reducers/template.ts b/src/flux/reducers/template.ts index 1b9eabf..cc2736a 100644 --- a/src/flux/reducers/template.ts +++ b/src/flux/reducers/template.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateTemplate(state: Store.Template, action) { - switch (action) { + switch (action.type) { case Actions.RECEIVE_TEMPLATE: return action.template; default: diff --git a/src/flux/reducers/warnings.ts b/src/flux/reducers/warnings.ts index a874f8f..433fc74 100644 --- a/src/flux/reducers/warnings.ts +++ b/src/flux/reducers/warnings.ts @@ -2,7 +2,7 @@ import Actions from '../actions'; import Store from '../store'; export default function updateWarnings(state, action) { - switch (action) { + switch (action.type) { // case Actions.UPDATE_WARNINGS: // return { ...state }; default: diff --git a/test/tslint.json b/test/tslint.json index 64f25d6..0138ebe 100644 --- a/test/tslint.json +++ b/test/tslint.json @@ -2,6 +2,7 @@ "extends": "../tslint.json", "rules": { "no-unused-expression": false, - "no-string-literal": false + "no-string-literal": false, + "object-literal-sort-keys": false } } diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts index 16655f3..47e0389 100644 --- a/test/unit/flux/actions.ts +++ b/test/unit/flux/actions.ts @@ -5,7 +5,7 @@ import Selectors from '../../../src/flux/selectors'; import * as utils from '../../../src/flux/utils'; import suite from '../_suite'; -suite.only('Actions', ({ expect, spy, stub }) => { +suite('Actions', ({ expect, spy, stub }) => { let actions: Actions; const bridge: any = { a: 'b' }; diff --git a/test/unit/flux/reducers/autocomplete.ts b/test/unit/flux/reducers/autocomplete.ts new file mode 100644 index 0000000..849e3fe --- /dev/null +++ b/test/unit/flux/reducers/autocomplete.ts @@ -0,0 +1,74 @@ +import Actions from '../../../../src/flux/actions'; +import autocomplete from '../../../../src/flux/reducers/autocomplete'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite.only('autocomplete', ({ expect }) => { + let actions: Actions; + beforeEach(() => actions = new Actions({}, {})); + + describe('autocompleteUpdate()', () => { + it('should update state on UPDATE_AUTOCOMPLETE_QUERY', () => { + const query = 'brown shoes'; + const category = { field: 'a', values: ['b'] }; + const products = []; + const suggestions = []; + const state: Store.Autocomplete = { + category, + products, + query: 'red shoes', + suggestions, + }; + const newState = { + category, + products, + query, + suggestions, + }; + + const reducer = autocomplete(state, { type: Actions.UPDATE_AUTOCOMPLETE_QUERY, query }); + + expect(reducer).to.eql(newState); + }); + + it('should update state on RECEIVE_AUTOCOMPLETE_SUGGESTIONS', () => { + const query = 'brown shoes'; + const categoryValues = ['a', 'c', 'd']; + const suggestions = ['e', 'f', 'g']; + const products = []; + const state: Store.Autocomplete = { + category: { field: 'a', values: ['b'] }, + products, + query, + suggestions, + }; + const newState = { + category: { field: 'a', values: categoryValues }, + products, + query, + suggestions, + }; + + const reducer = autocomplete(state, { + type: Actions.RECEIVE_AUTOCOMPLETE_SUGGESTIONS, + categoryValues, + suggestions, + }); + + expect(reducer).to.eql(newState); + }); + + it('should return state on default', () => { + const state: Store.Autocomplete = { + category: { field: 'a', values: ['b'] }, + products: [], + query: 'red shoes', + suggestions: [], + }; + + const reducer = autocomplete(state, {}); + + expect(reducer).to.eql(state); + }); + }); +}); diff --git a/test/unit/flux/reducers/collections.ts b/test/unit/flux/reducers/collections.ts new file mode 100644 index 0000000..51cb0b4 --- /dev/null +++ b/test/unit/flux/reducers/collections.ts @@ -0,0 +1,110 @@ +import Actions from '../../../../src/flux/actions'; +import collections from '../../../../src/flux/reducers/collections'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite.only('collections', ({ expect, spy }) => { + let actions: Actions; + beforeEach(() => actions = new Actions({}, {})); + + describe('updateCollections()', () => { + it('should update state on SELECT_COLLECTION', () => { + const allIds = ['Department', 'Main']; + const Department = { + label: 'All content', + name: 'contents', + total: 750, + }; + const Main = { + label: 'Main content', + name: 'mains', + total: 600, + }; + const selected = 'Main'; + const state: Store.Indexed.Selectable = { + allIds, + byId: { + Department, + Main, + }, + selected: 'Department', + }; + const newState = { + allIds, + byId: { + Department, + Main, + }, + selected, + }; + + const reducer = collections(state, { type: Actions.SELECT_COLLECTION, id: selected }); + + expect(reducer).to.eql(newState); + }); + + it('should update state on RECEIVE_AUTOCOMPLETE_SUGGESTIONS', () => { + const allIds = ['Department', 'Main']; + const label = 'All content'; + const name = 'contents'; + const total = 750; + const Main = { + label: 'Main content', + name: 'mains', + total: 600, + }; + const selected = 'Main'; + const state: Store.Indexed.Selectable = { + allIds, + byId: { + Department: { + label, + name, + total: 650, + }, + Main, + }, + selected, + }; + const newState = { + allIds, + byId: { + Department: { label, name, total }, + Main, + }, + selected, + }; + + const reducer = collections(state, { + type: Actions.RECEIVE_COLLECTION_COUNT, + collection: 'Department', + count: total, + }); + + expect(reducer).to.eql(newState); + }); + + it('should return state on default', () => { + const state: Store.Indexed.Selectable = { + allIds: ['Department', 'Main'], + byId: { + Department: { + label: 'All content', + name: 'contents', + total: 650, + }, + Main: { + label: 'Main content', + name: 'mains', + total: 600, + }, + }, + selected: 'Main', + }; + + const reducer = collections(state, {}); + + expect(reducer).to.eql(state); + }); + }); +}); From 8e2c181544915353bc542b724319d0fa65ae77e9 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Tue, 25 Apr 2017 12:38:57 -0400 Subject: [PATCH 42/56] all response action creator tests --- src/flux/actions.ts | 2 +- src/flux/adapters/response.ts | 4 ++-- src/flux/pager.ts | 5 ++--- test/unit/flux/actions.ts | 13 ++++++++++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 7b9d011..bbbee4a 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -69,7 +69,7 @@ class Actions { dispatch(this.receiveProducts(results.records.map((product) => product.allMeta), results.totalRecordCount)); // tslint:disable-next-line max-line-length dispatch(this.receiveNavigations(ResponseAdapter.combineNavigations(results.availableNavigation, results.selectedNavigation))); - dispatch(this.receivePage(ResponseAdapter.extractPage(state, results))); + dispatch(this.receivePage(ResponseAdapter.extractPage(state))); dispatch(this.receiveTemplate(ResponseAdapter.extractTemplate(results.template))); dispatch(this.receiveCollectionCount(state.data.collections.selected, results.totalRecordCount)); } diff --git a/src/flux/adapters/response.ts b/src/flux/adapters/response.ts index 1726e33..2275d82 100644 --- a/src/flux/adapters/response.ts +++ b/src/flux/adapters/response.ts @@ -111,8 +111,8 @@ namespace Response { Object.assign(zones, { [key]: Response.extractZone(template.zones[key]) }), {}), }); - export const extractPage = (store: Store.State, results: Results): Page => - new Pager(store, results).build(); + export const extractPage = (store: Store.State): Page => + new Pager(store).build(); } export default Response; diff --git a/src/flux/pager.ts b/src/flux/pager.ts index 6f123b7..2967575 100644 --- a/src/flux/pager.ts +++ b/src/flux/pager.ts @@ -8,7 +8,7 @@ const MAX_RECORDS = 10000; export class Pager { - constructor(private state: Store.State, private results: Results) { } + constructor(private state: Store.State) { } previousPage(currentPage: number) { return currentPage > 1 ? currentPage - 1 : null; @@ -40,7 +40,7 @@ export class Pager { // TODO move this default into the reducer setup const pageSize = this.state.data.page.size || 10; const currentPage = this.state.data.page.current; - const totalRecords = this.results.totalRecordCount; + const totalRecords = this.state.data.recordCount; const finalPage = this.finalPage(pageSize, totalRecords); return { @@ -50,7 +50,6 @@ export class Pager { previous: this.previousPage(currentPage), range: this.pageNumbers(currentPage, finalPage, this.state.data.page.limit), to: this.toResult(currentPage, pageSize, totalRecords), - total: totalRecords, }; } diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts index 47e0389..d71ce16 100644 --- a/test/unit/flux/actions.ts +++ b/test/unit/flux/actions.ts @@ -204,7 +204,7 @@ suite('Actions', ({ expect, spy, stub }) => { expect(combineNavigations).to.be.calledWith(results.availableNavigation, results.selectedNavigation); expect(dispatch).to.be.calledWith(receiveNavigationsAction); expect(receivePage).to.be.calledWith(page); - expect(extractPage).to.be.calledWith(state, results); + expect(extractPage).to.be.calledWith(state); expect(dispatch).to.be.calledWith(receivePageAction); expect(receiveTemplate).to.be.calledWith(template); expect(extractTemplate).to.be.calledWith(results.template); @@ -317,5 +317,16 @@ suite('Actions', ({ expect, spy, stub }) => { expect(thunk).to.be.calledWith(Actions.RECEIVE_AUTOCOMPLETE_SUGGESTIONS, { suggestions, categoryValues }); }); }); + + describe('receiveDetailsProduct()', () => { + it('should create a RECEIVE_DETAILS_PRODUCT action', () => { + const product: any = { a: 'b' }; + const thunk = stub(utils, 'thunk'); + + actions.receiveDetailsProduct(product); + + expect(thunk).to.be.calledWith(Actions.RECEIVE_DETAILS_PRODUCT, { product }); + }); + }); }); }); From 40fdb1e47514189c3a926e5c8e8a48d8231eab0f Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Tue, 25 Apr 2017 15:25:28 -0400 Subject: [PATCH 43/56] more tests --- package.json | 11 ++--- src/flux/actions.ts | 8 ++-- src/polyfills.ts | 13 ++++-- test/bootstrap.ts | 1 + test/tsconfig.json | 6 +++ test/unit/_suite.ts | 7 +-- test/unit/flux/actions.ts | 68 ++++++++++++++++++++++++++++ test/utils/sinon-as-promised.ts | 16 +++++++ typings.json | 3 +- webpack.config.js | 4 +- yarn.lock | 79 ++++++++++++++++++--------------- 11 files changed, 161 insertions(+), 55 deletions(-) create mode 100644 test/tsconfig.json create mode 100644 test/utils/sinon-as-promised.ts diff --git a/package.json b/package.json index b037a79..6ce3174 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "homepage": "https://github.com/groupby/api-javascript#readme", "devDependencies": { "@types/node": "^7.0.13", + "@types/sinon": "^2.1.3", "awesome-typescript-loader": "^3.1.2", "chai": "^3.2.0", "codacy-coverage": "^2.0.2", @@ -48,29 +49,29 @@ "istanbul": "^0.4.5", "karma": "^1.6.0", "karma-coverage": "^1.1.1", - "karma-mocha": "^1.0.1", + "karma-mocha": "^1.3.0", "karma-mocha-reporter": "^2.2.3", "karma-phantomjs-launcher": "^1.0.4", "karma-sinon-chai": "^1.3.1", "karma-source-map-support": "^1.2.0", "karma-webpack": "^2.0.3", - "mocha": "^3.2.0", + "mocha": "^3.3.0", "mocha-suite": "^1.0.8", "object-assign": "^4.1.0", "phantomjs-prebuilt": "^2.1.7", "remap-istanbul": "^0.9.5", - "rimraf": "^2.5.4", + "rimraf": "^2.6.1", "sinon": "^2.1.0", "sinon-chai": "^2.9.0", "sourcemap-istanbul-instrumenter-loader": "^0.2.0", "tslint": "^5.1.0", "tslint-eslint-rules": "^4.0.0", - "tslint-loader": "^3.5.2", + "tslint-loader": "^3.5.3", "typedoc": "^0.5.10", "typescript": "^2.2.2", "typings": "^2.1.1", "webpack": "^2.4.1", - "xhr-mock": "^1.6.0" + "xhr-mock": "^1.8.0" }, "dependencies": { "@types/axios": "^0.9.35", diff --git a/src/flux/actions.ts b/src/flux/actions.ts index bbbee4a..dabcf0d 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -19,9 +19,11 @@ class Actions { (dispatch: Dispatch, getStore: () => Store.State) => { const state = getStore(); if (Selectors.hasMoreRefinements(state, navigationId)) { - this.bridge.refinements(Selectors.searchRequest(state), navigationId) - .then(({ navigation: { name, refinements } }) => - dispatch(this.receiveMoreRefinements(name, refinements.map(ResponseAdapter.extractRefinement)))); + return this.bridge.refinements(Selectors.searchRequest(state), navigationId) + .then(({ navigation: { name, refinements } }) => { + const remapped = refinements.map(ResponseAdapter.extractRefinement); + return dispatch(this.receiveMoreRefinements(name, remapped)); + }); } } diff --git a/src/polyfills.ts b/src/polyfills.ts index baa69a2..bc9dc64 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,5 +1,10 @@ +import * as arrayFind from 'array.prototype.find'; +import * as arrayFindIndex from 'array.prototype.findIndex'; +import * as objectAssign from 'es6-object-assign'; +import * as promise from 'es6-promise'; import 'es6-symbol/implement'; -require('array.prototype.find').shim(); -require('array.prototype.findindex').shim(); -require('es6-object-assign').polyfill(); -require('es6-promise').polyfill(); + +arrayFind.shim(); +arrayFindIndex.shim(); +objectAssign.polyfill(); +promise.polyfill(); diff --git a/test/bootstrap.ts b/test/bootstrap.ts index 92cd316..3b95b1a 100644 --- a/test/bootstrap.ts +++ b/test/bootstrap.ts @@ -1,5 +1,6 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import '../src/polyfills'; +import './utils/sinon-as-promised'; chai.use(sinonChai); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..ac04585 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig", + "compilerOptions": { + "types": ["sinon"] + } +} diff --git a/test/unit/_suite.ts b/test/unit/_suite.ts index dfc530c..0245617 100644 --- a/test/unit/_suite.ts +++ b/test/unit/_suite.ts @@ -1,8 +1,9 @@ import { expect } from 'chai'; import * as suite from 'mocha-suite'; +import * as sinon from 'sinon'; export default suite((tests) => { - let sandbox: Sinon.SinonSandbox; + let sandbox: sinon.SinonSandbox; beforeEach(() => sandbox = sinon.sandbox.create()); afterEach(() => sandbox.restore()); @@ -16,8 +17,8 @@ export default suite((tests) => { export interface Utils { expect: Chai.ExpectStatic; - spy: Sinon.SinonSpyStatic; - stub: Sinon.SinonStubStatic; + spy: sinon.SinonSpyStatic; + stub: sinon.SinonStubStatic; } export type Suite = UtilsSuite & { diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts index d71ce16..77e8903 100644 --- a/test/unit/flux/actions.ts +++ b/test/unit/flux/actions.ts @@ -1,3 +1,4 @@ +import * as sinon from 'sinon'; import { BrowserBridge } from '../../../src/core/bridge'; import Actions from '../../../src/flux/actions'; import ResponseAdapter from '../../../src/flux/adapters/response'; @@ -18,6 +19,73 @@ suite('Actions', ({ expect, spy, stub }) => { }); }); + describe('fetch action creators', () => { + describe('fetchMoreRefinements()', () => { + it('should return a thunk', () => { + const thunk = actions.fetchMoreRefinements('brand'); + + expect(thunk).to.be.a('function'); + }); + + it('should not fetch if more refinements not available', () => { + const navigationId = 'brand'; + const state = { a: 'b' }; + const dispatch = spy(); + const getStore = spy(() => state); + const hasMoreRefinements = stub(Selectors, 'hasMoreRefinements').returns(false); + const action = actions.fetchMoreRefinements(navigationId); + + action(dispatch, getStore); + + expect(getStore).to.be.called; + expect(hasMoreRefinements).to.be.calledWith(state, navigationId); + expect(dispatch).to.not.be.called; + }); + + it('should fetch more refinements', (done) => { + const name = 'brand'; + const state = { a: 'b' }; + const search = { e: 'f' }; + const action = actions.fetchMoreRefinements(name); + const refinementsStub = actions['bridge'].refinements + = stub().resolves({ navigation: { name, refinements: ['c', 'd'] } }); + const searchRequest = stub(Selectors, 'searchRequest').returns(search); + stub(Selectors, 'hasMoreRefinements').returns(true); + stub(actions, 'receiveMoreRefinements'); + stub(ResponseAdapter, 'extractRefinement').callsFake((s) => s); + + const builtAction = action(() => null, () => state) + .then(() => { + expect(searchRequest).to.be.calledWith(state); + expect(refinementsStub).to.be.calledWith(search, name); + done(); + }); + }); + + it('should store more refinements result', (done) => { + const name = 'brand'; + const state = { a: 'b' }; + const moreRefinementsAction = { e: 'f' }; + const action = actions.fetchMoreRefinements(name); + const dispatch = spy(); + const extractRefinement = stub(ResponseAdapter, 'extractRefinement').callsFake((value) => value); + const receiveMoreRefinements = stub(actions, 'receiveMoreRefinements').returns(moreRefinementsAction); + actions['bridge'].refinements = stub().resolves({ navigation: { name, refinements: ['c', 'd'] } }); + stub(Selectors, 'hasMoreRefinements').returns(true); + stub(Selectors, 'searchRequest'); + + const builtAction = action(dispatch, () => state) + .then(() => { + expect(extractRefinement).to.be.calledWith('c'); + expect(extractRefinement).to.be.calledWith('d'); + expect(receiveMoreRefinements).to.be.calledWith(name, ['c', 'd']); + expect(dispatch).to.be.calledWith(moreRefinementsAction); + done(); + }); + }); + }); + }); + describe('request action creators', () => { describe('updateSearch()', () => { it('should create an UPDATE_SEARCH action', () => { diff --git a/test/utils/sinon-as-promised.ts b/test/utils/sinon-as-promised.ts new file mode 100644 index 0000000..4c24702 --- /dev/null +++ b/test/utils/sinon-as-promised.ts @@ -0,0 +1,16 @@ +import * as sinon from 'sinon'; + +export function resolves(value: any) { + return this.returns(Promise.resolve(value)); +} + +sinon.stub['resolves'] = resolves; + +export function rejects(err: any) { + if (typeof err === 'string') { + err = new Error(err); + } + return this.returns(Promise.reject(err)); +} + +sinon.stub['rejects'] = rejects; diff --git a/typings.json b/typings.json index f947b70..5c6f013 100644 --- a/typings.json +++ b/typings.json @@ -6,8 +6,7 @@ "filter-object": "registry:npm/filter-object#2.1.0+20160816021609" }, "globalDevDependencies": { - "chai": "registry:dt/chai#3.4.0+20160601211834", - "sinon": "registry:dt/sinon#1.16.1+20161208163514" + "chai": "registry:dt/chai#3.4.0+20160601211834" }, "devDependencies": { "sinon-chai": "registry:dt/sinon-chai#2.7.0+20160628004037", diff --git a/webpack.config.js b/webpack.config.js index 1cdaef1..0fd3947 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -38,8 +38,8 @@ module.exports = { exclude: path.resolve(__dirname, 'node_modules'), loader: 'awesome-typescript-loader', options: { - inlineSourceMap: true, - sourceMap: true + sourceMap: false, + inlineSourceMap: true } }) } diff --git a/yarn.lock b/yarn.lock index c36962a..b52fcbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -58,6 +58,10 @@ dependencies: "@types/node" "*" +"@types/sinon@^2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-2.1.3.tgz#90e7b02348455e0d5781d1467a8a61b9bd91a468" + abbrev@1, abbrev@1.0.x: version "1.0.9" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" @@ -891,7 +895,7 @@ debug@2, debug@2.6.3: dependencies: ms "0.7.2" -debug@2.2.0, debug@^2.2.0, debug@~2.2.0: +debug@2.2.0, debug@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" dependencies: @@ -903,6 +907,12 @@ debug@2.3.3: dependencies: ms "0.7.2" +debug@2.6.0, debug@^2.2.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" + dependencies: + ms "0.7.2" + debug@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351" @@ -982,11 +992,7 @@ di@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" -diff@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" - -diff@^3.1.0, diff@^3.2.0: +diff@3.2.0, diff@^3.1.0, diff@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" @@ -1543,9 +1549,9 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob@7.0.5: - version "7.0.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" +glob@7.1.1, glob@^7.0.0, glob@^7.0.5, glob@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -1574,17 +1580,6 @@ glob@^6.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.5, glob@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - global@^4.3.0: version "4.3.1" resolved "https://registry.yarnpkg.com/global/-/global-4.3.1.tgz#5f757908c7cbabce54f386ae440e11e26b7916df" @@ -2210,7 +2205,7 @@ karma-mocha-reporter@^2.2.3: dependencies: chalk "1.1.3" -karma-mocha@^1.0.1: +karma-mocha@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-1.3.0.tgz#eeaac7ffc0e201eb63c467440d2b69c7cf3778bf" dependencies: @@ -2686,16 +2681,16 @@ mocha-suite@^1.0.8: dependencies: "@types/mocha" "^2.2.40" -mocha@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.2.0.tgz#7dc4f45e5088075171a68896814e6ae9eb7a85e3" +mocha@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.3.0.tgz#d29b7428d3f52c82e2e65df1ecb7064e1aabbfb5" dependencies: browser-stdout "1.3.0" commander "2.9.0" - debug "2.2.0" - diff "1.4.0" + debug "2.6.0" + diff "3.2.0" escape-string-regexp "1.0.5" - glob "7.0.5" + glob "7.1.1" growl "1.9.2" json3 "3.3.2" lodash.create "3.1.1" @@ -3166,6 +3161,10 @@ querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" +querystringify@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" + randomatic@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" @@ -3421,7 +3420,7 @@ require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" -requires-port@1.x.x: +requires-port@1.0.x, requires-port@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -3448,13 +3447,13 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.4.4, rimraf@^2.6.0: +rimraf@2, rimraf@^2.4.4, rimraf@^2.6.0, rimraf@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: glob "^7.0.5" -rimraf@^2.5.4, rimraf@~2.5.1, rimraf@~2.5.4: +rimraf@~2.5.1, rimraf@~2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: @@ -3916,9 +3915,9 @@ tslint-eslint-rules@^4.0.0: tslib "^1.0.0" tsutils "^1.4.0" -tslint-loader@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/tslint-loader/-/tslint-loader-3.5.2.tgz#ef2f512c05a96fb1a22d94c32d22fb7975ef9917" +tslint-loader@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/tslint-loader/-/tslint-loader-3.5.3.tgz#343f74122d94f356b689457d3f59f64a69ab606f" dependencies: loader-utils "^1.0.2" mkdirp "^0.5.1" @@ -4130,6 +4129,13 @@ url-parse-lax@^1.0.0: dependencies: prepend-http "^1.0.1" +url-parse@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.8.tgz#7a65b3a8d57a1e86af6b4e2276e34774167c0156" + dependencies: + querystringify "0.0.x" + requires-port "1.0.x" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -4325,11 +4331,12 @@ xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" -xhr-mock@^1.6.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/xhr-mock/-/xhr-mock-1.7.0.tgz#c3749f2c68d34ee88af7af5b84ac50764c604351" +xhr-mock@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/xhr-mock/-/xhr-mock-1.8.0.tgz#306c0e9eeba2654c7b25d43450b4224c45b97cbe" dependencies: global "^4.3.0" + url-parse "^1.1.7" xmlhttprequest-ssl@1.5.3: version "1.5.3" From 7514ea94731751c0a0397908aa30e4b356102c1f Mon Sep 17 00:00:00 2001 From: Victoria Johns Date: Tue, 25 Apr 2017 17:18:06 -0400 Subject: [PATCH 44/56] Add tests reducers --- src/flux/reducers/navigations.ts | 28 ++--- test/unit/flux/reducers/autocomplete.ts | 45 +++----- test/unit/flux/reducers/collections.ts | 92 +++++----------- test/unit/flux/reducers/details.ts | 55 ++++++++++ test/unit/flux/reducers/error.ts | 0 test/unit/flux/reducers/navigations.ts | 137 ++++++++++++++++++++++++ test/unit/flux/reducers/page.ts | 135 +++++++++++++++++++++++ 7 files changed, 386 insertions(+), 106 deletions(-) create mode 100644 test/unit/flux/reducers/details.ts create mode 100644 test/unit/flux/reducers/error.ts create mode 100644 test/unit/flux/reducers/navigations.ts create mode 100644 test/unit/flux/reducers/page.ts diff --git a/src/flux/reducers/navigations.ts b/src/flux/reducers/navigations.ts index d25c8a2..594b4bf 100644 --- a/src/flux/reducers/navigations.ts +++ b/src/flux/reducers/navigations.ts @@ -11,7 +11,7 @@ export default function updateNavigations(state: Store.Indexed const byIds = state.allIds.reduce( (newById, index) => Object.assign(newById, { [index]: { ...state.byId[index], selected: [] } }), { }, ); - if (!(navigationId && action.index)) { + if (!(navigationId && refinementIndex != null)) { return { ...state, byId: byIds, @@ -22,16 +22,16 @@ export default function updateNavigations(state: Store.Indexed byId: { ...byIds, [navigationId]: { - ...state.byId[refinementIndex], + ...state.byId[navigationId], // TODO: maybe check if already there - selected: refinementIndex, + selected: [refinementIndex], }, }, }; } } case Actions.SELECT_REFINEMENT: - if (navigationId && refinementIndex) { + if (navigationId && refinementIndex != null) { return { ...state, byId: { @@ -47,16 +47,18 @@ export default function updateNavigations(state: Store.Indexed return state; } case Actions.DESELECT_REFINEMENT: - return { - ...state, - byId: { - ...state.byId, - [navigationId]: { - ...state.byId[navigationId], - selected: state.byId[navigationId].selected.filter((index) => index !== refinementIndex), + if (navigationId && refinementIndex != null) { + return { + ...state, + byId: { + ...state.byId, + [navigationId]: { + ...state.byId[navigationId], + selected: state.byId[navigationId].selected.filter((index) => index !== refinementIndex), + }, }, - }, - }; + }; + } default: return state; } diff --git a/test/unit/flux/reducers/autocomplete.ts b/test/unit/flux/reducers/autocomplete.ts index 849e3fe..2ea2798 100644 --- a/test/unit/flux/reducers/autocomplete.ts +++ b/test/unit/flux/reducers/autocomplete.ts @@ -5,45 +5,37 @@ import suite from '../../_suite'; suite.only('autocomplete', ({ expect }) => { let actions: Actions; + const query = 'brown shoes'; + const category = { field: 'a', values: ['b'] }; + const suggestions = ['e', 'f', 'g']; + const products = []; + const state: Store.Autocomplete = { + category, + products, + query, + suggestions, + }; beforeEach(() => actions = new Actions({}, {})); describe('autocompleteUpdate()', () => { - it('should update state on UPDATE_AUTOCOMPLETE_QUERY', () => { - const query = 'brown shoes'; - const category = { field: 'a', values: ['b'] }; - const products = []; - const suggestions = []; - const state: Store.Autocomplete = { - category, - products, - query: 'red shoes', - suggestions, - }; + it('should update query state on UPDATE_AUTOCOMPLETE_QUERY', () => { + const newQuery = 'red shoes'; const newState = { category, products, - query, + query: newQuery, suggestions, }; - const reducer = autocomplete(state, { type: Actions.UPDATE_AUTOCOMPLETE_QUERY, query }); + const reducer = autocomplete(state, { type: Actions.UPDATE_AUTOCOMPLETE_QUERY, query: newQuery }); expect(reducer).to.eql(newState); }); it('should update state on RECEIVE_AUTOCOMPLETE_SUGGESTIONS', () => { - const query = 'brown shoes'; const categoryValues = ['a', 'c', 'd']; - const suggestions = ['e', 'f', 'g']; - const products = []; - const state: Store.Autocomplete = { - category: { field: 'a', values: ['b'] }, - products, - query, - suggestions, - }; const newState = { - category: { field: 'a', values: categoryValues }, + category: { ...category, values: categoryValues }, products, query, suggestions, @@ -59,13 +51,6 @@ suite.only('autocomplete', ({ expect }) => { }); it('should return state on default', () => { - const state: Store.Autocomplete = { - category: { field: 'a', values: ['b'] }, - products: [], - query: 'red shoes', - suggestions: [], - }; - const reducer = autocomplete(state, {}); expect(reducer).to.eql(state); diff --git a/test/unit/flux/reducers/collections.ts b/test/unit/flux/reducers/collections.ts index 51cb0b4..a73450b 100644 --- a/test/unit/flux/reducers/collections.ts +++ b/test/unit/flux/reducers/collections.ts @@ -3,81 +3,64 @@ import collections from '../../../../src/flux/reducers/collections'; import Store from '../../../../src/flux/store'; import suite from '../../_suite'; -suite.only('collections', ({ expect, spy }) => { +suite.only('collections', ({ expect }) => { let actions: Actions; + const allIds = ['Department', 'Main']; + const Department = { + label: 'All content', + name: 'contents', + total: 750, + }; + const Main = { + label: 'Main content', + name: 'mains', + total: 600, + }; + const selected = 'Main'; + const state: Store.Indexed.Selectable = { + allIds, + byId: { + Department, + Main, + }, + selected, + }; beforeEach(() => actions = new Actions({}, {})); describe('updateCollections()', () => { it('should update state on SELECT_COLLECTION', () => { - const allIds = ['Department', 'Main']; - const Department = { - label: 'All content', - name: 'contents', - total: 750, - }; - const Main = { - label: 'Main content', - name: 'mains', - total: 600, - }; - const selected = 'Main'; - const state: Store.Indexed.Selectable = { - allIds, - byId: { - Department, - Main, - }, - selected: 'Department', - }; + const selectedCollection = 'Department'; const newState = { allIds, byId: { Department, Main, }, - selected, + selected: selectedCollection, }; - const reducer = collections(state, { type: Actions.SELECT_COLLECTION, id: selected }); + const reducer = collections(state, { type: Actions.SELECT_COLLECTION, id: selectedCollection }); expect(reducer).to.eql(newState); }); it('should update state on RECEIVE_AUTOCOMPLETE_SUGGESTIONS', () => { - const allIds = ['Department', 'Main']; - const label = 'All content'; - const name = 'contents'; - const total = 750; - const Main = { - label: 'Main content', - name: 'mains', - total: 600, - }; - const selected = 'Main'; - const state: Store.Indexed.Selectable = { + const total = 700; + const newState = { allIds, byId: { Department: { - label, - name, - total: 650, + ...Department, + total, }, Main, }, selected, }; - const newState = { - allIds, - byId: { - Department: { label, name, total }, - Main, - }, - selected, - }; const reducer = collections(state, { type: Actions.RECEIVE_COLLECTION_COUNT, - collection: 'Department', + collection: allIds[0], count: total, }); @@ -85,23 +68,6 @@ suite.only('collections', ({ expect, spy }) => { }); it('should return state on default', () => { - const state: Store.Indexed.Selectable = { - allIds: ['Department', 'Main'], - byId: { - Department: { - label: 'All content', - name: 'contents', - total: 650, - }, - Main: { - label: 'Main content', - name: 'mains', - total: 600, - }, - }, - selected: 'Main', - }; - const reducer = collections(state, {}); expect(reducer).to.eql(state); diff --git a/test/unit/flux/reducers/details.ts b/test/unit/flux/reducers/details.ts new file mode 100644 index 0000000..34fe658 --- /dev/null +++ b/test/unit/flux/reducers/details.ts @@ -0,0 +1,55 @@ +import Actions from '../../../../src/flux/actions'; +import details from '../../../../src/flux/reducers/details'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite.only('details', ({ expect }) => { + let actions: Actions; + const id = '19283'; + const product = { + id: '19293', + price: 20, + name: 'toy', + }; + const product2 = { + id: '13928', + price: 53, + name: 'pajamas', + }; + const state: Store.Details = { + id, + product, + }; + + beforeEach(() => actions = new Actions({}, {})); + + describe('updateDetails()', () => { + it('should update state on UPDATE_DETAILS_ID', () => { + const newState = { + id: product.id, + product, + }; + + const reducer = details(state, { type: Actions.UPDATE_DETAILS_ID, id: product.id }); + + expect(reducer).to.eql(newState); + }); + + it('should update state on RECEIVE_DETAILS_PRODUCT', () => { + const newState = { + id, + product: product2, + }; + + const reducer = details(state, { type: Actions.RECEIVE_DETAILS_PRODUCT, product: product2 }); + + expect(reducer).to.eql(newState); + }); + + it('should return state on default', () => { + const reducer = details(state, {}); + + expect(reducer).to.eql(state); + }); + }); +}); diff --git a/test/unit/flux/reducers/error.ts b/test/unit/flux/reducers/error.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/flux/reducers/navigations.ts b/test/unit/flux/reducers/navigations.ts new file mode 100644 index 0000000..231179e --- /dev/null +++ b/test/unit/flux/reducers/navigations.ts @@ -0,0 +1,137 @@ +import Actions from '../../../../src/flux/actions'; +import navigations from '../../../../src/flux/reducers/navigations'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite.only('navigations', ({ expect }) => { + let actions: Actions; + const allIds = ['Format', 'Section']; + const Format = { + field: 'format', + label: 'Format', + more: true, + or: true, + selected: [0, 2], + refinements: [ + { value: 'Hardcover', total: 200 }, + { value: 'Paper', total: 129 }, + { value: 'Audio Book', total: 293 }, + ], + }; + const Section = { + field: 'section', + label: 'Section', + more: true, + or: false, + selected: [3], + refinements: [ + { value: 'Books', total: 203 }, + { value: 'Gifts', total: 1231 }, + { value: 'Toys', total: 231 }, + { value: 'Teens', total: 193 }, + ], + }; + const state: Store.Indexed = { + allIds, + byId: { + Format, + Section, + }, + }; + beforeEach(() => actions = new Actions({}, {})); + + describe('updateNavigations()', () => { + it('should clear selected refinements state on UPDATE_SEARCH', () => { + const newState = { + allIds, + byId: { + Format: { + ...Format, + selected: [], + }, + Section: { + ...Section, + selected: [], + }, + }, + }; + + const reducer = navigations(state, { type: Actions.UPDATE_SEARCH, clear: true }); + + expect(reducer).to.eql(newState); + }); + + it('should clear and add selected refinement state on UPDATE_SEARCH', () => { + const newState = { + allIds, + byId: { + Format: { + ...Format, + selected: [0], + }, + Section: { + ...Section, + selected: [], + }, + }, + }; + + const reducer = navigations(state, { + type: Actions.UPDATE_SEARCH, + clear: true, + navigationId: 'Format', + index: 0, + }); + + expect(reducer).to.eql(newState); + }); + + it('should add selected refinement state on SELECT_REFINEMENT', () => { + const newState = { + allIds, + byId: { + Format, + Section: { + ...Section, + selected: [3, 0], + }, + }, + }; + + const reducer = navigations(state, { + type: Actions.SELECT_REFINEMENT, + navigationId: 'Section', + index: 0, + }); + + expect(reducer).to.eql(newState); + }); + + it('should remove selected refinement state on DESELECT_REFINEMENT', () => { + const newState = { + allIds, + byId: { + Format: { + ...Format, + selected: [2], + }, + Section, + }, + }; + + const reducer = navigations(state, { + type: Actions.DESELECT_REFINEMENT, + navigationId: 'Format', + index: 0, + }); + + expect(reducer).to.eql(newState); + }); + + it('should return state on default', () => { + const reducer = navigations(state, {}); + + expect(reducer).to.eql(state); + }); + }); +}); diff --git a/test/unit/flux/reducers/page.ts b/test/unit/flux/reducers/page.ts new file mode 100644 index 0000000..8f08905 --- /dev/null +++ b/test/unit/flux/reducers/page.ts @@ -0,0 +1,135 @@ +import Actions from '../../../../src/flux/actions'; +import page from '../../../../src/flux/reducers/page'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite.only('page', ({ expect }) => { + let actions: Actions; + const size: 10; + const current: 3; + const limit: 5; + const previous: 2; + const next: 4; + const last: 39; + const from: 21; + const to: 30; + const range: [1, 2, 3, 4, 5]; + const state: Store.Page = { + size, current, limit, previous, next, last, from, to, range, + }; + beforeEach(() => actions = new Actions({}, {})); + + describe('updatePage()', () => { + it('should clear selected refinements state on UPDATE_SEARCH', () => { + // const newState = { + // + // }; + // + // const reducer = navigations(state, { type: Actions.UPDATE_SEARCH, clear: true }); + // + // expect(reducer).to.eql(newState); + }); + // + // it('should clear and add selected refinement state on UPDATE_SEARCH', () => { + // const state: Store.Indexed = { + // allIds, + // byId: { + // Format, + // Section, + // }, + // }; + // const newState = { + // allIds, + // byId: { + // Format: { + // ...Format, + // selected: [0], + // }, + // Section: { + // ...Section, + // selected: [], + // }, + // }, + // }; + // + // const reducer = navigations(state, { + // type: Actions.UPDATE_SEARCH, + // clear: true, + // navigationId: 'Format', + // index: 0, + // }); + // + // expect(reducer).to.eql(newState); + // }); + // + // it('should add selected refinement state on SELECT_REFINEMENT', () => { + // const state: Store.Indexed = { + // allIds, + // byId: { + // Format, + // Section, + // }, + // }; + // const newState = { + // allIds, + // byId: { + // Format, + // Section: { + // ...Section, + // selected: [3, 0], + // }, + // }, + // }; + // + // const reducer = navigations(state, { + // type: Actions.SELECT_REFINEMENT, + // navigationId: 'Section', + // index: 0, + // }); + // + // expect(reducer).to.eql(newState); + // }); + // + // it('should remove selected refinement state on DESELECT_REFINEMENT', () => { + // const state: Store.Indexed = { + // allIds, + // byId: { + // Format, + // Section, + // }, + // }; + // const newState = { + // allIds, + // byId: { + // Format, + // Section: { + // ...Section, + // selected: [], + // }, + // }, + // }; + // + // const reducer = navigations(state, { + // type: Actions.DESELECT_REFINEMENT, + // navigationId: 'Section', + // index: 3, + // }); + // + // expect(reducer).to.eql(newState); + // }); + // + // it('should return state on default', () => { + // const state: Store.Indexed = { + // allIds, + // byId: { + // Format, + // Section, + // }, + // }; + // + // const reducer = navigations(state, {}); + // + // expect(reducer).to.eql(state); + // }); + }); +}); From 304ee1cbaa236832430250e8eedb4319061bbc8c Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Wed, 26 Apr 2017 01:18:55 -0400 Subject: [PATCH 45/56] pagerv2 --- src/flux/pagerV2.ts | 95 +++++++++++++++++++ test/unit/flux/pagerV2.ts | 189 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 src/flux/pagerV2.ts create mode 100644 test/unit/flux/pagerV2.ts diff --git a/src/flux/pagerV2.ts b/src/flux/pagerV2.ts new file mode 100644 index 0000000..a606bfe --- /dev/null +++ b/src/flux/pagerV2.ts @@ -0,0 +1,95 @@ +import { Results } from '../models/response'; +import { Page } from './actions'; +import { Events, FluxCapacitor } from './capacitor'; +import Store from './store'; +import range = require('lodash.range'); + +const MAX_RECORDS = 10000; + +export class Pager { + + constructor(private state: Store.State) { } + + previousPage(currentPage: number) { + return currentPage > 1 ? currentPage - 1 : null; + } + + nextPage(currentPage: number, finalPage: number) { + return (currentPage + 1 <= finalPage) ? currentPage + 1 : null; + } + + finalPage(pageSize: number, totalRecords: number) { + return Math.max(this.getPage(pageSize, this.restrictTotalRecords(pageSize, totalRecords)), 1); + } + + fromResult(currentPage: number, pageSize: number) { + return currentPage * pageSize + 1; + // TODO move the default value into reducer setup + // return this.flux.query.build().skip + 1 || 1; + } + + toResult(currentPage: number, pageSize: number, totalRecords: number) { + if ((currentPage * pageSize) > totalRecords) { + return ((currentPage - 1) * pageSize) + (totalRecords % currentPage); + } else { + return currentPage * pageSize; + } + } + + build(): Page { + // TODO move this default into the reducer setup + const pageSize = this.state.data.page.size || 10; + const currentPage = this.state.data.page.current; + const totalRecords = this.state.data.recordCount; + const last = this.finalPage(pageSize, totalRecords); + + return { + from: this.fromResult(currentPage, pageSize), + last, + next: this.nextPage(currentPage, last), + previous: this.previousPage(currentPage), + range: this.pageNumbers(currentPage, last, this.state.data.page.limit), + to: this.toResult(currentPage, pageSize, totalRecords), + }; + } + + pageNumbers(currentPage: number, finalPage: number, limit: number) { + return range(1, Math.min(finalPage + 1, limit + 1)) + .map(this.transformPages(currentPage, finalPage, limit)); + } + + restrictTotalRecords(pageSize: number, totalRecords: number) { + if (totalRecords > MAX_RECORDS) { + return MAX_RECORDS - (MAX_RECORDS % pageSize); + } else if ((totalRecords + pageSize) > MAX_RECORDS) { + if (MAX_RECORDS % pageSize === 0) { + return MAX_RECORDS; + } else { + return totalRecords - (totalRecords % pageSize); + } + } else { + return totalRecords; + } + } + + getPage(pageSize: number, totalRecords: number) { + return Math.ceil(totalRecords / pageSize); + } + + transformPages(currentPage: number, finalPage: number, limit: number) { + const border = Math.ceil(limit / 2); + return (value: number) => { + // account for 0-indexed pages + if (currentPage <= border || limit > finalPage) { + // pages start at beginning + return value; + } else if (currentPage > finalPage - border) { + // pages start and end in the middle + return value + finalPage - limit; + } else { + // pages end at last page + return value + currentPage - border; + } + }; + } +} diff --git a/test/unit/flux/pagerV2.ts b/test/unit/flux/pagerV2.ts new file mode 100644 index 0000000..379725b --- /dev/null +++ b/test/unit/flux/pagerV2.ts @@ -0,0 +1,189 @@ +import { Pager } from '../../../src/flux/pager'; +import { Events, FluxCapacitor, Query } from '../../../src/index'; +import suite from '../_suite'; + +// suite('Pager', ({ expect, stub }) => { +// const STATE = {}; +// let pager: Pager; +// +// beforeEach(() => pager = new Pager(STATE)); +// +// describe('constructor', () => { +// it('should set state', () => { +// expect(pager['state']).to.eq(STATE); +// }); +// }); +// +// describe('previousPage()', () => { +// it('should return previous page', () => { +// expect(pager.previousPage(2)).to.eq(1); +// expect(pager.previousPage(309)).to.eq(308); +// }); +// +// it('should return null', () => { +// expect(pager.previousPage(1)).to.be.null; +// }); +// }); +// +// describe('nextPage()', () => { +// it('should return next page', () => { +// expect(pager.nextPage(2, 3)).to.eq(3); +// expect(pager.nextPage(18, 40)).to.eq(19); +// }); +// +// it('should return null', () => { +// expect(pager.nextPage(2, 2)).to.be.null; +// }); +// }); +// +// describe('finalPage()', () => { +// it('should return final page', () => { +// const totalRecords = 423; +// const restrictedTotal = 300; +// const pageSize = 20; +// const page = 7; +// const getPage = stub(pager, 'getPage').returns(page); +// const restrictTotalRecords = stub(pager, 'restrictTotalRecords').returns(restrictedTotal); +// +// const finalPage = pager.finalPage(pageSize, totalRecords); +// +// expect(finalPage).to.eq(page); +// expect(restrictTotalRecords).to.be.calledWith(pageSize, totalRecords); +// expect(getPage).to.be.calledWith(pageSize, restrictedTotal); +// }); +// +// it('should return at least 1', () => { +// const getPage = stub(pager, 'getPage').returns(0); +// stub(pager, 'restrictTotalRecords'); +// +// expect(pager.finalPage(1, 0)).to.eq(1); +// }); +// }); +// +// describe('fromResult()', () => { +// it('should return first record index on page', () => { +// expect(pager.fromResult(14, 8)).to.eq(113); +// }); +// }); +// +// describe('toResult()', () => { +// it('should return last record index on page', () => { +// expect(pager.toResult(14, 7, 400)).to.eq(98); +// }); +// +// it('should clip the last page based on total records', () => { +// expect(pager.toResult(14, 7, 87)).to.eq(87); +// }); +// }); +// +// describe('build()', () => { +// it('should build page object', () => { +// const last = 30; +// const from = 13; +// const to = 29; +// const next = 4; +// const previous = 2; +// const range = [1, 2, 3, 4, 5]; +// const current = 3; +// const size = 14; +// const recordCount = 410; +// const limit = 7; +// const finalPage = stub(pager, 'finalPage').returns(last); +// const fromResult = stub(pager, 'fromResult').returns(from); +// const nextPage = stub(pager, 'nextPage').returns(next); +// const previousPage = stub(pager, 'previousPage').returns(previous); +// const pageNumbers = stub(pager, 'pageNumbers').returns(range); +// const toResult = stub(pager, 'toResult').returns(to); +// pager['state'] = { +// data: { +// page: { size, current, limit }, +// recordCount, +// }, +// }; +// +// const page = pager.build(); +// +// expect(page).to.eql({ +// from, +// to, +// previous, +// next, +// last, +// range, +// }); +// expect(finalPage).to.be.calledWith(size, recordCount); +// expect(fromResult).to.be.calledWith(current, size); +// expect(nextPage).to.be.calledWith(current, last); +// expect(previousPage).to.be.calledWith(current); +// expect(pageNumbers).to.be.calledWith(current, last, limit); +// expect(toResult).to.be.calledWith(current, size, recordCount); +// }); +// }); +// +// describe('pageNumbers', () => { +// it('should return an array of beginning at 1', () => { +// expect(pager.pageNumbers(1, 10, 5)).to.eql([1, 2, 3, 4, 5]); +// expect(pager.pageNumbers(2, 10, 5)).to.eql([1, 2, 3, 4, 5]); +// expect(pager.pageNumbers(3, 10, 5)).to.eql([1, 2, 3, 4, 5]); +// }); +// +// it('should start shifting the page range up', () => { +// expect(pager.pageNumbers(4, 10, 5)).to.eql([2, 3, 4, 5, 6]); +// }); +// +// it('should return an array of pages', () => { +// expect(pager.pageNumbers(6, 10, 5)).to.eql([4, 5, 6, 7, 8]); +// }); +// +// it('should return array ending at 10', () => { +// expect(pager.pageNumbers(10, 10, 5)).to.eql([6, 7, 8, 9, 10]); +// expect(pager.pageNumbers(9, 10, 5)).to.eql([6, 7, 8, 9, 10]); +// expect(pager.pageNumbers(8, 10, 5)).to.eql([6, 7, 8, 9, 10]); +// }); +// +// it('should start shifting the page range down', () => { +// expect(pager.pageNumbers(7, 10, 5)).to.eql([5, 6, 7, 8, 9]); +// }); +// +// it('should handle limit higher than available pages', () => { +// expect(pager.pageNumbers(11, 12, 13)).to.eql([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); +// }); +// +// it('should restrict ranges by last page', () => { +// expect(pager.pageNumbers(1, 5, 5)).to.eql([1, 2, 3, 4, 5]); +// expect(pager.pageNumbers(1, 4, 5)).to.eql([1, 2, 3, 4]); +// expect(pager.pageNumbers(1, 3, 5)).to.eql([1, 2, 3]); +// expect(pager.pageNumbers(1, 2, 5)).to.eql([1, 2]); +// expect(pager.pageNumbers(1, 1, 5)).to.eql([1]); +// }); +// }); +// +// describe('restrictTotalRecords()', () => { +// it('should return total records with max of MAX_RECORDS', () => { +// expect(pager.restrictTotalRecords(10, 20000)).to.eq(10000); +// expect(pager.restrictTotalRecords(12, 20000)).to.eq(9996); +// expect(pager.restrictTotalRecords(24, 20000)).to.eq(9984); +// expect(pager.restrictTotalRecords(50, 20000)).to.eq(10000); +// expect(pager.restrictTotalRecords(13, 9999)).to.eq(9997); +// expect(pager.restrictTotalRecords(50, 9960)).to.eq(10000); +// expect(pager.restrictTotalRecords(20, 100)).to.eq(100); +// }); +// }); +// +// describe('getPage()', () => { +// it('should get the number of the specified page', () => { +// expect(pager.getPage(4, 9)).to.eq(3); +// }); +// }); +// +// describe('transformPages()', () => { +// it('should return page transformer', () => { +// expect(pager.transformPages(1, 2, 3)).to.be.a('function'); +// }); +// +// it('should return current page value', () => { +// expect(pager.transformPages(1, 2, 3)(8)).to.eq(8); +// expect(pager.transformPages(3, 2, 3)(8)).to.eq(8); +// }); +// }); +// }); From b4584a3091c6ab2e1a8ba043ce6b64961f515a30 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Wed, 26 Apr 2017 08:57:15 -0400 Subject: [PATCH 46/56] pager tests --- src/core/bridge.ts | 30 +- src/core/query.ts | 23 +- src/flux/capacitor.ts | 4 +- src/flux/pager.ts | 30 +- src/flux/pagerV2.ts | 95 ---- src/models/request.ts | 8 +- test/unit/flux/capacitor.ts | 605 +----------------------- test/unit/flux/pager.ts | 394 +++++---------- test/unit/flux/pagerV2.ts | 189 -------- test/unit/flux/reducers/autocomplete.ts | 2 +- test/unit/flux/reducers/collections.ts | 28 +- 11 files changed, 198 insertions(+), 1210 deletions(-) delete mode 100644 src/flux/pagerV2.ts delete mode 100644 test/unit/flux/pagerV2.ts diff --git a/src/core/bridge.ts b/src/core/bridge.ts index d03af33..757ca58 100644 --- a/src/core/bridge.ts +++ b/src/core/bridge.ts @@ -1,7 +1,7 @@ +import * as axios from 'axios'; import { Request } from '../models/request'; import { Record, RefinementResults, Results } from '../models/response'; import { Query } from './query'; -import * as axios from 'axios'; const SEARCH = '/search'; const REFINEMENTS = '/refinements'; @@ -15,14 +15,12 @@ export interface RawRecord extends Record { _snippet?: string; } -export interface BridgeCallback { - (err?: Error, res?: Results): void; -} +export type BridgeCallback = (err?: Error, res?: Results) => void; export type BridgeQuery = string | Query | Request; export const DEFAULT_CONFIG: BridgeConfig = { - timeout: 1500 + timeout: 1500, }; export abstract class AbstractBridge { @@ -39,8 +37,10 @@ export abstract class AbstractBridge { } search(query: BridgeQuery, callback?: BridgeCallback): Promise { - let { request, queryParams } = this.extractRequest(query); - if (request === null) return this.generateError(INVALID_QUERY_ERROR, callback); + const { request, queryParams } = this.extractRequest(query); + if (request === null) { + return this.generateError(INVALID_QUERY_ERROR, callback); + } const response = this.fireRequest(this.bridgeUrl, request, queryParams) .then((res) => res.records ? Object.assign(res, { records: res.records.map(this.convertRecordFields) }) : res); @@ -48,8 +48,10 @@ export abstract class AbstractBridge { } refinements(query: BridgeQuery, navigationName: string, callback?: BridgeCallback): Promise { - let { request } = this.extractRequest(query); - if (request === null) return this.generateError(INVALID_QUERY_ERROR, callback); + const { request } = this.extractRequest(query); + if (request === null) { + return this.generateError(INVALID_QUERY_ERROR, callback); + } const refinementsRequest = { originalQuery: request, navigationName }; @@ -59,7 +61,7 @@ export abstract class AbstractBridge { protected abstract augmentRequest(request: any): any; - private handleResponse(response: PromiseLike, callback: Function) { + private handleResponse(response: PromiseLike, callback: (error?: Error, results?: T) => void) { if (callback) { response.then((res) => callback(undefined, res), (err) => callback(err)); } else { @@ -88,13 +90,13 @@ export abstract class AbstractBridge { private fireRequest(url: string, body: Request | any, queryParams: any = {}): Axios.IPromise { const options = { - url, - method: 'post', - params: queryParams, data: this.augmentRequest(body), headers: this.headers, + method: 'post', + params: queryParams, responseType: 'json', - timeout: this.config.timeout + timeout: this.config.timeout, + url, }; return axios(options) .then((res) => res.data) diff --git a/src/core/query.ts b/src/core/query.ts index 9813459..bcd6288 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -1,3 +1,8 @@ +import * as clone from 'clone'; +import deepEqual = require('deep-equal'); +import filterObject = require('filter-object'); +import * as qs from 'qs'; + import { Biasing, CustomUrlParam, @@ -7,19 +12,15 @@ import { SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement, - Sort + Sort, } from '../models/request'; import { Navigation, RangeRefinement, Refinement, - ValueRefinement + ValueRefinement, } from '../models/response'; import { NavigationConverter } from '../utils/converter'; -import * as clone from 'clone'; -import * as deepEqual from 'deep-equal'; -import filterObject = require('filter-object'); -import * as qs from 'qs'; const REFINEMENT_MASK = '{navigationName,value,low,high}'; @@ -75,7 +76,9 @@ export class Query { withoutSelectedRefinements(...refinements: Array): Query { refinements.forEach((refinement) => { const index = this.request.refinements.findIndex((ref) => deepEqual(ref, refinement)); - if (index > -1) this.request.refinements.splice(index, 1); + if (index > -1) { + this.request.refinements.splice(index, 1); + } }); return this; } @@ -144,7 +147,7 @@ export class Query { navigationName, value, exclude, - type: 'Value' + type: 'Value', }); } @@ -154,7 +157,7 @@ export class Query { low, high, exclude, - type: 'Range' + type: 'Range', }); } @@ -239,7 +242,7 @@ export class Query { } private clearEmptyArrays(request: Request): Request { - for (let key in request) { + for (const key in request) { if (request[key] instanceof Array && request[key].length === 0) { delete request[key]; } diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index 3d1b6c3..d3cda3c 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -3,7 +3,7 @@ import * as redux from 'redux'; import filterObject = require('filter-object'); import { BrowserBridge } from '../core/bridge'; import { Query, QueryConfiguration } from '../core/query'; -import { RangeRefinement, Sort, ValueRefinement } from '../models/request'; +import { SelectedRangeRefinement, SelectedValueRefinement, Sort } from '../models/request'; import { Navigation, RefinementResults, Results } from '../models/response'; import ActionPack from './actions'; import Observer from './observer'; @@ -59,7 +59,7 @@ export namespace Events { } export { Pager }; -export type FluxRefinement = ValueRefinement | RangeRefinement; +export type FluxRefinement = SelectedValueRefinement | SelectedRangeRefinement; export interface FluxConfiguration extends QueryConfiguration { bridge?: FluxBridgeConfig; diff --git a/src/flux/pager.ts b/src/flux/pager.ts index 2967575..a606bfe 100644 --- a/src/flux/pager.ts +++ b/src/flux/pager.ts @@ -19,7 +19,7 @@ export class Pager { } finalPage(pageSize: number, totalRecords: number) { - return Math.max(this.getPage(this.restrictTotalRecords(totalRecords, pageSize), pageSize), 1); + return Math.max(this.getPage(pageSize, this.restrictTotalRecords(pageSize, totalRecords)), 1); } fromResult(currentPage: number, pageSize: number) { @@ -41,44 +41,44 @@ export class Pager { const pageSize = this.state.data.page.size || 10; const currentPage = this.state.data.page.current; const totalRecords = this.state.data.recordCount; - const finalPage = this.finalPage(pageSize, totalRecords); + const last = this.finalPage(pageSize, totalRecords); return { from: this.fromResult(currentPage, pageSize), - last: finalPage, - next: this.nextPage(currentPage, finalPage), + last, + next: this.nextPage(currentPage, last), previous: this.previousPage(currentPage), - range: this.pageNumbers(currentPage, finalPage, this.state.data.page.limit), + range: this.pageNumbers(currentPage, last, this.state.data.page.limit), to: this.toResult(currentPage, pageSize, totalRecords), }; } - pageNumbers(currentPage: number, finalPage: number, limit: number): number[] { + pageNumbers(currentPage: number, finalPage: number, limit: number) { return range(1, Math.min(finalPage + 1, limit + 1)) .map(this.transformPages(currentPage, finalPage, limit)); } - restrictTotalRecords(total: number, pageSize: number): number { - if (total > MAX_RECORDS) { + restrictTotalRecords(pageSize: number, totalRecords: number) { + if (totalRecords > MAX_RECORDS) { return MAX_RECORDS - (MAX_RECORDS % pageSize); - } else if ((total + pageSize) > MAX_RECORDS) { + } else if ((totalRecords + pageSize) > MAX_RECORDS) { if (MAX_RECORDS % pageSize === 0) { return MAX_RECORDS; } else { - return total - (total % pageSize); + return totalRecords - (totalRecords % pageSize); } } else { - return total; + return totalRecords; } } - getPage(record: number, pageSize: number): number { - return Math.ceil(record / pageSize); + getPage(pageSize: number, totalRecords: number) { + return Math.ceil(totalRecords / pageSize); } - transformPages(currentPage: number, finalPage: number, limit: number): (value: number) => number { + transformPages(currentPage: number, finalPage: number, limit: number) { const border = Math.ceil(limit / 2); - return (value: number): number => { + return (value: number) => { // account for 0-indexed pages if (currentPage <= border || limit > finalPage) { // pages start at beginning diff --git a/src/flux/pagerV2.ts b/src/flux/pagerV2.ts deleted file mode 100644 index a606bfe..0000000 --- a/src/flux/pagerV2.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Results } from '../models/response'; -import { Page } from './actions'; -import { Events, FluxCapacitor } from './capacitor'; -import Store from './store'; -import range = require('lodash.range'); - -const MAX_RECORDS = 10000; - -export class Pager { - - constructor(private state: Store.State) { } - - previousPage(currentPage: number) { - return currentPage > 1 ? currentPage - 1 : null; - } - - nextPage(currentPage: number, finalPage: number) { - return (currentPage + 1 <= finalPage) ? currentPage + 1 : null; - } - - finalPage(pageSize: number, totalRecords: number) { - return Math.max(this.getPage(pageSize, this.restrictTotalRecords(pageSize, totalRecords)), 1); - } - - fromResult(currentPage: number, pageSize: number) { - return currentPage * pageSize + 1; - // TODO move the default value into reducer setup - // return this.flux.query.build().skip + 1 || 1; - } - - toResult(currentPage: number, pageSize: number, totalRecords: number) { - if ((currentPage * pageSize) > totalRecords) { - return ((currentPage - 1) * pageSize) + (totalRecords % currentPage); - } else { - return currentPage * pageSize; - } - } - - build(): Page { - // TODO move this default into the reducer setup - const pageSize = this.state.data.page.size || 10; - const currentPage = this.state.data.page.current; - const totalRecords = this.state.data.recordCount; - const last = this.finalPage(pageSize, totalRecords); - - return { - from: this.fromResult(currentPage, pageSize), - last, - next: this.nextPage(currentPage, last), - previous: this.previousPage(currentPage), - range: this.pageNumbers(currentPage, last, this.state.data.page.limit), - to: this.toResult(currentPage, pageSize, totalRecords), - }; - } - - pageNumbers(currentPage: number, finalPage: number, limit: number) { - return range(1, Math.min(finalPage + 1, limit + 1)) - .map(this.transformPages(currentPage, finalPage, limit)); - } - - restrictTotalRecords(pageSize: number, totalRecords: number) { - if (totalRecords > MAX_RECORDS) { - return MAX_RECORDS - (MAX_RECORDS % pageSize); - } else if ((totalRecords + pageSize) > MAX_RECORDS) { - if (MAX_RECORDS % pageSize === 0) { - return MAX_RECORDS; - } else { - return totalRecords - (totalRecords % pageSize); - } - } else { - return totalRecords; - } - } - - getPage(pageSize: number, totalRecords: number) { - return Math.ceil(totalRecords / pageSize); - } - - transformPages(currentPage: number, finalPage: number, limit: number) { - const border = Math.ceil(limit / 2); - return (value: number) => { - // account for 0-indexed pages - if (currentPage <= border || limit > finalPage) { - // pages start at beginning - return value; - } else if (currentPage > finalPage - border) { - // pages start and end in the middle - return value + finalPage - limit; - } else { - // pages end at last page - return value + currentPage - border; - } - }; - } -} diff --git a/src/models/request.ts b/src/models/request.ts index ca24c7a..4e5063a 100644 --- a/src/models/request.ts +++ b/src/models/request.ts @@ -5,7 +5,7 @@ export type SortOrder = 'Ascending' | 'Descending'; export interface Request { // query parameters query?: string; - refinements?: Refinement[]; + refinements?: SelectedRefinement[]; // query configuration fields?: string[]; @@ -46,18 +46,18 @@ export interface CustomUrlParam { value: string; } -export interface Refinement { +export interface SelectedRefinement { type: RefinementType; navigationName: string; exclude?: boolean; } -export interface RangeRefinement extends Refinement { +export interface SelectedRangeRefinement extends SelectedRefinement { low?: number; high?: number; } -export interface ValueRefinement extends Refinement { +export interface SelectedValueRefinement extends SelectedRefinement { value: string; } diff --git a/test/unit/flux/capacitor.ts b/test/unit/flux/capacitor.ts index ec78f1a..69c042f 100644 --- a/test/unit/flux/capacitor.ts +++ b/test/unit/flux/capacitor.ts @@ -14,9 +14,9 @@ const DETAILS_RESULT = { records: [{}] }; suite('FluxCapacitor', ({ expect, spy, stub }) => { const LISTENER = () => null; - let create: Sinon.SinonStub; - let listen: Sinon.SinonStub; - let subscribe: Sinon.SinonSpy; + let create: sinon.SinonStub; + let listen: sinon.SinonStub; + let subscribe: sinon.SinonSpy; let flux: FluxCapacitor; beforeEach(() => { @@ -116,7 +116,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { }); describe('actions', () => { - let dispatch: Sinon.SinonSpy; + let dispatch: sinon.SinonSpy; beforeEach(() => { dispatch = spy(); @@ -153,7 +153,7 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { flux.reset(query, { field, index }); - expect(updateSearch).to.be.calledWith({ query, field, index, clear: true }); + expect(updateSearch).to.be.calledWith({ query, navigationId: field, index, clear: true }); }); it('should fallback to null query and empty refinements', () => { @@ -161,7 +161,12 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { flux.reset(); - expect(updateSearch).to.be.calledWith({ query: null, refinements: [], clear: true }); + expect(updateSearch).to.be.calledWith({ + query: null, + navigationId: undefined, + index: undefined, + clear: true, + }); }); }); @@ -177,22 +182,13 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { describe('sort()', () => { it('should dispatch updateSorts()', () => { - const sort = { field: 'price', descending: true }; + const sort = 'Price Ascending'; const updateSorts = stub(flux.actions, 'updateSorts'); flux.sort(sort); expect(updateSorts).to.be.calledWith(sort); }); - - it('should accept multiple sorts', () => { - const sorts = [{ field: 'price', descending: true }, { field: 'popularity' }]; - const updateSorts = stub(flux.actions, 'updateSorts'); - - flux.sort(sorts); - - expect(updateSorts).to.be.calledWith(sorts); - }); }); describe('refine()', () => { @@ -236,581 +232,4 @@ suite('FluxCapacitor', ({ expect, spy, stub }) => { }); }); }); - - // describe('search()', () => { - // it('should make a search request', (done) => { - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).query).to.eq('testing'); - // done(); - // }); - // - // flux.search('testing'); - // }); - // - // describe('events', () => { - // it('should emit a search event before searching', (done) => { - // flux.bridge.search = (): any => expect.fail(); - // flux.on(Events.SEARCH, (query) => { - // expect(query.query).to.eq('danish'); - // done(); - // }); - // - // flux.search('danish'); - // }); - // - // it('should emit a results event', (done) => { - // flux.bridge.search = (): any => Promise.resolve('ok'); - // // mock.post(SEARCH_URL, (req, res) => res.body('ok')); - // flux.on(Events.RESULTS, () => done()); - // - // flux.search(''); - // }); - // - // it('should not emit a redirect event', (done) => { - // flux.bridge.search = (): any => ({ then: (cb) => cb({}) }); - // flux.on(Events.REDIRECT, () => expect.fail()); - // - // flux.search(''); - // done(); - // }); - // - // it('should emit a redirect event', (done) => { - // const redirect = 'something.html'; - // flux.bridge.search = (): any => ({ then: (cb) => cb({ redirect }) }); - // flux.on(Events.REDIRECT, (url) => expect(url).to.eq(redirect)); - // - // flux.search(''); - // done(); - // }); - // - // it('should not emit a query_changed event on subsequent equivalent requests', (done) => { - // flux.bridge.search = (): any => Promise.resolve('ok'); - // - // flux.search('apple') - // .then(() => flux.on(Events.QUERY_CHANGED, () => expect.fail())) - // .then(() => flux.search('apple')) - // .then(() => done()); - // }); - // - // it('should emit a query_changed event on changing the query', (done) => { - // flux.bridge.search = (): any => Promise.resolve('ok'); - // - // flux.search('shoes') - // .then(() => flux.on(Events.QUERY_CHANGED, (query) => { - // expect(query).to.eq('other'); - // done(); - // })) - // .then(() => flux.search('other')); - // }); - // - // it('should emit a query_changed with case insensitivity', (done) => { - // flux.bridge.search = (): any => Promise.resolve('ok'); - // - // flux.search('apple') - // .then(() => flux.on(Events.QUERY_CHANGED, () => expect.fail())) - // .then(() => flux.search('ApPle')) - // .then(() => done()); - // }); - // }); - // }); - // - // describe('refinements()', () => { - // it('should make a refinements request', (done) => { - // mock.post(REFINEMENTS_URL, (req, res) => { - // expect(JSON.parse(req.body()).navigationName).to.eq('brand'); - // done(); - // }); - // - // flux.refinements('brand'); - // }); - // - // describe('events', () => { - // it('should emit a refinement_results event', (done) => { - // mock.post(REFINEMENTS_URL, (req, res) => res.body('ok')); - // flux.on(Events.REFINEMENT_RESULTS, () => done()); - // - // flux.refinements(''); - // }); - // }); - // }); - // - // describe('refine()', () => { - // it('should make a request on refinement', (done) => { - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).refinements.length).to.eq(1); - // done(); - // }); - // - // flux.refine(SELECTED_REFINEMENT); - // }); - // - // it('should reset paging on refinement', (done) => { - // flux.query.skip(20); - // mock.post(SEARCH_URL, (req, res) => { - // expect(flux.query.build().skip).to.eq(0); - // done(); - // }); - // - // flux.refine(SELECTED_REFINEMENT); - // }); - // - // it('should skip reset paging on refinement', (done) => { - // flux.query.skip(20); - // mock.post(SEARCH_URL, (req, res) => res.body('ok')); - // - // flux.refine(SELECTED_REFINEMENT, { reset: false }) - // .then(() => { - // expect(flux.query.build().skip).to.eq(20); - // done(); - // }); - // }); - // }); - // - // describe('events', () => { - // it('should emit refinements_changed event on refinement', (done) => { - // mock.post(SEARCH_URL, (req, res) => res.body(JSON.stringify(REFINEMENT_RESULT))); - // flux.on(Events.REFINEMENTS_CHANGED, (data) => { - // expect(data.available).to.eq('a'); - // expect(data.selected).to.eq('b'); - // done(); - // }); - // - // flux.refine(SELECTED_REFINEMENT); - // }); - // }); - // - // describe('unrefine()', () => { - // it('should make a request on un-refinement', (done) => { - // flux.query.withSelectedRefinements(SELECTED_REFINEMENT); - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).refinements).to.not.be.ok; - // done(); - // }); - // - // flux.unrefine(SELECTED_REFINEMENT); - // }); - // - // it('should un-refine with deep equality', (done) => { - // flux.query.withSelectedRefinements(SELECTED_REFINEMENT); - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).refinements).to.not.be.ok; - // done(); - // }); - // - // // intentionally not using SELECTED_REFINEMENT - // flux.unrefine({ type: 'Value', navigationName: 'brand', value: 'DeWalt' }); - // }); - // - // it('should reset paging on un-refinement', (done) => { - // flux.query.skip(20); - // flux.query.withSelectedRefinements(SELECTED_REFINEMENT); - // mock.post(SEARCH_URL, (req, res) => res.body('ok')); - // - // flux.unrefine(SELECTED_REFINEMENT) - // .then(() => { - // expect(flux.query.build().skip).to.eq(0); - // done(); - // }); - // }); - // - // describe('events', () => { - // it('should emit refinements_changed event on un-refinement', (done) => { - // flux.query.withSelectedRefinements(SELECTED_REFINEMENT); - // mock.post(SEARCH_URL, (req, res) => res.body(JSON.stringify(REFINEMENT_RESULT))); - // flux.on(Events.REFINEMENTS_CHANGED, (data) => { - // expect(data.available).to.eq('a'); - // expect(data.selected).to.eq('b'); - // done(); - // }); - // - // flux.unrefine(SELECTED_REFINEMENT); - // }); - // }); - // }); - // - // describe('paging behaviour', () => { - // beforeEach(() => { - // flux.query.skip(20); - // flux.results = { totalRecordCount: 300 }; - // }); - // - // it('should reset paging', (done) => { - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).skip).to.eq(0); - // done(); - // }); - // - // flux.page.reset(); - // }); - // - // it('should page forward', (done) => { - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).skip).to.eq(30); - // done(); - // }); - // - // flux.page.next(); - // }); - // - // it('should page backward', (done) => { - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).skip).to.eq(10); - // done(); - // }); - // flux.page.prev(); - // }); - // - // it('should advance to last page', (done) => { - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).skip).to.eq(290); - // done(); - // }); - // - // flux.page.last(); - // }); - // }); - // - // describe('resizing behaviour', () => { - // it('should resize the page and keep skip', (done) => { - // flux.query.withPageSize(10); - // flux.query.skip(20); - // flux.page.pageExists = () => true; - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).skip).to.eq(20); - // expect(JSON.parse(req.body()).pageSize).to.eq(20); - // done(); - // }); - // - // flux.resize(20, false); - // }); - // - // it('should resize the page and keep skip on the same page', (done) => { - // flux.query.withPageSize(10); - // flux.query.skip(30); - // flux.page.pageExists = () => true; - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).skip).to.eq(20); - // expect(JSON.parse(req.body()).pageSize).to.eq(20); - // done(); - // }); - // - // flux.resize(20, false); - // }); - // - // it('should resize the page and bring skip to 0', (done) => { - // flux.query.withPageSize(10); - // flux.query.skip(20); - // flux.page.pageExists = () => true; - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).skip).to.eq(0); - // expect(JSON.parse(req.body()).pageSize).to.eq(30); - // done(); - // }); - // - // flux.resize(30, true); - // }); - // - // it('should resize from smaller to larger and keep skip when total near max', (done) => { - // flux.query.withPageSize(12); - // flux.query.skip(9984); - // flux.page.pageExists = () => true; - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).skip).to.eq(9960); - // expect(JSON.parse(req.body()).pageSize).to.eq(24); - // done(); - // }); - // - // flux.resize(24, false); - // }); - // - // it('should resize from larger to smaller and keep skip when total near max', (done) => { - // flux.query.withPageSize(50); - // flux.query.skip(9950); - // flux.page.pageExists = () => true; - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).skip).to.eq(9947); - // expect(JSON.parse(req.body()).pageSize).to.eq(49); - // done(); - // }); - // - // flux.resize(49, false); - // }); - // }); - // - // describe('rewrite()', () => { - // it('should rewrite the query', (done) => { - // const newQuery = 'montana'; - // flux.query.withQuery('alabama'); - // flux.search = (query): any => Promise.resolve(expect(query).to.eq(newQuery)); - // - // flux.rewrite(newQuery) - // .then(() => done()); - // }); - // - // it('should rewrite the query but not perform a search', () => { - // const newQuery = 'montana'; - // flux.query.withQuery('alabama'); - // flux.search = (query): any => expect.fail(); - // - // flux.rewrite(newQuery, { skipSearch: true }); - // - // expect(flux.query.raw.query).to.eq(newQuery); - // }); - // - // it('should emit events on search', (done) => { - // const newQuery = 'montana'; - // flux.query.withQuery('alabama'); - // - // flux.search = (query): any => ({ then: (cb) => cb() }); - // flux.emit = (event, data): any => { - // expect(data).to.eq(newQuery); - // done(); - // }; - // - // flux.rewrite(newQuery); - // }); - // - // it('should emit events when not searching', () => { - // const newQuery = 'montana'; - // flux.query.withQuery('alabama'); - // flux.emit = (event, data): any => { - // switch (event) { - // case Events.REWRITE_QUERY: - // return expect(data).to.eq(newQuery); - // case Events.QUERY_CHANGED: - // break; - // default: - // expect.fail(); - // } - // }; - // - // flux.rewrite(newQuery, { skipSearch: true }); - // }); - // }); - // - // describe('reset behaviour', () => { - // it('should reset the query', (done) => { - // flux.query.withQuery('alabama'); - // flux.resetRecall = () => null; - // mock.post(SEARCH_URL, (req, res) => { - // const body = JSON.parse(req.body()); - // expect(body.query).to.eq(''); - // done(); - // }); - // - // flux.reset(); - // }); - // - // it('should accept a new query on reset', (done) => { - // flux.query.withQuery('alabama'); - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).query).to.eq('texas'); - // done(); - // }); - // - // flux.reset('texas'); - // }); - // - // describe('events', () => { - // it('should emit events', (done) => { - // mock.post(SEARCH_URL, (req, res) => res.body('ok')); - // - // let count = 0; - // const checkComplete = () => { - // if (++count === 2) { - // done(); - // } - // }; - // flux.on(Events.RESET, checkComplete); - // flux.on(Events.PAGE_CHANGED, checkComplete); - // flux.reset(); - // }); - // }); - // }); - // - // describe('sort()', () => { - // it('should reset paging but not refinements', (done) => { - // const refinement: SelectedValueRefinement = { navigationName: 'brand', type: 'Value', value: 'DeWalt' }; - // flux.query.skip(30) - // .withSelectedRefinements(refinement); - // mock.post(SEARCH_URL, (req, res) => { - // const body = JSON.parse(req.body()); - // expect(body.skip).to.eq(0); - // expect(body.sort).to.eql([{ field: 'price', order: 'Ascending' }]); - // expect(body.refinements).to.eql([refinement]); - // done(); - // }); - // - // flux.sort({ field: 'price', order: 'Ascending' }); - // }); - // - // it('should add sorts', (done) => { - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).sort).to.eql([{ field: 'price', order: 'Ascending' }]); - // done(); - // }); - // - // flux.sort({ field: 'price', order: 'Ascending' }); - // }); - // - // it('should add more sorts', (done) => { - // flux.query.withSorts({ field: 'title', order: 'Descending' }); - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).sort.length).to.eq(2); - // done(); - // }); - // - // flux.sort({ field: 'price', order: 'Ascending' }); - // }); - // - // it('should remove sorts', (done) => { - // flux.query.withSorts({ field: 'price', order: 'Descending' }); - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).sort).to.eql([{ field: 'price', order: 'Ascending' }]); - // done(); - // }); - // - // flux.sort({ field: 'price', order: 'Ascending' }); - // }); - // - // it('should remove all sorts', (done) => { - // const sorts: Sort[] = [ - // { field: 'price', order: 'Descending' }, - // { field: 'other', order: 'Ascending' }, - // { field: 'type', order: 'Descending' }, - // ]; - // flux.query.withSorts(...sorts); - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).sort).to.eql([{ field: 'price', order: 'Ascending' }]); - // done(); - // }); - // - // flux.sort({ field: 'price', order: 'Ascending' }, sorts); - // }); - // - // it('should emit sort event', (done) => { - // const sort: any = { field: 'price', order: 'Ascending' }; - // mock.post(SEARCH_URL, (req, res) => res.body('ok')); - // flux.on(Events.SORT, (newSort) => { - // expect(newSort).to.be.ok; - // done(); - // }); - // - // flux.sort(sort); - // }); - // }); - // - // describe('details()', () => { - // it('should refine by id', (done) => { - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).refinements).to.eql([{ navigationName: 'id', type: 'Value', value: '14830' }]); - // done(); - // }); - // - // flux.details('14830'); - // }); - // - // it('should refine by specified field', (done) => { - // const navigationName = 'variants.id'; - // mock.post(SEARCH_URL, (req, res) => { - // expect(JSON.parse(req.body()).refinements).to.eql([{ navigationName, type: 'Value', value: '14830' }]); - // done(); - // }); - // - // flux.details('14830', navigationName); - // }); - // - // it('should persist area, collection, language, fields', (done) => { - // flux.query.withConfiguration({ - // area: 'nonProd', - // collection: 'offbrand', - // fields: ['title', 'price'], - // language: 'zh', - // }); - // mock.post(SEARCH_URL, (req, res) => { - // const body = JSON.parse(req.body()); - // expect(body.area).to.eq('nonProd'); - // expect(body.collection).to.eq('offbrand'); - // expect(body.language).to.eq('zh'); - // expect(body.pageSize).to.eq(1); - // expect(body.fields).to.eql(['title', 'price']); - // done(); - // }); - // - // flux.details('14830'); - // }); - // - // it('should emit details event', (done) => { - // mock.post(SEARCH_URL, (req, res) => res.body(JSON.stringify(DETAILS_RESULT))); - // flux.on(Events.DETAILS, (data) => { - // expect(data).to.be.ok; - // done(); - // }); - // - // flux.details('14830'); - // }); - // }); - // - // describe('switchCollection()', () => { - // it('should switch collection', (done) => { - // const collection = 'other'; - // mock.post(SEARCH_URL, (req, res) => res.body('ok')); - // flux.query.withConfiguration({ collection: 'something' }); - // - // flux.switchCollection(collection) - // .then(() => { - // expect(flux.query.raw.collection).to.eq(collection); - // done(); - // }); - // }); - // - // it('should reset paging, sort and refinements on switch collection', (done) => { - // const collection = 'other'; - // mock.post(SEARCH_URL, (req, res) => res.body('ok')); - // - // flux.query.withConfiguration({ collection: 'something' }) - // .withSelectedRefinements({ navigationName: 'brand', type: 'Value', value: 'Nike' }) - // .withSorts({ field: 'price', order: 'Descending' }) - // .skip(30); - // - // flux.switchCollection(collection) - // .then(() => { - // const rawQuery = flux.query.raw; - // expect(rawQuery.collection).to.eq(collection); - // expect(rawQuery.skip).to.eq(0); - // expect(rawQuery.sort).to.be.empty; - // expect(rawQuery.refinements).to.be.empty; - // done(); - // }); - // }); - // - // it('should emit collection_changed event', (done) => { - // const collection = 'support'; - // mock.post(SEARCH_URL, (req, res) => res.body('ok')); - // flux.on(Events.COLLECTION_CHANGED, (coll) => { - // expect(coll).to.eq(collection); - // done(); - // }); - // - // flux.switchCollection(collection); - // }); - // }); - // - // it('should reset recall', () => { - // flux.query - // .withQuery('alabama') - // .withPageSize(20) - // .skip(34) - // .withSelectedRefinements({ navigationName: 'a', value: 'b', type: 'Value' }) - // .withOrFields('boots', 'hats'); - // - // flux.resetRecall(); - // - // const request = flux.query.raw; - // expect(request.pageSize).to.be.ok; - // expect(request.orFields).to.be.ok; - // expect(request.refinements).to.eql([]); - // expect(request.skip).to.not.be.ok; - // expect(request.query).to.eq(''); - // }); }); diff --git a/test/unit/flux/pager.ts b/test/unit/flux/pager.ts index dfe663e..0b14488 100644 --- a/test/unit/flux/pager.ts +++ b/test/unit/flux/pager.ts @@ -2,340 +2,188 @@ import { Pager } from '../../../src/flux/pager'; import { Events, FluxCapacitor, Query } from '../../../src/index'; import suite from '../_suite'; -suite('Pager', ({ expect }) => { - function flux(opts: { start: number, total?: number, pageSize?: number } | number, search?: Function): FluxCapacitor { - const recordStart = typeof opts === 'number' ? opts : opts.start; - const totalRecordCount = typeof opts === 'object' && Number(opts.total) >= 0 ? opts.total : 30; - const pageSize = typeof opts === 'object' && Number(opts.pageSize) >= 0 ? opts.pageSize : 10; - return { - emit: (event: string) => null, - query: new Query() - .skip(recordStart) - .withPageSize(pageSize), - results: { totalRecordCount }, - search, - }; - } +suite('Pager', ({ expect, stub }) => { + const STATE = {}; + let pager: Pager; - it('should be defined', () => { - expect(new Pager({})).to.be.ok; - }); - - it('should be defined with defaults', () => { - const mockFlux = flux({ start: 0, total: 40, pageSize: 5 }, () => null); - const pager = new Pager(mockFlux); - - expect(pager.currentPage).to.eq(1); - expect(pager.previousPage).to.eq(null); - expect(pager.nextPage).to.eq(2); - expect(pager.firstPage).to.eq(1); - expect(pager.finalPage).to.eq(8); - expect(pager.fromResult).to.eq(1); - expect(pager.toResult).to.eq(5); - expect(pager.totalRecords).to.eq(40); - expect(pager.pageNumbers()).to.eql([1, 2, 3, 4, 5]); - expect(mockFlux.query.build().pageSize).to.eq(5); - }); + beforeEach(() => pager = new Pager(STATE)); - it('first page change', (done) => { - const mockFlux = flux(0, function() { - expect(this.query.raw.skip).to.eq(10); - done(); + describe('constructor', () => { + it('should set state', () => { + expect(pager['state']).to.eq(STATE); }); - mockFlux.query = new Query(); - new Pager(mockFlux).next(); - }); - - it('should move skip forward', () => { - const pager = new Pager(flux(11, function() { - expect(this.query.raw.skip).to.eq(20); - })); - pager.next(); - - expect(pager.currentPage).to.eq(3); - expect(pager.previousPage).to.eq(2); - expect(pager.nextPage).to.eq(null); - expect(pager.firstPage).to.eq(1); - expect(pager.finalPage).to.eq(3); - expect(pager.fromResult).to.eq(21); - expect(pager.toResult).to.eq(30); - expect(pager.totalRecords).to.eq(30); - expect(pager.pageNumbers()).to.eql([1, 2, 3]); - }); - - it('should move skip backward', () => { - const pager = new Pager(flux(11, function() { - expect(this.query.raw.skip).to.eq(0); - })); - pager.prev(); - - expect(pager.currentPage).to.eq(1); - expect(pager.previousPage).to.eq(null); - expect(pager.nextPage).to.eq(2); - expect(pager.firstPage).to.eq(1); - expect(pager.finalPage).to.eq(3); - expect(pager.fromResult).to.eq(1); - expect(pager.toResult).to.eq(10); - expect(pager.totalRecords).to.eq(30); - expect(pager.pageNumbers()).to.eql([1, 2, 3]); - }); - - it('should not change skip when cannot page backward', () => { - const mockFlux = flux(2, () => null); - const pager = new Pager(mockFlux); - expect(mockFlux.query.raw.skip).to.eq(2); - pager.prev(); - expect(mockFlux.query.raw.skip).to.eq(2); - }); - - it('should not change skip when cannot page forward', () => { - const mockFlux = flux(29, () => null); - const pager = new Pager(mockFlux); - expect(mockFlux.query.raw.skip).to.eq(29); - pager.next(); - expect(mockFlux.query.raw.skip).to.eq(29); }); - it('should reset the skip to 0', (done) => { - new Pager(flux(2, function() { - expect(this.query.raw.skip).to.eq(0); - done(); - })).reset(); - }); - - it('should step to the last page', (done) => { - new Pager(flux({ start: 31, total: 45 }, function() { - expect(this.query.raw.skip).to.eq(40); - done(); - })).next(); - }); - - it('should step down from last page', (done) => { - new Pager(flux({ start: 41, total: 45 }, function() { - expect(this.query.raw.skip).to.eq(30); - done(); - })).prev(); - }); - - it('should skip to the last page', (done) => { - new Pager(flux({ start: 1, total: 45 }, function() { - expect(this.query.raw.skip).to.eq(40); - done(); - })).last(); - }); + describe('previousPage()', () => { + it('should return previous page', () => { + expect(pager.previousPage(2)).to.eq(1); + expect(pager.previousPage(309)).to.eq(308); + }); - it('max skip should not go beyond 10000 records', (done) => { - new Pager(flux({ start: 1, total: 14000 }, function() { - expect(this.query.raw.skip).to.eq(9990); - done(); - })).last(); + it('should return null', () => { + expect(pager.previousPage(1)).to.be.null; + }); }); - describe('pageExists()', () => { - it('should return true', () => { - const pager = new Pager(flux({ start: 10, total: 200 }, () => null)); - expect(pager.pageExists(20)).to.be.true; + describe('nextPage()', () => { + it('should return next page', () => { + expect(pager.nextPage(2, 3)).to.eq(3); + expect(pager.nextPage(18, 40)).to.eq(19); }); - it('should return false', () => { - const pager = new Pager(flux({ start: 10, total: 200 }, () => null)); - expect(pager.pageExists(21)).to.be.false; + it('should return null', () => { + expect(pager.nextPage(2, 2)).to.be.null; }); }); - describe('switchPage() behaviour', () => { - it('should switchPage to the first page', () => { - const pager = new Pager(flux({ start: 10, total: 200 }, function() { - expect(this.query.raw.skip).to.eq(0); - })); - pager.switchPage(1); - expect(pager.currentPage).to.eq(1); - expect(pager.fromResult).to.eq(1); - expect(pager.toResult).to.eq(10); - }); + describe('finalPage()', () => { + it('should return final page', () => { + const totalRecords = 423; + const restrictedTotal = 300; + const pageSize = 20; + const page = 7; + const getPage = stub(pager, 'getPage').returns(page); + const restrictTotalRecords = stub(pager, 'restrictTotalRecords').returns(restrictedTotal); + + const finalPage = pager.finalPage(pageSize, totalRecords); - it('should switchPage to a page', () => { - const pager = new Pager(flux({ start: 1, total: 200 }, function() { - expect(this.query.raw.skip).to.eq(70); - })); - pager.switchPage(8); - expect(pager.currentPage).to.eq(8); - expect(pager.fromResult).to.eq(71); - expect(pager.toResult).to.eq(80); + expect(finalPage).to.eq(page); + expect(restrictTotalRecords).to.be.calledWith(pageSize, totalRecords); + expect(getPage).to.be.calledWith(pageSize, restrictedTotal); }); - describe('error states', () => { - it('should not switchPage past the results', (done) => { - new Pager(flux({ start: 1, total: 30 })) - .switchPage(8) - .catch(() => done()); - }); + it('should return at least 1', () => { + const getPage = stub(pager, 'getPage').returns(0); + stub(pager, 'restrictTotalRecords'); - it('should not switchPage to lower than the first page', (done) => { - new Pager(flux(1)) - .switchPage(-2) - .catch(() => done()); - }); + expect(pager.finalPage(1, 0)).to.eq(1); }); }); - describe('restrictTotalRecords()', () => { - it('should return total records that is less than MAX_RECORDS', () => { - const pager = new Pager({}); - expect(pager.restrictTotalRecords(20000, 10)).to.eq(10000); - expect(pager.restrictTotalRecords(20000, 12)).to.eq(9996); - expect(pager.restrictTotalRecords(20000, 24)).to.eq(9984); - expect(pager.restrictTotalRecords(20000, 50)).to.eq(10000); - expect(pager.restrictTotalRecords(9999, 13)).to.eq(9997); - expect(pager.restrictTotalRecords(9960, 50)).to.eq(10000); - expect(pager.restrictTotalRecords(100, 20)).to.eq(100); + describe('fromResult()', () => { + it('should return first record index on page', () => { + expect(pager.fromResult(14, 8)).to.eq(113); }); }); - describe('current page behaviour', () => { - it('should return the current page', () => { - expect(new Pager(flux(1)).currentPage).to.eq(1); + describe('toResult()', () => { + it('should return last record index on page', () => { + expect(pager.toResult(14, 7, 400)).to.eq(98); }); - it('should return from the middle', () => { - expect(new Pager(flux(45)).currentPage).to.eq(5); - }); - - it('should change based on pageSize', () => { - expect(new Pager(flux({ start: 36, pageSize: 12 })).currentPage).to.eq(4); + it('should clip the last page based on total records', () => { + expect(pager.toResult(14, 7, 87)).to.eq(87); }); }); - describe('total pages behaviour', () => { - it('should return the total number of pages', () => { - expect(new Pager(flux({ start: 1, total: 457 })).finalPage).to.eq(46); - }); + describe('build()', () => { + it('should build page object', () => { + const last = 30; + const from = 13; + const to = 29; + const next = 4; + const previous = 2; + const range = [1, 2, 3, 4, 5]; + const current = 3; + const size = 14; + const recordCount = 410; + const limit = 7; + const finalPage = stub(pager, 'finalPage').returns(last); + const fromResult = stub(pager, 'fromResult').returns(from); + const nextPage = stub(pager, 'nextPage').returns(next); + const previousPage = stub(pager, 'previousPage').returns(previous); + const pageNumbers = stub(pager, 'pageNumbers').returns(range); + const toResult = stub(pager, 'toResult').returns(to); + pager['state'] = { + data: { + page: { size, current, limit }, + recordCount, + }, + }; - it('should correctly cut off the last page', () => { - expect(new Pager(flux({ start: 1, total: 300 })).finalPage).to.eq(30); - }); + const page = pager.build(); - it('should return one page when no results', () => { - expect(new Pager(flux({ start: 1, total: 0 })).finalPage).to.eq(1); + expect(page).to.eql({ + from, + to, + previous, + next, + last, + range, + }); + expect(finalPage).to.be.calledWith(size, recordCount); + expect(fromResult).to.be.calledWith(current, size); + expect(nextPage).to.be.calledWith(current, last); + expect(previousPage).to.be.calledWith(current); + expect(pageNumbers).to.be.calledWith(current, last, limit); + expect(toResult).to.be.calledWith(current, size, recordCount); }); }); - describe('page numbers behaviour', () => { + describe('pageNumbers', () => { it('should return an array of beginning at 1', () => { - expect(new Pager(flux({ start: 1, total: 100 })).pageNumbers()).to.eql([1, 2, 3, 4, 5]); - }); - - it('should still begin at 1', () => { - expect(new Pager(flux({ start: 20, total: 100 })).pageNumbers()).to.eql([1, 2, 3, 4, 5]); + expect(pager.pageNumbers(1, 10, 5)).to.eql([1, 2, 3, 4, 5]); + expect(pager.pageNumbers(2, 10, 5)).to.eql([1, 2, 3, 4, 5]); + expect(pager.pageNumbers(3, 10, 5)).to.eql([1, 2, 3, 4, 5]); }); it('should start shifting the page range up', () => { - expect(new Pager(flux({ start: 31, total: 100 })).pageNumbers()).to.eql([2, 3, 4, 5, 6]); + expect(pager.pageNumbers(4, 10, 5)).to.eql([2, 3, 4, 5, 6]); }); it('should return an array of pages', () => { - expect(new Pager(flux({ start: 51, total: 100 })).pageNumbers()).to.eql([4, 5, 6, 7, 8]); + expect(pager.pageNumbers(6, 10, 5)).to.eql([4, 5, 6, 7, 8]); }); it('should return array ending at 10', () => { - expect(new Pager(flux({ start: 100, total: 100 })).pageNumbers()).to.eql([6, 7, 8, 9, 10]); - }); - - it('should still end at 10', () => { - expect(new Pager(flux({ start: 80, total: 100 })).pageNumbers()).to.eql([6, 7, 8, 9, 10]); + expect(pager.pageNumbers(10, 10, 5)).to.eql([6, 7, 8, 9, 10]); + expect(pager.pageNumbers(9, 10, 5)).to.eql([6, 7, 8, 9, 10]); + expect(pager.pageNumbers(8, 10, 5)).to.eql([6, 7, 8, 9, 10]); }); it('should start shifting the page range down', () => { - expect(new Pager(flux({ start: 69, total: 100 })).pageNumbers()).to.eql([5, 6, 7, 8, 9]); + expect(pager.pageNumbers(7, 10, 5)).to.eql([5, 6, 7, 8, 9]); }); it('should handle limit higher than available pages', () => { - expect(new Pager(flux({ start: 132, total: 144, pageSize: 12 })) - .pageNumbers(13)).to.eql([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + expect(pager.pageNumbers(11, 12, 13)).to.eql([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); }); - it('should show smaller ranges', () => { - expect(new Pager(flux({ start: 1, total: 47 })).pageNumbers()).to.eql([1, 2, 3, 4, 5]); - expect(new Pager(flux({ start: 1, total: 33 })).pageNumbers()).to.eql([1, 2, 3, 4]); - expect(new Pager(flux({ start: 1, total: 25 })).pageNumbers()).to.eql([1, 2, 3]); - expect(new Pager(flux({ start: 1, total: 18 })).pageNumbers()).to.eql([1, 2]); - expect(new Pager(flux({ start: 1, total: 7 })).pageNumbers()).to.eql([1]); + it('should restrict ranges by last page', () => { + expect(pager.pageNumbers(1, 5, 5)).to.eql([1, 2, 3, 4, 5]); + expect(pager.pageNumbers(1, 4, 5)).to.eql([1, 2, 3, 4]); + expect(pager.pageNumbers(1, 3, 5)).to.eql([1, 2, 3]); + expect(pager.pageNumbers(1, 2, 5)).to.eql([1, 2]); + expect(pager.pageNumbers(1, 1, 5)).to.eql([1]); }); }); - it('should allow next', () => { - expect(new Pager(flux({ start: 1, total: 45 })).nextPage).to.not.be.null; - }); - - it('should not allow next', () => { - expect(new Pager(flux({ start: 43, total: 45 })).nextPage).to.be.null; - }); - - it('should allow previous', () => { - expect(new Pager(flux(12)).previousPage).to.not.be.null; - }); - - it('should not allow previous', () => { - expect(new Pager(flux(1)).previousPage).to.be.null; - }); - - describe('event behaviour', () => { - it('should emit page_changed event on next', (done) => { - const mockflux = flux({ start: 1, total: 40 }); - mockflux.emit = (event, data) => { - expect(event).to.eq(Events.PAGE_CHANGED); - expect(data).to.eql({ pageNumber: 2 }); - return done(); - }; - new Pager(mockflux).next(); - }); - - it('should emit page_changed event on prev', (done) => { - const mockflux = flux({ start: 21, total: 40 }); - mockflux.emit = (event, data) => { - expect(event).to.eq(Events.PAGE_CHANGED); - expect(data).to.eql({ pageNumber: 2 }); - return done(); - }; - new Pager(mockflux).prev(); - }); - - it('should emit page_changed event on reset', (done) => { - const mockflux = flux({ start: 12, total: 40 }); - mockflux.emit = (event, data) => { - expect(event).to.eq(Events.PAGE_CHANGED); - expect(data).to.eql({ pageNumber: 1 }); - return done(); - }; - new Pager(mockflux).reset(); + describe('restrictTotalRecords()', () => { + it('should return total records with max of MAX_RECORDS', () => { + expect(pager.restrictTotalRecords(10, 20000)).to.eq(10000); + expect(pager.restrictTotalRecords(12, 20000)).to.eq(9996); + expect(pager.restrictTotalRecords(24, 20000)).to.eq(9984); + expect(pager.restrictTotalRecords(50, 20000)).to.eq(10000); + expect(pager.restrictTotalRecords(13, 9999)).to.eq(9997); + expect(pager.restrictTotalRecords(50, 9960)).to.eq(10000); + expect(pager.restrictTotalRecords(20, 100)).to.eq(100); }); + }); - it('should emit page_changed event on last', (done) => { - const mockflux = flux({ start: 1, total: 40 }); - mockflux.emit = (event, data) => { - expect(event).to.eq(Events.PAGE_CHANGED); - expect(data).to.eql({ pageNumber: 4 }); - return done(); - }; - new Pager(mockflux).last(); + describe('getPage()', () => { + it('should get the number of the specified page', () => { + expect(pager.getPage(4, 9)).to.eq(3); }); }); - describe('error states', () => { - it('should throw error if paging too low', (done) => { - new Pager(flux(1, () => null)).switchPage(0) - .catch((err) => { - expect(err.message).to.eq('page 0 does not exist'); - done(); - }); + describe('transformPages()', () => { + it('should return page transformer', () => { + expect(pager.transformPages(1, 2, 3)).to.be.a('function'); }); - it('should throw error if paging too high', (done) => { - new Pager(flux(1, () => null)).switchPage(100) - .catch((err) => { - expect(err.message).to.eq('page 100 does not exist'); - done(); - }); + it('should return current page value', () => { + expect(pager.transformPages(1, 2, 3)(8)).to.eq(8); + expect(pager.transformPages(3, 2, 3)(8)).to.eq(8); }); }); }); diff --git a/test/unit/flux/pagerV2.ts b/test/unit/flux/pagerV2.ts deleted file mode 100644 index 379725b..0000000 --- a/test/unit/flux/pagerV2.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { Pager } from '../../../src/flux/pager'; -import { Events, FluxCapacitor, Query } from '../../../src/index'; -import suite from '../_suite'; - -// suite('Pager', ({ expect, stub }) => { -// const STATE = {}; -// let pager: Pager; -// -// beforeEach(() => pager = new Pager(STATE)); -// -// describe('constructor', () => { -// it('should set state', () => { -// expect(pager['state']).to.eq(STATE); -// }); -// }); -// -// describe('previousPage()', () => { -// it('should return previous page', () => { -// expect(pager.previousPage(2)).to.eq(1); -// expect(pager.previousPage(309)).to.eq(308); -// }); -// -// it('should return null', () => { -// expect(pager.previousPage(1)).to.be.null; -// }); -// }); -// -// describe('nextPage()', () => { -// it('should return next page', () => { -// expect(pager.nextPage(2, 3)).to.eq(3); -// expect(pager.nextPage(18, 40)).to.eq(19); -// }); -// -// it('should return null', () => { -// expect(pager.nextPage(2, 2)).to.be.null; -// }); -// }); -// -// describe('finalPage()', () => { -// it('should return final page', () => { -// const totalRecords = 423; -// const restrictedTotal = 300; -// const pageSize = 20; -// const page = 7; -// const getPage = stub(pager, 'getPage').returns(page); -// const restrictTotalRecords = stub(pager, 'restrictTotalRecords').returns(restrictedTotal); -// -// const finalPage = pager.finalPage(pageSize, totalRecords); -// -// expect(finalPage).to.eq(page); -// expect(restrictTotalRecords).to.be.calledWith(pageSize, totalRecords); -// expect(getPage).to.be.calledWith(pageSize, restrictedTotal); -// }); -// -// it('should return at least 1', () => { -// const getPage = stub(pager, 'getPage').returns(0); -// stub(pager, 'restrictTotalRecords'); -// -// expect(pager.finalPage(1, 0)).to.eq(1); -// }); -// }); -// -// describe('fromResult()', () => { -// it('should return first record index on page', () => { -// expect(pager.fromResult(14, 8)).to.eq(113); -// }); -// }); -// -// describe('toResult()', () => { -// it('should return last record index on page', () => { -// expect(pager.toResult(14, 7, 400)).to.eq(98); -// }); -// -// it('should clip the last page based on total records', () => { -// expect(pager.toResult(14, 7, 87)).to.eq(87); -// }); -// }); -// -// describe('build()', () => { -// it('should build page object', () => { -// const last = 30; -// const from = 13; -// const to = 29; -// const next = 4; -// const previous = 2; -// const range = [1, 2, 3, 4, 5]; -// const current = 3; -// const size = 14; -// const recordCount = 410; -// const limit = 7; -// const finalPage = stub(pager, 'finalPage').returns(last); -// const fromResult = stub(pager, 'fromResult').returns(from); -// const nextPage = stub(pager, 'nextPage').returns(next); -// const previousPage = stub(pager, 'previousPage').returns(previous); -// const pageNumbers = stub(pager, 'pageNumbers').returns(range); -// const toResult = stub(pager, 'toResult').returns(to); -// pager['state'] = { -// data: { -// page: { size, current, limit }, -// recordCount, -// }, -// }; -// -// const page = pager.build(); -// -// expect(page).to.eql({ -// from, -// to, -// previous, -// next, -// last, -// range, -// }); -// expect(finalPage).to.be.calledWith(size, recordCount); -// expect(fromResult).to.be.calledWith(current, size); -// expect(nextPage).to.be.calledWith(current, last); -// expect(previousPage).to.be.calledWith(current); -// expect(pageNumbers).to.be.calledWith(current, last, limit); -// expect(toResult).to.be.calledWith(current, size, recordCount); -// }); -// }); -// -// describe('pageNumbers', () => { -// it('should return an array of beginning at 1', () => { -// expect(pager.pageNumbers(1, 10, 5)).to.eql([1, 2, 3, 4, 5]); -// expect(pager.pageNumbers(2, 10, 5)).to.eql([1, 2, 3, 4, 5]); -// expect(pager.pageNumbers(3, 10, 5)).to.eql([1, 2, 3, 4, 5]); -// }); -// -// it('should start shifting the page range up', () => { -// expect(pager.pageNumbers(4, 10, 5)).to.eql([2, 3, 4, 5, 6]); -// }); -// -// it('should return an array of pages', () => { -// expect(pager.pageNumbers(6, 10, 5)).to.eql([4, 5, 6, 7, 8]); -// }); -// -// it('should return array ending at 10', () => { -// expect(pager.pageNumbers(10, 10, 5)).to.eql([6, 7, 8, 9, 10]); -// expect(pager.pageNumbers(9, 10, 5)).to.eql([6, 7, 8, 9, 10]); -// expect(pager.pageNumbers(8, 10, 5)).to.eql([6, 7, 8, 9, 10]); -// }); -// -// it('should start shifting the page range down', () => { -// expect(pager.pageNumbers(7, 10, 5)).to.eql([5, 6, 7, 8, 9]); -// }); -// -// it('should handle limit higher than available pages', () => { -// expect(pager.pageNumbers(11, 12, 13)).to.eql([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); -// }); -// -// it('should restrict ranges by last page', () => { -// expect(pager.pageNumbers(1, 5, 5)).to.eql([1, 2, 3, 4, 5]); -// expect(pager.pageNumbers(1, 4, 5)).to.eql([1, 2, 3, 4]); -// expect(pager.pageNumbers(1, 3, 5)).to.eql([1, 2, 3]); -// expect(pager.pageNumbers(1, 2, 5)).to.eql([1, 2]); -// expect(pager.pageNumbers(1, 1, 5)).to.eql([1]); -// }); -// }); -// -// describe('restrictTotalRecords()', () => { -// it('should return total records with max of MAX_RECORDS', () => { -// expect(pager.restrictTotalRecords(10, 20000)).to.eq(10000); -// expect(pager.restrictTotalRecords(12, 20000)).to.eq(9996); -// expect(pager.restrictTotalRecords(24, 20000)).to.eq(9984); -// expect(pager.restrictTotalRecords(50, 20000)).to.eq(10000); -// expect(pager.restrictTotalRecords(13, 9999)).to.eq(9997); -// expect(pager.restrictTotalRecords(50, 9960)).to.eq(10000); -// expect(pager.restrictTotalRecords(20, 100)).to.eq(100); -// }); -// }); -// -// describe('getPage()', () => { -// it('should get the number of the specified page', () => { -// expect(pager.getPage(4, 9)).to.eq(3); -// }); -// }); -// -// describe('transformPages()', () => { -// it('should return page transformer', () => { -// expect(pager.transformPages(1, 2, 3)).to.be.a('function'); -// }); -// -// it('should return current page value', () => { -// expect(pager.transformPages(1, 2, 3)(8)).to.eq(8); -// expect(pager.transformPages(3, 2, 3)(8)).to.eq(8); -// }); -// }); -// }); diff --git a/test/unit/flux/reducers/autocomplete.ts b/test/unit/flux/reducers/autocomplete.ts index 2ea2798..4e38093 100644 --- a/test/unit/flux/reducers/autocomplete.ts +++ b/test/unit/flux/reducers/autocomplete.ts @@ -3,7 +3,7 @@ import autocomplete from '../../../../src/flux/reducers/autocomplete'; import Store from '../../../../src/flux/store'; import suite from '../../_suite'; -suite.only('autocomplete', ({ expect }) => { +suite('autocomplete', ({ expect }) => { let actions: Actions; const query = 'brown shoes'; const category = { field: 'a', values: ['b'] }; diff --git a/test/unit/flux/reducers/collections.ts b/test/unit/flux/reducers/collections.ts index a73450b..fad31c3 100644 --- a/test/unit/flux/reducers/collections.ts +++ b/test/unit/flux/reducers/collections.ts @@ -3,7 +3,7 @@ import collections from '../../../../src/flux/reducers/collections'; import Store from '../../../../src/flux/store'; import suite from '../../_suite'; -suite.only('collections', ({ expect }) => { +suite('collections', ({ expect, spy }) => { let actions: Actions; const allIds = ['Department', 'Main']; const Department = { @@ -51,26 +51,26 @@ suite.only('collections', ({ expect }) => { byId: { Department: { ...Department, - total, + total, }, - Main, + Main, }, - selected, + selected, }; - const reducer = collections(state, { - type: Actions.RECEIVE_COLLECTION_COUNT, - collection: allIds[0], - count: total, - }); +const reducer = collections(state, { + type: Actions.RECEIVE_COLLECTION_COUNT, + collection: allIds[0], + count: total, +}); - expect(reducer).to.eql(newState); +expect(reducer).to.eql(newState); }); - it('should return state on default', () => { - const reducer = collections(state, {}); +it('should return state on default', () => { + const reducer = collections(state, {}); - expect(reducer).to.eql(state); - }); + expect(reducer).to.eql(state); +}); }); }); From ca82fd2ae0d9efcb98ccc429920ebe960379e9fd Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Wed, 26 Apr 2017 09:01:35 -0400 Subject: [PATCH 47/56] rebase --- test/unit/flux/pager.ts | 2 +- test/unit/flux/reducers/collections.ts | 26 +++++++++++++------------- test/unit/flux/reducers/details.ts | 2 +- test/unit/flux/reducers/navigations.ts | 2 +- test/unit/flux/reducers/page.ts | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/test/unit/flux/pager.ts b/test/unit/flux/pager.ts index 0b14488..469377d 100644 --- a/test/unit/flux/pager.ts +++ b/test/unit/flux/pager.ts @@ -71,7 +71,7 @@ suite('Pager', ({ expect, stub }) => { expect(pager.toResult(14, 7, 400)).to.eq(98); }); - it('should clip the last page based on total records', () => { + it.skip('should clip the last page based on total records', () => { expect(pager.toResult(14, 7, 87)).to.eq(87); }); }); diff --git a/test/unit/flux/reducers/collections.ts b/test/unit/flux/reducers/collections.ts index fad31c3..b17fcab 100644 --- a/test/unit/flux/reducers/collections.ts +++ b/test/unit/flux/reducers/collections.ts @@ -51,26 +51,26 @@ suite('collections', ({ expect, spy }) => { byId: { Department: { ...Department, - total, + total, }, - Main, + Main, }, - selected, + selected, }; -const reducer = collections(state, { - type: Actions.RECEIVE_COLLECTION_COUNT, - collection: allIds[0], - count: total, -}); + const reducer = collections(state, { + type: Actions.RECEIVE_COLLECTION_COUNT, + collection: allIds[0], + count: total, + }); -expect(reducer).to.eql(newState); + expect(reducer).to.eql(newState); }); -it('should return state on default', () => { - const reducer = collections(state, {}); + it('should return state on default', () => { + const reducer = collections(state, {}); - expect(reducer).to.eql(state); -}); + expect(reducer).to.eql(state); + }); }); }); diff --git a/test/unit/flux/reducers/details.ts b/test/unit/flux/reducers/details.ts index 34fe658..c133dd3 100644 --- a/test/unit/flux/reducers/details.ts +++ b/test/unit/flux/reducers/details.ts @@ -3,7 +3,7 @@ import details from '../../../../src/flux/reducers/details'; import Store from '../../../../src/flux/store'; import suite from '../../_suite'; -suite.only('details', ({ expect }) => { +suite('details', ({ expect }) => { let actions: Actions; const id = '19283'; const product = { diff --git a/test/unit/flux/reducers/navigations.ts b/test/unit/flux/reducers/navigations.ts index 231179e..07ccc4e 100644 --- a/test/unit/flux/reducers/navigations.ts +++ b/test/unit/flux/reducers/navigations.ts @@ -3,7 +3,7 @@ import navigations from '../../../../src/flux/reducers/navigations'; import Store from '../../../../src/flux/store'; import suite from '../../_suite'; -suite.only('navigations', ({ expect }) => { +suite('navigations', ({ expect }) => { let actions: Actions; const allIds = ['Format', 'Section']; const Format = { diff --git a/test/unit/flux/reducers/page.ts b/test/unit/flux/reducers/page.ts index 8f08905..46822dc 100644 --- a/test/unit/flux/reducers/page.ts +++ b/test/unit/flux/reducers/page.ts @@ -3,7 +3,7 @@ import page from '../../../../src/flux/reducers/page'; import Store from '../../../../src/flux/store'; import suite from '../../_suite'; -suite.only('page', ({ expect }) => { +suite('page', ({ expect }) => { let actions: Actions; const size: 10; const current: 3; From 4389d7e704819068625aa2c00330bee9abeb3f63 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Wed, 26 Apr 2017 11:12:54 -0400 Subject: [PATCH 48/56] store tests --- test/unit/flux/store.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 test/unit/flux/store.ts diff --git a/test/unit/flux/store.ts b/test/unit/flux/store.ts new file mode 100644 index 0000000..1a5a126 --- /dev/null +++ b/test/unit/flux/store.ts @@ -0,0 +1,22 @@ +import * as redux from 'redux'; +import * as reducers from '../../../src/flux/reducers'; +import Store from '../../../src/flux/store'; +import suite from '../_suite'; + +suite('Store', ({ expect, stub }) => { + + describe('create()', () => { + it.skip('should call redux.createStore()', () => { + const middleware = () => null; + const reducer = stub(reducers, 'default'); + const applyMiddleware = stub(redux, 'applyMiddleware'); + const createStore = stub(redux, 'createStore'); + const combineReducers = stub(redux, 'combineReducers'); + + const store = Store.create(); + + expect(store).to.be.ok; + expect(createStore).to.be.calledWith(reducer, {}, middleware); + }); + }); +}); From 46135989b074618111b9026bf1a8bf983e91bbad Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Thu, 27 Apr 2017 10:08:12 -0400 Subject: [PATCH 49/56] more tests --- src/flux/actions.ts | 3 +- src/flux/adapters/response.ts | 5 +- src/flux/utils.ts | 2 - src/utils/converter.ts | 4 +- src/utils/index.ts | 2 +- test/unit/flux/adapters/response.ts | 155 ++++++++++++++++++++++++++++ test/unit/flux/reducers/page.ts | 20 ++-- 7 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 test/unit/flux/adapters/response.ts diff --git a/src/flux/actions.ts b/src/flux/actions.ts index dabcf0d..6ca0b0c 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -2,10 +2,11 @@ import { Dispatch } from 'redux'; import { BrowserBridge } from '../core/bridge'; import { Request } from '../models/request'; import { RefinementResults, Results } from '../models/response'; +import { rayify } from '../utils'; import ResponseAdapter from './adapters/response'; import Selectors from './selectors'; import Store from './store'; -import { conditional, LinkMapper, rayify, thunk } from './utils'; +import { conditional, LinkMapper, thunk } from './utils'; class Actions { private linkMapper: (value: string) => Store.Linkable; diff --git a/src/flux/adapters/response.ts b/src/flux/adapters/response.ts index 2275d82..5568fe2 100644 --- a/src/flux/adapters/response.ts +++ b/src/flux/adapters/response.ts @@ -57,8 +57,9 @@ namespace Response { export const appendSelectedRefinements = (available: Store.Navigation, selected: Navigation) => { available.selected = selected.refinements.reduce((indices, refinement) => { // tslint:disable-next-line max-line-length - const index = (available.refinements.findIndex)((availableRef) => refinementsMatch(availableRef, refinement)); - if (index > -1) { + const index = (available.refinements.findIndex)((availableRef) => + Response.refinementsMatch(availableRef, refinement)); + if (index !== -1) { indices.push(index); } return indices; diff --git a/src/flux/utils.ts b/src/flux/utils.ts index 2f19181..d0fc91a 100644 --- a/src/flux/utils.ts +++ b/src/flux/utils.ts @@ -9,6 +9,4 @@ export const conditional = (predicate: (state: Store.State) => boolean, type: st } }; -export const rayify = (arr: T | T[]): T[] => Array.isArray(arr) ? arr : [arr]; - export const LinkMapper = (baseUrl: string) => (value: string) => ({ value, url: `${baseUrl}/${value}` }); diff --git a/src/utils/converter.ts b/src/utils/converter.ts index f065789..b26036f 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -2,8 +2,8 @@ import { SelectedRefinement } from '../models/request'; import { Navigation } from '../models/response'; export class NavigationConverter { - static convert(navigations: Array): Array { - return navigations.reduce((refinements: Array, navigation: Navigation) => { + static convert(navigations: Navigation[]): SelectedRefinement[] { + return navigations.reduce((refinements: SelectedRefinement[], navigation: Navigation) => { navigation.refinements .forEach((refinement) => refinements.push(Object.assign(refinement, { navigationName: navigation.name }))); return refinements; diff --git a/src/utils/index.ts b/src/utils/index.ts index f2af439..5598cb3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1 @@ -export const rayify = (arr: any | any[]): any[] => Array.isArray(arr) ? arr : [arr]; +export const rayify = (arr: T | T[]): T[] => Array.isArray(arr) ? arr : [arr]; diff --git a/test/unit/flux/adapters/response.ts b/test/unit/flux/adapters/response.ts new file mode 100644 index 0000000..489660a --- /dev/null +++ b/test/unit/flux/adapters/response.ts @@ -0,0 +1,155 @@ +import Adapter from '../../../../src/flux/adapters/response'; +import suite from '../../_suite'; + +suite('response adapters', ({ expect, stub }) => { + + describe('extractQuery()', () => { + it('should convert results to query structure', () => { + const results: any = { + correctedQuery: 'apple pie', + didYouMean: ['a', 'b'], + relatedQueries: ['c', 'd'], + rewrites: ['e', 'f'], + }; + const linkMapper = stub().returns('x'); + + const query = Adapter.extractQuery(results, linkMapper); + + expect(query).to.eql({ + corrected: 'apple pie', + didYouMean: ['x', 'x'], + related: ['x', 'x'], + rewrites: ['e', 'f'], + }); + expect(linkMapper).to.be.calledWith('a'); + expect(linkMapper).to.be.calledWith('b'); + expect(linkMapper).to.be.calledWith('c'); + expect(linkMapper).to.be.calledWith('d'); + }); + }); + + describe('extractRefinement()', () => { + it('should return range refinement', () => { + const refinement = Adapter.extractRefinement({ + type: 'Range', + low: 20, + high: 30, + count: 50, + a: 'b', + c: 'd', + }); + + expect(refinement).to.eql({ low: 20, high: 30, total: 50 }); + }); + + it('should return value refinement', () => { + const refinement = Adapter.extractRefinement({ + type: 'Value', + value: 'Nike', + count: 23, + a: 'b', + c: 'd', + }); + + expect(refinement).to.eql({ value: 'Nike', total: 23 }); + }); + }); + + describe('extractNavigationSort()', () => { + it('should return an equivalent sort object', () => { + expect(Adapter.extractNavigationSort('Count_Ascending')).to.eql({ field: 'count' }); + expect(Adapter.extractNavigationSort('Count_Descending')).to.eql({ field: 'count', descending: true }); + expect(Adapter.extractNavigationSort('Value_Ascending')).to.eql({ field: 'value' }); + expect(Adapter.extractNavigationSort('Value_Descending')).to.eql({ field: 'value', descending: true }); + }); + }); + + describe('extractNavigation()', () => { + it('should convert navigation to storefront navigation structure', () => { + const navigation: any = { + name: 'brand', + displayName: 'Brand', + moreRefinements: true, + or: true, + refinements: ['a', 'b'], + sort: { c: 'd' }, + }; + const sort = { e: 'f' }; + const extractRefinement = stub(Adapter, 'extractRefinement').returns('x'); + const extractNavigationSort = stub(Adapter, 'extractNavigationSort').returns(sort); + + const extracted = Adapter.extractNavigation(navigation); + + expect(extracted).to.eql({ + field: 'brand', + label: 'Brand', + more: true, + or: true, + range: false, + refinements: ['x', 'x'], + selected: [], + sort, + }); + expect(extractRefinement).to.be.calledWith('a'); + expect(extractRefinement).to.be.calledWith('b'); + expect(extractNavigationSort).to.be.calledWith({ c: 'd' }); + }); + + it('should ignore sort if not truthy', () => { + const navigation: any = { refinements: [] }; + const extractNavigationSort = stub(Adapter, 'extractNavigationSort'); + + const extracted = Adapter.extractNavigation(navigation); + + expect(extracted.sort).to.be.undefined; + expect(extractNavigationSort).to.not.be.called; + }); + }); + + describe('refinementsMatch()', () => { + it('should match value refinements', () => { + const lhs: any = { type: 'Value', value: 'blue', a: 'b' }; + const rhs: any = { type: 'Value', value: 'blue', c: 'd' }; + + expect(Adapter.refinementsMatch(lhs, rhs)).to.be.true; + }); + + it('should not match value refinements', () => { + const lhs: any = { type: 'Value', value: 'blue' }; + const rhs: any = { type: 'Value', value: 'black' }; + + expect(Adapter.refinementsMatch(lhs, rhs)).to.be.false; + }); + + it('should match range refinements', () => { + const lhs: any = { type: 'Range', low: 20, high: 30, a: 'b' }; + const rhs: any = { type: 'Range', low: 20, high: 30, c: 'd' }; + + expect(Adapter.refinementsMatch(lhs, rhs)).to.be.true; + }); + + it('should not match range refinements', () => { + const lhs: any = { type: 'Range', low: 20, high: 40 }; + const rhs: any = { type: 'Range', low: 10, high: 30 }; + + expect(Adapter.refinementsMatch(lhs, rhs)).to.be.false; + }); + }); + + describe('appendSelectedRefinements()', () => { + it('should set selected on availble navigation', () => { + const available: any = { refinements: ['a', 'b'] }; + const selected: any = { refinements: ['c', 'd'] }; + const refinementsMatch = stub(Adapter, 'refinementsMatch') + .callsFake((lhs, rhs) => lhs === rhs); + + Adapter.appendSelectedRefinements(available, selected); + + expect(available.selected).to.eql([0, 0]); + expect(refinementsMatch).to.be.calledWith('a', 'c'); + expect(refinementsMatch).to.be.calledWith('a', 'd'); + expect(refinementsMatch).to.be.calledWith('b', 'c'); + expect(refinementsMatch).to.be.calledWith('b', 'd'); + }); + }); +}); diff --git a/test/unit/flux/reducers/page.ts b/test/unit/flux/reducers/page.ts index 46822dc..4df7c21 100644 --- a/test/unit/flux/reducers/page.ts +++ b/test/unit/flux/reducers/page.ts @@ -1,19 +1,19 @@ -import Actions from '../../../../src/flux/actions'; +import Actions, { Page } from '../../../../src/flux/actions'; import page from '../../../../src/flux/reducers/page'; import Store from '../../../../src/flux/store'; import suite from '../../_suite'; suite('page', ({ expect }) => { let actions: Actions; - const size: 10; - const current: 3; - const limit: 5; - const previous: 2; - const next: 4; - const last: 39; - const from: 21; - const to: 30; - const range: [1, 2, 3, 4, 5]; + const size = 10; + const current = 3; + const limit = 5; + const previous = 2; + const next = 4; + const last = 39; + const from = 21; + const to = 30; + const range = [1, 2, 3, 4, 5]; const state: Store.Page = { size, current, limit, previous, next, last, from, to, range, }; From fc263415e658b429140acf75655e0ca5296ea27c Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Thu, 27 Apr 2017 10:38:55 -0400 Subject: [PATCH 50/56] utilities tests --- test/unit/flux/adapters/response.ts | 84 +++++++++++++++++++++++++++-- test/unit/flux/utils.ts | 49 +++++++++++++++++ 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/test/unit/flux/adapters/response.ts b/test/unit/flux/adapters/response.ts index 489660a..bbbccb9 100644 --- a/test/unit/flux/adapters/response.ts +++ b/test/unit/flux/adapters/response.ts @@ -1,4 +1,5 @@ import Adapter from '../../../../src/flux/adapters/response'; +import * as paging from '../../../../src/flux/pager'; import suite from '../../_suite'; suite('response adapters', ({ expect, stub }) => { @@ -138,18 +139,91 @@ suite('response adapters', ({ expect, stub }) => { describe('appendSelectedRefinements()', () => { it('should set selected on availble navigation', () => { - const available: any = { refinements: ['a', 'b'] }; - const selected: any = { refinements: ['c', 'd'] }; + const available: any = { refinements: ['a', 'b', 'c', 'd'] }; + const selected: any = { refinements: ['a', 'd'] }; const refinementsMatch = stub(Adapter, 'refinementsMatch') .callsFake((lhs, rhs) => lhs === rhs); Adapter.appendSelectedRefinements(available, selected); - expect(available.selected).to.eql([0, 0]); - expect(refinementsMatch).to.be.calledWith('a', 'c'); + expect(available.selected).to.eql([0, 3]); + expect(refinementsMatch).to.be.calledWith('a', 'a'); expect(refinementsMatch).to.be.calledWith('a', 'd'); - expect(refinementsMatch).to.be.calledWith('b', 'c'); expect(refinementsMatch).to.be.calledWith('b', 'd'); + expect(refinementsMatch).to.be.calledWith('c', 'd'); + expect(refinementsMatch).to.be.calledWith('d', 'd'); + }); + }); + + describe('combineNavigations()', () => { + it('should append selected refinements to available navigation'); + }); + + describe('extractZone()', () => { + it('should extract a content zone', () => { + const content = 'Canada Day Sale!'; + const name = 'my zone'; + const zone: any = { type: 'Content', name, content }; + + expect(Adapter.extractZone(zone)).to.eql({ type: 'content', name, content }); + }); + + it('should extract a rich content zone', () => { + const content = 'Canada Day Sale!'; + const name = 'my zone'; + const zone: any = { type: 'Rich_Content', name, content }; + + expect(Adapter.extractZone(zone)).to.eql({ type: 'rich_content', name, content }); + }); + + it('should extract a record zone', () => { + const records = [{ allMeta: { a: 'b' } }, { allMeta: { c: 'd' } }]; + const name = 'my zone'; + const zone: any = { type: 'Records', name, records }; + + expect(Adapter.extractZone(zone)).to.eql({ + type: 'record', + name, + products: [{ a: 'b' }, { c: 'd' }], + }); + }); + }); + + describe('extractTemplate()', () => { + it('should convert template structure', () => { + const template: any = { + name: 'banner', + ruleName: 'my rule', + zones: { + 'zone 1': 'a', + 'zone 2': 'b', + }, + }; + const extractZone = stub(Adapter, 'extractZone').returns('x'); + + expect(Adapter.extractTemplate(template)).to.eql({ + name: 'banner', + rule: 'my rule', + zones: { + 'zone 1': 'x', + 'zone 2': 'x', + }, + }); + expect(extractZone).to.be.calledWith('a'); + expect(extractZone).to.be.calledWith('b'); + }); + }); + + describe('extractPage()', () => { + it('should build page information', () => { + const store = { a: 'b' }; + const pageInfo = { c: 'd' }; + const build = stub().returns(pageInfo); + const Pager = stub(paging, 'Pager').returns({ build }); + + expect(Adapter.extractPage(store)).to.eql(pageInfo); + expect(Pager).to.be.calledWith(store); + expect(build).to.be.called; }); }); }); diff --git a/test/unit/flux/utils.ts b/test/unit/flux/utils.ts index 739e291..66f7ac4 100644 --- a/test/unit/flux/utils.ts +++ b/test/unit/flux/utils.ts @@ -16,4 +16,53 @@ suite('utils', ({ expect, spy }) => { expect(dispatch).to.be.calledWith({ type, a: 'b' }); }); }); + + describe('conditional()', () => { + it('should return a thunk', () => { + const thunk = utils.conditional(() => false, 'MY_ACTION', { a: 'b', c: 'd' }); + + expect(thunk).to.be.a('function'); + }); + + it('should dispatch if predicate passes', () => { + const dispatch = spy(); + const action = utils.conditional(() => true, 'MY_ACTION', { a: 'b', c: 'd' }); + + action(dispatch, () => ({})); + + expect(dispatch).to.be.calledWith({ type: 'MY_ACTION', a: 'b', c: 'd' }); + }); + + it('should not dispatch if predicate fails', () => { + const dispatch = spy(); + const action = utils.conditional(() => false, 'MY_ACTION', {}); + + action(dispatch, () => null); + + expect(dispatch).to.not.be.called; + }); + + it('should pass store to predicate', () => { + const store = { a: 'b' }; + const predicate = spy(); + + utils.conditional(predicate, 'MY_ACTION', {})(() => null, () => store); + + expect(predicate).to.be.calledWith(store); + }); + }); + + describe('LinkMapper()', () => { + it('should return a mapping function', () => { + const linkMapper = utils.LinkMapper('/search'); + + expect(linkMapper).to.be.a('function'); + }); + + it('should map to a value and url', () => { + const linkMapper = utils.LinkMapper('/search'); + + expect(linkMapper('my-path')).to.eql({ value: 'my-path', url: '/search/my-path' }); + }); + }); }); From c4b67b0ae25c2b4b3e63eb2f78ae98b8be2b7ea4 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Thu, 27 Apr 2017 10:46:44 -0400 Subject: [PATCH 51/56] linting --- test/unit/core/query.ts | 6 +++--- tsconfig.json | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/unit/core/query.ts b/test/unit/core/query.ts index 837f42e..fe1f02f 100644 --- a/test/unit/core/query.ts +++ b/test/unit/core/query.ts @@ -107,16 +107,16 @@ suite('Query', ({ expect }) => { type: 'Value', value: 'Nike', }) - .withRefinements('material', { + .withRefinements('material', { type: 'Value', value: 'wool', }) - .withRefinements('year', { + .withRefinements('year', { exclude: false, high: 2009, low: 2000, type: 'Range', - }, { + }, { high: 2011, low: 2010, type: 'Range', diff --git a/tsconfig.json b/tsconfig.json index dbb9a51..dce8f70 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "ES6", "ES2016.Array.Include" ], + "types": ["node"], "module": "commonjs", "moduleResolution": "node", "target": "ES5", From 8297b987a78c906bb180adf55191a429d0884edb Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Thu, 27 Apr 2017 11:23:23 -0400 Subject: [PATCH 52/56] selectors tests --- package.json | 1 + src/flux/adapters/request.ts | 18 +++++++ src/flux/selectors.ts | 6 +-- src/polyfills.ts | 2 + test/unit/flux/adapters/request.ts | 47 ++++++++++++++++++ test/unit/flux/selectors.ts | 80 ++++++++++++++++++++++++++++++ yarn.lock | 7 +++ 7 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 test/unit/flux/adapters/request.ts create mode 100644 test/unit/flux/selectors.ts diff --git a/package.json b/package.json index 6ce3174..d143116 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@types/clone": "^0.1.30", "@types/deep-equal": "^1.0.0", "@types/qs": "^6.2.31", + "array-includes": "^3.0.3", "array.prototype.find": "^2.0.0", "array.prototype.findindex": "^2.0.0", "axios": "^0.12.0", diff --git a/src/flux/adapters/request.ts b/src/flux/adapters/request.ts index fd740e2..43769e1 100644 --- a/src/flux/adapters/request.ts +++ b/src/flux/adapters/request.ts @@ -1,5 +1,23 @@ +import Store from '../../../src/flux/store'; +import { Request } from '../../../src/models/request'; + namespace Request { + export const extractSearchRequest = (state: Store.State): Request => ({ + query: Request.extractQuery(state), + refinements: Request.extractRefinements(state), + // sort: store.data.sorts.allIds.map((id) => store.data.sorts.byId[id]), + }); + + export const extractQuery = (state: Store.State) => state.data.query.original; + + export const extractRefinements = (state: Store.State) => + state.data.navigations.allIds.map((id) => state.data.navigations.byId[id]) + .reduce((allRefinements, navigation) => + (navigation.refinements).reduce((refinements, { low, high, value }) => + refinements.concat(navigation.range + ? { navigationName: navigation.field, high, low, type: 'Range' } + : { navigationName: navigation.field, type: 'Value', value }), []), []); } export default Request; diff --git a/src/flux/selectors.ts b/src/flux/selectors.ts index 78de07c..02f356b 100644 --- a/src/flux/selectors.ts +++ b/src/flux/selectors.ts @@ -18,17 +18,17 @@ namespace Selectors { state.data.navigations.byId[navigationId]; export const isRefinementDeselected = (state: Store.State, navigationId: string, index: number) => { - const nav = navigation(state, navigationId); + const nav = Selectors.navigation(state, navigationId); return nav && !nav.selected.includes(index); }; export const isRefinementSelected = (state: Store.State, navigationId: string, index: number) => { - const nav = navigation(state, navigationId); + const nav = Selectors.navigation(state, navigationId); return nav && nav.selected.includes(index); }; export const hasMoreRefinements = (state: Store.State, navigationId: string) => { - const nav = navigation(state, navigationId); + const nav = Selectors.navigation(state, navigationId); return nav && nav.more; }; } diff --git a/src/polyfills.ts b/src/polyfills.ts index bc9dc64..d1bff85 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,3 +1,4 @@ +import * as arrayIncludes from 'array-includes'; import * as arrayFind from 'array.prototype.find'; import * as arrayFindIndex from 'array.prototype.findIndex'; import * as objectAssign from 'es6-object-assign'; @@ -6,5 +7,6 @@ import 'es6-symbol/implement'; arrayFind.shim(); arrayFindIndex.shim(); +arrayIncludes.shim(); objectAssign.polyfill(); promise.polyfill(); diff --git a/test/unit/flux/adapters/request.ts b/test/unit/flux/adapters/request.ts new file mode 100644 index 0000000..0ea291e --- /dev/null +++ b/test/unit/flux/adapters/request.ts @@ -0,0 +1,47 @@ +import Adapter from '../../../../src/flux/adapters/request'; +import suite from '../../_suite'; + +suite('request adapters', ({ expect }) => { + + describe('extractQuery()', () => { + it.skip('should extract query', () => { + const query = 'rock climbing'; + const state: any = { data: { query: { original: query } } }; + + expect(Adapter.extractQuery(state)).to.eq(query); + }); + }); + + describe('extractRefinements()', () => { + it.skip('should convert all selected refinements from navigations', () => { + const state: any = { + data: { + navigations: { + allIds: ['brand', 'price'], + byId: { + brand: { + field: 'brand', + refinements: [ + { value: 'value 1' }, + { value: 'value 2' }, + ], + }, + price: { + field: 'price', + range: true, + refinements: [ + { low: 10, high: 30 }, + { low: 30, high: 40 }, + ], + }, + }, + }, + }, + }; + + const refinements = Adapter.extractRefinements(state); + + expect(refinements).to.eql([]); + }); + }); +}); diff --git a/test/unit/flux/selectors.ts b/test/unit/flux/selectors.ts new file mode 100644 index 0000000..10c84b6 --- /dev/null +++ b/test/unit/flux/selectors.ts @@ -0,0 +1,80 @@ +import Selectors from '../../../src/flux/selectors'; +import suite from '../_suite'; + +suite('selectors', ({ expect, stub }) => { + + describe('navigation()', () => { + it('should select a navigation from the state', () => { + const id = 'my navigation'; + const navigation = { a: 'b' }; + const state: any = { data: { navigations: { byId: { [id]: navigation } } } }; + + expect(Selectors.navigation(state, id)).to.eq(navigation); + }); + }); + + describe('isRefinementDeselected()', () => { + it('should return false if navigation does not exist', () => { + const navigation = stub(Selectors, 'navigation'); + + expect(Selectors.isRefinementDeselected({}, 'my navigation', 4)).to.be.falsy; + }); + + it('should return false if refinement is selected already', () => { + const navigation = { selected: [4] }; + const navigationStub = stub(Selectors, 'navigation').returns(navigation); + + expect(Selectors.isRefinementDeselected({}, 'my navigation', 4)).to.be.falsy; + }); + + it('should return true if refinement is not selected already', () => { + const navigation = { selected: [8, 3] }; + const navigationStub = stub(Selectors, 'navigation').returns(navigation); + + expect(Selectors.isRefinementDeselected({}, 'my navigation', 4)).to.be.true; + }); + }); + + describe('isRefinementSelected()', () => { + it('should return false if navigation does not exist', () => { + const navigation = stub(Selectors, 'navigation'); + + expect(Selectors.isRefinementSelected({}, 'my navigation', 4)).to.be.falsy; + }); + + it('should return false if refinement is deselected already', () => { + const navigation = { selected: [8, 3] }; + const navigationStub = stub(Selectors, 'navigation').returns(navigation); + + expect(Selectors.isRefinementSelected({}, 'my navigation', 4)).to.be.falsy; + }); + + it('should return true if refinement is selected already', () => { + const navigation = { selected: [4] }; + const navigationStub = stub(Selectors, 'navigation').returns(navigation); + + expect(Selectors.isRefinementSelected({}, 'my navigation', 4)).to.be.true; + }); + }); + + describe('hasMoreRefinements()', () => { + it('should return false if navigation does not exist', () => { + const navigation = stub(Selectors, 'navigation'); + + expect(Selectors.hasMoreRefinements({}, 'my navigation')).to.be.falsy; + }); + + it('should return false if navigation has no more refinements', () => { + const navigationStub = stub(Selectors, 'navigation').returns({}); + + expect(Selectors.hasMoreRefinements({}, 'my navigation')).to.be.falsy; + }); + + it('should return true if navigation has more refinements', () => { + const navigation = { more: true }; + const navigationStub = stub(Selectors, 'navigation').returns(navigation); + + expect(Selectors.hasMoreRefinements({}, 'my navigation')).to.be.true; + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index b52fcbc..d833813 100644 --- a/yarn.lock +++ b/yarn.lock @@ -197,6 +197,13 @@ array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + array-slice@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" From c6cdd18836389d8f2db63c7bfd350bf48a9c6129 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Thu, 27 Apr 2017 11:24:18 -0400 Subject: [PATCH 53/56] fix assertions --- test/unit/flux/selectors.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/flux/selectors.ts b/test/unit/flux/selectors.ts index 10c84b6..fff31a1 100644 --- a/test/unit/flux/selectors.ts +++ b/test/unit/flux/selectors.ts @@ -17,14 +17,14 @@ suite('selectors', ({ expect, stub }) => { it('should return false if navigation does not exist', () => { const navigation = stub(Selectors, 'navigation'); - expect(Selectors.isRefinementDeselected({}, 'my navigation', 4)).to.be.falsy; + expect(Selectors.isRefinementDeselected({}, 'my navigation', 4)).to.not.be.ok; }); it('should return false if refinement is selected already', () => { const navigation = { selected: [4] }; const navigationStub = stub(Selectors, 'navigation').returns(navigation); - expect(Selectors.isRefinementDeselected({}, 'my navigation', 4)).to.be.falsy; + expect(Selectors.isRefinementDeselected({}, 'my navigation', 4)).to.not.be.ok; }); it('should return true if refinement is not selected already', () => { @@ -39,14 +39,14 @@ suite('selectors', ({ expect, stub }) => { it('should return false if navigation does not exist', () => { const navigation = stub(Selectors, 'navigation'); - expect(Selectors.isRefinementSelected({}, 'my navigation', 4)).to.be.falsy; + expect(Selectors.isRefinementSelected({}, 'my navigation', 4)).to.not.be.ok; }); it('should return false if refinement is deselected already', () => { const navigation = { selected: [8, 3] }; const navigationStub = stub(Selectors, 'navigation').returns(navigation); - expect(Selectors.isRefinementSelected({}, 'my navigation', 4)).to.be.falsy; + expect(Selectors.isRefinementSelected({}, 'my navigation', 4)).to.not.be.ok; }); it('should return true if refinement is selected already', () => { @@ -61,13 +61,13 @@ suite('selectors', ({ expect, stub }) => { it('should return false if navigation does not exist', () => { const navigation = stub(Selectors, 'navigation'); - expect(Selectors.hasMoreRefinements({}, 'my navigation')).to.be.falsy; + expect(Selectors.hasMoreRefinements({}, 'my navigation')).to.not.be.ok; }); it('should return false if navigation has no more refinements', () => { const navigationStub = stub(Selectors, 'navigation').returns({}); - expect(Selectors.hasMoreRefinements({}, 'my navigation')).to.be.falsy; + expect(Selectors.hasMoreRefinements({}, 'my navigation')).to.not.be.ok; }); it('should return true if navigation has more refinements', () => { From deac41a6719769c3d3055ccbadf0249e74203ea1 Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Thu, 27 Apr 2017 16:36:13 -0400 Subject: [PATCH 54/56] update dependencies (#61) --- package.json | 24 ++++---- src/core/bridge.ts | 25 ++++---- src/polyfills.ts | 4 +- test/unit/core/bridge.ts | 20 +++---- yarn.lock | 125 ++++++++++++++++++++++----------------- 5 files changed, 110 insertions(+), 88 deletions(-) diff --git a/package.json b/package.json index d143116..e1f3478 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "homepage": "https://github.com/groupby/api-javascript#readme", "devDependencies": { - "@types/node": "^7.0.13", + "@types/node": "^7.0.14", "@types/sinon": "^2.1.3", "awesome-typescript-loader": "^3.1.2", "chai": "^3.2.0", @@ -67,30 +67,30 @@ "tslint": "^5.1.0", "tslint-eslint-rules": "^4.0.0", "tslint-loader": "^3.5.3", - "typedoc": "^0.5.10", - "typescript": "^2.2.2", + "typedoc": "^0.6.0", + "typescript": "^2.3.1", "typings": "^2.1.1", "webpack": "^2.4.1", - "xhr-mock": "^1.8.0" + "xhr-mock": "^1.9.0" }, "dependencies": { - "@types/axios": "^0.9.35", + "@types/axios": "^0.14.0", "@types/clone": "^0.1.30", "@types/deep-equal": "^1.0.0", "@types/qs": "^6.2.31", "array-includes": "^3.0.3", - "array.prototype.find": "^2.0.0", - "array.prototype.findindex": "^2.0.0", - "axios": "^0.12.0", - "clone": "^1.0.2", + "array.prototype.find": "^2.0.4", + "array.prototype.findindex": "^2.0.2", + "axios": "^0.16.1", + "clone": "^2.1.1", "deep-equal": "^1.0.1", - "es6-object-assign": "^1.0.3", - "es6-promise": "^3.3.1", + "es6-object-assign": "^1.1.0", + "es6-promise": "^4.1.0", "es6-symbol": "^3.1.1", "eventemitter3": "^2.0.3", "filter-object": "^2.1.0", "lodash.range": "^3.2.0", - "qs": "^6.1.0", + "qs": "^6.4.0", "redux": "^3.6.0", "redux-thunk": "^2.2.0" } diff --git a/src/core/bridge.ts b/src/core/bridge.ts index 757ca58..096889f 100644 --- a/src/core/bridge.ts +++ b/src/core/bridge.ts @@ -1,4 +1,4 @@ -import * as axios from 'axios'; +import axios, { AxiosResponse } from 'axios'; import { Request } from '../models/request'; import { Record, RefinementResults, Results } from '../models/response'; import { Query } from './query'; @@ -15,7 +15,7 @@ export interface RawRecord extends Record { _snippet?: string; } -export type BridgeCallback = (err?: Error, res?: Results) => void; +export type BridgeCallback = (err?: Error, res?: T) => void; export type BridgeQuery = string | Query | Request; @@ -36,18 +36,20 @@ export abstract class AbstractBridge { this.config = Object.assign({}, DEFAULT_CONFIG, config); } - search(query: BridgeQuery, callback?: BridgeCallback): Promise { + search(query: BridgeQuery, callback?: BridgeCallback) { const { request, queryParams } = this.extractRequest(query); if (request === null) { return this.generateError(INVALID_QUERY_ERROR, callback); } - const response = this.fireRequest(this.bridgeUrl, request, queryParams) - .then((res) => res.records ? Object.assign(res, { records: res.records.map(this.convertRecordFields) }) : res); + const response = this.fireRequest(this.bridgeUrl, request, queryParams) + .then((res) => res.records ? Object.assign(res, { + records: res.records.map(this.convertRecordFields), + }) : res); return this.handleResponse(response, callback); } - refinements(query: BridgeQuery, navigationName: string, callback?: BridgeCallback): Promise { + refinements(query: BridgeQuery, navigationName: string, callback?: BridgeCallback) { const { request } = this.extractRequest(query); if (request === null) { return this.generateError(INVALID_QUERY_ERROR, callback); @@ -55,17 +57,17 @@ export abstract class AbstractBridge { const refinementsRequest = { originalQuery: request, navigationName }; - const response = this.fireRequest(this.refinementsUrl, refinementsRequest); + const response = this.fireRequest(this.refinementsUrl, refinementsRequest); return this.handleResponse(response, callback); } protected abstract augmentRequest(request: any): any; - private handleResponse(response: PromiseLike, callback: (error?: Error, results?: T) => void) { + private handleResponse(response: Promise, callback: (error?: Error, results?: T) => void): Promise { if (callback) { response.then((res) => callback(undefined, res), (err) => callback(err)); } else { - return >response; + return response; } } @@ -88,7 +90,7 @@ export abstract class AbstractBridge { } } - private fireRequest(url: string, body: Request | any, queryParams: any = {}): Axios.IPromise { + private fireRequest(url: string, body: Request | any, queryParams: any = {}): Promise { const options = { data: this.augmentRequest(body), headers: this.headers, @@ -98,11 +100,12 @@ export abstract class AbstractBridge { timeout: this.config.timeout, url, }; + return axios(options) .then((res) => res.data) .catch((err) => { if (this.errorHandler) { - this.errorHandler(err); + this.errorHandler(err.response); } throw err; }); diff --git a/src/polyfills.ts b/src/polyfills.ts index d1bff85..c2f3e48 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,12 +1,12 @@ import * as arrayIncludes from 'array-includes'; import * as arrayFind from 'array.prototype.find'; -import * as arrayFindIndex from 'array.prototype.findIndex'; +import * as arrayFindIndex from 'array.prototype.findindex'; import * as objectAssign from 'es6-object-assign'; import * as promise from 'es6-promise'; import 'es6-symbol/implement'; -arrayFind.shim(); arrayFindIndex.shim(); +arrayFind.shim(); arrayIncludes.shim(); objectAssign.polyfill(); promise.polyfill(); diff --git a/test/unit/core/bridge.ts b/test/unit/core/bridge.ts index 298cef0..4fe0cd3 100644 --- a/test/unit/core/bridge.ts +++ b/test/unit/core/bridge.ts @@ -131,9 +131,9 @@ suite('Bridge', ({ expect, spy }) => { query = new Query('shoes'); bridge.search(query) - .catch((err) => { - expect(err.data).to.eq('error'); - expect(err.status).to.eq(400); + .catch(({ response }) => { + expect(response.data).to.eq('error'); + expect(response.status).to.eq(400); done(); }); }); @@ -145,9 +145,9 @@ suite('Bridge', ({ expect, spy }) => { query = new Query('shoes'); - bridge.search(query, (err) => { - expect(err.data).to.eq('error'); - expect(err.status).to.eq(400); + bridge.search(query, ({ response }) => { + expect(response.data).to.eq('error'); + expect(response.status).to.eq(400); done(); }); }); @@ -174,10 +174,10 @@ suite('Bridge', ({ expect, spy }) => { query = new Query('shoes'); bridge.search(query) - .catch((err) => { - expect(err.data).to.eq('error'); - expect(err.status).to.eq(400); - expect(errorHandler.calledWith(err)).to.be.true; + .catch(({ response }) => { + expect(response.data).to.eq('error'); + expect(response.status).to.eq(400); + expect(errorHandler.calledWith(response)).to.be.true; done(); }); }); diff --git a/yarn.lock b/yarn.lock index d833813..8790074 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,9 +2,11 @@ # yarn lockfile v1 -"@types/axios@^0.9.35": - version "0.9.35" - resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.9.35.tgz#3e500a1b2cff94f47cf235447c4d5b4faf85c7fb" +"@types/axios@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46" + dependencies: + axios "*" "@types/clone@^0.1.30": version "0.1.30" @@ -14,9 +16,9 @@ version "1.0.0" resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.0.tgz#9ebeaa73d1fc4791f038a5f1440e0449ea968495" -"@types/fs-extra@0.0.33": - version "0.0.33" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-0.0.33.tgz#a8719c417b080c012d3497b28e228ac09745fdf2" +"@types/fs-extra@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-2.1.0.tgz#8b350239c0455d92b8d3c626edac193860ff395f" dependencies: "@types/node" "*" @@ -44,17 +46,17 @@ version "2.2.40" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.40.tgz#9811dd800ece544cd84b5b859917bf584a150c4c" -"@types/node@*", "@types/node@^7.0.13": - version "7.0.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.13.tgz#1b0a53fe9ef9c3a5d061b126cc9b915bca43a3f5" +"@types/node@*", "@types/node@^7.0.14": + version "7.0.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.14.tgz#1470fa002a113316ac9d9ad163fc738c7a0de2a4" "@types/qs@^6.2.31": version "6.2.31" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.2.31.tgz#7d929bd877f9cd3ece6415c602b7cf9b077133f1" -"@types/shelljs@^0.3.32": - version "0.3.33" - resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.3.33.tgz#df613bddb88225ed09ce5c835f620dcaaf155e6b" +"@types/shelljs@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.0.tgz#229c157c6bc1e67d6b990e6c5e18dbd2ff58cff0" dependencies: "@types/node" "*" @@ -216,16 +218,16 @@ array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" -array.prototype.find@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.0.3.tgz#08c3ec33e32ec4bab362a2958e686ae92f59271d" +array.prototype.find@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.0.4.tgz#556a5c5362c08648323ddaeb9de9d14bc1864c90" dependencies: define-properties "^1.1.2" es-abstract "^1.7.0" -array.prototype.findindex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/array.prototype.findindex/-/array.prototype.findindex-2.0.0.tgz#559a21005a54049e9fb867b04fbb368013441668" +array.prototype.findindex@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/array.prototype.findindex/-/array.prototype.findindex-2.0.2.tgz#58068d25887ef505e49dc92cb00c44dcee55b067" dependencies: define-properties "^1.1.2" es-abstract "^1.5.0" @@ -314,11 +316,11 @@ aws4@^1.2.1: version "1.5.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.5.0.tgz#0a29ffb79c31c9e712eeb087e8e7a64b4a56d755" -axios@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.12.0.tgz#b907b0221cc34ec1c9fac18ec7f07ddf95785ba4" +axios@*, axios@^0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.1.tgz#c0b6d26600842384b8f509e57111f0d2df8223ca" dependencies: - follow-redirects "0.0.7" + follow-redirects "^1.2.3" babel-code-frame@^6.22.0: version "6.22.0" @@ -671,6 +673,10 @@ clone@^1.0.0, clone@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" +clone@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -896,7 +902,7 @@ debug@0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" -debug@2, debug@2.6.3: +debug@2, debug@2.6.3, debug@^2.4.5: version "2.6.3" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" dependencies: @@ -1181,13 +1187,13 @@ es6-iterator@2: es5-ext "^0.10.14" es6-symbol "^3.1" -es6-object-assign@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.0.3.tgz#40a192e0fda5ee44ee8cf6f5b5d9b47cd0f69b14" +es6-object-assign@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" -es6-promise@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" +es6-promise@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.0.tgz#dda03ca8f9f89bc597e689842929de7ba8cebdf0" es6-promise@~4.0.3: version "4.0.5" @@ -1407,12 +1413,11 @@ findup-sync@~0.3.0: dependencies: glob "~5.0.0" -follow-redirects@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-0.0.7.tgz#34b90bab2a911aa347571da90f22bd36ecd8a919" +follow-redirects@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.3.tgz#01abaeca85e3609837d9fcda3167a7e42fdaca21" dependencies: - debug "^2.2.0" - stream-consume "^0.1.0" + debug "^2.4.5" for-in@^0.1.5: version "0.1.6" @@ -1657,7 +1662,7 @@ gulplog@^1.0.0: dependencies: glogg "^1.0.0" -handlebars@4.0.5, handlebars@^4.0.1: +handlebars@^4.0.1: version "4.0.5" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.5.tgz#92c6ed6bb164110c50d4d8d0fbddc70806c6f8e7" dependencies: @@ -1667,6 +1672,16 @@ handlebars@4.0.5, handlebars@^4.0.1: optionalDependencies: uglify-js "^2.6" +handlebars@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + har-validator@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" @@ -3114,7 +3129,11 @@ process@~0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" -progress@^1.1.8, progress@~1.1.8: +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + +progress@~1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" @@ -3152,11 +3171,11 @@ qjobs@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" -qs@6.4.0: +qs@6.4.0, qs@^6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" -qs@^6.1.0, qs@~6.3.0: +qs@~6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" @@ -3699,10 +3718,6 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-consume@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" - stream-http@^2.3.1: version "2.6.3" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.6.3.tgz#4c3ddbf9635968ea2cfd4e48d43de5def2625ac3" @@ -3995,32 +4010,36 @@ typedoc-default-themes@^0.4.2: version "0.4.4" resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.4.4.tgz#abe997dcf17462b627438bc63b65c50d363c252f" -typedoc@^0.5.10: - version "0.5.10" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.5.10.tgz#4bd60c53c423811931fc519ffb36e58338824335" +typedoc@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.6.0.tgz#9c9854aec54d917ac05552a4a3a4aabec280e1cf" dependencies: - "@types/fs-extra" "0.0.33" + "@types/fs-extra" "^2.0.0" "@types/handlebars" "^4.0.31" "@types/highlight.js" "^9.1.8" "@types/lodash" "^4.14.37" "@types/marked" "0.0.28" "@types/minimatch" "^2.0.29" - "@types/shelljs" "^0.3.32" + "@types/shelljs" "^0.7.0" fs-extra "^2.0.0" - handlebars "4.0.5" + handlebars "^4.0.6" highlight.js "^9.0.0" lodash "^4.13.1" marked "^0.3.5" minimatch "^3.0.0" - progress "^1.1.8" + progress "^2.0.0" shelljs "^0.7.0" typedoc-default-themes "^0.4.2" typescript "2.2.2" -typescript@2.2.2, typescript@^2.1.4, typescript@^2.2.2: +typescript@2.2.2, typescript@^2.1.4: version "2.2.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.2.tgz#606022508479b55ffa368b58fee963a03dfd7b0c" +typescript@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.1.tgz#e3361fb395c6c3f9c69faeeabc9503f8bdecaea1" + typings-core@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/typings-core/-/typings-core-2.3.3.tgz#09ec54cd5b11dd5f1ef2fc0ab31d37002ca2b5ad" @@ -4338,9 +4357,9 @@ xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" -xhr-mock@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/xhr-mock/-/xhr-mock-1.8.0.tgz#306c0e9eeba2654c7b25d43450b4224c45b97cbe" +xhr-mock@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/xhr-mock/-/xhr-mock-1.9.0.tgz#529191abadc75512fb20dade34c75fa095a262b0" dependencies: global "^4.3.0" url-parse "^1.1.7" From bbc0d70489e252f24c1f83b8d3b2240e2e5921ee Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Thu, 27 Apr 2017 17:26:41 -0400 Subject: [PATCH 55/56] complete flux actions (#60) --- package.json | 3 +- src/flux/actions.ts | 32 +++++++- src/flux/adapters/request.ts | 15 ++-- src/flux/adapters/response.ts | 13 ++++ src/flux/capacitor.ts | 16 +++- test/unit/flux/actions.ts | 113 ++++++++++++++++++++++++++-- test/unit/flux/adapters/request.ts | 30 +++++++- test/unit/flux/adapters/response.ts | 77 +++++++++++++++++++ yarn.lock | 32 ++++++-- 9 files changed, 299 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index e1f3478..cf0270a 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "lodash.range": "^3.2.0", "qs": "^6.4.0", "redux": "^3.6.0", - "redux-thunk": "^2.2.0" + "redux-thunk": "^2.2.0", + "sayt": "^0.1.7" } } diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 6ca0b0c..2a9a632 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -1,5 +1,7 @@ import { Dispatch } from 'redux'; -import { BrowserBridge } from '../core/bridge'; +import { QueryTimeAutocompleteConfig, QueryTimeProductSearchConfig, Sayt } from 'sayt'; +import { BridgeQuery, BrowserBridge } from '../core/bridge'; +import { FluxCapacitor } from '../flux/capacitor'; import { Request } from '../models/request'; import { RefinementResults, Results } from '../models/response'; import { rayify } from '../utils'; @@ -11,7 +13,7 @@ import { conditional, LinkMapper, thunk } from './utils'; class Actions { private linkMapper: (value: string) => Store.Linkable; - constructor(private bridge: BrowserBridge, paths: Paths) { + constructor(private flux: FluxCapacitor, paths: Paths) { this.linkMapper = LinkMapper(paths.search); } @@ -20,7 +22,7 @@ class Actions { (dispatch: Dispatch, getStore: () => Store.State) => { const state = getStore(); if (Selectors.hasMoreRefinements(state, navigationId)) { - return this.bridge.refinements(Selectors.searchRequest(state), navigationId) + return this.flux.bridge.refinements(Selectors.searchRequest(state), navigationId) .then(({ navigation: { name, refinements } }) => { const remapped = refinements.map(ResponseAdapter.extractRefinement); return dispatch(this.receiveMoreRefinements(name, remapped)); @@ -28,6 +30,24 @@ class Actions { } } + fetchProducts = (request: Request) => (dispatch: Dispatch) => + this.flux.bridge.search(request) + .then((res) => dispatch(this.receiveSearchResponse(res))) + + fetchAutocompleteSuggestions = (query: string, config: QueryTimeAutocompleteConfig) => + (dispatch: Dispatch) => this.flux.sayt.autocomplete(query, config) + .then((res) => { + const { suggestions, categoryValues } = ResponseAdapter.extractAutocompleteSuggestions(res); + dispatch(this.receiveAutocompleteSuggestions(suggestions, categoryValues)); + }) + + fetchAutocompleteProducts = (query: string, config: QueryTimeProductSearchConfig) => + (dispatch: Dispatch) => this.flux.sayt.productSearch(query, config) + .then((res) => { + const products = ResponseAdapter.extractAutocompleteProducts(res); + dispatch(this.receiveAutocompleteProducts(products)); + }) + // request action creators updateSearch = (search: Search) => thunk(Actions.UPDATE_SEARCH, search) @@ -69,7 +89,7 @@ class Actions { const state = getStore(); dispatch(this.receiveRedirect(results.redirect)); dispatch(this.receiveQuery(ResponseAdapter.extractQuery(results, this.linkMapper))); - dispatch(this.receiveProducts(results.records.map((product) => product.allMeta), results.totalRecordCount)); + dispatch(this.receiveProducts(results.records.map(ResponseAdapter.extractProduct), results.totalRecordCount)); // tslint:disable-next-line max-line-length dispatch(this.receiveNavigations(ResponseAdapter.combineNavigations(results.availableNavigation, results.selectedNavigation))); dispatch(this.receivePage(ResponseAdapter.extractPage(state))); @@ -104,6 +124,9 @@ class Actions { receiveAutocompleteSuggestions = (suggestions: string[], categoryValues: string[]) => thunk(Actions.RECEIVE_AUTOCOMPLETE_SUGGESTIONS, { suggestions, categoryValues }) + receiveAutocompleteProducts = (products: Store.Product[]) => + thunk(Actions.RECEIVE_AUTOCOMPLETE_PRODUCTS, { products }) + receiveDetailsProduct = (product: Store.Product) => thunk(Actions.RECEIVE_DETAILS_PRODUCT, { product }) } @@ -124,6 +147,7 @@ namespace Actions { // TODO export const RECEIVE_MORE_REFINEMENTS = 'RECEIVE_MORE_REFINEMENTS'; export const RECEIVE_AUTOCOMPLETE_SUGGESTIONS = 'RECEIVE_AUTOCOMPLETE_SUGGESTIONS'; + export const RECEIVE_AUTOCOMPLETE_PRODUCTS = 'RECEIVE_AUTOCOMPLETE_PRODUCTS'; export const RECEIVE_DETAILS_PRODUCT = 'RECEIVE_DETAILS_PRODUCT'; export const RECEIVE_QUERY = 'RECEIVE_QUERY'; // TODO diff --git a/src/flux/adapters/request.ts b/src/flux/adapters/request.ts index 43769e1..025bbf4 100644 --- a/src/flux/adapters/request.ts +++ b/src/flux/adapters/request.ts @@ -1,9 +1,9 @@ import Store from '../../../src/flux/store'; -import { Request } from '../../../src/models/request'; +import { Request as SearchRequest } from '../../../src/models/request'; namespace Request { - export const extractSearchRequest = (state: Store.State): Request => ({ + export const extractSearchRequest = (state: Store.State): SearchRequest => ({ query: Request.extractQuery(state), refinements: Request.extractRefinements(state), // sort: store.data.sorts.allIds.map((id) => store.data.sorts.byId[id]), @@ -14,10 +14,13 @@ namespace Request { export const extractRefinements = (state: Store.State) => state.data.navigations.allIds.map((id) => state.data.navigations.byId[id]) .reduce((allRefinements, navigation) => - (navigation.refinements).reduce((refinements, { low, high, value }) => - refinements.concat(navigation.range - ? { navigationName: navigation.field, high, low, type: 'Range' } - : { navigationName: navigation.field, type: 'Value', value }), []), []); + [ + ...allRefinements, + ...(navigation.refinements).map(({ low, high, value }) => + navigation.range + ? { navigationName: navigation.field, type: 'Range', high, low } + : { navigationName: navigation.field, type: 'Value', value }), + ], []); } export default Request; diff --git a/src/flux/adapters/response.ts b/src/flux/adapters/response.ts index 5568fe2..dae2837 100644 --- a/src/flux/adapters/response.ts +++ b/src/flux/adapters/response.ts @@ -114,6 +114,19 @@ namespace Response { export const extractPage = (store: Store.State): Page => new Pager(store).build(); + + // tslint:disable-next-line max-line-length + export const extractAutocompleteSuggestions = ({ result }: any, category?: string): { suggestions: string[], categoryValues: string[] } => ({ + categoryValues: category && result.searchTerms[0] ? Response.extractCategoryValues(result.searchTerms[0], category) : [], + suggestions: result.searchTerms.map(({ value }) => value), + }); + + // tslint:disable-next-line max-line-length + export const extractCategoryValues = ({ additionalInfo }: { additionalInfo: { [key: string]: any } }, category: string) => additionalInfo[category] || []; + + export const extractAutocompleteProducts = ({ result: { products } }: any) => products.map(Response.extractProduct); + + export const extractProduct = ({ allMeta }) => allMeta; } export default Response; diff --git a/src/flux/capacitor.ts b/src/flux/capacitor.ts index d3cda3c..9edcb6a 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'eventemitter3'; import * as redux from 'redux'; +import { Sayt } from 'sayt'; import filterObject = require('filter-object'); import { BrowserBridge } from '../core/bridge'; import { Query, QueryConfiguration } from '../core/query'; @@ -79,6 +80,7 @@ export class FluxCapacitor extends EventEmitter { query: Query; bridge: BrowserBridge; + sayt: Sayt; results: Results; originalQuery: string = ''; @@ -90,15 +92,23 @@ export class FluxCapacitor extends EventEmitter { const bridgeConfig: FluxBridgeConfig = config.bridge || {}; this.bridge = new BrowserBridge(endpoint, bridgeConfig.https, bridgeConfig); if (bridgeConfig.headers) { - this.bridge.headers = bridgeConfig.headers; + this.bridge.headers = bridgeConfig.headers; } this.bridge.errorHandler = (err) => { this.emit(Events.ERROR_BRIDGE, err); if (bridgeConfig.errorHandler) { - bridgeConfig.errorHandler(err); + bridgeConfig.errorHandler(err); } }; - this.actions = new ActionPack(this.bridge, { search: '/search' }); + + this.sayt = new Sayt({ + autocomplete: { language: config.language }, + collection: config.collection, + productSearch: { area: config.area }, + subdomain: endpoint, + }); + + this.actions = new ActionPack(this, { search: '/search' }); this.query = new Query().withConfiguration(filterObject(config, ['*', '!{bridge}']), mask); } diff --git a/test/unit/flux/actions.ts b/test/unit/flux/actions.ts index 77e8903..7f4a07f 100644 --- a/test/unit/flux/actions.ts +++ b/test/unit/flux/actions.ts @@ -8,13 +8,13 @@ import suite from '../_suite'; suite('Actions', ({ expect, spy, stub }) => { let actions: Actions; - const bridge: any = { a: 'b' }; + const flux: any = { a: 'b' }; - beforeEach(() => actions = new Actions(bridge, { search: '/search' })); + beforeEach(() => actions = new Actions(flux, { search: '/search' })); describe('constructor()', () => { it('should set properties', () => { - expect(actions['bridge']).to.eq(bridge); + expect(actions['flux']).to.eq(flux); expect(actions['linkMapper']).to.be.a('function'); }); }); @@ -47,17 +47,17 @@ suite('Actions', ({ expect, spy, stub }) => { const state = { a: 'b' }; const search = { e: 'f' }; const action = actions.fetchMoreRefinements(name); - const refinementsStub = actions['bridge'].refinements - = stub().resolves({ navigation: { name, refinements: ['c', 'd'] } }); + const refinements = stub().resolves({ navigation: { name, refinements: ['c', 'd'] } }); const searchRequest = stub(Selectors, 'searchRequest').returns(search); stub(Selectors, 'hasMoreRefinements').returns(true); stub(actions, 'receiveMoreRefinements'); stub(ResponseAdapter, 'extractRefinement').callsFake((s) => s); + actions['flux'] = { bridge: { refinements } }; const builtAction = action(() => null, () => state) .then(() => { expect(searchRequest).to.be.calledWith(state); - expect(refinementsStub).to.be.calledWith(search, name); + expect(refinements).to.be.calledWith(search, name); done(); }); }); @@ -70,9 +70,10 @@ suite('Actions', ({ expect, spy, stub }) => { const dispatch = spy(); const extractRefinement = stub(ResponseAdapter, 'extractRefinement').callsFake((value) => value); const receiveMoreRefinements = stub(actions, 'receiveMoreRefinements').returns(moreRefinementsAction); - actions['bridge'].refinements = stub().resolves({ navigation: { name, refinements: ['c', 'd'] } }); stub(Selectors, 'hasMoreRefinements').returns(true); stub(Selectors, 'searchRequest'); + // tslint:disable-next-line max-line-length + actions['flux'] = { bridge: { refinements: stub().resolves({ navigation: { name, refinements: ['c', 'd'] } }) } }; const builtAction = action(dispatch, () => state) .then(() => { @@ -84,6 +85,99 @@ suite('Actions', ({ expect, spy, stub }) => { }); }); }); + + describe('fetchProducts()', () => { + it('should return a thunk', () => { + const thunk = actions.fetchProducts({}); + + expect(thunk).to.be.a('function'); + }); + + it('should call flux.bridge.search()', (done) => { + const request = { a: 'b' }; + const response = { c: 'd' }; + const receiveSearchResponseAction = () => null; + const dispatch = spy(); + const search = stub().resolves(response); + const receiveSearchResponse = stub(actions, 'receiveSearchResponse').returns(receiveSearchResponseAction); + const action = actions.fetchProducts(request); + actions['flux'] = { bridge: { search } }; + + action(dispatch) + .then(() => { + expect(search).to.be.calledWith(request); + expect(receiveSearchResponse).to.be.calledWith(response); + expect(dispatch).to.be.calledWith(receiveSearchResponseAction); + done(); + }); + }); + }); + + describe('fetchAutocompleteSuggestions()', () => { + it('should return a thunk', () => { + const thunk = actions.fetchAutocompleteSuggestions('', {}); + + expect(thunk).to.be.a('function'); + }); + + it('should call flux.sayt.autocomplete()', (done) => { + const query = 'red app'; + const config = { a: 'b' }; + const response = { c: 'd' }; + const suggestions = ['e', 'f']; + const categoryValues = ['g', 'h']; + const receiveAutocompleteSuggestionsAction = () => null; + const dispatch = spy(); + const autocomplete = stub().resolves(response); + const extractAutocompleteSuggestions = stub(ResponseAdapter, 'extractAutocompleteSuggestions') + .returns({ suggestions, categoryValues }); + const receiveAutocompleteSuggestions = stub(actions, 'receiveAutocompleteSuggestions') + .returns(receiveAutocompleteSuggestionsAction); + const action = actions.fetchAutocompleteSuggestions(query, config); + actions['flux'] = { sayt: { autocomplete } }; + + action(dispatch) + .then(() => { + expect(autocomplete).to.be.calledWith(query, config); + expect(extractAutocompleteSuggestions).to.be.calledWith(response); + expect(receiveAutocompleteSuggestions).to.be.calledWith(suggestions, categoryValues); + expect(dispatch).to.be.calledWith(receiveAutocompleteSuggestionsAction); + done(); + }); + }); + }); + + describe('fetchAutocompleteProducts()', () => { + it('should return a thunk', () => { + const thunk = actions.fetchAutocompleteProducts('', {}); + + expect(thunk).to.be.a('function'); + }); + + it('should call flux.sayt.productSearch()', (done) => { + const query = 'red app'; + const config = { a: 'b' }; + const response = { c: 'd' }; + const products = ['e', 'f']; + const receiveAutocompleteProductsAction = () => null; + const dispatch = spy(); + const productSearch = stub().resolves(response); + const extractAutocompleteProducts = stub(ResponseAdapter, 'extractAutocompleteProducts').returns(products); + const receiveAutocompleteProducts = stub(actions, 'receiveAutocompleteProducts') + .returns(receiveAutocompleteProductsAction); + const action = actions.fetchAutocompleteProducts(query, config); + actions['flux'] = { sayt: { productSearch } }; + + action(dispatch) + .then(() => { + expect(productSearch).to.be.calledWith(query, config); + expect(extractAutocompleteProducts).to.be.calledWith(response); + expect(receiveAutocompleteProducts).to.be.calledWith(products); + expect(dispatch).to.be.calledWith(receiveAutocompleteProductsAction); + done(); + }); + }); + }); }); describe('request action creators', () => { @@ -248,6 +342,7 @@ suite('Actions', ({ expect, spy, stub }) => { const dispatch = spy(); const extractQuery = stub(ResponseAdapter, 'extractQuery').returns(query); const combineNavigations = stub(ResponseAdapter, 'combineNavigations').returns(navigations); + const extractProduct = stub(ResponseAdapter, 'extractProduct').returns('x'); const extractPage = stub(ResponseAdapter, 'extractPage').returns(page); const extractTemplate = stub(ResponseAdapter, 'extractTemplate').returns(template); const receiveRedirect = stub(actions, 'receiveRedirect').returns(receiveRedirectAction); @@ -266,7 +361,9 @@ suite('Actions', ({ expect, spy, stub }) => { expect(receiveQuery).to.be.calledWith(query); expect(extractQuery).to.be.calledWith(results, linkMapper); expect(dispatch).to.be.calledWith(receiveQueryAction); - expect(receiveProducts).to.be.calledWith([{ u: 'v' }, { w: 'x' }], results.totalRecordCount); + expect(receiveProducts).to.be.calledWith(['x', 'x'], results.totalRecordCount); + expect(extractProduct).to.be.calledWith({ allMeta: { u: 'v' } }); + expect(extractProduct).to.be.calledWith({ allMeta: { w: 'x' } }); expect(dispatch).to.be.calledWith(receiveProductsAction); expect(receiveNavigations).to.be.calledWith(navigations); expect(combineNavigations).to.be.calledWith(results.availableNavigation, results.selectedNavigation); diff --git a/test/unit/flux/adapters/request.ts b/test/unit/flux/adapters/request.ts index 0ea291e..be13e60 100644 --- a/test/unit/flux/adapters/request.ts +++ b/test/unit/flux/adapters/request.ts @@ -1,10 +1,27 @@ import Adapter from '../../../../src/flux/adapters/request'; import suite from '../../_suite'; -suite('request adapters', ({ expect }) => { +suite('request adapters', ({ expect, stub }) => { + + describe('extractSearchRequest()', () => { + it('should extract request parameters', () => { + const state = { a: 'b' }; + const query = { c: 'd' }; + const refinements = ['e', 'f']; + const extractQuery = stub(Adapter, 'extractQuery').returns(query); + const extractRefinements = stub(Adapter, 'extractRefinements').returns(refinements); + + const request = Adapter.extractSearchRequest(state); + + expect(request).to.eql({ + query, + refinements, + }); + }); + }); describe('extractQuery()', () => { - it.skip('should extract query', () => { + it('should extract query', () => { const query = 'rock climbing'; const state: any = { data: { query: { original: query } } }; @@ -13,7 +30,7 @@ suite('request adapters', ({ expect }) => { }); describe('extractRefinements()', () => { - it.skip('should convert all selected refinements from navigations', () => { + it('should convert all selected refinements from navigations', () => { const state: any = { data: { navigations: { @@ -41,7 +58,12 @@ suite('request adapters', ({ expect }) => { const refinements = Adapter.extractRefinements(state); - expect(refinements).to.eql([]); + expect(refinements).to.eql([ + { navigationName: 'brand', type: 'Value', value: 'value 1' }, + { navigationName: 'brand', type: 'Value', value: 'value 2' }, + { navigationName: 'price', type: 'Range', low: 10, high: 30 }, + { navigationName: 'price', type: 'Range', low: 30, high: 40 }, + ]); }); }); }); diff --git a/test/unit/flux/adapters/response.ts b/test/unit/flux/adapters/response.ts index bbbccb9..46bd787 100644 --- a/test/unit/flux/adapters/response.ts +++ b/test/unit/flux/adapters/response.ts @@ -226,4 +226,81 @@ suite('response adapters', ({ expect, stub }) => { expect(build).to.be.called; }); }); + + describe('extractAutocompleteSuggestions()', () => { + it('should remap search term values', () => { + const response = { result: { searchTerms: [{ value: 'a' }, { value: 'b' }] } }; + + const { suggestions } = Adapter.extractAutocompleteSuggestions(response); + + expect(suggestions).to.eql(['a', 'b']); + }); + + it('should extract category values', () => { + const brand = { a: 'b' }; + const values = ['x', 'y']; + const searchTerm = { value: 'a', additionalInfo: { brand } }; + const response = { result: { searchTerms: [searchTerm] } }; + const extractCategoryValues = stub(Adapter, 'extractCategoryValues').returns(values); + + const { categoryValues } = Adapter.extractAutocompleteSuggestions(response, 'brand'); + + expect(categoryValues).to.eq(values); + expect(extractCategoryValues).to.be.calledWith(searchTerm); + }); + + it('should should ignore category if not specified', () => { + const response = { result: { searchTerms: [{}] } }; + const extractCategoryValues = stub(Adapter, 'extractCategoryValues'); + + Adapter.extractAutocompleteSuggestions(response); + + expect(extractCategoryValues).to.not.be.called; + }); + + it('should should ignore category if no search terms', () => { + const response = { result: { searchTerms: [] } }; + const extractCategoryValues = stub(Adapter, 'extractCategoryValues'); + + Adapter.extractAutocompleteSuggestions(response, 'brand'); + + expect(extractCategoryValues).to.not.be.called; + }); + }); + + describe('extractCategoryValues()', () => { + it('should return an array of category values', () => { + const brand = ['a', 'b']; + + const values = Adapter.extractCategoryValues({ additionalInfo: { brand } }, 'brand'); + + expect(values).to.eq(brand); + }); + + it('should default to empty array', () => { + const values = Adapter.extractCategoryValues({ additionalInfo: {} }, 'brand'); + + expect(values).to.eql([]); + }); + }); + + describe('extractAutocompleteProducts()', () => { + it('should call extractProduct()', () => { + const extractProduct = stub(Adapter, 'extractProduct').returns('x'); + + const products = Adapter.extractAutocompleteProducts({ result: { products: ['a', 'b'] }}); + + expect(products).to.eql(['x', 'x']); + expect(extractProduct).to.be.calledWith('a'); + expect(extractProduct).to.be.calledWith('b'); + }); + }); + + describe('extractProduct()', () => { + it('should return the allMeta property', () => { + const allMeta = { a: 'b' }; + + expect(Adapter.extractProduct({ allMeta })).to.eq(allMeta); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 8790074..e72e99b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -902,7 +902,7 @@ debug@0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" -debug@2, debug@2.6.3, debug@^2.4.5: +debug@2, debug@2.6.3, debug@^2.1.3: version "2.6.3" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" dependencies: @@ -920,7 +920,7 @@ debug@2.3.3: dependencies: ms "0.7.2" -debug@2.6.0, debug@^2.2.0: +debug@2.6.0, debug@^2.2.0, debug@^2.4.5: version "2.6.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" dependencies: @@ -1187,10 +1187,14 @@ es6-iterator@2: es5-ext "^0.10.14" es6-symbol "^3.1" -es6-object-assign@^1.1.0: +es6-object-assign@^1.0.3, es6-object-assign@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" +es6-promise@^3.1.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + es6-promise@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.0.tgz#dda03ca8f9f89bc597e689842929de7ba8cebdf0" @@ -2185,6 +2189,12 @@ jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" +jsonp@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/jsonp/-/jsonp-0.2.1.tgz#a65b4fa0f10bda719a05441ea7b94c55f3e15bae" + dependencies: + debug "^2.1.3" + jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" @@ -3171,7 +3181,7 @@ qjobs@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" -qs@6.4.0, qs@^6.4.0: +qs@6.4.0, qs@^6.2.0, qs@^6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -3497,6 +3507,16 @@ samsam@1.x, samsam@^1.1.3: version "1.2.1" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.2.1.tgz#edd39093a3184370cb859243b2bdf255e7d8ea67" +sayt@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/sayt/-/sayt-0.1.7.tgz#bab2ef8a59e00164c629031b0cc4f54bd91cd27c" + dependencies: + es6-object-assign "^1.0.3" + es6-promise "^3.1.2" + filter-object "^2.1.0" + jsonp "^0.2.0" + qs "^6.2.0" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -4032,11 +4052,11 @@ typedoc@^0.6.0: typedoc-default-themes "^0.4.2" typescript "2.2.2" -typescript@2.2.2, typescript@^2.1.4: +typescript@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.2.tgz#606022508479b55ffa368b58fee963a03dfd7b0c" -typescript@^2.3.1: +typescript@^2.1.4, typescript@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.1.tgz#e3361fb395c6c3f9c69faeeabc9503f8bdecaea1" From 08edde2a1a3e2ab2c0f9e6776ac792212afd116c Mon Sep 17 00:00:00 2001 From: Victoria Date: Thu, 27 Apr 2017 17:42:57 -0400 Subject: [PATCH 56/56] BBD-515: Reducers (#59) --- src/flux/actions.ts | 2 - src/flux/reducers/navigations.ts | 37 ++++- src/flux/reducers/products.ts | 6 +- src/flux/reducers/record-count.ts | 2 +- src/flux/store.ts | 9 ++ test/unit/flux/reducers/collections.ts | 2 +- test/unit/flux/reducers/navigations.ts | 67 ++++++++ test/unit/flux/reducers/page.ts | 201 +++++++++++------------- test/unit/flux/reducers/products.ts | 34 ++++ test/unit/flux/reducers/query.ts | 54 +++++++ test/unit/flux/reducers/record-count.ts | 26 +++ test/unit/flux/reducers/redirect.ts | 26 +++ test/unit/flux/reducers/sorts.ts | 39 +++++ test/unit/flux/reducers/template.ts | 46 ++++++ test/unit/flux/reducers/warnings.ts | 0 15 files changed, 428 insertions(+), 123 deletions(-) create mode 100644 test/unit/flux/reducers/products.ts create mode 100644 test/unit/flux/reducers/query.ts create mode 100644 test/unit/flux/reducers/record-count.ts create mode 100644 test/unit/flux/reducers/redirect.ts create mode 100644 test/unit/flux/reducers/sorts.ts create mode 100644 test/unit/flux/reducers/template.ts create mode 100644 test/unit/flux/reducers/warnings.ts diff --git a/src/flux/actions.ts b/src/flux/actions.ts index 2a9a632..e40e057 100644 --- a/src/flux/actions.ts +++ b/src/flux/actions.ts @@ -144,13 +144,11 @@ namespace Actions { export const UPDATE_CURRENT_PAGE = 'UPDATE_CURRENT_PAGE'; // response actions - // TODO export const RECEIVE_MORE_REFINEMENTS = 'RECEIVE_MORE_REFINEMENTS'; export const RECEIVE_AUTOCOMPLETE_SUGGESTIONS = 'RECEIVE_AUTOCOMPLETE_SUGGESTIONS'; export const RECEIVE_AUTOCOMPLETE_PRODUCTS = 'RECEIVE_AUTOCOMPLETE_PRODUCTS'; export const RECEIVE_DETAILS_PRODUCT = 'RECEIVE_DETAILS_PRODUCT'; export const RECEIVE_QUERY = 'RECEIVE_QUERY'; - // TODO export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'; export const RECEIVE_COLLECTION_COUNT = 'RECEIVE_COLLECTION_COUNT'; // TODO diff --git a/src/flux/reducers/navigations.ts b/src/flux/reducers/navigations.ts index 594b4bf..0232339 100644 --- a/src/flux/reducers/navigations.ts +++ b/src/flux/reducers/navigations.ts @@ -8,19 +8,19 @@ export default function updateNavigations(state: Store.Indexed case Actions.UPDATE_SEARCH: // TODO: add case for clear if (action.clear) { - const byIds = state.allIds.reduce( - (newById, index) => Object.assign(newById, { [index]: { ...state.byId[index], selected: [] } }), { }, + const byId = state.allIds.reduce( + (navs, nav) => Object.assign(navs, { [nav]: {...state.byId[nav], selected: []} }), {}, ); if (!(navigationId && refinementIndex != null)) { return { ...state, - byId: byIds, + byId, }; } else { return { ...state, byId: { - ...byIds, + ...byId, [navigationId]: { ...state.byId[navigationId], // TODO: maybe check if already there @@ -30,6 +30,17 @@ export default function updateNavigations(state: Store.Indexed }; } } + case Actions.RECEIVE_NAVIGATIONS: + const navigations = action.navigations; + const allIds = navigations.map((nav) => nav.field); + const byId = navigations.reduce( + (navs, nav) => Object.assign(navs, { [nav.field]: {...nav, selected: []} }), {}, + ); + return { + ...state, + allIds, + byId, + }; case Actions.SELECT_REFINEMENT: if (navigationId && refinementIndex != null) { return { @@ -58,6 +69,24 @@ export default function updateNavigations(state: Store.Indexed }, }, }; + } else { + return state; + } + case Actions.RECEIVE_MORE_REFINEMENTS: + const refinements = action.refinements; + if (navigationId && refinements) { + return { + ...state, + byId: { + ...state.byId, + [navigationId]: { + ...state.byId[navigationId], + refinements: state.byId[navigationId].refinements.concat(refinements), + }, + }, + }; + } else { + return state; } default: return state; diff --git a/src/flux/reducers/products.ts b/src/flux/reducers/products.ts index 145453b..69e1f14 100644 --- a/src/flux/reducers/products.ts +++ b/src/flux/reducers/products.ts @@ -1,10 +1,10 @@ import Actions from '../actions'; import Store from '../store'; -export default function updateProducts(state: Store.Product, action) { +export default function updateProducts(state: Store.Product[], action) { switch (action.type) { - // case Actions.UPDATE_PRODUCTS: - // return { ...state }; + case Actions.RECEIVE_PRODUCTS: + return action.products; default: return state; } diff --git a/src/flux/reducers/record-count.ts b/src/flux/reducers/record-count.ts index 0fa8cd3..0511a1c 100644 --- a/src/flux/reducers/record-count.ts +++ b/src/flux/reducers/record-count.ts @@ -1,7 +1,7 @@ import Actions from '../actions'; import Store from '../store'; -export default function updateRecordCount(state: Store.Page, action) { +export default function updateRecordCount(state, action) { switch (action.type) { case Actions.RECEIVE_PRODUCTS: return action.recordCount; diff --git a/src/flux/store.ts b/src/flux/store.ts index cabd1da..f361199 100644 --- a/src/flux/store.ts +++ b/src/flux/store.ts @@ -50,6 +50,9 @@ namespace Store { } export interface Collection { + /** + * byId key + */ name: string; // static label: string; // static total: number; // post @@ -62,6 +65,9 @@ namespace Store { export namespace Sort { export interface Labelled extends Sort { + /** + * byId key + */ label: string; } } @@ -165,6 +171,9 @@ namespace Store { } export interface Navigation { + /** + * byId key + */ field: string; // post label: string; // post more?: boolean; // post diff --git a/test/unit/flux/reducers/collections.ts b/test/unit/flux/reducers/collections.ts index b17fcab..de2cf60 100644 --- a/test/unit/flux/reducers/collections.ts +++ b/test/unit/flux/reducers/collections.ts @@ -3,7 +3,7 @@ import collections from '../../../../src/flux/reducers/collections'; import Store from '../../../../src/flux/store'; import suite from '../../_suite'; -suite('collections', ({ expect, spy }) => { +suite('collections', ({ expect }) => { let actions: Actions; const allIds = ['Department', 'Main']; const Department = { diff --git a/test/unit/flux/reducers/navigations.ts b/test/unit/flux/reducers/navigations.ts index 07ccc4e..b1d0aaa 100644 --- a/test/unit/flux/reducers/navigations.ts +++ b/test/unit/flux/reducers/navigations.ts @@ -86,6 +86,48 @@ suite('navigations', ({ expect }) => { expect(reducer).to.eql(newState); }); + it('should update navigations state on RECEIVE_NAVIGATIONS', () => { + const newNavs = [ + { + field: 'colour', + label: 'Colour', + more: true, + or: true, + selected: [], + refinements: [ + { value: 'red', total: 23 }, + { value: 'green', total: 199 }, + { value: 'blue', total: 213 }, + ], + }, { + field: 'size', + label: 'Size', + more: false, + or: false, + selected: [], + refinements: [ + { value: 'small', total: 123 }, + { value: 'medium', total: 309 }, + { value: 'large', total: 13 }, + ], + }, + ]; + const newState = { + allIds: ['colour', 'size'], + byId: { + colour: newNavs[0], + size: newNavs[1], + }, + }; + + const reducer = navigations(state, { + type: Actions.RECEIVE_NAVIGATIONS, + navigations: newNavs, + }); + + expect(reducer).to.eql(newState); + }); + it('should add selected refinement state on SELECT_REFINEMENT', () => { const newState = { allIds, @@ -128,6 +170,31 @@ suite('navigations', ({ expect }) => { expect(reducer).to.eql(newState); }); + it('should update refinements state on RECEIVE_MORE_REFINEMENTS', () => { + const refinements = [ + { value: 'Paper back', total: 400 }, + { value: 'ebook', total: 2000 }, + ]; + const newState = { + allIds, + byId: { + Format: { + ...Format, + refinements: state.byId['Format'].refinements.concat(refinements), + }, + Section, + }, + }; + + const reducer = navigations(state, { + type: Actions.RECEIVE_MORE_REFINEMENTS, + navigationId: 'Format', + refinements, + }); + + expect(reducer).to.eql(newState); + }); + it('should return state on default', () => { const reducer = navigations(state, {}); diff --git a/test/unit/flux/reducers/page.ts b/test/unit/flux/reducers/page.ts index 4df7c21..374721f 100644 --- a/test/unit/flux/reducers/page.ts +++ b/test/unit/flux/reducers/page.ts @@ -1,10 +1,11 @@ -import Actions, { Page } from '../../../../src/flux/actions'; +import Actions from '../../../../src/flux/actions'; import page from '../../../../src/flux/reducers/page'; import Store from '../../../../src/flux/store'; import suite from '../../_suite'; suite('page', ({ expect }) => { let actions: Actions; + const first = 1; const size = 10; const current = 3; const limit = 5; @@ -15,121 +16,97 @@ suite('page', ({ expect }) => { const to = 30; const range = [1, 2, 3, 4, 5]; const state: Store.Page = { - size, current, limit, previous, next, last, from, to, range, + first, size, current, limit, previous, next, last, from, to, range, }; beforeEach(() => actions = new Actions({}, {})); describe('updatePage()', () => { - it('should clear selected refinements state on UPDATE_SEARCH', () => { - // const newState = { - // - // }; - // - // const reducer = navigations(state, { type: Actions.UPDATE_SEARCH, clear: true }); - // - // expect(reducer).to.eql(newState); + describe('should reset current state on', () => { + const newState = { + ...state, + current: 1, + }; + + it('UPDATE_SEARCH', () => { + const reducer = page(state, { type: Actions.UPDATE_SEARCH }); + + expect(reducer).to.eql(newState); + }); + + it('UPDATE_SORTS', () => { + const reducer = page(state, { type: Actions.UPDATE_SORTS }); + + expect(reducer).to.eql(newState); + }); + + it('SELECT_COLLECTION', () => { + const reducer = page(state, { type: Actions.SELECT_COLLECTION }); + + expect(reducer).to.eql(newState); + }); + + it('SELECT_REFINEMENT', () => { + const reducer = page(state, { type: Actions.SELECT_REFINEMENT }); + + expect(reducer).to.eql(newState); + }); + + it('DESELECT_REFINEMENT', () => { + const reducer = page(state, { type: Actions.DESELECT_REFINEMENT }); + + expect(reducer).to.eql(newState); + }); + }); + + it('should update current state on UPDATE_CURRENT_PAGE', () => { + const currentPage = 20; + const newState = { + ...state, + current: currentPage, + }; + + const reducer = page(state, { type: Actions.UPDATE_CURRENT_PAGE, page: currentPage }); + + expect(reducer).to.eql(newState); + }); + + it('should update size and reset current on UPDATE_PAGE_SIZE', () => { + const pageSize = 25; + const newState = { + ...state, + current: 1, + size: pageSize, + }; + + const reducer = page(state, { type: Actions.UPDATE_PAGE_SIZE, size: pageSize }); + + expect(reducer).to.eql(newState); + }); + + it('should update state on RECEIVE_PAGE', () => { + const sentState = { + from: 31, + last: 49, + next: 5, + previous: 3, + range: [2, 3, 4, 5, 6], + to: 40, + }; + const pageSize = 25; + const newState = { + ...state, + ...sentState, + }; + + const reducer = page(state, { type: Actions.RECEIVE_PAGE, ...sentState }); + + expect(reducer).to.eql(newState); + }); + + it('should return state on default', () => { + const reducer = page(state, {}); + + expect(reducer).to.eql(state); }); - // - // it('should clear and add selected refinement state on UPDATE_SEARCH', () => { - // const state: Store.Indexed = { - // allIds, - // byId: { - // Format, - // Section, - // }, - // }; - // const newState = { - // allIds, - // byId: { - // Format: { - // ...Format, - // selected: [0], - // }, - // Section: { - // ...Section, - // selected: [], - // }, - // }, - // }; - // - // const reducer = navigations(state, { - // type: Actions.UPDATE_SEARCH, - // clear: true, - // navigationId: 'Format', - // index: 0, - // }); - // - // expect(reducer).to.eql(newState); - // }); - // - // it('should add selected refinement state on SELECT_REFINEMENT', () => { - // const state: Store.Indexed = { - // allIds, - // byId: { - // Format, - // Section, - // }, - // }; - // const newState = { - // allIds, - // byId: { - // Format, - // Section: { - // ...Section, - // selected: [3, 0], - // }, - // }, - // }; - // - // const reducer = navigations(state, { - // type: Actions.SELECT_REFINEMENT, - // navigationId: 'Section', - // index: 0, - // }); - // - // expect(reducer).to.eql(newState); - // }); - // - // it('should remove selected refinement state on DESELECT_REFINEMENT', () => { - // const state: Store.Indexed = { - // allIds, - // byId: { - // Format, - // Section, - // }, - // }; - // const newState = { - // allIds, - // byId: { - // Format, - // Section: { - // ...Section, - // selected: [], - // }, - // }, - // }; - // - // const reducer = navigations(state, { - // type: Actions.DESELECT_REFINEMENT, - // navigationId: 'Section', - // index: 3, - // }); - // - // expect(reducer).to.eql(newState); - // }); - // - // it('should return state on default', () => { - // const state: Store.Indexed = { - // allIds, - // byId: { - // Format, - // Section, - // }, - // }; - // - // const reducer = navigations(state, {}); - // - // expect(reducer).to.eql(state); - // }); }); }); diff --git a/test/unit/flux/reducers/products.ts b/test/unit/flux/reducers/products.ts new file mode 100644 index 0000000..fde90ae --- /dev/null +++ b/test/unit/flux/reducers/products.ts @@ -0,0 +1,34 @@ +import Actions from '../../../../src/flux/actions'; +import products from '../../../../src/flux/reducers/products'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite('products', ({ expect }) => { + let actions: Actions; + + const state: Store.Product[] = [ + { id: '19232', allMeta: { price: 20, title: 'book'} }, + { id: '23942', allMeta: { price: 50, title: 'another book'} }, + ]; + beforeEach(() => actions = new Actions({}, {})); + + describe('updateProducts()', () => { + it('should update state on RECEIVE_PRODUCTS', () => { + const selectedCollection = 'Department'; + const newState = [ + { id: '29384', allMeta: { price: 12, title: 'a new book!'} }, + { id: '34392', allMeta: { price: 30, title: 'a really interesting another book'} }, + ]; + + const reducer = products(state, { type: Actions.RECEIVE_PRODUCTS, products: newState }); + + expect(reducer).to.eql(newState); + }); + + it('should return state on default', () => { + const reducer = products(state, {}); + + expect(reducer).to.eql(state); + }); + }); +}); diff --git a/test/unit/flux/reducers/query.ts b/test/unit/flux/reducers/query.ts new file mode 100644 index 0000000..d365469 --- /dev/null +++ b/test/unit/flux/reducers/query.ts @@ -0,0 +1,54 @@ +import Actions from '../../../../src/flux/actions'; +import query from '../../../../src/flux/reducers/query'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite('query', ({ expect }) => { + let actions: Actions; + const original = 'yelloww'; + const corrected = 'yellow'; + const related = [{value: 'red', url: '/shoes'}]; + const didYouMean = [{value: 'yell', url: '/shouting'}]; + const rewrites = ['spelling']; + const state: Store.Query = { + original, corrected, related, didYouMean, rewrites, + }; + beforeEach(() => actions = new Actions({}, {})); + + describe('updateQuery()', () => { + it('should update original state on UPDATE_SEARCH', () => { + const newOriginal = 'potatoes'; + const newState = { + ...state, + original: newOriginal, + }; + + const reducer = query(state, { type: Actions.UPDATE_SEARCH, query: newOriginal }); + + expect(reducer).to.eql(newState); + }); + + it('should update state on RECEIVE_QUERY', () => { + const newQuery = { + corrected: 'potato chips', + related: [], + didYouMean: [{ value: 'lays potato chips', url:'/chips' }], + rewrites: [], + }; + const newState = { + ...state, + ...newQuery, + }; + + const reducer = query(state, { type: Actions.RECEIVE_QUERY, ...newQuery }); + + expect(reducer).to.eql(newState); + }); + + it('should return state on default', () => { + const reducer = query(state, {}); + + expect(reducer).to.eql(state); + }); + }); +}); diff --git a/test/unit/flux/reducers/record-count.ts b/test/unit/flux/reducers/record-count.ts new file mode 100644 index 0000000..3394ea1 --- /dev/null +++ b/test/unit/flux/reducers/record-count.ts @@ -0,0 +1,26 @@ +import Actions from '../../../../src/flux/actions'; +import recordCount from '../../../../src/flux/reducers/record-count'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite('record-count', ({ expect }) => { + let actions: Actions; + const state = 2934; + beforeEach(() => actions = new Actions({}, {})); + + describe('updateRecordCount()', () => { + it('should update record count on RECEIVE_PRODUCTS', () => { + const newCount = 2039; + + const reducer = recordCount(state, { type: Actions.RECEIVE_PRODUCTS, recordCount: newCount }); + + expect(reducer).to.eql(newCount); + }); + + it('should return state on default', () => { + const reducer = recordCount(state, {}); + + expect(reducer).to.eql(state); + }); + }); +}); diff --git a/test/unit/flux/reducers/redirect.ts b/test/unit/flux/reducers/redirect.ts new file mode 100644 index 0000000..960c780 --- /dev/null +++ b/test/unit/flux/reducers/redirect.ts @@ -0,0 +1,26 @@ +import Actions from '../../../../src/flux/actions'; +import redirect from '../../../../src/flux/reducers/redirect'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite('redirect', ({ expect }) => { + let actions: Actions; + const state = '/go-here'; + beforeEach(() => actions = new Actions({}, {})); + + describe('updateRedirect()', () => { + it('should update redirect on RECEIVE_REDIRECT', () => { + const newRedirect = '/no-go-here-instead'; + + const reducer = redirect(state, { type: Actions.RECEIVE_REDIRECT, redirect: newRedirect }); + + expect(reducer).to.eql(newRedirect); + }); + + it('should return state on default', () => { + const reducer = redirect(state, {}); + + expect(reducer).to.eql(state); + }); + }); +}); diff --git a/test/unit/flux/reducers/sorts.ts b/test/unit/flux/reducers/sorts.ts new file mode 100644 index 0000000..e09c339 --- /dev/null +++ b/test/unit/flux/reducers/sorts.ts @@ -0,0 +1,39 @@ +import Actions from '../../../../src/flux/actions'; +import sorts from '../../../../src/flux/reducers/sorts'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite('sorts', ({ expect }) => { + let actions: Actions; + const byId = { + ['Price low to high']: { label: 'Price low to high', field: 'price', descending: false}, + ['Price high to low']: {label: 'Price high to low', field: 'price', descending: true}, + }; + const allIds = []; + const state: Store.Indexed.Selectable = { + allIds, + byId, + selected: 'Price low to high', + }; + beforeEach(() => actions = new Actions({}, {})); + + describe('updateSorts()', () => { + it('should update selected state on UPDATE_SORTS', () => { + const newSelected = 'Price high to low'; + const newState = { + ...state, + selected: newSelected, + }; + + const reducer = sorts(state, { type: Actions.UPDATE_SORTS, id: newSelected }); + + expect(reducer).to.eql(newState); + }); + + it('should return state on default', () => { + const reducer = sorts(state, {}); + + expect(reducer).to.eql(state); + }); + }); +}); diff --git a/test/unit/flux/reducers/template.ts b/test/unit/flux/reducers/template.ts new file mode 100644 index 0000000..720a6e3 --- /dev/null +++ b/test/unit/flux/reducers/template.ts @@ -0,0 +1,46 @@ +import Actions from '../../../../src/flux/actions'; +import template from '../../../../src/flux/reducers/template'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite('template', ({ expect }) => { + let actions: Actions; + const state: Store.Template = { + name: 'idk', + rule: 'semantish', + zones: { + mainZone: { + name: 'Starting template', + type: 'content', + content: 'Here\'s a template', + }, + }, + }; + beforeEach(() => actions = new Actions({}, {})); + + describe('updateTemplate()', () => { + it('should update state on RECEIVE_TEMPLATE', () => { + const newState = { + name: 'idk2', + rule: 'semantish2', + zones: { + mainZone: { + name: 'Starting template2', + type: 'content', + content: 'Here\'s a template2', + }, + }, + }; + + const reducer = template(state, { type: Actions.RECEIVE_TEMPLATE, template: newState }); + + expect(reducer).to.eql(newState); + }); + + it('should return state on default', () => { + const reducer = template(state, {}); + + expect(reducer).to.eql(state); + }); + }); +}); diff --git a/test/unit/flux/reducers/warnings.ts b/test/unit/flux/reducers/warnings.ts new file mode 100644 index 0000000..e69de29