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/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..cf0270a 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" @@ -42,53 +39,60 @@ }, "homepage": "https://github.com/groupby/api-javascript#readme", "devDependencies": { - "@types/node": "^7.0.4", - "awesome-typescript-loader": "^2.2.4", + "@types/node": "^7.0.14", + "@types/sinon": "^2.1.3", + "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-chai": "^0.1.0", + "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-sinon": "^1.0.5", + "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": "^1.7.0", - "mocha": "^2.2.5", + "karma-webpack": "^2.0.3", + "mocha": "^3.3.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", - "rimraf": "^2.5.4", - "sinon": "^1.17.6", + "remap-istanbul": "^0.9.5", + "rimraf": "^2.6.1", + "sinon": "^2.1.0", + "sinon-chai": "^2.9.0", "sourcemap-istanbul-instrumenter-loader": "^0.2.0", - "tslint-eslint-rules": "^1.3.0", - "tslint-loader": "^2.1.5", - "typedoc": "^0.4.5", - "typescript": "2.0.2", - "typings": "^1.3.3", - "webpack": "^1.13.2", - "xhr-mock": "^1.6.0" + "tslint": "^5.1.0", + "tslint-eslint-rules": "^4.0.0", + "tslint-loader": "^3.5.3", + "typedoc": "^0.6.0", + "typescript": "^2.3.1", + "typings": "^2.1.1", + "webpack": "^2.4.1", + "xhr-mock": "^1.9.0" }, "dependencies": { - "@types/axios": "^0.9.35", + "@types/axios": "^0.14.0", "@types/clone": "^0.1.30", - "@types/deep-equal": "^0.0.30", - "@types/eventemitter3": "^1.2.0", - "@types/qs": "^6.2.30", - "array.prototype.find": "^2.0.0", - "array.prototype.findindex": "^2.0.0", - "axios": "^0.12.0", - "clone": "^1.0.2", + "@types/deep-equal": "^1.0.0", + "@types/qs": "^6.2.31", + "array-includes": "^3.0.3", + "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", - "eventemitter3": "^1.2.0", + "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", + "sayt": "^0.1.7" } } diff --git a/src/core/bridge.ts b/src/core/bridge.ts index d03af33..096889f 100644 --- a/src/core/bridge.ts +++ b/src/core/bridge.ts @@ -1,7 +1,7 @@ +import axios, { AxiosResponse } 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?: T) => void; export type BridgeQuery = string | Query | Request; export const DEFAULT_CONFIG: BridgeConfig = { - timeout: 1500 + timeout: 1500, }; export abstract class AbstractBridge { @@ -38,32 +36,38 @@ export abstract class AbstractBridge { this.config = Object.assign({}, DEFAULT_CONFIG, config); } - search(query: BridgeQuery, callback?: BridgeCallback): Promise { - let { request, queryParams } = this.extractRequest(query); - if (request === null) return this.generateError(INVALID_QUERY_ERROR, callback); + 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 { - let { request } = this.extractRequest(query); - if (request === null) return this.generateError(INVALID_QUERY_ERROR, callback); + refinements(query: BridgeQuery, navigationName: string, callback?: BridgeCallback) { + const { request } = this.extractRequest(query); + if (request === null) { + return this.generateError(INVALID_QUERY_ERROR, callback); + } 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: Function) { + 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; } } @@ -86,21 +90,22 @@ 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 = { - 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) .catch((err) => { if (this.errorHandler) { - this.errorHandler(err); + this.errorHandler(err.response); } throw err; }); 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/actions.ts b/src/flux/actions.ts new file mode 100644 index 0000000..e40e057 --- /dev/null +++ b/src/flux/actions.ts @@ -0,0 +1,212 @@ +import { Dispatch } from 'redux'; +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'; +import ResponseAdapter from './adapters/response'; +import Selectors from './selectors'; +import Store from './store'; +import { conditional, LinkMapper, thunk } from './utils'; + +class Actions { + private linkMapper: (value: string) => Store.Linkable; + + constructor(private flux: FluxCapacitor, paths: Paths) { + this.linkMapper = LinkMapper(paths.search); + } + + // fetch action creators + fetchMoreRefinements = (navigationId: string) => + (dispatch: Dispatch, getStore: () => Store.State) => { + const state = getStore(); + if (Selectors.hasMoreRefinements(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)); + }); + } + } + + 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) + + 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 }) + + 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 }) + + updateAutocompleteQuery = (query: string) => + conditional((state) => state.data.autocomplete.query !== query, + Actions.UPDATE_AUTOCOMPLETE_QUERY, { query }) + + // response action creators + receiveSearchResponse = (results: Results) => + (dispatch: Dispatch, getStore: () => Store.State) => { + const state = getStore(); + dispatch(this.receiveRedirect(results.redirect)); + dispatch(this.receiveQuery(ResponseAdapter.extractQuery(results, this.linkMapper))); + 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))); + dispatch(this.receiveTemplate(ResponseAdapter.extractTemplate(results.template))); + dispatch(this.receiveCollectionCount(state.data.collections.selected, results.totalRecordCount)); + } + + receiveQuery = (query: Query) => + thunk(Actions.RECEIVE_QUERY, query) + + receiveProducts = (products: Store.Product[], recordCount: number) => + thunk(Actions.RECEIVE_PRODUCTS, { products, recordCount }) + + receiveCollectionCount = (collection: string, count: number) => + thunk(Actions.RECEIVE_COLLECTION_COUNT, { collection, count }) + + receiveNavigations = (navigations: Store.Navigation[]) => + thunk(Actions.RECEIVE_NAVIGATIONS, { navigations }) + + receivePage = (page: Page) => + thunk(Actions.RECEIVE_PAGE, page) + + receiveTemplate = (template: Store.Template) => + thunk(Actions.RECEIVE_TEMPLATE, { template }) + + receiveRedirect = (redirect: string) => + thunk(Actions.RECEIVE_REDIRECT, { redirect }) + + receiveMoreRefinements = (navigationId: string, refinements: any) => + thunk(Actions.RECEIVE_MORE_REFINEMENTS, { navigationId, refinements }) + + 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 }) +} + +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_PAGE_SIZE = 'UPDATE_PAGE_SIZE'; + export const UPDATE_CURRENT_PAGE = 'UPDATE_CURRENT_PAGE'; + + // response actions + 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'; + 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'; + export const RECEIVE_REDIRECT = 'RECEIVE_REDIRECT'; +} + +export default Actions; + +export interface Query { + corrected?: string; + related: Store.Query.Related[]; + didYouMean: Store.Query.DidYouMean[]; + rewrites: string[]; +} + +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 namespace Search { + export type Refinement = ValueRefinement | RangeRefinement; + + export interface BaseRefinement { + field: string; + } + + export interface ValueRefinement extends BaseRefinement { + value: string; + } + + export interface RangeRefinement extends BaseRefinement { + low?: number; + high?: number; + } +} + +export interface Page { + previous: number; + next: number; + last: number; + from: number; + to: 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..025bbf4 --- /dev/null +++ b/src/flux/adapters/request.ts @@ -0,0 +1,26 @@ +import Store from '../../../src/flux/store'; +import { Request as SearchRequest } from '../../../src/models/request'; + +namespace 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]), + }); + + 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) => + [ + ...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 new file mode 100644 index 0000000..dae2837 --- /dev/null +++ b/src/flux/adapters/response.ts @@ -0,0 +1,132 @@ +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) => + Response.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): 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 f47b6a0..9edcb6a 100644 --- a/src/flux/capacitor.ts +++ b/src/flux/capacitor.ts @@ -1,25 +1,62 @@ +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'; import { SelectedRangeRefinement, SelectedValueRefinement, Sort } from '../models/request'; import { Navigation, RefinementResults, Results } from '../models/response'; +import ActionPack from './actions'; +import Observer from './observer'; import { Pager } from './pager'; -import * as EventEmitter from 'eventemitter3'; -import filterObject = require('filter-object'); +import Store from './store'; 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'; + // 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 + 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 + + // sort events + export const SORTS_UPDATED = 'sorts_updated'; // mixed + + // product events + export const PRODUCTS_UPDATED = 'products_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 SELECTED_REFINEMENTS_UPDATED = 'selected_refinements_updated'; // post + + // 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 + + // 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_TOTAL_UPDATED = 'page_total_updated'; // post + export const PAGE_SIZE_UPDATED = 'page_size_updated'; // pre + export const CURRENT_PAGE_UPDATED = 'current_page_updated'; // pre + + // redirect event 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'; + + // error events + export const ERROR_BRIDGE = 'error:bridge'; } export { Pager }; @@ -38,167 +75,77 @@ export interface FluxBridgeConfig { export class FluxCapacitor extends EventEmitter { + store: redux.Store = Store.create(); + actions: ActionPack; + query: Query; bridge: BrowserBridge; + sayt: Sayt; 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) 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); - 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; - }); - } - - refinements(navigationName: string): Promise { - return this.bridge.refinements(this.query, navigationName) - .then((results) => { - this.emit(Events.REFINEMENT_RESULTS, results); - return results; - }); - } - - 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); - } + this.sayt = new Sayt({ + autocomplete: { language: config.language }, + collection: config.collection, + productSearch: { area: config.area }, + subdomain: endpoint, + }); - 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); - } - } + this.actions = new ActionPack(this, { search: '/search' }); - 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; - }); + this.query = new Query().withConfiguration(filterObject(config, ['*', '!{bridge}']), mask); } - refine(refinement: FluxRefinement, config: RefinementConfig = { reset: true }): Promise { - this.query.withSelectedRefinements(refinement); - if (config.skipSearch) return Promise.resolve(this.navigationInfo); - return this.doRefinement(config); + search(query: string = this.originalQuery) { + this.store.dispatch(this.actions.updateSearch({ query })); } - unrefine(refinement: FluxRefinement, config: RefinementConfig = { reset: true }): Promise { - this.query.withoutSelectedRefinements(refinement); - if (config.skipSearch) return Promise.resolve(this.navigationInfo); - return this.doRefinement(config); + refinements(navigationName: string) { + this.store.dispatch(this.actions.fetchMoreRefinements(navigationName)); } - 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; - }); + reset(query: string = null, { field: navigationId, index }: { field: string, index: number } = {}) { + this.store.dispatch(this.actions.updateSearch({ query, navigationId, index, clear: true })); } - 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; - }); + resize(pageSize: number) { + this.store.dispatch(this.actions.updatePageSize(pageSize)); } - private emitQueryChanged(oldQuery: string, newQuery: string) { - if (oldQuery.toLowerCase() !== newQuery.toLowerCase()) { - this.emit(Events.QUERY_CHANGED, newQuery); - } + sort(label: string) { + this.store.dispatch(this.actions.updateSorts(label)); } - private get filteredRequest() { - return filterObject(this.query.raw, '!{query,refinements,skip}'); + refine(navigationName: string, index: number) { + this.store.dispatch(this.actions.selectRefinement(navigationName, index)); } - private resetPaging(reset: boolean): Promise { - return reset ? this.page.reset() : this.search(); + unrefine(navigationName: string, index: number) { + this.store.dispatch(this.actions.deselectRefinement(navigationName, index)); } - private doRefinement({ reset }: RefinementConfig): Promise { - return this.resetPaging(reset) - .then(() => this.emit(Events.REFINEMENTS_CHANGED, this.navigationInfo)) - .then(() => this.navigationInfo); + details(id: string) { + this.store.dispatch(this.actions.updateDetailsId(id)); } - private get navigationInfo(): NavigationInfo { - return { - available: this.results.availableNavigation, - selected: this.results.selectedNavigation - }; + switchCollection(collection: string) { + this.store.dispatch(this.actions.selectCollection(collection)); } } - -export interface NavigationInfo { - available: Navigation[]; - selected: Navigation[]; -} - -export interface RefinementConfig { - reset?: boolean; - skipSearch?: boolean; -} - -export interface RewriteConfig { - skipSearch?: boolean; -} diff --git a/src/flux/observer.ts b/src/flux/observer.ts new file mode 100644 index 0000000..77d3423 --- /dev/null +++ b/src/flux/observer.ts @@ -0,0 +1,107 @@ +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 interface Map { [key: string]: Observer | Map; } + export type Node = Map | Observer | (Observer & Map); + + export function listen(flux: FluxCapacitor) { + let oldState; + + return () => { + const state = flux.store.getState(); + + Observer.resolve(oldState, state, Observer.create(flux)); + + oldState = state; + }; + } + + 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) { + if (Observer.shouldObserve(oldState, newState, observer)) { + observer(oldState, newState); + } + + 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])); + } + + 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); + + return { + data: { + autocomplete: Object.assign(emit(Events.AUTOCOMPLETE_UPDATED), { + products: emit(Events.AUTOCOMPLETE_PRODUCTS_UPDATED), + query: emit(Events.AUTOCOMPLETE_QUERY_UPDATED), + }), + + collections: { + [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) => { + if (oldNavigation.selected !== newNavigation.selected) { + flux.emit(`${Events.SELECTED_REFINEMENTS_UPDATED}:${newNavigation.field}`, newNavigation.selected); + } + }, + }), + + 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), + + 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), + }, + }; + } +} + +export default Observer; diff --git a/src/flux/pager.ts b/src/flux/pager.ts index f45e3bb..a606bfe 100644 --- a/src/flux/pager.ts +++ b/src/flux/pager.ts @@ -1,121 +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 flux: FluxCapacitor) { } + constructor(private state: Store.State) { } - 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(pageSize, this.restrictTotalRecords(pageSize, totalRecords)), 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; - } - - pageExists(page: number): boolean { - return page <= this.finalPage && page >= this.firstPage; - } - - pageNumbers(limit: number = 5): number[] { - return range(1, Math.min(this.finalPage + 1, limit + 1)) - .map(this.transformPages(limit)); + 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), + }; } - 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) { + 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): number { - return Math.ceil(record / this.pageSize); + getPage(pageSize: number, totalRecords: number) { + return Math.ceil(totalRecords / pageSize); } - private transformPages(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 (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/reducers/autocomplete.ts b/src/flux/reducers/autocomplete.ts new file mode 100644 index 0000000..2d8203a --- /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.type) { + 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..c38561b --- /dev/null +++ b/src/flux/reducers/collections.ts @@ -0,0 +1,23 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateCollections(state: Store.Indexed.Selectable, action) { + switch (action.type) { + case Actions.SELECT_COLLECTION: + return { ...state, selected: action.id }; + case Actions.RECEIVE_COLLECTION_COUNT: + const collection = action.collection; + return { + ...state, + byId: { + ...state.byId, + [collection]: { + ...state.byId[collection], + total: action.count, + }, + }, + }; + default: + return state; + } +} diff --git a/src/flux/reducers/details.ts b/src/flux/reducers/details.ts new file mode 100644 index 0000000..bf424c7 --- /dev/null +++ b/src/flux/reducers/details.ts @@ -0,0 +1,13 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateDetails(state: Store.Details, action) { + switch (action.type) { + case Actions.UPDATE_DETAILS_ID: + return { ...state, id: action.id }; + case Actions.RECEIVE_DETAILS_PRODUCT: + return { ...state, product: action.product }; + default: + return state; + } +} diff --git a/src/flux/reducers/errors.ts b/src/flux/reducers/errors.ts new file mode 100644 index 0000000..32acb5f --- /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.type) { + // 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..2378a51 --- /dev/null +++ b/src/flux/reducers/index.ts @@ -0,0 +1,35 @@ +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 recordCount from './record-count'; +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, + recordCount, + redirect, + sorts, + template, + warnings, + }), +}); diff --git a/src/flux/reducers/navigations.ts b/src/flux/reducers/navigations.ts new file mode 100644 index 0000000..0232339 --- /dev/null +++ b/src/flux/reducers/navigations.ts @@ -0,0 +1,94 @@ +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.type) { + case Actions.UPDATE_SEARCH: + // TODO: add case for clear + if (action.clear) { + const byId = state.allIds.reduce( + (navs, nav) => Object.assign(navs, { [nav]: {...state.byId[nav], selected: []} }), {}, + ); + if (!(navigationId && refinementIndex != null)) { + return { + ...state, + byId, + }; + } else { + return { + ...state, + byId: { + ...byId, + [navigationId]: { + ...state.byId[navigationId], + // TODO: maybe check if already there + selected: [refinementIndex], + }, + }, + }; + } + } + 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 { + ...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: + if (navigationId && refinementIndex != null) { + return { + ...state, + byId: { + ...state.byId, + [navigationId]: { + ...state.byId[navigationId], + selected: state.byId[navigationId].selected.filter((index) => index !== refinementIndex), + }, + }, + }; + } 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/page.ts b/src/flux/reducers/page.ts new file mode 100644 index 0000000..8fb18da --- /dev/null +++ b/src/flux/reducers/page.ts @@ -0,0 +1,29 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updatePage(state: Store.Page, action) { + switch (action.type) { + 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 }; + case Actions.RECEIVE_PAGE: + return { + ...state, + 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/products.ts b/src/flux/reducers/products.ts new file mode 100644 index 0000000..69e1f14 --- /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.type) { + case Actions.RECEIVE_PRODUCTS: + return action.products; + default: + return state; + } +} diff --git a/src/flux/reducers/query.ts b/src/flux/reducers/query.ts new file mode 100644 index 0000000..1f64b38 --- /dev/null +++ b/src/flux/reducers/query.ts @@ -0,0 +1,19 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateQuery(state: Store.Query, action) { + switch (action.type) { + case Actions.UPDATE_SEARCH: + return { ...state, original: action.query }; + case Actions.RECEIVE_QUERY: + return { + ...state, + corrected: action.corrected, + didYouMean: action.didYouMean, + related: action.related, + rewrites: action.rewrites, + }; + default: + return state; + } +} diff --git a/src/flux/reducers/record-count.ts b/src/flux/reducers/record-count.ts new file mode 100644 index 0000000..0511a1c --- /dev/null +++ b/src/flux/reducers/record-count.ts @@ -0,0 +1,11 @@ +import Actions from '../actions'; +import Store from '../store'; + +export default function updateRecordCount(state, action) { + switch (action.type) { + case Actions.RECEIVE_PRODUCTS: + return action.recordCount; + default: + return state; + } +} diff --git a/src/flux/reducers/redirect.ts b/src/flux/reducers/redirect.ts new file mode 100644 index 0000000..ca36c05 --- /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.type) { + case Actions.RECEIVE_REDIRECT: + return action.redirect; + default: + return state; + } +} diff --git a/src/flux/reducers/sorts.ts b/src/flux/reducers/sorts.ts new file mode 100644 index 0000000..38fedcc --- /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.type) { + 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..cc2736a --- /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: Store.Template, action) { + switch (action.type) { + case Actions.RECEIVE_TEMPLATE: + return action.template; + default: + return state; + } +} diff --git a/src/flux/reducers/warnings.ts b/src/flux/reducers/warnings.ts new file mode 100644 index 0000000..433fc74 --- /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.type) { + // case Actions.UPDATE_WARNINGS: + // return { ...state }; + default: + return state; + } +} diff --git a/src/flux/selectors.ts b/src/flux/selectors.ts new file mode 100644 index 0000000..02f356b --- /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 = Selectors.navigation(state, navigationId); + return nav && !nav.selected.includes(index); + }; + + export const isRefinementSelected = (state: Store.State, navigationId: string, index: number) => { + const nav = Selectors.navigation(state, navigationId); + return nav && nav.selected.includes(index); + }; + + export const hasMoreRefinements = (state: Store.State, navigationId: string) => { + const nav = Selectors.navigation(state, navigationId); + return nav && nav.more; + }; +} + +export default Selectors; diff --git a/src/flux/store.ts b/src/flux/store.ts new file mode 100644 index 0000000..f361199 --- /dev/null +++ b/src/flux/store.ts @@ -0,0 +1,241 @@ +import * as redux from 'redux'; +import thunk from 'redux-thunk'; +import { Request } from '../models/request'; +import { Results } from '../models/response'; +import reducer from './reducers'; + +namespace Store { + + export interface State { + data?: { + query: Query; // mixed + + recordCount: number; // post + sorts: Indexed.Selectable; // pre + products: Product[]; // post + collections: Indexed.Selectable; // mixed + navigations: Indexed; // mixed + + autocomplete: Autocomplete; // mixed + + page: Page; // mixed + + template: Template; // post + + details: Details; // mixed + + redirect?: string; // post + + errors: string[]; // post + warnings: string[]; // post + }; + ui?: { + [tagName: string]: { + [tagId: number]: any; + }; + }; + } + + export interface Query { + original: string; // pre + corrected?: string; // post + related: Query.Related[]; // post + didYouMean: Query.DidYouMean[]; // post + rewrites: string[]; // post + } + + export namespace Query { + export type Related = Linkable; + export type DidYouMean = Linkable; + } + + export interface Collection { + /** + * byId key + */ + name: string; // static + label: string; // static + total: number; // post + } + + export interface Sort { + field: string; + descending?: boolean; + } + + export namespace Sort { + export interface Labelled extends Sort { + /** + * byId key + */ + label: string; + } + } + + export interface Page { + /** + * number of products per page + */ + size: number; // pre + + /** + * current page number + */ + current: number; // pre + + /** + * number of first page + */ + first: 1; // static + /** + * maximum number of page numbers to display + */ + limit: number; // static + + /** + * number of next page + */ + previous: number; // post + /** + * number of previous page + */ + next: number; // post + /** + * number of last page + */ + last: number; // post + + /** + * start of displayed products + */ + from: number; // post + /** + * end of displayed products + */ + to: 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 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 { + type: 'content'; + content: string; + } + + export interface RichContentZone extends BaseZone { + type: 'rich_content'; + content: string; + } + + export interface RecordZone extends BaseZone { + type: 'record'; + products: Product[]; + } + + export interface Details { + id: string; // pre + product: Product; // post + } + + export interface Product { + id: string; // post + [key: string]: any; // post + } + + export interface Navigation { + /** + * byId key + */ + field: string; // post + label: string; // post + more?: boolean; // post + range?: boolean; // post + or?: boolean; // post + selected: number[]; // pre + refinements: Array; // post + sort?: Sort; // post + } + + export interface BaseRefinement { + total: number; // post + } + + export type Refinement = ValueRefinement | RangeRefinement; + + export interface ValueRefinement extends BaseRefinement { + value: string; // post + } + + export interface RangeRefinement extends BaseRefinement { + low: number; // post + high: number; // post + } + + export interface Autocomplete { + query: string; // pre + suggestions: string[]; // post + category: Autocomplete.Category; // static & post + products: Product[]; // post + } + + export namespace Autocomplete { + export interface Category { + field: string; // static + values: string[]; // post + } + } + + export interface Indexed { + byId: { [key: string]: T }; + allIds: string[]; + } + + export namespace Indexed { + export interface Selectable extends Indexed { + selected: string; + } + } + + export interface Linkable { + value: string; // post + url: string; // post (generated) + } + + export function create() { + return redux.createStore( + reducer, + {}, + redux.applyMiddleware(thunk), + ); + } +} + +export default Store; diff --git a/src/flux/utils.ts b/src/flux/utils.ts new file mode 100644 index 0000000..d0fc91a --- /dev/null +++ b/src/flux/utils.ts @@ -0,0 +1,12 @@ +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 LinkMapper = (baseUrl: string) => (value: string) => ({ value, url: `${baseUrl}/${value}` }); diff --git a/src/models/request.ts b/src/models/request.ts index b93dac0..4e5063a 100644 --- a/src/models/request.ts +++ b/src/models/request.ts @@ -1,39 +1,39 @@ -import { RangeRefinement, Refinement, ValueRefinement } from './response'; +import { RefinementType } 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 { @@ -46,14 +46,19 @@ export interface CustomUrlParam { value: string; } -export interface SelectedRefinement extends Refinement { +export interface SelectedRefinement { + type: RefinementType; navigationName: string; + exclude?: boolean; } -export interface SelectedRangeRefinement extends SelectedRefinement, RangeRefinement { +export interface SelectedRangeRefinement extends SelectedRefinement { + low?: number; + high?: number; } -export interface SelectedValueRefinement extends SelectedRefinement, ValueRefinement { +export interface SelectedValueRefinement extends SelectedRefinement { + value: string; } export interface RestrictNavigation { diff --git a/src/models/response.ts b/src/models/response.ts index ab4475d..8a425dc 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; @@ -34,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; @@ -50,6 +74,7 @@ export interface Record { id: string; url: string; title: string; + collection: string; snippet?: string; allMeta: any; } @@ -60,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/src/polyfills.ts b/src/polyfills.ts index 9e24282..c2f3e48 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,4 +1,12 @@ -require('array.prototype.find').shim(); -require('array.prototype.findindex').shim(); -require('es6-object-assign').polyfill(); -require('es6-promise').polyfill(); +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'; +import * as promise from 'es6-promise'; +import 'es6-symbol/implement'; + +arrayFindIndex.shim(); +arrayFind.shim(); +arrayIncludes.shim(); +objectAssign.polyfill(); +promise.polyfill(); 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 new file mode 100644 index 0000000..5598cb3 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export const rayify = (arr: T | T[]): T[] => Array.isArray(arr) ? arr : [arr]; diff --git a/test/bootstrap.ts b/test/bootstrap.ts new file mode 100644 index 0000000..3b95b1a --- /dev/null +++ b/test/bootstrap.ts @@ -0,0 +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/capacitor.ts b/test/capacitor.ts deleted file mode 100644 index a74ff63..0000000 --- a/test/capacitor.ts +++ /dev/null @@ -1,674 +0,0 @@ -import { Events, FluxCapacitor, Results, SelectedValueRefinement, Sort } from '../src/index'; -import { expect } from 'chai'; -import * as mock from 'xhr-mock'; - -const CUSTOMER_ID = 'services'; -const SEARCH_URL = `http://${CUSTOMER_ID}-cors.groupbycloud.com/api/v1/search`; -const REFINEMENTS_URL = `${SEARCH_URL}/refinements`; -const SELECTED_REFINEMENT: SelectedValueRefinement = { type: 'Value', navigationName: 'brand', value: 'DeWalt' }; -const REFINEMENT_RESULT = { availableNavigation: 'a', selectedNavigation: 'b' }; -const DETAILS_RESULT = { records: [{}] }; - -describe('FluxCapacitor', function() { - let flux: FluxCapacitor; - - beforeEach(() => { - mock.setup(); - flux = new FluxCapacitor(CUSTOMER_ID); - }); - - afterEach(() => { - mock.teardown(); - 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 - } - }); - - 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 } }); - - expect(flux.bridge.headers).to.eq(headers); - }); - - it('should set HTTPS on bridge', () => { - flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { https: true } }); - - expect(flux.bridge.baseUrl).to.eq('https://services-cors.groupbycloud.com:443/api/v1'); - }); - - 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.bridge.errorHandler).to.be.a('function'); - - flux.bridge.errorHandler(error); - }); - - it('should set configured errorHandler on bridge', () => { - const errorHandler = sinon.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(); - }); - - 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(); // tslint:disable-line:no-constant-condition - }; - 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', - language: 'zh', - fields: ['title', 'price'] - }); - 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/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/karma.entry.ts b/test/karma.entry.ts similarity index 68% rename from karma.entry.ts rename to test/karma.entry.ts index 9b5f449..5827c77 100644 --- a/karma.entry.ts +++ b/test/karma.entry.ts @@ -1,7 +1,8 @@ -import './src/polyfills'; +// tslint:disable ban-types +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/pager.ts b/test/pager.ts deleted file mode 100644 index c8801f0..0000000 --- a/test/pager.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { Pager } from '../src/flux/pager'; -import { Events, FluxCapacitor, Query } from '../src/index'; -import { expect } from 'chai'; - -describe('Pager', function() { - 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 { - query: new Query() - .skip(recordStart) - .withPageSize(pageSize), - results: { totalRecordCount }, - emit: (event: string) => null, - search - }; - } - - 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); - }); - - it('first page change', (done) => { - const mockFlux = flux(0, function() { - expect(this.query.raw.skip).to.eq(10); - done(); - }); - 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(); - }); - - 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(); - }); - - describe('pageExists()', () => { - it('should return true', () => { - const pager = new Pager(flux({ start: 10, total: 200 }, () => null)); - expect(pager.pageExists(20)).to.be.true; - }); - - it('should return false', () => { - const pager = new Pager(flux({ start: 10, total: 200 }, () => null)); - expect(pager.pageExists(21)).to.be.false; - }); - }); - - 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); - }); - - 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); - }); - - describe('error states', () => { - it('should not switchPage past the results', (done) => { - new Pager(flux({ start: 1, total: 30 })) - .switchPage(8) - .catch(() => done()); - }); - - it('should not switchPage to lower than the first page', (done) => { - new Pager(flux(1)) - .switchPage(-2) - .catch(() => done()); - }); - }); - }); - - 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('current page behaviour', () => { - it('should return the current page', () => { - expect(new Pager(flux(1)).currentPage).to.eq(1); - }); - - 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); - }); - }); - - describe('total pages behaviour', () => { - it('should return the total number of pages', () => { - expect(new Pager(flux({ start: 1, total: 457 })).finalPage).to.eq(46); - }); - - it('should correctly cut off the last page', () => { - expect(new Pager(flux({ start: 1, total: 300 })).finalPage).to.eq(30); - }); - - it('should return one page when no results', () => { - expect(new Pager(flux({ start: 1, total: 0 })).finalPage).to.eq(1); - }); - }); - - describe('page numbers behaviour', () => { - 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]); - }); - - it('should start shifting the page range up', () => { - expect(new Pager(flux({ start: 31, total: 100 })).pageNumbers()).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]); - }); - - 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]); - }); - - it('should start shifting the page range down', () => { - expect(new Pager(flux({ start: 69, total: 100 })).pageNumbers()).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]); - }); - - 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 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(); - }); - - 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('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(); - }); - }); - - 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(); - }); - }); - }); -}); 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/tslint.json b/test/tslint.json new file mode 100644 index 0000000..0138ebe --- /dev/null +++ b/test/tslint.json @@ -0,0 +1,8 @@ +{ + "extends": "../tslint.json", + "rules": { + "no-unused-expression": false, + "no-string-literal": false, + "object-literal-sort-keys": false + } +} diff --git a/test/unit/_suite.ts b/test/unit/_suite.ts new file mode 100644 index 0000000..0245617 --- /dev/null +++ b/test/unit/_suite.ts @@ -0,0 +1,29 @@ +import { expect } from 'chai'; +import * as suite from 'mocha-suite'; +import * as sinon from 'sinon'; + +export default 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.SinonStubStatic; +} + +export type Suite = UtilsSuite & { + only: UtilsSuite; + skip: UtilsSuite; +}; + +export type UtilsSuite = (description: string, tests: (utils: Utils) => void) => void; diff --git a/test/bridge.ts b/test/unit/core/bridge.ts similarity index 86% rename from test/bridge.ts rename to test/unit/core/bridge.ts index 1e96030..4fe0cd3 100644 --- a/test/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 * as mock from 'xhr-mock'; +import { BrowserBridge, CloudBridge } from '../../../src/core/bridge'; +import { Query } from '../../../src/core/query'; +import suite from '../_suite'; const CLIENT_KEY = 'XXX-XXX-XXX-XXX'; const CUSTOMER_ID = 'services'; -describe('Bridge', () => { +suite('Bridge', ({ expect, spy }) => { let bridge; let query; @@ -27,17 +27,13 @@ describe('Bridge', () => { }); 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 @@ describe('Bridge', () => { }); 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) @@ -135,9 +131,9 @@ describe('Bridge', () => { 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(); }); }); @@ -149,9 +145,9 @@ describe('Bridge', () => { 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(); }); }); @@ -171,17 +167,17 @@ 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'); }); 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(); }); }); @@ -206,7 +202,7 @@ describe('Bridge', () => { 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 @@ describe('Bridge', () => { 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/query.ts b/test/unit/core/query.ts similarity index 87% rename from test/query.ts rename to test/unit/core/query.ts index e655b0d..fe1f02f 100644 --- a/test/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(() => { @@ -21,20 +21,20 @@ describe('Query', function() { 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 @@ describe('Query', function() { .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 @@ describe('Query', function() { 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 @@ describe('Query', function() { 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', { + .withRefinements('material', { type: 'Value', - value: 'wool' + value: 'wool', }) - .withRefinements('year', { - type: 'Range', - low: 2000, + .withRefinements('year', { + 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/actions.ts b/test/unit/flux/actions.ts new file mode 100644 index 0000000..7f4a07f --- /dev/null +++ b/test/unit/flux/actions.ts @@ -0,0 +1,497 @@ +import * as sinon from 'sinon'; +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'; + +suite('Actions', ({ expect, spy, stub }) => { + let actions: Actions; + const flux: any = { a: 'b' }; + + beforeEach(() => actions = new Actions(flux, { search: '/search' })); + + describe('constructor()', () => { + it('should set properties', () => { + expect(actions['flux']).to.eq(flux); + expect(actions['linkMapper']).to.be.a('function'); + }); + }); + + 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 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(refinements).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); + 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(() => { + 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('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', () => { + describe('updateSearch()', () => { + it('should create an UPDATE_SEARCH action', () => { + const data: any = { a: 'b' }; + const thunk = stub(utils, 'thunk'); + + actions.updateSearch(data); + + expect(thunk).to.be.calledWith(Actions.UPDATE_SEARCH, data); + }); + }); + + describe('selectRefinement()', () => { + it('should create a SELECT_REFINEMENT action', () => { + const navigationId = 'brand'; + const index = 3; + const state = { a: 'b' }; + const conditional = stub(utils, 'conditional'); + const isRefinementDeselected = stub(Selectors, 'isRefinementDeselected'); + + actions.selectRefinement(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 }); + }); + }); + + describe('deselectRefinement()', () => { + it('should create a DESELECT_REFINEMENT action', () => { + const navigationId = 'brand'; + const index = 3; + const state = { a: 'b' }; + const conditional = stub(utils, 'conditional'); + const isRefinementSelected = stub(Selectors, 'isRefinementSelected'); + + actions.deselectRefinement(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 conditional = stub(utils, 'conditional'); + + actions.selectCollection(id); + + expect(conditional).to.be.calledWith(sinon.match((predicate) => + predicate({ data: { collections: { selected: 'tutorials' } } })), + Actions.SELECT_COLLECTION, { id }); + }); + }); + + describe('updateSorts()', () => { + it('should create a UPDATE_SORTS action', () => { + const id = 'Price Ascending'; + const conditional = stub(utils, 'conditional'); + + actions.updateSorts(id); + + 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 conditional = stub(utils, 'conditional'); + + actions.updatePageSize(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 conditional = stub(utils, 'conditional'); + + actions.updateCurrentPage(page); + + expect(conditional).to.be.calledWith(sinon.match((predicate) => + predicate({ data: { page: { current: 3 } } })), + Actions.UPDATE_CURRENT_PAGE, { page }); + }); + }); + + describe('updateDetailsId()', () => { + it('should create an UPDATE_CURRENT_PAGE action', () => { + const id = '123'; + const thunk = stub(utils, 'thunk'); + + actions.updateDetailsId(id); + + 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 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); + 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(['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); + expect(dispatch).to.be.calledWith(receiveNavigationsAction); + expect(receivePage).to.be.calledWith(page); + 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); + 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 }); + }); + }); + + 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 }); + }); + }); + }); +}); diff --git a/test/unit/flux/adapters/request.ts b/test/unit/flux/adapters/request.ts new file mode 100644 index 0000000..be13e60 --- /dev/null +++ b/test/unit/flux/adapters/request.ts @@ -0,0 +1,69 @@ +import Adapter from '../../../../src/flux/adapters/request'; +import suite from '../../_suite'; + +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('should extract query', () => { + const query = 'rock climbing'; + const state: any = { data: { query: { original: query } } }; + + expect(Adapter.extractQuery(state)).to.eq(query); + }); + }); + + describe('extractRefinements()', () => { + it('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([ + { 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 new file mode 100644 index 0000000..46bd787 --- /dev/null +++ b/test/unit/flux/adapters/response.ts @@ -0,0 +1,306 @@ +import Adapter from '../../../../src/flux/adapters/response'; +import * as paging from '../../../../src/flux/pager'; +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', '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, 3]); + expect(refinementsMatch).to.be.calledWith('a', 'a'); + expect(refinementsMatch).to.be.calledWith('a', 'd'); + 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; + }); + }); + + 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/test/unit/flux/capacitor.ts b/test/unit/flux/capacitor.ts new file mode 100644 index 0000000..69c042f --- /dev/null +++ b/test/unit/flux/capacitor.ts @@ -0,0 +1,235 @@ +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'; + +const CUSTOMER_ID = 'services'; +const SEARCH_URL = `http://${CUSTOMER_ID}-cors.groupbycloud.com/api/v1/search`; +const REFINEMENTS_URL = `${SEARCH_URL}/refinements`; +const SELECTED_REFINEMENT: SelectedValueRefinement = { type: 'Value', navigationName: 'brand', value: 'DeWalt' }; +const REFINEMENT_RESULT = { availableNavigation: 'a', selectedNavigation: 'b' }; +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 flux: FluxCapacitor; + + beforeEach(() => { + mock.setup(); + subscribe = spy(); + create = stub(Store, 'create').returns({ subscribe }); + listen = stub(Observer, 'listen').returns(LISTENER); + flux = new FluxCapacitor(CUSTOMER_ID); + }); + + afterEach(() => { + mock.teardown(); + flux = null; + }); + + 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; + }); + + 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' }; + + 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, + }, + }); + + 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 } }); + + expect(flux.bridge.headers).to.eq(headers); + }); + + it('should set HTTPS on bridge', () => { + flux = new FluxCapacitor(CUSTOMER_ID, { bridge: { https: true } }); + + expect(flux.bridge.baseUrl).to.eq('https://services-cors.groupbycloud.com:443/api/v1'); + }); + + 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.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('actions', () => { + let dispatch: sinon.SinonSpy; + + beforeEach(() => { + dispatch = spy(); + flux.store = { dispatch }; + }); + + describe('search()', () => { + it('should dispatch updateSearch()', () => { + const query = 'half moon'; + const updateSearch = stub(flux.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(flux.actions, 'updateSearch'); + + flux.search(); + + expect(updateSearch).to.be.calledWith({ query }); + }); + }); + + 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(flux.actions, 'updateSearch'); + + flux.reset(query, { field, index }); + + expect(updateSearch).to.be.calledWith({ query, navigationId: field, index, clear: true }); + }); + + it('should fallback to null query and empty refinements', () => { + const updateSearch = stub(flux.actions, 'updateSearch'); + + flux.reset(); + + expect(updateSearch).to.be.calledWith({ + query: null, + navigationId: undefined, + index: undefined, + clear: true, + }); + }); + }); + + describe('resize()', () => { + it('should dispatch updatePageSize()', () => { + const updatePageSize = stub(flux.actions, 'updatePageSize'); + + flux.resize(24); + + expect(updatePageSize).to.be.calledWith(24); + }); + }); + + describe('sort()', () => { + it('should dispatch updateSorts()', () => { + const sort = 'Price Ascending'; + const updateSorts = stub(flux.actions, 'updateSorts'); + + flux.sort(sort); + + expect(updateSorts).to.be.calledWith(sort); + }); + }); + + describe('refine()', () => { + it('should dispatch selectRefinement()', () => { + const selectRefinement = stub(flux.actions, 'selectRefinement'); + + flux.refine('brand', 3); + + expect(selectRefinement).to.be.calledWith('brand', 3); + }); + }); + + describe('unrefine()', () => { + it('should dispatch deselectRefinement()', () => { + const deselectRefinement = stub(flux.actions, 'deselectRefinement'); + + flux.unrefine('brand', 3); + + expect(deselectRefinement).to.be.calledWith('brand', 3); + }); + }); + + describe('details()', () => { + it('should dispatch updateDetailsId()', () => { + const id = '123123'; + const updateDetailsId = stub(flux.actions, 'updateDetailsId'); + + flux.details(id); + + expect(updateDetailsId).to.be.calledWith(id); + }); + }); + + describe('switchCollection()', () => { + it('should dispatch selectCollection()', () => { + const selectCollection = stub(flux.actions, 'selectCollection'); + + flux.switchCollection('products'); + + expect(selectCollection).to.be.calledWith('products'); + }); + }); + }); +}); diff --git a/test/unit/flux/observer.ts b/test/unit/flux/observer.ts new file mode 100644 index 0000000..1c88655 --- /dev/null +++ b/test/unit/flux/observer.ts @@ -0,0 +1,287 @@ +import { Events } from '../../../src/flux/capacitor'; +import Observer, { DETAIL_QUERY_INDICATOR, INDEXED } from '../../../src/flux/observer'; +import suite from '../_suite'; + +suite('Observer', ({ expect, spy, stub }) => { + describe('listen()', () => { + it('should return a function', () => { + const observer = Observer.listen({}); + + expect(observer).to.be.a('function'); + }); + + it('should call store.getState()', () => { + const getState = 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 = stub(Observer, 'resolve'); + const create = stub(Observer, 'create'); + const observer = Observer.listen(flux); + + observer(); + + expect(resolve).to.be.calledWith(undefined, newState); + expect(create).to.be.calledWith(flux); + }); + }); + + describe('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.skip('should not call the observer if not a function', () => { + expect(() => Observer.resolve(1, 2, {})).to.not.throw(); + }); + + it.skip('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.skip('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.skip('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', () => { + const OBJ = { a: 'b' }; + let emit; + let observers; + + beforeEach(() => { + emit = spy(); + 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 emit QUERY_UPDATED event', () => { + observers.data.query(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.QUERY_UPDATED, OBJ); + }); + + it('should emit CORRECTED_QUERY_UPDATED event', () => { + observers.data.query.corrected(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.CORRECTED_QUERY_UPDATED, OBJ); + }); + + it('should emit DID_YOU_MEANS_UPDATED event', () => { + observers.data.query.didYouMeans(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.DID_YOU_MEANS_UPDATED, OBJ); + }); + + it('should emit ORIGINAL_QUERY_UPDATED event', () => { + observers.data.query.original(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.ORIGINAL_QUERY_UPDATED, OBJ); + }); + + it('should emit RELATED_QUERIES_UPDATED event', () => { + observers.data.query.related(undefined, OBJ); + + expect(emit).to.be.calledWith(Events.RELATED_QUERIES_UPDATED, OBJ); + }); + + it('should emit QUERY_REWRITES_UPDATED event', () => { + observers.data.query.rewrites(undefined, OBJ); + + 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); + }); + }); + }); + }); +}); diff --git a/test/unit/flux/pager.ts b/test/unit/flux/pager.ts new file mode 100644 index 0000000..469377d --- /dev/null +++ b/test/unit/flux/pager.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.skip('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 new file mode 100644 index 0000000..4e38093 --- /dev/null +++ b/test/unit/flux/reducers/autocomplete.ts @@ -0,0 +1,59 @@ +import Actions from '../../../../src/flux/actions'; +import autocomplete from '../../../../src/flux/reducers/autocomplete'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite('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 query state on UPDATE_AUTOCOMPLETE_QUERY', () => { + const newQuery = 'red shoes'; + const newState = { + category, + products, + query: newQuery, + suggestions, + }; + + 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 categoryValues = ['a', 'c', 'd']; + const newState = { + category: { ...category, 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 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..de2cf60 --- /dev/null +++ b/test/unit/flux/reducers/collections.ts @@ -0,0 +1,76 @@ +import Actions from '../../../../src/flux/actions'; +import collections from '../../../../src/flux/reducers/collections'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite('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 selectedCollection = 'Department'; + const newState = { + allIds, + byId: { + Department, + Main, + }, + selected: selectedCollection, + }; + + const reducer = collections(state, { type: Actions.SELECT_COLLECTION, id: selectedCollection }); + + expect(reducer).to.eql(newState); + }); + + it('should update state on RECEIVE_AUTOCOMPLETE_SUGGESTIONS', () => { + const total = 700; + const newState = { + allIds, + byId: { + Department: { + ...Department, + total, + }, + Main, + }, + selected, + }; + + const reducer = collections(state, { + type: Actions.RECEIVE_COLLECTION_COUNT, + collection: allIds[0], + count: total, + }); + + expect(reducer).to.eql(newState); + }); + + it('should return state on default', () => { + 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..c133dd3 --- /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('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..b1d0aaa --- /dev/null +++ b/test/unit/flux/reducers/navigations.ts @@ -0,0 +1,204 @@ +import Actions from '../../../../src/flux/actions'; +import navigations from '../../../../src/flux/reducers/navigations'; +import Store from '../../../../src/flux/store'; +import suite from '../../_suite'; + +suite('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 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, + 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 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, {}); + + 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..374721f --- /dev/null +++ b/test/unit/flux/reducers/page.ts @@ -0,0 +1,112 @@ +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; + 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 = { + first, size, current, limit, previous, next, last, from, to, range, + }; + beforeEach(() => actions = new Actions({}, {})); + + describe('updatePage()', () => { + 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); + }); + }); +}); 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 diff --git a/test/unit/flux/selectors.ts b/test/unit/flux/selectors.ts new file mode 100644 index 0000000..fff31a1 --- /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.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.not.be.ok; + }); + + 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.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.not.be.ok; + }); + + 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.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.not.be.ok; + }); + + 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/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); + }); + }); +}); diff --git a/test/unit/flux/utils.ts b/test/unit/flux/utils.ts new file mode 100644 index 0000000..66f7ac4 --- /dev/null +++ b/test/unit/flux/utils.ts @@ -0,0 +1,68 @@ +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' }); + }); + }); + + 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' }); + }); + }); +}); 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/tsconfig.json b/tsconfig.json index d85ca19..dce8f70 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,10 @@ "compilerOptions": { "lib": [ "ES5", - "ES6" + "ES6", + "ES2016.Array.Include" ], + "types": ["node"], "module": "commonjs", "moduleResolution": "node", "target": "ES5", diff --git a/tslint.json b/tslint.json index 786b379..a245a1a 100644 --- a/tslint.json +++ b/tslint.json @@ -1,80 +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, - "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, - "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"] } } diff --git a/typings.json b/typings.json index ca0d9d9..5c6f013 100644 --- a/typings.json +++ b/typings.json @@ -6,11 +6,10 @@ "filter-object": "registry:npm/filter-object#2.1.0+20160816021609" }, "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" + "chai": "registry:dt/chai#3.4.0+20160601211834" }, "devDependencies": { + "sinon-chai": "registry:dt/sinon-chai#2.7.0+20160628004037", "xhr-mock": "registry:npm/xhr-mock#1.6.0+20160610174323" } } diff --git a/webpack.config.js b/webpack.config.js index 8ae9ee7..0fd3947 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: { + sourceMap: false, + inlineSourceMap: true + } + }) } }; diff --git a/yarn.lock b/yarn.lock index 312a662..e72e99b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,29 +2,67 @@ # 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" 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/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" "*" + +"@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.14": + version "7.0.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.14.tgz#1470fa002a113316ac9d9ad163fc738c7a0de2a4" -"@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.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.0.tgz#229c157c6bc1e67d6b990e6c5e18dbd2ff58cff0" + 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" @@ -37,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" @@ -52,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" @@ -60,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" @@ -140,6 +199,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" @@ -152,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" @@ -178,6 +244,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" @@ -204,32 +278,35 @@ 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" -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" @@ -239,11 +316,19 @@ 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" + 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" @@ -265,10 +350,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" @@ -303,26 +384,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" @@ -332,18 +417,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: @@ -367,11 +450,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" @@ -383,7 +513,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: @@ -418,10 +552,18 @@ 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@^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" + 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" @@ -445,7 +587,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: @@ -465,7 +607,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: @@ -480,6 +622,16 @@ 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" + +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" @@ -490,12 +642,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" @@ -505,6 +657,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" @@ -513,14 +673,23 @@ 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" +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" + +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" @@ -539,21 +708,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: @@ -587,26 +754,23 @@ 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" - 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" @@ -632,7 +796,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" @@ -640,19 +804,41 @@ 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-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" -cross-spawn@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.0.1.tgz#a3bbb302db2297cbea3c04edf36941f4613aa399" +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: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" + 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" + dependencies: + lru-cache "^4.0.0" + which "^1.2.8" cryptiles@2.x.x: version "2.0.5" @@ -660,14 +846,24 @@ 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" + 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" @@ -679,6 +875,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" @@ -700,13 +902,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.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" +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: 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: @@ -718,11 +920,23 @@ debug@2.3.3: dependencies: ms "0.7.2" +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: + 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" + 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" @@ -765,15 +979,20 @@ 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" -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" dependencies: - repeating "^2.0.0" + 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" dezalgo@^1.0.0: version "1.0.3" @@ -786,13 +1005,17 @@ 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.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" -diff@^2.2.1: - version "2.2.3" - resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" +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" @@ -818,9 +1041,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" @@ -830,11 +1053,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" @@ -850,13 +1071,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" @@ -867,7 +1104,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" @@ -882,33 +1119,25 @@ 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@^2.2.2: - version "2.3.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-2.3.0.tgz#a115c32504b6302e85a76269d7a57ccdd962e359" +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: graceful-fs "^4.1.2" - memory-fs "^0.3.0" + memory-fs "^0.4.0" object-assign "^4.0.1" - tapable "^0.2.3" - -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" + tapable "^0.2.5" ent@~2.2.0: version "2.2.0" @@ -943,25 +1172,51 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.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" +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-promise@^3.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, 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" + 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" -escape-string-regexp@1.0.2, 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" +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" escodegen@1.8.x: version "1.8.1" @@ -990,14 +1245,35 @@ 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@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" +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" + 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" @@ -1079,13 +1355,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" @@ -1096,10 +1365,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" @@ -1123,16 +1388,22 @@ 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: + 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" @@ -1146,12 +1417,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" @@ -1179,21 +1449,18 @@ 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@^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" @@ -1267,10 +1534,18 @@ 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" +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" @@ -1290,14 +1565,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.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" 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: @@ -1317,17 +1596,6 @@ 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: - 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" @@ -1341,27 +1609,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" @@ -1402,7 +1666,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: @@ -1412,6 +1676,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" @@ -1463,6 +1737,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" @@ -1483,6 +1763,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" @@ -1491,12 +1779,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: @@ -1534,6 +1823,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" @@ -1575,10 +1873,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" @@ -1589,6 +1883,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" @@ -1620,6 +1918,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" @@ -1654,6 +1958,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" @@ -1730,7 +2038,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" @@ -1786,26 +2094,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: @@ -1824,13 +2113,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" @@ -1865,6 +2147,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" @@ -1875,6 +2161,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" @@ -1883,7 +2175,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" @@ -1893,10 +2185,34 @@ 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" + +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" +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" @@ -1905,10 +2221,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" @@ -1919,28 +2231,30 @@ 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" -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: 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" -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" @@ -1948,9 +2262,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" @@ -1958,33 +2272,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" @@ -2008,19 +2326,25 @@ 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" + +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" @@ -2059,7 +2383,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-utils@^0.2.6, loader-utils@^0.2.7: +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: @@ -2068,14 +2396,37 @@ 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" +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" @@ -2108,6 +2459,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" @@ -2163,7 +2522,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.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" @@ -2185,15 +2544,15 @@ 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" 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: @@ -2210,11 +2569,11 @@ 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" -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: @@ -2243,18 +2602,7 @@ 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.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" - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -memory-fs@~0.4.1: +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: @@ -2294,6 +2642,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" @@ -2314,25 +2669,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" + +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@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" @@ -2341,10 +2691,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" @@ -2361,20 +2707,27 @@ mkdirp@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" -mocha@^2.2.5: - version "2.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" +mocha-suite@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/mocha-suite/-/mocha-suite-1.0.8.tgz#130b9c9faf6f92b4ff1cf72cc6e523c09ef6b8d3" dependencies: - commander "2.3.0" - debug "2.2.0" - diff "1.4.0" - escape-string-regexp "1.0.2" - glob "3.2.11" + "@types/mocha" "^2.2.40" + +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.6.0" + diff "3.2.0" + escape-string-regexp "1.0.5" + glob "7.1.1" 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" @@ -2398,20 +2751,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" @@ -2444,10 +2801,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" @@ -2475,10 +2828,20 @@ 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" +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" @@ -2500,7 +2863,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" @@ -2523,7 +2886,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: @@ -2577,30 +2940,21 @@ 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-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-tmpdir@^1.0.0: +os-tmpdir@~1.0.1: 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" @@ -2609,6 +2963,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" @@ -2618,7 +2982,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: @@ -2660,6 +3024,20 @@ 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" + +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" @@ -2668,9 +3046,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" @@ -2726,26 +3106,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" - 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" @@ -2771,15 +3139,17 @@ 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" -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" @@ -2789,6 +3159,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" @@ -2797,11 +3177,15 @@ 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.1.0, qs@~6.3.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" + +qs@~6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" @@ -2813,6 +3197,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" @@ -2820,7 +3208,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" @@ -2841,13 +3233,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" @@ -2886,9 +3271,9 @@ 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: - 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" @@ -2927,18 +3312,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" @@ -2970,6 +3343,19 @@ 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" + 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" @@ -2989,13 +3375,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" @@ -3061,14 +3448,28 @@ request@^2.34, request@^2.79.0, request@~2.79.0: tunnel-agent "~0.4.1" uuid "^3.0.0" -requires-port@1.x.x: +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.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" -resolve@1.1.x, resolve@^1.1.6, resolve@^1.1.7: +resolve@1.1.x: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" +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: + 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" @@ -3082,19 +3483,39 @@ 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.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.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" -samsam@1.1.2, samsam@~1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" +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.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" @@ -3114,7 +3535,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" @@ -3126,23 +3547,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" - -sha.js@2.2.6: - version "2.2.6" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" +sha.js@^2.3.6: + version "2.4.8" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" 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" + inherits "^2.0.1" shelljs@^0.7.0: version "0.7.6" @@ -3152,22 +3565,26 @@ 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" -sinon@^1.17.6: - version "1.17.7" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf" +sinon-chai@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.9.0.tgz#34d820042bc9661a14527130d401eb462c49bb84" + +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" @@ -3190,15 +3607,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" @@ -3215,16 +3632,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: @@ -3233,17 +3650,17 @@ 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.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: 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" @@ -3253,7 +3670,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: @@ -3277,13 +3694,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" @@ -3298,7 +3708,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" @@ -3317,7 +3727,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" @@ -3328,10 +3738,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" @@ -3346,7 +3752,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: @@ -3354,6 +3760,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" @@ -3380,19 +3793,29 @@ 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" 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" -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" @@ -3408,11 +3831,11 @@ supports-color@^3.1.0: dependencies: has-flag "^1.0.0" -tapable@^0.1.8, tapable@~0.1.8: - version "0.1.10" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" +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.2.3: +tapable@^0.2.5, tapable@~0.2.5: version "0.2.6" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d" @@ -3437,7 +3860,17 @@ 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" + +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" dependencies: @@ -3458,19 +3891,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" @@ -3478,6 +3905,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" @@ -3486,10 +3919,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" @@ -3516,34 +3945,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.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/tslint-loader/-/tslint-loader-3.5.3.tgz#343f74122d94f356b689457d3f59f64a69ab606f" 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" @@ -3571,6 +4011,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" @@ -3582,103 +4026,105 @@ 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-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.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.4.5.tgz#d12c8edc6bb97be7199c56dade65e0cc12d12e8b" - dependencies: - fs-extra "^0.30.0" - handlebars "4.0.5" +typedoc@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.6.0.tgz#9c9854aec54d917ac05552a4a3a4aabec280e1cf" + dependencies: + "@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.7.0" + fs-extra "^2.0.0" + 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.0" - typescript "1.8.10" + typedoc-default-themes "^0.4.2" + typescript "2.2.2" -typescript@1.8.10: - version "1.8.10" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-1.8.10.tgz#b475d6e0dff0bf50f296e5ca6ef9fbb5c7320f1e" - -typescript@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.0.2.tgz#a63f848074498a4dfa7f293afda0835d501d9540" +typescript@2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.2.tgz#606022508479b55ffa368b58fee963a03dfd7b0c" -typescript@^2.0.3: - version "2.1.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.1.5.tgz#6fe9479e00e01855247cea216e7561bafcdbcd4a" +typescript@^2.1.4, typescript@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.1.tgz#e3361fb395c6c3f9c69faeeabc9503f8bdecaea1" -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" -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" @@ -3696,33 +4142,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" -underscore.string@^3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.4.tgz#2c2a3f9f83e64762fdc45e6ceac65142864213db" +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" dependencies: - sprintf-js "^1.0.3" - util-deprecate "^1.0.2" + 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" @@ -3730,6 +4175,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" @@ -3737,13 +4189,14 @@ 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.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" @@ -3751,7 +4204,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: @@ -3761,10 +4214,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" @@ -3800,12 +4249,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: @@ -3814,13 +4263,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" @@ -3830,27 +4272,44 @@ 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.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: @@ -3884,6 +4343,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" @@ -3896,9 +4362,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" @@ -3907,17 +4373,16 @@ 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" - resolved "https://registry.yarnpkg.com/xhr-mock/-/xhr-mock-1.7.0.tgz#c3749f2c68d34ee88af7af5b84ac50764c604351" +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" xmlhttprequest-ssl@1.5.3: version "1.5.3" @@ -3927,10 +4392,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"