├── .circleci └── config.yml ├── .codesandbox └── ci.json ├── .eslintignore ├── .eslintrc.js ├── .git-blame-ignore-revs ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── documentation-src ├── content │ ├── concepts.md │ ├── examples.md │ ├── gettingstarted.md │ ├── images │ │ ├── al-helper-logo.svg │ │ ├── algolia-mark-white.svg │ │ ├── background_footer.svg │ │ ├── community-badge.svg │ │ ├── concepts │ │ │ ├── Events.svg │ │ │ ├── conjunctive-facets.svg │ │ │ ├── derive-simplified.svg │ │ │ ├── disjunctive-facets.svg │ │ │ ├── search-cycle.svg │ │ │ ├── search-states.svg │ │ │ ├── setting-parameters.svg │ │ │ └── state-is-immutable.svg │ │ ├── icon-arrow-pipe.svg │ │ ├── icon-github.svg │ │ ├── icon-heart-dark.svg │ │ ├── icon-heart-light.svg │ │ └── icon-search-white.svg │ ├── index.md │ ├── js │ │ └── main.js │ ├── reference.md │ └── upgrade.md ├── layouts │ ├── common │ │ ├── footer.pug │ │ ├── header.pug │ │ └── metas.pug │ ├── documentation.pug │ └── main.pug ├── metalsmith.js ├── partials │ ├── event.hbs │ ├── jsdoc.hbs │ ├── member.hbs │ ├── method.hbs │ ├── typedef.hbs │ └── types.hbs ├── plugins │ ├── handlebars-helpers.js │ ├── headings.js │ └── jsdoc-data.js └── stylesheets │ ├── components │ ├── _buttons.scss │ ├── _clipboard.scss │ ├── _code-highlight.scss │ ├── _containers.scss │ ├── _documentation.scss │ ├── _examples-intro.scss │ ├── _examples.scss │ ├── _fonts.scss │ ├── _footer.scss │ ├── _home.scss │ ├── _icons.scss │ ├── _inputs.scss │ ├── _media.scss │ ├── _sidebar.scss │ ├── _visual-helper.scss │ └── docs │ │ └── _method.scss │ ├── index.scss │ └── vendors │ ├── _animations.scss │ ├── _base.scss │ ├── _communityHeader.scss │ ├── _helpers.scss │ ├── _mixins.scss │ ├── _normalize.scss │ └── _variables.scss ├── examples └── readme.md ├── index.d.ts ├── index.html ├── index.js ├── jest.config.js ├── jest.setup.js ├── package.json ├── patches └── metalsmith-in-place+1.4.4.patch ├── scripts ├── build.sh ├── conventional-changelog │ └── index.js ├── lib │ └── conventionalChangelog.js ├── release.js ├── retry.sh ├── staging-doc.sh └── test.sh ├── src ├── DerivedHelper │ └── index.js ├── SearchParameters │ ├── RefinementList.js │ └── index.js ├── SearchResults │ ├── generate-hierarchical-tree.js │ └── index.js ├── algoliasearch.helper.js ├── functions │ ├── compact.js │ ├── defaultsPure.js │ ├── escapeFacetValue.js │ ├── find.js │ ├── findIndex.js │ ├── formatSort.js │ ├── inherits.js │ ├── intersection.js │ ├── merge.js │ ├── objectHasKeys.js │ ├── omit.js │ ├── orderBy.js │ └── valToNumber.js ├── requestBuilder.js ├── utils │ └── isValidUserToken.js └── version.js ├── test ├── datasets │ ├── SearchParameters │ │ └── search.dataset.js │ └── SearchResults │ │ ├── getFacetValues.dataset.js │ │ └── getRefinements.dataset.js ├── integration-spec │ ├── helper.derive.js │ ├── helper.distinct.facet.js │ ├── helper.filters.js │ ├── helper.geo.js │ ├── helper.highlight.js │ ├── helper.insights.js │ ├── helper.numerics.js │ ├── helper.results.getFacetValues.js │ ├── helper.searchForFacetValues.js │ ├── helper.searchOnce.js │ └── helper.tags.js ├── integration-utils.js ├── replayTools.js ├── run.js ├── spec │ ├── SearchParameters │ │ ├── RefinementList │ │ │ └── clear.js │ │ ├── _clearNumericRefinements.js │ │ ├── _parseNumbers.js │ │ ├── constructorFn.js │ │ ├── getConjunctiveRefinements.js │ │ ├── getDisjunctiveRefinements.js │ │ ├── getExcludeRefinements.js │ │ ├── hierarchical-facets │ │ │ ├── add.js │ │ │ └── remove.js │ │ ├── isDisjunctiveFacetRefined.js │ │ ├── isExcludeRefined.js │ │ ├── isFacetRefined.js │ │ ├── isHierarchicalFacetRefined.js │ │ ├── isNumericRefined.js │ │ ├── listAttributes.js │ │ ├── noChanges.js │ │ ├── numericAttributes.js │ │ ├── numericFilters.js │ │ ├── removeXFacet.js │ │ ├── removeXFacetRefinement.js │ │ ├── resetPage.js │ │ ├── setQueryParameter.js │ │ └── setQueryParameters.js │ ├── SearchResults │ │ ├── getFacet.js │ │ ├── getFacetStats.js │ │ ├── getFacetValues-facetOrdering.js │ │ ├── getFacetValues.js │ │ ├── getFacetValues │ │ │ ├── conjunctive.json │ │ │ ├── disjunctive.json │ │ │ ├── hierarchical.json │ │ │ ├── noFilters.json │ │ │ └── sparse.json │ │ ├── getRefinements.js │ │ ├── getRefinements │ │ │ ├── conjunctive-brand-apple.json │ │ │ ├── disjunctive-type-trendcase.json │ │ │ ├── dummy-tags.json │ │ │ ├── exclude-apple.json │ │ │ ├── exclude-artificial-results.json │ │ │ ├── hierarchical-cards.json │ │ │ ├── noFilters.json │ │ │ └── numeric-rating-3.json │ │ └── initialization.js │ ├── algoliasearch.helper │ │ ├── addFacetRefinement.js │ │ ├── clears.js │ │ ├── client.js │ │ ├── constructor.js │ │ ├── derive │ │ │ ├── detach.js │ │ │ ├── empty-index.js │ │ │ ├── events.js │ │ │ └── multiqueries.js │ │ ├── distinct.js │ │ ├── events.js │ │ ├── excludes.js │ │ ├── facetFilters.js │ │ ├── findAnswers.js │ │ ├── getNumericRefinement.js │ │ ├── getQuery.js │ │ ├── hasRefinements.js │ │ ├── incorrectFacetDefinition.js │ │ ├── numericFilters.js │ │ ├── pages.js │ │ ├── pendingSearch.js │ │ ├── queryID.js │ │ ├── searchForFacetValues.js │ │ ├── searchOnce.js │ │ ├── setQueryParameter.js │ │ ├── state.js │ │ └── tags.js │ ├── functions │ │ ├── compact.js │ │ ├── defaultsPure.js │ │ ├── find.js │ │ ├── findIndex.js │ │ ├── formatSort.js │ │ ├── intersection.js │ │ ├── merge.js │ │ ├── orderBy.js │ │ └── valToNumber.js │ ├── hierarchical-facets │ │ ├── add-remove.js │ │ ├── attributes-order.js │ │ ├── breadcrumb.js │ │ ├── custom-prefix-path.js │ │ ├── custom-separator.js │ │ ├── do-not-show-parent-level.js │ │ ├── facet-value-length.js │ │ ├── getFacetValues.js │ │ ├── no-refinement.js │ │ ├── no-trim.js │ │ ├── objects-with-multiple-categories.js │ │ ├── one-level.js │ │ ├── pagination.js │ │ ├── parent-toggleRefine.js │ │ ├── refined-no-result.js │ │ ├── show-parent-level.js │ │ ├── simple-usage.js │ │ ├── sort-by.js │ │ ├── two-facets.js │ │ ├── unknown-facet.js │ │ └── with-a-disjunctive-facet.js │ ├── refinements.js │ ├── requestBuilder.js │ ├── search.js │ └── utils │ │ └── isValidUserToken.js └── types.ts ├── tsconfig.json ├── types ├── algoliasearch.d.ts └── algoliasearch.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | aliases: 2 | - &install_yarn_version 3 | name: Install specific Yarn version 4 | command: | 5 | curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.15.2 6 | echo 'export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH"' >> $BASH_ENV 7 | 8 | - &restore_yarn_cache 9 | name: Restore Yarn cache 10 | keys: 11 | - yarn-{{ .Branch }}-packages-{{ checksum "yarn.lock" }} 12 | 13 | - &save_yarn_cache 14 | name: Save Yarn cache 15 | key: yarn-{{ .Branch }}-packages-{{ checksum "yarn.lock" }} 16 | paths: 17 | - ~/.cache/yarn 18 | 19 | - &run_yarn_install 20 | name: Install dependencies 21 | command: yarn install 22 | 23 | defaults: &defaults 24 | working_directory: ~/algoliasearch-helper-js 25 | docker: 26 | - image: cimg/node:14.18.0 27 | 28 | version: 2 29 | jobs: 30 | build: 31 | <<: *defaults 32 | steps: 33 | - checkout 34 | - run: *install_yarn_version 35 | - restore_cache: *restore_yarn_cache 36 | - run: *run_yarn_install 37 | - save_cache: *save_yarn_cache 38 | - run: 39 | name: Build 40 | command: yarn build 41 | 42 | test: 43 | <<: *defaults 44 | steps: 45 | - checkout 46 | - run: *install_yarn_version 47 | - restore_cache: *restore_yarn_cache 48 | - run: *run_yarn_install 49 | - save_cache: *save_yarn_cache 50 | - run: 51 | name: Lint & Code styles 52 | command: yarn lint 53 | - run: 54 | name: Unit & Integration Tests 55 | command: yarn test --maxWorkers=4 56 | - run: 57 | name: Type checking 58 | command: yarn tsc 59 | 60 | 'test algoliasearch v3': 61 | <<: *defaults 62 | steps: 63 | - checkout 64 | - run: *install_yarn_version 65 | - restore_cache: *restore_yarn_cache 66 | - run: *run_yarn_install 67 | - save_cache: *save_yarn_cache 68 | - run: 69 | name: Install algoliasearch v3 70 | command: yarn add --dev algoliasearch@3.35.1 @types/algoliasearch@3.34.10 71 | - run: 72 | name: Unit & Integration Tests 73 | command: yarn test --maxWorkers=4 74 | - run: 75 | name: Type checking 76 | command: yarn tsc 77 | 78 | docs: 79 | <<: *defaults 80 | steps: 81 | - checkout 82 | - run: *install_yarn_version 83 | - restore_cache: *restore_yarn_cache 84 | - run: *run_yarn_install 85 | - save_cache: *save_yarn_cache 86 | - run: 87 | name: Build documentation 88 | command: yarn doc 89 | - store_artifacts: 90 | path: documentation 91 | 92 | workflows: 93 | version: 2 94 | ci: 95 | jobs: 96 | - build 97 | - test 98 | - test algoliasearch v3 99 | - docs 100 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": [ 3 | "github/algolia/create-instantsearch-app/tree/templates/javascript-helper", 4 | "instantsearchjs-es-template-pcw1k" 5 | ], 6 | "node": "14" 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | documentation-src/ 3 | documentation/ 4 | scripts/rollup.esm.config.js 5 | *.ts 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('eslint').Linter.Config} 3 | */ 4 | const config = { 5 | extends: [ 6 | 'algolia', 7 | 'algolia/jest', 8 | 'algolia/react', 9 | 'algolia/typescript', 10 | 'plugin:react-hooks/recommended', 11 | ], 12 | overrides: [ 13 | { 14 | // TODO: should be scoped to helper folder once in monorepo 15 | files: ['*.js'], 16 | rules: { 17 | // Helper uses CommonJS for now 18 | 'import/no-commonjs': 'off', 19 | strict: 'off', 20 | // Helper uses ES5 for now 21 | 'no-var': 'off', 22 | 'vars-on-top': 'off', 23 | 'object-shorthand': 'off', 24 | 'prefer-template': 'off', 25 | 'prefer-spread': 'off', 26 | 'prefer-rest-params': 'off', 27 | }, 28 | }, 29 | { 30 | // TODO: should be scoped to helper folder once in monorepo 31 | files: ['test/**/*.js'], 32 | rules: { 33 | 'no-console': 'off', 34 | 'jest/no-done-callback': 'off', 35 | 'jest/no-conditional-expect': 'off', 36 | }, 37 | env: { 38 | jest: true, 39 | }, 40 | globals: { 41 | test: true, 42 | beforeAll: true, 43 | }, 44 | }, 45 | ], 46 | }; 47 | 48 | module.exports = config; 49 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Introduced prettier 2 | cc83731fc794a45d05b915e7419c59f25eb812c5 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # Built documentation folder, tracked in gh-pages 31 | documentation 32 | 33 | dist 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.18.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | documentation-src/partials 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "proseWrap": "never", 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to contribute? 2 | 3 | Basic flow: 4 | 5 | - fork this repository 6 | - setup the project: `yarn` 7 | - start dev mode `yarn dev` and open http://localhost:8090/\_\_zuul 8 | - you can also run both browser and node.js tests by using `npm test` 9 | - code, add a [test](./test/) for every fix or new feature 10 | - commit 11 | - open a pull request 12 | 13 | We will then review your pull request and makes sure your fix helps the community. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Algolia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # algoliasearch-helper-js has a new home 👋 2 | 3 | This project has moved and is now part of the [InstantSearch monorepo](https://github.com/algolia/instantsearch)! **The library remains unchanged and is still available on npm and CDNs like jsDelivr.** 4 | 5 | You can [browse the code](https://github.com/algolia/instantsearch/tree/master/packages), find [existing issues](https://github.com/algolia/instantsearch/issues?q=is%3Aissue+is%3Aopen+label%3A%22Library%3A+AlgoliaSearch+Helper%22) and follow [new releases](https://github.com/algolia/instantsearch/releases) over there. 6 | -------------------------------------------------------------------------------- /documentation-src/content/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: documentation.pug 3 | title: Uses of the Helper 4 | --- 5 | 6 | ## Common patterns 7 | 8 | These examples showcase the main features and patterns commonly found using the Helper. They do not contain advanced UI implementations. If you want a pre-built widget library, we suggest you to have a look at [instantsearch.js](https://community.algolia.com/instantsearch.js/). 9 | 10 | ### Results 11 | 12 | This first sample shows you how to implement a search displaying results and how to make use of the highlighting provided by Algolia to display insights about the results to your users. 13 | 14 | {{{codepen "JKyQxx"}}} 15 | 16 | ### Facet list 17 | 18 | This example present you how to configure and use facets in your application. 19 | 20 | {{{codepen "LkjwLE"}}} 21 | 22 | ### Excluding facets 23 | 24 | With excluding facet the user can choose not to have certain values in the results. This very powerful feature is configure the same way the conjunctive facets are. 25 | 26 | {{{codepen "LkzEgG"}}} 27 | 28 | ### Disjunctive facet list 29 | 30 | {{{codepen "bZoVKZ"}}} 31 | 32 | ### Hierarchical facet list 33 | 34 | {{{codepen "QEBRoN"}}} 35 | 36 | ## Use with frameworks 37 | 38 | ### Angular 1 39 | 40 | {{{codepen "PzZobK"}}} 41 | 42 | ### Backbone 43 | 44 | {{{codepen "AXVLmy"}}} 45 | -------------------------------------------------------------------------------- /documentation-src/content/images/algolia-mark-white.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /documentation-src/content/images/community-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /documentation-src/content/images/icon-arrow-pipe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /documentation-src/content/images/icon-github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /documentation-src/content/images/icon-heart-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /documentation-src/content/images/icon-heart-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /documentation-src/content/images/icon-search-white.svg: -------------------------------------------------------------------------------- 1 | Icons/Search -------------------------------------------------------------------------------- /documentation-src/content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: main.pug 3 | title: Algolia JS Helper 4 | subtitle: A set of utilities for further customizing search behavior 5 | --- 6 | 7 | Don't sweat, the content of this file won't be read. 8 | 9 | It needs to be there so that we can render the layout. 10 | -------------------------------------------------------------------------------- /documentation-src/layouts/common/footer.pug: -------------------------------------------------------------------------------- 1 | section.ac-footer 2 | .ac-footer-container 3 | p.ac-footer-links 4 | | Code licensed under 5 | a.ac-footer-link-item(href='https://raw.githubusercontent.com/algolia/algoliasearch-helper-js/master/LICENSE') MIT 6 | br 7 | a.ac-footer-link-item(href='https://github.com/algolia/algoliasearch-helper-js') GitHub 8 | a.ac-footer-link-item(href='https://github.com/algolia/algoliasearch-helper-js/issues') issues 9 | .ac-footer-container.ac-footer-brand 10 | p This project is part of 11 | img.ac-footer-brand-icon(src='images/community-badge.svg') 12 | figure 13 | img.ac-footer-brand-logo(src='https://res.cloudinary.com/hilnmyskv/image/upload/v1484219849/algolia-community-logo-words-darkbg.svg') 14 | figcaption Algolia Community 15 | a.ac-footer-btn.ac-footer-btn-cta(href='https://community.algolia.com/') See More 16 | span.ac-icon.ac-icon-love-dark 17 | p.ac-footer-version Latest version: #{pkg.version} 18 | .ac-footer-actions 19 | .footer-container 20 | p Build unique search experiences with Algolia 21 | a.ac-footer-btn.ac-footer-btn-ghost-grey(href='https://www.algolia.com/why?utm_medium=social-owned&utm_source=helper%20website&utm_campaign=homepage&utm_term=why') 22 | | See Why 23 | span.ac-icon-triangle 24 | a.ac-footer-btn.ac-footer-btn-ghost-pink(href='https://www.algolia.com/users/sign_up?utm_medium=social-owned&utm_source=helper%20website&utm_campaign=homepage&utm_term=get_started') Get Started For Free 25 | script(src='js/main.js') 26 | -------------------------------------------------------------------------------- /documentation-src/layouts/common/metas.pug: -------------------------------------------------------------------------------- 1 | meta(content='The Algolia JS Helper is the toolbox to create advanced search experience ', name='description') 2 | meta(content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0', name='viewport') 3 | // / Twitter card 4 | meta(content='summary_large_image', name='twitter:card') 5 | meta(content='@Algolia', name='twitter:site') 6 | meta(content='@Algolia', name='twitter:creator') 7 | meta(content='Algolia JS Helper', name='twitter:title') 8 | meta(content='The Algolia JS Helper is the toolbox to create advanced search experience ', name='twitter:description') 9 | meta(content='Algolia Product image', name='twitter:image') 10 | // / OG meta 11 | meta(content=pkg.homepage, property='og:url') 12 | meta(content='Algolia JS Helper documentation', property='og:title') 13 | meta(content='#', property='og:image') 14 | meta(content='article', property='og:type') 15 | meta(content='The Algolia JS Helper is the toolbox to create advanced search experience ', property='og:description') 16 | meta(content='Algolia JS Helper', property='og:site_name') 17 | -------------------------------------------------------------------------------- /documentation-src/layouts/documentation.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(content='IE=edge', http-equiv='X-UA-Compatible') 5 | meta(charset='utf-8') 6 | meta(content='width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no', name='viewport') 7 | link(href='#', rel='icon', type='image/x-icon') 8 | include ./common/metas.pug 9 | // Use title if it's in the page YAML frontmatter 10 | title= title 11 | link(href='https://fonts.googleapis.com/css?family=Montserrat:500,600|Open+Sans', rel='stylesheet', type='text/css') 12 | link(rel='stylesheet', href='css/index.css') 13 | link(rel='stylesheet', href='https://yandex.st/highlightjs/8.0/styles/github.min.css') 14 | // Google Tag Manager 15 | noscript 16 | iframe(src='//www.googletagmanager.com/ns.html?id=GTM-N8JP8G', height='0', width='0', style='display:none;visibility:hidden') 17 | script. 18 | (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 19 | new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 20 | j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 21 | '//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 22 | })(window,document,'script','dataLayer','GTM-N8JP8G'); 23 | body(class=page_classes) 24 | include ./common/header.pug 25 | section.hero-section.shrink 26 | .container 27 | .hero-container 28 | .fl-right 29 | img(src='images/al-helper-logo.svg') 30 | .fl-left 31 | a(href='https://www.algolia.com/?utm_medium=social-owned&utm_source=algoliasearch-helper%20website&utm_campaign=homepage', title='Algolia built this') 32 | span#icon-algolia.icon.icon-algolia-small.no-mobile 33 | h1= title 34 | section.documentation-section 35 | .container 36 | nav.sidebar 37 | ul 38 | each item in headings 39 | li(class='level-' + item.tag) 40 | a(href='#' + item.id)= item.text 41 | .documentation-container 42 | | !{contents} 43 | include ./common/footer.pug 44 | -------------------------------------------------------------------------------- /documentation-src/metalsmith.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | 4 | var metalsmith = require('metalsmith'); 5 | var inPlace = require('metalsmith-in-place'); 6 | var markdown = require('metalsmith-markdown'); 7 | var jsdoc = require('./plugins/jsdoc-data'); 8 | var registerHandleBarHelpers = require('./plugins/handlebars-helpers'); 9 | var headings = require('./plugins/headings'); 10 | var layouts = require('metalsmith-layouts'); 11 | var metallic = require('metalsmith-metallic'); 12 | var sass = require('@metalsmith/sass'); 13 | var marked = require('marked'); 14 | 15 | var isDev = process.env.NODE_ENV === 'development'; 16 | 17 | var projectRoot = path.join(__dirname, '..'); 18 | var documentationRoot = path.join(projectRoot, 'documentation'); 19 | var cssRoot = path.join(documentationRoot, 'css'); 20 | 21 | var src = { 22 | stylesheets: path.join(__dirname, './stylesheets'), 23 | content: path.join(__dirname, './content'), 24 | layouts: path.join(__dirname, './layouts'), 25 | partials: path.join(__dirname, './partials'), 26 | js: path.join(__dirname, './js/'), 27 | }; 28 | 29 | var customMarkedRenderer = new marked.Renderer(); 30 | var oldHeadingRenderer = customMarkedRenderer.heading; 31 | customMarkedRenderer.heading = function (text, level) { 32 | // do not add id to h4+ 33 | if (level > 3) { 34 | return '' + text + ''; 35 | } 36 | return oldHeadingRenderer.apply(this, arguments); 37 | }; 38 | 39 | var project = require('../package.json'); 40 | var builder = metalsmith(projectRoot) 41 | .metadata({ 42 | pkg: project, 43 | }) 44 | .ignore('.*') 45 | .clean(false) 46 | .source(src.content) 47 | .destination(documentationRoot) 48 | .use( 49 | sass({ 50 | style: isDev ? 'expanded' : 'compressed', 51 | sourceMap: isDev, 52 | sourceMapIncludeSources: isDev, 53 | loadPaths: ['node_modules'], 54 | entries: { 55 | [path.join(src.stylesheets, 'index.scss')]: path.join( 56 | cssRoot, 57 | 'index.css' 58 | ), 59 | }, 60 | }) 61 | ) 62 | .use( 63 | jsdoc({ 64 | src: 'src/algoliasearch.helper.js', 65 | namespace: 'helper', 66 | }) 67 | ) 68 | .use( 69 | jsdoc({ 70 | src: 'src/SearchResults/index.js', 71 | namespace: 'results', 72 | }) 73 | ) 74 | .use( 75 | jsdoc({ 76 | src: 'src/SearchParameters/index.js', 77 | namespace: 'state', 78 | }) 79 | ) 80 | .use( 81 | jsdoc({ 82 | src: 'index.js', 83 | namespace: 'main', 84 | }) 85 | ) 86 | .use( 87 | inPlace({ 88 | engine: 'handlebars', 89 | partials: 'documentation-src/partials', 90 | exposeConsolidate: registerHandleBarHelpers, 91 | }) 92 | ) 93 | .use(metallic()) 94 | .use( 95 | markdown({ 96 | gfm: true, 97 | renderer: customMarkedRenderer, 98 | }) 99 | ) 100 | .use(headings('h2, h3')) 101 | .use( 102 | layouts({ 103 | engine: 'pug', 104 | directory: src.layouts, 105 | }) 106 | ); 107 | 108 | builder.build(function (err) { 109 | if (err) { 110 | throw err; 111 | } 112 | console.log('Metalsmith build finished'); 113 | }); 114 | -------------------------------------------------------------------------------- /documentation-src/partials/event.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{#event}}{{name}}{{/event}}
3 |
4 | 5 | {{{description}}} 6 | 7 |
8 | 21 | {{#if examples}} 22 |
23 | {{#each examples}} 24 | 25 | ```javascript 26 | {{{this}}} 27 | ``` 28 | 29 | {{/each}} 30 |
31 | {{/if}} 32 |
33 | -------------------------------------------------------------------------------- /documentation-src/partials/jsdoc.hbs: -------------------------------------------------------------------------------- 1 | {{!-- 2 | The kinds "typedef", "member" and "event" have self-named partials. 3 | "function" and "module" are special cases of "kind" that are mapped to "method". 4 | --}} 5 | {{> (lookup this "kind") }} 6 | {{#*inline "function"}}{{> method this}}{{/inline}} 7 | {{#*inline "module"}}{{> method this}}{{/inline}} 8 | -------------------------------------------------------------------------------- /documentation-src/partials/member.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{name}} - {{> types type}}
3 |
4 | 5 | {{{description}}} 6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /documentation-src/partials/method.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | {{name}} 6 | ({{{cleanParameters params}}}) ⇒ {{#each returns}}{{> types type}}{{else}}undefined{{/each}} 7 | 8 |
9 | {{#if chainable}} 10 | chainable 11 | {{/if}} 12 | {{#each fires}} 13 | {{this}} 14 | {{/each}} 15 |
16 |
17 | 18 | {{{description}}} 19 | 20 |
21 | {{#if params}} 22 |
23 | {{#each params as |v k|}} 24 |
25 | {{name}} 26 | {{> types type}} 27 |
28 | 29 | {{{description}}} 30 | 31 |
32 |
33 | {{/each}} 34 |
35 | {{/if}} 36 | {{#if chainable}}{{else}}{{#each returns}} 37 |
38 |
39 | {{> types type}} 40 | 41 | 42 | {{{description}}} 43 | 44 | 45 |
46 |
47 | {{else}} 48 |
This method does not return anything
49 | {{/each}} 50 | {{/if}} 51 | {{#if examples}} 52 |
53 | {{#each examples}} 54 | 55 | ```javascript 56 | {{{this}}} 57 | ``` 58 | 59 | {{/each}} 60 |
61 | {{/if}} 62 |
63 | 64 | -------------------------------------------------------------------------------- /documentation-src/partials/typedef.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{name}} 4 | - 5 | {{#if properties}} 6 | record-like object 7 | {{else}} 8 | {{> types type}} 9 | {{/if}} 10 |
11 |
12 | 13 | {{{description}}} 14 | 15 |
16 | {{#if properties}} 17 |
18 | {{#each properties}} 19 |
20 | {{name}} 21 | {{> types type}} 22 | 23 | 24 | {{{description}}} 25 | 26 | 27 |
28 | {{/each}} 29 |
30 | {{/if}} 31 | {{#if params}} 32 |
33 | {{#each params as |v k|}} 34 |
35 | {{name}} 36 | {{> types type}} 37 |
38 | 39 | {{{description}}} 40 | 41 |
42 |
43 | {{/each}} 44 |
45 | {{/if}} 46 | {{#if returns}} 47 | {{#each returns}} 48 |
49 |
50 | {{> types type}} 51 | 52 | 53 | {{{description}}} 54 | 55 | 56 |
57 |
58 | {{/each}} 59 | {{/if}} 60 | {{#if examples}} 61 |
62 | {{#each examples}} 63 | 64 | ```javascript 65 | {{{this}}} 66 | ``` 67 | 68 | {{/each}} 69 |
70 | {{/if}} 71 |
72 | -------------------------------------------------------------------------------- /documentation-src/partials/types.hbs: -------------------------------------------------------------------------------- 1 | {{#each this.names}}{{#type}}{{this}}{{/type}}{{/each}} 2 | -------------------------------------------------------------------------------- /documentation-src/plugins/handlebars-helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Handlebars = require('handlebars'); 3 | 4 | module.exports = function (requires) { 5 | requires.handlebars = Handlebars; 6 | 7 | var arrayRegexp = /^Array\.<(.+?)>$/; 8 | var mapRegexp = /^Object\.$/; 9 | var unionRegexp = /([^|()]+)/gi; 10 | function formatGenerics(type) { 11 | var matchArray = arrayRegexp.exec(type); 12 | if (matchArray) return formatGenerics(matchArray[1]) + '[]'; 13 | var matchMap = mapRegexp.exec(type); 14 | if (matchMap) return '{ string: ' + formatGenerics(matchMap[1]) + ' }'; 15 | var union = []; 16 | var m = unionRegexp.exec(type); 17 | while (m) { 18 | union.push(m[1]); 19 | m = unionRegexp.exec(type); 20 | } 21 | if (union.length > 1) 22 | return '(' + union.map(formatGenerics).join('|') + ')'; 23 | return type; 24 | } 25 | Handlebars.registerHelper('type', function (options) { 26 | return options.fn(formatGenerics(this)); 27 | }); 28 | 29 | Handlebars.registerHelper('event', function (options) { 30 | return options.fn(this).split(':')[1]; 31 | }); 32 | 33 | Handlebars.registerHelper('codepen', function (hash, height) { 34 | var h = typeof height === 'number' ? height : 790; 35 | var url = 36 | '//codepen.io/Algolia/embed/' + 37 | hash + 38 | '?height=' + 39 | h + 40 | '&theme-id=light&slug-hash=' + 41 | hash + 42 | '&default-tab=result&user=Algolia&embed-version=2'; 43 | var codepenContent = 44 | ''; 51 | var downloadLink = 52 | 'Download this example.'; 55 | return new Handlebars.SafeString(downloadLink + codepenContent); 56 | }); 57 | 58 | Handlebars.registerHelper('cleanParameters', function (parameters) { 59 | if (!parameters) return ''; 60 | return new Handlebars.SafeString( 61 | parameters 62 | .map(function (p, k) { 63 | if (p.name.indexOf('.') !== -1) return ''; 64 | if (p.optional) 65 | return `${p.name}`; 66 | return `${p.name}`; 67 | }) 68 | .join('') 69 | ); 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /documentation-src/plugins/headings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { JSDOM } = require('jsdom'); 4 | 5 | module.exports = function ({ selector = 'h2,h3' } = {}) { 6 | return function (files) { 7 | return Object.keys(files).forEach(function (file) { 8 | if (!file.endsWith('.html')) return; 9 | var data = files[file]; 10 | var contents = data.contents.toString(); 11 | var { window } = new JSDOM(contents); 12 | data.headings = []; 13 | 14 | window.document.querySelectorAll(selector).forEach(function (element) { 15 | data.headings.push({ 16 | id: element.id, 17 | tag: element.tagName.toLowerCase(), 18 | text: element.textContent, 19 | }); 20 | }); 21 | }); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /documentation-src/plugins/jsdoc-data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var jsdoc = require('jsdoc-to-markdown'); 3 | 4 | /** 5 | * Same as lodash keyBy, turns an array of objects into an object keyed by the 6 | * value of the given key. When there are multiple objects with the same key, 7 | * the last one wins. 8 | */ 9 | function keyBy(arr, key) { 10 | return arr.reduce(function (obj, o) { 11 | obj[o[key]] = o; 12 | return obj; 13 | }, {}); 14 | } 15 | 16 | module.exports = function (opts) { 17 | if (!opts.src) throw new Error('opts.src must be defined'); 18 | var namespace = opts.namespace; 19 | 20 | return function (_files, metalsmith) { 21 | var files = metalsmith.path(opts.src); 22 | return jsdoc.getTemplateData({ files: files }).then((data) => { 23 | var filteredData = data.filter(function (o) { 24 | return !o.deprecated; 25 | }); 26 | var metadata = metalsmith.metadata(); 27 | if (!namespace) metadata.jsdoc = keyBy(filteredData, 'longname'); 28 | else { 29 | metadata.jsdoc = metadata.jsdoc || {}; 30 | metadata.jsdoc[namespace] = keyBy(filteredData, 'name'); 31 | // console.log(JSON.stringify(metadata.jsdoc, null, 2)); 32 | } 33 | }); 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/components/_buttons.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | background: $accent-color-darkish; 3 | color: #fff; 4 | border: 1px solid $accent-color; 5 | height: 40px; 6 | max-width: 140px; 7 | line-height: 40px; 8 | padding: 0 1em; 9 | text-align: center; 10 | display: block; 11 | text-decoration: none; 12 | border-radius: 3px; 13 | text-transform: uppercase; 14 | font-weight: 600; 15 | font-size: 12px; 16 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1); 17 | transition: background 0.1s ease; 18 | cursor: pointer; 19 | 20 | &:hover, 21 | &:active { 22 | background: $accent-color; 23 | } 24 | 25 | &.btn-cta { 26 | max-width: 230px; 27 | margin: 32px auto; 28 | } 29 | 30 | &.btn-grey { 31 | background: $silver; 32 | border-color: $light-blue-grey; 33 | border-width: 2px; 34 | color: $grey-blue; 35 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1), 0 2px 4px 0 rgba(0, 0, 0, 0.1); 36 | } 37 | 38 | &.btn-black { 39 | background: $black; 40 | border-color: mix($black, #fff, 90%); 41 | } 42 | 43 | &-ghost { 44 | background: transparent; 45 | border-color: currentColor; 46 | font-weight: bold; 47 | max-width: inherit; 48 | padding: 0 32px; 49 | 50 | &-grey { 51 | color: $steel; 52 | 53 | &:hover { 54 | background: $steel; 55 | border-color: $steel; 56 | color: #fff; 57 | 58 | .ico-triangle { 59 | border-left-color: #fff; 60 | } 61 | } 62 | } 63 | 64 | &-pink { 65 | color: $accent-color; 66 | 67 | &:hover { 68 | background: $accent-color; 69 | border-color: $accent-color; 70 | color: #fff; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/components/_clipboard.scss: -------------------------------------------------------------------------------- 1 | .code { 2 | position: relative; 3 | 4 | .clipboard { 5 | position: absolute; 6 | top: 0; 7 | right: 0; 8 | opacity: 0; 9 | transition: opacity 150ms; 10 | } 11 | 12 | &:hover { 13 | .clipboard { 14 | opacity: 1; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/components/_examples-intro.scss: -------------------------------------------------------------------------------- 1 | // icon - degree - colorStart - colorEnd 2 | $square-map: ( 3 | (input, 135, #13c4a5, #10a4b8), 4 | (form, 138, #af84e3, #5071c7), 5 | (city, 314, #ec4918, #e25d8d), 6 | (country, 134, #fad961, #f76b1c), 7 | (map, 135, #81bf30, #00bdbd), 8 | (advanced, 135, #bfbfbf, #8a8a8a) 9 | ); 10 | 11 | .examples-intro { 12 | margin-top: 4em; 13 | margin-bottom: 6em; 14 | 15 | float: left; 16 | width: 100%; 17 | display: none; 18 | 19 | @include small-mq { 20 | margin-bottom: 1.5em; 21 | } 22 | 23 | body.examples & { 24 | display: block; 25 | } 26 | 27 | .items-holder { 28 | margin: 2em auto 2em; 29 | display: block; 30 | max-width: 940px; 31 | padding: 0em; 32 | display: flex; 33 | flex-wrap: wrap; 34 | flex-direction: row; 35 | 36 | @include small-mq { 37 | align-items: center; 38 | margin-bottom: 0; 39 | } 40 | 41 | a { 42 | text-decoration: none; 43 | color: inherit; 44 | } 45 | 46 | .item { 47 | max-width: 120px; 48 | height: 120px; 49 | float: left; 50 | flex: 1 1 auto; 51 | margin: 0 22px; 52 | text-align: center; 53 | 54 | &:first-of-type { 55 | margin-left: 0; 56 | } 57 | &:last-of-type { 58 | margin-right: 0; 59 | } 60 | 61 | @include small-mq { 62 | margin: auto auto 58px; 63 | width: 50%; 64 | 65 | &:first-of-type { 66 | margin-left: auto; 67 | } 68 | &:last-of-type { 69 | margin-right: auto; 70 | } 71 | } 72 | 73 | span.square { 74 | display: block; 75 | position: relative; 76 | max-width: 120px; 77 | height: 120px; 78 | border-radius: 16px; 79 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.3); 80 | transition: box-shadow 0.3s ease, transform 0.1s ease; 81 | 82 | @each $class, $degrees, $color-a, $color-b in $square-map { 83 | &.#{$class} { 84 | background: linear-gradient( 85 | #{$degrees}deg, 86 | #{$color-a} 0, 87 | #{$color-b} 100% 88 | ); 89 | } 90 | } 91 | 92 | &:hover { 93 | box-shadow: none; 94 | transform: scale(1.05); 95 | 96 | & ~ .name { 97 | opacity: 1; 98 | } 99 | } 100 | } 101 | 102 | svg { 103 | position: absolute; 104 | top: 50%; 105 | left: 50%; 106 | transform: translate(-50%, -50%); 107 | } 108 | 109 | .name { 110 | font-family: 'Open Sans', Helvetica Neue, helvetica, sans-serif; 111 | opacity: 0.8; 112 | font-size: 16px; 113 | font-weight: 300; 114 | line-height: 52px; 115 | transition: opacity 0.3s ease; 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/components/_examples.scss: -------------------------------------------------------------------------------- 1 | .form-control { 2 | width: 100%; 3 | box-sizing: border-box; 4 | padding-left: 16px; 5 | line-height: 40px; 6 | height: 40px; 7 | border: 1px solid #cccccc; 8 | border-radius: 3px; 9 | outline: none; 10 | } 11 | 12 | .form-group { 13 | margin-bottom: 1em; 14 | clear: both; 15 | 16 | label { 17 | display: block; 18 | font-weight: bold; 19 | font-size: 0.8em; 20 | } 21 | } 22 | 23 | #map-example-container, 24 | #map-example-container-paris { 25 | // trigger zindex value otherwise 26 | // map goes hover other elements (like header) 27 | position: relative; 28 | z-index: 0; 29 | } 30 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/components/_fonts.scss: -------------------------------------------------------------------------------- 1 | p a { 2 | color: #21a4d7; 3 | text-decoration: none; 4 | } 5 | 6 | /* heading settings 7 | =======================*/ 8 | h1, 9 | h2, 10 | h3, 11 | h4 { 12 | padding: 0; 13 | margin: 0; 14 | } 15 | 16 | h1 { 17 | font-size: 46px; 18 | } 19 | 20 | h3 { 21 | font-size: 20px; 22 | } 23 | 24 | h3 { 25 | font-size: 18px; 26 | } 27 | 28 | /* Paragraphs 29 | ======================*/ 30 | p { 31 | font-family: 'Open Sans', Helvetica Neue, helvetica, sans-serif; 32 | font-size: 20px; 33 | line-height: 32px; 34 | color: $charcoal-grey-75; 35 | 36 | @include small-mq { 37 | font-size: 16px; 38 | } 39 | 40 | & + ul, 41 | & + ol { 42 | font-size: 20px; 43 | line-height: 32px; 44 | color: $charcoal-grey-75; 45 | } 46 | } 47 | 48 | /* Big titles 49 | ======================*/ 50 | h2.title { 51 | text-align: center; 52 | width: 100%; 53 | font-size: 1.8em; 54 | color: $charcoal-grey; 55 | display: block; 56 | position: relative; 57 | margin-top: 120px; 58 | margin-bottom: 80px; 59 | 60 | &:after { 61 | content: ''; 62 | position: absolute; 63 | width: 50px; 64 | height: 3px; 65 | background: $accent-color; 66 | top: 2.8em; 67 | left: 0; 68 | right: 0; 69 | margin: auto; 70 | } 71 | 72 | ~ p { 73 | color: $charcoal-grey; 74 | font-size: 1.3em; 75 | text-align: center; 76 | 77 | @include small-mq { 78 | font-size: 16px; 79 | } 80 | } 81 | 82 | @include small-mq { 83 | margin-top: 60px; 84 | font-size: 22px; 85 | } 86 | } 87 | 88 | /* Small titles 89 | ======================*/ 90 | h3.title { 91 | font-size: 1.5em; 92 | color: $greyish-brown; 93 | font-weight: bold; 94 | margin: 0; 95 | padding: 0; 96 | line-height: 1.2em; 97 | 98 | ~ p { 99 | margin: 32px 0; 100 | padding: 8px 0 0; 101 | } 102 | } 103 | h4 { 104 | font-size: 18px; 105 | } 106 | 107 | /* Anchors / links 108 | ====================*/ 109 | .link { 110 | text-transform: uppercase; 111 | font-size: 16px; 112 | color: $accent-color; 113 | font-weight: 600; 114 | text-decoration: none; 115 | padding: 0 8px; 116 | } 117 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/components/_home.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: $font-settings; 3 | } 4 | 5 | .container { 6 | width: 100%; 7 | height: 100%; 8 | margin: $navigation-height auto; 9 | } 10 | 11 | .welcome { 12 | padding: 2em 0; 13 | text-align: center; 14 | border: 1px solid rgba(black, 0.1); 15 | max-width: $max-container-width; 16 | margin: 6em auto 0; 17 | } 18 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/components/_icons.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | text-indent: -99999px; 3 | color: transparent; 4 | transition: background 0.2s ease; 5 | 6 | &-love { 7 | background: url($icon-heart) no-repeat center center / contain; 8 | } 9 | 10 | &-love-dark { 11 | background: url($icon-heart-dark) no-repeat center center / contain; 12 | } 13 | 14 | &-mail { 15 | background: url($icon-mail) no-repeat 8px 7px / 70%; 16 | } 17 | 18 | &-algolia-small { 19 | background: url(../images/algolia-mark-white.svg) no-repeat center center / 20 | contain; 21 | } 22 | } 23 | 24 | // Small css shape 25 | .ico-triangle { 26 | width: 0; 27 | height: 0; 28 | border-top: 5px solid transparent; 29 | border-bottom: 5px solid transparent; 30 | border-left: 5px solid $steel; 31 | position: relative; 32 | display: inline-block; 33 | margin-left: 8px; 34 | } 35 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/components/_inputs.scss: -------------------------------------------------------------------------------- 1 | input { 2 | outline: none; 3 | border-radius: 4px; 4 | 5 | @include appearance(none !important); 6 | &::-webkit-search-decoration, 7 | &::-webkit-search-cancel-button, 8 | &::-webkit-search-results-button, 9 | &::-webkit-search-results-decoration { 10 | display: none !important; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/components/_media.scss: -------------------------------------------------------------------------------- 1 | .media-object { 2 | display: block; 3 | width: 100%; 4 | height: 100%; 5 | float: left; 6 | padding: 0; 7 | margin: 0; 8 | 9 | img { 10 | object: { 11 | fit: cover; 12 | position: center center; 13 | } 14 | display: block; 15 | padding: 4px; 16 | line-height: 1.42857143; 17 | background-color: #fff; 18 | border: 1px solid #ddd; 19 | border-radius: 4px; 20 | width: 100%; 21 | max-width: 400px; 22 | 23 | &.gif { 24 | display: block; 25 | max-width: 100%; 26 | height: auto; 27 | padding: 4px; 28 | line-height: 1.42857143; 29 | background-color: #fff; 30 | border: 1px solid #ddd; 31 | border-radius: 4px; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/components/_sidebar.scss: -------------------------------------------------------------------------------- 1 | nav.sidebar { 2 | float: left; 3 | width: 100%; 4 | height: 100%; 5 | position: absolute; 6 | max-width: $sidebar-width; 7 | border-right: 1px solid #d8d8d8; 8 | padding-top: 120px; 9 | overflow: auto; 10 | 11 | ul { 12 | list-style: none; 13 | width: 100%; 14 | max-width: $sidebar-width; 15 | padding: 0; 16 | margin: 0; 17 | float: left; 18 | 19 | &.fixed { 20 | position: fixed; 21 | top: $navigation-height; 22 | z-index: 9; 23 | max-height: calc(100vh - #{$navigation-height}); 24 | overflow: auto; 25 | } 26 | 27 | li { 28 | width: 100%; 29 | line-height: 32px; 30 | 31 | &.level-h2 { 32 | } 33 | &.level-h3 { 34 | line-height: 24px; 35 | font-size: 12px; 36 | padding-left: 12px; 37 | font-family: 'Open sans'; 38 | } 39 | 40 | a { 41 | text-decoration: none; 42 | display: block; 43 | width: 100%; 44 | color: $charcoal-grey-75; 45 | font-weight: 400; 46 | 47 | &:focus, 48 | &:target { 49 | @extend %active-links; 50 | } 51 | &.active { 52 | @extend %active-links; 53 | } 54 | } 55 | } 56 | } 57 | 58 | @include small-mq { 59 | width: calc(100% + 16px); 60 | max-width: calc(100% + 16px); 61 | position: absolute; 62 | max-height: 46px; 63 | left: 0; 64 | top: 16px; 65 | border: none; 66 | padding-top: 0; 67 | display: none; 68 | 69 | select { 70 | width: calc(100% - 32px); 71 | margin: 8px 8px; 72 | visibility: hidden; 73 | } 74 | 75 | &.fixed { 76 | background: $brand-primary; 77 | color: #fff; 78 | top: 50px; 79 | display: block; 80 | 81 | select { 82 | visibility: visible; 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @import 'vendors/normalize', 'vendors/variables', 'vendors/mixins', 4 | 'vendors/base', 'vendors/helpers', 'vendors/animations', 5 | 'vendors/communityHeader'; 6 | 7 | @import 'components/footer', 'components/home', 'components/code-highlight', 8 | 'components/icons', 'components/containers', 'components/buttons', 9 | 'components/media', 'components/inputs', 'components/fonts', 10 | 'components/sidebar', 'components/documentation', 'components/examples-intro', 11 | 'components/examples', 'components/footer', 'components/visual-helper', 12 | 'components/clipboard'; 13 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/vendors/_animations.scss: -------------------------------------------------------------------------------- 1 | // Hero Illustration 2 | $timingFunction: 3s ease infinite; 3 | 4 | @mixin anim($name, $p1, $p2, $p3) { 5 | @keyframes #{$name} { 6 | 0%, 7 | 20%, 8 | 80%, 9 | 100% { 10 | transform: #{$p1}; 11 | } 12 | 10%, 13 | 40%, 14 | 50% { 15 | transform: #{$p2}; 16 | } 17 | 60% { 18 | transform: #{$p3}; 19 | } 20 | } 21 | } 22 | @mixin animEllipse($name, $p1, $p2, $p3) { 23 | @keyframes #{$name} { 24 | 0%, 25 | 20%, 26 | 80%, 27 | 100% { 28 | transform: #{$p1}; 29 | } 30 | 10%, 31 | 40%, 32 | 50% { 33 | transform: #{$p2}; 34 | opacity: 0.6; 35 | } 36 | 60% { 37 | transform: #{$p3}; 38 | opacity: 0.6; 39 | } 40 | } 41 | } 42 | 43 | #pin { 44 | animation: bounce $timingFunction; 45 | } 46 | ellipse { 47 | transform-origin: center center; 48 | -moz-transform-origin: 50% 40%; 49 | animation: bounceShadow $timingFunction; 50 | -moz-animation: mozBounceShadow $timingFunction; 51 | } 52 | // Fix svg in FF 53 | #plan-holder { 54 | -moz-transform: translateY(10px); 55 | } 56 | #plan { 57 | -moz-transform: scale(1.1); 58 | } 59 | 60 | @include anim( 61 | 'bounce', 62 | 'translateY(0)', 63 | 'translateY(-5px)', 64 | 'translateY(-5px)' 65 | ); 66 | @include animEllipse( 67 | 'bounceShadow', 68 | 'scale(1) translateY(12px)', 69 | 'scale(0.75) translateY(12px) ', 70 | 'scale(0.75) translateY(12px) ' 71 | ); 72 | @include animEllipse( 73 | 'mozBounceShadow', 74 | 'scale(1) translateY(16px)', 75 | 'scale(0.75) translateY(16px) translateX(-8px) ', 76 | 'scale(0.75) translateY(16px) translateX(-8px) ' 77 | ); 78 | 79 | // Footer heartbeat 80 | @keyframes pulse { 81 | 0% { 82 | box-shadow: 0 0 0 0 rgba($red-pink, 0.4); 83 | } 84 | 70% { 85 | box-shadow: 0 0 0 30px rgba($red-pink, 0); 86 | } 87 | 100% { 88 | box-shadow: 0 0 0 0 rgba($red-pink, 0); 89 | } 90 | } 91 | 92 | @keyframes pulseHeart { 93 | 0% { 94 | transform: scale(1.3); 95 | } 96 | 70% { 97 | transform: scale(1.15); 98 | } 99 | 100% { 100 | transform: scale(1); 101 | } 102 | } 103 | 104 | .icon-love-dark { 105 | display: block; 106 | position: relative; 107 | 108 | &:before { 109 | content: ''; 110 | display: block; 111 | width: 8px; 112 | height: 8px; 113 | position: absolute; 114 | top: 45%; 115 | bottom: 0; 116 | left: 50%; 117 | transform: translate(-50%, -50%); 118 | right: 0; 119 | z-index: -1; 120 | border-radius: 100%; 121 | } 122 | } 123 | 124 | .inner-bloc:hover .icon-love-dark { 125 | animation: pulseHeart 1s ease infinite; 126 | 127 | &:before { 128 | animation: pulse 1s ease infinite !important; 129 | } 130 | } 131 | 132 | // part of animate.css 133 | @keyframes fadeInDown { 134 | 0% { 135 | opacity: 0; 136 | transform: translate3d(0, -100%, 0); 137 | } 138 | 139 | 100% { 140 | opacity: 1; 141 | transform: none; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/vendors/_base.scss: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Montserrat:500,600|Open+Sans); 2 | 3 | * { 4 | box-sizing: border-box; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6 { 15 | font-family: 'Montserrat', Helvetica Neue, helvetica, sans-serif; 16 | } 17 | 18 | :root { 19 | font-family: 'Open Sans', Helvetica Neue, helvetica, sans-serif; 20 | } 21 | 22 | hr { 23 | margin: 0; 24 | } 25 | 26 | figure { 27 | figcaption { 28 | text-indent: -9999px; 29 | color: transparent; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/vendors/_helpers.scss: -------------------------------------------------------------------------------- 1 | $sizes: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10; 2 | 3 | [class*='bloc-'] { 4 | float: left; 5 | } 6 | @each $size in $sizes { 7 | .bloc-#{$size} { 8 | $size: $size * 10%; 9 | width: $size; 10 | } 11 | } 12 | 13 | .relative { 14 | position: relative; 15 | overflow: hidden; 16 | } 17 | 18 | .center-text { 19 | text-align: center; 20 | } 21 | 22 | .fl-left { 23 | float: left; 24 | } 25 | 26 | .fl-right { 27 | float: right; 28 | } 29 | 30 | hr { 31 | border: none; 32 | float: left; 33 | clear: both; 34 | &:before, 35 | &:after { 36 | content: ''; 37 | display: table; 38 | clear: both; 39 | } 40 | 41 | width: 100%; 42 | height: 1px; 43 | background: #e5e5e5; 44 | } 45 | 46 | .pipe { 47 | width: 2px; 48 | height: 20px; 49 | background: $accent-color; 50 | display: inline-block; 51 | float: left; 52 | margin: 20px 16px 0; 53 | line-height: $navigation-height; 54 | 55 | & + span { 56 | font-weight: bold; 57 | width: 0; 58 | float: left; 59 | } 60 | 61 | body.index & { 62 | display: none; 63 | 64 | & + span { 65 | display: none !important; 66 | } 67 | } 68 | } 69 | 70 | // The following helper classes 71 | // made responsive desing easy 72 | // - no-desktop : Only display on mobile 73 | // - no-mobile : Only display on desktop 74 | // - display-on-small : only display on smallish screen ( no mobile ) 75 | 76 | .no-desktop { 77 | @include hide(); 78 | @include small-mq { 79 | @include hide(); 80 | } 81 | 82 | @include mobile-mq { 83 | @include unhide(block); 84 | } 85 | 86 | &.community-badge { 87 | @include mobile-mq { 88 | @include unhide(inline); 89 | } 90 | } 91 | 92 | @media screen and (orientation: landscape) { 93 | @include hide(); 94 | } 95 | } 96 | 97 | .no-mobile { 98 | @include unhide(block); 99 | 100 | &.nav-icon { 101 | @include unhide(inline); 102 | 103 | @include small-mq { 104 | @include hide(); 105 | } 106 | } 107 | 108 | @include small-mq { 109 | @include hide(); 110 | } 111 | } 112 | 113 | .display-on-small { 114 | @include hide(); 115 | 116 | @media (max-width: 960px) { 117 | display: inline-block !important; 118 | visibility: visible; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /documentation-src/stylesheets/vendors/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | @mixin placeholder { 3 | $placeholders: ':-webkit-input' ':-moz' '-moz' '-ms-input'; 4 | @each $placeholder in $placeholders { 5 | &:#{$placeholder}-placeholder { 6 | @content; 7 | } 8 | } 9 | } 10 | @mixin better-fonts() { 11 | h1, 12 | h2, 13 | h3, 14 | h4, 15 | h5, 16 | h6, 17 | span, 18 | div, 19 | p, 20 | pre, 21 | code, 22 | a, 23 | strong, 24 | em, 25 | i { 26 | -webkit-font-smoothing: antialiased; 27 | -moz-osx-font-smoothing: grayscale; 28 | } 29 | } 30 | 31 | @mixin footer-text { 32 | color: #496f92; 33 | text-transform: uppercase; 34 | font-weight: bold; 35 | font-size: 12px; 36 | margin: 0; 37 | font-family: 'Open Sans', Helvetica Neue, helvetica, sans-serif; 38 | font-weight: 800; 39 | float: none; 40 | } 41 | 42 | @mixin hide() { 43 | display: none !important; 44 | visibility: hidden; 45 | } 46 | 47 | @mixin unhide($type) { 48 | display: $type !important; 49 | visibility: visible; 50 | } 51 | @mixin appearance($value) { 52 | -webkit-appearance: $value; 53 | -moz-appearance: $value; 54 | appearance: $value; 55 | } 56 | 57 | // Responsive Breakpoints 58 | @mixin big-min-mq { 59 | @media (min-width: $bp-big) { 60 | @content; 61 | } 62 | } 63 | @mixin huge-min-mq { 64 | @media (min-width: $bp-huge) { 65 | @content; 66 | } 67 | } 68 | 69 | @mixin big-mq { 70 | @media (max-width: $bp-big) { 71 | @content; 72 | } 73 | } 74 | @mixin medium-mq { 75 | @media (max-width: $bp-medium) { 76 | @content; 77 | } 78 | } 79 | @mixin small-mq { 80 | @media (max-width: $bp-medium) { 81 | @content; 82 | } 83 | } 84 | @mixin mobile-mq { 85 | @media (max-width: $bp-small) { 86 | @content; 87 | } 88 | } 89 | @mixin tablet-mq-portrait { 90 | @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: portrait) { 91 | @content; 92 | } 93 | } 94 | @mixin tablet-mq-landscape { 95 | @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) { 96 | @content; 97 | } 98 | } 99 | 100 | // Placeholders 101 | 102 | %fadeInDown { 103 | animation-name: fadeInDown; 104 | } 105 | 106 | %animated { 107 | -webkit-animation-duration: 0.3s; 108 | animation-duration: 0.3s; 109 | -webkit-animation-fill-mode: both; 110 | animation-fill-mode: both; 111 | } 112 | 113 | %active-links { 114 | color: $accent-color; 115 | box-shadow: inset -2px 0 0 0 $accent-color; 116 | 117 | @include small-mq { 118 | font-weight: bold; 119 | box-shadow: none; 120 | color: #fff; 121 | border-bottom: 2px solid $accent-color; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | The examples moved to [codepen](http://codepen.io/collection/nwWJbe/). 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 15 | 16 | 17 | 18 |
19 |

AlgoliaSearchHelper

20 |

Getting started with the examples

21 | 33 |

Play with the examples locally

34 |

35 | To get you started with the helper examples on your machine, follow 36 | these steps: 37 |

38 | 53 |

API documentation

54 | JSDoc 55 |

More informations api Algolia API

56 |

57 | All informations related to Algolia can be found on 58 | the web documentation 61 |

62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AlgoliaSearchHelper = require('./src/algoliasearch.helper'); 4 | 5 | var SearchParameters = require('./src/SearchParameters'); 6 | var SearchResults = require('./src/SearchResults'); 7 | 8 | /** 9 | * The algoliasearchHelper module is the function that will let its 10 | * contains everything needed to use the Algoliasearch 11 | * Helper. It is a also a function that instanciate the helper. 12 | * To use the helper, you also need the Algolia JS client v3. 13 | * @example 14 | * //using the UMD build 15 | * var client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76'); 16 | * var helper = algoliasearchHelper(client, 'bestbuy', { 17 | * facets: ['shipping'], 18 | * disjunctiveFacets: ['category'] 19 | * }); 20 | * helper.on('result', function(event) { 21 | * console.log(event.results); 22 | * }); 23 | * helper 24 | * .toggleFacetRefinement('category', 'Movies & TV Shows') 25 | * .toggleFacetRefinement('shipping', 'Free shipping') 26 | * .search(); 27 | * @example 28 | * // The helper is an event emitter using the node API 29 | * helper.on('result', updateTheResults); 30 | * helper.once('result', updateTheResults); 31 | * helper.removeListener('result', updateTheResults); 32 | * helper.removeAllListeners('result'); 33 | * @module algoliasearchHelper 34 | * @param {AlgoliaSearch} client an AlgoliaSearch client 35 | * @param {string} index the name of the index to query 36 | * @param {SearchParameters|object} opts an object defining the initial config of the search. It doesn't have to be a {SearchParameters}, just an object containing the properties you need from it. 37 | * @return {AlgoliaSearchHelper} The helper instance 38 | */ 39 | function algoliasearchHelper(client, index, opts) { 40 | return new AlgoliaSearchHelper(client, index, opts); 41 | } 42 | 43 | /** 44 | * The version currently used 45 | * @member module:algoliasearchHelper.version 46 | * @type {number} 47 | */ 48 | algoliasearchHelper.version = require('./src/version'); 49 | 50 | /** 51 | * Constructor for the Helper. 52 | * @member module:algoliasearchHelper.AlgoliaSearchHelper 53 | * @type {AlgoliaSearchHelper} 54 | */ 55 | algoliasearchHelper.AlgoliaSearchHelper = AlgoliaSearchHelper; 56 | 57 | /** 58 | * Constructor for the object containing all the parameters of the search. 59 | * @member module:algoliasearchHelper.SearchParameters 60 | * @type {SearchParameters} 61 | */ 62 | algoliasearchHelper.SearchParameters = SearchParameters; 63 | 64 | /** 65 | * Constructor for the object containing the results of the search. 66 | * @member module:algoliasearchHelper.SearchResults 67 | * @type {SearchResults} 68 | */ 69 | algoliasearchHelper.SearchResults = SearchResults; 70 | 71 | module.exports = algoliasearchHelper; 72 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | testEnvironment: 'node', 5 | testMatch: ['/test/spec/**/*.[jt]s?(x)'], 6 | watchPlugins: [ 7 | 'jest-watch-typeahead/filename', 8 | 'jest-watch-typeahead/testname', 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | 'use strict'; 4 | 5 | jest.setTimeout(20000); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "algoliasearch-helper", 3 | "version": "3.13.3", 4 | "description": "Helper for implementing advanced search features with algolia", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "homepage": "https://community.algolia.com/algoliasearch-helper-js/", 8 | "scripts": { 9 | "build": "./scripts/build.sh", 10 | "lint": "eslint .", 11 | "doc": "node documentation-src/metalsmith.js", 12 | "doc:publish": "yarn run doc && gh-pages -d documentation", 13 | "doc:staging": "scripts/staging-doc.sh", 14 | "readme:toc": "doctoc --notitle README.md --maxlevel 3", 15 | "test": "scripts/retry.sh 3 'scripts/test.sh'", 16 | "test:unit": "jest", 17 | "test:watch": "jest --watch", 18 | "release": "./scripts/release.js", 19 | "changelog:view-last": "conventional-changelog -u -n scripts/conventional-changelog/", 20 | "changelog:update": "conventional-changelog -i CHANGELOG -s -u -n scripts/conventional-changelog/", 21 | "prepare": "patch-package" 22 | }, 23 | "author": { 24 | "name": "Algolia Inc.", 25 | "url": "https://www.algolia.com" 26 | }, 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/algolia/algoliasearch-helper-js.git" 31 | }, 32 | "files": [ 33 | "dist", 34 | "src", 35 | "index.js", 36 | "index.d.ts", 37 | "types/algoliasearch.d.ts", 38 | "types/algoliasearch.js" 39 | ], 40 | "devDependencies": { 41 | "@types/algoliasearch": "3.34.11", 42 | "algoliasearch": "4.8.3", 43 | "doctoc": "1.3.0", 44 | "patch-package": "6.2.2", 45 | "eslint": "6.8.0", 46 | "eslint-config-algolia": "16.0.0", 47 | "eslint-config-prettier": "6.15.0", 48 | "eslint-import-resolver-typescript": "2.5.0", 49 | "eslint-plugin-deprecation": "1.3.2", 50 | "eslint-plugin-eslint-comments": "3.2.0", 51 | "eslint-plugin-import": "2.24.2", 52 | "eslint-plugin-jasmine": "4.1.0", 53 | "eslint-plugin-jest": "27.0.4", 54 | "eslint-plugin-prettier": "3.4.0", 55 | "eslint-plugin-react": "7.26.0", 56 | "eslint-plugin-react-hooks": "4.3.0", 57 | "eslint-plugin-vue": "9.8.0", 58 | "eslint-plugin-wdio": "5.11.0", 59 | "@typescript-eslint/eslint-plugin": "5.38.1", 60 | "@typescript-eslint/parser": "4.31.2", 61 | "prettier": "2.4.1", 62 | "gh-pages": "1.0.0", 63 | "jest": "24.7.1", 64 | "jest-watch-typeahead": "0.3.0", 65 | "browserify": "14.5.0", 66 | "exorcist": "1.0.1", 67 | "uglify-js": "2.8.29", 68 | "colors": "1.1.2", 69 | "conventional-changelog-cli": "1.3.2", 70 | "pretty-bytes-cli": "2.0.0", 71 | "prompt": "1.0.0", 72 | "semver": "5.3.0", 73 | "shelljs": "0.7.8", 74 | "mversion": "1.13.0", 75 | "yargs": "13.2.4", 76 | "jsdoc-to-markdown": "8.0.0", 77 | "handlebars": "4.1.0", 78 | "metalsmith": "2.5.1", 79 | "metalsmith-in-place": "1.4.4", 80 | "metalsmith-layouts": "1.8.1", 81 | "metalsmith-markdown": "1.3.0", 82 | "metalsmith-metallic": "1.0.0", 83 | "@metalsmith/sass": "1.4.0", 84 | "pug": "2.0.3", 85 | "typescript": "5.1.3" 86 | }, 87 | "dependencies": { 88 | "@algolia/events": "^4.0.1" 89 | }, 90 | "peerDependencies": { 91 | "algoliasearch": ">= 3.1 < 6" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /patches/metalsmith-in-place+1.4.4.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/metalsmith-in-place/lib/index.js b/node_modules/metalsmith-in-place/lib/index.js 2 | index 4c0092a..9924129 100644 3 | --- a/node_modules/metalsmith-in-place/lib/index.js 4 | +++ b/node_modules/metalsmith-in-place/lib/index.js 5 | @@ -24,7 +24,9 @@ module.exports = plugin; 6 | * 7 | * Options supported by metalsmith-in-place 8 | */ 9 | -var settings = ['engine', 'partials', 'pattern', 'rename']; 10 | +// we need access to handlebars helpers (codepen etc.), so we use exposeConsolidate 11 | +// to add plugins/handlebars-helpers.js 12 | +var settings = ['engine', 'partials', 'pattern', 'rename', 'exposeConsolidate']; 13 | 14 | /** 15 | * Metalsmith plugin for in-place templating. 16 | @@ -57,6 +59,10 @@ function plugin(opts){ 17 | throw new Error('Unknown template engine: "' + opts.engine + '"'); 18 | } 19 | 20 | + if (typeof opts.exposeConsolidate === 'function') { 21 | + opts.exposeConsolidate(consolidate.requires); 22 | + } 23 | + 24 | // Map options to local variables 25 | var engine = opts.engine; 26 | var partials = opts.partials; 27 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # exit when error 4 | 5 | [ -z $CIRCLE_BUILD_NUM ] && CI='false' || CI='true' 6 | 7 | if [ $CI == 'true' ]; then 8 | set -x # debug messages 9 | fi 10 | 11 | bundle='algoliasearch.helper' 12 | 13 | echo "Build" 14 | 15 | mkdir -p dist 16 | 17 | browserify index.js \ 18 | --standalone algoliasearchHelper \ 19 | --debug | \ 20 | exorcist dist/algoliasearch.helper.js.map > dist/algoliasearch.helper.js 21 | 22 | echo "..Minify" 23 | 24 | uglifyjs dist/algoliasearch.helper.js \ 25 | --mangle \ 26 | --compress=warnings=false \ 27 | --in-source-map "dist/algoliasearch.helper.js.map" \ 28 | --source-map "dist/algoliasearch.helper.min.js.map" \ 29 | --output dist/algoliasearch.helper.min.js 30 | 31 | echo '..Gzipped file size' 32 | 33 | echo "${bundle}.min.js gzipped will weigh" $(cat dist/"${bundle}".min.js | gzip -9 | wc -c | pretty-bytes) 34 | -------------------------------------------------------------------------------- /scripts/conventional-changelog/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var parserOpts = { 3 | // eslint-disable-next-line no-useless-escape 4 | headerPattern: /^(\w*)(?:\((.*)\))?\: (.*)$/, 5 | headerCorrespondence: ['type', 'scope', 'subject'], 6 | noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES'], 7 | revertPattern: /^revert:\s([\s\S]*?)\s*This reverts commit (\w*)\./, 8 | revertCorrespondence: ['header', 'hash'], 9 | }; 10 | 11 | var writerOpts = { 12 | transform: function (commit) { 13 | return commit; 14 | }, 15 | groupBy: 'type', 16 | commitGroupsSort: 'title', 17 | commitsSort: ['scope', 'subject'], 18 | noteGroupsSort: 'title', 19 | headerPartial: '{{version}} - {{date}}', 20 | mainTemplate: `{{> header}} 21 | 22 | {{#each commitGroups}} 23 | {{#each commits}} 24 | {{> commit root=@root}} 25 | {{/each}} 26 | {{/each}} 27 | 28 | {{> footer}} 29 | `, 30 | commitPartial: ` * {{header}} 31 | 32 | {{~!-- commit link --}} {{#if @root.linkReferences~}} 33 | {{~#if @root.repository}} 34 | {{~#if @root.host}} 35 | {{~@root.host}}/ 36 | {{~/if}} 37 | {{~#if @root.owner}} 38 | {{~@root.owner}}/ 39 | {{~/if}} 40 | {{~@root.repository}} 41 | {{~else}} 42 | {{~@root.repoUrl}} 43 | {{~/if}}/ 44 | {{~@root.commit}}/{{hash}} 45 | {{~else}} 46 | {{~hash}} 47 | {{~/if}} 48 | 49 | {{~!-- commit references --}} 50 | {{~#if references~}} 51 | , closes 52 | {{~#each references}} {{#if @root.linkReferences~}} 53 | {{~#if @root.repository}} 54 | {{~#if @root.host}} 55 | {{~@root.host}}/ 56 | {{~/if}} 57 | {{~#if this.repository}} 58 | {{~#if this.owner}} 59 | {{~this.owner}}/ 60 | {{~/if}} 61 | {{~this.repository}} 62 | {{~else}} 63 | {{~#if @root.owner}} 64 | {{~@root.owner}}/ 65 | {{~/if}} 66 | {{~@root.repository}} 67 | {{~/if}} 68 | {{~else}} 69 | {{~@root.repoUrl}} 70 | {{~/if}}/ 71 | {{~@root.issue}}/{{this.issue}} 72 | {{~else}} 73 | {{~#if this.owner}} 74 | {{~this.owner}}/ 75 | {{~/if}} 76 | {{~this.repository}}#{{this.issue}} 77 | {{~/if}}{{/each}} 78 | {{~/if}} 79 | 80 | `, 81 | }; 82 | 83 | /* 84 | writerOpts.mainTemplate = template; 85 | writerOpts.headerPartial = header; 86 | writerOpts.commitPartial = commit; 87 | writerOpts.footerPartial = footer; 88 | */ 89 | 90 | module.exports = { 91 | parserOpts: parserOpts, 92 | writerOpts: writerOpts, 93 | }; 94 | -------------------------------------------------------------------------------- /scripts/lib/conventionalChangelog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | showChangelog, 5 | updateChangelog, 6 | getChangelog, 7 | }; 8 | 9 | const colors = require('colors/safe'); 10 | 11 | const baseConvChangelog = 12 | 'conventional-changelog --config scripts/conventional-changelog/'; 13 | 14 | function showChangelog(shell) { 15 | shell.echo(colors.yellow.underline('\nNext version changelog:')); 16 | const changelog = shell.exec(`${baseConvChangelog} -u`, { 17 | silent: true, 18 | }).stdout; 19 | shell.echo(colors.white(changelog)); 20 | } 21 | 22 | function updateChangelog(shell) { 23 | shell.echo(colors.yellow('⚠️ Updating changelog')); 24 | shell.exec(`${baseConvChangelog} --infile CHANGELOG --same-file`, { 25 | silent: true, 26 | }); 27 | } 28 | 29 | function getChangelog(shell) { 30 | const changelog = shell 31 | .exec(`${baseConvChangelog} -u`, { silent: true }) 32 | .trim() 33 | .split('\n'); 34 | changelog.splice(1, 0, ''); 35 | return changelog; 36 | } 37 | -------------------------------------------------------------------------------- /scripts/retry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Retry a command up to a specific number of times until it exits successfully 3 | # Usage: scripts/retry.sh 4 | 5 | retries="$1" 6 | command="$2" 7 | 8 | echo trying "$command", $retries retries left 9 | 10 | # Run the command, and save the exit code 11 | ($command) 12 | exit_code=$? 13 | 14 | # If the exit code is non-zero (i.e. command failed), and we have not 15 | # reached the maximum number of retries, run the command again 16 | if [[ $exit_code -ne 0 && $retries -gt 0 ]]; then 17 | scripts/retry.sh $(($retries - 1)) "$command" 18 | else 19 | # Return the exit code from the command 20 | exit $exit_code 21 | fi 22 | 23 | -------------------------------------------------------------------------------- /scripts/staging-doc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ev # exit when error 4 | 5 | yarn 6 | yarn run doc 7 | surge documentation 8 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # exit when error 4 | 5 | [ -z $CIRCLE_BUILD_NUM ] && CI='false' || CI='true' 6 | 7 | if [ $CI == 'true' ]; then 8 | set -x # debug messages 9 | fi 10 | 11 | node test/run.js 12 | -------------------------------------------------------------------------------- /src/DerivedHelper/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('@algolia/events'); 4 | var inherits = require('../functions/inherits'); 5 | 6 | /** 7 | * A DerivedHelper is a way to create sub requests to 8 | * Algolia from a main helper. 9 | * @class 10 | * @classdesc The DerivedHelper provides an event based interface for search callbacks: 11 | * - search: when a search is triggered using the `search()` method. 12 | * - result: when the response is retrieved from Algolia and is processed. 13 | * This event contains a {@link SearchResults} object and the 14 | * {@link SearchParameters} corresponding to this answer. 15 | * @param {AlgoliaSearchHelper} mainHelper the main helper 16 | * @param {function} fn the function to create the derived state 17 | */ 18 | function DerivedHelper(mainHelper, fn) { 19 | this.main = mainHelper; 20 | this.fn = fn; 21 | this.lastResults = null; 22 | } 23 | 24 | inherits(DerivedHelper, EventEmitter); 25 | 26 | /** 27 | * Detach this helper from the main helper 28 | * @return {undefined} 29 | * @throws Error if the derived helper is already detached 30 | */ 31 | DerivedHelper.prototype.detach = function () { 32 | this.removeAllListeners(); 33 | this.main.detachDerivedHelper(this); 34 | }; 35 | 36 | DerivedHelper.prototype.getModifiedState = function (parameters) { 37 | return this.fn(parameters); 38 | }; 39 | 40 | module.exports = DerivedHelper; 41 | -------------------------------------------------------------------------------- /src/functions/compact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function compact(array) { 4 | if (!Array.isArray(array)) { 5 | return []; 6 | } 7 | 8 | return array.filter(Boolean); 9 | }; 10 | -------------------------------------------------------------------------------- /src/functions/defaultsPure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // NOTE: this behaves like lodash/defaults, but doesn't mutate the target 4 | // it also preserve keys order 5 | module.exports = function defaultsPure() { 6 | var sources = Array.prototype.slice.call(arguments); 7 | 8 | return sources.reduceRight(function (acc, source) { 9 | Object.keys(Object(source)).forEach(function (key) { 10 | if (source[key] === undefined) { 11 | return; 12 | } 13 | if (acc[key] !== undefined) { 14 | // remove if already added, so that we can add it in correct order 15 | // eslint-disable-next-line no-param-reassign 16 | delete acc[key]; 17 | } 18 | // eslint-disable-next-line no-param-reassign 19 | acc[key] = source[key]; 20 | }); 21 | return acc; 22 | }, {}); 23 | }; 24 | -------------------------------------------------------------------------------- /src/functions/escapeFacetValue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Replaces a leading - with \- 5 | * @private 6 | * @param {any} value the facet value to replace 7 | * @returns {any} the escaped facet value or the value if it was not a string 8 | */ 9 | function escapeFacetValue(value) { 10 | if (typeof value !== 'string') return value; 11 | 12 | return String(value).replace(/^-/, '\\-'); 13 | } 14 | 15 | /** 16 | * Replaces a leading \- with - 17 | * @private 18 | * @param {any} value the escaped facet value 19 | * @returns {any} the unescaped facet value or the value if it was not a string 20 | */ 21 | function unescapeFacetValue(value) { 22 | if (typeof value !== 'string') return value; 23 | 24 | return value.replace(/^\\-/, '-'); 25 | } 26 | 27 | module.exports = { 28 | escapeFacetValue: escapeFacetValue, 29 | unescapeFacetValue: unescapeFacetValue, 30 | }; 31 | -------------------------------------------------------------------------------- /src/functions/find.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // @MAJOR can be replaced by native Array#find when we change support 4 | module.exports = function find(array, comparator) { 5 | if (!Array.isArray(array)) { 6 | return undefined; 7 | } 8 | 9 | for (var i = 0; i < array.length; i++) { 10 | if (comparator(array[i])) { 11 | return array[i]; 12 | } 13 | } 14 | 15 | return undefined; 16 | }; 17 | -------------------------------------------------------------------------------- /src/functions/findIndex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // @MAJOR can be replaced by native Array#findIndex when we change support 4 | module.exports = function find(array, comparator) { 5 | if (!Array.isArray(array)) { 6 | return -1; 7 | } 8 | 9 | for (var i = 0; i < array.length; i++) { 10 | if (comparator(array[i])) { 11 | return i; 12 | } 13 | } 14 | return -1; 15 | }; 16 | -------------------------------------------------------------------------------- /src/functions/formatSort.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var find = require('./find'); 4 | 5 | /** 6 | * Transform sort format from user friendly notation to lodash format 7 | * @param {string[]} sortBy array of predicate of the form "attribute:order" 8 | * @param {string[]} [defaults] array of predicate of the form "attribute:order" 9 | * @return {array.} array containing 2 elements : attributes, orders 10 | */ 11 | module.exports = function formatSort(sortBy, defaults) { 12 | var defaultInstructions = (defaults || []).map(function (sort) { 13 | return sort.split(':'); 14 | }); 15 | 16 | return sortBy.reduce( 17 | function preparePredicate(out, sort) { 18 | var sortInstruction = sort.split(':'); 19 | 20 | var matchingDefault = find( 21 | defaultInstructions, 22 | function (defaultInstruction) { 23 | return defaultInstruction[0] === sortInstruction[0]; 24 | } 25 | ); 26 | 27 | if (sortInstruction.length > 1 || !matchingDefault) { 28 | out[0].push(sortInstruction[0]); 29 | out[1].push(sortInstruction[1]); 30 | return out; 31 | } 32 | 33 | out[0].push(matchingDefault[0]); 34 | out[1].push(matchingDefault[1]); 35 | return out; 36 | }, 37 | [[], []] 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/functions/inherits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function inherits(ctor, superCtor) { 4 | // eslint-disable-next-line no-param-reassign 5 | ctor.prototype = Object.create(superCtor.prototype, { 6 | constructor: { 7 | value: ctor, 8 | enumerable: false, 9 | writable: true, 10 | configurable: true, 11 | }, 12 | }); 13 | } 14 | 15 | module.exports = inherits; 16 | -------------------------------------------------------------------------------- /src/functions/intersection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function intersection(arr1, arr2) { 4 | return arr1.filter(function (value, index) { 5 | return ( 6 | arr2.indexOf(value) > -1 && 7 | arr1.indexOf(value) === index /* skips duplicates */ 8 | ); 9 | }); 10 | } 11 | 12 | module.exports = intersection; 13 | -------------------------------------------------------------------------------- /src/functions/merge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function clone(value) { 4 | if (typeof value === 'object' && value !== null) { 5 | return _merge(Array.isArray(value) ? [] : {}, value); 6 | } 7 | return value; 8 | } 9 | 10 | function isObjectOrArrayOrFunction(value) { 11 | return ( 12 | typeof value === 'function' || 13 | Array.isArray(value) || 14 | Object.prototype.toString.call(value) === '[object Object]' 15 | ); 16 | } 17 | 18 | function _merge(target, source) { 19 | if (target === source) { 20 | return target; 21 | } 22 | 23 | for (var key in source) { 24 | if ( 25 | !Object.prototype.hasOwnProperty.call(source, key) || 26 | key === '__proto__' || 27 | key === 'constructor' 28 | ) { 29 | // eslint-disable-next-line no-continue 30 | continue; 31 | } 32 | 33 | var sourceVal = source[key]; 34 | var targetVal = target[key]; 35 | 36 | if (typeof targetVal !== 'undefined' && typeof sourceVal === 'undefined') { 37 | // eslint-disable-next-line no-continue 38 | continue; 39 | } 40 | 41 | if ( 42 | isObjectOrArrayOrFunction(targetVal) && 43 | isObjectOrArrayOrFunction(sourceVal) 44 | ) { 45 | // eslint-disable-next-line no-param-reassign 46 | target[key] = _merge(targetVal, sourceVal); 47 | } else { 48 | // eslint-disable-next-line no-param-reassign 49 | target[key] = clone(sourceVal); 50 | } 51 | } 52 | return target; 53 | } 54 | 55 | /** 56 | * This method is like Object.assign, but recursively merges own and inherited 57 | * enumerable keyed properties of source objects into the destination object. 58 | * 59 | * NOTE: this behaves like lodash/merge, but: 60 | * - does mutate functions if they are a source 61 | * - treats non-plain objects as plain 62 | * - does not work for circular objects 63 | * - treats sparse arrays as sparse 64 | * - does not convert Array-like objects (Arguments, NodeLists, etc.) to arrays 65 | * 66 | * @param {Object} target The destination object. 67 | * @param {...Object} [sources] The source objects. 68 | * @returns {Object} Returns `object`. 69 | */ 70 | function merge(target) { 71 | if (!isObjectOrArrayOrFunction(target)) { 72 | // eslint-disable-next-line no-param-reassign 73 | target = {}; 74 | } 75 | 76 | for (var i = 1, l = arguments.length; i < l; i++) { 77 | var source = arguments[i]; 78 | 79 | if (isObjectOrArrayOrFunction(source)) { 80 | _merge(target, source); 81 | } 82 | } 83 | return target; 84 | } 85 | 86 | module.exports = merge; 87 | -------------------------------------------------------------------------------- /src/functions/objectHasKeys.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function objectHasKeys(obj) { 4 | return obj && Object.keys(obj).length > 0; 5 | } 6 | 7 | module.exports = objectHasKeys; 8 | -------------------------------------------------------------------------------- /src/functions/omit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // https://github.com/babel/babel/blob/3aaafae053fa75febb3aa45d45b6f00646e30ba4/packages/babel-helpers/src/helpers.js#L604-L620 4 | function _objectWithoutPropertiesLoose(source, excluded) { 5 | if (source === null) return {}; 6 | var target = {}; 7 | var sourceKeys = Object.keys(source); 8 | var key; 9 | var i; 10 | for (i = 0; i < sourceKeys.length; i++) { 11 | key = sourceKeys[i]; 12 | // eslint-disable-next-line no-continue 13 | if (excluded.indexOf(key) >= 0) continue; 14 | target[key] = source[key]; 15 | } 16 | return target; 17 | } 18 | 19 | module.exports = _objectWithoutPropertiesLoose; 20 | -------------------------------------------------------------------------------- /src/functions/orderBy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function compareAscending(value, other) { 4 | if (value !== other) { 5 | var valIsDefined = value !== undefined; 6 | var valIsNull = value === null; 7 | 8 | var othIsDefined = other !== undefined; 9 | var othIsNull = other === null; 10 | 11 | if ( 12 | (!othIsNull && value > other) || 13 | (valIsNull && othIsDefined) || 14 | !valIsDefined 15 | ) { 16 | return 1; 17 | } 18 | if ( 19 | (!valIsNull && value < other) || 20 | (othIsNull && valIsDefined) || 21 | !othIsDefined 22 | ) { 23 | return -1; 24 | } 25 | } 26 | return 0; 27 | } 28 | 29 | /** 30 | * @param {Array} collection object with keys in attributes 31 | * @param {Array} iteratees attributes 32 | * @param {Array} orders asc | desc 33 | * @return {Array} sorted collection 34 | */ 35 | function orderBy(collection, iteratees, orders) { 36 | if (!Array.isArray(collection)) { 37 | return []; 38 | } 39 | 40 | if (!Array.isArray(orders)) { 41 | // eslint-disable-next-line no-param-reassign 42 | orders = []; 43 | } 44 | 45 | var result = collection.map(function (value, index) { 46 | return { 47 | criteria: iteratees.map(function (iteratee) { 48 | return value[iteratee]; 49 | }), 50 | index: index, 51 | value: value, 52 | }; 53 | }); 54 | 55 | result.sort(function comparer(object, other) { 56 | var index = -1; 57 | 58 | while (++index < object.criteria.length) { 59 | var res = compareAscending(object.criteria[index], other.criteria[index]); 60 | if (res) { 61 | if (index >= orders.length) { 62 | return res; 63 | } 64 | if (orders[index] === 'desc') { 65 | return -res; 66 | } 67 | return res; 68 | } 69 | } 70 | 71 | // This ensures a stable sort in V8 and other engines. 72 | // See https://bugs.chromium.org/p/v8/issues/detail?id=90 for more details. 73 | return object.index - other.index; 74 | }); 75 | 76 | return result.map(function (res) { 77 | return res.value; 78 | }); 79 | } 80 | 81 | module.exports = orderBy; 82 | -------------------------------------------------------------------------------- /src/functions/valToNumber.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function valToNumber(v) { 4 | if (typeof v === 'number') { 5 | return v; 6 | } else if (typeof v === 'string') { 7 | return parseFloat(v); 8 | } else if (Array.isArray(v)) { 9 | return v.map(valToNumber); 10 | } 11 | 12 | throw new Error( 13 | 'The value should be a number, a parsable string or an array of those.' 14 | ); 15 | } 16 | 17 | module.exports = valToNumber; 18 | -------------------------------------------------------------------------------- /src/utils/isValidUserToken.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function isValidUserToken(userToken) { 4 | if (userToken === null) { 5 | return false; 6 | } 7 | return /^[a-zA-Z0-9_-]{1,64}$/.test(userToken); 8 | }; 9 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = '3.13.3'; 4 | -------------------------------------------------------------------------------- /test/datasets/SearchResults/getFacetValues.dataset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (require.main === module) { 4 | /* 5 | * This file generates the tests files so that the tests can be repeated offline. 6 | * To regenerate the files, just run it again. 7 | */ 8 | 9 | var path = require('path'); 10 | 11 | var replayTools = require('../../replayTools'); 12 | var Helper = require('../../../src/algoliasearch.helper'); 13 | var HelperSaver = replayTools.toSaver( 14 | Helper, 15 | path.join(__dirname.replace('datasets', 'spec'), 'getFacetValues') 16 | ); 17 | var algoliasearch = require('algoliasearch'); 18 | 19 | var client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76'); 20 | var helper = new HelperSaver(client, 'instant_search', { 21 | disjunctiveFacets: ['brand'], 22 | maxValuesPerFacet: 3, 23 | }); 24 | 25 | var initialState = helper.state; 26 | 27 | helper 28 | .searchOnce() 29 | .then(function () { 30 | helper.__saveLastToFile('noFilters.json'); 31 | 32 | var otherState = initialState.addDisjunctiveFacetRefinement( 33 | 'brand', 34 | 'Apple' 35 | ); 36 | return helper.searchOnce(otherState); 37 | }) 38 | .then(function () { 39 | helper.__saveLastToFile('disjunctive.json'); 40 | }) 41 | .then( 42 | function () { 43 | console.log('Dataset sucessfully generated'); 44 | }, 45 | function (e) { 46 | console.error(e); 47 | } 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /test/datasets/SearchResults/getRefinements.dataset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (require.main === module) { 4 | /* 5 | * This file generates the tests files so that the tests can be repeated offline. 6 | * To regenerate the files, just run it again. 7 | */ 8 | 9 | var path = require('path'); 10 | 11 | var replayTools = require('../../replayTools'); 12 | var Helper = require('../../../src/algoliasearch.helper'); 13 | var HelperSaver = replayTools.toSaver( 14 | Helper, 15 | path.join(__dirname.replace('datasets', 'spec'), 'getRefinements') 16 | ); 17 | var algoliasearch = require('algoliasearch'); 18 | 19 | var client = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76'); 20 | var helper = new HelperSaver(client, 'instant_search', { 21 | facets: ['brand'], 22 | disjunctiveFacets: ['type', 'rating'], 23 | hierarchicalFacets: [ 24 | { 25 | name: 'hierarchicalCategories', 26 | attributes: [ 27 | 'hierarchicalCategories.lvl0', 28 | 'hierarchicalCategories.lvl1', 29 | 'hierarchicalCategories.lvl2', 30 | 'hierarchicalCategories.lvl3', 31 | ], 32 | }, 33 | ], 34 | }); 35 | 36 | var initialState = helper.state; 37 | 38 | helper 39 | .searchOnce() 40 | .then(function () { 41 | helper.__saveLastToFile('noFilters.json'); 42 | 43 | var otherState = initialState.addFacetRefinement('brand', 'Apple'); 44 | return helper.searchOnce(otherState); 45 | }) 46 | .then(function () { 47 | helper.__saveLastToFile('conjunctive-brand-apple.json'); 48 | 49 | var otherState = initialState.addDisjunctiveFacetRefinement( 50 | 'type', 51 | 'Trend cases' 52 | ); 53 | return helper.searchOnce(otherState); 54 | }) 55 | .then(function () { 56 | helper.__saveLastToFile('disjunctive-type-trendcase.json'); 57 | 58 | var otherState = initialState.addNumericRefinement('rating', '=', 3); 59 | return helper.searchOnce(otherState); 60 | }) 61 | .then(function () { 62 | helper.__saveLastToFile('numeric-rating-3.json'); 63 | 64 | var otherState = initialState.toggleHierarchicalFacetRefinement( 65 | 'hierarchicalCategories', 66 | 'Best Buy Gift Cards > Entertainment Gift Cards' 67 | ); 68 | return helper.searchOnce(otherState); 69 | }) 70 | .then(function () { 71 | helper.__saveLastToFile('hierarchical-cards.json'); 72 | 73 | var otherState = initialState 74 | .addTagRefinement('foo') 75 | .addTagRefinement('bar'); 76 | return helper.searchOnce(otherState); 77 | }) 78 | .then(function () { 79 | helper.__saveLastToFile('dummy-tags.json'); 80 | 81 | var otherState = initialState.addExcludeRefinement('brand', 'Apple'); 82 | return helper.searchOnce(otherState); 83 | }) 84 | .then(function () { 85 | helper.__saveLastToFile('exclude-apple.json'); 86 | }) 87 | .then( 88 | function () { 89 | console.log('Dataset sucessfully generated'); 90 | }, 91 | function (e) { 92 | console.error(e); 93 | } 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /test/integration-spec/helper.derive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../integration-utils'); 4 | var setup = utils.setupSimple; 5 | var createIndexName = utils.createIndexName; 6 | 7 | var algoliasearchHelper = require('../../'); 8 | 9 | var indexName = createIndexName('helper_derive'); 10 | 11 | var dataset = [ 12 | { objectID: '0', content: 'tata' }, 13 | { objectID: '1', content: 'toto' }, 14 | ]; 15 | 16 | var config = {}; 17 | 18 | var client; 19 | beforeAll(function () { 20 | return setup(indexName, dataset, config).then(function (c) { 21 | client = c; 22 | }); 23 | }); 24 | 25 | test('[INT][DERIVE] Query the same index twice with different query', function () { 26 | return Promise.resolve() 27 | .then(function () { 28 | var mainHelper = algoliasearchHelper(client, indexName, { 29 | facets: ['f'], 30 | disjunctiveFacets: ['df'], 31 | hierarchicalFacets: [ 32 | { 33 | name: 'products', 34 | attributes: ['categories.lvl0', 'categories.lvl1'], 35 | }, 36 | ], 37 | }); 38 | 39 | var derivedHelper = mainHelper.derive(function (state) { 40 | return state.setQuery('toto'); 41 | }); 42 | 43 | var mainResponse = new Promise(function (resolve) { 44 | mainHelper.on('result', resolve); 45 | }); 46 | 47 | var derivedResponse = new Promise(function (resolve) { 48 | derivedHelper.on('result', resolve); 49 | }); 50 | 51 | mainHelper.search(); 52 | 53 | return Promise.all([mainResponse, derivedResponse]); 54 | }) 55 | .then(function (responses) { 56 | var mainResponse = responses[0]; 57 | 58 | expect(mainResponse.state.query).toBeUndefined(); 59 | expect(mainResponse.results.hits.length).toBe(2); 60 | 61 | var derivedResponse = responses[1]; 62 | 63 | expect(derivedResponse.state.query).toBe('toto'); 64 | expect(derivedResponse.results.hits.length).toBe(1); 65 | expect(derivedResponse.results.hits[0].objectID).toBe('1'); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/integration-spec/helper.distinct.facet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../integration-utils'); 4 | var setup = utils.setupSimple; 5 | var createIndexName = utils.createIndexName; 6 | 7 | var algoliasearchHelper = require('../../'); 8 | 9 | var indexName = createIndexName('helper_distinct.facet'); 10 | 11 | var dataset = [ 12 | { type: 'shoes', name: 'Adidas Stan Smith', colors: ['blue', 'red'] }, 13 | { type: 'shoes', name: 'Converse Chuck Taylor', colors: ['blue', 'green'] }, 14 | { type: 'shoes', name: 'Nike Air Jordan', colors: ['gold', 'red'] }, 15 | ]; 16 | 17 | var config = { 18 | attributesToIndex: ['type', 'colors', 'name'], 19 | attributeForDistinct: 'type', 20 | attributesForFaceting: ['type', 'colors'], 21 | }; 22 | 23 | var client; 24 | beforeAll(function () { 25 | return setup(indexName, dataset, config).then(function (c) { 26 | client = c; 27 | }); 28 | }); 29 | 30 | test('[INT][FILTERS] Using distinct should let me retrieve all facet without distinct', function (done) { 31 | var helper = algoliasearchHelper(client, indexName, { 32 | facets: ['colors'], 33 | }); 34 | 35 | helper.on('error', function (event) { 36 | done.fail(event.error); 37 | }); 38 | 39 | helper.on('result', function (event) { 40 | expect(event.results.hits.length).toBe(1); 41 | expect(event.results.facets[0].data).toEqual({ 42 | blue: 2, 43 | red: 2, 44 | gold: 1, 45 | green: 1, 46 | }); 47 | 48 | client.deleteIndex(indexName); 49 | 50 | if (!process.browser) { 51 | client.destroy(); 52 | } 53 | 54 | done(); 55 | }); 56 | 57 | helper.setQueryParameter('distinct', true).search(); 58 | }); 59 | -------------------------------------------------------------------------------- /test/integration-spec/helper.filters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../integration-utils'); 4 | var setup = utils.setupSimple; 5 | var createIndexName = utils.createIndexName; 6 | 7 | var algoliasearchHelper = require('../../'); 8 | 9 | var indexName = createIndexName('helper_filters'); 10 | 11 | var dataset = [ 12 | { facet: ['f1', 'f2'] }, 13 | { facet: ['f1', 'f3'] }, 14 | { facet: ['f2', 'f3'] }, 15 | ]; 16 | 17 | var config = { 18 | attributesToIndex: ['facet'], 19 | attributesForFaceting: ['facet'], 20 | }; 21 | 22 | var client; 23 | beforeAll(function () { 24 | return setup(indexName, dataset, config).then(function (c) { 25 | client = c; 26 | }); 27 | }); 28 | 29 | test('[INT][FILTERS] Should retrieve different values for multi facetted records', function (done) { 30 | var helper = algoliasearchHelper(client, indexName, { 31 | facets: ['facet'], 32 | }); 33 | 34 | var calls = 0; 35 | 36 | helper.on('error', function (event) { 37 | done.fail(event.error); 38 | }); 39 | 40 | helper.on('result', function (event) { 41 | calls++; 42 | 43 | var results = event.results; 44 | 45 | if (calls === 1) { 46 | expect(results.hits.length).toBe(2); 47 | expect(results.facets[0].data).toEqual({ 48 | f1: 2, 49 | f2: 1, 50 | f3: 1, 51 | }); 52 | 53 | helper.addRefine('facet', 'f2').search(); 54 | } 55 | 56 | if (calls === 2) { 57 | expect(results.hits.length).toBe(1); 58 | expect(results.facets[0].data).toEqual({ 59 | f1: 1, 60 | f2: 1, 61 | }); 62 | 63 | helper.toggleRefine('facet', 'f3').search(); 64 | } 65 | 66 | if (calls === 3) { 67 | expect(results.hits.length).toBe(0); 68 | expect(results.facets[0]).toBe(undefined); 69 | 70 | helper.removeRefine('facet', 'f2').search(); 71 | } 72 | 73 | if (calls === 4) { 74 | expect(results.hits.length).toBe(1); 75 | expect(results.facets[0].data).toEqual({ 76 | f1: 1, 77 | f3: 1, 78 | }); 79 | 80 | client.deleteIndex(indexName); 81 | 82 | if (!process.browser) { 83 | client.destroy(); 84 | } 85 | 86 | done(); 87 | } 88 | }); 89 | 90 | helper.addRefine('facet', 'f1').search(); 91 | }); 92 | -------------------------------------------------------------------------------- /test/integration-spec/helper.geo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../integration-utils'); 4 | var setup = utils.setupSimple; 5 | var createIndexName = utils.createIndexName; 6 | 7 | var algoliasearchHelper = require('../../'); 8 | 9 | var indexName = createIndexName('helper_geo'); 10 | 11 | var dataset = [ 12 | { objectID: '1', _geoloc: { lat: 1, lng: 1 } }, 13 | { objectID: '2', _geoloc: { lat: 1, lng: 2 } }, 14 | { objectID: '3', _geoloc: { lat: 2, lng: 1 } }, 15 | { objectID: '4', _geoloc: { lat: 2, lng: 2 } }, 16 | ]; 17 | 18 | var config = {}; 19 | 20 | var client; 21 | beforeAll(function () { 22 | return setup(indexName, dataset, config).then(function (c) { 23 | client = c; 24 | }); 25 | }); 26 | 27 | test('[INT][GEO-SEARCH] search inside a single polygon with a string', function (done) { 28 | var helper = algoliasearchHelper(client, indexName, {}); 29 | helper.on('result', function (event) { 30 | expect(event.results.hits.length).toBe(1); 31 | expect(event.results.hits[0].objectID).toBe('1'); 32 | done(); 33 | }); 34 | 35 | helper.setQueryParameter('insidePolygon', '0,0,1.1,0,1.1,1.1,0,1.1').search(); 36 | }); 37 | 38 | test('[INT][GEO-SEARCH] search inside a single polygon with an array', function (done) { 39 | var helper = algoliasearchHelper(client, indexName, {}); 40 | helper.on('result', function (event) { 41 | expect(event.results.hits.length).toBe(1); 42 | expect(event.results.hits[0].objectID).toBe('1'); 43 | done(); 44 | }); 45 | 46 | helper 47 | .setQueryParameter('insidePolygon', [[0, 0, 1.1, 0, 1.1, 1.1, 0, 1.1]]) 48 | .search(); 49 | }); 50 | 51 | test('[INT][GEO-SEARCH] search inside two polygons with an array', function (done) { 52 | var helper = algoliasearchHelper(client, indexName, {}); 53 | 54 | helper.on('result', function (event) { 55 | expect(event.results.hits.length).toBe(2); 56 | var sortedHits = event.results.hits.sort(function (a, b) { 57 | return a.objectID.localeCompare(b.objectID); 58 | }); 59 | expect(sortedHits[0].objectID).toBe('1'); 60 | expect(sortedHits[1].objectID).toBe('4'); 61 | done(); 62 | }); 63 | 64 | helper 65 | .setQueryParameter('insidePolygon', [ 66 | [0, 0, 1.1, 0, 1.1, 1.1, 0, 1.1], 67 | [1.5, 1.5, 2.1, 1.5, 2.1, 2.1, 1.5, 2.1], 68 | ]) 69 | .search(); 70 | }); 71 | -------------------------------------------------------------------------------- /test/integration-spec/helper.highlight.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../integration-utils'); 4 | var setup = utils.setupSimple; 5 | var createIndexName = utils.createIndexName; 6 | 7 | var algoliasearchHelper = require('../../'); 8 | 9 | var indexName = createIndexName('helper_highlight'); 10 | 11 | var dataset = [ 12 | { facet: ['f1', 'f2'] }, 13 | { facet: ['f1', 'f3'] }, 14 | { facet: ['f2', 'f3'] }, 15 | ]; 16 | 17 | var config = { 18 | attributesToIndex: ['facet'], 19 | attributesForFaceting: ['facet'], 20 | }; 21 | 22 | var client; 23 | beforeAll(function () { 24 | return setup(indexName, dataset, config).then(function (c) { 25 | client = c; 26 | }); 27 | }); 28 | 29 | test('[INT][HIGHLIGHT] The highlight should be consistent with the parameters', function (done) { 30 | var helper = algoliasearchHelper(client, indexName, { 31 | attributesToHighlight: ['facet'], 32 | facets: ['facet'], 33 | }); 34 | 35 | var calls = 0; 36 | helper.on('result', function (event) { 37 | calls++; 38 | if (calls === 1) { 39 | expect(event.results.hits[0]._highlightResult.facet[0].value).toBe( 40 | 'f1' 41 | ); 42 | expect(event.results.hits[1]._highlightResult.facet[0].value).toBe( 43 | 'f1' 44 | ); 45 | helper 46 | .setQueryParameter('highlightPostTag', '') 47 | .setQueryParameter('highlightPreTag', '') 48 | .search(); 49 | } else if (calls === 2) { 50 | expect(event.results.hits[0]._highlightResult.facet[0].value).toBe( 51 | 'f1' 52 | ); 53 | expect(event.results.hits[1]._highlightResult.facet[0].value).toBe( 54 | 'f1' 55 | ); 56 | client.deleteIndex(indexName); 57 | if (!process.browser) { 58 | client.destroy(); 59 | } 60 | done(); 61 | } 62 | }); 63 | 64 | helper.setQuery('f1').search(); 65 | }); 66 | -------------------------------------------------------------------------------- /test/integration-spec/helper.insights.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../integration-utils'); 4 | var setup = utils.setupSimple; 5 | var createIndexName = utils.createIndexName; 6 | 7 | var algoliasearchHelper = require('../../'); 8 | 9 | var indexName = createIndexName('helper_insights'); 10 | 11 | var dataset = [{ objectID: '1', name: 'TestName' }]; 12 | 13 | var config = {}; 14 | 15 | var client; 16 | beforeAll(function () { 17 | return setup(indexName, dataset, config).then(function (c) { 18 | client = c; 19 | }); 20 | }); 21 | 22 | test('[INT][INSIGHTS] search with clickAnalytics should have a queryID', function (done) { 23 | var helper = algoliasearchHelper(client, indexName, { clickAnalytics: true }); 24 | helper.on('result', function (event) { 25 | expect(event.results.queryID).toEqual(expect.any(String)); 26 | done(); 27 | }); 28 | 29 | helper.search(); 30 | }); 31 | 32 | test('[INT][INSIGHTS] search without clickAnalytics should not have a queryID', function (done) { 33 | var helper = algoliasearchHelper(client, indexName, {}); 34 | helper.on('result', function (event) { 35 | expect(event.results.queryID).toBeUndefined(); 36 | done(); 37 | }); 38 | 39 | helper.search(); 40 | }); 41 | -------------------------------------------------------------------------------- /test/integration-spec/helper.results.getFacetValues.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../integration-utils'); 4 | var setup = utils.setupSimple; 5 | var createIndexName = utils.createIndexName; 6 | 7 | var algoliasearchHelper = require('../../'); 8 | 9 | var indexName = createIndexName('helper_getfacetvalues'); 10 | 11 | var dataset = []; 12 | 13 | var config = {}; 14 | 15 | var client; 16 | beforeAll(function () { 17 | return setup(indexName, dataset, config).then(function (c) { 18 | client = c; 19 | }); 20 | }); 21 | 22 | test('[INT][GETFACETVALUES] When the results are empty, getFacetValues should not crash', function (done) { 23 | var helper = algoliasearchHelper(client, indexName, { 24 | facets: ['f'], 25 | disjunctiveFacets: ['df'], 26 | hierarchicalFacets: [ 27 | { 28 | name: 'products', 29 | attributes: ['categories.lvl0', 'categories.lvl1'], 30 | }, 31 | ], 32 | }); 33 | 34 | helper.on('result', function (event) { 35 | expect(event.results.getFacetValues('f')).toEqual([]); 36 | expect(event.results.getFacetValues('df')).toEqual([]); 37 | expect(event.results.getFacetValues('products')).toEqual({ 38 | name: 'products', 39 | count: null, 40 | isRefined: true, 41 | path: null, 42 | escapedValue: null, 43 | exhaustive: true, 44 | data: null, 45 | }); 46 | 47 | done(); 48 | }); 49 | 50 | helper.search(); 51 | }); 52 | -------------------------------------------------------------------------------- /test/integration-spec/helper.searchOnce.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../integration-utils'); 4 | var setup = utils.setupSimple; 5 | var createIndexName = utils.createIndexName; 6 | 7 | var algoliasearchHelper = require('../../'); 8 | 9 | var indexName = createIndexName('helper_searchonce'); 10 | 11 | var dataset = [ 12 | { objectID: '1', facet: ['f1', 'f2'] }, 13 | { objectID: '2', facet: ['f1', 'f3'] }, 14 | { objectID: '3', facet: ['f2', 'f3'] }, 15 | ]; 16 | 17 | var config = { 18 | attributesToIndex: ['facet'], 19 | attributesForFaceting: ['facet'], 20 | }; 21 | 22 | var client; 23 | beforeAll(function () { 24 | return setup(indexName, dataset, config).then(function (c) { 25 | client = c; 26 | }); 27 | }); 28 | 29 | test('[INT][SEARCHONCE] Should be able to search once with custom parameters without changing main search state', function (done) { 30 | var helper = algoliasearchHelper(client, indexName); 31 | var state0 = helper.state; 32 | 33 | var calls = 1; 34 | helper.on('error', function (event) { 35 | done.fail(event.error); 36 | }); 37 | 38 | helper.on('result', function (event) { 39 | if (calls === 3) { 40 | expect(event.results.hits.length).toBe(3); 41 | done(); 42 | } else { 43 | done.fail('Should not trigger the result event until the third call'); 44 | } 45 | }); 46 | 47 | var state1 = state0.setFacets(['facet']).addFacetRefinement('facet', 'f1'); 48 | helper.searchOnce(state1).then(function (res) { 49 | expect(helper.state).toBe(state0); 50 | expect(res.state).toEqual(state1); 51 | expect(res.content.hits.length).toBe(2); 52 | expect( 53 | res.content.hits.find(function (hit) { 54 | return hit.objectID === '2'; 55 | }) 56 | ).toBeTruthy(); 57 | expect( 58 | res.content.hits.find(function (hit) { 59 | return hit.objectID === '1'; 60 | }) 61 | ).toBeTruthy(); 62 | calls++; 63 | var state2 = state0.setFacets(['facet']).addFacetRefinement('facet', 'f2'); 64 | helper.searchOnce(state2, function (err, c, s) { 65 | expect(err).toBe(null); 66 | expect(helper.state).toBe(state0); 67 | expect(s).toEqual(state2); 68 | expect(c.hits.length).toBe(2); 69 | expect( 70 | c.hits.find(function (hit) { 71 | return hit.objectID === '1'; 72 | }) 73 | ).toBeTruthy(); 74 | expect( 75 | c.hits.find(function (hit) { 76 | return hit.objectID === '3'; 77 | }) 78 | ).toBeTruthy(); 79 | calls++; 80 | helper.search(); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/integration-spec/helper.tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../integration-utils'); 4 | var setup = utils.setupSimple; 5 | var createIndexName = utils.createIndexName; 6 | 7 | var algoliasearchHelper = require('../../'); 8 | 9 | var indexName = createIndexName('helper_refinements'); 10 | 11 | var dataset = [ 12 | { objectID: '0', _tags: ['t1', 't2'] }, 13 | { objectID: '1', _tags: ['t1', 't3'] }, 14 | { objectID: '2', _tags: ['t2', 't3'] }, 15 | { objectID: '3', _tags: ['t3', 't4'] }, 16 | ]; 17 | 18 | var config = {}; 19 | 20 | var client; 21 | beforeAll(function () { 22 | return setup(indexName, dataset, config).then(function (c) { 23 | client = c; 24 | }); 25 | }); 26 | 27 | function hitsToParsedID(h) { 28 | return parseInt(h.objectID, 10); 29 | } 30 | 31 | test('[INT][TAGS]Test tags operations on the helper and their results on the algolia API', function (done) { 32 | var helper = algoliasearchHelper(client, indexName, {}); 33 | 34 | var calls = 0; 35 | 36 | helper.on('error', function (event) { 37 | done.fail(event.error); 38 | }); 39 | 40 | helper.on('result', function (event) { 41 | calls++; 42 | 43 | var results = event.results; 44 | 45 | if (calls === 1) { 46 | expect(results.hits.length).toBe(4); 47 | expect(results.hits.map(hitsToParsedID).sort()).toEqual([0, 1, 2, 3]); 48 | helper.addTag('t1').search(); 49 | } 50 | 51 | if (calls === 2) { 52 | expect(results.hits.length).toBe(2); 53 | expect(results.hits.map(hitsToParsedID).sort()).toEqual([0, 1]); 54 | helper.addTag('t2').search(); 55 | } 56 | 57 | if (calls === 3) { 58 | expect(results.hits.length).toBe(1); 59 | expect(results.hits.map(hitsToParsedID).sort()).toEqual([0]); 60 | helper.removeTag('t2').toggleTag('t3').toggleTag('t1').search(); 61 | } 62 | 63 | if (calls === 4) { 64 | expect(results.hits.length).toBe(3); 65 | expect(results.hits.map(hitsToParsedID).sort()).toEqual([1, 2, 3]); 66 | helper.clearTags().setQueryParameter('tagFilters', 't3,(t1,t2)').search(); 67 | } 68 | 69 | if (calls === 5) { 70 | expect(results.hits.length).toBe(2); 71 | expect(results.hits.map(hitsToParsedID).sort()).toEqual([1, 2]); 72 | client.deleteIndex(indexName); 73 | if (!process.browser) { 74 | client.destroy(); 75 | } 76 | done(); 77 | } 78 | }); 79 | 80 | helper.search(); 81 | }); 82 | -------------------------------------------------------------------------------- /test/integration-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearch = require('algoliasearch'); 4 | 5 | function setup(indexName, fn) { 6 | var appID = process.env.INTEGRATION_TEST_APPID; 7 | var key = process.env.INTEGRATION_TEST_API_KEY; 8 | 9 | var client = algoliasearch(appID, key, { 10 | // all indexing requests must be done in https 11 | protocol: 'https:', 12 | }); 13 | client.deleteIndex = 14 | client.deleteIndex || 15 | function (deleteIndexName) { 16 | return client.initIndex(deleteIndexName).delete(); 17 | }; 18 | client.listIndexes = client.listIndexes || client.listIndices; 19 | 20 | var index = client.initIndex(indexName); 21 | index.addObjects = 22 | index.addObjects || 23 | function (objects) { 24 | return index 25 | .saveObjects(objects, { autoGenerateObjectIDIfNotExist: true }) 26 | .then(function (content) { 27 | return { 28 | taskID: content.taskIDs[0], 29 | }; 30 | }); 31 | }; 32 | index.clearIndex = index.clearIndex || index.clearObjects; 33 | 34 | return index 35 | .clearIndex() 36 | .then(function (content) { 37 | return index.waitTask(content.taskID); 38 | }) 39 | .then(function () { 40 | return fn(client, index); 41 | }); 42 | } 43 | 44 | function withDatasetAndConfig(indexName, dataset, config) { 45 | return setup(indexName, function (client, index) { 46 | return index 47 | .addObjects(dataset) 48 | .then(function () { 49 | return index.setSettings(config); 50 | }) 51 | .then(function (content) { 52 | return index.waitTask(content.taskID); 53 | }) 54 | .then(function () { 55 | return client; 56 | }); 57 | }); 58 | } 59 | 60 | function createIndexName(name) { 61 | return ( 62 | '_circle-algoliasearch-helper-js-' + 63 | (process.env.CIRCLE_BUILD_NUM || 'DEV') + 64 | name + 65 | Math.round(Math.random() * 5000) 66 | ); 67 | } 68 | 69 | module.exports = { 70 | setupSimple: withDatasetAndConfig, 71 | createIndexName: createIndexName, 72 | }; 73 | -------------------------------------------------------------------------------- /test/replayTools.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | toSaver: wrapHelperToSave, 4 | }; 5 | 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | 9 | /** 10 | * Monkey-patch a Helper constructor to spy 11 | * on data from Algolia and save the server response. 12 | * This is used to created data for tests. 13 | * @param {Helper} Helper a vanilla Helper constructor function 14 | * @param {string} folderName the folder where to save the data 15 | * @return {HelperSaver} a Helper enhnaced with save function 16 | */ 17 | function wrapHelperToSave(Helper, folderName) { 18 | var savedParameters = {}; 19 | 20 | createFolderIfNone(folderName); 21 | 22 | function WrappedHelper() { 23 | Helper.apply(this, Array.prototype.slice.call(arguments)); 24 | } 25 | 26 | WrappedHelper.prototype = Object.create(Helper.prototype); 27 | WrappedHelper.prototype.constructor = WrappedHelper; 28 | WrappedHelper.prototype._handleResponse = function ( 29 | state, 30 | queryId, 31 | err, 32 | content 33 | ) { 34 | savedParameters.state = state; 35 | savedParameters.error = err; 36 | savedParameters.content = content; 37 | 38 | Helper.prototype._handleResponse.apply( 39 | this, 40 | Array.prototype.slice.call(arguments) 41 | ); 42 | }; 43 | WrappedHelper.prototype.searchOnce = function (options) { 44 | var state = this.state.setQueryParameters(options); 45 | 46 | return Helper.prototype.searchOnce.call(this, state).then( 47 | function (contentAndState) { 48 | savedParameters.content = contentAndState._originalResponse; 49 | savedParameters.state = contentAndState.state; 50 | savedParameters.error = null; 51 | return contentAndState; 52 | }, 53 | function (error) { 54 | savedParameters.content = null; 55 | savedParameters.state = state; 56 | savedParameters.error = error; 57 | throw error; 58 | } 59 | ); 60 | }; 61 | WrappedHelper.prototype.__getParameters = function () { 62 | return savedParameters; 63 | }; 64 | WrappedHelper.prototype.__saveLastToFile = function (filename) { 65 | // folder if it doesn't exist 66 | // create filename.json 67 | // save json of savedParameters to file 68 | var filePath = path.join(folderName, filename); 69 | fs.writeFileSync(filePath, JSON.stringify(savedParameters, null, 2)); 70 | }; 71 | 72 | return WrappedHelper; 73 | } 74 | 75 | function createFolderIfNone(folderPath) { 76 | try { 77 | fs.statSync(folderPath); 78 | } catch (err) { 79 | try { 80 | fs.mkdirSync(folderPath); 81 | } catch (e2) { 82 | console.error("can't create folder"); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var jest = require('jest'); 5 | var algoliasearch = require('algoliasearch'); 6 | var staticJestConfig = require('../jest.config'); 7 | 8 | var enableIntegrationTest = 9 | process.env.ONLY_UNIT !== 'true' && 10 | process.env.INTEGRATION_TEST_API_KEY && 11 | process.env.INTEGRATION_TEST_APPID; 12 | 13 | var projectsRootPaths = [path.resolve(__dirname, '..')]; 14 | var dynamicJestConfig = Object.assign({}, staticJestConfig, { 15 | maxWorkers: 4, 16 | setupFilesAfterEnv: staticJestConfig.setupFilesAfterEnv || [], 17 | }); 18 | 19 | if (enableIntegrationTest) { 20 | dynamicJestConfig.testMatch.push( 21 | '/test/integration-spec/**/*.[jt]s?(x)' 22 | ); 23 | dynamicJestConfig.setupFilesAfterEnv.push( 24 | path.resolve(__dirname, '..', 'jest.setup.js') 25 | ); 26 | } 27 | 28 | jest.runCLI(dynamicJestConfig, projectsRootPaths).then(function (response) { 29 | if (!response.results.success) { 30 | process.exitCode = response.globalConfig.testFailureExitCode; 31 | } 32 | 33 | if (enableIntegrationTest) { 34 | var client = algoliasearch( 35 | process.env.INTEGRATION_TEST_APPID, 36 | process.env.INTEGRATION_TEST_API_KEY 37 | ); 38 | client.deleteIndex = 39 | client.deleteIndex || 40 | function (deleteIndexName) { 41 | return client.initIndex(deleteIndexName).delete(); 42 | }; 43 | client.listIndexes = client.listIndexes || client.listIndices; 44 | 45 | client.listIndexes().then((content) => { 46 | content.items 47 | .map((i) => i.name) 48 | .filter((n) => n.indexOf('_circle-algoliasearch-helper') !== -1) 49 | .forEach((n) => client.deleteIndex(n)); 50 | }); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/RefinementList/clear.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RefinementList = require('../../../../src/SearchParameters/RefinementList'); 4 | var clear = RefinementList.clearRefinement; 5 | 6 | test('When removing all refinements of a state without any', function () { 7 | var initialRefinementList = {}; 8 | expect(clear(initialRefinementList)).toBe(initialRefinementList); 9 | }); 10 | 11 | test('When removing refinements of a specific attribute, and there are no refinements for this attribute', function () { 12 | var initialRefinementList = { 13 | attribute: ['test'], 14 | }; 15 | 16 | expect(clear(initialRefinementList, 'notThisAttribute')).toEqual( 17 | initialRefinementList 18 | ); 19 | }); 20 | 21 | test('When removing refinements of a specific attribute, and another refinement is a substring of this attribute', function () { 22 | var initialRefinementList = { 23 | Brand: ['HP'], 24 | 'CPU type': ['Core i5'], 25 | 'Motherboard CPU type': ['Intel Core X'], 26 | }; 27 | var expectedRefinementList = { 28 | Brand: ['HP'], 29 | 'CPU type': ['Core i5'], 30 | }; 31 | 32 | expect(clear(initialRefinementList, 'Motherboard CPU type')).toEqual( 33 | expectedRefinementList 34 | ); 35 | }); 36 | 37 | test('When removing numericRefinements using a function, and there are no changes', function () { 38 | var initialRefinementList = { 39 | attribute: ['test'], 40 | }; 41 | 42 | function clearNothing() { 43 | return false; 44 | } 45 | function clearUndefinedAttribute(value, attribute) { 46 | return attribute === 'category'; 47 | } 48 | function clearUndefinedValue(value) { 49 | return value === 'toast'; 50 | } 51 | 52 | expect(clear(initialRefinementList, clearNothing, 'facet')).toBe( 53 | initialRefinementList 54 | ); 55 | expect(clear(initialRefinementList, clearUndefinedAttribute, 'facet')).toBe( 56 | initialRefinementList 57 | ); 58 | expect(clear(initialRefinementList, clearUndefinedValue, 'facet')).toBe( 59 | initialRefinementList 60 | ); 61 | }); 62 | 63 | test('calling clear on a empty refinement removes it', function () { 64 | var initialRefinementList = { 65 | attribute: [], 66 | }; 67 | 68 | expect(clear(initialRefinementList, 'attribute')).toEqual({}); 69 | }); 70 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/_clearNumericRefinements.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('When removing all numeric refinements of a state without any', function () { 6 | var state = SearchParameters.make({}); 7 | expect(state._clearNumericRefinements()).toBe(state.numericRefinements); 8 | }); 9 | 10 | test('When removing numericRefinements of a specific attribute, which has no refinements', function () { 11 | var state = SearchParameters.make({ 12 | numericRefinements: { 13 | size: {}, 14 | }, 15 | }); 16 | 17 | expect(state._clearNumericRefinements('size')).toEqual({}); 18 | }); 19 | 20 | test('When removing numericRefinements of a specific attribute, and there are no refinements for this attribute', function () { 21 | var state = SearchParameters.make({ 22 | numericRefinements: { 23 | price: { '>': [300] }, 24 | }, 25 | }); 26 | 27 | expect(state._clearNumericRefinements('size')).toEqual( 28 | state.numericRefinements 29 | ); 30 | }); 31 | 32 | test('When removing refinements of a specific attribute, and another refinement is a substring of this attribute', function () { 33 | var state = SearchParameters.make({ 34 | numericRefinements: { 35 | price: { '>': [300] }, 36 | 'price with taxes': { '>': [300] }, 37 | }, 38 | }); 39 | 40 | const expectedNumericRefinements = { 41 | price: { '>': [300] }, 42 | }; 43 | 44 | expect(state._clearNumericRefinements('price with taxes')).toEqual( 45 | expectedNumericRefinements 46 | ); 47 | }); 48 | 49 | test('When removing numericRefinements using a function, and there are no changes', function () { 50 | var state = SearchParameters.make({ 51 | numericRefinements: { 52 | price: { '>': [300, 30] }, 53 | size: { '=': [32, 30] }, 54 | }, 55 | }); 56 | 57 | function clearNothing() { 58 | return false; 59 | } 60 | function clearUndefinedAttribute(v, attribute) { 61 | return attribute === 'distance'; 62 | } 63 | function clearUndefinedOperator(v) { 64 | return v.op === '<'; 65 | } 66 | function clearUndefinedValue(v) { 67 | return v.val === 3; 68 | } 69 | 70 | expect(state._clearNumericRefinements(clearNothing)).toBe( 71 | state.numericRefinements 72 | ); 73 | expect(state._clearNumericRefinements(clearUndefinedAttribute)).toBe( 74 | state.numericRefinements 75 | ); 76 | expect(state._clearNumericRefinements(clearUndefinedOperator)).toBe( 77 | state.numericRefinements 78 | ); 79 | expect(state._clearNumericRefinements(clearUndefinedValue)).toBe( 80 | state.numericRefinements 81 | ); 82 | }); 83 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/getConjunctiveRefinements.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('getConjunctiveRefinements returns value in facets', function () { 6 | var state = new SearchParameters({ 7 | facets: ['test'], 8 | facetsRefinements: { 9 | test: ['zongo'], 10 | }, 11 | }); 12 | 13 | expect(state.getConjunctiveRefinements('test')).toEqual(['zongo']); 14 | }); 15 | 16 | test('getConjunctiveRefinements returns [] if facet is not conjunctive', function () { 17 | var state = new SearchParameters({ 18 | facetsRefinements: { 19 | test: ['zongo'], 20 | }, 21 | }); 22 | 23 | expect(state.getConjunctiveRefinements('test')).toEqual([]); 24 | }); 25 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/getDisjunctiveRefinements.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('getDisjunctiveRefinements returns value in facets', function () { 6 | var state = new SearchParameters({ 7 | disjunctiveFacets: ['test'], 8 | disjunctiveFacetsRefinements: { 9 | test: ['zongo'], 10 | }, 11 | }); 12 | 13 | expect(state.getDisjunctiveRefinements('test')).toEqual(['zongo']); 14 | }); 15 | 16 | test('getDisjunctiveRefinements returns [] if facet is not disjunctive', function () { 17 | var state = new SearchParameters({ 18 | disjunctiveFacetsRefinements: { 19 | test: ['zongo'], 20 | }, 21 | }); 22 | 23 | expect(state.getDisjunctiveRefinements('test')).toEqual([]); 24 | }); 25 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/getExcludeRefinements.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('getExcludeRefinements returns value in facets', function () { 6 | var state = new SearchParameters({ 7 | facets: ['test'], 8 | facetsExcludes: { 9 | test: ['zongo'], 10 | }, 11 | }); 12 | 13 | expect(state.getExcludeRefinements('test')).toEqual(['zongo']); 14 | }); 15 | 16 | test('getExcludeRefinements returns [] if facet is not conjunctive', function () { 17 | var state = new SearchParameters({ 18 | facetsExcludes: { 19 | test: ['zongo'], 20 | }, 21 | }); 22 | 23 | expect(state.getExcludeRefinements('test')).toEqual([]); 24 | }); 25 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/hierarchical-facets/add.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../../src/SearchParameters'); 4 | 5 | test('Should add a refinement', function () { 6 | var state0 = SearchParameters.make({ 7 | hierarchicalFacets: [ 8 | { 9 | name: 'categories', 10 | attributes: [ 11 | 'categories.lvl0', 12 | 'categories.lvl1', 13 | 'categories.lvl2', 14 | 'categories.lvl3', 15 | ], 16 | }, 17 | ], 18 | }); 19 | 20 | expect(state0.getHierarchicalRefinement('categories')).toEqual([]); 21 | var state1 = state0.addHierarchicalFacetRefinement('categories', 'men'); 22 | expect(state1.getHierarchicalRefinement('categories')).toEqual(['men']); 23 | }); 24 | 25 | test('Should throw if there is already a refinement', function () { 26 | var state0 = SearchParameters.make({ 27 | hierarchicalFacets: [ 28 | { 29 | name: 'categories', 30 | attributes: [ 31 | 'categories.lvl0', 32 | 'categories.lvl1', 33 | 'categories.lvl2', 34 | 'categories.lvl3', 35 | ], 36 | }, 37 | ], 38 | }); 39 | 40 | expect(state0.getHierarchicalRefinement('categories')).toEqual([]); 41 | var state1 = state0.toggleHierarchicalFacetRefinement('categories', 'beers'); 42 | expect( 43 | state1.addHierarchicalFacetRefinement.bind(state1, 'categories', 'men') 44 | ).toThrow(); 45 | }); 46 | 47 | test('Should throw if the facet is not defined', function () { 48 | var state0 = SearchParameters.make({}); 49 | 50 | expect( 51 | state0.addHierarchicalFacetRefinement.bind(state0, 'categories', 'men') 52 | ).toThrow(); 53 | }); 54 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/hierarchical-facets/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../../src/SearchParameters'); 4 | 5 | test('Should remove a refinement', function () { 6 | var state0 = SearchParameters.make({ 7 | hierarchicalFacets: [ 8 | { 9 | name: 'categories', 10 | attributes: [ 11 | 'categories.lvl0', 12 | 'categories.lvl1', 13 | 'categories.lvl2', 14 | 'categories.lvl3', 15 | ], 16 | }, 17 | ], 18 | }).addHierarchicalFacetRefinement('categories', 'men'); 19 | 20 | expect(state0.getHierarchicalRefinement('categories')).toEqual(['men']); 21 | var state1 = state0.removeHierarchicalFacetRefinement('categories'); 22 | expect(state1.getHierarchicalRefinement('categories')).toEqual([]); 23 | }); 24 | 25 | test('Should not throw if there is no refinement', function () { 26 | var state0 = SearchParameters.make({ 27 | hierarchicalFacets: [ 28 | { 29 | name: 'categories', 30 | attributes: [ 31 | 'categories.lvl0', 32 | 'categories.lvl1', 33 | 'categories.lvl2', 34 | 'categories.lvl3', 35 | ], 36 | }, 37 | ], 38 | }); 39 | 40 | expect(state0.removeHierarchicalFacetRefinement('categories')).toEqual( 41 | SearchParameters.make({ 42 | hierarchicalFacets: [ 43 | { 44 | name: 'categories', 45 | attributes: [ 46 | 'categories.lvl0', 47 | 'categories.lvl1', 48 | 'categories.lvl2', 49 | 'categories.lvl3', 50 | ], 51 | }, 52 | ], 53 | }) 54 | ); 55 | }); 56 | 57 | test('Should not throw if the facet is not defined', function () { 58 | var state0 = SearchParameters.make({}); 59 | 60 | expect(state0.removeHierarchicalFacetRefinement('categories')).toEqual( 61 | new SearchParameters() 62 | ); 63 | }); 64 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/isDisjunctiveFacetRefined.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('isDisjunctiveFacetRefined returns true if value in disjunctiveFacetsRefinements', function () { 6 | var state = new SearchParameters({ 7 | disjunctiveFacets: ['test'], 8 | disjunctiveFacetsRefinements: { 9 | test: ['zongo'], 10 | }, 11 | }); 12 | 13 | expect(state.isDisjunctiveFacetRefined('test', 'zongo')).toBe(true); 14 | }); 15 | 16 | test('isDisjunctiveFacetRefined returns true if something is refined when not passing a value', function () { 17 | var state = new SearchParameters({ 18 | disjunctiveFacets: ['test'], 19 | disjunctiveFacetsRefinements: { 20 | test: ['zongo'], 21 | }, 22 | }); 23 | 24 | expect(state.isDisjunctiveFacetRefined('test')).toBe(true); 25 | }); 26 | 27 | test('isDisjunctiveFacetRefined returns false if value not in disjunctiveFacetsRefinements', function () { 28 | var state = new SearchParameters({ 29 | disjunctiveFacets: ['test'], 30 | disjunctiveFacetsRefinements: { 31 | test: ['zongo'], 32 | }, 33 | }); 34 | 35 | expect(state.isDisjunctiveFacetRefined('test', 'not zongo')).toBe(false); 36 | }); 37 | 38 | test('isDisjunctiveFacetRefined returns false if facet is not disjunctive', function () { 39 | var state = new SearchParameters({ 40 | disjunctiveFacetsRefinements: { 41 | test: ['zongo'], 42 | }, 43 | }); 44 | 45 | expect(state.isDisjunctiveFacetRefined('test', 'zongo')).toBe(false); 46 | }); 47 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/isExcludeRefined.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('isExcludeRefined returns true if value in facetsExcludes', function () { 6 | var state = new SearchParameters({ 7 | facets: ['test'], 8 | facetsExcludes: { 9 | test: ['zongo'], 10 | }, 11 | }); 12 | 13 | expect(state.isExcludeRefined('test', 'zongo')).toBe(true); 14 | }); 15 | 16 | test('isExcludeRefined returns true if something is refined when not passing a value', function () { 17 | var state = new SearchParameters({ 18 | facets: ['test'], 19 | facetsExcludes: { 20 | test: ['zongo'], 21 | }, 22 | }); 23 | 24 | expect(state.isExcludeRefined('test')).toBe(true); 25 | }); 26 | 27 | test('isExcludeRefined returns false if value not in facetsExcludes', function () { 28 | var state = new SearchParameters({ 29 | facets: ['test'], 30 | facetsExcludes: { 31 | test: ['zongo'], 32 | }, 33 | }); 34 | 35 | expect(state.isExcludeRefined('test', 'not zongo')).toBe(false); 36 | }); 37 | 38 | test('isExcludeRefined returns false if facet is not conjunctive', function () { 39 | var state = new SearchParameters({ 40 | facetsExcludes: { 41 | test: ['zongo'], 42 | }, 43 | }); 44 | 45 | expect(state.isExcludeRefined('test', 'zongo')).toBe(false); 46 | }); 47 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/isFacetRefined.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('isFacetRefined returns true if value in facetsRefinements', function () { 6 | var state = new SearchParameters({ 7 | facets: ['test'], 8 | facetsRefinements: { 9 | test: ['zongo'], 10 | }, 11 | }); 12 | 13 | expect(state.isFacetRefined('test', 'zongo')).toBe(true); 14 | }); 15 | 16 | test('isFacetRefined returns true if something is refined when not passing a value', function () { 17 | var state = new SearchParameters({ 18 | facets: ['test'], 19 | facetsRefinements: { 20 | test: ['zongo'], 21 | }, 22 | }); 23 | 24 | expect(state.isFacetRefined('test')).toBe(true); 25 | }); 26 | 27 | test('isFacetRefined returns false if value not in facetsRefinements', function () { 28 | var state = new SearchParameters({ 29 | facets: ['test'], 30 | facetsRefinements: { 31 | test: ['zongo'], 32 | }, 33 | }); 34 | 35 | expect(state.isFacetRefined('test', 'not zongo')).toBe(false); 36 | }); 37 | 38 | test('isFacetRefined returns false if facet is not conjunctive', function () { 39 | var state = new SearchParameters({ 40 | facetsRefinements: { 41 | test: ['zongo'], 42 | }, 43 | }); 44 | 45 | expect(state.isFacetRefined('test', 'zongo')).toBe(false); 46 | }); 47 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/isHierarchicalFacetRefined.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('isHierarchicalFacetRefined returns true if value in hierarchicalFacetsRefinements', function () { 6 | var state = new SearchParameters({ 7 | hierarchicalFacets: [ 8 | { 9 | name: 'categories', 10 | attributes: ['categories.lvl0', 'categories.lvl1'], 11 | separator: ' | ', 12 | }, 13 | ], 14 | hierarchicalFacetsRefinements: { 15 | categories: ['beers | fancy ones'], 16 | }, 17 | }); 18 | 19 | expect( 20 | state.isHierarchicalFacetRefined('categories', 'beers | fancy ones') 21 | ).toBe(true); 22 | }); 23 | 24 | test('isHierarchicalFacetRefined returns true if something is refined when not passing a value', function () { 25 | var state = new SearchParameters({ 26 | hierarchicalFacets: [ 27 | { 28 | name: 'categories', 29 | attributes: ['categories.lvl0', 'categories.lvl1'], 30 | separator: ' | ', 31 | }, 32 | ], 33 | hierarchicalFacetsRefinements: { 34 | categories: ['beers | fancy ones'], 35 | }, 36 | }); 37 | 38 | expect(state.isHierarchicalFacetRefined('categories')).toBe(true); 39 | }); 40 | 41 | test('isHierarchicalFacetRefined returns false if value not in hierarchicalFacetsRefinements', function () { 42 | var state = new SearchParameters({ 43 | hierarchicalFacets: [ 44 | { 45 | name: 'categories', 46 | attributes: ['categories.lvl0', 'categories.lvl1'], 47 | separator: ' | ', 48 | }, 49 | ], 50 | hierarchicalFacetsRefinements: { 51 | categories: ['beers | fancy ones'], 52 | }, 53 | }); 54 | 55 | expect( 56 | state.isHierarchicalFacetRefined('categories', 'beers | cheap ones') 57 | ).toBe(false); 58 | }); 59 | 60 | test('isHierarchicalFacetRefined returns false if facet is not hierarchical', function () { 61 | var state = new SearchParameters({ 62 | hierarchicalFacetsRefinements: { 63 | categories: ['beers | fancy ones'], 64 | }, 65 | }); 66 | 67 | expect( 68 | state.isHierarchicalFacetRefined('categories', 'beers | fancy ones') 69 | ).toBe(false); 70 | }); 71 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/isNumericRefined.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('isNumericRefined with 3 parameters', function () { 6 | var params = new SearchParameters(); 7 | 8 | expect(params.isNumericRefined('age', '>', '3')).toBeFalsy(); 9 | expect(params.isNumericRefined('age', '>', '7')).toBeFalsy(); 10 | expect(params.isNumericRefined('age', '<', '7')).toBeFalsy(); 11 | expect(params.isNumericRefined('size', '>', '3')).toBeFalsy(); 12 | 13 | var paramsWithNumerics = params.addNumericRefinement('age', '>', '3'); 14 | 15 | expect(paramsWithNumerics.isNumericRefined('age', '>', '3')).toBeTruthy(); 16 | expect(paramsWithNumerics.isNumericRefined('age', '>', 3)).toBeTruthy(); 17 | expect(paramsWithNumerics.isNumericRefined('age', '>', '7')).toBeFalsy(); 18 | expect(paramsWithNumerics.isNumericRefined('age', '<', '7')).toBeFalsy(); 19 | expect(paramsWithNumerics.isNumericRefined('size', '>', '3')).toBeFalsy(); 20 | 21 | var paramsWithArray = params.addNumericRefinement('age', '=', [3, '4']); 22 | expect(paramsWithArray.isNumericRefined('age', '=', [3, 4])).toBeTruthy(); 23 | expect(paramsWithArray.isNumericRefined('age', '=', ['3', 4])).toBeTruthy(); 24 | expect(paramsWithArray.isNumericRefined('age', '=', [3, '4'])).toBeTruthy(); 25 | expect(paramsWithArray.isNumericRefined('age', '=', ['3', '4'])).toBeTruthy(); 26 | expect(paramsWithArray.isNumericRefined('age', '=', 3)).toBeFalsy(); 27 | expect(paramsWithArray.isNumericRefined('age', '=', '3')).toBeFalsy(); 28 | }); 29 | 30 | test('isNumericRefined with 2 parameters', function () { 31 | var params = new SearchParameters(); 32 | 33 | expect(params.isNumericRefined('age', '>')).toBeFalsy(); 34 | expect(params.isNumericRefined('size', '>')).toBeFalsy(); 35 | 36 | var paramsWithNumerics = params.addNumericRefinement('age', '>', '3'); 37 | 38 | expect(paramsWithNumerics.isNumericRefined('age', '>')).toBeTruthy(); 39 | expect(paramsWithNumerics.isNumericRefined('size', '>')).toBeFalsy(); 40 | }); 41 | 42 | test('isNumericRefined with 1 parameter', function () { 43 | var params = new SearchParameters(); 44 | 45 | expect(params.isNumericRefined('age')).toBeFalsy(); 46 | expect(params.isNumericRefined('size')).toBeFalsy(); 47 | 48 | var paramsWithNumerics = params.addNumericRefinement('age', '>', '3'); 49 | 50 | expect(paramsWithNumerics.isNumericRefined('age')).toBeTruthy(); 51 | expect(paramsWithNumerics.isNumericRefined('size')).toBeFalsy(); 52 | }); 53 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/listAttributes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('addFacet should add a facet to the facets list', function () { 6 | var state = SearchParameters.make({}).addFacet('facet'); 7 | 8 | expect(state.facets).toEqual(['facet']); 9 | }); 10 | 11 | test('removeFacet should remove a facet from the facets list', function () { 12 | var state = SearchParameters.make({}).addFacet('facet').removeFacet('facet'); 13 | 14 | expect(state.facets).toEqual([]); 15 | 16 | state = SearchParameters.make({}) 17 | .addFacet('facet') 18 | .addFacetRefinement('facet', 'value') 19 | .removeFacet('facet'); 20 | 21 | expect(state.facetsRefinements).toEqual({}); 22 | }); 23 | 24 | test('addDisjunctiveFacet should add a facet to the disjunctiveFacets list', function () { 25 | var state = SearchParameters.make({}).addDisjunctiveFacet('facet'); 26 | 27 | expect(state.disjunctiveFacets).toEqual(['facet']); 28 | }); 29 | 30 | test('removeDisjunctiveFacet should remove a facet from the disjunctiveFacets list', function () { 31 | var state = SearchParameters.make({}) 32 | .addDisjunctiveFacet('facet') 33 | .removeDisjunctiveFacet('facet'); 34 | 35 | expect(state.disjunctiveFacets).toEqual([]); 36 | 37 | state = SearchParameters.make({}) 38 | .addDisjunctiveFacet('facet') 39 | .addDisjunctiveFacetRefinement('facet', 'value') 40 | .removeDisjunctiveFacet('facet'); 41 | 42 | expect(state.disjunctiveFacetsRefinements).toEqual({}); 43 | }); 44 | 45 | test('addHierarchicalFacet should add a facet to the hierarchicalFacets list', function () { 46 | var state = SearchParameters.make({}).addHierarchicalFacet({ name: 'facet' }); 47 | 48 | expect(state.hierarchicalFacets).toEqual([{ name: 'facet' }]); 49 | }); 50 | 51 | test('addHierarchicalFacet should throw when a facet with the same name is already declared', function () { 52 | expect(function () { 53 | SearchParameters.make({ 54 | hierarchicalFacets: [{ name: 'facet' }], 55 | }).addHierarchicalFacet({ name: 'facet' }); 56 | }).toThrow(); 57 | }); 58 | 59 | test('removeHierarchicalFacet should remove a facet from the hierarchicalFacets list', function () { 60 | var state = SearchParameters.make({}) 61 | .addHierarchicalFacet({ name: 'facet' }) 62 | .removeHierarchicalFacet('facet'); 63 | 64 | expect(state.hierarchicalFacets).toEqual([]); 65 | 66 | state = SearchParameters.make({}) 67 | .addHierarchicalFacet({ name: 'facet' }) 68 | .toggleHierarchicalFacetRefinement('facet', 'value') 69 | .removeHierarchicalFacet('facet'); 70 | 71 | expect(state.hierarchicalFacetsRefinements).toEqual({}); 72 | }); 73 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/numericAttributes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | var stateWithStringForIntegers = { 6 | minWordSizefor1Typo: '1', 7 | minWordSizefor2Typos: '2', 8 | minProximity: '2', 9 | page: '10', 10 | hitsPerPage: '500', 11 | getRankingInfo: '1', 12 | distinct: '1', 13 | maxValuesPerFacet: '10', 14 | aroundRadius: '10', 15 | aroundPrecision: '2', 16 | minimumAroundRadius: '234', 17 | }; 18 | 19 | test('Constructor should parse the numeric attributes', function () { 20 | var state = new SearchParameters(stateWithStringForIntegers); 21 | 22 | Object.entries(stateWithStringForIntegers).forEach(function ([k, v]) { 23 | var parsedValue = parseFloat(v); 24 | expect(state[k]).toBe(parsedValue); 25 | }); 26 | }); 27 | 28 | test('setQueryParameter should parse the numeric attributes', function () { 29 | var state0 = new SearchParameters(); 30 | 31 | Object.entries(stateWithStringForIntegers).forEach(function ([k, v]) { 32 | var parsedValue = parseFloat(v); 33 | var state1 = state0.setQueryParameter(k, v); 34 | expect(state1[k]).toBe(parsedValue); 35 | }); 36 | }); 37 | 38 | test('setQueryParameters should parse the numeric attributes', function () { 39 | var state0 = new SearchParameters(); 40 | var state1 = state0.setQueryParameters(stateWithStringForIntegers); 41 | 42 | Object.entries(stateWithStringForIntegers).forEach(function ([k, v]) { 43 | var parsedValue = parseFloat(v); 44 | expect(state1[k]).toBe(parsedValue); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/numericFilters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test.each([ 6 | ['Should be able to add remove strings', '40'], 7 | ['Should be able to add remove numbers', 40], 8 | ['Should be able to add remove arrays', [40, '30']], 9 | ])(`%s`, function (_, value) { 10 | var attribute = 'attribute'; 11 | var operator = '='; 12 | 13 | var state0 = new SearchParameters(); 14 | var state1 = state0.addNumericRefinement(attribute, operator, value); 15 | var stateEmpty = new SearchParameters({ 16 | numericRefinements: { 17 | [attribute]: { 18 | [operator]: [], 19 | }, 20 | }, 21 | }); 22 | 23 | // Ensure that we add and then remove the same value, and get a state equivalent to the initial one 24 | expect(state1.isNumericRefined(attribute, operator, value)).toBeTruthy(); 25 | var state2 = state1.removeNumericRefinement(attribute, operator, value); 26 | expect(state2.isNumericRefined(attribute, operator, value)).toBeFalsy(); 27 | expect(state2).toEqual(stateEmpty); 28 | }); 29 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/resetPage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('without a previous page it should return the given SearchParameters', () => { 6 | const parameters = new SearchParameters(); 7 | const parametersWithPageReseted = parameters.resetPage(); 8 | 9 | expect(parameters).toBe(parametersWithPageReseted); 10 | expect(parametersWithPageReseted.page).toBeUndefined(); 11 | }); 12 | 13 | test('with a previous page of 0 it should return the given SearchParameters', () => { 14 | const parameters = new SearchParameters({ 15 | page: 0, 16 | }); 17 | 18 | const parametersWithPageReseted = parameters.resetPage(); 19 | 20 | expect(parameters).toBe(parametersWithPageReseted); 21 | expect(parametersWithPageReseted.page).toBe(0); 22 | }); 23 | 24 | test('with a previous page it should set the page to 0', () => { 25 | const parameters = new SearchParameters({ 26 | page: 5, 27 | }); 28 | 29 | const parametersWithPageReseted = parameters.resetPage(); 30 | 31 | expect(parameters).not.toBe(parametersWithPageReseted); 32 | expect(parametersWithPageReseted.page).toBe(0); 33 | }); 34 | -------------------------------------------------------------------------------- /test/spec/SearchParameters/setQueryParameter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | 5 | test('setqueryparameter should update existing parameter', function () { 6 | var sp = new SearchParameters({ 7 | facets: ['facet'], 8 | }); 9 | 10 | var newValue = []; 11 | var newsp = sp.setQueryParameter('facets', newValue); 12 | 13 | expect(newsp.facets).toEqual(newValue); 14 | }); 15 | 16 | test('setqueryparameter should add non-existing parameter', function () { 17 | var sp = new SearchParameters({ 18 | facets: ['facet'], 19 | }); 20 | 21 | var newValue = ['attributesToHighlight']; 22 | var newsp = sp.setQueryParameter('attributesToHighlight', newValue); 23 | 24 | expect(newsp.attributesToHighlight).toEqual(newValue); 25 | }); 26 | 27 | test('setQueryParameter should not create a new instance if the update is non effective', function () { 28 | var sp = new SearchParameters({ 29 | facets: ['facet'], 30 | maxValuesPerFacet: 10, 31 | }); 32 | 33 | var newValue = 10; 34 | var newsp = sp.setQueryParameter('maxValuesPerFacet', newValue); 35 | 36 | expect(newsp).toEqual(sp); 37 | }); 38 | 39 | test('setQueryParameter should not throw an error when trying to add an unknown parameter, and actually add it', function () { 40 | var state0 = new SearchParameters(); 41 | 42 | var state1 = state0.setQueryParameter('betaParameter', 'configValue'); 43 | // manual test that the warnonce message actually display message once 44 | state0.setQueryParameter('betaParameter', 'configValue'); 45 | expect(state1.betaParameter).toEqual('configValue'); 46 | }); 47 | 48 | test('setQueryParameter should warn about invalid userToken', function () { 49 | const message = 50 | '[algoliasearch-helper] The `userToken` parameter is invalid. This can lead to wrong analytics.\n - Format: [a-zA-Z0-9_-]{1,64}'; 51 | console.warn = jest.fn(); 52 | 53 | var state = new SearchParameters(); 54 | state.setQueryParameter('userToken', null); 55 | expect(console.warn).toHaveBeenCalledTimes(1); 56 | expect(console.warn).toHaveBeenLastCalledWith(message); 57 | 58 | state.setQueryParameter('userToken', ''); 59 | expect(console.warn).toHaveBeenCalledTimes(2); 60 | expect(console.warn).toHaveBeenLastCalledWith(message); 61 | 62 | state.setQueryParameter('userToken', 'my invalid token!'); 63 | expect(console.warn).toHaveBeenCalledTimes(3); 64 | expect(console.warn).toHaveBeenLastCalledWith(message); 65 | }); 66 | -------------------------------------------------------------------------------- /test/spec/SearchResults/getFacet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchResults = require('../../../src/SearchResults'); 4 | 5 | test('getFacetByName should return a given facet be it disjunctive or conjunctive', function () { 6 | var data = require('../../datasets/SearchParameters/search.dataset')(); 7 | 8 | var result = new SearchResults(data.searchParams, data.response.results); 9 | 10 | var cityFacet = result.getFacetByName('city'); 11 | 12 | expect(cityFacet.name).toBe('city'); 13 | expect(cityFacet.data).toEqual({ 14 | 'New York': 1, 15 | Paris: 3, 16 | 'San Francisco': 1, 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/spec/SearchResults/getFacetStats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchResults = require('../../../src/SearchResults'); 4 | var SearchParameters = require('../../../src/SearchParameters'); 5 | 6 | var response = { 7 | results: [ 8 | { 9 | page: 0, 10 | index: 'test_hotels-node', 11 | facets: { 12 | age: {}, 13 | price: {}, 14 | }, 15 | facets_stats: { 16 | age: { 17 | min: 21, 18 | max: 42, 19 | avg: 31.5, 20 | }, 21 | price: { 22 | min: 30, 23 | max: 60, 24 | avg: 33.5, 25 | }, 26 | }, 27 | params: 28 | 'query=&hitsPerPage=20&page=0&facets=%5B%5D&facetFilters=%5B%5B%2' + 29 | '2city%3AParis%22%2C%22city%3ANew%20York%22%5D%5D', 30 | exhaustiveFacetsCount: true, 31 | nbHits: 4, 32 | query: '', 33 | processingTimeMS: 2, 34 | nbPages: 1, 35 | hitsPerPage: 20, 36 | }, 37 | ], 38 | }; 39 | 40 | test('getFacetStats(facetName) returns stats for any facet or disjunctiveFacet', function () { 41 | var searchParams = new SearchParameters({ 42 | facets: ['age', 'country'], 43 | disjunctiveFacets: ['price'], 44 | }); 45 | var result = new SearchResults(searchParams, response.results); 46 | 47 | expect(result.getFacetStats('city')).toBeUndefined(); 48 | expect(result.getFacetStats('country')).toBeUndefined(); 49 | expect(result.getFacetStats('age')).toEqual( 50 | response.results[0].facets_stats.age 51 | ); 52 | expect(result.getFacetStats('price')).toEqual( 53 | response.results[0].facets_stats.price 54 | ); 55 | }); 56 | 57 | test('getFacetStats(facetName) returns stats if the facet is both a regular and disjunctive facet', function () { 58 | var searchParams = new SearchParameters({ 59 | facets: ['price'], 60 | disjunctiveFacets: ['price'], 61 | }); 62 | var result = new SearchResults(searchParams, response.results); 63 | 64 | expect(result.getFacetStats('price')).toEqual( 65 | response.results[0].facets_stats.price 66 | ); 67 | }); 68 | -------------------------------------------------------------------------------- /test/spec/SearchResults/getFacetValues/conjunctive.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": { 3 | "results": [ 4 | { 5 | "hits": [ 6 | { 7 | "objectID": "1696302" 8 | } 9 | ], 10 | "nbHits": 10000, 11 | "page": 0, 12 | "nbPages": 1000, 13 | "hitsPerPage": 1, 14 | "processingTimeMS": 1, 15 | "facets": { 16 | "brand": { 17 | "Insignia™": 551, 18 | "Samsung": 511, 19 | "Apple": 386 20 | } 21 | }, 22 | "exhaustiveFacetsCount": true, 23 | "query": "", 24 | "params": "query=&maxValuesPerFacet=3&page=0&hitsPerPage=1&attributesToRetrieve=%5B%5D&attributesToHighlight=%5B%5D&attributesToSnippet=%5B%5D&tagFilters=&facets=brand", 25 | "index": "instant_search" 26 | } 27 | ] 28 | }, 29 | "state": { 30 | "index": "instant_search", 31 | "query": "", 32 | "facets": ["brand"], 33 | "disjunctiveFacets": [], 34 | "hierarchicalFacets": [], 35 | "facetsRefinements": { 36 | "brand": ["Apple"] 37 | }, 38 | "facetsExcludes": {}, 39 | "disjunctiveFacetsRefinements": {}, 40 | "numericRefinements": {}, 41 | "tagRefinements": [], 42 | "hierarchicalFacetsRefinements": {}, 43 | "maxValuesPerFacet": 3, 44 | "page": 0 45 | }, 46 | "error": null 47 | } 48 | -------------------------------------------------------------------------------- /test/spec/SearchResults/getRefinements/dummy-tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": { 3 | "results": [ 4 | { 5 | "hits": [], 6 | "nbHits": 0, 7 | "page": 0, 8 | "nbPages": 0, 9 | "hitsPerPage": 20, 10 | "processingTimeMS": 1, 11 | "facets": {}, 12 | "exhaustiveFacetsCount": true, 13 | "query": "", 14 | "params": "query=&page=0&facets=%5B%22brand%22%2C%22type%22%2C%22rating%22%2C%22hierarchicalCategories.lvl0%22%5D&tagFilters=foo%2Cbar", 15 | "index": "instant_search" 16 | } 17 | ] 18 | }, 19 | "state": { 20 | "index": "instant_search", 21 | "query": "", 22 | "facets": ["brand"], 23 | "disjunctiveFacets": ["type", "rating"], 24 | "hierarchicalFacets": [ 25 | { 26 | "name": "hierarchicalCategories", 27 | "attributes": [ 28 | "hierarchicalCategories.lvl0", 29 | "hierarchicalCategories.lvl1", 30 | "hierarchicalCategories.lvl2", 31 | "hierarchicalCategories.lvl3" 32 | ] 33 | } 34 | ], 35 | "facetsRefinements": {}, 36 | "facetsExcludes": {}, 37 | "disjunctiveFacetsRefinements": {}, 38 | "numericRefinements": {}, 39 | "tagRefinements": ["foo", "bar"], 40 | "hierarchicalFacetsRefinements": {}, 41 | "page": 0 42 | }, 43 | "error": null 44 | } 45 | -------------------------------------------------------------------------------- /test/spec/SearchResults/getRefinements/exclude-artificial-results.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": { 3 | "results": [ 4 | { 5 | "query": "", 6 | "page": 0, 7 | "hitsPerPage": 20, 8 | "hits": [], 9 | "nbHits": 0, 10 | "nbPages": 0, 11 | "params": "", 12 | "exhaustiveNbHits": true, 13 | "exhaustiveFacetsCount": true, 14 | "processingTimeMS": 0, 15 | "index": "instant_search" 16 | } 17 | ] 18 | }, 19 | "state": { 20 | "index": "instant_search", 21 | "query": "", 22 | "facets": ["brand"], 23 | "disjunctiveFacets": [], 24 | "hierarchicalFacets": [], 25 | "facetsRefinements": {}, 26 | "facetsExcludes": { 27 | "brand": ["Apple"] 28 | }, 29 | "disjunctiveFacetsRefinements": {}, 30 | "numericRefinements": {}, 31 | "tagRefinements": [], 32 | "hierarchicalFacetsRefinements": {}, 33 | "page": 0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/spec/SearchResults/initialization.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SearchParameters = require('../../../src/SearchParameters'); 4 | var SearchResults = require('../../../src/SearchResults'); 5 | 6 | test('processingTime should be the sum of all individual times', function () { 7 | var result = new SearchResults(new SearchParameters(), [ 8 | { 9 | processingTimeMS: 1, 10 | }, 11 | { 12 | processingTimeMS: 1, 13 | }, 14 | ]); 15 | 16 | expect(result.processingTimeMS).toBe(2); 17 | }); 18 | 19 | test('processingTime should ignore undefined', function () { 20 | var result = new SearchResults(new SearchParameters(), [ 21 | { 22 | processingTimeMS: undefined, 23 | }, 24 | { 25 | processingTimeMS: 1, 26 | }, 27 | ]); 28 | 29 | expect(result.processingTimeMS).toBe(1); 30 | }); 31 | 32 | test('options should override search result keys', function () { 33 | var result = new SearchResults( 34 | new SearchParameters(), 35 | [ 36 | { 37 | __isArtificial: false, 38 | }, 39 | ], 40 | { 41 | __isArtificial: true, 42 | } 43 | ); 44 | 45 | expect(result.__isArtificial).toBe(true); 46 | }); 47 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/addFacetRefinement.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliaSearchHelper = require('../../../'); 4 | 5 | var fakeClient = {}; 6 | 7 | test('addFacetRefinement keeps the order of refinements', function () { 8 | var helper = algoliaSearchHelper(fakeClient, null, { 9 | facets: ['facet1', 'facet2'], 10 | }); 11 | 12 | helper.addFacetRefinement('facet1', 'facetValue'); 13 | helper.addFacetRefinement('facet2', 'facetValue'); 14 | 15 | expect(helper.state.facets).toEqual(['facet1', 'facet2']); 16 | expect(helper.state.facetsRefinements).toEqual({ 17 | facet1: ['facetValue'], 18 | facet2: ['facetValue'], 19 | }); 20 | expect(Object.keys(helper.state.facetsRefinements)).toEqual([ 21 | 'facet1', 22 | 'facet2', 23 | ]); 24 | }); 25 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/constructor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | 5 | test('not vulnerable to prototype pollution', () => { 6 | try { 7 | algoliasearchHelper({}, '', { constructor: { prototype: { test: 123 } } }); 8 | } catch (e) { 9 | // even if it throws an error, we need to be sure no vulnerability happens 10 | } 11 | 12 | expect({}.test).toBeUndefined(); 13 | }); 14 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/derive/detach.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../../'); 4 | 5 | test('[derived helper] detach a derived helper', function (done) { 6 | var client = { 7 | search: searchTest, 8 | }; 9 | var helper = algoliasearchHelper(client, 'indexName'); 10 | var derivedHelper = helper.derive(function (s) { 11 | return s; 12 | }); 13 | derivedHelper.on('result', function () {}); 14 | helper.search(); 15 | derivedHelper.detach(); 16 | helper.search(); 17 | 18 | var nbRequest; 19 | function searchTest(requests) { 20 | nbRequest = nbRequest || 0; 21 | if (nbRequest === 0) { 22 | expect(requests.length).toBe(2); 23 | expect(requests[0]).toEqual(requests[1]); 24 | expect(derivedHelper.listeners('result').length).toBe(1); 25 | nbRequest++; 26 | } else if (nbRequest === 1) { 27 | expect(requests.length).toBe(1); 28 | expect(derivedHelper.listeners('result').length).toBe(0); 29 | done(); 30 | } 31 | 32 | return new Promise(function () {}); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/derive/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../../'); 4 | 5 | function makeFakeClient() { 6 | return { 7 | search: jest.fn(function () { 8 | return new Promise(function () {}); 9 | }), 10 | searchForFacetValues: jest.fn(function () { 11 | return new Promise(function () {}); 12 | }), 13 | }; 14 | } 15 | 16 | test('[derived helper] emit a search event', function () { 17 | var searched = jest.fn(); 18 | var client = makeFakeClient(); 19 | var helper = algoliasearchHelper(client, 'index'); 20 | var derivedHelper = helper.derive(function (s) { 21 | return s; 22 | }); 23 | 24 | derivedHelper.on('search', searched); 25 | 26 | expect(searched).toHaveBeenCalledTimes(0); 27 | 28 | helper.search(); 29 | 30 | expect(searched).toHaveBeenCalledTimes(1); 31 | expect(searched).toHaveBeenLastCalledWith({ 32 | state: helper.state, 33 | results: null, 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/derive/multiqueries.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../../'); 4 | 5 | function makeFakeClient(assertions) { 6 | return { 7 | search: function () { 8 | assertions.apply(null, arguments); 9 | 10 | return new Promise(function () {}); 11 | }, 12 | }; 13 | } 14 | 15 | test('trigger a search without derivation', function () { 16 | var client = makeFakeClient(assertions); 17 | var helper = algoliasearchHelper(client, 'indexName'); 18 | 19 | helper.search(); 20 | 21 | function assertions(requests) { 22 | expect(requests.length).toBe(1); 23 | } 24 | }); 25 | 26 | test('trigger a search with one derivation without a state change', function () { 27 | var client = makeFakeClient(assertions); 28 | var helper = algoliasearchHelper(client, 'indexName'); 29 | 30 | helper.derive(function (state) { 31 | return state; 32 | }); 33 | 34 | helper.search(); 35 | 36 | function assertions(requests) { 37 | expect(requests.length).toBe(2); 38 | expect(requests[0]).toEqual(requests[1]); 39 | } 40 | }); 41 | 42 | test('trigger a search with one derivation with a state change', function () { 43 | var client = makeFakeClient(assertions); 44 | var helper = algoliasearchHelper(client, 'indexName'); 45 | 46 | helper.derive(function (state) { 47 | return state.setQuery('otherQuery'); 48 | }); 49 | 50 | helper.search(); 51 | 52 | function assertions(requests) { 53 | expect(requests.length).toBe(2); 54 | expect(requests[0].params.query).toBeUndefined(); 55 | expect(requests[1].params.query).toBe('otherQuery'); 56 | 57 | // eslint-disable-next-line no-param-reassign 58 | delete requests[0].params.query; 59 | // eslint-disable-next-line no-param-reassign 60 | delete requests[1].params.query; 61 | 62 | expect(requests[0]).toEqual(requests[1]); 63 | } 64 | }); 65 | 66 | test('trigger a search with derivation only', function () { 67 | var client = makeFakeClient(assertions); 68 | var helper = algoliasearchHelper(client, 'indexName'); 69 | 70 | helper.derive(function (state) { 71 | return state; 72 | }); 73 | 74 | helper.searchOnlyWithDerivedHelpers(); 75 | 76 | function assertions(requests) { 77 | expect(requests.length).toBe(1); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/excludes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | 5 | var fakeClient = {}; 6 | 7 | test('addExclude should add an exclusion', function () { 8 | var helper = algoliasearchHelper(fakeClient, null, { 9 | facets: ['facet'], 10 | }); 11 | 12 | helper._search = function () {}; 13 | 14 | var facetName = 'facet'; 15 | var facetValueToExclude = 'brand'; 16 | 17 | expect(helper.state.facetsExcludes[facetName]).toBeFalsy(); 18 | helper.addExclude(facetName, facetValueToExclude); 19 | expect(helper.state.facetsExcludes[facetName]).toBeTruthy(); 20 | expect( 21 | helper.state.facetsExcludes[facetName][0] === facetValueToExclude 22 | ).toBeTruthy(); 23 | }); 24 | 25 | test('removeExclude should remove an exclusion', function (done) { 26 | var helper = algoliasearchHelper(fakeClient, null, { 27 | facets: ['facet'], 28 | }); 29 | 30 | helper._search = function () {}; 31 | 32 | var facetName = 'facet'; 33 | var facetValueToExclude = 'brand'; 34 | 35 | helper.addExclude(facetName, facetValueToExclude); 36 | expect(helper.state.facetsExcludes[facetName].length === 1).toBeTruthy(); 37 | helper.removeExclude(facetName, facetValueToExclude); 38 | expect(helper.state.facetsExcludes[facetName]).toEqual([]); 39 | 40 | try { 41 | helper.removeExclude(facetName, facetValueToExclude); 42 | } catch (e) { 43 | done.fail('Removing unset exclusions should be ok...'); 44 | } 45 | 46 | done(); 47 | }); 48 | 49 | test('isExcluded should allow to omit the value', function () { 50 | var facetName = 'foo'; 51 | var facetValueToExclude = 'brand'; 52 | var facetValueNotExcluded = 'bar'; 53 | 54 | var helper = algoliasearchHelper(fakeClient, null, { 55 | facets: [facetName], 56 | }); 57 | 58 | expect(helper.isExcluded(facetName, facetValueToExclude)).toBeFalsy(); 59 | expect(helper.isExcluded(facetName, facetValueNotExcluded)).toBeFalsy(); 60 | expect(helper.isExcluded(facetName)).toBeFalsy(); 61 | helper.addExclude(facetName, facetValueToExclude); 62 | expect(helper.isExcluded(facetName, facetValueToExclude)).toBeTruthy(); 63 | expect(helper.isExcluded(facetName, facetValueNotExcluded)).toBeFalsy(); 64 | expect(helper.isExcluded(facetName)).toBeTruthy(); 65 | }); 66 | 67 | test('isExcluded should report exclusion correctly', function () { 68 | var helper = algoliasearchHelper(fakeClient, null, { 69 | facets: ['facet'], 70 | }); 71 | 72 | helper._search = function () {}; 73 | 74 | var facetName = 'facet'; 75 | var facetValueToExclude = 'brand'; 76 | 77 | expect(helper.isExcluded(facetName, facetValueToExclude)).toBeFalsy(); 78 | helper.addExclude(facetName, facetValueToExclude); 79 | expect(helper.isExcluded(facetName, facetValueToExclude)).toBeTruthy(); 80 | helper.removeExclude(facetName, facetValueToExclude); 81 | expect(helper.isExcluded(facetName, facetValueToExclude)).toBeFalsy(); 82 | }); 83 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/facetFilters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | var requestBuilder = require('../../../src/requestBuilder'); 5 | 6 | var fakeClient = {}; 7 | 8 | test('The filters should contain the different filters for a single conjunctive facet with multiple refinements', function () { 9 | var facetName = 'myFacet'; 10 | var helper = algoliasearchHelper(fakeClient, '', { 11 | facets: [facetName], 12 | }); 13 | 14 | helper.addRefine(facetName, 'value1'); 15 | expect(requestBuilder._getFacetFilters(helper.state)).toEqual([ 16 | facetName + ':value1', 17 | ]); 18 | helper.addRefine(facetName, 'value2'); 19 | expect(requestBuilder._getFacetFilters(helper.state)).toEqual([ 20 | facetName + ':value1', 21 | facetName + ':value2', 22 | ]); 23 | helper.toggleRefine(facetName, 'value3'); 24 | expect(requestBuilder._getFacetFilters(helper.state)).toEqual([ 25 | facetName + ':value1', 26 | facetName + ':value2', 27 | facetName + ':value3', 28 | ]); 29 | helper.removeRefine(facetName, 'value3'); 30 | expect(requestBuilder._getFacetFilters(helper.state)).toEqual([ 31 | facetName + ':value1', 32 | facetName + ':value2', 33 | ]); 34 | helper.addRefine(facetName, 'value1'); 35 | expect(requestBuilder._getFacetFilters(helper.state)).toEqual([ 36 | facetName + ':value1', 37 | facetName + ':value2', 38 | ]); 39 | }); 40 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/findAnswers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | 5 | function makeFakeFindAnswersResponse() { 6 | return { 7 | exhaustiveFacetsCount: true, 8 | facetHits: [], 9 | processingTimeMS: 3, 10 | }; 11 | } 12 | 13 | function setupTestEnvironment(helperOptions) { 14 | var findAnswers = jest.fn(function () { 15 | return Promise.resolve([makeFakeFindAnswersResponse()]); 16 | }); 17 | 18 | var fakeClient = { 19 | initIndex: function () { 20 | return { 21 | findAnswers: findAnswers, 22 | }; 23 | }, 24 | }; 25 | 26 | var helper = algoliasearchHelper(fakeClient, 'index', helperOptions); 27 | 28 | return { 29 | findAnswers: findAnswers, 30 | helper: helper, 31 | }; 32 | } 33 | 34 | test('returns an empty array with no derived helper', function () { 35 | var env = setupTestEnvironment(); 36 | var helper = env.helper; 37 | var findAnswers = env.findAnswers; 38 | 39 | return helper 40 | .findAnswers({ 41 | attributesForPrediction: ['description'], 42 | queryLanguages: ['en'], 43 | nbHits: 1, 44 | }) 45 | .then(function (result) { 46 | expect(findAnswers).toHaveBeenCalledTimes(0); 47 | expect(result).toEqual([]); 48 | }); 49 | }); 50 | 51 | test('returns a correct result with one derivation', function () { 52 | var env = setupTestEnvironment(); 53 | var helper = env.helper; 54 | var findAnswers = env.findAnswers; 55 | 56 | helper.derive(function (state) { 57 | return state; 58 | }); 59 | 60 | return helper 61 | .findAnswers({ 62 | attributesForPrediction: ['description'], 63 | queryLanguages: ['en'], 64 | nbHits: 1, 65 | }) 66 | .then(function (result) { 67 | expect(findAnswers).toHaveBeenCalledTimes(1); 68 | expect(result).toEqual([makeFakeFindAnswersResponse()]); 69 | }); 70 | }); 71 | 72 | test('runs findAnswers with facets', function () { 73 | var env = setupTestEnvironment({ facets: ['facet1'] }); 74 | var helper = env.helper; 75 | var findAnswers = env.findAnswers; 76 | helper.addFacetRefinement('facet1', 'facetValue'); 77 | 78 | helper.derive(function (state) { 79 | return state; 80 | }); 81 | 82 | helper.setQuery('hello'); 83 | 84 | return helper 85 | .findAnswers({ 86 | attributesForPrediction: ['description'], 87 | queryLanguages: ['en'], 88 | nbHits: 1, 89 | }) 90 | .then(function () { 91 | expect(findAnswers).toHaveBeenCalledTimes(1); 92 | expect(findAnswers).toHaveBeenCalledWith('hello', ['en'], { 93 | attributesForPrediction: ['description'], 94 | nbHits: 1, 95 | params: { 96 | facetFilters: ['facet1:facetValue'], 97 | facets: ['facet1'], 98 | query: 'hello', 99 | tagFilters: '', 100 | }, 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/getNumericRefinement.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliaSearchHelper = require('../../../'); 4 | 5 | var fakeClient = {}; 6 | 7 | test('getNumericRefinement with single value addNumericRefinement', function () { 8 | var helper = algoliaSearchHelper(fakeClient, null); 9 | 10 | helper.addNumericRefinement('attribute', '=', 0); 11 | helper.addNumericRefinement('attribute', '=', 34); 12 | 13 | expect(helper.getNumericRefinement('attribute', '=')).toEqual([0, 34]); 14 | }); 15 | 16 | test('getNumericRefinement with multiple values addNumericRefinement', function () { 17 | var helper = algoliaSearchHelper(fakeClient, null); 18 | 19 | helper.addNumericRefinement('attribute', '=', [0, 34]); 20 | 21 | expect(helper.getNumericRefinement('attribute', '=')).toEqual([[0, 34]]); 22 | }); 23 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/getQuery.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliaSearchHelper = require('../../../'); 4 | 5 | var fakeClient = {}; 6 | 7 | test('getQuery', function () { 8 | var helper = algoliaSearchHelper(fakeClient, 'IndexName', { 9 | disjunctiveFacets: ['df1', 'df2', 'df3'], 10 | disjunctiveFacetsRefinements: { 11 | df1: ['DF1-VAL-1'], 12 | df2: ['DF2-VAL-1', 'DF2-VAL-2'], 13 | }, 14 | facets: ['facet1', 'facet2', 'facet3'], 15 | facetsRefinements: { 16 | facet1: ['FACET1-VAL-1'], 17 | facet2: ['FACET2-VAL-1', 'FACET2-VAL2'], 18 | }, 19 | minWordSizefor1Typo: 8, 20 | ignorePlurals: true, 21 | }); 22 | 23 | expect(helper.getQuery()).toEqual({ 24 | minWordSizefor1Typo: 8, 25 | ignorePlurals: true, 26 | facets: ['facet1', 'facet2', 'facet3', 'df1', 'df2', 'df3'], 27 | tagFilters: '', 28 | facetFilters: [ 29 | 'facet1:FACET1-VAL-1', 30 | 'facet2:FACET2-VAL-1', 31 | 'facet2:FACET2-VAL2', 32 | ['df1:DF1-VAL-1'], 33 | ['df2:DF2-VAL-1', 'df2:DF2-VAL-2'], 34 | ], 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/hasRefinements.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | 5 | var fakeClient = {}; 6 | 7 | test('undefined attribute', function () { 8 | var helper = algoliasearchHelper(fakeClient, 'index'); 9 | expect(helper.hasRefinements('unknown')).toBe(false); 10 | }); 11 | 12 | describe('numericRefinement', function () { 13 | test('with refinement', function () { 14 | var helper = algoliasearchHelper(fakeClient, 'index'); 15 | 16 | helper.addNumericRefinement('price', '=', 1337); 17 | 18 | expect(helper.hasRefinements('price')).toBe(true); 19 | }); 20 | 21 | test('without refinement', function () { 22 | var helper = algoliasearchHelper(fakeClient, 'index'); 23 | 24 | helper.addNumericRefinement('price', '=', 1337); 25 | helper.clearRefinements('price'); 26 | 27 | expect(helper.hasRefinements('price')).toBe(false); 28 | }); 29 | }); 30 | 31 | describe('facet', function () { 32 | test('with refinement', function () { 33 | var helper = algoliasearchHelper(fakeClient, 'index', { 34 | facets: ['color'], 35 | }); 36 | 37 | helper.toggleFacetRefinement('color', 'red'); 38 | 39 | expect(helper.hasRefinements('color')).toBe(true); 40 | }); 41 | 42 | test('without refinement', function () { 43 | var helper = algoliasearchHelper(fakeClient, 'index', { 44 | facets: ['color'], 45 | }); 46 | 47 | expect(helper.hasRefinements('color')).toBe(false); 48 | }); 49 | }); 50 | 51 | describe('disjunctiveFacet', function () { 52 | test('with refinement', function () { 53 | var helper = algoliasearchHelper(fakeClient, 'index', { 54 | disjunctiveFacets: ['author'], 55 | }); 56 | 57 | helper.toggleFacetRefinement('author', 'John Spartan'); 58 | 59 | expect(helper.hasRefinements('author')).toBe(true); 60 | }); 61 | 62 | test('without refinement', function () { 63 | var helper = algoliasearchHelper(fakeClient, 'index', { 64 | disjunctiveFacets: ['author'], 65 | }); 66 | 67 | expect(helper.hasRefinements('author')).toBe(false); 68 | }); 69 | }); 70 | 71 | describe('hierarchicalFacet', function () { 72 | test('with refinement', function () { 73 | var helper = algoliasearchHelper(fakeClient, 'index', { 74 | hierarchicalFacets: [ 75 | { 76 | name: 'category', 77 | attributes: ['category.lvl0', 'category.lvl1'], 78 | }, 79 | ], 80 | }); 81 | 82 | helper.toggleFacetRefinement('category', 'Action Movies > Max'); 83 | 84 | expect(helper.hasRefinements('category')).toBe(true); 85 | }); 86 | 87 | test('without refinement', function () { 88 | var helper = algoliasearchHelper(fakeClient, 'index', { 89 | hierarchicalFacets: [ 90 | { 91 | name: 'category', 92 | attributes: ['category.lvl0', 'category.lvl1'], 93 | }, 94 | ], 95 | }); 96 | 97 | expect(helper.hasRefinements('category')).toBe(false); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/incorrectFacetDefinition.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | 5 | var fakeClient = {}; 6 | 7 | test('Conjunctive facet should be declared to be refined', function () { 8 | var h = algoliasearchHelper(fakeClient, '', {}); 9 | 10 | expect(h.addRefine.bind(h, 'undeclaredFacet', 'value')).toThrow(); 11 | expect(h.removeRefine.bind(h, 'undeclaredFacet', 'value')).toThrow(); 12 | }); 13 | 14 | test('Conjunctive facet should be declared to be excluded', function () { 15 | var h = algoliasearchHelper(fakeClient, '', {}); 16 | 17 | expect(h.addExclude.bind(h, 'undeclaredFacet', 'value')).toThrow(); 18 | expect(h.removeExclude.bind(h, 'undeclaredFacet', 'value')).toThrow(); 19 | }); 20 | 21 | test('Conjuctive facet should be declared to be refine', function () { 22 | var h = algoliasearchHelper(fakeClient, '', {}); 23 | 24 | expect(h.addDisjunctiveRefine.bind(h, 'undeclaredFacet', 'value')).toThrow(); 25 | expect( 26 | h.removeDisjunctiveRefine.bind(h, 'undeclaredFacet', 'value') 27 | ).toThrow(); 28 | }); 29 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/pages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | 5 | var fakeClient = {}; 6 | 7 | test('setChange should change the current page', function () { 8 | var helper = algoliasearchHelper(fakeClient, null, null); 9 | 10 | expect(helper.getPage()).toBeUndefined(); 11 | 12 | helper.setPage(3); 13 | 14 | expect(helper.getPage()).toBe(3); 15 | }); 16 | 17 | test('nextPage should increment the page by one', function () { 18 | var helper = algoliasearchHelper(fakeClient, null, null); 19 | 20 | expect(helper.getPage()).toBeUndefined(); 21 | 22 | helper.nextPage(); 23 | helper.nextPage(); 24 | helper.nextPage(); 25 | 26 | expect(helper.getPage()).toBe(3); 27 | }); 28 | 29 | test('previousPage should decrement the current page by one', function () { 30 | var helper = algoliasearchHelper(fakeClient, null, null); 31 | 32 | expect(helper.getPage()).toBeUndefined(); 33 | 34 | helper.setPage(3); 35 | 36 | expect(helper.getPage()).toBe(3); 37 | 38 | helper.previousPage(); 39 | 40 | expect(helper.getPage()).toBe(2); 41 | }); 42 | 43 | test('previousPage should throw an error without a current page', function () { 44 | var helper = algoliasearchHelper(fakeClient, null, null); 45 | 46 | expect(function () { 47 | helper.previousPage(); 48 | }).toThrow('Page requested below 0.'); 49 | }); 50 | 51 | test('pages should be reset if the mutation might change the number of pages', function () { 52 | var helper = algoliasearchHelper(fakeClient, '', { 53 | facets: ['facet1', 'f2'], 54 | disjunctiveFacets: ['f1'], 55 | }); 56 | 57 | [ 58 | ['clearRefinements'], 59 | ['setQuery', 'query'], 60 | ['addNumericRefinement', 'facet', '>', '2'], 61 | ['removeNumericRefinement', 'facet', '>'], 62 | 63 | ['addExclude', 'facet1', 'val2'], 64 | ['removeExclude', 'facet1', 'val2'], 65 | 66 | ['addRefine', 'f2', 'val'], 67 | ['removeRefine', 'f2', 'val'], 68 | 69 | ['addDisjunctiveRefine', 'f1', 'val'], 70 | ['removeDisjunctiveRefine', 'f1', 'val'], 71 | 72 | ['toggleRefine', 'f1', 'v1'], 73 | ['toggleExclude', 'facet1', '55'], 74 | ].forEach(function ([fn, ...args]) { 75 | helper.setPage(10); 76 | 77 | expect(helper.getPage()).toBe(10); 78 | 79 | helper[fn](...args); 80 | 81 | expect(helper.getPage()).toBe(0); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/queryID.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | 5 | var fakeClient = {}; 6 | 7 | test('the queryid should keep increasing when new requests arrives', function () { 8 | var initialQueryID; 9 | var client = { 10 | search: function () { 11 | initialQueryID++; 12 | return new Promise(function () {}); 13 | }, 14 | }; 15 | var helper = algoliasearchHelper(client, null, {}); 16 | 17 | initialQueryID = helper._queryId; 18 | 19 | helper.search().search().search().search().search(); 20 | 21 | expect(helper._queryId).toBe(initialQueryID); 22 | }); 23 | 24 | test('the response handler should check that the query is not outdated', function (done) { 25 | var testData = require('../../datasets/SearchParameters/search.dataset')(); 26 | var shouldTriggerResult = true; 27 | var callCount = 0; 28 | 29 | var helper = algoliasearchHelper(fakeClient, null, {}); 30 | 31 | helper.on('result', function () { 32 | callCount++; 33 | 34 | if (!shouldTriggerResult) { 35 | done.fail('The id was outdated'); 36 | } 37 | }); 38 | 39 | var states = [ 40 | { 41 | state: helper.state, 42 | queriesCount: 1, 43 | helper: helper, 44 | }, 45 | ]; 46 | 47 | helper._dispatchAlgoliaResponse( 48 | states, 49 | helper._lastQueryIdReceived + 1, 50 | testData.response 51 | ); 52 | helper._dispatchAlgoliaResponse( 53 | states, 54 | helper._lastQueryIdReceived + 10, 55 | testData.response 56 | ); 57 | expect(callCount).toBe(2); 58 | 59 | shouldTriggerResult = false; 60 | 61 | helper._dispatchAlgoliaResponse( 62 | states, 63 | helper._lastQueryIdReceived - 1, 64 | testData.response 65 | ); 66 | expect(callCount).toBe(2); 67 | 68 | done(); 69 | }); 70 | 71 | test('the error handler should check that the query is not outdated', function (done) { 72 | var shouldTriggerError = true; 73 | var callCount = 0; 74 | 75 | var helper = algoliasearchHelper(fakeClient, null, {}); 76 | 77 | helper.on('error', function () { 78 | callCount++; 79 | 80 | if (!shouldTriggerError) { 81 | done.fail('The id was outdated'); 82 | } 83 | }); 84 | 85 | helper._dispatchAlgoliaError(helper._lastQueryIdReceived + 1, new Error()); 86 | helper._dispatchAlgoliaError(helper._lastQueryIdReceived + 10, new Error()); 87 | expect(callCount).toBe(2); 88 | 89 | shouldTriggerError = false; 90 | 91 | helper._dispatchAlgoliaError(helper._lastQueryIdReceived - 1, new Error()); 92 | expect(callCount).toBe(2); 93 | 94 | done(); 95 | }); 96 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/setQueryParameter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | 5 | var fakeClient = {}; 6 | 7 | test('setChange should change the current state', function () { 8 | var helper = algoliasearchHelper(fakeClient, null, null); 9 | var changed = false; 10 | 11 | helper.on('change', function () { 12 | changed = true; 13 | }); 14 | 15 | expect(helper.getPage()).toBeUndefined(); 16 | expect(changed).toBe(false); 17 | helper.setQueryParameter('page', 22); 18 | expect(helper.getPage()).toBe(22); 19 | expect(changed).toBe(true); 20 | }); 21 | 22 | test('setChange should not change the current state: no real modification', function () { 23 | var helper = algoliasearchHelper(fakeClient, null, { page: 0 }); 24 | var changed = false; 25 | var initialState = helper.state; 26 | 27 | helper.on('change', function () { 28 | changed = true; 29 | }); 30 | 31 | expect(helper.getPage()).toBe(0); 32 | expect(changed).toBe(false); 33 | helper.setQueryParameter('page', 0); 34 | expect(helper.getPage()).toBe(0); 35 | expect(changed).toBe(false); 36 | expect(helper.state).toBe(initialState); 37 | }); 38 | 39 | test('setQueryParameter should warn about invalid userToken', function () { 40 | const message = 41 | '[algoliasearch-helper] The `userToken` parameter is invalid. This can lead to wrong analytics.\n - Format: [a-zA-Z0-9_-]{1,64}'; 42 | console.warn = jest.fn(); 43 | 44 | var helper = algoliasearchHelper(fakeClient, null, {}); 45 | helper.setQueryParameter('userToken', null); 46 | expect(console.warn).toHaveBeenCalledTimes(1); 47 | expect(console.warn).toHaveBeenLastCalledWith(message); 48 | 49 | helper.setQueryParameter('userToken', ''); 50 | expect(console.warn).toHaveBeenCalledTimes(2); 51 | expect(console.warn).toHaveBeenLastCalledWith(message); 52 | 53 | helper.setQueryParameter('userToken', 'my invalid token!'); 54 | expect(console.warn).toHaveBeenCalledTimes(3); 55 | expect(console.warn).toHaveBeenLastCalledWith(message); 56 | }); 57 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | 5 | var SearchParameters = algoliasearchHelper.SearchParameters; 6 | 7 | var fakeClient = {}; 8 | 9 | test('setState should set the state of the helper and trigger a change event', function (done) { 10 | var state0 = { query: 'a query' }; 11 | var state1 = { query: 'another query' }; 12 | 13 | var helper = algoliasearchHelper(fakeClient, null, state0); 14 | 15 | expect(helper.state).toEqual(new SearchParameters(state0)); 16 | 17 | helper.on('change', function (event) { 18 | expect(helper.state).toEqual(new SearchParameters(state1)); 19 | expect(event.state).toEqual(new SearchParameters(state1)); 20 | done(); 21 | }); 22 | 23 | helper.setState(state1); 24 | }); 25 | 26 | test('setState should set a default hierarchicalFacetRefinement when a rootPath is defined', function () { 27 | var searchParameters = { 28 | hierarchicalFacets: [ 29 | { 30 | name: 'hierarchicalCategories.lvl0', 31 | attributes: [ 32 | 'hierarchicalCategories.lvl0', 33 | 'hierarchicalCategories.lvl1', 34 | 'hierarchicalCategories.lvl2', 35 | ], 36 | separator: ' > ', 37 | rootPath: 'Cameras & Camcorders', 38 | showParentLevel: true, 39 | }, 40 | ], 41 | }; 42 | 43 | var helper = algoliasearchHelper(fakeClient, null, searchParameters); 44 | var initialHelperState = Object.assign({}, helper.state); 45 | 46 | expect(initialHelperState.hierarchicalFacetsRefinements).toEqual({ 47 | 'hierarchicalCategories.lvl0': ['Cameras & Camcorders'], 48 | }); 49 | 50 | // reset state 51 | helper.setState( 52 | helper.state.removeHierarchicalFacet('hierarchicalCategories.lvl0') 53 | ); 54 | expect(helper.state.hierarchicalFacetsRefinements).toEqual({}); 55 | 56 | // re-add `hierarchicalFacets` 57 | helper.setState(Object.assign({}, helper.state, searchParameters)); 58 | var finalHelperState = Object.assign({}, helper.state); 59 | 60 | expect(initialHelperState).toEqual(finalHelperState); 61 | expect(finalHelperState.hierarchicalFacetsRefinements).toEqual({ 62 | 'hierarchicalCategories.lvl0': ['Cameras & Camcorders'], 63 | }); 64 | }); 65 | 66 | test('setState should warn about invalid userToken', function () { 67 | const message = 68 | '[algoliasearch-helper] The `userToken` parameter is invalid. This can lead to wrong analytics.\n - Format: [a-zA-Z0-9_-]{1,64}'; 69 | console.warn = jest.fn(); 70 | 71 | var helper = algoliasearchHelper(fakeClient, null, {}); 72 | helper.setState({ userToken: null }); 73 | expect(console.warn).toHaveBeenCalledTimes(1); 74 | expect(console.warn).toHaveBeenLastCalledWith(message); 75 | 76 | helper.setState({ userToken: '' }); 77 | expect(console.warn).toHaveBeenCalledTimes(2); 78 | expect(console.warn).toHaveBeenLastCalledWith(message); 79 | 80 | helper.setState({ userToken: 'my invalid token!' }); 81 | expect(console.warn).toHaveBeenCalledTimes(3); 82 | expect(console.warn).toHaveBeenLastCalledWith(message); 83 | }); 84 | -------------------------------------------------------------------------------- /test/spec/algoliasearch.helper/tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var algoliasearchHelper = require('../../../index'); 4 | var requestBuilder = require('../../../src/requestBuilder'); 5 | 6 | var fakeClient = {}; 7 | 8 | test('Tag filters: operations on tags list', function () { 9 | var helper = algoliasearchHelper(fakeClient, null, null); 10 | 11 | helper.addTag('tag').addTag('tag2'); 12 | expect(helper.getTags()).toEqual(['tag', 'tag2']); 13 | helper.removeTag('tag'); 14 | expect(helper.getTags()).toEqual(['tag2']); 15 | helper.toggleTag('tag3').toggleTag('tag2').toggleTag('tag4'); 16 | expect(helper.getTags()).toEqual(['tag3', 'tag4']); 17 | }); 18 | 19 | test('Tags filters: advanced query', function () { 20 | var helper = algoliasearchHelper(fakeClient, null, null); 21 | 22 | var complexQuery = '(sea, city), romantic, -mountain'; 23 | 24 | helper.setQueryParameter('tagFilters', complexQuery); 25 | 26 | expect(requestBuilder._getTagFilters(helper.state)).toEqual(complexQuery); 27 | }); 28 | 29 | test('Tags filters: switching between advanced and simple API should be forbidden without clearing the refinements first', function (done) { 30 | var helper = algoliasearchHelper(fakeClient, null, null); 31 | 32 | helper.addTag('tag').addTag('tag2'); 33 | expect(requestBuilder._getTagFilters(helper.state)).toEqual('tag,tag2'); 34 | 35 | var complexQuery = '(sea, city), romantic, -mountain'; 36 | 37 | try { 38 | helper.setQueryParameter('tagFilters', complexQuery); 39 | done.fail("Can't switch directly from the advanced API to the managed API"); 40 | } catch (e0) { 41 | helper.clearTags().setQueryParameter('tagFilters', complexQuery); 42 | expect(requestBuilder._getTagFilters(helper.state)).toEqual(complexQuery); 43 | } 44 | 45 | try { 46 | helper.addTag('tag').addTag('tag2'); 47 | done.fail("Can't switch directly from the managed API to the advanced API"); 48 | } catch (e1) { 49 | helper 50 | .setQueryParameter('tagFilters', undefined) 51 | .addTag('tag') 52 | .addTag('tag2'); 53 | expect(requestBuilder._getTagFilters(helper.state)).toEqual('tag,tag2'); 54 | } 55 | 56 | done(); 57 | }); 58 | -------------------------------------------------------------------------------- /test/spec/functions/compact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var compact = require('../../../src/functions/compact'); 4 | 5 | test('compact removes falsy values from arrays', function () { 6 | expect(compact([2])).toEqual([2]); 7 | expect(compact([2, false, null, undefined, '', 0])).toEqual([2]); 8 | expect(compact([2, '45', 44])).toEqual([2, '45', 44]); 9 | }); 10 | 11 | test('returns an array for nullish values', function () { 12 | expect(compact(null)).toEqual([]); 13 | expect(compact(undefined)).toEqual([]); 14 | }); 15 | -------------------------------------------------------------------------------- /test/spec/functions/defaultsPure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var defaults = require('../../../src/functions/defaultsPure'); 4 | 5 | // tests modified from lodash source 6 | 7 | it('should assign source properties if missing on `object`', function () { 8 | var actual = defaults({ a: 1 }, { a: 2, b: 2 }); 9 | expect(actual).toEqual({ a: 1, b: 2 }); 10 | }); 11 | 12 | it('should accept multiple sources', function () { 13 | var expected = { a: 1, b: 2, c: 3 }; 14 | var actual = defaults({ a: 1, b: 2 }, { b: 3 }, { c: 3 }); 15 | 16 | expect(actual).toEqual(expected); 17 | 18 | actual = defaults({ a: 1, b: 2 }, { b: 3, c: 3 }, { c: 2 }); 19 | expect(actual).toEqual(expected); 20 | }); 21 | 22 | it('should not overwrite `null` values', function () { 23 | var actual = defaults({ a: null }, { a: 1 }); 24 | expect(actual.a).toBe(null); 25 | }); 26 | 27 | it('should overwrite `undefined` values', function () { 28 | var actual = defaults({ a: undefined }, { a: 1 }); 29 | expect(actual.a).toBe(1); 30 | }); 31 | 32 | it('should assign `undefined` values', function () { 33 | var source = { a: undefined, b: 1 }; 34 | var actual = defaults({}, source); 35 | 36 | expect(actual).toEqual({ a: undefined, b: 1 }); 37 | }); 38 | 39 | it('should assign properties that shadow those on `Object.prototype`', function () { 40 | var object = { 41 | constructor: Object.prototype.constructor, 42 | hasOwnProperty: Object.prototype.hasOwnProperty, 43 | isPrototypeOf: Object.prototype.isPrototypeOf, 44 | propertyIsEnumerable: Object.prototype.propertyIsEnumerable, 45 | toLocaleString: Object.prototype.toLocaleString, 46 | toString: Object.prototype.toString, 47 | valueOf: Object.prototype.valueOf, 48 | }; 49 | 50 | var source = { 51 | constructor: 1, 52 | hasOwnProperty: 2, 53 | isPrototypeOf: 3, 54 | propertyIsEnumerable: 4, 55 | toLocaleString: 5, 56 | toString: 6, 57 | valueOf: 7, 58 | }; 59 | 60 | var expected = Object.assign({}, source); 61 | expect(defaults({}, source)).toEqual(expected); 62 | 63 | expected = Object.assign({}, object); 64 | expect(defaults({}, object, source)).toEqual(expected); 65 | }); 66 | 67 | it('should keep the keys order with facets', function () { 68 | var actual = defaults( 69 | {}, 70 | { 71 | 'Insignia™': 551, 72 | Samsung: 511, 73 | Apple: 386, 74 | }, 75 | { 76 | Apple: 386, 77 | } 78 | ); 79 | expect(Object.keys(actual)).toEqual(['Insignia™', 'Samsung', 'Apple']); 80 | }); 81 | 82 | it('should keep the keys order when adding facet refinements', function () { 83 | var actual = defaults( 84 | {}, 85 | { 86 | facet2: ['facetValue'], 87 | }, 88 | { 89 | facet1: ['facetValue'], 90 | } 91 | ); 92 | expect(Object.keys(actual)).toEqual(['facet1', 'facet2']); 93 | }); 94 | 95 | it('does not pollute the prototype', () => { 96 | var payload = JSON.parse('{"__proto__": {"polluted": "vulnerable to PP"}}'); 97 | var subject = {}; 98 | 99 | expect(subject.polluted).toBe(undefined); 100 | 101 | const out = defaults({}, payload); 102 | 103 | expect(out).toEqual({}); 104 | 105 | expect({}.polluted).toBe(undefined); 106 | }); 107 | -------------------------------------------------------------------------------- /test/spec/functions/find.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var find = require('../../../src/functions/find'); 4 | 5 | test('find returns the first match based on the comparator', function () { 6 | expect( 7 | find([1], function () { 8 | return true; 9 | }) 10 | ).toBe(1); 11 | expect( 12 | find([1, 2], function () { 13 | return true; 14 | }) 15 | ).toBe(1); 16 | 17 | expect( 18 | find([{ nice: false }, { nice: true }], function (el) { 19 | return el.nice; 20 | }) 21 | ).toEqual({ nice: true }); 22 | }); 23 | 24 | test('find returns undefined in non-found cases', function () { 25 | expect( 26 | find([], function () { 27 | return false; 28 | }) 29 | ).toBeUndefined(); 30 | expect( 31 | find(undefined, function () { 32 | return false; 33 | }) 34 | ).toBeUndefined(); 35 | 36 | expect(function () { 37 | find([1, 2, 3], undefined); 38 | }).toThrow(); 39 | }); 40 | -------------------------------------------------------------------------------- /test/spec/functions/findIndex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var findIndex = require('../../../src/functions/findIndex'); 4 | 5 | test('findIndex returns the first match based on the comparator', function () { 6 | expect( 7 | findIndex([1], function () { 8 | return true; 9 | }) 10 | ).toBe(0); 11 | expect( 12 | findIndex([1, 2], function () { 13 | return true; 14 | }) 15 | ).toBe(0); 16 | 17 | expect( 18 | findIndex([{ nice: false }, { nice: true }], function (el) { 19 | return el.nice; 20 | }) 21 | ).toBe(1); 22 | }); 23 | 24 | test('findIndex returns -1 in non-found cases', function () { 25 | expect( 26 | findIndex([], function () { 27 | return false; 28 | }) 29 | ).toBe(-1); 30 | expect( 31 | findIndex(undefined, function () { 32 | return false; 33 | }) 34 | ).toBe(-1); 35 | 36 | expect(function () { 37 | findIndex([1, 2, 3], undefined); 38 | }).toThrow(); 39 | }); 40 | -------------------------------------------------------------------------------- /test/spec/functions/formatSort.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var formatSort = require('../../../src/functions/formatSort'); 4 | 5 | it('splits into attribute & direction', function () { 6 | expect(formatSort(['isRefined:desc', 'isNotRefined:desc'])).toEqual([ 7 | ['isRefined', 'isNotRefined'], 8 | ['desc', 'desc'], 9 | ]); 10 | }); 11 | 12 | it('leaves direction empty if no direction was given', function () { 13 | expect(formatSort(['isRefined:desc', 'isNotRefined'])).toEqual([ 14 | ['isRefined', 'isNotRefined'], 15 | ['desc', undefined], 16 | ]); 17 | }); 18 | 19 | it('takes from defaults if no direction was given', function () { 20 | expect( 21 | formatSort( 22 | ['isRefined:desc', 'isNotRefined'], 23 | ['books:asc', 'isRefined:desc', 'isNotRefined:asc'] 24 | ) 25 | ).toEqual([ 26 | ['isRefined', 'isNotRefined'], 27 | ['desc', 'asc'], 28 | ]); 29 | }); 30 | 31 | it('leaves direction empty if no direction was given & no default matches', function () { 32 | expect( 33 | formatSort( 34 | ['isRefined:desc', 'isNotRefined'], 35 | ['books:asc', 'isRefined:desc'] 36 | ) 37 | ).toEqual([ 38 | ['isRefined', 'isNotRefined'], 39 | ['desc', undefined], 40 | ]); 41 | }); 42 | -------------------------------------------------------------------------------- /test/spec/functions/intersection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var intersection = require('../../../src/functions/intersection'); 4 | 5 | test('it should find intersection between two array', function () { 6 | var actual = intersection([2, 1], [2]); 7 | expect(actual).toStrictEqual([2]); 8 | }); 9 | 10 | test('it should keep the order of the first array', function () { 11 | expect(intersection([0, 1, 2, 3, 4], [4, 0, 3])).toStrictEqual([0, 3, 4]); 12 | }); 13 | 14 | test('it should not produce duplicate primitive values', function () { 15 | expect(intersection([0, 0, 1, 2], [0, 2])).toStrictEqual([0, 2]); 16 | expect(intersection(['0', '0', '1', '2'], ['0', '2'])).toStrictEqual([ 17 | '0', 18 | '2', 19 | ]); 20 | }); 21 | -------------------------------------------------------------------------------- /test/spec/functions/orderBy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var orderBy = require('../../../src/functions/orderBy'); 4 | 5 | var objects = [ 6 | { a: 'x', b: 3 }, 7 | { a: 'y', b: 4 }, 8 | { a: 'x', b: 1 }, 9 | { a: 'y', b: 2 }, 10 | ]; 11 | 12 | it('should sort by a single property by a specified order', function () { 13 | expect(orderBy(objects, ['a'], ['desc'])).toEqual([ 14 | objects[1], 15 | objects[3], 16 | objects[0], 17 | objects[2], 18 | ]); 19 | }); 20 | 21 | it('should sort by multiple properties by specified orders', function () { 22 | expect(orderBy(objects, ['a', 'b'], ['desc', 'asc'])).toEqual([ 23 | objects[3], 24 | objects[1], 25 | objects[2], 26 | objects[0], 27 | ]); 28 | }); 29 | 30 | it('should sort by a property in ascending order when its order is not specified', function () { 31 | expect(orderBy(objects, ['a', 'b'])).toEqual([ 32 | objects[2], 33 | objects[0], 34 | objects[3], 35 | objects[1], 36 | ]); 37 | 38 | expect(orderBy(objects, ['a', 'b'], ['desc'])).toEqual([ 39 | objects[3], 40 | objects[1], 41 | objects[2], 42 | objects[0], 43 | ]); 44 | 45 | [null, undefined, false, 0, NaN, ''].forEach(function (order) { 46 | expect(orderBy(objects, ['a', 'b'], ['desc', order])).toEqual([ 47 | objects[3], 48 | objects[1], 49 | objects[2], 50 | objects[0], 51 | ]); 52 | }); 53 | }); 54 | 55 | it('should return an empty array when collections is no array', function () { 56 | expect(orderBy(undefined)).toEqual([]); 57 | expect(orderBy(false)).toEqual([]); 58 | expect(orderBy({})).toEqual([]); 59 | expect(orderBy({}, [], [])).toEqual([]); 60 | }); 61 | -------------------------------------------------------------------------------- /test/spec/functions/valToNumber.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var valToNumber = require('../../../src/functions/valToNumber'); 4 | 5 | test('valToNumber makes numbers, strings and arrays of strings and numbers to be numbers', function () { 6 | expect(valToNumber(2)).toBe(2); 7 | expect(valToNumber('2')).toBe(2); 8 | expect(valToNumber([2, '45', 44])).toEqual([2, 45, 44]); 9 | }); 10 | -------------------------------------------------------------------------------- /test/spec/hierarchical-facets/add-remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fakeClient = {}; 4 | 5 | test('hierarchical facets: add a facet -> set page to 0, trigger change', function () { 6 | expect.assertions(2); 7 | var algoliasearchHelper = require('../../../'); 8 | var helper = algoliasearchHelper(fakeClient, '', { 9 | hierarchicalFacets: [ 10 | { 11 | name: 'categories', 12 | attributes: [ 13 | 'categories.lvl0', 14 | 'categories.lvl1', 15 | 'categories.lvl2', 16 | 'categories.lvl3', 17 | ], 18 | }, 19 | ], 20 | }).setPage(2); 21 | 22 | helper.once('change', function () { 23 | expect(helper.getPage()).toBe(0); 24 | expect(helper.getHierarchicalFacetBreadcrumb('categories')).toEqual([ 25 | 'men', 26 | ]); 27 | }); 28 | 29 | helper.addHierarchicalFacetRefinement('categories', 'men'); 30 | }); 31 | 32 | test('hierarchical facets: remove a facet -> set page to 0, trigger change', function () { 33 | expect.assertions(2); 34 | var algoliasearchHelper = require('../../../'); 35 | var helper = algoliasearchHelper(fakeClient, '', { 36 | hierarchicalFacets: [ 37 | { 38 | name: 'categories', 39 | attributes: [ 40 | 'categories.lvl0', 41 | 'categories.lvl1', 42 | 'categories.lvl2', 43 | 'categories.lvl3', 44 | ], 45 | }, 46 | ], 47 | }) 48 | .setPage(2) 49 | .addHierarchicalFacetRefinement('categories', 'men'); 50 | 51 | helper.once('change', function () { 52 | expect(helper.getPage()).toBe(0); 53 | expect(helper.getHierarchicalFacetBreadcrumb('categories')).toEqual([]); 54 | }); 55 | 56 | helper.removeHierarchicalFacetRefinement('categories'); 57 | }); 58 | -------------------------------------------------------------------------------- /test/spec/hierarchical-facets/breadcrumb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | test('hierarchical facets: using getHierarchicalFacetBreadcrumb()', function () { 4 | var algoliasearch = require('algoliasearch'); 5 | 6 | var algoliasearchHelper = require('../../../'); 7 | 8 | var appId = 'hierarchical-simple-appId'; 9 | var apiKey = 'hierarchical-simple-apiKey'; 10 | var indexName = 'hierarchical-simple-indexName'; 11 | 12 | var client = algoliasearch(appId, apiKey); 13 | var helper = algoliasearchHelper(client, indexName, { 14 | hierarchicalFacets: [ 15 | { 16 | name: 'categories', 17 | attributes: [ 18 | 'categories.lvl0', 19 | 'categories.lvl1', 20 | 'categories.lvl2', 21 | 'categories.lvl3', 22 | ], 23 | }, 24 | ], 25 | }); 26 | 27 | helper.toggleRefine('categories', 'beers > IPA > Flying dog'); 28 | 29 | expect(helper.getHierarchicalFacetBreadcrumb('categories')).toEqual([ 30 | 'beers', 31 | 'IPA', 32 | 'Flying dog', 33 | ]); 34 | }); 35 | 36 | test('hierarchical facets: using getHierarchicalFacetBreadcrumb before the first refinement', function () { 37 | var algoliasearch = require('algoliasearch'); 38 | 39 | var algoliasearchHelper = require('../../../'); 40 | 41 | var appId = 'hierarchical-simple-appId'; 42 | var apiKey = 'hierarchical-simple-apiKey'; 43 | var indexName = 'hierarchical-simple-indexName'; 44 | 45 | var client = algoliasearch(appId, apiKey); 46 | var helper = algoliasearchHelper(client, indexName, { 47 | hierarchicalFacets: [ 48 | { 49 | name: 'categories', 50 | attributes: [ 51 | 'categories.lvl0', 52 | 'categories.lvl1', 53 | 'categories.lvl2', 54 | 'categories.lvl3', 55 | ], 56 | }, 57 | ], 58 | }); 59 | 60 | expect(helper.getHierarchicalFacetBreadcrumb('categories')).toEqual([]); 61 | }); 62 | 63 | test('hierarchical facets: using getHierarchicalFacetBreadcrumb on an undefined facet', function () { 64 | var algoliasearch = require('algoliasearch'); 65 | 66 | var algoliasearchHelper = require('../../../'); 67 | 68 | var appId = 'hierarchical-simple-appId'; 69 | var apiKey = 'hierarchical-simple-apiKey'; 70 | var indexName = 'hierarchical-simple-indexName'; 71 | 72 | var client = algoliasearch(appId, apiKey); 73 | var helper = algoliasearchHelper(client, indexName, { 74 | hierarchicalFacetsRefinements: { categories: ['beers > IPA > Flying dog'] }, 75 | }); 76 | 77 | expect(helper.getHierarchicalFacetBreadcrumb('categories')).toEqual([]); 78 | }); 79 | -------------------------------------------------------------------------------- /test/spec/hierarchical-facets/do-not-show-parent-level.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | test('hierarchical facets: do not show parent level', function (done) { 4 | var algoliasearch = require('algoliasearch'); 5 | 6 | var algoliasearchHelper = require('../../../'); 7 | 8 | var appId = 'hierarchical-simple-appId'; 9 | var apiKey = 'hierarchical-simple-apiKey'; 10 | var indexName = 'hierarchical-simple-indexName'; 11 | 12 | var client = algoliasearch(appId, apiKey); 13 | var helper = algoliasearchHelper(client, indexName, { 14 | hierarchicalFacets: [ 15 | { 16 | name: 'categories', 17 | attributes: ['categories.lvl0', 'categories.lvl1'], 18 | separator: ' | ', 19 | showParentLevel: false, 20 | }, 21 | ], 22 | }); 23 | 24 | helper.toggleRefine('categories', 'beers | IPA'); 25 | 26 | var algoliaResponse = { 27 | results: [ 28 | { 29 | query: 'a', 30 | index: indexName, 31 | hits: [{ objectID: 'one' }, { objectID: 'two' }], 32 | nbHits: 2, 33 | page: 0, 34 | nbPages: 1, 35 | hitsPerPage: 20, 36 | exhaustiveFacetsCount: true, 37 | facets: { 38 | 'categories.lvl0': { beers: 2 }, 39 | 'categories.lvl1': { 'beers | IPA': 2 }, 40 | }, 41 | }, 42 | { 43 | query: 'a', 44 | index: indexName, 45 | hits: [{ objectID: 'one' }], 46 | nbHits: 1, 47 | page: 0, 48 | nbPages: 1, 49 | hitsPerPage: 1, 50 | facets: { 51 | 'categories.lvl0': { beers: 3 }, 52 | 'categories.lvl1': { 'beers | IPA': 2, 'beers | Belgian': 1 }, 53 | }, 54 | }, 55 | { 56 | query: 'a', 57 | index: indexName, 58 | hits: [{ objectID: 'one' }], 59 | nbHits: 1, 60 | page: 0, 61 | nbPages: 1, 62 | hitsPerPage: 1, 63 | facets: { 64 | 'categories.lvl0': { beers: 3 }, 65 | }, 66 | }, 67 | ], 68 | }; 69 | 70 | var expectedHelperResponse = [ 71 | { 72 | name: 'categories', 73 | count: null, 74 | isRefined: true, 75 | path: null, 76 | escapedValue: null, 77 | exhaustive: true, 78 | data: [ 79 | { 80 | name: 'beers', 81 | path: 'beers', 82 | escapedValue: 'beers', 83 | count: 3, 84 | isRefined: true, 85 | exhaustive: true, 86 | data: [ 87 | { 88 | name: 'IPA', 89 | path: 'beers | IPA', 90 | escapedValue: 'beers | IPA', 91 | count: 2, 92 | isRefined: true, 93 | exhaustive: true, 94 | data: null, 95 | }, 96 | ], 97 | }, 98 | ], 99 | }, 100 | ]; 101 | 102 | client.search = jest.fn(function () { 103 | return Promise.resolve(algoliaResponse); 104 | }); 105 | 106 | helper.setQuery('a').search(); 107 | helper.once('result', function (event) { 108 | expect(client.search).toHaveBeenCalledTimes(1); 109 | expect(event.results.hierarchicalFacets).toEqual(expectedHelperResponse); 110 | done(); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/spec/hierarchical-facets/facet-value-length.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | test('hierarchical facets: facet value called length', function (done) { 4 | var algoliasearch = require('algoliasearch'); 5 | var algoliasearchHelper = require('../../../'); 6 | 7 | var appId = 'hierarchical-simple-appId'; 8 | var apiKey = 'hierarchical-simple-apiKey'; 9 | var indexName = 'hierarchical-simple-indexName'; 10 | 11 | var client = algoliasearch(appId, apiKey); 12 | var helper = algoliasearchHelper(client, indexName, { 13 | hierarchicalFacets: [ 14 | { 15 | name: 'categories', 16 | attributes: ['categories.lvl0'], 17 | }, 18 | ], 19 | }); 20 | 21 | var algoliaResponse = { 22 | results: [ 23 | { 24 | query: 'a', 25 | index: indexName, 26 | hits: [{ objectID: 'one' }], 27 | nbHits: 3, 28 | page: 0, 29 | nbPages: 1, 30 | hitsPerPage: 20, 31 | exhaustiveFacetsCount: true, 32 | facets: { 33 | // value length can cause lodash to turn an object into an array 34 | 'categories.lvl0': { beers: 8, length: 3 }, 35 | }, 36 | }, 37 | ], 38 | }; 39 | 40 | var expectedHelperResponse = [ 41 | { 42 | name: 'categories', 43 | count: null, 44 | isRefined: true, 45 | path: null, 46 | escapedValue: null, 47 | exhaustive: true, 48 | data: [ 49 | { 50 | name: 'beers', 51 | path: 'beers', 52 | escapedValue: 'beers', 53 | count: 8, 54 | isRefined: false, 55 | exhaustive: true, 56 | data: null, 57 | }, 58 | { 59 | name: 'length', 60 | path: 'length', 61 | escapedValue: 'length', 62 | count: 3, 63 | isRefined: false, 64 | exhaustive: true, 65 | data: null, 66 | }, 67 | ], 68 | }, 69 | ]; 70 | 71 | client.search = jest.fn(function () { 72 | return Promise.resolve(algoliaResponse); 73 | }); 74 | 75 | helper.search(); 76 | helper.once('result', function (event) { 77 | expect(event.results.hierarchicalFacets).toEqual(expectedHelperResponse); 78 | done(); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/spec/hierarchical-facets/no-refinement.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | test('hierarchical facets: no refinement', function (done) { 4 | var algoliasearch = require('algoliasearch'); 5 | 6 | var algoliasearchHelper = require('../../../'); 7 | 8 | var appId = 'hierarchical-simple-appId'; 9 | var apiKey = 'hierarchical-simple-apiKey'; 10 | var indexName = 'hierarchical-simple-indexName'; 11 | 12 | var client = algoliasearch(appId, apiKey); 13 | var helper = algoliasearchHelper(client, indexName, { 14 | hierarchicalFacets: [ 15 | { 16 | name: 'categories', 17 | attributes: ['categories.lvl0'], 18 | }, 19 | ], 20 | }); 21 | 22 | var algoliaResponse = { 23 | results: [ 24 | { 25 | query: 'a', 26 | index: indexName, 27 | hits: [{ objectID: 'one' }, { objectID: 'two' }], 28 | nbHits: 5, 29 | page: 0, 30 | nbPages: 1, 31 | hitsPerPage: 20, 32 | exhaustiveFacetsCount: true, 33 | facets: { 34 | 'categories.lvl0': { beers: 2, fruits: 3 }, 35 | }, 36 | }, 37 | ], 38 | }; 39 | 40 | var expectedHelperResponse = [ 41 | { 42 | name: 'categories', 43 | count: null, 44 | isRefined: true, 45 | path: null, 46 | escapedValue: null, 47 | exhaustive: true, 48 | data: [ 49 | { 50 | name: 'beers', 51 | path: 'beers', 52 | escapedValue: 'beers', 53 | count: 2, 54 | isRefined: false, 55 | exhaustive: true, 56 | data: null, 57 | }, 58 | { 59 | name: 'fruits', 60 | path: 'fruits', 61 | escapedValue: 'fruits', 62 | count: 3, 63 | isRefined: false, 64 | exhaustive: true, 65 | data: null, 66 | }, 67 | ], 68 | }, 69 | ]; 70 | 71 | client.search = jest.fn(function () { 72 | return Promise.resolve(algoliaResponse); 73 | }); 74 | 75 | helper.setQuery('a').search(); 76 | helper.once('result', function (event) { 77 | var queries = client.search.mock.calls[0][0]; 78 | var hitsQuery = queries[0]; 79 | 80 | expect(queries.length).toBe(1); 81 | expect(client.search).toHaveBeenCalledTimes(1); 82 | expect(hitsQuery.params.facets).toEqual(['categories.lvl0']); 83 | expect(hitsQuery.params.facetFilters).toBe(undefined); 84 | expect(event.results.hierarchicalFacets).toEqual(expectedHelperResponse); 85 | done(); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/spec/hierarchical-facets/one-level.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | test('hierarchical facets: only one level deep', function (done) { 4 | var algoliasearch = require('algoliasearch'); 5 | 6 | var algoliasearchHelper = require('../../../'); 7 | 8 | var appId = 'hierarchical-simple-appId'; 9 | var apiKey = 'hierarchical-simple-apiKey'; 10 | var indexName = 'hierarchical-simple-indexName'; 11 | 12 | var client = algoliasearch(appId, apiKey); 13 | var helper = algoliasearchHelper(client, indexName, { 14 | hierarchicalFacets: [ 15 | { 16 | name: 'categories', 17 | attributes: ['categories.lvl0'], 18 | }, 19 | ], 20 | }); 21 | 22 | helper.toggleRefine('categories', 'beers'); 23 | 24 | var algoliaResponse = { 25 | results: [ 26 | { 27 | query: 'a', 28 | index: indexName, 29 | hits: [{ objectID: 'one' }, { objectID: 'two' }], 30 | nbHits: 2, 31 | page: 0, 32 | nbPages: 1, 33 | hitsPerPage: 20, 34 | exhaustiveFacetsCount: true, 35 | facets: { 36 | 'categories.lvl0': { beers: 2 }, 37 | }, 38 | }, 39 | { 40 | query: 'a', 41 | index: indexName, 42 | hits: [{ objectID: 'one' }], 43 | nbHits: 1, 44 | page: 0, 45 | nbPages: 1, 46 | hitsPerPage: 1, 47 | facets: { 48 | 'categories.lvl0': { beers: 2, fruits: 3 }, 49 | }, 50 | }, 51 | ], 52 | }; 53 | 54 | var expectedHelperResponse = [ 55 | { 56 | name: 'categories', 57 | count: null, 58 | isRefined: true, 59 | path: null, 60 | escapedValue: null, 61 | exhaustive: true, 62 | data: [ 63 | { 64 | name: 'beers', 65 | path: 'beers', 66 | escapedValue: 'beers', 67 | count: 2, 68 | isRefined: true, 69 | exhaustive: true, 70 | data: null, 71 | }, 72 | { 73 | name: 'fruits', 74 | path: 'fruits', 75 | escapedValue: 'fruits', 76 | count: 3, 77 | isRefined: false, 78 | exhaustive: true, 79 | data: null, 80 | }, 81 | ], 82 | }, 83 | ]; 84 | 85 | client.search = jest.fn(function () { 86 | return Promise.resolve(algoliaResponse); 87 | }); 88 | 89 | helper.setQuery('a').search(); 90 | helper.once('result', function (event) { 91 | var queries = client.search.mock.calls[0][0]; 92 | var hitsQuery = queries[0]; 93 | var parentValuesQuery = queries[1]; 94 | 95 | expect(queries.length).toBe(2); 96 | expect(client.search).toHaveBeenCalledTimes(1); 97 | expect(hitsQuery.params.facets).toEqual(['categories.lvl0']); 98 | expect(hitsQuery.params.facetFilters).toEqual([['categories.lvl0:beers']]); 99 | expect(parentValuesQuery.params.facets).toEqual(['categories.lvl0']); 100 | expect(parentValuesQuery.params.facetFilters).toBe(undefined); 101 | expect(event.results.hierarchicalFacets).toEqual(expectedHelperResponse); 102 | done(); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/spec/hierarchical-facets/refined-no-result.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | test('hierarchical facets: no results', function (done) { 4 | var algoliasearch = require('algoliasearch'); 5 | 6 | var algoliasearchHelper = require('../../../'); 7 | 8 | var appId = 'hierarchical-toggleRefine-appId'; 9 | var apiKey = 'hierarchical-toggleRefine-apiKey'; 10 | var indexName = 'hierarchical-toggleRefine-indexName'; 11 | 12 | var client = algoliasearch(appId, apiKey); 13 | var helper = algoliasearchHelper(client, indexName, { 14 | hierarchicalFacets: [ 15 | { 16 | name: 'categories', 17 | attributes: [ 18 | 'categories.lvl0', 19 | 'categories.lvl1', 20 | 'categories.lvl2', 21 | 'categories.lvl3', 22 | ], 23 | }, 24 | ], 25 | }); 26 | 27 | helper.toggleRefine('categories', 'beers > IPA > Flying dog'); 28 | 29 | var algoliaResponse = { 30 | results: [ 31 | { 32 | query: 'badquery', 33 | index: indexName, 34 | hits: [], 35 | nbHits: 0, 36 | page: 0, 37 | nbPages: 0, 38 | hitsPerPage: 6, 39 | exhaustiveFacetsCount: true, 40 | facets: {}, 41 | }, 42 | { 43 | query: 'badquery', 44 | index: indexName, 45 | hits: [], 46 | nbHits: 0, 47 | page: 0, 48 | nbPages: 0, 49 | hitsPerPage: 1, 50 | facets: { 51 | 'categories.lvl0': { beers: 20, fruits: 5, sales: 20 }, 52 | }, 53 | }, 54 | { 55 | query: 'badquery', 56 | index: indexName, 57 | hits: [], 58 | nbHits: 1, 59 | page: 0, 60 | nbPages: 1, 61 | hitsPerPage: 1, 62 | facets: { 63 | 'categories.lvl0': { beers: 20, fruits: 5, sales: 20 }, 64 | }, 65 | }, 66 | ], 67 | }; 68 | 69 | client.search = jest.fn(function () { 70 | return Promise.resolve(algoliaResponse); 71 | }); 72 | 73 | helper.setQuery('badquery').search(); 74 | 75 | helper.once('result', function (event) { 76 | expect(event.results.hierarchicalFacets).toEqual([ 77 | { 78 | name: 'categories', 79 | count: null, 80 | isRefined: true, 81 | path: null, 82 | escapedValue: null, 83 | exhaustive: true, 84 | data: null, 85 | }, 86 | ]); 87 | done(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/spec/hierarchical-facets/unknown-facet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var algoliasearch = require('algoliasearch'); 3 | 4 | var algoliasearchHelper = require('../../../'); 5 | 6 | test('hierarchical facets: throw on unknown facet', function () { 7 | var appId = 'hierarchical-throw-appId'; 8 | var apiKey = 'hierarchical-throw-apiKey'; 9 | var indexName = 'hierarchical-throw-indexName'; 10 | 11 | var client = algoliasearch(appId, apiKey); 12 | var helper = algoliasearchHelper(client, indexName, { 13 | hierarchicalFacets: [ 14 | { 15 | name: 'categories', 16 | attributes: ['categories.lvl0'], 17 | }, 18 | ], 19 | }); 20 | 21 | expect(function () { 22 | helper.toggleRefine('unknownFacet', 'beers'); 23 | }).toThrow(); 24 | }); 25 | -------------------------------------------------------------------------------- /test/spec/hierarchical-facets/with-a-disjunctive-facet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | test('hierarchical facets: combined with a disjunctive facet', function () { 4 | var algoliasearch = require('algoliasearch'); 5 | 6 | var algoliasearchHelper = require('../../../'); 7 | 8 | var appId = 'hierarchical-simple-appId'; 9 | var apiKey = 'hierarchical-simple-apiKey'; 10 | var indexName = 'hierarchical-simple-indexName'; 11 | 12 | var client = algoliasearch(appId, apiKey); 13 | var helper = algoliasearchHelper(client, indexName, { 14 | disjunctiveFacets: ['colors'], 15 | hierarchicalFacets: [ 16 | { 17 | name: 'categories', 18 | attributes: ['categories.lvl0', 'categories.lvl1', 'categories.lvl2'], 19 | }, 20 | ], 21 | }); 22 | 23 | helper.toggleRefine('categories', 'beers > IPA'); 24 | helper.toggleRefine('colors', 'blue'); 25 | 26 | client.search = jest.fn(function () { 27 | return new Promise(function () {}); 28 | }); 29 | 30 | helper.setQuery('a').search(); 31 | 32 | var disjunctiveFacetsValuesQuery = client.search.mock.calls[0][0][1]; 33 | 34 | expect(disjunctiveFacetsValuesQuery.params.facetFilters).toEqual([ 35 | ['categories.lvl1:beers > IPA'], 36 | ]); 37 | }); 38 | -------------------------------------------------------------------------------- /test/spec/utils/isValidUserToken.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isValidUserToken = require('../../../src/utils/isValidUserToken'); 4 | 5 | test('returns true with valid user token', function () { 6 | expect(isValidUserToken('abc')).toEqual(true); 7 | expect(isValidUserToken('abc-def')).toEqual(true); 8 | expect(isValidUserToken('abc-def_ghi012')).toEqual(true); 9 | }); 10 | 11 | test('returns false with invalid user token', function () { 12 | expect(isValidUserToken(null)).toEqual(false); 13 | expect(isValidUserToken('')).toEqual(false); 14 | expect(isValidUserToken('my token')).toEqual(false); 15 | }); 16 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import algoliasearch from 'algoliasearch'; 2 | import algoliasearchHelper, { 3 | AlgoliaSearchHelper, 4 | SearchParameters, 5 | SearchResults, 6 | } from '..'; 7 | 8 | const helper: AlgoliaSearchHelper = algoliasearchHelper( 9 | algoliasearch('', ''), 10 | '' 11 | ); 12 | 13 | helper.derive( 14 | () => 15 | new SearchParameters({ 16 | query: 'something fun', 17 | }) 18 | ); 19 | 20 | new SearchResults(new SearchParameters(), []); 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "lib": ["es2015"], 5 | "esModuleInterop": true 6 | }, 7 | "files": ["./index.d.ts", "test/types.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /types/algoliasearch.js: -------------------------------------------------------------------------------- 1 | // fake file to allow `export * from 'algoliasearch-helper/types/algoliasearch'` 2 | --------------------------------------------------------------------------------