From a847b0146091f377b0add2808fa8a4c376bbfe0f Mon Sep 17 00:00:00 2001 From: Sumedha Pramod Date: Thu, 16 Aug 2018 15:04:36 -0700 Subject: [PATCH] Chore: Replace Karma with Jest for testing framework (#199) --- .babelrc | 19 +- .eslintrc | 10 +- README.md | 109 +- build/karma.conf.js | 117 - build/webpack.karma.config.js | 15 - docs/dev-setup.md | 48 +- package.json | 49 +- src/__tests__/AnnotationDialog-test.html | 7 - src/__tests__/AnnotationDialog-test.js | 786 +++--- src/__tests__/AnnotationService-test.js | 111 +- src/__tests__/AnnotationThread-test.html | 1 - src/__tests__/AnnotationThread-test.js | 700 ++--- src/__tests__/Annotator-test.html | 2 - src/__tests__/Annotator-test.js | 553 ++-- src/__tests__/BoxAnnotations-test.js | 108 +- src/__tests__/CommentBox-test.js | 267 +- src/__tests__/CreateAnnotationDialog-test.js | 224 +- src/__tests__/util-test.html | 19 - src/__tests__/util-test.js | 414 +-- .../AnnotationModeController-test.js | 408 ++- .../__tests__/DrawingModeController-test.js | 402 +-- .../__tests__/HighlightModeController-test.js | 85 +- .../__tests__/PointModeController-test.js | 218 +- src/doc/DocHighlightDialog.js | 2 +- .../__tests__/CreateHighlightDialog-test.html | 1 - .../__tests__/CreateHighlightDialog-test.js | 214 +- src/doc/__tests__/DocAnnotator-test.html | 1 - src/doc/__tests__/DocAnnotator-test.js | 1020 +++---- src/doc/__tests__/DocDrawingDialog-test.html | 13 - src/doc/__tests__/DocDrawingDialog-test.js | 201 +- src/doc/__tests__/DocDrawingThread-test.html | 1 - src/doc/__tests__/DocDrawingThread-test.js | 306 +-- .../__tests__/DocHighlightDialog-test.html | 13 - src/doc/__tests__/DocHighlightDialog-test.js | 474 ++-- .../__tests__/DocHighlightThread-test.html | 6 - src/doc/__tests__/DocHighlightThread-test.js | 343 ++- src/doc/__tests__/DocPointDialog-test.html | 1 - src/doc/__tests__/DocPointDialog-test.js | 24 +- src/doc/__tests__/DocPointThread-test.html | 1 - src/doc/__tests__/DocPointThread-test.js | 59 +- src/doc/__tests__/docUtil-test.html | 11 - src/doc/__tests__/docUtil-test.js | 336 +-- .../__tests__/DrawingContainer-test.js | 78 +- src/drawing/__tests__/DrawingPath-test.js | 68 +- src/drawing/__tests__/DrawingThread-test.js | 295 +-- src/image/__tests__/ImageAnnotator-test.html | 4 - src/image/__tests__/ImageAnnotator-test.js | 123 +- .../__tests__/ImagePointDialog-test.html | 6 - src/image/__tests__/ImagePointDialog-test.js | 30 +- .../__tests__/ImagePointThread-test.html | 4 - src/image/__tests__/ImagePointThread-test.js | 44 +- src/image/__tests__/imageUtil-test.html | 5 - src/image/__tests__/imageUtil-test.js | 139 +- test-utils/enzyme-adapter.js | 7 + test-utils/fileMock.js | 3 + test-utils/styleMock.js | 3 + yarn.lock | 2359 ++++++----------- 57 files changed, 4836 insertions(+), 6031 deletions(-) delete mode 100644 build/karma.conf.js delete mode 100644 build/webpack.karma.config.js delete mode 100644 src/__tests__/AnnotationDialog-test.html delete mode 100644 src/__tests__/AnnotationThread-test.html delete mode 100644 src/__tests__/Annotator-test.html delete mode 100644 src/__tests__/util-test.html delete mode 100644 src/doc/__tests__/CreateHighlightDialog-test.html delete mode 100644 src/doc/__tests__/DocAnnotator-test.html delete mode 100644 src/doc/__tests__/DocDrawingDialog-test.html delete mode 100644 src/doc/__tests__/DocDrawingThread-test.html delete mode 100644 src/doc/__tests__/DocHighlightDialog-test.html delete mode 100644 src/doc/__tests__/DocHighlightThread-test.html delete mode 100644 src/doc/__tests__/DocPointDialog-test.html delete mode 100644 src/doc/__tests__/DocPointThread-test.html delete mode 100644 src/doc/__tests__/docUtil-test.html delete mode 100644 src/image/__tests__/ImageAnnotator-test.html delete mode 100644 src/image/__tests__/ImagePointDialog-test.html delete mode 100644 src/image/__tests__/ImagePointThread-test.html delete mode 100644 src/image/__tests__/imageUtil-test.html create mode 100644 test-utils/enzyme-adapter.js create mode 100644 test-utils/fileMock.js create mode 100644 test-utils/styleMock.js diff --git a/.babelrc b/.babelrc index 1bfeb3146..b65be9bef 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,3 @@ -// Configuration for Babel transpilation { "presets": [ ["es2015", { "modules": false }], @@ -21,17 +20,13 @@ }, "test": { "plugins": [ - ["istanbul", { - "exclude": [ - "src/polyfill.js", - "**/*-test.js", - "build/**", - "docs/**", - "lib'**" - ] - }], - ["babel-plugin-transform-require-ignore", { "extensions": [".scss"] }] + "transform-es2015-modules-commonjs", + [ + "babel-plugin-transform-require-ignore", { + "extensions": [".scss", ".css"] + } + ] ] - }, + } } } diff --git a/.eslintrc b/.eslintrc index 613cb8540..a392ee695 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,12 +9,13 @@ "es6": true, "browser": true, "mocha": true, - "commonjs": true + "commonjs": true, + "jest": true }, "globals": { - "after": false, + "afterAll": false, "afterEach": false, - "before": false, + "beforeAll": false, "beforeEach": false, "describe": false, "expect": false, @@ -27,7 +28,8 @@ "Feature": false, "Before": false, "After": false, - "Scenario": false + "Scenario": false, + "jest": false }, "rules": { "quotes": [ diff --git a/README.md b/README.md index 55ec34da0..f8a04ec9c 100644 --- a/README.md +++ b/README.md @@ -5,29 +5,29 @@ [![npm version](https://img.shields.io/npm/v/box-ui-elements.svg?style=flat-square)](https://www.npmjs.com/package/box-ui-elements) [![Greenkeeper badge](https://badges.greenkeeper.io/box/box-annotations.svg)](https://greenkeeper.io/) +# [Box Annotations](https://developer.box.com/docs/getting-started-with-new-box-view#section-annotations) -[Box Annotations](https://developer.box.com/docs/getting-started-with-new-box-view#section-annotations) -==================================================================== Box Annotations allow developers to provide collaboration capabilities right from within the embedded Box preview in their application. Box Annotations fit a wide range of use cases and can be used to draw the previewer's attention and/or provide feedback on specific parts of a document or images. To learn more about Box Content Preview and for further documentation on how to use it, please go to our page on [Box Content Preview](https://developer.box.com/docs/box-content-preview). Box Content Preview currently supports four annotation types - highlight comment, highlight only, draw, and point annotation. Box Annotations are today supported on documents and image previews only. You can find the full list of supported file types for Box Content Preview at https://community.box.com/t5/Managing-Your-Content/What-file-types-and-fonts-are-supported-by-Box-s-Content-Preview/ta-p/327#FileTypesSupported. -Browser Support ---------------- -* Desktop Chrome, Firefox, Safari, Edge, and Internet Explorer 11 -* Mobile support available for iOS Safari, Android Chrome +## Browser Support + +* Desktop Chrome, Firefox, Safari, Edge, and Internet Explorer 11 +* Mobile support available for iOS Safari, Android Chrome If you are using Internet Explorer 11, which doesn't natively support promises, include a polyfill.io script (see sample code below) or a Promise library like Bluebird. -Supported Locales ------------------ +## Supported Locales + `en-AU`, `en-CA`, `en-GB`, `en-US`, `bn-IN`, `da-DK`, `de-DE`, `es-419`, `es-ES`, `fi-FI`, `fr-CA`, `fr-FR`, `hi-IN`,`it-IT`, `ja-JP`, `ko-KR`, `nb-NO`, `nl-NL`, `pl-PL`, `pt-BR`, `ru-RU`, `sv-SE`, `tr-TR`, `zh-CN`, `zh-TW` -Usage ------ +## Usage + Box Annotations can be used by pulling from our [NPM package](https://www.npmjs.com/package/box-annotations). ### Instantiating Box Annotations inside Box Content Preview with default options + ```javascript var preview = new Box.Preview(); preview.show('FILE_ID', 'ACCESS_TOKEN', { @@ -35,9 +35,11 @@ preview.show('FILE_ID', 'ACCESS_TOKEN', { showAnnotations: true }); ``` + Where the default enabled types are `point`, `highlight`, and `highlight-comment` for the Document Annotator and `point` for the Image Annotator. ### Passing an instance of Box Annotations into Box Content Preview + ```javascript import BoxAnnotations from 'box-annotations'; @@ -50,88 +52,89 @@ preview.show(FILE_ID, ACCESS_TOKEN, { boxAnnotations }); ``` + Where `viewerOptions` is an optional object of viewer-specific annotator options and `disabledAnnotationTypes` is an optional string array of valid annotation types to disable. See [Enabling/Disabling Annotations and Annotation Types](docs/enabling-types.md) below for more details on viewer-specific annotation configurations. -Access Token ------------- +## Access Token + Box Annotations needs an access token to make Box API calls. You can either get an access token from the token endpoint (https://developer.box.com/reference#token) or generate a developer token on your application management page (https://blog.box.com/blog/introducing-developer-tokens/). If your application requires the end user to only be able to access a subset of the Annotations functionality, you can use [Token Exchange](https://developer.box.com/reference#token-exchange) to appropriately downscope your App/Managed or Service Account token to a resulting token that has the desired set of permissions, and can thus, be securely passed to the end user client initializing Annotations. See [the following documentation](docs/auth.md) for more details on Annotation-specific scopes to go alongside Token Exchange. These allow developers to enable/disable functionality on Box Annotations by configuring the appropriate scopes on the downscoped token. To learn more, see [Special Scopes for Box UI Elements](https://developer.box.com/v2.0/docs/special-scopes-for-box-ui-elements). -Supported Annotation Types --------------------- +## Supported Annotation Types + Point annotations are supported on both document and image formats. Highlight comment, highlight only, and draw annotations are only supported on document formats. Supported document file extensions: `pdf, doc, docx, ppt, pptx` Supported image file extensions: `ai, bmp, dcm, eps, gif, png, ps, psd, svs, tga, tif, tiff` -Development Setup ------------------ -1. Install Node v8.9.4 or higher. -2. Install yarn package manager `https://yarnpkg.com/en/docs/install`. Alternatively, you can replace any `yarn` command with `npm`. -3. Fork the upstream repo `https://github.com/box/box-annotations`. -4. Clone your fork locally `git clone git@github.com:[YOUR GITHUB USERNAME]/box-annotations.git`. -5. Navigate to the cloned folder `cd box-annotations` -6. Add the upstream repo to your remotes `git remote add upstream git@github.com:box/box-annotations.git`. -7. Verify your remotes are properly set up `git remote -v`. You should pull updates from the Box repo `upstream` and push changes to your fork `origin`. -8. Install dependencies `yarn install` -9. Test your first build! `yarn run build` +## Development Setup + +1. Install Node v8.9.4 or higher. +2. Install yarn package manager `https://yarnpkg.com/en/docs/install`. Alternatively, you can replace any `yarn` command with `npm`. +3. Fork the upstream repo `https://github.com/box/box-annotations`. +4. Clone your fork locally `git clone git@github.com:[YOUR GITHUB USERNAME]/box-annotations.git`. +5. Navigate to the cloned folder `cd box-annotations` +6. Add the upstream repo to your remotes `git remote add upstream git@github.com:box/box-annotations.git`. +7. Verify your remotes are properly set up `git remote -v`. You should pull updates from the Box repo `upstream` and push changes to your fork `origin`. +8. Install dependencies `yarn install` +9. Test your first build! `yarn run build` 10. To test only local annotation changes, see [instantiating a custom instance of Box Annotations](https://github.com/box/box-annotations/#passing-an-instance-of-box-annotations-into-box-content-preview). 11. To link and test your local code changes along with your local Preview changes, run `yarn link` in this repository and `yarn link box-annotations` wherever [Box Content Preview](github.com/box/box-content-preview/) is cloned locally. For more information on contributing see [Contributing](docs/contributing.md). -While Developing ----------------- +## While Developing + Install the following plugins in your preferred editor -* Editor Config (standardizes basic editor configuration) -* ESLint (Javascript linting) -* Prettier & Prettier - ESLint (Automatic Javascript formatting following ESLint config) -* Stylelint (CSS linting) +* Editor Config (standardizes basic editor configuration) +* ESLint (Javascript linting) +* Prettier & Prettier - ESLint (Automatic Javascript formatting following ESLint config) +* Stylelint (CSS linting) ### Yarn commands -* `yarn run build` to generate resource bundles and JS webpack bundles. -* `yarn run watch` to only generate JS webpack bundles on file changes. -* `yarn run test` launches karma tests with PhantomJS. -* `yarn run test -- --src=PATH/TO/SRC/FILENAME` launches test only for `src/PATH/TO/SRC/__tests__/FILENAME-test.js` instead of all tests. For example, `yarn run test -- --src=doc/DocAnnotator` launches tests for `src/doc/__tests__/DocAnnotator-test.js`. This also works for directories, e.g. `yarn run test -- --src=doc/`. -* `yarn run debug` launches karma tests with PhantomJS for debugging. Open the URL mentioned in the console. -* `yarn run debug -- --src=path/to/src/FILENAME` launches debugging for `src/path/to/src/__tests__/FILENAME-test.js` instead of all tests. Open the URL mentioned in the console. +* `yarn run build` to generate resource bundles and JS webpack bundles. +* `yarn run watch` to only generate JS webpack bundles on file changes. +* `yarn run test` launches jest. +* `yarn run test -- --src=PATH/TO/SRC/FILENAME` launches test only for `src/PATH/TO/SRC/__tests__/FILENAME-test.js` instead of all tests. For example, `yarn run test -- --src=doc/DocAnnotator` launches tests for `src/doc/__tests__/DocAnnotator-test.js`. This also works for directories, e.g. `yarn run test -- --src=doc/`. +* `yarn run debug` launches jest for debugging. Open the URL mentioned in the console. +* `yarn run debug -- --src=path/to/src/FILENAME` launches debugging for `src/path/to/src/__tests__/FILENAME-test.js` instead of all tests. Open the URL mentioned in the console. For more script commands see `package.json`. Test coverage reports are available under reports/coverage. ### Config files -* .babelrc - https://babeljs.io/docs/usage/babelrc/ -* .editorconfig - http://editorconfig.org/ -* .eslintignore - http://eslint.org/docs/user-guide/configuring#ignoring-files-and-directories -* .eslintrc - http://eslint.org/docs/user-guide/configuring -* .gitignore - https://git-scm.com/docs/gitignore -* .stylelintrc - https://stylelint.io/user-guide/configuration/ -* .travis.yml - https://docs.travis-ci.com/user/customizing-the-build -* browserslist - https://github.com/ai/browserslist -* commitlint.config.js - https://github.com/marionebl/commitlint -* postcss.config.js - https://github.com/postcss/postcss-loader - -Support -------- +* .babelrc - https://babeljs.io/docs/usage/babelrc/ +* .editorconfig - http://editorconfig.org/ +* .eslintignore - http://eslint.org/docs/user-guide/configuring#ignoring-files-and-directories +* .eslintrc - http://eslint.org/docs/user-guide/configuring +* .gitignore - https://git-scm.com/docs/gitignore +* .stylelintrc - https://stylelint.io/user-guide/configuration/ +* .travis.yml - https://docs.travis-ci.com/user/customizing-the-build +* browserslist - https://github.com/ai/browserslist +* commitlint.config.js - https://github.com/marionebl/commitlint +* postcss.config.js - https://github.com/postcss/postcss-loader + +## Support + If you have any questions, please search our [issues list](https://github.com/box/box-annotations/issues) to see if they have been previously answered. Report new issues [here](https://github.com/box/box-annotations/issues/new). For general Box Platform, API, Elements, and Annotations questions, please visit our [developer forum](https://community.box.com/t5/Developer-Forum/bd-p/DeveloperForum) or contact us via one of our [available support channels](https://community.box.com/t5/Community/ct-p/English). -Copyright and License ---------------------- +## Copyright and License + Copyright 2016-present Box, Inc. All Rights Reserved. Licensed under the Box Software License Agreement v.20170516. You may not use this file except in compliance with the License. You may obtain a copy of the License at - https://developer.box.com/docs/box-sdk-license +https://developer.box.com/docs/box-sdk-license Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/build/karma.conf.js b/build/karma.conf.js deleted file mode 100644 index 4836cbff8..000000000 --- a/build/karma.conf.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ -const webpackConfig = require('./webpack.karma.config'); - -const getTestFile = (src) => { - if (!src) { - return [ - 'src/**/*-test.js', - 'src/**/*-test.html' - ]; - } - - if (src.endsWith('/')) { - return [ - `src/${src}**/*-test.js`, - `src/${src}**/*-test.html` - ]; - } - - const frags = src.split('/'); - const fileName = frags[frags.length - 1]; - if (!fileName) { - throw new Error('Incorrect path to source file'); - } - - const path = src.replace(fileName, ''); - const base = path ? `src/${path}` : 'src'; - return [ - `${base}/__tests__/${fileName}-test.js`, - `${base}/__tests__/${fileName}-test.html` - ]; -}; - -module.exports = (config) => config.set({ - autoWatch: false, - - basePath: '..', - - browserConsoleLogOptions: { - level: 'log', - format: '%b %T: %m', - terminal: true - }, - - browsers: ['PhantomJS'], - - browserNoActivityTimeout: 100000, - - captureConsole: true, - - colors: true, - - coverageReporter: { - check: config.src ? {} : { - global: { - statements: 80, - branches: 80, - functions: 80, - lines: 80 - } - }, - reporters: [ - { - type: 'html', - dir: 'reports/coverage/html' - }, - { - type: 'cobertura', - dir: 'reports/coverage/cobertura' - }, - { type: 'text' } - ] - }, - - junitReporter: { - outputDir: 'reports/coverage/junit', - outputFile: 'junit.xml' - }, - - frameworks: [ - 'mocha', - 'sinon-stub-promise', - 'chai-sinon', - 'chai-dom', - 'chai', - 'sinon', - 'fixture' - ], - - files: [ - 'node_modules/babel-polyfill/dist/polyfill.js' - ].concat(getTestFile(config.src)), - - exclude: [], - - preprocessors: { - 'src/**/*-test.js': ['webpack', 'sourcemap'], - 'src/**/*-test.html': ['html2js'] - }, - - phantomjsLauncher: { - exitOnResourceError: false - }, - - port: 9876, - - reporters: ['mocha', 'coverage', 'junit'], - - logLevel: config.LOG_ERROR, - - singleRun: true, - - webpack: webpackConfig, - - webpackMiddleware: { - noInfo: true - } -}); diff --git a/build/webpack.karma.config.js b/build/webpack.karma.config.js deleted file mode 100644 index deb64f67c..000000000 --- a/build/webpack.karma.config.js +++ /dev/null @@ -1,15 +0,0 @@ -require('babel-polyfill'); - -const commonConfig = require('./webpack.common.config'); - -module.exports = Object.assign(commonConfig(), { - devtool: 'inline-source-map', - resolve: { - alias: { - sinon: 'sinon/pkg/sinon' - } - }, - externals: { - mocha: 'mocha' - } -}); diff --git a/docs/dev-setup.md b/docs/dev-setup.md index 3a2d388da..42fb5d52a 100644 --- a/docs/dev-setup.md +++ b/docs/dev-setup.md @@ -1,33 +1,33 @@ -Development Setup ------------------ -1. Install Node v8.9.4 or higher. -2. Install yarn package manager `https://yarnpkg.com/en/docs/install`. Alternatively, you can replace any `yarn` command with `npm`. -3. Fork the upstream repo `https://github.com/box/box-annotations`. -4. Clone your fork locally `git clone git@github.com:[YOUR GITHUB USERNAME]/box-annotations.git`. -5. Navigate to the cloned folder `cd box-annotations` -6. Add the upstream repo to your remotes `git remote add upstream git@github.com:box/box-annotations.git`. -7. Verify your remotes are properly set up `git remote -v`. You should pull updates from the Box repo `upstream` and push changes to your fork `origin`. -8. Install dependencies `yarn install` -9. Test your first build! `yarn run build` +## Development Setup + +1. Install Node v8.9.4 or higher. +2. Install yarn package manager `https://yarnpkg.com/en/docs/install`. Alternatively, you can replace any `yarn` command with `npm`. +3. Fork the upstream repo `https://github.com/box/box-annotations`. +4. Clone your fork locally `git clone git@github.com:[YOUR GITHUB USERNAME]/box-annotations.git`. +5. Navigate to the cloned folder `cd box-annotations` +6. Add the upstream repo to your remotes `git remote add upstream git@github.com:box/box-annotations.git`. +7. Verify your remotes are properly set up `git remote -v`. You should pull updates from the Box repo `upstream` and push changes to your fork `origin`. +8. Install dependencies `yarn install` +9. Test your first build! `yarn run build` 10. To test only local annotation changes, see [instantiating a custom instance of Box Annotations](https://github.com/box/box-annotations/#passing-an-instance-of-box-annotations-into-box-content-preview). 11. To link and test your local code changes along with your local Preview changes, run `yarn link` in this repository and `yarn link box-annotations` wherever [Box Content Preview](github.com/box/box-content-preview/) is cloned locally. -While Developing ----------------- +## While Developing + Install the following plugins in your preferred editor -* Editor Config (standardizes basic editor configuration) -* ESLint (Javascript linting) -* Prettier & Prettier - ESLint (Automatic Javascript formatting following ESLint config) -* Stylelint (CSS linting) +* Editor Config (standardizes basic editor configuration) +* ESLint (Javascript linting) +* Prettier & Prettier - ESLint (Automatic Javascript formatting following ESLint config) +* Stylelint (CSS linting) ### Yarn commands -* `yarn run build` to generate resource bundles and JS webpack bundles. -* `yarn run watch` to only generate JS webpack bundles on file changes. -* `yarn run test` launches karma tests with PhantomJS. -* `yarn run test -- --src=PATH/TO/SRC/FILENAME` launches test only for `src/PATH/TO/SRC/__tests__/FILENAME-test.js` instead of all tests. For example, `yarn run test -- --src=doc/DocAnnotator` launches tests for `src/doc/__tests__/DocAnnotator-test.js`. This also works for directories, e.g. `yarn run test -- --src=doc/`. -* `yarn run debug` launches karma tests with PhantomJS for debugging. Open the URL mentioned in the console. -* `yarn run debug -- --src=path/to/src/FILENAME` launches debugging for `src/path/to/src/__tests__/FILENAME-test.js` instead of all tests. Open the URL mentioned in the console. +* `yarn run build` to generate resource bundles and JS webpack bundles. +* `yarn run watch` to only generate JS webpack bundles on file changes. +* `yarn run test` launches jest. +* `yarn run test -- --src=PATH/TO/SRC/FILENAME` launches test only for `src/PATH/TO/SRC/__tests__/FILENAME-test.js` instead of all tests. For example, `yarn run test -- --src=doc/DocAnnotator` launches tests for `src/doc/__tests__/DocAnnotator-test.js`. This also works for directories, e.g. `yarn run test -- --src=doc/`. +* `yarn run debug` launches jest for debugging. Open the URL mentioned in the console. +* `yarn run debug -- --src=path/to/src/FILENAME` launches debugging for `src/path/to/src/__tests__/FILENAME-test.js` instead of all tests. Open the URL mentioned in the console. -For more script commands see `package.json`. Test coverage reports are available under reports/coverage. \ No newline at end of file +For more script commands see `package.json`. Test coverage reports are available under reports/coverage. diff --git a/package.json b/package.json index 11755e0fd..c90eddfa7 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "babel-cli": "^6.26.0", "babel-core": "^6.26.0", "babel-eslint": "^8.2.3", + "babel-jest": "^23.4.2", "babel-loader": "^7.1.2", - "babel-plugin-istanbul": "^4.1.6", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-object-rest-spread": "^6.26.0", @@ -38,6 +38,7 @@ "conventional-github-releaser": "^2.0.0", "css-loader": "^0.28.7", "cssnano-cli": "^1.0.5", + "enzyme-to-json": "^3.3.4", "eslint": "^4.12.1", "eslint-config-airbnb": "^16.1.0", "eslint-config-prettier": "^2.9.0", @@ -50,22 +51,8 @@ "fetch-mock": "^6.0.0", "fetch-mock-forwarder": "^1.0.0", "husky": "^0.14.3", - "karma": "^2.0.0", - "karma-chai": "^0.1.0", - "karma-chai-dom": "^1.1.0", - "karma-chai-sinon": "^0.1.5", - "karma-chrome-launcher": "^2.2.0", - "karma-coverage": "^1.1.1", - "karma-fixture": "^0.2.6", - "karma-html2js-preprocessor": "^1.1.0", - "karma-junit-reporter": "^1.2.0", - "karma-mocha": "^1.3.0", - "karma-mocha-reporter": "^2.2.5", - "karma-phantomjs-launcher": "^1.0.4", - "karma-sinon": "^1.0.5", - "karma-sinon-stub-promise": "^1.0.0", - "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^2.0.6", + "jest": "^23.5.0", + "jest-canvas-mock": "^1.1.0", "lint-staged": "^7.0.4", "mocha": "^5.1.1", "node-noop": "^1.0.0", @@ -94,8 +81,8 @@ "scripts": { "build": "yarn install && BABEL_ENV=production NODE_ENV=production CI=1 node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js -p --progress --colors --config build/webpack.config.js", "watch": "yarn install && BABEL_ENV=dev NODE_ENV=dev ./node_modules/.bin/webpack --watch --progress --colors --config build/webpack.config.js", - "test": "yarn install && NODE_ENV=test ./node_modules/.bin/karma start build/karma.conf.js", - "debug": "yarn install && NODE_ENV=test ./node_modules/.bin/karma start build/karma.conf.js --no-single-run --auto-watch", + "test": "yarn install && NODE_ENV=test yarn run jest", + "debug": "yarn install && NODE_ENV=test yarn run jest --watch", "setup-travis": "cd functional-tests && yarn install && node app.js", "selenium-build": "BABEL_ENV=production NODE_ENV=production CI=1 node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js -p --progress --colors --config build/webpack.selenium.config.js", "functional-tests": "node functional-tests/app.js & node ./node_modules/codeceptjs/bin/codecept.js run --steps", @@ -120,5 +107,29 @@ "prettier-eslint --print-width 120 --single-quote --tab-width 4 --write", "git add" ] + }, + "jest": { + "clearMocks": true, + "globals": { + "__NAME__": "name", + "__VERSION__": "version" + }, + "setupFiles": [ + "jest-canvas-mock" + ], + "moduleNameMapper": { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test-utils/fileMock.js", + "\\.(css|less|html)$": "/test-utils/styleMock.js" + }, + "transformIgnorePatterns": [ + "node_modules/(?!(box-react-ui)/)" + ], + "collectCoverage": false, + "coverageDirectory": "/reports", + "collectCoverageFrom": [ + "src/**/*.js", + "!**/node_modules/**", + "!**/__tests__/**" + ] } } diff --git a/src/__tests__/AnnotationDialog-test.html b/src/__tests__/AnnotationDialog-test.html deleted file mode 100644 index a1807db2a..000000000 --- a/src/__tests__/AnnotationDialog-test.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
-
-
-
-
-
diff --git a/src/__tests__/AnnotationDialog-test.js b/src/__tests__/AnnotationDialog-test.js index 735f002af..0476b4899 100644 --- a/src/__tests__/AnnotationDialog-test.js +++ b/src/__tests__/AnnotationDialog-test.js @@ -6,25 +6,31 @@ import * as util from '../util'; import * as constants from '../constants'; const CLASS_FLIPPED_DIALOG = 'ba-annotation-dialog-flipped'; -const CLASS_REPLY_TEXTAREA = 'reply-textarea'; -const CLASS_REPLY_CONTAINER = 'reply-container'; +const SELECTOR_REPLY_TEXTAREA = '.reply-textarea'; +const SELECTOR_REPLY_CONTAINER = '.reply-container'; const CLASS_ANIMATE_DIALOG = 'ba-animate-show-dialog'; -const CLASS_BUTTON_DELETE_COMMENT = 'delete-comment-btn'; -const CLASS_COMMENTS_CONTAINER = 'annotation-comments'; +const SELECTOR_BUTTON_DELETE_COMMENT = '.delete-comment-btn'; +const SELECTOR_COMMENTS_CONTAINER = '.annotation-comments'; const SELECTOR_DELETE_CONFIRMATION = '.delete-confirmation'; const CLASS_INVALID_INPUT = 'ba-invalid-input'; let dialog; -const sandbox = sinon.sandbox.create(); -let stubs = {}; - -describe('AnnotationDialog', () => { - before(() => { - fixture.setBase('src'); - }); +const html = `
+
+
+
+
+
+
`; + +describe('Annotator', () => { + let rootElement; + let annotation; beforeEach(() => { - fixture.load('__tests__/AnnotationDialog-test.html'); + rootElement = document.createElement('div'); + rootElement.innerHTML = html; + document.body.appendChild(rootElement); dialog = new AnnotationDialog({ annotatedElement: document.querySelector(constants.SELECTOR_ANNOTATED_ELEMENT), @@ -33,14 +39,25 @@ describe('AnnotationDialog', () => { annotations: {}, canAnnotate: true }); + dialog.localized = { addCommentPlaceholder: 'add comment placeholder', posting: 'posting' }; + + annotation = new Annotation({ + annotationID: 1, + text: 'the preview sdk is amazing!', + user: { id: 1, name: 'user' }, + permissions: { can_delete: true } + }); + dialog.setup(); document.querySelector(constants.SELECTOR_ANNOTATED_ELEMENT).appendChild(dialog.element); - stubs.emit = sandbox.stub(dialog, 'emit'); + dialog.emit = jest.fn(); + util.hideElement = jest.fn(); + util.showElement = jest.fn(); dialog.isMobile = false; }); @@ -51,32 +68,26 @@ describe('AnnotationDialog', () => { } dialog.element = null; - - sandbox.verifyAndRestore(); - if (typeof dialog.destroy === 'function') { - dialog.destroy(); - dialog = null; - } - - stubs = {}; + document.body.removeChild(rootElement); + dialog = null; }); describe('destroy()', () => { it('should unbind DOM listeners and cleanup its HTML', () => { - const unbindStub = sandbox.stub(dialog, 'unbindDOMListeners'); + dialog.unbindDOMListeners = jest.fn(); dialog.destroy(); - expect(unbindStub).to.be.called; - expect(dialog.element).to.be.null; + expect(dialog.unbindDOMListeners).toBeCalled(); + expect(dialog.element).toBeNull(); }); }); describe('show()', () => { beforeEach(() => { - stubs.position = sandbox.stub(dialog, 'position'); - stubs.focus = sandbox.stub(util, 'focusTextArea'); - stubs.scroll = sandbox.stub(dialog, 'scrollToLastComment'); - sandbox.stub(dialog, 'showMobileDialog'); + dialog.position = jest.fn(); + util.focusTextArea = jest.fn(); + dialog.scrollToLastComment = jest.fn(); + dialog.showMobileDialog = jest.fn(); dialog.canAnnotate = true; dialog.element.classList.add(constants.CLASS_HIDDEN); }); @@ -84,17 +95,17 @@ describe('AnnotationDialog', () => { it('should show the mobile dialog if on a mobile device', () => { dialog.isMobile = true; dialog.show(); - expect(dialog.showMobileDialog).to.be.called; - expect(stubs.scroll).to.be.called; - expect(dialog.emit).to.be.calledWith('annotationshow'); + expect(dialog.showMobileDialog).toBeCalled(); + expect(dialog.scrollToLastComment).toBeCalled(); + expect(dialog.emit).toBeCalledWith('annotationshow'); }); it('should do nothing if the dialog is already visible ', () => { dialog.element.classList.remove(constants.CLASS_HIDDEN); dialog.show(); - expect(dialog.showMobileDialog).to.not.be.called; - expect(stubs.scroll).to.not.be.called; - expect(dialog.emit).to.not.be.calledWith('annotationshow'); + expect(dialog.showMobileDialog).not.toBeCalled(); + expect(dialog.scrollToLastComment).not.toBeCalled(); + expect(dialog.emit).not.toBeCalledWith('annotationshow'); }); it('should not reposition the dialog if the reply textarea is already active', () => { @@ -102,9 +113,9 @@ describe('AnnotationDialog', () => { dialog.activateReply(); dialog.show(); - expect(stubs.scroll).to.be.called; - expect(stubs.position).to.not.be.called; - expect(dialog.emit).to.not.be.calledWith('annotationshow'); + expect(dialog.scrollToLastComment).toBeCalled(); + expect(dialog.position).not.toBeCalled(); + expect(dialog.emit).not.toBeCalledWith('annotationshow'); }); it('should position the dialog if not on a mobile device', () => { @@ -114,20 +125,20 @@ describe('AnnotationDialog', () => { commentsTextArea.classList.remove(constants.CLASS_ACTIVE); dialog.show(); - expect(stubs.position).to.be.called; - expect(stubs.scroll).to.be.called; - expect(dialog.emit).to.be.calledWith('annotationshow'); + expect(dialog.position).toBeCalled(); + expect(dialog.scrollToLastComment).toBeCalled(); + expect(dialog.emit).toBeCalledWith('annotationshow'); }); }); describe('scrollToLastComment()', () => { beforeEach(() => { - sandbox.stub(util, 'focusTextArea'); + util.focusTextArea = jest.fn(); }); it('should activate the reply text area if the dialog has multiple annotations', () => { dialog.hasAnnotations = true; - sandbox.stub(dialog, 'activateReply'); + dialog.activateReply = jest.fn(); dialog.scrollToLastComment(); expect(dialog.activateReply); @@ -140,10 +151,10 @@ describe('AnnotationDialog', () => { clientHeight: 300, scrollTop: 0 }; - sandbox.stub(dialog.element, 'querySelector').returns(annotationEl); + dialog.element.querySelector = jest.fn().mockReturnValue(annotationEl); dialog.scrollToLastComment(); - expect(annotationEl.scrollTop).to.equal(200); + expect(annotationEl.scrollTop).toEqual(200); }); it('should set the flipped dialog scroll height to the bottom of the comments container', () => { @@ -153,199 +164,173 @@ describe('AnnotationDialog', () => { scrollTop: 0 }; dialog.dialogEl.classList.add('ba-annotation-dialog-flipped'); - sandbox.stub(dialog.element, 'querySelector').returns(annotationEl); + dialog.element.querySelector = jest.fn().mockReturnValue(annotationEl); dialog.scrollToLastComment(); - expect(annotationEl.scrollTop).to.equal(500); + expect(annotationEl.scrollTop).toEqual(500); }); }); describe('showMobileDialog()', () => { beforeEach(() => { - stubs.show = sandbox.stub(util, 'showElement'); - stubs.bind = sandbox.stub(dialog, 'bindDOMListeners'); + dialog.bindDOMListeners = jest.fn(); - dialog.container = { - querySelector: sandbox.stub().returns(dialog.element) - }; + dialog.container = document.createElement('div'); + dialog.container.querySelector = jest.fn().mockReturnValue(dialog.element); dialog.element.classList.add(constants.CLASS_MOBILE_ANNOTATION_DIALOG); dialog.element.classList.add(constants.CLASS_HIDDEN); - sandbox.stub(dialog.element, 'querySelector').returns(document.createElement('div')); + dialog.element.querySelector = jest.fn().mockReturnValue(document.createElement('div')); }); it('should populate the mobile dialog if using a mobile browser', () => { dialog.highlightDialogEl = null; dialog.showMobileDialog(); - expect(stubs.show).to.be.calledWith(dialog.element); - expect(stubs.bind).to.be.called; - expect(dialog.element.classList.contains(constants.CLASS_MOBILE_ANNOTATION_DIALOG)).to.be.true; - expect(dialog.element.classList.contains(CLASS_ANIMATE_DIALOG)).to.be.true; + expect(util.showElement).toBeCalledWith(dialog.element); + expect(dialog.bindDOMListeners).toBeCalled(); + expect(dialog.element.classList.contains(constants.CLASS_MOBILE_ANNOTATION_DIALOG)).toBeTruthy(); + expect(dialog.element.classList.contains(CLASS_ANIMATE_DIALOG)).toBeTruthy(); }); it('should reset the annotation dialog to be a plain highlight if no comments are present', () => { dialog.highlightDialogEl = {}; - sandbox - .stub(dialog.element, 'querySelectorAll') - .withArgs('.annotation-comment') - .returns([]); + + const headerEl = document.createElement('div'); + headerEl.classList.add(constants.CLASS_MOBILE_DIALOG_HEADER); + dialog.element.appendChild(headerEl); + dialog.showMobileDialog(); - expect(dialog.element.classList.contains(constants.CLASS_ANNOTATION_PLAIN_HIGHLIGHT)).to.be.true; + expect(dialog.element.classList).toContain(constants.CLASS_ANNOTATION_PLAIN_HIGHLIGHT); }); it('should not re-show the dialog if it is already visible', () => { dialog.element.classList.remove(constants.CLASS_HIDDEN); dialog.showMobileDialog(); - expect(stubs.show).to.not.be.called; + expect(util.showElement).not.toBeCalled(); }); }); describe('hideMobileDialog()', () => { it('should do nothing if the dialog element does not exist', () => { dialog.element = null; - stubs.hide = sandbox.stub(util, 'hideElement'); dialog.hideMobileDialog(); - expect(stubs.hide).to.not.be.called; + expect(util.hideElement).not.toBeCalled(); }); it('should hide the mobile annotations dialog', () => { dialog.element = document.querySelector(constants.SELECTOR_MOBILE_ANNOTATION_DIALOG); - stubs.hide = sandbox.stub(util, 'hideElement'); - stubs.unbind = sandbox.stub(dialog, 'unbindDOMListeners'); + dialog.unbindDOMListeners = jest.fn(); dialog.hasAnnotations = true; dialog.hideMobileDialog(); - expect(stubs.hide).to.be.called; - expect(stubs.unbind).to.be.called; + expect(util.hideElement).toBeCalled(); + expect(dialog.unbindDOMListeners).toBeCalled(); }); }); describe('hide()', () => { beforeEach(() => { + dialog.isMobile = false; dialog.element.classList.remove(constants.CLASS_HIDDEN); - stubs.unbind = sandbox.stub(dialog, 'unbindDOMListeners'); + dialog.unbindDOMListeners = jest.fn(); }); it('should do nothing if element is already hidden', () => { dialog.element.classList.add(constants.CLASS_HIDDEN); - sandbox.stub(util, 'hideElement'); dialog.hide(); - expect(util.hideElement).to.not.have.called; - expect(dialog.emit).to.not.be.calledWith('annotationhide'); + expect(util.hideElement).not.toBeCalled(); + expect(dialog.emit).not.toBeCalledWith('annotationhide'); }); it('should hide dialog immediately', () => { - sandbox.stub(dialog, 'toggleFlippedThreadEl'); + dialog.toggleFlippedThreadEl = jest.fn(); dialog.hide(); - expect(dialog.element).to.have.class(constants.CLASS_HIDDEN); - expect(dialog.toggleFlippedThreadEl).to.be.called; - expect(dialog.emit).to.be.calledWith('annotationhide'); + expect(util.hideElement).toBeCalledWith(dialog.element); + expect(dialog.toggleFlippedThreadEl).toBeCalled(); + expect(dialog.emit).toBeCalledWith('annotationhide'); }); it('should hide the mobile dialog if using a mobile browser', () => { dialog.isMobile = true; - sandbox.stub(dialog, 'hideMobileDialog'); - sandbox.stub(dialog, 'toggleFlippedThreadEl'); + dialog.hideMobileDialog = jest.fn(); + dialog.toggleFlippedThreadEl = jest.fn(); dialog.hide(); - expect(dialog.hideMobileDialog).to.be.called; - expect(dialog.toggleFlippedThreadEl).to.be.called; + expect(dialog.hideMobileDialog).toBeCalled(); + expect(dialog.toggleFlippedThreadEl).toBeCalled(); dialog.element = null; - expect(dialog.emit).to.be.calledWith('annotationhide'); + expect(dialog.emit).toBeCalledWith('annotationhide'); }); }); describe('addAnnotation()', () => { beforeEach(() => { - stubs.addEl = sandbox.stub(dialog, 'addAnnotationElement'); + dialog.addAnnotationElement = jest.fn(); }); it('should add annotation to the dialog and deactivate the reply area', () => { dialog.addAnnotation(new Annotation({})); - expect(stubs.addEl).to.be.called; + expect(dialog.addAnnotationElement).toBeCalled(); }); it('should hide the create section and show the show section if there are no annotations', () => { - // Add dialog to DOM - dialog.annotatedElement.appendChild(dialog.element); + dialog.hasAnnotations = false; dialog.addAnnotation(new Annotation({})); const createSectionEl = document.querySelector(constants.SECTION_CREATE); const showSectionEl = document.querySelector(constants.SECTION_SHOW); - expect(createSectionEl).to.have.class(constants.CLASS_HIDDEN); - expect(showSectionEl).to.not.have.class(constants.CLASS_HIDDEN); + expect(util.hideElement).toBeCalledWith(createSectionEl); + expect(util.showElement).toBeCalledWith(showSectionEl); }); }); describe('removeAnnotation()', () => { it('should remove annotation element and deactivate reply', () => { - dialog.addAnnotation( - new Annotation({ - annotationID: 'someID', - text: 'blah', - user: {}, - permissions: {} - }) - ); + dialog.addAnnotation(annotation); dialog.removeAnnotation('someID'); const annotationEl = dialog.element.querySelector('[data-annotation-id="someID"]'); - expect(annotationEl).to.be.null; + expect(annotationEl).toBeNull(); }); it('should focus the reply text area', () => { - const replyTextEl = dialog.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - sandbox.stub(replyTextEl, 'focus'); + const replyTextEl = dialog.element.querySelector(SELECTOR_REPLY_TEXTAREA); + replyTextEl.focus = jest.fn(); dialog.removeAnnotation('someID'); - expect(replyTextEl.focus).to.be.called; + expect(replyTextEl.focus).toBeCalled(); }); }); describe('element()', () => { it('should return dialog element', () => { - expect(dialog.element).to.equal(dialog.element); + expect(dialog.element).toEqual(dialog.element); }); }); describe('setup()', () => { beforeEach(() => { const dialogEl = document.createElement('div'); - sandbox.stub(dialog, 'generateDialogEl').returns(dialogEl); - stubs.bind = sandbox.stub(dialog, 'bindDOMListeners'); - stubs.addSorted = sandbox.stub(dialog, 'addSortedAnnotations'); - stubs.unbind = sandbox.stub(dialog, 'unbindDOMListeners'); - - stubs.annotation = new Annotation({ - annotationID: 'someID', - text: 'blah', - user: {}, - permissions: {}, - threadNumber: 1 - }); - + dialog.generateDialogEl = jest.fn().mockReturnValue(dialogEl); + dialog.bindDOMListeners = jest.fn(); + dialog.addSortedAnnotations = jest.fn(); + dialog.unbindDOMListeners = jest.fn(); dialog.isMobile = false; }); it('should set up HTML element, add annotations to the dialog, and bind DOM listeners', () => { - dialog.setup([stubs.annotation], {}); - expect(dialog.element).to.not.be.null; - expect(dialog.element.dataset.threadNumber).to.equal('1'); - expect(stubs.bind).to.be.called; - expect(dialog.threadEl).not.be.null; - }); - - it('should not set thread number if there are no annotations in the thread', () => { - dialog.setup([], {}); - expect(dialog.element.dataset.threadNumber).to.be.undefined; + dialog.setup([annotation], {}); + expect(dialog.element).not.toBeNull(); + expect(dialog.bindDOMListeners).toBeCalled(); + expect(dialog.threadEl).not.toBeUndefined(); }); it('should not create dialog element if using a mobile browser', () => { dialog.isMobile = true; - dialog.setup([stubs.annotation, stubs.annotation], {}); - expect(stubs.bind).to.not.be.called; - expect(stubs.addSorted).to.be.called; + dialog.setup([annotation, annotation], {}); + expect(dialog.bindDOMListeners).not.toBeCalled(); + expect(dialog.addSortedAnnotations).toBeCalled(); dialog.element = null; }); }); @@ -395,176 +380,164 @@ describe('AnnotationDialog', () => { }; dialog.addSortedAnnotations(annotations); - const annotationContainerEl = dialog.dialogEl.querySelector(`.${CLASS_COMMENTS_CONTAINER}`); - expect(annotationContainerEl.childNodes[0]).to.have.attr('data-annotation-id', '1'); - expect(annotationContainerEl.childNodes[1]).to.have.attr('data-annotation-id', '3'); - expect(annotationContainerEl.childNodes[2]).to.have.attr('data-annotation-id', '2'); + const annotationContainerEl = dialog.dialogEl.querySelector(SELECTOR_COMMENTS_CONTAINER); + expect(annotationContainerEl.childNodes[0].dataset.annotationId).toEqual('1'); + expect(annotationContainerEl.childNodes[1].dataset.annotationId).toEqual('3'); + expect(annotationContainerEl.childNodes[2].dataset.annotationId).toEqual('2'); }); }); describe('bindDOMListeners()', () => { beforeEach(() => { - stubs.replyTextEl = dialog.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - stubs.annotationTextEl = dialog.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); - stubs.add = sandbox.stub(dialog.element, 'addEventListener'); - sandbox.stub(stubs.replyTextEl, 'addEventListener'); - sandbox.stub(stubs.annotationTextEl, 'addEventListener'); + dialog.postReplyTextEl = dialog.element.querySelector(SELECTOR_REPLY_TEXTAREA); + dialog.annotationTextEl = dialog.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); + dialog.element.addEventListener = jest.fn(); + dialog.postReplyTextEl.addEventListener = jest.fn(); + dialog.annotationTextEl.addEventListener = jest.fn(); }); it('should bind ALL DOM listeners for touch enabled laptops', () => { dialog.hasTouch = true; dialog.bindDOMListeners(); - expect(stubs.add).to.be.calledWith('keydown', sinon.match.func); - expect(stubs.add).to.be.calledWith('click', sinon.match.func); - expect(stubs.add).to.be.calledWith('mouseup', sinon.match.func); - expect(stubs.add).to.be.calledWith('wheel', sinon.match.func); - expect(stubs.add).to.be.calledWith('touchstart', dialog.clickHandler); - expect(stubs.add).to.be.calledWith('touchstart', dialog.stopPropagation); - expect(stubs.replyTextEl.addEventListener).to.be.calledWith('focus', sinon.match.func); - expect(stubs.annotationTextEl.addEventListener).to.be.calledWith('focus', sinon.match.func); + expect(dialog.element.addEventListener).toBeCalledWith('keydown', expect.any(Function)); + expect(dialog.element.addEventListener).toBeCalledWith('click', expect.any(Function)); + expect(dialog.element.addEventListener).toBeCalledWith('mouseup', expect.any(Function)); + expect(dialog.element.addEventListener).toBeCalledWith('wheel', expect.any(Function)); + expect(dialog.element.addEventListener).toBeCalledWith('touchstart', dialog.clickHandler); + expect(dialog.element.addEventListener).toBeCalledWith('touchstart', dialog.stopPropagation); + expect(dialog.postReplyTextEl.addEventListener).toBeCalledWith('focus', expect.any(Function)); + expect(dialog.annotationTextEl.addEventListener).toBeCalledWith('focus', expect.any(Function)); }); it('should not bind touch events if not on a touch enabled devices', () => { dialog.bindDOMListeners(); - expect(stubs.add).to.be.calledWith('keydown', sinon.match.func); - expect(stubs.add).to.be.calledWith('click', sinon.match.func); - expect(stubs.add).to.be.calledWith('mouseup', sinon.match.func); - expect(stubs.add).to.be.calledWith('wheel', sinon.match.func); - expect(stubs.add).to.not.be.calledWith('touchstart', dialog.clickHandler); - expect(stubs.add).to.not.be.calledWith('touchstart', dialog.stopPropagation); + expect(dialog.element.addEventListener).toBeCalledWith('keydown', expect.any(Function)); + expect(dialog.element.addEventListener).toBeCalledWith('click', expect.any(Function)); + expect(dialog.element.addEventListener).toBeCalledWith('mouseup', expect.any(Function)); + expect(dialog.element.addEventListener).toBeCalledWith('wheel', expect.any(Function)); + expect(dialog.element.addEventListener).not.toBeCalledWith('touchstart', dialog.clickHandler); + expect(dialog.element.addEventListener).not.toBeCalledWith('touchstart', dialog.stopPropagation); dialog.element = null; }); it('should not bind mouseenter/leave events for mobile browsers', () => { dialog.isMobile = true; - dialog.showMobileDialog(); - stubs.add = sandbox.stub(dialog.element, 'addEventListener'); dialog.bindDOMListeners(); - expect(stubs.add).to.be.calledWith('keydown', sinon.match.func); - expect(stubs.add).to.be.calledWith('mouseup', sinon.match.func); - expect(stubs.add).to.not.be.calledWith('click', sinon.match.func); - expect(stubs.add).to.be.calledWith('wheel', sinon.match.func); + expect(dialog.element.addEventListener).toBeCalledWith('keydown', expect.any(Function)); + expect(dialog.element.addEventListener).toBeCalledWith('mouseup', expect.any(Function)); + expect(dialog.element.addEventListener).not.toBeCalledWith('click', expect.any(Function)); + expect(dialog.element.addEventListener).toBeCalledWith('wheel', expect.any(Function)); dialog.element = null; }); }); describe('validateTextArea()', () => { - it('should do nothing if keyboard event was not in a textarea', () => { - stubs.textEl = document.createElement('div'); - stubs.textEl.classList.add(constants.CLASS_INVALID_INPUT); - dialog.validateTextArea({ target: stubs.textEl }); - expect(stubs.textEl).to.have.class(constants.CLASS_INVALID_INPUT); - }); + const textEl = document.createElement('textarea'); it('should do nothing if textarea is blank', () => { - stubs.textEl = document.createElement('textarea'); - stubs.textEl.classList.add(constants.CLASS_INVALID_INPUT); - dialog.validateTextArea({ target: stubs.textEl }); - expect(stubs.textEl).to.have.class(constants.CLASS_INVALID_INPUT); + textEl.classList.add(constants.CLASS_INVALID_INPUT); + dialog.validateTextArea({ target: textEl }); + expect(textEl.classList).toContain(constants.CLASS_INVALID_INPUT); }); it('should remove red border around textarea', () => { - stubs.textEl = document.createElement('textarea'); - stubs.textEl.classList.add(constants.CLASS_INVALID_INPUT); - stubs.textEl.value = 'words'; - dialog.validateTextArea({ target: stubs.textEl }); - expect(stubs.textEl).to.not.have.class(constants.CLASS_INVALID_INPUT); + textEl.classList.add(constants.CLASS_INVALID_INPUT); + textEl.value = 'words'; + dialog.validateTextArea({ target: textEl }); + expect(textEl.classList).not.toContain(constants.CLASS_INVALID_INPUT); }); }); describe('unbindDOMListeners()', () => { beforeEach(() => { - stubs.replyTextEl = dialog.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - stubs.annotationTextEl = dialog.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); - stubs.remove = sandbox.stub(dialog.element, 'removeEventListener'); - sandbox.stub(stubs.replyTextEl, 'removeEventListener'); - sandbox.stub(stubs.annotationTextEl, 'removeEventListener'); + dialog.postReplyTextEl = dialog.element.querySelector(SELECTOR_REPLY_TEXTAREA); + dialog.annotationTextEl = dialog.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); + dialog.element.removeEventListener = jest.fn(); + dialog.postReplyTextEl.removeEventListener = jest.fn(); + dialog.annotationTextEl.removeEventListener = jest.fn(); }); it('should unbind ALL DOM listeners for touch enabled laptops', () => { dialog.hasTouch = true; dialog.unbindDOMListeners(); - expect(stubs.remove).to.be.calledWith('keydown', sinon.match.func); - expect(stubs.remove).to.be.calledWith('click', sinon.match.func); - expect(stubs.remove).to.be.calledWith('mouseup', sinon.match.func); - expect(stubs.remove).to.be.calledWith('touchstart', dialog.clickHandler); - expect(stubs.remove).to.be.calledWith('touchstart', dialog.stopPropagation); - expect(stubs.remove).to.be.calledWith('wheel', sinon.match.func); - expect(stubs.replyTextEl.removeEventListener).to.be.calledWith('focus', dialog.validateTextArea); - expect(stubs.annotationTextEl.removeEventListener).to.be.calledWith('focus', dialog.validateTextArea); + expect(dialog.element.removeEventListener).toBeCalledWith('keydown', expect.any(Function)); + expect(dialog.element.removeEventListener).toBeCalledWith('click', expect.any(Function)); + expect(dialog.element.removeEventListener).toBeCalledWith('mouseup', expect.any(Function)); + expect(dialog.element.removeEventListener).toBeCalledWith('touchstart', dialog.clickHandler); + expect(dialog.element.removeEventListener).toBeCalledWith('touchstart', dialog.stopPropagation); + expect(dialog.element.removeEventListener).toBeCalledWith('wheel', expect.any(Function)); + expect(dialog.postReplyTextEl.removeEventListener).toBeCalledWith('focus', dialog.validateTextArea); + expect(dialog.annotationTextEl.removeEventListener).toBeCalledWith('focus', dialog.validateTextArea); }); it('should not bind touch events if not on a touch enabled devices', () => { dialog.unbindDOMListeners(); - expect(stubs.remove).to.be.calledWith('keydown', sinon.match.func); - expect(stubs.remove).to.be.calledWith('click', sinon.match.func); - expect(stubs.remove).to.be.calledWith('mouseup', sinon.match.func); - expect(stubs.remove).to.be.calledWith('wheel', sinon.match.func); - expect(stubs.remove).to.not.be.calledWith('touchstart', dialog.clickHandler); - expect(stubs.remove).to.not.be.calledWith('touchstart', dialog.stopPropagation); + expect(dialog.element.removeEventListener).toBeCalledWith('keydown', expect.any(Function)); + expect(dialog.element.removeEventListener).toBeCalledWith('click', expect.any(Function)); + expect(dialog.element.removeEventListener).toBeCalledWith('mouseup', expect.any(Function)); + expect(dialog.element.removeEventListener).toBeCalledWith('wheel', expect.any(Function)); + expect(dialog.element.removeEventListener).not.toBeCalledWith('touchstart', dialog.clickHandler); + expect(dialog.element.removeEventListener).not.toBeCalledWith('touchstart', dialog.stopPropagation); dialog.element = null; }); it('should not bind mouseenter/leave events for mobile browsers', () => { dialog.isMobile = true; - dialog.showMobileDialog(); - stubs.remove = sandbox.stub(dialog.element, 'removeEventListener'); dialog.unbindDOMListeners(); - expect(stubs.remove).to.be.calledWith('keydown', sinon.match.func); - expect(stubs.remove).to.be.calledWith('mouseup', sinon.match.func); - expect(stubs.remove).to.not.be.calledWith('click', sinon.match.func); - expect(stubs.remove).to.be.calledWith('wheel', sinon.match.func); + expect(dialog.element.removeEventListener).toBeCalledWith('keydown', expect.any(Function)); + expect(dialog.element.removeEventListener).toBeCalledWith('mouseup', expect.any(Function)); + expect(dialog.element.removeEventListener).not.toBeCalledWith('click', expect.any(Function)); + expect(dialog.element.removeEventListener).toBeCalledWith('wheel', expect.any(Function)); }); }); describe('keydownHandler()', () => { it('should cancel any unsaved annotations when user presses Esc on pending dialog', () => { - stubs.cancelAnnotation = sandbox.stub(dialog, 'cancelAnnotation'); + dialog.cancelAnnotation = jest.fn(); dialog.hasAnnotations = false; dialog.keydownHandler({ key: 'U+001B', // esc key - stopPropagation: () => {} + stopPropagation: jest.fn() }); - expect(stubs.cancelAnnotation).to.be.called; + expect(dialog.cancelAnnotation).toBeCalled(); }); it('should hide the dialog when user presses Esc if not creating a new annotation', () => { - stubs.hide = sandbox.stub(dialog, 'hide'); + dialog.hide = jest.fn(); dialog.hasAnnotations = true; dialog.keydownHandler({ key: 'U+001B', // esc key - stopPropagation: () => {} + stopPropagation: jest.fn() }); - expect(stubs.hide).to.be.called; + expect(dialog.hide).toBeCalled(); }); it('should scroll to the bottom area when user presses a key inside the reply area', () => { - stubs.scrollToLastComment = sandbox.stub(dialog, 'scrollToLastComment'); + dialog.scrollToLastComment = jest.fn(); dialog.keydownHandler({ key: ' ', // space - target: dialog.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`), - stopPropagation: () => {} + target: dialog.element.querySelector(SELECTOR_REPLY_TEXTAREA), + stopPropagation: jest.fn() }); - expect(stubs.scrollToLastComment).to.be.called; + expect(dialog.scrollToLastComment).toBeCalled(); }); }); describe('stopPropagation()', () => { it('should stop propagation on the event', () => { const event = { - stopPropagation: () => {} + stopPropagation: jest.fn() }; - stubs.stop = sandbox.stub(event, 'stopPropagation'); dialog.stopPropagation(event); - expect(stubs.stop).to.be.called; + expect(event.stopPropagation).toBeCalled(); }); }); @@ -579,17 +552,17 @@ describe('AnnotationDialog', () => { // Add buttons const btn = document.createElement('button'); btn.classList.add(constants.CLASS_DISABLED); - sandbox.stub(annotationEl, 'querySelectorAll').returns([btn, btn]); + annotationEl.querySelectorAll = jest.fn().mockReturnValue([btn, btn]); const wrongEl = document.createElement('div'); wrongEl.setAttribute('data-annotation-id', 'invalid'); - sandbox.stub(wrongEl, 'querySelectorAll'); + wrongEl.querySelectorAll = jest.fn(); dialog.element.appendChild(wrongEl); dialog.enable('123'); - expect(annotationEl.querySelectorAll).to.be.called; - expect(btn).to.not.have.class(constants.CLASS_DISABLED); - expect(wrongEl.querySelectorAll).to.not.be.called; + expect(annotationEl.querySelectorAll).toBeCalled(); + expect(btn.classList).not.toContain(constants.CLASS_DISABLED); + expect(wrongEl.querySelectorAll).not.toBeCalled(); }); }); @@ -603,140 +576,149 @@ describe('AnnotationDialog', () => { // Add buttons const btn = document.createElement('button'); - sandbox.stub(annotationEl, 'querySelectorAll').returns([btn, btn]); + annotationEl.querySelectorAll = jest.fn().mockReturnValue([btn, btn]); const wrongEl = document.createElement('div'); wrongEl.setAttribute('data-annotation-id', 'invalid'); - sandbox.stub(wrongEl, 'querySelectorAll'); + wrongEl.querySelectorAll = jest.fn(); dialog.element.appendChild(wrongEl); dialog.disable('123'); - expect(annotationEl.querySelectorAll).to.be.called; - expect(btn).to.have.class(constants.CLASS_DISABLED); - expect(wrongEl.querySelectorAll).to.not.be.called; + expect(annotationEl.querySelectorAll).toBeCalled(); + expect(btn.classList).toContain(constants.CLASS_DISABLED); + expect(wrongEl.querySelectorAll).not.toBeCalled(); }); }); describe('clickHandler()', () => { + let event; + beforeEach(() => { - stubs.event = { - stopPropagation: sandbox.stub(), - preventDefault: sandbox.stub(), + event = { + stopPropagation: jest.fn(), + preventDefault: jest.fn(), target: document.createElement('div') }; - stubs.post = sandbox.stub(dialog, 'postAnnotation'); - stubs.cancel = sandbox.stub(dialog, 'cancelAnnotation'); - stubs.deactivate = sandbox.stub(dialog, 'deactivateReply'); - stubs.activate = sandbox.stub(dialog, 'activateReply'); - stubs.findClosest = sandbox.stub(util, 'findClosestDataType'); - stubs.showDelete = sandbox.stub(dialog, 'showDeleteConfirmation'); - stubs.hideDelete = sandbox.stub(dialog, 'hideDeleteConfirmation'); - stubs.delete = sandbox.stub(dialog, 'deleteAnnotation'); - stubs.reply = sandbox.stub(dialog, 'postReply'); - stubs.hideMobile = sandbox.stub(dialog, 'hideMobileDialog'); - dialog.isMobile = false; + dialog.postAnnotation = jest.fn(); + dialog.cancelAnnotation = jest.fn(); + dialog.deactivateReply = jest.fn(); + dialog.activateReply = jest.fn(); + util.findClosestDataType = jest.fn(); + dialog.showDeleteConfirmation = jest.fn(); + dialog.hideDeleteConfirmation = jest.fn(); + dialog.deleteAnnotation = jest.fn(); + dialog.postReply = jest.fn(); + dialog.hideMobileDialog = jest.fn(); + + dialog.isMobile = false; dialog.element.classList.remove(constants.CLASS_HIDDEN); }); it('should only stop propagation on a desktop device', () => { - dialog.clickHandler(stubs.event); - expect(stubs.event.stopPropagation).to.be.called; - expect(stubs.event.preventDefault).to.not.be.called; + dialog.clickHandler(event); + expect(event.stopPropagation).toBeCalled(); + expect(event.preventDefault).not.toBeCalled(); }); it('should only stop propagation on a mobile device', () => { dialog.isMobile = true; - dialog.clickHandler(stubs.event); - expect(stubs.event.stopPropagation).to.be.called; - expect(stubs.event.preventDefault).to.not.be.called; + dialog.clickHandler(event); + expect(event.stopPropagation).toBeCalled(); + expect(event.preventDefault).not.toBeCalled(); }); it('should only prevent default on button clicks for mobile devices', () => { - stubs.event.target = document.createElement('button'); + event.target = document.createElement('button'); dialog.isMobile = true; - dialog.clickHandler(stubs.event); - expect(stubs.event.stopPropagation).to.be.called; - expect(stubs.event.preventDefault).to.be.called; + dialog.clickHandler(event); + expect(event.stopPropagation).toBeCalled(); + expect(event.preventDefault).toBeCalled(); }); it('should post annotation when post annotation button is clicked', () => { - stubs.findClosest.returns(constants.DATA_TYPE_POST); + util.findClosestDataType = jest.fn().mockReturnValue(constants.DATA_TYPE_POST); - dialog.clickHandler(stubs.event); - expect(stubs.post).to.be.called; + dialog.clickHandler(event); + expect(dialog.postAnnotation).toBeCalled(); }); it('should cancel annotation when cancel annotation button is clicked', () => { - stubs.findClosest.returns(constants.DATA_TYPE_CANCEL); - dialog.clickHandler(stubs.event); - expect(stubs.cancel).to.be.called; + util.findClosestDataType = jest.fn().mockReturnValue(constants.DATA_TYPE_CANCEL); + dialog.clickHandler(event); + expect(dialog.cancelAnnotation).toBeCalled(); }); it('should cancel annotation when mobile dialog close button is clicked', () => { - stubs.findClosest.returns(constants.DATA_TYPE_MOBILE_CLOSE); - dialog.clickHandler(stubs.event); - expect(stubs.cancel).to.be.called; + util.findClosestDataType = jest.fn().mockReturnValue(constants.DATA_TYPE_MOBILE_CLOSE); + dialog.clickHandler(event); + expect(dialog.cancelAnnotation).toBeCalled(); }); it('should activate reply area when textarea is clicked', () => { - stubs.findClosest.returns(constants.DATA_TYPE_REPLY_TEXTAREA); + util.findClosestDataType = jest.fn().mockReturnValue(constants.DATA_TYPE_REPLY_TEXTAREA); - dialog.clickHandler(stubs.event); - expect(stubs.activate).to.be.called; + dialog.clickHandler(event); + expect(dialog.activateReply).toBeCalled(); }); it('should deactivate reply area when cancel reply button is clicked', () => { - stubs.findClosest.returns(constants.DATA_TYPE_CANCEL_REPLY); + util.findClosestDataType = jest.fn().mockReturnValue(constants.DATA_TYPE_CANCEL_REPLY); - dialog.clickHandler(stubs.event); - expect(stubs.deactivate).to.be.calledWith(true); + dialog.clickHandler(event); + expect(dialog.deactivateReply).toBeCalledWith(true); }); it('should post reply when post reply button is clicked', () => { - stubs.findClosest.returns(constants.DATA_TYPE_POST_REPLY); + util.findClosestDataType = jest.fn().mockReturnValue(constants.DATA_TYPE_POST_REPLY); - dialog.clickHandler(stubs.event); - expect(stubs.reply).to.be.called; + dialog.clickHandler(event); + expect(dialog.postReply).toBeCalled(); }); it('should show delete confirmation when delete button is clicked', () => { - stubs.findClosest.onFirstCall().returns(constants.DATA_TYPE_DELETE); - stubs.findClosest.onSecondCall().returns('someID'); + util.findClosestDataType = jest + .fn() + .mockReturnValueOnce(constants.DATA_TYPE_DELETE) + .mockReturnValue('someID'); - dialog.clickHandler(stubs.event); - expect(stubs.showDelete).to.be.calledWith('someID'); + dialog.clickHandler(event); + expect(dialog.showDeleteConfirmation).toBeCalledWith('someID'); }); it('should cancel deletion when cancel delete button is clicked', () => { - stubs.findClosest.onFirstCall().returns(constants.DATA_TYPE_CANCEL_DELETE); - stubs.findClosest.onSecondCall().returns('someID'); + util.findClosestDataType = jest + .fn() + .mockReturnValueOnce(constants.DATA_TYPE_CANCEL_DELETE) + .mockReturnValue('someID'); - dialog.clickHandler(stubs.event); - expect(stubs.hideDelete).to.be.calledWith('someID'); + dialog.clickHandler(event); + expect(dialog.hideDeleteConfirmation).toBeCalledWith('someID'); }); it('should confirm deletion when confirm delete button is clicked', () => { - stubs.findClosest.onFirstCall().returns(constants.DATA_TYPE_CONFIRM_DELETE); - stubs.findClosest.onSecondCall().returns('someID'); + util.findClosestDataType = jest + .fn() + .mockReturnValueOnce(constants.DATA_TYPE_CONFIRM_DELETE) + .mockReturnValue('someID'); - dialog.clickHandler(stubs.event); - expect(stubs.delete).to.be.calledWith('someID'); + dialog.clickHandler(event); + expect(dialog.deleteAnnotation).toBeCalledWith('someID'); }); it('should do nothing if dataType does not match any button in the annotation dialog', () => { - stubs.findClosest.returns(null); - - dialog.clickHandler(stubs.event); - expect(stubs.post).to.not.be.called; - expect(stubs.reply).to.not.be.called; - expect(stubs.cancel).to.not.be.called; - expect(stubs.deactivate).to.not.be.called; - expect(stubs.activate).to.not.be.called; - expect(stubs.reply).to.not.be.called; - expect(stubs.showDelete).to.not.be.called; - expect(stubs.hideDelete).to.not.be.called; - expect(stubs.delete).to.not.be.called; + util.findClosestDataType = jest.fn().mockReturnValue(null); + + dialog.clickHandler(event); + expect(dialog.postAnnotation).not.toBeCalled(); + expect(dialog.postReply).not.toBeCalled(); + expect(dialog.cancelAnnotation).not.toBeCalled(); + expect(dialog.deactivateReply).not.toBeCalled(); + expect(dialog.activateReply).not.toBeCalled(); + expect(dialog.postReply).not.toBeCalled(); + expect(dialog.showDeleteConfirmation).not.toBeCalled(); + expect(dialog.hideDeleteConfirmation).not.toBeCalled(); + expect(dialog.deleteAnnotation).not.toBeCalled(); }); }); @@ -751,7 +733,7 @@ describe('AnnotationDialog', () => { }) ); const annotationComment = document.querySelector(constants.SELECTOR_ANNOTATION_COMMENT_TEXT); - expect(annotationComment).to.contain.html('the preview sdk is awesome!'); + expect(annotationComment.textContent).toContain('the preview sdk is awesome!'); }); it('should display the posting message if the user id is 0', () => { @@ -764,7 +746,7 @@ describe('AnnotationDialog', () => { }) ); const username = document.querySelector(constants.SELECTOR_USER_NAME); - expect(username).to.contain.html(dialog.localized.posting); + expect(username.textContent).toContain(dialog.localized.posting); }); it('should display user name if the user id is not 0', () => { @@ -777,7 +759,7 @@ describe('AnnotationDialog', () => { }) ); const username = document.querySelector(constants.SELECTOR_USER_NAME); - expect(username).to.contain.html('user'); + expect(username.textContent).toContain('user'); }); it('should not the delete icon if the user does not have delete permissions', () => { @@ -789,8 +771,8 @@ describe('AnnotationDialog', () => { permissions: { can_delete: false } }) ); - const deleteButton = document.querySelector(`.${CLASS_BUTTON_DELETE_COMMENT}`); - expect(deleteButton).to.be.null; + const deleteButton = document.querySelector(SELECTOR_BUTTON_DELETE_COMMENT); + expect(deleteButton).toBeNull(); }); it('should not add the delete icon if the delete permission is not specified', () => { @@ -802,8 +784,8 @@ describe('AnnotationDialog', () => { permissions: {} }) ); - const deleteButton = document.querySelector(`.${CLASS_BUTTON_DELETE_COMMENT}`); - expect(deleteButton).to.be.null; + const deleteButton = document.querySelector(SELECTOR_BUTTON_DELETE_COMMENT); + expect(deleteButton).toBeNull(); }); it('should make delete icon visible if the user has delete permission', () => { @@ -815,8 +797,8 @@ describe('AnnotationDialog', () => { permissions: { can_delete: true } }) ); - const deleteButton = document.querySelector(`.${CLASS_BUTTON_DELETE_COMMENT}`); - expect(deleteButton).to.not.have.class(constants.CLASS_HIDDEN); + const deleteButton = document.querySelector(SELECTOR_BUTTON_DELETE_COMMENT); + expect(deleteButton.classList).not.toContain(constants.CLASS_HIDDEN); }); it('should hide the delete confirmation UI by default', () => { @@ -829,24 +811,7 @@ describe('AnnotationDialog', () => { }) ); const deleteConfirmation = document.querySelector(SELECTOR_DELETE_CONFIRMATION); - expect(deleteConfirmation).to.have.class(constants.CLASS_HIDDEN); - }); - - it('should correctly format the date and time in a different locale', () => { - const date = new Date(); - stubs.locale = sandbox.stub(Date.prototype, 'toLocaleString'); - dialog.locale = 'en-GB'; - - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true }, - created: date - }) - ); - expect(stubs.locale).to.be.calledWith('en-GB'); + expect(deleteConfirmation.classList).toContain(constants.CLASS_HIDDEN); }); it('should add a
for each newline', () => { @@ -864,7 +829,7 @@ describe('AnnotationDialog', () => { }) ); const breaks = document.querySelectorAll(`${constants.SELECTOR_ANNOTATION_COMMENT_TEXT} br`); - expect(breaks.length === 3).to.be.true; + expect(breaks.length === 3).toBeTruthy(); }); it('should respect symbols added to the text', () => { @@ -878,8 +843,8 @@ describe('AnnotationDialog', () => { }) ); const annotationComment = document.querySelector(constants.SELECTOR_ANNOTATION_COMMENT_TEXT); - expect(annotationComment.textContent).to.equal(text); - expect(annotationComment.textContent.includes('&')).to.be.false; + expect(annotationComment.textContent).toEqual(text); + expect(annotationComment.textContent).toContain('&&&'); }); }); @@ -887,15 +852,15 @@ describe('AnnotationDialog', () => { it('should not post an annotation to the dialog if it has no text', () => { const annotationTextEl = dialog.element.querySelector(constants.SELECTOR_ANNOTATION_TEXTAREA); dialog.postAnnotation(); - expect(stubs.emit).to.not.be.called; - expect(annotationTextEl).to.have.class(CLASS_INVALID_INPUT); + expect(dialog.emit).not.toBeCalled(); + expect(annotationTextEl.classList).toContain(CLASS_INVALID_INPUT); }); it('should post an annotation to the dialog if it has text', () => { document.querySelector('textarea').innerHTML += 'the preview SDK is great!'; dialog.postAnnotation(); - expect(stubs.emit).to.be.calledWith('annotationcreate', { text: 'the preview SDK is great!' }); + expect(dialog.emit).toBeCalledWith('annotationcreate', { text: 'the preview SDK is great!' }); }); it('should clear the annotation text element after posting', () => { @@ -903,165 +868,115 @@ describe('AnnotationDialog', () => { annotationTextEl.innerHTML += 'the preview SDK is great!'; dialog.postAnnotation(); - expect(annotationTextEl).to.have.value(''); + expect(annotationTextEl.value).toEqual(''); }); }); describe('cancelAnnotation()', () => { it('should emit the annotationcancel message', () => { dialog.cancelAnnotation(); - expect(stubs.emit).to.be.calledWith('annotationcancel'); + expect(dialog.emit).toBeCalledWith('annotationcancel'); }); }); describe('activateReply()', () => { it('should do nothing if the dialogEl does not exist', () => { dialog.dialogEl = null; - sandbox.stub(util, 'showElement'); dialog.activateReply(); - expect(util.showElement).to.not.be.called; + expect(util.showElement).not.toBeCalled(); }); it('should do nothing if reply textarea is already active', () => { - const replyTextEl = dialog.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`); + const replyTextEl = dialog.element.querySelector(SELECTOR_REPLY_TEXTAREA); replyTextEl.classList.add(constants.CLASS_ACTIVE); - sandbox.stub(util, 'showElement'); - dialog.activateReply(); - expect(util.showElement).to.not.be.called; + expect(util.showElement).not.toBeCalled(); }); it('should do nothing if reply text area does not exist', () => { - const replyTextEl = dialog.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`); + const replyTextEl = dialog.element.querySelector(SELECTOR_REPLY_TEXTAREA); replyTextEl.parentNode.removeChild(replyTextEl); - sandbox.stub(util, 'showElement'); - dialog.activateReply(); - expect(util.showElement).to.not.be.called; + expect(util.showElement).not.toBeCalled(); }); it('should show the correct UI when the reply textarea is activated', () => { - document.querySelector('textarea').innerHTML += 'the preview SDK is great!'; - dialog.addAnnotationElement({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }); - const replyTextEl = document.querySelector(`.${CLASS_REPLY_TEXTAREA}`); - const buttonContainer = replyTextEl.parentNode.querySelector(constants.SELECTOR_BUTTON_CONTAINER); + document.querySelector('textarea').textContent = 'the preview SDK is great!'; + dialog.addAnnotationElement(annotation); + const replyTextEl = document.querySelector(SELECTOR_REPLY_TEXTAREA); + replyTextEl.classList.remove(constants.CLASS_ACTIVE); dialog.activateReply(); - expect(replyTextEl).to.have.class(constants.CLASS_ACTIVE); - expect(buttonContainer).to.not.have.class(constants.CLASS_HIDDEN); + expect(replyTextEl.classList).toContain(constants.CLASS_ACTIVE); }); }); describe('deactivateReply()', () => { it('should do nothing if element does not exist', () => { dialog.dialogEl = null; - sandbox.stub(util, 'resetTextarea'); + util.resetTextarea = jest.fn(); dialog.deactivateReply(); - expect(util.resetTextarea).to.not.be.called; + expect(util.resetTextarea).not.toBeCalled(); }); it('should do nothing if reply text area does not exist', () => { - const replyTextEl = dialog.element.querySelector(`.${CLASS_REPLY_CONTAINER}`); + const replyTextEl = dialog.element.querySelector(SELECTOR_REPLY_CONTAINER); replyTextEl.parentNode.removeChild(replyTextEl); - sandbox.stub(util, 'showElement'); - dialog.deactivateReply(); - expect(util.showElement).to.not.be.called; + expect(util.showElement).not.toBeCalled(); }); it('should show the correct UI when the reply textarea is deactivated', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const replyTextEl = document.querySelector(`.${CLASS_REPLY_TEXTAREA}`); + dialog.addAnnotationElement(annotation); + const replyTextEl = document.querySelector(SELECTOR_REPLY_TEXTAREA); const buttonContainer = replyTextEl.parentNode.querySelector(constants.SELECTOR_BUTTON_CONTAINER); dialog.deactivateReply(); - expect(replyTextEl).to.not.have.class(constants.CLASS_ACTIVE); - expect(buttonContainer).to.have.class(constants.CLASS_HIDDEN); + expect(replyTextEl.classList).not.toContain(constants.CLASS_ACTIVE); + expect(buttonContainer.classList).toContain(constants.CLASS_HIDDEN); }); }); describe('postReply()', () => { it('should not post reply to the dialog if it has no text', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); + dialog.addAnnotationElement(annotation); dialog.activateReply(); - const replyTextEl = dialog.element.querySelector(`.${CLASS_REPLY_TEXTAREA}`); + const replyTextEl = dialog.element.querySelector(SELECTOR_REPLY_TEXTAREA); dialog.postReply(); - expect(stubs.emit).to.not.be.called; - expect(replyTextEl).to.have.class(CLASS_INVALID_INPUT); + expect(dialog.emit).not.toBeCalled(); + expect(replyTextEl.classList).toContain(CLASS_INVALID_INPUT); }); it('should post a reply to the dialog if it has text', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const replyTextEl = document.querySelector(`.${CLASS_REPLY_TEXTAREA}`); + dialog.addAnnotationElement(annotation); + const replyTextEl = document.querySelector(SELECTOR_REPLY_TEXTAREA); dialog.activateReply(); replyTextEl.innerHTML += 'the preview SDK is great!'; dialog.postReply(); - expect(stubs.emit).to.be.calledWith('annotationcreate', { text: 'the preview SDK is great!' }); + expect(dialog.emit).toBeCalledWith('annotationcreate', { text: 'the preview SDK is great!' }); }); it('should clear the reply text element after posting', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const replyTextEl = document.querySelector(`.${CLASS_REPLY_TEXTAREA}`); + dialog.addAnnotationElement(annotation); + const replyTextEl = document.querySelector(SELECTOR_REPLY_TEXTAREA); dialog.activateReply(); replyTextEl.innerHTML += 'the preview SDK is great!'; - sandbox.stub(replyTextEl, 'focus'); + replyTextEl.focus = jest.fn(); dialog.postReply(); - expect(replyTextEl).to.have.value(''); - expect(replyTextEl.focus).to.be.called; + expect(replyTextEl.value).toEqual(''); + expect(replyTextEl.focus).toBeCalled(); }); }); describe('showDeleteConfirmation()', () => { it('should show the correct UI when a user clicks on delete', () => { - dialog.addAnnotationElement( - new Annotation({ - annotationID: 1, - text: 'the preview sdk is amazing!', - user: { id: 1, name: 'user' }, - permissions: { can_delete: true } - }) - ); - const showElementStub = sandbox.stub(util, 'showElement'); - + dialog.addAnnotationElement(annotation); dialog.showDeleteConfirmation(1); - expect(showElementStub).to.be.called; + expect(util.showElement).toBeCalled(); }); }); @@ -1075,11 +990,10 @@ describe('AnnotationDialog', () => { permissions: { can_delete: true } }) ); - const hideElementStub = sandbox.stub(util, 'hideElement'); dialog.showDeleteConfirmation(1); dialog.hideDeleteConfirmation(1); - expect(hideElementStub).to.be.called; + expect(util.hideElement).toBeCalled(); }); }); @@ -1095,7 +1009,7 @@ describe('AnnotationDialog', () => { ); dialog.deleteAnnotation(1); - expect(stubs.emit).to.be.calledWith('annotationdelete', { annotationID: 1 }); + expect(dialog.emit).toBeCalledWith('annotationdelete', { annotationID: 1 }); }); }); @@ -1104,16 +1018,16 @@ describe('AnnotationDialog', () => { const dialogEl = dialog.generateDialogEl(0); const createSectionEl = dialogEl.querySelector(constants.SECTION_CREATE); const showSectionEl = dialogEl.querySelector(constants.SECTION_SHOW); - expect(createSectionEl).to.not.have.class(constants.CLASS_HIDDEN); - expect(showSectionEl).to.have.class(constants.CLASS_HIDDEN); + expect(createSectionEl.classList).not.toContain(constants.CLASS_HIDDEN); + expect(showSectionEl.classList).toContain(constants.CLASS_HIDDEN); }); it('should generate an annotations dialog element with annotations', () => { const dialogEl = dialog.generateDialogEl(1); const createSectionEl = dialogEl.querySelector(constants.SECTION_CREATE); const showSectionEl = dialogEl.querySelector(constants.SECTION_SHOW); - expect(createSectionEl).to.have.class(constants.CLASS_HIDDEN); - expect(showSectionEl).to.not.have.class(constants.CLASS_HIDDEN); + expect(createSectionEl.classList).toContain(constants.CLASS_HIDDEN); + expect(showSectionEl.classList).not.toContain(constants.CLASS_HIDDEN); }); it('should not add the create section nor the reply container in read-only mode', () => { @@ -1121,11 +1035,11 @@ describe('AnnotationDialog', () => { const dialogEl = dialog.generateDialogEl(1); const createSectionEl = dialogEl.querySelector(constants.SECTION_CREATE); - const replyContainerEl = dialogEl.querySelector(`.${CLASS_REPLY_CONTAINER}`); + const replyContainerEl = dialogEl.querySelector(SELECTOR_REPLY_CONTAINER); const showSectionEl = dialogEl.querySelector(constants.SECTION_SHOW); - expect(createSectionEl).to.be.null; - expect(replyContainerEl).to.be.null; - expect(showSectionEl).to.not.have.class(constants.CLASS_HIDDEN); + expect(createSectionEl).toBeNull(); + expect(replyContainerEl).toBeNull(); + expect(showSectionEl.classList).not.toContain(constants.CLASS_HIDDEN); }); }); @@ -1133,9 +1047,10 @@ describe('AnnotationDialog', () => { const containerHeight = 5; beforeEach(() => { - sandbox.stub(dialog.element, 'querySelector').returns(document.createElement('div')); - sandbox.stub(dialog, 'fitDialogHeightInPage'); - sandbox.stub(dialog, 'toggleFlippedThreadEl'); + dialog.element = document.createElement('div'); + dialog.element.querySelector = jest.fn().mockReturnValue(document.createElement('div')); + dialog.fitDialogHeightInPage = jest.fn(); + dialog.toggleFlippedThreadEl = jest.fn(); }); afterEach(() => { @@ -1144,18 +1059,18 @@ describe('AnnotationDialog', () => { it('should keep the dialog below the annotation icon if the annotation is in the top half of the viewport', () => { const { top, bottom } = dialog.flipDialog(2, containerHeight); - expect(dialog.element).to.not.have.class(CLASS_FLIPPED_DIALOG); - expect(top).to.not.equal(''); - expect(bottom).to.equal(''); - expect(dialog.fitDialogHeightInPage).to.be.called; - expect(dialog.toggleFlippedThreadEl).to.be.called; + expect(dialog.element.classList).not.toContain(CLASS_FLIPPED_DIALOG); + expect(top).not.toEqual(''); + expect(bottom).toEqual(''); + expect(dialog.fitDialogHeightInPage).toBeCalled(); + expect(dialog.toggleFlippedThreadEl).toBeCalled(); }); it('should flip the dialog above the annotation icon if the annotation is in the lower half of the viewport', () => { const { top, bottom } = dialog.flipDialog(4, containerHeight); - expect(dialog.element).to.have.class(CLASS_FLIPPED_DIALOG); - expect(top).to.equal(''); - expect(bottom).to.not.equal(''); + expect(dialog.element.classList).toContain(CLASS_FLIPPED_DIALOG); + expect(top).toEqual(''); + expect(bottom).not.toEqual(''); }); }); @@ -1166,30 +1081,21 @@ describe('AnnotationDialog', () => { }); it('should do nothing if the dialog is not flipped', () => { - stubs.add = sandbox.stub(dialog.threadEl.classList, 'add'); - stubs.remove = sandbox.stub(dialog.threadEl.classList, 'remove'); dialog.toggleFlippedThreadEl(); - expect(stubs.add).to.not.be.called; - expect(stubs.remove).to.not.be.called; + expect(dialog.threadEl.classList).not.toContain(CLASS_FLIPPED_DIALOG); }); it('should reset thread icon if dialog is flipped and hidden', () => { dialog.element.classList.add(CLASS_FLIPPED_DIALOG); - stubs.add = sandbox.stub(dialog.threadEl.classList, 'add'); - stubs.remove = sandbox.stub(dialog.threadEl.classList, 'remove'); dialog.toggleFlippedThreadEl(); - expect(stubs.add).to.be.called; - expect(stubs.remove).to.not.be.called; + expect(dialog.threadEl.classList).toContain(CLASS_FLIPPED_DIALOG); }); it('should flip thread icon if dialog is flipped and not hidden', () => { dialog.element.classList.add(CLASS_FLIPPED_DIALOG); dialog.element.classList.add(constants.CLASS_HIDDEN); - stubs.add = sandbox.stub(dialog.threadEl.classList, 'add'); - stubs.remove = sandbox.stub(dialog.threadEl.classList, 'remove'); dialog.toggleFlippedThreadEl(); - expect(stubs.add).to.not.be.called; - expect(stubs.remove).to.be.called; + expect(dialog.threadEl.classList).not.toContain(CLASS_FLIPPED_DIALOG); }); }); @@ -1197,23 +1103,23 @@ describe('AnnotationDialog', () => { it('should allow scrolling on annotations dialog if file is a powerpoint', () => { dialog.dialogEl = { style: {}, - querySelector: sandbox.stub().returns(null) + querySelector: jest.fn().mockReturnValue(null) }; dialog.container = { clientHeight: 100 }; dialog.fitDialogHeightInPage(); - expect(dialog.dialogEl.style.maxHeight).to.equal('20px'); + expect(dialog.dialogEl.style.maxHeight).toEqual('20px'); }); it('should allow scrolling on annotations dialog if file is a powerpoint', () => { const commentsEl = document.createElement('div'); dialog.dialogEl = { style: {}, - querySelector: sandbox.stub().returns(commentsEl) + querySelector: jest.fn().mockReturnValue(commentsEl) }; dialog.container = { clientHeight: 100 }; dialog.fitDialogHeightInPage(); - expect(dialog.dialogEl.style.maxHeight).to.equal('20px'); - expect(commentsEl.style.maxHeight).to.equal('20px'); + expect(dialog.dialogEl.style.maxHeight).toEqual('20px'); + expect(commentsEl.style.maxHeight).toEqual('20px'); }); }); }); diff --git a/src/__tests__/AnnotationService-test.js b/src/__tests__/AnnotationService-test.js index 9c6d0244c..16cfb912c 100644 --- a/src/__tests__/AnnotationService-test.js +++ b/src/__tests__/AnnotationService-test.js @@ -7,21 +7,19 @@ import * as util from '../util'; const API_HOST = 'https://app.box.com/api'; let annotationService; -let sandbox; describe('AnnotationService', () => { beforeEach(() => { - sandbox = sinon.sandbox.create(); annotationService = new AnnotationService({ apiHost: API_HOST, fileId: 1, token: 'someToken', canAnnotate: true }); + annotationService.emit = jest.fn(); }); afterEach(() => { - sandbox.verifyAndRestore(); fetchMock.restore(); }); @@ -29,12 +27,12 @@ describe('AnnotationService', () => { it('should return a rfc4122v4-compliant GUID', () => { const GUID = AnnotationService.generateID(); const regex = /^[a-z0-9]{8}-[a-z0-9]{4}-4[a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$/i; - expect(GUID.match(regex).length).to.satisfy; + expect(GUID.match(regex).length).toBeGreaterThan(0); }); it('should (almost always) return unique GUIDs', () => { // eslint-disable-next-line no-self-compare - expect(AnnotationService.generateID() === AnnotationService.generateID()).to.be.false; + expect(AnnotationService.generateID() === AnnotationService.generateID()).toBeFalsy(); }); }); @@ -66,17 +64,16 @@ describe('AnnotationService', () => { created_by: {} } }); - const emitStub = sandbox.stub(annotationService, 'emit'); return annotationService.create(annotationToSave).then((createdAnnotation) => { - expect(createdAnnotation.fileVersionId).to.equal(annotationToSave.fileVersionId); - expect(createdAnnotation.threadID).to.equal(annotationToSave.threadID); - expect(createdAnnotation.threadNumber).to.equal(annotationToSave.threadNumber); - expect(createdAnnotation.type).to.equal(annotationToSave.type); - expect(createdAnnotation.text).to.equal(annotationToSave.text); - expect(createdAnnotation.location.x).to.equal(annotationToSave.location.x); - expect(createdAnnotation.location.y).to.equal(annotationToSave.location.y); - expect(emitStub).to.not.be.called; + expect(createdAnnotation.fileVersionId).toEqual(annotationToSave.fileVersionId); + expect(createdAnnotation.threadID).toEqual(annotationToSave.threadID); + expect(createdAnnotation.threadNumber).toEqual(annotationToSave.threadNumber); + expect(createdAnnotation.type).toEqual(annotationToSave.type); + expect(createdAnnotation.text).toEqual(annotationToSave.text); + expect(createdAnnotation.location.x).toEqual(annotationToSave.location.x); + expect(createdAnnotation.location.y).toEqual(annotationToSave.location.y); + expect(annotationService.emit).not.toBeCalled(); }); }); @@ -86,17 +83,16 @@ describe('AnnotationService', () => { type: 'error' } }); - const emitStub = sandbox.stub(annotationService, 'emit'); return annotationService.create(annotationToSave).then( () => { throw new Error('Annotation should not be returned'); }, (error) => { - expect(error.message).to.equal('Could not create annotation'); - expect(emitStub).to.be.calledWith('annotationerror', { + expect(error.message).toEqual('Could not create annotation'); + expect(annotationService.emit).toBeCalledWith('annotationerror', { reason: 'create', - error: sinon.match.string + error: expect.any(String) }); } ); @@ -104,9 +100,7 @@ describe('AnnotationService', () => { }); describe('read()', () => { - const url = `${ - API_HOST - }/2.0/files/1/annotations?version=2&fields=item,thread,details,message,created_by,created_at,modified_at,permissions`; + const url = `${API_HOST}/2.0/files/1/annotations?version=2&fields=item,thread,details,message,created_by,created_at,modified_at,permissions`; it('should return array of annotations for the specified file and file version', () => { const annotation1 = new Annotation({ @@ -163,15 +157,15 @@ describe('AnnotationService', () => { }); return annotationService.read(2).then((annotations) => { - expect(Object.keys(annotations).length).to.equal(2); + expect(Object.keys(annotations).length).toEqual(2); const firstAnnotationId = Object.keys(annotations)[0]; const createdAnnotation1 = annotations[firstAnnotationId]; - expect(createdAnnotation1.text).to.equal(annotation1.text); + expect(createdAnnotation1.text).toEqual(annotation1.text); const secondAnnotationId = Object.keys(annotations)[1]; const createdAnnotation2 = annotations[secondAnnotationId]; - expect(createdAnnotation2.text).to.equal(annotation2.text); + expect(createdAnnotation2.text).toEqual(annotation2.text); }); }); @@ -187,7 +181,7 @@ describe('AnnotationService', () => { throw new Error('Annotations should not be returned'); }, (error) => { - expect(error.message).to.equal('Could not read annotations from file version with ID 2'); + expect(error.message).toEqual('Could not read annotations from file version with ID 2'); } ); }); @@ -198,11 +192,9 @@ describe('AnnotationService', () => { it('should successfully delete the annotation', () => { fetchMock.mock(url, 204); - const emitStub = sandbox.stub(annotationService, 'emit'); - return annotationService.delete(3).then(() => { - expect(fetchMock.called(url)).to.be.true; - expect(emitStub).to.not.be.called; + expect(fetchMock.called(url)).toBeTruthy(); + expect(annotationService.emit).not.toBeCalled(); }); }); @@ -212,17 +204,16 @@ describe('AnnotationService', () => { type: 'error' } }); - const emitStub = sandbox.stub(annotationService, 'emit'); return annotationService.delete(3).then( () => { throw new Error('Annotation should not have been deleted'); }, (error) => { - expect(error.message).to.equal('Could not delete annotation with ID 3'); - expect(emitStub).to.be.calledWith('annotationerror', { + expect(error.message).toEqual('Could not delete annotation with ID 3'); + expect(annotationService.emit).toBeCalledWith('annotationerror', { reason: 'delete', - error: sinon.match.string + error: expect.any(String) }); } ); @@ -266,11 +257,11 @@ describe('AnnotationService', () => { 2: annotation2, 3: annotation3 }; - sandbox.stub(annotationService, 'read').returns(Promise.resolve(threads)); - sandbox.stub(annotationService, 'createThreadMap').returns(threads); + annotationService.read = jest.fn().mockResolvedValue(threads); + annotationService.createThreadMap = jest.fn().mockReturnValue(threads); return annotationService.getThreadMap(2).then(() => { - expect(annotationService.createThreadMap).to.be.called; + expect(annotationService.createThreadMap).toBeCalled(); }); }); }); @@ -319,12 +310,12 @@ describe('AnnotationService', () => { const threadMap = annotationService.createThreadMap([annotation1, annotation2, annotation3, annotation4]); - expect(Object.keys(threadMap[annotation1.threadID]).length).to.equal(3); + expect(Object.keys(threadMap[annotation1.threadID]).length).toEqual(3); const thread = threadMap[annotation1.threadID]; - expect(thread[1]).to.deep.equal(annotation1); - expect(thread[1].threadNumber).to.equal(annotation1.threadNumber); - expect(thread).to.not.contain(annotation2); + expect(thread[1]).toStrictEqual(annotation1); + expect(thread[1].threadNumber).toEqual(annotation1.threadNumber); + expect(thread).not.toContain(annotation2); }); }); @@ -344,7 +335,7 @@ describe('AnnotationService', () => { }; const annotation1 = annotationService.createAnnotation(data); - expect(annotation1 instanceof Annotation).to.be.true; + expect(annotation1 instanceof Annotation).toBeTruthy(); }); }); @@ -392,10 +383,10 @@ describe('AnnotationService', () => { annotationService.annotations = []; annotationService.readFromMarker(resolve, reject, 2, 'a', 1); promise.then((result) => { - expect(Object.keys(result).length).to.equal(1); + expect(Object.keys(result).length).toEqual(1); const firstAnnotation = util.getFirstAnnotation(result); - expect(firstAnnotation.text).to.equal(annotation2.text); - expect(firstAnnotation.threadNumber).to.equal(annotation2.threadNumber); + expect(firstAnnotation.text).toEqual(annotation2.text); + expect(firstAnnotation.threadNumber).toEqual(annotation2.threadNumber); }); }); @@ -407,7 +398,6 @@ describe('AnnotationService', () => { type: 'error' } }); - const emitStub = sandbox.stub(annotationService, 'emit'); let resolve; let reject; @@ -423,10 +413,10 @@ describe('AnnotationService', () => { throw new Error('Annotation should not have been deleted'); }, (error) => { - expect(error.message).to.equal('Could not read annotations from file version with ID 2'); - expect(emitStub).to.be.calledWith('annotationerror', { + expect(error.message).toEqual('Could not read annotations from file version with ID 2'); + expect(annotationService.emit).toBeCalledWith('annotationerror', { reason: 'read', - error: sinon.match.string + error: expect.any(String) }); } ); @@ -436,7 +426,6 @@ describe('AnnotationService', () => { const markerUrl = annotationService.getReadUrl(2, 'a', 1); fetchMock.mock(markerUrl, 401); - const emitStub = sandbox.stub(annotationService, 'emit'); let resolve; let reject; @@ -448,10 +437,10 @@ describe('AnnotationService', () => { annotationService.annotations = []; annotationService.readFromMarker(resolve, reject, 2, 'a', 1); return promise.catch((error) => { - expect(error.message).to.equal('Could not read annotations from file due to invalid or expired token'); - expect(emitStub).to.be.calledWith('annotationerror', { + expect(error.message).toEqual('Could not read annotations from file due to invalid or expired token'); + expect(annotationService.emit).toBeCalledWith('annotationerror', { reason: 'authorization', - error: sinon.match.string + error: expect.any(String) }); }); }); @@ -462,12 +451,12 @@ describe('AnnotationService', () => { annotationService.api = 'box'; annotationService.fileId = 1; const fileVersionId = 2; - const url = `${annotationService.api}/2.0/files/${annotationService.fileId}/annotations?version=${ - fileVersionId - }&fields=item,thread,details,message,created_by,created_at,modified_at,permissions`; + const url = `${annotationService.api}/2.0/files/${ + annotationService.fileId + }/annotations?version=${fileVersionId}&fields=item,thread,details,message,created_by,created_at,modified_at,permissions`; const result = annotationService.getReadUrl(fileVersionId); - expect(result).to.equal(url); + expect(result).toEqual(url); }); it('should add a marker and limit if provided', () => { @@ -476,14 +465,12 @@ describe('AnnotationService', () => { const fileVersionId = 2; const marker = 'next_annotation'; const limit = 1; - const url = `${annotationService.api}/2.0/files/${annotationService.fileId}/annotations?version=${ - fileVersionId - }&fields=item,thread,details,message,created_by,created_at,modified_at,permissions&marker=${marker}&limit=${ - limit - }`; + const url = `${annotationService.api}/2.0/files/${ + annotationService.fileId + }/annotations?version=${fileVersionId}&fields=item,thread,details,message,created_by,created_at,modified_at,permissions&marker=${marker}&limit=${limit}`; const result = annotationService.getReadUrl(fileVersionId, marker, limit); - expect(result).to.equal(url); + expect(result).toEqual(url); }); }); }); diff --git a/src/__tests__/AnnotationThread-test.html b/src/__tests__/AnnotationThread-test.html deleted file mode 100644 index 5ed98fbe4..000000000 --- a/src/__tests__/AnnotationThread-test.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/src/__tests__/AnnotationThread-test.js b/src/__tests__/AnnotationThread-test.js index 09b6ef488..f89343c92 100644 --- a/src/__tests__/AnnotationThread-test.js +++ b/src/__tests__/AnnotationThread-test.js @@ -13,21 +13,20 @@ import { } from '../constants'; let thread; -const sandbox = sinon.sandbox.create(); -let stubs = {}; +const html = '
'; describe('AnnotationThread', () => { - before(() => { - fixture.setBase('src'); - }); + let rootElement; beforeEach(() => { - fixture.load('__tests__/AnnotationThread-test.html'); + rootElement = document.createElement('div'); + rootElement.innerHTML = html; + document.body.appendChild(rootElement); thread = new AnnotationThread({ annotatedElement: document.querySelector(SELECTOR_ANNOTATED_ELEMENT), annotations: [], - annotationService: {}, + annotationService: { user: { id: '1' } }, fileVersionId: '1', isMobile: false, location: {}, @@ -37,57 +36,48 @@ describe('AnnotationThread', () => { }); thread.dialog = { - activateReply: () => {}, - addListener: () => {}, - addAnnotation: () => {}, - destroy: () => {}, - setup: () => {}, - removeAllListeners: () => {}, - show: () => {}, - hide: () => {}, - scrollToLastComment: () => {} - }; - stubs.dialogMock = sandbox.mock(thread.dialog); - - thread.annotationService = { - user: { id: '1' } + activateReply: jest.fn(), + addListener: jest.fn(), + addAnnotation: jest.fn(), + destroy: jest.fn(), + setup: jest.fn(), + removeAllListeners: jest.fn(), + show: jest.fn(), + hide: jest.fn(), + scrollToLastComment: jest.fn(), + removeAnnotation: jest.fn(), + enable: jest.fn(), + disable: jest.fn() }; - stubs.emit = sandbox.stub(thread, 'emit'); + thread.emit = jest.fn(); }); afterEach(() => { - thread.annotationService = undefined; - sandbox.verifyAndRestore(); - if (typeof stubs.destroy === 'function') { - stubs.destroy(); - thread = null; - } - stubs = {}; + document.body.removeChild(rootElement); + thread = null; }); describe('destroy()', () => { beforeEach(() => { thread.state = STATES.pending; - stubs.unbindCustom = sandbox.stub(thread, 'unbindCustomListenersOnDialog'); - stubs.unbindDOM = sandbox.stub(thread, 'unbindDOMListeners'); - stubs.destroyDialog = sandbox.stub(thread.dialog, 'destroy'); + thread.unbindCustomListenersOnDialog = jest.fn(); + thread.unbindDOMListeners = jest.fn(); }); it('should unbind listeners and remove thread element and broadcast that the thread was deleted', () => { thread.destroy(); - expect(stubs.unbindCustom).to.be.called; - expect(stubs.unbindDOM).to.be.called; - expect(stubs.emit).to.not.be.calledWith(THREAD_EVENT.threadDelete); + expect(thread.unbindCustomListenersOnDialog).toBeCalled(); + expect(thread.unbindDOMListeners).toBeCalled(); + expect(thread.emit).not.toBeCalledWith(THREAD_EVENT.threadDelete); }); it('should emit annotationthreaddeleted only if thread is not in a pending state', () => { thread.state = STATES.inactive; - thread.destroy(); - expect(stubs.unbindCustom).to.be.called; - expect(stubs.unbindDOM).to.be.called; - expect(stubs.emit).to.be.calledWith(THREAD_EVENT.threadDelete); + expect(thread.unbindCustomListenersOnDialog).toBeCalled(); + expect(thread.unbindDOMListeners).toBeCalled(); + expect(thread.emit).toBeCalledWith(THREAD_EVENT.threadDelete); }); it('should not destroy the dialog on mobile', () => { @@ -95,22 +85,22 @@ describe('AnnotationThread', () => { thread.isMobile = true; thread.destroy(); - expect(stubs.unbindCustom).to.not.be.called; - expect(stubs.destroyDialog).to.not.be.called; + expect(thread.unbindCustomListenersOnDialog).not.toBeCalled(); + expect(thread.dialog.destroy).not.toBeCalled(); }); }); describe('hide()', () => { it('should hide the thread element', () => { thread.hide(); - expect(thread.element).to.have.class(CLASS_HIDDEN); + expect(thread.element.classList).toContain(CLASS_HIDDEN); }); }); describe('reset()', () => { it('should set the thread state to inactive', () => { thread.reset(); - expect(thread.state).to.equal(STATES.inactive); + expect(thread.state).toEqual(STATES.inactive); }); }); @@ -122,141 +112,85 @@ describe('AnnotationThread', () => { }); it('returns true if thread\'s dialog is visible', () => { - expect(thread.isDialogVisible()).to.be.true; + expect(thread.isDialogVisible()).toBeTruthy(); }); it('returns false if thread\'s dialog is hidden', () => { thread.dialog.element.classList.add(CLASS_HIDDEN); - expect(thread.isDialogVisible()).to.be.false; + expect(thread.isDialogVisible()).toBeFalsy(); }); }); describe('showDialog()', () => { it('should setup the thread dialog if the dialog element does not already exist', () => { thread.dialog.element = null; - stubs.dialogMock.expects('setup'); - stubs.dialogMock.expects('show'); thread.showDialog(); + expect(thread.dialog.setup).toBeCalled(); + expect(thread.dialog.show).toBeCalled(); }); it('should not setup the thread dialog if the dialog element already exists', () => { thread.dialog.element = {}; - stubs.dialogMock.expects('setup').never(); - stubs.dialogMock.expects('show'); thread.showDialog(); + expect(thread.dialog.setup).not.toBeCalled(); + expect(thread.dialog.show).toBeCalled(); }); }); describe('hideDialog()', () => { it('should hide the thread dialog', () => { - stubs.dialogMock.expects('hide'); thread.hideDialog(); - expect(thread.state).to.equal(STATES.inactive); + expect(thread.state).toEqual(STATES.inactive); + expect(thread.dialog.hide).toBeCalled(); }); }); describe('saveAnnotation()', () => { - let annotationService; - beforeEach(() => { - annotationService = { - create: () => {} - }; - - thread = new AnnotationThread({ - annotatedElement: document.querySelector(SELECTOR_ANNOTATED_ELEMENT), - annotations: [], - annotationService, - fileVersionId: '1', - location: {}, - threadID: '2', - threadNumber: '1', - type: 'point' - }); - - sandbox.stub(thread, 'getThreadEventData').returns({}); - stubs.create = sandbox.stub(annotationService, 'create'); - thread.dialog = { - addAnnotation: () => {}, - activateReply: () => {}, - disable: () => {} - }; - const dialogMock = sandbox.mock(thread.dialog); - dialogMock.expects('disable'); + thread.getThreadEventData = jest.fn().mockReturnValue({}); + thread.annotationService.create = jest.fn(); + thread.handleThreadSaveError = jest.fn(); + thread.updateTemporaryAnnotation = jest.fn(); }); it('should save an annotation with the specified type and text', (done) => { - stubs.create.returns(Promise.resolve({})); - stubs.updateTemp = sandbox.stub(thread, 'updateTemporaryAnnotation'); + thread.annotationService.create = jest.fn().mockResolvedValue({}); const promise = thread.saveAnnotation('point', 'blah'); - promise - .then(() => { - expect(stubs.updateTemp).to.be.called; - done(); - }) - .catch(() => { - sinon.assert.failException; - }); - expect(stubs.create).to.be.calledWith( - sinon.match({ - fileVersionId: '1', - type: 'point', - text: 'blah', - threadID: '2', - threadNumber: '1' - }) - ); + promise.then(() => { + expect(thread.updateTemporaryAnnotation).toBeCalled(); + done(); + }); + expect(thread.annotationService.create).toBeCalled(); + expect(thread.dialog.disable).toBeCalled(); }); it('should delete the temporary annotation and broadcast an error if there was an error saving', (done) => { - stubs.create.returns(Promise.reject()); - stubs.handleError = sandbox.stub(thread, 'handleThreadSaveError'); - stubs.serverSave = sandbox.stub(thread, 'updateTemporaryAnnotation'); + thread.annotationService.create = jest.fn().mockRejectedValue({}); const promise = thread.saveAnnotation('point', 'blah'); - promise - .then(() => { - expect(stubs.handleError).to.be.called; - done(); - }) - .catch(() => { - sinon.assert.failException; - }); - expect(stubs.create).to.be.called; - expect(stubs.serverSave).to.not.be.called; + promise.then(() => { + expect(thread.handleThreadSaveError).toBeCalled(); + done(); + }); + expect(thread.annotationService.create).toBeCalled(); + expect(thread.updateTemporaryAnnotation).not.toBeCalled(); + expect(thread.dialog.disable).toBeCalled(); }); }); describe('updateTemporaryAnnotation()', () => { - let annotationService; - beforeEach(() => { - annotationService = { - create: () => {} - }; - - thread = new AnnotationThread({ - annotatedElement: document.querySelector(SELECTOR_ANNOTATED_ELEMENT), - annotations: {}, - annotationService, - fileVersionId: '1', - location: {}, - threadID: '2', - threadNumber: '1', - type: 'point' - }); - - stubs.create = sandbox.stub(annotationService, 'create'); - stubs.saveAnnotationToThread = sandbox.stub(thread, 'saveAnnotationToThread'); - sandbox.stub(thread, 'getThreadEventData').returns({}); + thread.annotationService.create = jest.fn(); + thread.saveAnnotationToThread = jest.fn(); + thread.getThreadEventData = jest.fn().mockReturnValue({}); }); it('should save annotation to thread if it does not exist in annotations array', () => { const serverAnnotation = 'real annotation'; const tempAnnotation = serverAnnotation; thread.updateTemporaryAnnotation(tempAnnotation, serverAnnotation); - expect(stubs.saveAnnotationToThread).to.be.called; + expect(thread.saveAnnotationToThread).toBeCalled(); }); it('should overwrite a local annotation to the thread if it does exist as an associated annotation', () => { @@ -264,77 +198,67 @@ describe('AnnotationThread', () => { const tempAnnotation = { annotationID: 1 }; thread.annotations[tempAnnotation.annotationID] = tempAnnotation; - expect(thread.annotations[123]).to.be.undefined; + expect(thread.annotations[123]).toBeUndefined(); thread.updateTemporaryAnnotation(tempAnnotation, serverAnnotation); - expect(stubs.saveAnnotationToThread).to.not.be.called; - expect(thread.annotations[123]).to.deep.equal(serverAnnotation); + expect(thread.saveAnnotationToThread).not.toBeCalled(); + expect(thread.annotations[123]).toStrictEqual(serverAnnotation); }); - it('should emit an annotationsaved event on success', (done) => { + it('should emit an annotationsaved event on success', () => { const serverAnnotation = { threadNumber: 1 }; const tempAnnotation = serverAnnotation; thread.threadNumber = undefined; - thread.addListener(THREAD_EVENT.save, () => { - expect(stubs.saveAnnotationToThread).to.be.called; - done(); - }); + thread.emit = jest.fn(); thread.updateTemporaryAnnotation(tempAnnotation, serverAnnotation); + expect(thread.emit).toBeCalledWith(THREAD_EVENT.save); }); it('should update thread number and replace temporary annotation if dialog exists', () => { const serverAnnotation = { annotationID: 123 }; const tempAnnotation = { annotationID: 1 }; thread.threadNumber = 'something'; - thread.dialog = { - addAnnotation: () => {}, - removeAnnotation: () => {}, - enable: () => {}, - scrollToLastComment: () => {}, - element: { - dataset: { threadNumber: undefined } - } - }; - const dialogMock = sandbox.mock(thread.dialog); + thread.dialog.element = document.createElement('div'); - dialogMock.expects('enable').withArgs(serverAnnotation.annotationID); - dialogMock.expects('addAnnotation').withArgs(serverAnnotation); - dialogMock.expects('removeAnnotation').withArgs(tempAnnotation.annotationID); - dialogMock.expects('scrollToLastComment'); thread.updateTemporaryAnnotation(tempAnnotation, serverAnnotation); - expect(thread.dialog.element.dataset.threadNumber).to.not.be.undefined; + expect(thread.dialog.element.dataset.threadNumber).not.toBeUndefined(); + expect(thread.dialog.enable).toBeCalledWith(serverAnnotation.annotationID); + expect(thread.dialog.addAnnotation).toBeCalledWith(serverAnnotation); + expect(thread.dialog.removeAnnotation).toBeCalledWith(tempAnnotation.annotationID); + expect(thread.dialog.scrollToLastComment).toBeCalled(); }); it('should only show dialog immediately on mobile devices', () => { const serverAnnotation = { threadNumber: 1 }; const tempAnnotation = serverAnnotation; - sandbox.stub(thread, 'showDialog'); + thread.showDialog = jest.fn(); // Don't show dialog on web browsers thread.updateTemporaryAnnotation(tempAnnotation, serverAnnotation); - expect(thread.showDialog).to.not.be.called; - expect(thread.state).to.not.equal(STATES.hover); + expect(thread.showDialog).not.toBeCalled(); + expect(thread.state).not.toEqual(STATES.hover); // Only show dialog on mobile browsers thread.isMobile = true; thread.updateTemporaryAnnotation(tempAnnotation, serverAnnotation); - expect(thread.showDialog).to.be.called; - expect(thread.state).to.equal(STATES.hover); + expect(thread.showDialog).toBeCalled(); + expect(thread.state).toEqual(STATES.hover); }); }); describe('deleteAnnotation()', () => { let annotationService; + let annotation; + let annotation2; + const threadPromise = Promise.resolve(); beforeEach(() => { - stubs.threadPromise = Promise.resolve(); annotationService = { user: { id: 1 }, - delete: sandbox.stub().returns(stubs.threadPromise) + delete: jest.fn().mockResolvedValue(threadPromise) }; - stubs.serviceDelete = annotationService.delete; - stubs.annotation = { + annotation = { annotationID: 'someID', permissions: { can_delete: true @@ -342,7 +266,7 @@ describe('AnnotationThread', () => { threadID: 1 }; - stubs.annotation2 = { + annotation2 = { annotationID: 'someID2', permissions: { can_delete: false @@ -352,7 +276,7 @@ describe('AnnotationThread', () => { thread = new AnnotationThread({ annotatedElement: document.querySelector(SELECTOR_ANNOTATED_ELEMENT), - annotations: { someID: stubs.annotation }, + annotations: { someID: annotation }, annotationService, fileVersionId: '1', isMobile: false, @@ -363,172 +287,147 @@ describe('AnnotationThread', () => { }); thread.dialog = { - addListener: () => {}, - addAnnotation: () => {}, - activateReply: () => {}, - destroy: () => {}, - removeAllListeners: () => {}, - show: () => {}, - hide: () => {}, - removeAnnotation: () => {}, - hideMobileDialog: () => {} + addListener: jest.fn(), + addAnnotation: jest.fn(), + activateReply: jest.fn(), + destroy: jest.fn(), + removeAllListeners: jest.fn(), + show: jest.fn(), + hide: jest.fn(), + removeAnnotation: jest.fn(), + hideMobileDialog: jest.fn(), + setup: jest.fn() }; - stubs.dialogMock = sandbox.mock(thread.dialog); - stubs.isPlain = sandbox.stub(util, 'isPlainHighlight'); - stubs.cancel = sandbox.stub(thread, 'cancelFirstComment'); - stubs.destroy = sandbox.stub(thread, 'destroy'); - sandbox.stub(thread, 'showDialog'); - sandbox.stub(thread, 'getThreadEventData').returns({ + util.isPlainHighlight = jest.fn(); + util.getFirstAnnotation = jest.fn().mockReturnValue(annotation); + thread.cancelFirstComment = jest.fn(); + thread.destroy = jest.fn(); + thread.showDialog = jest.fn(); + thread.getThreadEventData = jest.fn().mockReturnValue({ threadNumber: 1 }); + thread.emit = jest.fn(); }); it('should destroy the thread if the deleted annotation was the last annotation in the thread', (done) => { thread.isMobile = false; - stubs.dialogMock.expects('removeAnnotation').never(); - stubs.dialogMock.expects('hideMobileDialog').never(); + util.getFirstAnnotation = jest.fn(); const promise = thread.deleteAnnotation('someID', false); - promise - .then(() => { - stubs.threadPromise.then(() => { - expect(stubs.destroy).to.be.called; - done(); - }); - }) - .catch(() => { - sinon.assert.failException; + promise.then(() => { + threadPromise.then(() => { + expect(thread.destroy).toBeCalled(); + expect(thread.dialog.removeAnnotation).not.toBeCalled(); + expect(thread.dialog.hideMobileDialog).not.toBeCalled(); + done(); }); + }); }); it('should destroy the thread and hide the mobile dialog if the deleted annotation was the last annotation in the thread on mobile', (done) => { thread.isMobile = true; - stubs.dialogMock.expects('removeAnnotation'); - stubs.dialogMock.expects('hideMobileDialog'); + util.getFirstAnnotation = jest.fn(); const promise = thread.deleteAnnotation('someID', false); - promise - .then(() => { - done(); - }) - .catch(() => { - sinon.assert.failException; - }); + promise.then(() => { + expect(thread.dialog.removeAnnotation).toBeCalled(); + expect(thread.dialog.hideMobileDialog).toBeCalled(); + done(); + }); }); it('should remove the relevant annotation from its dialog if the deleted annotation was not the last one', (done) => { // Add another annotation to thread so 'someID' isn't the only annotation - thread.annotations[stubs.annotation2.annotationID] = stubs.annotation2; - stubs.dialogMock.expects('removeAnnotation').withArgs('someID'); - stubs.dialogMock.expects('activateReply'); + thread.annotations[annotation2.annotationID] = annotation2; const promise = thread.deleteAnnotation('someID', false); - promise - .then(() => { - done(); - }) - .catch(() => { - sinon.assert.failException; - }); + promise.then(() => { + expect(thread.dialog.removeAnnotation).toBeCalledWith('someID'); + expect(thread.dialog.activateReply).toBeCalled(); + done(); + }); }); it('should make a server call to delete an annotation with the specified ID if useServer is true', (done) => { const promise = thread.deleteAnnotation('someID', true); - promise - .then(() => { - expect(stubs.emit).to.not.be.calledWith(THREAD_EVENT.threadCleanup); - expect(annotationService.delete).to.be.calledWith('someID'); - done(); - }) - .catch(() => { - sinon.assert.failException; - }); + promise.then(() => { + expect(thread.emit).not.toBeCalledWith(THREAD_EVENT.threadCleanup); + expect(annotationService.delete).toBeCalledWith('someID'); + done(); + }); }); it('should also delete blank highlight comment from the server when removing the last comment on a highlight thread', (done) => { - stubs.annotation2.permissions.can_delete = false; - thread.annotations[stubs.annotation2.annotationID] = stubs.annotation2; - stubs.isPlain.returns(true); + annotation2.permissions.can_delete = false; + thread.annotations[annotation2.annotationID] = annotation2; + util.isPlain = jest.fn().mockReturnValue(true); const promise = thread.deleteAnnotation('someID', true); - promise - .then(() => { - expect(annotationService.delete).to.be.calledWith('someID'); - done(); - }) - .catch(() => { - sinon.assert.failException; - }); + promise.then(() => { + expect(annotationService.delete).toBeCalledWith('someID'); + done(); + }); }); it('should not make a server call to delete an annotation with the specified ID if useServer is false', (done) => { const promise = thread.deleteAnnotation('someID', false); - promise - .then(() => { - expect(annotationService.delete).to.not.be.called; - done(); - }) - .catch(() => { - sinon.assert.failException; - }); + promise.then(() => { + expect(annotationService.delete).not.toBeCalled(); + done(); + }); }); it('should broadcast an error if there was an error deleting from server', (done) => { - stubs.serviceDelete.returns(Promise.reject()); + annotationService.delete = jest.fn().mockRejectedValue(); const promise = thread.deleteAnnotation('someID', true); - promise - .then(() => { - sinon.assert.failException; - }) - .catch(() => { - expect(annotationService.delete).to.be.called; - done(); - }); + promise.catch(() => { + expect(annotationService.delete).toBeCalled(); + done(); + }); }); it('should toggle highlight dialogs with the delete of the last comment if user does not have permission to delete the entire annotation', () => { - thread.annotations[stubs.annotation2.annotationID] = stubs.annotation2; - stubs.isPlain.returns(true); - thread.deleteAnnotation('someID', false); - expect(stubs.cancel).to.be.called; - expect(stubs.destroy).to.not.be.called; + thread.annotations[annotation2.annotationID] = annotation2; + util.isPlain = jest.fn().mockReturnValue(true); + + const promise = thread.deleteAnnotation('someID'); + promise.then(() => { + expect(thread.cancelFirstComment).toBeCalled(); + expect(thread.destroy).not.toBeCalled(); + }); }); it('should destroy the annotation with the delete of the last comment if the user has permissions', () => { - stubs.annotation2.permissions.can_delete = true; - thread.annotations[stubs.annotation2.annotationID] = stubs.annotation2; - stubs.isPlain.returns(true); + annotation2.permissions.can_delete = true; + thread.annotations[annotation2.annotationID] = annotation2; + util.isPlain = jest.fn().mockReturnValue(true); const promise = thread.deleteAnnotation('someID'); - promise - .then(() => { - expect(stubs.emit).to.be.calledWith(THREAD_EVENT.threadCleanup); - expect(stubs.emit).to.be.calledWith(THREAD_EVENT.delete); - }) - .catch(() => { - sinon.assert.failException; - }); - expect(stubs.cancel).to.not.be.called; - expect(stubs.destroy).to.be.called; + promise.then(() => { + expect(thread.emit).toBeCalledWith(THREAD_EVENT.threadCleanup); + expect(thread.emit).toBeCalledWith(THREAD_EVENT.delete); + expect(thread.cancelFirstComment).not.toBeCalled(); + expect(thread.destroy).toBeCalled(); + }); }); }); describe('scrollIntoView()', () => { it('should scroll to annotation page and center annotation in viewport', () => { - sandbox.stub(thread, 'scrollToPage'); - sandbox.stub(thread, 'centerAnnotation'); + thread.scrollToPage = jest.fn(); + thread.centerAnnotation = jest.fn(); thread.scrollIntoView(); expect(thread.scrollToPage); - expect(thread.centerAnnotation).to.be.calledWith(sinon.match.number); + expect(thread.centerAnnotation).toBeCalledWith(expect.any(Number)); }); }); describe('scrollToPage()', () => { it('should do nothing if annotation does not have a location or page', () => { const pageEl = { - scrollIntoView: sandbox.stub() + scrollIntoView: jest.fn() }; thread.location = {}; @@ -536,19 +435,19 @@ describe('AnnotationThread', () => { thread.location = null; thread.scrollToPage(); - expect(pageEl.scrollIntoView).to.not.be.called; + expect(pageEl.scrollIntoView).not.toBeCalled(); }); it('should scroll annotation\'s page into view', () => { thread.location = { page: 1 }; const pageEl = { - scrollIntoView: sandbox.stub() + scrollIntoView: jest.fn() }; thread.annotatedElement = { - querySelector: sandbox.stub().returns(pageEl) + querySelector: jest.fn().mockReturnValue(pageEl) }; thread.scrollToPage(); - expect(pageEl.scrollIntoView).to.be.called; + expect(pageEl.scrollIntoView).toBeCalled(); }); }); @@ -563,64 +462,67 @@ describe('AnnotationThread', () => { it('should scroll so annotation is vertically centered in viewport', () => { thread.centerAnnotation(50); - expect(thread.annotatedElement.scrollTop).to.equal(50); + expect(thread.annotatedElement.scrollTop).toEqual(50); }); it('should scroll so annotation is vertically centered in viewport', () => { thread.centerAnnotation(150); - expect(thread.annotatedElement.scrollTop).to.equal(200); + expect(thread.annotatedElement.scrollTop).toEqual(200); }); }); describe('location()', () => { it('should get location', () => { - expect(thread.location).to.equal(thread.location); + expect(thread.location).toEqual(thread.location); }); }); describe('threadID()', () => { it('should get threadID', () => { - expect(thread.threadID).to.equal(thread.threadID); + expect(thread.threadID).toEqual(thread.threadID); }); }); describe('thread()', () => { it('should get thread', () => { - expect(thread.thread).to.equal(thread.thread); + expect(thread.thread).toEqual(thread.thread); }); }); describe('type()', () => { it('should get type', () => { - expect(thread.type).to.equal(thread.type); + expect(thread.type).toEqual(thread.type); }); }); describe('state()', () => { it('should get state', () => { - expect(thread.state).to.equal(thread.state); + expect(thread.state).toEqual(thread.state); }); }); describe('setup()', () => { beforeEach(() => { - stubs.create = sandbox.stub(thread, 'createDialog'); - stubs.bind = sandbox.stub(thread, 'bindCustomListenersOnDialog'); - stubs.setup = sandbox.stub(thread, 'setupElement'); + thread.createDialog = jest.fn(); + thread.bindCustomListenersOnDialog = jest.fn(); + thread.setupElement = jest.fn(); + thread.destroy = jest.fn(); + util.getFirstAnnotation = jest.fn().mockReturnValue({}); }); it('should setup dialog', () => { thread.dialog = {}; thread.setup(); - expect(stubs.create).to.be.called; - expect(stubs.bind).to.be.called; - expect(stubs.setup).to.be.called; - expect(thread.dialog.isMobile).to.equal(thread.isMobile); + expect(thread.createDialog).toBeCalled(); + expect(thread.bindCustomListenersOnDialog).toBeCalled(); + expect(thread.setupElement).toBeCalled(); + expect(thread.dialog.isMobile).toEqual(thread.isMobile); }); it('should set state to pending if thread is initialized with no annotations', () => { + util.getFirstAnnotation = jest.fn().mockReturnValue(null); thread.setup(); - expect(thread.state).to.equal(STATES.pending); + expect(thread.state).toEqual(STATES.pending); }); it('should set state to inactive if thread is initialized with annotations', () => { @@ -635,132 +537,105 @@ describe('AnnotationThread', () => { threadNumber: '1', type: 'point' }); + thread.destroy = jest.fn(); thread.setup(); - expect(thread.state).to.equal(STATES.inactive); + expect(thread.state).toEqual(STATES.inactive); }); }); describe('setupElement()', () => { it('should create element and bind listeners', () => { - stubs.bind = sandbox.stub(thread, 'bindDOMListeners'); - + thread.bindDOMListeners = jest.fn(); thread.setupElement(); - expect(thread.element instanceof HTMLElement).to.be.true; - expect(thread.element).to.have.class(CLASS_ANNOTATION_POINT_MARKER); - expect(stubs.bind).to.be.called; + expect(thread.element instanceof HTMLElement).toBeTruthy(); + expect(thread.element.classList).toContain(CLASS_ANNOTATION_POINT_MARKER); + expect(thread.bindDOMListeners).toBeCalled(); }); }); describe('bindDOMListeners()', () => { beforeEach(() => { thread.element = document.createElement('div'); - stubs.add = sandbox.stub(thread.element, 'addEventListener'); + thread.element.addEventListener = jest.fn(); thread.isMobile = false; }); - it('should do nothing if element does not exist', () => { - thread.element = null; - thread.bindDOMListeners(); - expect(stubs.add).to.not.be.called; - }); - it('should bind DOM listeners', () => { thread.bindDOMListeners(); - expect(stubs.add).to.be.calledWith('click', sinon.match.func); + expect(thread.element.addEventListener).toBeCalledWith('click', expect.any(Function)); }); it('should not add mouseleave listener for mobile browsers', () => { thread.isMobile = true; thread.bindDOMListeners(); - expect(stubs.add).to.be.calledWith('click', sinon.match.func); + expect(thread.element.addEventListener).toBeCalledWith('click', expect.any(Function)); }); }); describe('unbindDOMListeners()', () => { beforeEach(() => { thread.element = document.createElement('div'); - stubs.remove = sandbox.stub(thread.element, 'removeEventListener'); + thread.element.removeEventListener = jest.fn(); thread.isMobile = false; }); - it('should do nothing if element does not exist', () => { - thread.element = null; - thread.unbindDOMListeners(); - expect(stubs.remove).to.not.be.called; - }); - it('should unbind DOM listeners', () => { thread.unbindDOMListeners(); - expect(stubs.remove).to.be.calledWith('click', sinon.match.func); + expect(thread.element.removeEventListener).toBeCalledWith('click', expect.any(Function)); }); it('should not add mouseleave listener for mobile browsers', () => { thread.isMobile = true; thread.unbindDOMListeners(); - expect(stubs.remove).to.be.calledWith('click', sinon.match.func); + expect(thread.element.removeEventListener).toBeCalledWith('click', expect.any(Function)); }); }); describe('bindCustomListenersOnDialog()', () => { - it('should do nothing if dialog does not exist', () => { - thread.dialog = null; - stubs.dialogMock.expects('addListener').never(); - thread.bindCustomListenersOnDialog(); - }); - it('should bind custom listeners on dialog', () => { - stubs.dialogMock.expects('addListener').withArgs('annotationcreate', sinon.match.func); - stubs.dialogMock.expects('addListener').withArgs('annotationcancel', sinon.match.func); - stubs.dialogMock.expects('addListener').withArgs('annotationdelete', sinon.match.func); - stubs.dialogMock.expects('addListener').withArgs('annotationshow', sinon.match.func); - stubs.dialogMock.expects('addListener').withArgs('annotationhide', sinon.match.func); thread.bindCustomListenersOnDialog(); + expect(thread.dialog.addListener).toBeCalledWith('annotationcreate', expect.any(Function)); + expect(thread.dialog.addListener).toBeCalledWith('annotationcancel', expect.any(Function)); + expect(thread.dialog.addListener).toBeCalledWith('annotationdelete', expect.any(Function)); + expect(thread.dialog.addListener).toBeCalledWith('annotationshow', expect.any(Function)); + expect(thread.dialog.addListener).toBeCalledWith('annotationhide', expect.any(Function)); }); }); describe('unbindCustomListenersOnDialog()', () => { - it('should do nothing if dialog does not exist', () => { - thread.dialog = null; - stubs.dialogMock.expects('removeAllListeners').never(); - thread.unbindCustomListenersOnDialog(); - }); - it('should unbind custom listeners from dialog', () => { - stubs.dialogMock - .expects('removeAllListeners') - .withArgs([ - 'annotationcreate', - 'annotationcancel', - 'annotationdelete', - 'annotationshow', - 'annotationhide' - ]); thread.unbindCustomListenersOnDialog(); + expect(thread.dialog.removeAllListeners).toBeCalledWith([ + 'annotationcreate', + 'annotationcancel', + 'annotationdelete', + 'annotationshow', + 'annotationhide' + ]); }); }); describe('cancelUnsavedAnnotation()', () => { - it('should destroy thread if in a pending/pending-active state', () => { - sandbox.stub(thread, 'destroy'); - sandbox.stub(thread, 'hideDialog'); - stubs.isPending = sandbox.stub(util, 'isPending').returns(true); + beforeEach(() => { + thread.destroy = jest.fn(); + thread.hideDialog = jest.fn(); + }); + it('should destroy thread if in a pending/pending-active state', () => { + util.isPending = jest.fn().mockReturnValue(true); thread.cancelUnsavedAnnotation(); - expect(thread.destroy).to.be.called; - expect(thread.emit).to.be.calledWith(THREAD_EVENT.cancel); - expect(thread.hideDialog).to.not.be.called; + expect(thread.destroy).toBeCalled(); + expect(thread.emit).toBeCalledWith(THREAD_EVENT.cancel); + expect(thread.hideDialog).not.toBeCalled(); }); it('should not destroy thread if not in a pending/pending-active state', () => { - sandbox.stub(thread, 'destroy'); - sandbox.stub(thread, 'hideDialog'); - stubs.isPending = sandbox.stub(util, 'isPending').returns(false); - + util.isPending = jest.fn().mockReturnValue(false); thread.cancelUnsavedAnnotation(); - expect(thread.destroy).to.not.be.called; - expect(thread.emit).to.not.be.calledWith(THREAD_EVENT.cancel); - expect(thread.hideDialog).to.be.called; + expect(thread.destroy).not.toBeCalled(); + expect(thread.emit).not.toBeCalledWith(THREAD_EVENT.cancel); + expect(thread.hideDialog).toBeCalled(); }); }); @@ -769,7 +644,7 @@ describe('AnnotationThread', () => { thread.annotationService.user = { id: -1 }; thread.threadNumber = undefined; const data = thread.getThreadEventData(); - expect(data).to.deep.equal({ + expect(data).toStrictEqual({ type: thread.type, threadID: thread.threadID }); @@ -779,7 +654,7 @@ describe('AnnotationThread', () => { thread.annotationService.user = { id: 1 }; thread.threadNumber = undefined; const data = thread.getThreadEventData(); - expect(data).to.deep.equal({ + expect(data).toStrictEqual({ type: thread.type, threadID: thread.threadID, userId: 1 @@ -790,7 +665,7 @@ describe('AnnotationThread', () => { thread.annotationService.user = { id: -1 }; thread.threadNumber = 1; const data = thread.getThreadEventData(); - expect(data).to.deep.equal({ + expect(data).toStrictEqual({ type: thread.type, threadID: thread.threadID, threadNumber: 1 @@ -801,45 +676,46 @@ describe('AnnotationThread', () => { describe('createElement()', () => { it('should create an element with the right class and attribute', () => { const element = thread.createElement(); - expect(element).to.have.class(CLASS_ANNOTATION_POINT_MARKER); - expect(element).to.have.attribute('data-type', DATA_TYPE_ANNOTATION_INDICATOR); - }); - }); - - describe('mouseoutHandler()', () => { - it('should do nothing if event does not exist', () => { - stubs.isInDialog = sandbox.stub(util, 'isInDialog'); - thread.mouseoutHandler(); - expect(stubs.isInDialog).to.not.be.called; - }); - - it('should not call hideDialog if there are no annotations in the thread', () => { - stubs.hide = sandbox.stub(thread, 'hideDialog'); - thread.mouseoutHandler({}); - expect(stubs.hide).to.not.be.called; - }); - - it('should call hideDialog if there are annotations in the thread', () => { - stubs.hide = sandbox.stub(thread, 'hideDialog'); - const annotation = new Annotation({ - fileVersionId: '2', - threadID: '1', - type: 'point', - text: 'blah', - threadNumber: '1', - location: { x: 0, y: 0 }, - created: Date.now() - }); - - thread.annotations = [annotation]; - thread.mouseoutHandler({}); - expect(stubs.hide).to.be.called; - }); - }); + expect(element.classList).toContain(CLASS_ANNOTATION_POINT_MARKER); + expect(element.dataset.type).toEqual(DATA_TYPE_ANNOTATION_INDICATOR); + }); + }); + + // describe('mouseoutHandler()', () => { + // it('should do nothing if event does not exist', () => { + // util.isInDialog = jest.fn(); + // thread.mouseoutHandler(); + // expect(util.isInDialog).not.toBeCalled(); + // }); + + // it('should not call hideDialog if there are no annotations in the thread', () => { + // thread.annotations = []; + // thread.hideDialog = jest.fn(); + // thread.mouseoutHandler({}); + // expect(thread.hideDialog).not.toBeCalled(); + // }); + + // it('should call hideDialog if there are annotations in the thread', () => { + // thread.hideDialog = jest.fn(); + // const annotation = new Annotation({ + // fileVersionId: '2', + // threadID: '1', + // type: 'point', + // text: 'blah', + // threadNumber: '1', + // location: { x: 0, y: 0 }, + // created: Date.now() + // }); + + // thread.annotations = [annotation]; + // thread.mouseoutHandler({}); + // expect(thread.hideDialog).toBeCalled(); + // }); + // }); describe('saveAnnotationToThread()', () => { it('should add the annotation to the thread, and add to the dialog when the dialog exists', () => { - stubs.push = sandbox.stub(thread.annotations, 'push'); + thread.annotations.push = jest.fn(); const annotation = new Annotation({ fileVersionId: '2', threadID: '1', @@ -850,10 +726,10 @@ describe('AnnotationThread', () => { created: Date.now() }); - stubs.dialogMock.expects('activateReply'); - stubs.dialogMock.expects('addAnnotation').withArgs(annotation); thread.saveAnnotationToThread(annotation); - expect(thread.annotations[annotation.annotationID]).to.not.be.undefined; + expect(thread.annotations[annotation.annotationID]).not.toBeUndefined(); + expect(thread.dialog.activateReply).toBeCalled(); + expect(thread.dialog.addAnnotation).toBeCalledWith(annotation); }); it('should not try to push an annotation to the dialog if it doesn\'t exist', () => { @@ -868,36 +744,34 @@ describe('AnnotationThread', () => { }); thread.dialog = undefined; - stubs.dialogMock.expects('activateReply').never(); - stubs.dialogMock.expects('addAnnotation').never(); thread.saveAnnotationToThread(annotation); - expect(thread.annotations[annotation.annotationID]).to.not.be.undefined; + expect(thread.annotations[annotation.annotationID]).not.toBeUndefined(); }); }); describe('createAnnotationDialog()', () => { it('should correctly create the annotation data object', () => { const annotationData = thread.createAnnotationData('highlight', 'test'); - expect(annotationData.location).to.equal(thread.location); - expect(annotationData.fileVersionId).to.equal(thread.fileVersionId); - expect(annotationData.thread).to.equal(thread.thread); - expect(annotationData.user.id).to.equal('1'); + expect(annotationData.location).toEqual(thread.location); + expect(annotationData.fileVersionId).toEqual(thread.fileVersionId); + expect(annotationData.thread).toEqual(thread.thread); + expect(annotationData.user.id).toEqual('1'); }); }); describe('createAnnotation()', () => { it('should create a new point annotation', () => { - sandbox.stub(thread, 'saveAnnotation'); + thread.saveAnnotation = jest.fn(); thread.createAnnotation({ text: 'bleh' }); - expect(thread.saveAnnotation).to.be.calledWith(TYPES.point, 'bleh'); + expect(thread.saveAnnotation).toBeCalledWith(TYPES.point, 'bleh'); }); }); describe('deleteAnnotationWithID()', () => { it('should delete a point annotation with the matching annotationID', () => { - sandbox.stub(thread, 'deleteAnnotation'); + thread.deleteAnnotation = jest.fn(); thread.deleteAnnotationWithID({ annotationID: 1 }); - expect(thread.deleteAnnotation).to.be.calledWith(1); + expect(thread.deleteAnnotation).toBeCalledWith(1); }); }); @@ -915,26 +789,26 @@ describe('AnnotationThread', () => { thread.location = { y: 'something' }; thread.regenerateBoundary(); - expect(thread.minX).to.be.undefined; - expect(thread.minY).to.be.undefined; + expect(thread.minX).toBeUndefined(); + expect(thread.minY).toBeUndefined(); }); it('should set the min/max x/y values to the thread location', () => { thread.location = { x: 1, y: 2 }; thread.regenerateBoundary(); - expect(thread.minX).to.equal(1); - expect(thread.minY).to.equal(2); - expect(thread.maxX).to.equal(1); - expect(thread.maxY).to.equal(2); + expect(thread.minX).toEqual(1); + expect(thread.minY).toEqual(2); + expect(thread.maxX).toEqual(1); + expect(thread.maxY).toEqual(2); }); }); describe('handleThreadSaveError()', () => { it('should delete temp annotation and emit event', () => { - sandbox.stub(thread, 'deleteAnnotation'); + thread.deleteAnnotation = jest.fn(); thread.handleThreadSaveError(new Error(), 1); - expect(thread.deleteAnnotation).to.be.calledWith(1, false); - expect(thread.emit).to.be.calledWith(THREAD_EVENT.createError); + expect(thread.deleteAnnotation).toBeCalledWith(1, false); + expect(thread.emit).toBeCalledWith(THREAD_EVENT.createError); }); }); }); diff --git a/src/__tests__/Annotator-test.html b/src/__tests__/Annotator-test.html deleted file mode 100644 index 48a5d5c03..000000000 --- a/src/__tests__/Annotator-test.html +++ /dev/null @@ -1,2 +0,0 @@ - -
diff --git a/src/__tests__/Annotator-test.js b/src/__tests__/Annotator-test.js index 7a6041e55..09a62b6b8 100644 --- a/src/__tests__/Annotator-test.js +++ b/src/__tests__/Annotator-test.js @@ -1,8 +1,6 @@ /* eslint-disable no-unused-expressions */ -import EventEmitter from 'events'; import Annotator from '../Annotator'; import * as util from '../util'; -import AnnotationService from '../AnnotationService'; import { STATES, TYPES, @@ -13,37 +11,39 @@ import { } from '../constants'; let annotator; -let stubs = {}; -const sandbox = sinon.sandbox.create(); +let controller; +let thread; +const html = ` +
`; describe('Annotator', () => { - before(() => { - fixture.setBase('src'); - }); + let rootElement; beforeEach(() => { - fixture.load('__tests__/Annotator-test.html'); - - stubs.controller = { - init: () => {}, - addListener: () => {}, - registerThread: () => {}, - isEnabled: () => {}, - getButton: () => {}, - enter: () => {}, - exit: () => {}, - setupSharedDialog: () => {}, - getThreadByID: () => {} + rootElement = document.createElement('div'); + rootElement.innerHTML = html; + document.body.appendChild(rootElement); + + controller = { + init: jest.fn(), + addListener: jest.fn(), + registerThread: jest.fn(), + isEnabled: jest.fn(), + getButton: jest.fn(), + enter: jest.fn(), + exit: jest.fn(), + setupSharedDialog: jest.fn(), + getThreadByID: jest.fn() }; - stubs.controllerMock = sandbox.mock(stubs.controller); const options = { annotator: { NAME: 'name', - CONTROLLERS: { something: stubs.controller } + CONTROLLERS: { something: controller } }, modeButtons: { something: {} } }; + annotator = new Annotator({ canAnnotate: true, container: document, @@ -63,48 +63,23 @@ describe('Annotator', () => { } }); - stubs.thread = { + thread = { threadID: '123abc', - show: () => {}, - hide: () => {}, - addListener: () => {}, - unbindCustomListenersOnThread: () => {}, - removeListener: () => {}, - scrollIntoView: () => {}, - getThreadEventData: () => {}, - showDialog: () => {}, + show: jest.fn(), + hide: jest.fn(), + addListener: jest.fn(), + unbindCustomListenersOnThread: jest.fn(), + removeListener: jest.fn(), + scrollIntoView: jest.fn(), + getThreadEventData: jest.fn(), + showDialog: jest.fn(), type: 'something', location: { page: 1 } }; - stubs.threadMock = sandbox.mock(stubs.thread); - - stubs.thread2 = { - threadID: '456def', - show: () => {}, - hide: () => {}, - addListener: () => {}, - unbindCustomListenersOnThread: () => {}, - removeAllListeners: () => {}, - type: 'something', - location: { page: 2 } - }; - stubs.threadMock2 = sandbox.mock(stubs.thread2); - - stubs.thread3 = { - threadID: '789ghi', - show: () => {}, - hide: () => {}, - addListener: () => {}, - unbindCustomListenersOnThread: () => {}, - removeAllListeners: () => {}, - type: 'something', - location: { page: 2 } - }; - stubs.threadMock3 = sandbox.mock(stubs.thread3); }); afterEach(() => { - sandbox.verifyAndRestore(); + document.body.removeChild(rootElement); annotator.modeButtons = {}; annotator.modeControllers = {}; @@ -112,69 +87,68 @@ describe('Annotator', () => { annotator.destroy(); annotator = null; } - - stubs = {}; }); describe('init()', () => { beforeEach(() => { - const annotatedEl = document.querySelector(SELECTOR_ANNOTATED_ELEMENT); - sandbox.stub(annotator, 'getAnnotatedEl').returns(annotatedEl); + const annotatedEl = rootElement.querySelector(SELECTOR_ANNOTATED_ELEMENT); + annotator.getAnnotatedEl = jest.fn().mockReturnValue(annotatedEl); annotator.annotatedElement = annotatedEl; - stubs.scale = sandbox.stub(annotator, 'setScale'); - stubs.setup = sandbox.stub(annotator, 'setupAnnotations'); - stubs.show = sandbox.stub(annotator, 'loadAnnotations'); - stubs.setupMobileDialog = sandbox.stub(annotator, 'setupMobileDialog'); - stubs.getPermissions = sandbox.stub(annotator, 'getAnnotationPermissions'); + annotator.setScale = jest.fn(); + annotator.setupAnnotations = jest.fn(); + annotator.loadAnnotations = jest.fn(); + annotator.setupMobileDialog = jest.fn(); + annotator.getAnnotationPermissions = jest.fn(); annotator.permissions = { canAnnotate: true }; }); it('should set scale and setup annotations', () => { + annotator.isMobile = false; annotator.init(5); - expect(stubs.scale).to.be.calledWith(5); - expect(stubs.setup).to.be.called; - expect(stubs.show).to.be.called; + expect(annotator.setScale).toBeCalledWith(5); + expect(annotator.setupAnnotations).toBeCalled(); + expect(annotator.loadAnnotations).toBeCalled(); }); it('should setup mobile dialog for mobile browsers', () => { annotator.isMobile = true; annotator.init(); - expect(stubs.setupMobileDialog).to.be.called; + expect(annotator.setupMobileDialog).toBeCalled(); }); }); describe('setupMobileDialog()', () => { it('should generate mobile annotations dialog and append to container', () => { annotator.container = { - appendChild: sandbox.mock() + appendChild: jest.fn() }; annotator.setupMobileDialog(); - expect(annotator.container.appendChild).to.be.called; - expect(annotator.mobileDialogEl.children.length).to.equal(1); + expect(annotator.container.appendChild).toBeCalled(); + expect(annotator.mobileDialogEl.children.length).toEqual(1); }); }); describe('removeThreadFromSharedDialog()', () => { beforeEach(() => { - sandbox.stub(util, 'hideElement'); - sandbox.stub(util, 'showElement'); + util.hideElement = jest.fn(); + util.showElement = jest.fn(); }); it('should do nothing if the mobile dialog does not exist or is hidden', () => { annotator.removeThreadFromSharedDialog(); - expect(util.hideElement).to.not.be.called; + expect(util.hideElement).not.toBeCalled(); annotator.mobileDialogEl = { classList: { - contains: sandbox.stub().returns(true) + contains: jest.fn().mockReturnValue(true) }, - removeChild: sandbox.stub(), + removeChild: jest.fn(), lastChild: {} }; annotator.removeThreadFromSharedDialog(); - expect(util.hideElement).to.not.be.called; + expect(util.hideElement).not.toBeCalled(); }); it('should generate mobile annotations dialog and append to container', () => { @@ -182,16 +156,16 @@ describe('Annotator', () => { annotator.mobileDialogEl.appendChild(document.createElement('div')); annotator.removeThreadFromSharedDialog(); - expect(util.hideElement).to.be.called; - expect(util.showElement).to.be.called; - expect(annotator.mobileDialogEl.children.length).to.equal(0); + expect(util.hideElement).toBeCalled(); + expect(util.showElement).toBeCalled(); + expect(annotator.mobileDialogEl.children.length).toEqual(0); }); }); describe('loadAnnotations()', () => { beforeEach(() => { - sandbox.stub(annotator, 'render'); - sandbox.stub(annotator, 'emit'); + annotator.render = jest.fn(); + annotator.emit = jest.fn(); }); it('should fetch and then render annotations', () => { @@ -199,8 +173,8 @@ describe('Annotator', () => { annotator.loadAnnotations(); return annotator.fetchPromise .then(() => { - expect(annotator.render).to.be.called; - expect(annotator.emit).to.not.be.called; + expect(annotator.render).toBeCalled(); + expect(annotator.emit).not.toBeCalled(); }) .catch(() => { sinon.assert.failException; @@ -215,49 +189,49 @@ describe('Annotator', () => { sinon.assert.failException; }) .catch((err) => { - expect(annotator.render).to.not.be.called; - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.loadError, err); + expect(annotator.render).not.toBeCalled(); + expect(annotator.emit).toBeCalledWith(ANNOTATOR_EVENT.loadError, err); }); }); }); describe('setupAnnotations()', () => { it('should initialize thread map and bind DOM listeners', () => { - sandbox.stub(annotator, 'bindDOMListeners'); - sandbox.stub(annotator, 'bindCustomListenersOnService'); - sandbox.stub(annotator, 'addListener'); - sandbox.stub(annotator, 'setupControllers'); + annotator.bindDOMListeners = jest.fn(); + annotator.bindCustomListenersOnService = jest.fn(); + annotator.addListener = jest.fn(); + annotator.setupControllers = jest.fn(); annotator.setupAnnotations(); - expect(annotator.bindDOMListeners).to.be.called; - expect(annotator.bindCustomListenersOnService).to.be.called; - expect(annotator.setupControllers).to.be.called; - expect(annotator.addListener).to.be.calledWith(ANNOTATOR_EVENT.scale, sinon.match.func); + expect(annotator.bindDOMListeners).toBeCalled(); + expect(annotator.bindCustomListenersOnService).toBeCalled(); + expect(annotator.setupControllers).toBeCalled(); + expect(annotator.addListener).toBeCalledWith(ANNOTATOR_EVENT.scale, expect.any(Function)); }); }); describe('setupControllers()', () => { it('should instantiate controllers for enabled types', () => { - annotator.modeControllers = { something: stubs.controller }; + annotator.modeControllers = { something: controller }; annotator.options = { modeButtons: { something: {} } }; - stubs.controllerMock.expects('init'); - stubs.controllerMock.expects('addListener').withArgs('annotationcontrollerevent', sinon.match.func); annotator.setupControllers(); + expect(controller.init).toBeCalled(); + expect(controller.addListener).toBeCalledWith('annotationcontrollerevent', expect.any(Function)); }); it('should setup shared point dialog in the point controller', () => { - annotator.modeControllers = { point: stubs.controller }; + annotator.modeControllers = { point: controller }; annotator.isMobile = true; - stubs.controllerMock.expects('init'); - stubs.controllerMock.expects('setupSharedDialog').withArgs(annotator.container, { + annotator.setupControllers(); + expect(controller.init).toBeCalled(); + expect(controller.setupSharedDialog).toBeCalledWith(annotator.container, { isMobile: annotator.isMobile, hasTouch: annotator.hasTouch, localized: annotator.localized }); - annotator.setupControllers(); }); }); @@ -265,24 +239,24 @@ describe('Annotator', () => { beforeEach(() => { const annotatedEl = document.querySelector(SELECTOR_ANNOTATED_ELEMENT); annotator.annotatedElement = annotatedEl; - sandbox.stub(annotator, 'getAnnotatedEl').returns(annotatedEl); - sandbox.stub(annotator, 'setupAnnotations'); - sandbox.stub(annotator, 'loadAnnotations'); + annotator.getAnnotatedEl = jest.fn().mockReturnValue(annotatedEl); + annotator.setupAnnotations = jest.fn(); + annotator.loadAnnotations = jest.fn(); annotator.init(); annotator.setupControllers(); }); describe('destroy()', () => { it('should unbind custom listeners on thread and unbind DOM listeners', () => { - const unbindDOMStub = sandbox.stub(annotator, 'unbindDOMListeners'); - const unbindCustomListenersOnService = sandbox.stub(annotator, 'unbindCustomListenersOnService'); - const unbindListener = sandbox.stub(annotator, 'removeListener'); + annotator.unbindDOMListeners = jest.fn(); + annotator.unbindCustomListenersOnService = jest.fn(); + annotator.removeListener = jest.fn(); annotator.destroy(); - expect(unbindDOMStub).to.be.called; - expect(unbindCustomListenersOnService).to.be.called; - expect(unbindListener).to.be.calledWith(ANNOTATOR_EVENT.scale, sinon.match.func); + expect(annotator.unbindDOMListeners).toBeCalled(); + expect(annotator.unbindCustomListenersOnService).toBeCalled(); + expect(annotator.removeListener).toBeCalledWith(ANNOTATOR_EVENT.scale, expect.any(Function)); }); }); @@ -290,16 +264,16 @@ describe('Annotator', () => { it('should call hide on each thread in map', () => { annotator.modeControllers = { type: { - render: sandbox.stub() + render: jest.fn() }, type2: { - render: sandbox.stub() + render: jest.fn() } }; annotator.render(); - expect(annotator.modeControllers.type.render).to.be.called; - expect(annotator.modeControllers.type2.render).to.be.called; + expect(annotator.modeControllers.type.render).toBeCalled(); + expect(annotator.modeControllers.type2.render).toBeCalled(); }); }); @@ -307,15 +281,15 @@ describe('Annotator', () => { it('should call hide on each thread in map on page 1', () => { annotator.modeControllers = { type: { - renderPage: sandbox.stub() + renderPage: jest.fn() }, type2: { - renderPage: sandbox.stub() + renderPage: jest.fn() } }; annotator.renderPage(1); - expect(annotator.modeControllers.type.renderPage).to.be.calledWith(1); - expect(annotator.modeControllers.type2.renderPage).to.be.called; + expect(annotator.modeControllers.type.renderPage).toBeCalledWith(1); + expect(annotator.modeControllers.type2.renderPage).toBeCalled(); }); }); @@ -329,9 +303,9 @@ describe('Annotator', () => { } }; const permissions = annotator.getAnnotationPermissions(file); - expect(permissions.canAnnotate).to.be.false; - expect(permissions.canViewOwnAnnotations).to.be.true; - expect(permissions.canViewAllAnnotations).to.be.false; + expect(permissions.canAnnotate).toBeFalsy(); + expect(permissions.canViewOwnAnnotations).toBeTruthy(); + expect(permissions.canViewAllAnnotations).toBeFalsy(); }); }); @@ -339,49 +313,42 @@ describe('Annotator', () => { it('should set a data-scale attribute on the annotated element', () => { annotator.setScale(10); const annotatedEl = document.querySelector(SELECTOR_ANNOTATED_ELEMENT); - expect(annotatedEl).to.have.attribute('data-scale', '10'); + expect(annotatedEl.dataset.scale).toEqual('10'); }); }); describe('fetchAnnotations()', () => { - beforeEach(() => { - annotator.annotationService = { - getThreadMap: () => {} - }; - stubs.serviceMock = sandbox.mock(annotator.annotationService); + const threadMap = { + someID: [{}, {}], + someID2: [{}] + }; + const threadPromise = Promise.resolve(threadMap); - const threadMap = { - someID: [{}, {}], - someID2: [{}] - }; - stubs.threadPromise = Promise.resolve(threadMap); + beforeEach(() => { + annotator.annotationService.getThreadMap = jest.fn(); annotator.permissions = { canViewAllAnnotations: true, canViewOwnAnnotations: true }; - sandbox.stub(annotator, 'emit'); + annotator.emit = jest.fn(); }); it('should not fetch existing annotations if the user does not have correct permissions', () => { - stubs.serviceMock.expects('getThreadMap').never(); annotator.permissions = { canViewAllAnnotations: false, canViewOwnAnnotations: false }; const result = annotator.fetchAnnotations(); - result - .then(() => { - expect(result).to.be.true; - }) - .catch(() => { - sinon.assert.failException; - }); + result.then(() => { + expect(result).toBeTruthy(); + expect(annotator.annotationService.getThreadMap).not.toBeCalled(); + }); }); it('should fetch existing annotations if the user can view all annotations', () => { - stubs.serviceMock.expects('getThreadMap').returns(stubs.threadPromise); + annotator.annotationService.getThreadMap = jest.fn().mockReturnValue(threadPromise); annotator.permissions = { canViewAllAnnotations: false, canViewOwnAnnotations: true @@ -390,9 +357,9 @@ describe('Annotator', () => { const result = annotator.fetchAnnotations(); result .then(() => { - expect(result).to.be.true; - expect(annotator.threadMap).to.not.be.undefined; - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.fetch); + expect(result).toBeTruthy(); + expect(annotator.threadMap).not.toBeUndefined(); + expect(annotator.emit).toBeCalledWith(ANNOTATOR_EVENT.fetch); }) .catch(() => { sinon.assert.failException; @@ -400,121 +367,104 @@ describe('Annotator', () => { }); it('should fetch existing annotations if the user can view all annotations', () => { - stubs.serviceMock.expects('getThreadMap').returns(stubs.threadPromise); + annotator.annotationService.getThreadMap = jest.fn().mockReturnValue(threadPromise); annotator.permissions = { canViewAllAnnotations: true, canViewOwnAnnotations: false }; const result = annotator.fetchAnnotations(); - result - .then(() => { - expect(result).to.be.true; - stubs.threadPromise.then(() => { - expect(annotator.threadMap).to.not.be.undefined; - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.fetch); - }); - }) - .catch(() => { - sinon.assert.failException; + result.then(() => { + expect(result).toBeTruthy(); + threadPromise.then(() => { + expect(annotator.threadMap).not.toBeUndefined(); + expect(annotator.emit).toBeCalledWith(ANNOTATOR_EVENT.fetch); }); + }); }); }); describe('generateThreadMap()', () => { + const threadMap = { '123abc': thread }; + beforeEach(() => { - stubs.threadMap = { '123abc': stubs.thread }; const annotation = { location: {}, type: 'highlight' }; const lastAnnotation = { location: {}, type: 'highlight-comment' }; - sandbox.stub(util, 'getFirstAnnotation').returns(annotation); - sandbox.stub(util, 'getLastAnnotation').returns(lastAnnotation); + util.getFirstAnnotation = jest.fn().mockReturnValue(annotation); + util.getLastAnnotation = jest.fn().mockReturnValue(lastAnnotation); + annotator.createAnnotationThread = jest.fn(); }); it('should do nothing if annotator conf does not exist in options', () => { annotator.options = {}; - sandbox.stub(annotator, 'createAnnotationThread'); - annotator.generateThreadMap(stubs.threadMap); - expect(annotator.createAnnotationThread).to.not.be.called; + annotator.generateThreadMap(threadMap); + expect(annotator.createAnnotationThread).not.toBeCalled(); }); it('should reset and create a new thread map by from annotations fetched from server', () => { annotator.options.annotator = { NAME: 'name', TYPE: ['highlight-comment'] }; - sandbox.stub(annotator, 'createAnnotationThread').returns(stubs.thread); - annotator.generateThreadMap(stubs.threadMap); - expect(annotator.createAnnotationThread).to.be.called; + annotator.createAnnotationThread = jest.fn().mockReturnValue(thread); + annotator.generateThreadMap(threadMap); + expect(annotator.createAnnotationThread).toBeCalled(); }); it('should register thread if controller exists', () => { annotator.options.annotator = { NAME: 'name', TYPE: ['highlight-comment'] }; - annotator.modeControllers['highlight-comment'] = stubs.controller; - sandbox.stub(annotator, 'createAnnotationThread').returns(stubs.thread); - stubs.controllerMock.expects('registerThread'); - annotator.generateThreadMap(stubs.threadMap); + annotator.modeControllers['highlight-comment'] = controller; + annotator.createAnnotationThread = jest.fn().mockReturnValue(thread); + annotator.generateThreadMap(threadMap); + expect(controller.registerThread).toBeCalled(); }); it('should not register a highlight comment thread with a plain highlight for the first annotation', () => { annotator.options.annotator = { NAME: 'name', TYPE: ['highlight'] }; - annotator.modeControllers['highlight-comment'] = stubs.controller; - stubs.controllerMock.expects('registerThread').never(); - annotator.generateThreadMap(stubs.threadMap); + annotator.modeControllers['highlight-comment'] = controller; + annotator.generateThreadMap(threadMap); + expect(controller.registerThread).not.toBeCalled(); }); }); describe('bindCustomListenersOnService()', () => { - it('should do nothing if the service does not exist', () => { - annotator.annotationService = { - addListener: sandbox.stub() - }; - - annotator.bindCustomListenersOnService(); - expect(annotator.annotationService.addListener).to.not.be.called; - }); - it('should add an event listener', () => { - annotator.annotationService = new AnnotationService({ - apiHost: 'API', - fileId: 1, - token: 'someToken', - canAnnotate: true, - canDelete: true - }); - const addListenerStub = sandbox.stub(annotator.annotationService, 'addListener'); - + annotator.annotationService.addListener = jest.fn(); annotator.bindCustomListenersOnService(); - expect(addListenerStub).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.func); + expect(annotator.annotationService.addListener).toBeCalledWith( + ANNOTATOR_EVENT.error, + expect.any(Function) + ); }); }); describe('handleServicesErrors()', () => { beforeEach(() => { - sandbox.stub(annotator, 'emit'); + annotator.emit = jest.fn(); }); it('should emit annotatorerror on read error event', () => { annotator.handleServicesErrors({ reason: 'read' }); - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); + expect(annotator.emit).toBeCalledWith(ANNOTATOR_EVENT.error, expect.any(String)); }); it('should emit annotatorerror and show annotations on create error event', () => { annotator.handleServicesErrors({ reason: 'create' }); - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); - expect(annotator.loadAnnotations).to.be.called; + expect(annotator.emit).toBeCalledWith(ANNOTATOR_EVENT.error, expect.any(String)); + expect(annotator.loadAnnotations).toBeCalled(); }); it('should emit annotatorerror and show annotations on delete error event', () => { annotator.handleServicesErrors({ reason: 'delete' }); - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); - expect(annotator.loadAnnotations).to.be.called; + expect(annotator.emit).toBeCalledWith(ANNOTATOR_EVENT.error, expect.any(String)); + expect(annotator.loadAnnotations).toBeCalled(); }); it('should emit annotatorerror on authorization error event', () => { annotator.handleServicesErrors({ reason: 'authorization' }); - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); + expect(annotator.emit).toBeCalledWith(ANNOTATOR_EVENT.error, expect.any(String)); }); it('should not emit annotatorerror when event does not match', () => { annotator.handleServicesErrors({ reason: 'no match' }); - expect(annotator.emit).to.not.be.called; + expect(annotator.emit).not.toBeCalled(); }); }); @@ -523,99 +473,85 @@ describe('Annotator', () => { const data = { mode }; beforeEach(() => { - sandbox.stub(annotator, 'emit'); + annotator.emit = jest.fn(); }); it('should reset mobile annotation dialog on resetMobileDialog', () => { - sandbox.stub(annotator, 'removeThreadFromSharedDialog'); + annotator.removeThreadFromSharedDialog = jest.fn(); data.event = CONTROLLER_EVENT.resetMobileDialog; annotator.handleControllerEvents(data); - expect(annotator.removeThreadFromSharedDialog).to.be.called; + expect(annotator.removeThreadFromSharedDialog).toBeCalled(); }); it('should toggle annotation mode on togglemode', () => { - sandbox.stub(annotator, 'toggleAnnotationMode'); + annotator.toggleAnnotationMode = jest.fn(); data.event = CONTROLLER_EVENT.toggleMode; annotator.handleControllerEvents(data); - expect(annotator.toggleAnnotationMode).to.be.calledWith(mode); + expect(annotator.toggleAnnotationMode).toBeCalledWith(mode); }); it('should unbind dom listeners and emit message on mode enter', () => { - sandbox.stub(annotator, 'unbindDOMListeners'); + annotator.unbindDOMListeners = jest.fn(); data.event = CONTROLLER_EVENT.enter; annotator.handleControllerEvents(data); - expect(annotator.unbindDOMListeners).to.be.called; - expect(annotator.emit).to.be.calledWith(data.event, sinon.match.object); + expect(annotator.unbindDOMListeners).toBeCalled(); + expect(annotator.emit).toBeCalledWith(data.event, expect.any(Object)); }); it('should bind dom listeners and emit message on mode exit', () => { - sandbox.stub(annotator, 'bindDOMListeners'); + annotator.bindDOMListeners = jest.fn(); data.event = CONTROLLER_EVENT.exit; annotator.handleControllerEvents(data); - expect(annotator.bindDOMListeners).to.be.called; - expect(annotator.emit).to.be.calledWith(data.event, sinon.match.object); + expect(annotator.bindDOMListeners).toBeCalled(); + expect(annotator.emit).toBeCalledWith(data.event, expect.any(Object)); }); it('should create a point annotation thread on createThread', () => { - sandbox.stub(annotator, 'createPointThread'); + annotator.createPointThread = jest.fn(); data.event = CONTROLLER_EVENT.createThread; annotator.handleControllerEvents(data); - expect(annotator.createPointThread).to.be.calledWith(data.data); + expect(annotator.createPointThread).toBeCalledWith(data.data); }); }); describe('unbindCustomListenersOnService()', () => { - it('should do nothing if the service does not exist', () => { - annotator.annotationService = { - removeListener: sandbox.stub() - }; - - annotator.unbindCustomListenersOnService(); - expect(annotator.annotationService.removeListener).to.not.be.called; - }); - it('should remove an event listener', () => { - annotator.annotationService = new AnnotationService({ - apiHost: 'API', - fileId: 1, - token: 'someToken', - canAnnotate: true, - canDelete: true - }); - const removeListenerStub = sandbox.stub(annotator.annotationService, 'removeListener'); - + annotator.annotationService.removeListener = jest.fn(); annotator.unbindCustomListenersOnService(); - expect(removeListenerStub).to.be.called; + expect(annotator.annotationService.removeListener).toBeCalled(); }); }); describe('getCurrentAnnotationMode()', () => { it('should return null if no mode is enabled', () => { - annotator.modeControllers.something = stubs.controller; - stubs.controllerMock.expects('isEnabled').returns(false); - expect(annotator.getCurrentAnnotationMode()).to.be.null; + annotator.modeControllers.something = controller; + controller.isEnabled = jest.fn().mockReturnValue(false); + expect(annotator.getCurrentAnnotationMode()).toBeNull(); }); it('should return the current annotation mode', () => { - annotator.modeControllers.something = stubs.controller; - stubs.controllerMock.expects('isEnabled').returns(true); - expect(annotator.getCurrentAnnotationMode()).to.equal('something'); + annotator.modeControllers.something = controller; + controller.isEnabled = jest.fn().mockReturnValue(true); + expect(annotator.getCurrentAnnotationMode()).toEqual('something'); }); it('should null if no controllers exist', () => { annotator.modeControllers = {}; - expect(annotator.getCurrentAnnotationMode()).to.be.null; + expect(annotator.getCurrentAnnotationMode()).toBeNull(); }); }); describe('createPointThread()', () => { beforeEach(() => { - stubs.getLoc = sandbox.stub(annotator, 'getLocationFromEvent').returns({ page: 1 }); - sandbox.stub(annotator, 'emit'); - stubs.thread.dialog = { postAnnotation: sandbox.stub() }; + annotator.getLocationFromEvent = jest.fn().mockReturnValue({ page: 1 }); + annotator.emit = jest.fn(); + thread.dialog = { + postAnnotation: jest.fn(), + hasComments: false + }; annotator.modeControllers = { - point: stubs.controller + point: controller }; }); @@ -640,36 +576,35 @@ describe('Annotator', () => { pendingThreadID: '123abc', commentText: ' ' }); - expect(annotator.emit).to.not.be.called; + expect(annotator.emit).not.toBeCalled(); }); it('should do nothing if no location is returned fom the lastPointEvent', () => { - stubs.getLoc.returns(null); + annotator.getLocationFromEvent = jest.fn().mockReturnValue(null); const result = annotator.createPointThread({ lastPointEvent: {}, pendingThreadID: '123abc', commentText: 'text' }); - expect(annotator.emit).to.not.be.called; - expect(result).to.be.null; + expect(annotator.emit).not.toBeCalled(); + expect(result).toBeNull(); }); it('should do nothing the thread does not exist in the page specified by lastPointEvent', () => { - stubs.controllerMock.expects('getThreadByID').returns(null); + controller.getThreadByID = jest.fn().mockReturnValue(null); const result = annotator.createPointThread({ lastPointEvent: {}, pendingThreadID: '123abc', commentText: 'text' }); - expect(annotator.emit).to.not.be.called; - expect(result).to.be.null; + expect(annotator.emit).not.toBeCalled(); + expect(result).toBeNull(); }); it('should create a point annotation thread using lastPointEvent', () => { - stubs.threadMock.expects('showDialog'); - stubs.threadMock.expects('getThreadEventData').returns({}); - stubs.controllerMock.expects('getThreadByID').returns(stubs.thread); + thread.getThreadEventData = jest.fn().mockReturnValue({}); + controller.getThreadByID = jest.fn().mockReturnValue(thread); const result = annotator.createPointThread({ lastPointEvent: {}, @@ -677,41 +612,42 @@ describe('Annotator', () => { commentText: 'text' }); - expect(stubs.thread.dialog.hasComments).to.be.true; - expect(stubs.thread.state).to.equal(STATES.hover); - expect(stubs.thread.dialog.postAnnotation).to.be.calledWith('text'); - expect(annotator.emit).to.be.calledWith(THREAD_EVENT.threadSave, sinon.match.object); - expect(result).to.not.be.null; + expect(thread.dialog.hasComments).toBeTruthy(); + expect(thread.state).toEqual(STATES.hover); + expect(thread.dialog.postAnnotation).toBeCalledWith('text'); + expect(annotator.emit).toBeCalledWith(THREAD_EVENT.threadSave, expect.any(Object)); + expect(result).not.toBeNull(); + expect(thread.showDialog).toBeCalled(); }); }); describe('scrollToAnnotation()', () => { it('should do nothing if no threadID is provided', () => { - stubs.threadMock.expects('scrollIntoView').never(); annotator.scrollToAnnotation(); + expect(thread.scrollIntoView).not.toBeCalled(); }); it('should do nothing if threadID does not exist on page', () => { - stubs.threadMock.expects('scrollIntoView').never(); annotator.scrollToAnnotation('wrong'); + expect(thread.scrollIntoView).not.toBeCalled(); }); it('should scroll to annotation if threadID exists on page', () => { annotator.modeControllers = { type: { - getThreadByID: sandbox.stub().returns(stubs.thread), - threads: { 1: { '123abc': stubs.thread } } + getThreadByID: jest.fn().mockReturnValue(thread), + threads: { 1: { '123abc': thread } } } }; - stubs.threadMock.expects('scrollIntoView'); - annotator.scrollToAnnotation(stubs.thread.threadID); + annotator.scrollToAnnotation(thread.threadID); + expect(thread.scrollIntoView).toBeCalled(); }); }); describe('scaleAnnotations()', () => { it('should set scale and rotate annotations based on the annotated element', () => { - sandbox.stub(annotator, 'setScale'); - sandbox.stub(annotator, 'render'); + annotator.setScale = jest.fn(); + annotator.render = jest.fn(); const data = { scale: 5, @@ -719,94 +655,67 @@ describe('Annotator', () => { pageNum: 2 }; annotator.scaleAnnotations(data); - expect(annotator.setScale).to.be.calledWith(data.scale); - expect(annotator.render).to.be.called; + expect(annotator.setScale).toBeCalledWith(data.scale); + expect(annotator.render).toBeCalled(); }); }); describe('toggleAnnotationMode()', () => { beforeEach(() => { - annotator.modeControllers.something = stubs.controller; - sandbox.stub(annotator, 'hideAnnotations'); + annotator.modeControllers.something = controller; + annotator.hideAnnotations = jest.fn(); }); it('should exit the current mode', () => { - stubs.controllerMock.expects('isEnabled').returns(true); - stubs.controllerMock.expects('exit'); + controller.isEnabled = jest.fn().mockReturnValue(true); annotator.toggleAnnotationMode('something'); - expect(annotator.hideAnnotations).to.be.called; + expect(annotator.hideAnnotations).toBeCalled(); + expect(controller.exit).toBeCalled(); }); it('should enter the specified mode', () => { - sandbox.stub(annotator, 'getCurrentAnnotationMode'); - stubs.controllerMock.expects('enter'); + annotator.getCurrentAnnotationMode = jest.fn(); annotator.toggleAnnotationMode('something'); - expect(annotator.hideAnnotations).to.be.called; + expect(annotator.hideAnnotations).toBeCalled(); + expect(controller.enter).toBeCalled(); }); }); describe('handleValidationError()', () => { it('should do nothing if a annotatorerror was already emitted', () => { - sandbox.stub(annotator, 'emit'); + annotator.emit = jest.fn(); annotator.validationErrorEmitted = true; annotator.handleValidationError(); - expect(annotator.emit).to.not.be.calledWith(ANNOTATOR_EVENT.error); - expect(annotator.validationErrorEmitted).to.be.true; + expect(annotator.emit).not.toBeCalledWith(ANNOTATOR_EVENT.error); + expect(annotator.validationErrorEmitted).toBeTruthy(); }); it('should emit annotatorerror on first error', () => { - sandbox.stub(annotator, 'emit'); + annotator.emit = jest.fn(); annotator.validationErrorEmitted = false; annotator.handleValidationError(); - expect(annotator.emit).to.be.calledWith(ANNOTATOR_EVENT.error, sinon.match.string); - expect(annotator.validationErrorEmitted).to.be.true; + expect(annotator.emit).toBeCalledWith(ANNOTATOR_EVENT.error, expect.any(String)); + expect(annotator.validationErrorEmitted).toBeTruthy(); }); }); describe('emit()', () => { - const emitFunc = EventEmitter.prototype.emit; - - afterEach(() => { - Object.defineProperty(EventEmitter.prototype, 'emit', { value: emitFunc }); - }); - it('should pass through the event as well as broadcast it as a annotator event', () => { - const fileId = '1'; - const fileVersionId = '1'; const event = 'someEvent'; const data = {}; - const annotatorName = 'name'; - - annotator = new Annotator({ - canAnnotate: true, - container: document, - annotationService: {}, - isMobile: false, - annotator: { NAME: annotatorName }, - file: { - id: fileId, - file_version: { - id: fileVersionId - } - }, - location: { locale: 'en-US' }, - modeButtons: {}, - localizedStrings: { anonymousUserName: 'anonymous' } - }); - - const emitStub = sandbox.stub(); - Object.defineProperty(EventEmitter.prototype, 'emit', { value: emitStub }); + let annotatorEventOccured = false; + let eventOccurred = false; + annotator.addListener('annotatorevent', () => { + annotatorEventOccured = true; + }); + annotator.addListener('someEvent', () => { + eventOccurred = true; + }); annotator.emit(event, data); - expect(emitStub).to.be.calledWith(event, data); - expect(emitStub).to.be.calledWithMatch('annotatorevent', { - event, - data, - annotatorName, - fileId, - fileVersionId - }); + expect(annotatorEventOccured).toBeTruthy(); + expect(eventOccurred).toBeTruthy(); }); }); @@ -819,15 +728,15 @@ describe('Annotator', () => { it('should return false if annotations are not allowed on the current viewer', () => { annotator.options.annotator = undefined; - expect(annotator.isModeAnnotatable(TYPES.point)).to.be.false; + expect(annotator.isModeAnnotatable(TYPES.point)).toBeFalsy(); }); it('should return true if the type is supported by the viewer', () => { - expect(annotator.isModeAnnotatable(TYPES.point)).to.be.true; + expect(annotator.isModeAnnotatable(TYPES.point)).toBeTruthy(); }); it('should return false if the type is not supported by the viewer', () => { - expect(annotator.isModeAnnotatable('drawing')).to.be.false; + expect(annotator.isModeAnnotatable('drawing')).toBeFalsy(); }); }); }); diff --git a/src/__tests__/BoxAnnotations-test.js b/src/__tests__/BoxAnnotations-test.js index 264b228a5..e19313187 100644 --- a/src/__tests__/BoxAnnotations-test.js +++ b/src/__tests__/BoxAnnotations-test.js @@ -5,58 +5,53 @@ import * as util from '../util'; import DrawingModeController from '../controllers/DrawingModeController'; let loader; -let stubs; -const sandbox = sinon.sandbox.create(); describe('BoxAnnotations', () => { beforeEach(() => { - stubs = {}; loader = new BoxAnnotations(); }); afterEach(() => { - sandbox.verifyAndRestore(); - if (typeof loader.destroy === 'function') { loader.destroy(); } loader = null; - stubs = null; }); describe('getAnnotators()', () => { it('should return the loader\'s annotators', () => { - expect(loader.getAnnotators()).to.deep.equal(loader.annotators); + expect(loader.getAnnotators()).toStrictEqual(loader.annotators); }); it('should return an empty array if the loader doesn\'t have annotators', () => { loader.annotators = []; - expect(loader.getAnnotators()).to.deep.equal([]); + expect(loader.getAnnotators()).toStrictEqual([]); }); }); describe('getAnnotatorsForViewer()', () => { beforeEach(() => { - stubs.instantiateControllers = sandbox.stub(loader, 'instantiateControllers'); + loader.instantiateControllers = jest.fn(); }); + it('should return undefined if the annotator does not exist', () => { const annotator = loader.getAnnotatorsForViewer('not_supported_type'); - expect(annotator).to.be.undefined; - expect(stubs.instantiateControllers).to.be.called; + expect(annotator).toBeUndefined(); + expect(loader.instantiateControllers).toBeCalled(); }); it('should return the correct annotator for the viewer name', () => { const name = 'Document'; const annotator = loader.getAnnotatorsForViewer(name); - expect(annotator.NAME).to.equal(name); // First entry is Document annotator - expect(stubs.instantiateControllers).to.be.called; + expect(annotator.NAME).toEqual(name); // First entry is Document annotator + expect(loader.instantiateControllers).toBeCalled(); }); it('should return nothing if the viewer requested is disabled', () => { const annotator = loader.getAnnotatorsForViewer('Document', ['Document']); - expect(annotator).to.be.undefined; - expect(stubs.instantiateControllers).to.be.called; + expect(annotator).toBeUndefined(); + expect(loader.instantiateControllers).toBeCalled(); }); }); @@ -65,24 +60,24 @@ describe('BoxAnnotations', () => { const config = { CONTROLLERS: { [TYPES.draw]: { - CONSTRUCTOR: sandbox.stub() + CONSTRUCTOR: jest.fn() } } }; - expect(() => loader.instantiateControllers(config)).to.not.throw(); + expect(() => loader.instantiateControllers(config)).not.toThrow(); }); it('Should do nothing when given an undefined object', () => { const config = undefined; - expect(() => loader.instantiateControllers(config)).to.not.throw(); + expect(() => loader.instantiateControllers(config)).not.toThrow(); }); it('Should do nothing when config has no types', () => { const config = { TYPE: undefined }; - expect(() => loader.instantiateControllers(config)).to.not.throw(); + expect(() => loader.instantiateControllers(config)).not.toThrow(); }); it('Should instantiate controllers and assign them to the CONTROLLERS attribute', () => { @@ -92,65 +87,66 @@ describe('BoxAnnotations', () => { loader.viewerConfig = { enabledTypes: [TYPES.draw] }; loader.instantiateControllers(config); - expect(config.CONTROLLERS).to.not.equal(undefined); - expect(config.CONTROLLERS[TYPES.draw] instanceof DrawingModeController).to.be.true; + expect(config.CONTROLLERS).not.toEqual(undefined); + expect(config.CONTROLLERS[TYPES.draw] instanceof DrawingModeController).toBeTruthy(); const assignedControllers = Object.keys(config.CONTROLLERS); - expect(assignedControllers.length).to.equal(1); + expect(assignedControllers.length).toEqual(1); }); }); describe('getAnnotatorTypes()', () => { - beforeEach(() => { - stubs.config = { - NAME: 'Document', - VIEWER: ['Document'], - TYPE: ['point', 'highlight', 'highlight-comment', 'draw'], - DEFAULT_TYPES: ['point', 'highlight'] - }; - }); + const config = { + NAME: 'Document', + VIEWER: ['Document'], + TYPE: ['point', 'highlight', 'highlight-comment', 'draw'], + DEFAULT_TYPES: ['point', 'highlight'] + }; it('should use the specified types from options', () => { loader.viewerOptions = { Document: { enabledTypes: ['draw'] } }; - expect(loader.getAnnotatorTypes(stubs.config)).to.deep.equal(['draw']); + expect(loader.getAnnotatorTypes(config)).toStrictEqual(['draw']); }); it('should filter disabled annotation types from the annotator.TYPE', () => { loader.viewerConfig = { disabledTypes: ['point'] }; - expect(loader.getAnnotatorTypes(stubs.config)).to.deep.equal(['highlight']); + expect(loader.getAnnotatorTypes(config)).toStrictEqual(['highlight']); }); it('should filter and only keep allowed types of annotations', () => { loader.viewerConfig = { enabledTypes: ['point', 'timestamp'] }; - expect(loader.getAnnotatorTypes(stubs.config)).to.deep.equal(['point']); + expect(loader.getAnnotatorTypes(config)).toStrictEqual(['point']); loader.viewerOptions = { Document: { enabledTypes: ['point', 'timestamp'] } }; - expect(loader.getAnnotatorTypes(stubs.config)).to.deep.equal(['point']); + expect(loader.getAnnotatorTypes(config)).toStrictEqual(['point']); }); it('should respect default annotators if none provided', () => { - expect(loader.getAnnotatorTypes(stubs.config)).to.deep.equal(['point', 'highlight']); + expect(loader.getAnnotatorTypes(config)).toStrictEqual(['point', 'highlight']); }); }); describe('determineAnnotator()', () => { + let annotator; + let options; + beforeEach(() => { - stubs.instantiateControllers = sandbox.stub(loader, 'instantiateControllers'); - stubs.canLoad = sandbox.stub(util, 'canLoadAnnotations').returns(true); + util.canLoadAnnotations = jest.fn().mockReturnValue(true); + loader.instantiateControllers = jest.fn(); - stubs.annotator = { + annotator = { NAME: 'Document', VIEWER: ['Document'], TYPE: ['point'], DEFAULT_TYPES: ['point'] }; - stubs.options = { + options = { file: { permissions: {} }, @@ -158,27 +154,26 @@ describe('BoxAnnotations', () => { NAME: 'Document' } }; - sandbox.stub(loader, 'getAnnotatorTypes').returns(['point']); + loader.getAnnotatorTypes = jest.fn().mockReturnValue(['point']); }); it('should not return an annotator if the user has incorrect permissions/scopes', () => { - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(stubs.annotator); - stubs.canLoad.returns(false); - expect(loader.determineAnnotator(stubs.options)).to.be.null; + loader.getAnnotatorsForViewer = jest.fn().mockReturnValue(annotator); + util.canLoadAnnotations = jest.fn().mockReturnValue(false); + expect(loader.determineAnnotator(options)).toBeNull(); }); it('should choose the first annotator that matches the viewer', () => { const viewer = 'Document'; - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(stubs.annotator); - const annotator = loader.determineAnnotator(stubs.options); - expect(annotator.NAME).to.equal(viewer); - expect(loader.getAnnotatorsForViewer).to.be.called; + loader.getAnnotatorsForViewer = jest.fn().mockReturnValue(annotator); + const result = loader.determineAnnotator(options); + expect(result.NAME).toEqual(viewer); + expect(loader.getAnnotatorsForViewer).toBeCalled(); }); it('should not return an annotator if no matching annotator is found', () => { - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(undefined); - const annotator = loader.determineAnnotator(stubs.options); - expect(annotator).to.be.null; + loader.getAnnotatorsForViewer = jest.fn().mockReturnValue(null); + expect(loader.determineAnnotator(options)).toBeNull(); }); it('should return a copy of the annotator that matches', () => { @@ -191,20 +186,19 @@ describe('BoxAnnotations', () => { DEFAULT_TYPES: ['point'] }; - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(docAnnotator); - const annotator = loader.determineAnnotator(stubs.options); + loader.getAnnotatorsForViewer = jest.fn().mockReturnValue(docAnnotator); + const result = loader.determineAnnotator(options); - stubs.annotator.NAME = 'another_name'; - expect(annotator.NAME).to.not.equal(stubs.annotator.NAME); + annotator.NAME = 'another_name'; + expect(result.NAME).not.toEqual(annotator.NAME); }); it('should return null if the config for the viewer disables annotations', () => { const config = { enabled: false }; - sandbox.stub(loader, 'getAnnotatorsForViewer').returns(stubs.annotator); - const annotator = loader.determineAnnotator(stubs.options, config); - expect(annotator).to.be.null; + loader.getAnnotatorsForViewer = jest.fn().mockReturnValue(annotator); + expect(loader.determineAnnotator(options, config)).toBeNull(); }); }); }); diff --git a/src/__tests__/CommentBox-test.js b/src/__tests__/CommentBox-test.js index 0dc6b42db..2e822ea48 100644 --- a/src/__tests__/CommentBox-test.js +++ b/src/__tests__/CommentBox-test.js @@ -5,11 +5,13 @@ import { CLASS_HIDDEN, SELECTOR_ANNOTATION_BUTTON_CANCEL, SELECTOR_ANNOTATION_BUTTON_POST, + CLASS_ANNOTATION_BUTTON_CANCEL, + CLASS_ANNOTATION_BUTTON_POST, + CLASS_ANNOTATION_TEXTAREA, CLASS_INVALID_INPUT } from '../constants'; describe('CommentBox', () => { - const sandbox = sinon.sandbox.create(); let commentBox; let parentEl; @@ -46,94 +48,72 @@ describe('CommentBox', () => { }); it('should assign the parentEl to the container passed in', () => { - expect(tempCommentBox.parentEl).to.deep.equal(parentEl); + expect(tempCommentBox.parentEl).toStrictEqual(parentEl); }); it('should assign cancelText to the string passed in the config', () => { - expect(tempCommentBox.cancelText).to.equal(localized.cancelButton); + expect(tempCommentBox.cancelText).toEqual(localized.cancelButton); }); it('should assign postText to the string passed in the config', () => { - expect(tempCommentBox.postText).to.equal(localized.postButton); + expect(tempCommentBox.postText).toEqual(localized.postButton); }); it('should assign placeholderText to the string passed in the config', () => { - expect(tempCommentBox.placeholderText).to.equal(localized.addCommentPlaceholder); + expect(tempCommentBox.placeholderText).toEqual(localized.addCommentPlaceholder); }); }); describe('focus()', () => { beforeEach(() => { - // Kickstart creation of UI - commentBox.show(); - }); - - it('should do nothing if the textarea HTML doesn\'t exist', () => { - const focus = sandbox.stub(commentBox.textAreaEl, 'focus'); - - commentBox.textAreaEl.remove(); - commentBox.textAreaEl = null; - - commentBox.focus(); - expect(focus).to.not.be.called; + commentBox.textAreaEl = document.createElement('textarea'); + commentBox.textAreaEl.focus = jest.fn(); }); it('should focus on the text area contained by the comment box', () => { - const focus = sandbox.stub(commentBox.textAreaEl, 'focus'); - commentBox.focus(); - expect(focus).to.be.called; + expect(commentBox.textAreaEl.focus).toBeCalled(); }); }); describe('clear()', () => { - beforeEach(() => { - // Kickstart creation of UI - commentBox.show(); - sandbox.stub(commentBox, 'preventDefaultAndPropagation'); - }); - it('should overwrite the text area\'s value with an empty string', () => { + commentBox.textAreaEl = document.createElement('textarea'); commentBox.textAreaEl.value = 'yay'; commentBox.clear(); - expect(commentBox.textAreaEl.value).to.equal(''); + expect(commentBox.textAreaEl.value).toEqual(''); }); }); describe('hide()', () => { - beforeEach(() => { - // Kickstart creation of UI - commentBox.show(); - sandbox.stub(commentBox, 'preventDefaultAndPropagation'); - }); - it('should do nothing if the comment box HTML doesn\'t exist', () => { - const addClass = sandbox.stub(commentBox.containerEl.classList, 'add'); + util.hideElement = jest.fn(); commentBox.containerEl = null; commentBox.hide(); - - expect(addClass).to.not.be.called; + expect(util.hideElement).not.toBeCalled(); }); it('should add the hidden class to the comment box element', () => { + util.hideElement = jest.fn(); + commentBox.containerEl = document.createElement('div'); commentBox.hide(); - expect(commentBox.containerEl.classList.contains(CLASS_HIDDEN)).to.be.true; + expect(util.hideElement).toBeCalled(); }); }); describe('show()', () => { it('should invoke createComment box, if UI has not been created', () => { - const containerEl = document.createElement('div'); - commentBox.containerEl = undefined; + commentBox.containerEl = null; - const create = sandbox.stub(commentBox, 'createCommentBox').returns(containerEl); - const append = sandbox.stub(parentEl, 'appendChild'); - sandbox.stub(util, 'focusTextArea'); + commentBox.createCommentBox = jest.fn().mockReturnValue(document.createElement('div')); + commentBox.parentEl = document.createElement('div'); + util.focusTextArea = jest.fn(); + util.showElement = jest.fn(); commentBox.show(); - expect(create).to.be.called; - expect(append).to.be.calledWith(commentBox.containerEl); + expect(commentBox.createCommentBox).toBeCalled(); + expect(util.showElement).toBeCalled(); // Nullify to prevent fail during destroy commentBox.containerEl = null; @@ -141,46 +121,41 @@ describe('CommentBox', () => { it('should remove the hidden class from the container', () => { commentBox.show(); - expect(commentBox.containerEl.classList.contains(CLASS_HIDDEN)).to.be.false; - expect(util.focusTextArea).to.be.calledWith(commentBox.textAreaEl); + expect(commentBox.containerEl.classList.contains(CLASS_HIDDEN)).toBeFalsy(); + expect(util.focusTextArea).toBeCalledWith(commentBox.textAreaEl); }); }); describe('destroy()', () => { - it('should do nothing if it\'s UI has not been created', () => { - commentBox.containerEl = undefined; - const unchanged = 'not even the right kind of data'; - commentBox.parentEl = unchanged; - commentBox.destroy(); - expect(commentBox.parentEl).to.equal(unchanged); - }); + beforeEach(() => { + commentBox.containerEl = document.createElement('div'); - it('should remove the UI container from the parent element', () => { - commentBox.show(); - const remove = sandbox.stub(commentBox.containerEl, 'remove'); - commentBox.destroy(); - expect(remove).to.be.called; - }); + commentBox.cancelEl = document.createElement('div'); + commentBox.cancelEl.classList.add(CLASS_ANNOTATION_BUTTON_CANCEL); + commentBox.cancelEl.removeEventListener = jest.fn(); - it('should remove event listener from cancel button', () => { - commentBox.show(); - const remove = sandbox.stub(commentBox.cancelEl, 'removeEventListener'); - commentBox.destroy(); - expect(remove).to.be.called; + commentBox.postEl = document.createElement('div'); + commentBox.postEl.classList.add(CLASS_ANNOTATION_BUTTON_POST); + commentBox.postEl.removeEventListener = jest.fn(); + + commentBox.textAreaEl = document.createElement('div'); + commentBox.textAreaEl.classList.add(CLASS_ANNOTATION_TEXTAREA); + commentBox.textAreaEl.removeEventListener = jest.fn(); }); - it('should remove event listener from post button', () => { - commentBox.show(); - const remove = sandbox.stub(commentBox.postEl, 'removeEventListener'); + it('should do nothing if it\'s UI has not been created', () => { + commentBox.containerEl = undefined; + const unchanged = 'not even the right kind of data'; + commentBox.parentEl = unchanged; commentBox.destroy(); - expect(remove).to.be.called; + expect(commentBox.parentEl).toEqual(unchanged); }); - it('should remove event listener from text area', () => { - commentBox.show(); - const remove = sandbox.stub(commentBox.textAreaEl, 'removeEventListener'); + it('should remove event listeners', () => { commentBox.destroy(); - expect(remove).to.be.called; + expect(commentBox.cancelEl.removeEventListener).toBeCalled(); + expect(commentBox.postEl.removeEventListener).toBeCalled(); + expect(commentBox.textAreaEl.removeEventListener).toBeCalled(); }); }); @@ -191,154 +166,154 @@ describe('CommentBox', () => { }); it('should create and return a section element with ba-create-highlight-comment class on it', () => { - expect(el.nodeName).to.equal('SECTION'); - expect(el.classList.contains('ba-create-comment')).to.be.true; + expect(el.nodeName).toEqual('SECTION'); + expect(el.classList.contains('ba-create-comment')).toBeTruthy(); }); it('should create a text area with the provided placeholder text', () => { - expect(el.querySelector('textarea')).to.exist; + expect(el.querySelector('textarea')).not.toBeUndefined(); }); it('should create a cancel button with the provided cancel text', () => { - expect(el.querySelector(SELECTOR_ANNOTATION_BUTTON_CANCEL)).to.exist; + expect(el.querySelector(SELECTOR_ANNOTATION_BUTTON_CANCEL)).not.toBeUndefined(); }); it('should create a post button with the provided text', () => { - expect(el.querySelector(SELECTOR_ANNOTATION_BUTTON_POST)).to.exist; + expect(el.querySelector(SELECTOR_ANNOTATION_BUTTON_POST)).not.toBeUndefined(); }); }); describe('onCancel()', () => { beforeEach(() => { - sandbox.stub(commentBox, 'preventDefaultAndPropagation'); + commentBox.preventDefaultAndPropagation = jest.fn(); + commentBox.emit = jest.fn(); + commentBox.clear = jest.fn(); }); it('should invoke clear()', () => { - const clear = sandbox.stub(commentBox, 'clear'); - commentBox.onCancel({ preventDefault: () => {} }); - expect(clear).to.be.called; + commentBox.onCancel({ preventDefault: jest.fn() }); + expect(commentBox.clear).toBeCalled(); }); it('should emit a cancel event', () => { - const emit = sandbox.stub(commentBox, 'emit'); - commentBox.onCancel({ preventDefault: () => {} }); - expect(emit).to.be.calledWith(CommentBox.CommentEvents.cancel); + commentBox.onCancel({ preventDefault: jest.fn() }); + expect(commentBox.emit).toBeCalledWith(CommentBox.CommentEvents.cancel); }); }); describe('onPost()', () => { beforeEach(() => { commentBox.textAreaEl = document.createElement('textarea'); - sandbox.stub(commentBox, 'preventDefaultAndPropagation'); + commentBox.preventDefaultAndPropagation = jest.fn(); + commentBox.emit = jest.fn(); + commentBox.clear = jest.fn(); }); it('should invalidate textarea and do nothing if textarea is blank', () => { - const emit = sandbox.stub(commentBox, 'emit'); const text = ''; - commentBox.onPost({ preventDefault: () => {} }); - expect(emit).to.not.be.calledWith(CommentBox.CommentEvents.post, text); - expect(commentBox.textAreaEl).to.have.class(CLASS_INVALID_INPUT); + commentBox.onPost({ preventDefault: jest.fn() }); + expect(commentBox.emit).not.toBeCalledWith(CommentBox.CommentEvents.post, text); + expect(commentBox.textAreaEl.classList).toContain(CLASS_INVALID_INPUT); }); it('should emit a post event with the value of the text box', () => { - const emit = sandbox.stub(commentBox, 'emit'); const text = 'a comment'; commentBox.textAreaEl.value = text; - commentBox.onPost({ preventDefault: () => {} }); - expect(emit).to.be.calledWith(CommentBox.CommentEvents.post, text); + commentBox.onPost({ preventDefault: jest.fn() }); + expect(commentBox.emit).toBeCalledWith(CommentBox.CommentEvents.post, text); }); it('should invoke clear()', () => { - const clear = sandbox.stub(commentBox, 'clear'); - commentBox.onCancel({ preventDefault: () => {} }); - expect(clear).to.be.called; + commentBox.onCancel({ preventDefault: jest.fn() }); + expect(commentBox.clear).toBeCalled(); }); }); describe('createCommentBox()', () => { + beforeEach(() => { + const el = document.createElement('section'); + el.addEventListener = jest.fn(); + + const cancelEl = document.createElement('div'); + cancelEl.classList.add(CLASS_ANNOTATION_BUTTON_CANCEL); + cancelEl.addEventListener = jest.fn(); + el.appendChild(cancelEl); + + const postEl = document.createElement('div'); + postEl.classList.add(CLASS_ANNOTATION_BUTTON_POST); + postEl.addEventListener = jest.fn(); + el.appendChild(postEl); + + const textAreaEl = document.createElement('div'); + textAreaEl.classList.add(CLASS_ANNOTATION_TEXTAREA); + textAreaEl.addEventListener = jest.fn(); + el.appendChild(textAreaEl); + + commentBox.createHTML = jest.fn().mockReturnValue(el); + }); + it('should create and return an HTML element for the UI', () => { const el = commentBox.createCommentBox(); - expect(el.nodeName).to.exist; + expect(el.nodeName).not.toBeUndefined(); }); it('should create a reference to the text area', () => { commentBox.createCommentBox(); - expect(commentBox.textAreaEl).to.exist; + expect(commentBox.textAreaEl).not.toBeUndefined(); }); it('should create a reference to the cancel button', () => { commentBox.createCommentBox(); - expect(commentBox.cancelEl).to.exist; + expect(commentBox.cancelEl).not.toBeUndefined(); }); it('should create a reference to the post button', () => { commentBox.createCommentBox(); - expect(commentBox.postEl).to.exist; + expect(commentBox.postEl).not.toBeUndefined(); }); it('should add an event listener on the textarea, cancel and post buttons for desktop devices', () => { - const uiElement = { - addEventListener: sandbox.stub(), - removeEventListener: sandbox.stub() - }; - const el = document.createElement('section'); - sandbox.stub(el, 'querySelector').returns(uiElement); - sandbox.stub(commentBox, 'createHTML').returns(el); - - commentBox.createCommentBox(); - expect(uiElement.addEventListener).to.be.calledWith('click', commentBox.onCancel); - expect(uiElement.addEventListener).to.be.calledWith('click', commentBox.onPost); - expect(uiElement.addEventListener).to.be.calledWith('focus', commentBox.focus); - expect(uiElement.addEventListener).to.be.calledWith('focus', sinon.match.func); + const containerEl = commentBox.createCommentBox(); + expect(containerEl.addEventListener).not.toBeCalledWith('touchend', expect.any(Function)); + expect(commentBox.postEl.addEventListener).not.toBeCalledWith('touchend', expect.any(Function)); + expect(commentBox.cancelEl.addEventListener).not.toBeCalledWith('touchend', expect.any(Function)); - expect(uiElement.addEventListener).to.not.be.calledWith('touchend', sinon.match.func); - expect(uiElement.addEventListener).to.not.be.calledWith('touchend', sinon.match.func); - expect(uiElement.addEventListener).to.not.be.calledWith('touchend', sinon.match.func); + expect(containerEl.addEventListener).toBeCalledWith('click', expect.any(Function)); + expect(commentBox.cancelEl.addEventListener).toBeCalledWith('click', commentBox.onCancel); + expect(commentBox.postEl.addEventListener).toBeCalledWith('click', commentBox.onPost); + expect(commentBox.textAreaEl.addEventListener).toBeCalledWith('focus', commentBox.focus); }); it('should add an event listener on the textarea, cancel and post buttons if the user is on a touch-enabled non-mobile device', () => { - const uiElement = { - addEventListener: sandbox.stub(), - removeEventListener: sandbox.stub() - }; - const el = document.createElement('section'); - sandbox.stub(el, 'querySelector').returns(uiElement); - sandbox.stub(commentBox, 'createHTML').returns(el); commentBox.hasTouch = true; - commentBox.createCommentBox(); - expect(uiElement.addEventListener).to.be.calledWith('touchend', sinon.match.func); - expect(uiElement.addEventListener).to.be.calledWith('touchend', sinon.match.func); - expect(uiElement.addEventListener).to.be.calledWith('touchend', sinon.match.func); + const containerEl = commentBox.createCommentBox(); + + expect(containerEl.addEventListener).toBeCalledWith('touchend', expect.any(Function)); + expect(commentBox.postEl.addEventListener).toBeCalledWith('touchend', expect.any(Function)); + expect(commentBox.cancelEl.addEventListener).toBeCalledWith('touchend', expect.any(Function)); + expect(containerEl.addEventListener).toBeCalledWith('click', expect.any(Function)); - expect(uiElement.addEventListener).to.be.calledWith('focus', sinon.match.func); - expect(uiElement.addEventListener).to.be.calledWith('click', commentBox.onCancel); - expect(uiElement.addEventListener).to.be.calledWith('click', commentBox.onPost); - expect(uiElement.addEventListener).to.be.calledWith('focus', commentBox.focus); + expect(commentBox.cancelEl.addEventListener).toBeCalledWith('click', commentBox.onCancel); + expect(commentBox.postEl.addEventListener).toBeCalledWith('click', commentBox.onPost); + expect(commentBox.textAreaEl.addEventListener).toBeCalledWith('focus', commentBox.focus); commentBox.containerEl = null; }); it('should add an event listener on the textarea, cancel and post buttons if the user is on a touch-enabled mobile device', () => { - const uiElement = { - addEventListener: sandbox.stub(), - removeEventListener: sandbox.stub() - }; - const el = document.createElement('section'); - sandbox.stub(el, 'querySelector').returns(uiElement); - sandbox.stub(commentBox, 'createHTML').returns(el); commentBox.hasTouch = true; commentBox.isMobile = true; - commentBox.createCommentBox(); - expect(uiElement.addEventListener).to.be.calledWith('touchend', sinon.match.func); - expect(uiElement.addEventListener).to.be.calledWith('touchend', sinon.match.func); - expect(uiElement.addEventListener).to.be.calledWith('touchend', sinon.match.func); - - expect(uiElement.addEventListener).to.not.be.calledWith('focus', sinon.match.func); - expect(uiElement.addEventListener).to.not.be.calledWith('click', commentBox.onCancel); - expect(uiElement.addEventListener).to.not.be.calledWith('click', commentBox.onPost); - expect(uiElement.addEventListener).to.not.be.calledWith('focus', commentBox.focus); + const containerEl = commentBox.createCommentBox(); + expect(containerEl.addEventListener).toBeCalledWith('touchend', expect.any(Function)); + expect(commentBox.postEl.addEventListener).toBeCalledWith('touchend', expect.any(Function)); + expect(commentBox.cancelEl.addEventListener).toBeCalledWith('touchend', expect.any(Function)); + expect(containerEl.addEventListener).toBeCalledWith('click', expect.any(Function)); + + expect(commentBox.cancelEl.addEventListener).not.toBeCalledWith('click', commentBox.onCancel); + expect(commentBox.postEl.addEventListener).not.toBeCalledWith('click', commentBox.onPost); + expect(commentBox.textAreaEl.addEventListener).not.toBeCalledWith('focus', commentBox.focus); commentBox.containerEl = null; }); diff --git a/src/__tests__/CreateAnnotationDialog-test.js b/src/__tests__/CreateAnnotationDialog-test.js index 5d4e7af78..d629d2fad 100644 --- a/src/__tests__/CreateAnnotationDialog-test.js +++ b/src/__tests__/CreateAnnotationDialog-test.js @@ -9,14 +9,12 @@ import { import CommentBox from '../CommentBox'; import * as util from '../util'; -const sandbox = sinon.sandbox.create(); let dialog; let parentEl; const localized = { highlightToggle: 'highlight toggle', highlightComment: 'highlight comment' }; -const stubs = {}; describe('CreateAnnotationDialog', () => { beforeEach(() => { @@ -29,29 +27,27 @@ describe('CreateAnnotationDialog', () => { dialog.commentBox = { containerEl: document.createElement('div'), - hide: () => {}, - show: () => {}, - focus: () => {}, - removeListener: () => {}, - destroy: () => {}, - clear: () => {} + hide: jest.fn(), + show: jest.fn(), + focus: jest.fn(), + removeListener: jest.fn(), + destroy: jest.fn(), + clear: jest.fn() }; - stubs.boxMock = sandbox.mock(dialog.commentBox); }); afterEach(() => { - dialog.destroy(); dialog = null; parentEl = null; }); describe('contructor()', () => { it('should take config falsey value to disable highlights and comments, when passed in', () => { - expect(dialog.parentEl).to.not.be.undefined; - expect(dialog.isMobile).to.be.true; - expect(dialog.hasTouch).to.be.false; - expect(dialog.localized).to.equal(localized); - expect(dialog.isVisible).to.be.false; + expect(dialog.parentEl).not.toBeUndefined(); + expect(dialog.isMobile).toBeTruthy(); + expect(dialog.hasTouch).toBeFalsy(); + expect(dialog.localized).toEqual(localized); + expect(dialog.isVisible).toBeFalsy(); }); }); @@ -59,160 +55,164 @@ describe('CreateAnnotationDialog', () => { it('should assign the new parent reference', () => { const newParent = document.createElement('span'); dialog.setParentEl(newParent); - expect(dialog.parentEl).to.not.deep.equal(parentEl); - expect(dialog.parentEl).to.deep.equal(newParent); + expect(dialog.parentEl).not.toStrictEqual(parentEl); + expect(dialog.parentEl).toStrictEqual(newParent); }); }); describe('setPosition()', () => { - let update; beforeEach(() => { - update = sandbox.stub(dialog, 'updatePosition'); + dialog.updatePosition = jest.fn(); }); it('should set the x and y coordinates to the passed in values', () => { const x = 5; const y = 6; dialog.setPosition(x, y); - expect(dialog.position.x).to.equal(x); - expect(dialog.position.y).to.equal(y); + expect(dialog.position.x).toEqual(x); + expect(dialog.position.y).toEqual(y); }); it('should invoke updatePosition()', () => { dialog.setPosition(0, 0); - expect(update).to.be.called; + expect(dialog.updatePosition).toBeCalled(); }); }); describe('show()', () => { it('should invoke createElement() if no UI element has been created', () => { - const create = sandbox.spy(dialog, 'createElement'); + dialog.createElement = jest.fn(); dialog.show(); - expect(create.called).to.be.true; + expect(dialog.createElement).toBeCalled(); }); it('should set the parentEl to a new reference, via setParentEl(), if a new one is supplied', () => { - const set = sandbox.stub(dialog, 'setParentEl'); + dialog.setParentEl = jest.fn(); const newParent = document.createElement('span'); dialog.show(newParent); - expect(set).to.be.calledWith(newParent); + expect(dialog.setParentEl).toBeCalledWith(newParent); }); it('should invoke setButtonVisibility() to show the highlight buttons', () => { - const setVis = sandbox.stub(dialog, 'setButtonVisibility'); + dialog.setButtonVisibility = jest.fn(); dialog.show(); - expect(setVis).to.be.called; + expect(dialog.setButtonVisibility).toBeCalled(); }); it('should remove the hidden class from the UI element', () => { - const emit = sandbox.stub(dialog, 'emit'); + dialog.emit = jest.fn(); dialog.show(); - expect(dialog.containerEl.classList.contains(CLASS_HIDDEN)).to.be.false; - expect(emit).to.be.calledWith(CREATE_EVENT.init); + expect(dialog.containerEl.classList.contains(CLASS_HIDDEN)).toBeFalsy(); + expect(dialog.emit).toBeCalledWith(CREATE_EVENT.init); }); }); describe('showCommentBox()', () => { it('should show and focus comment box', () => { - stubs.boxMock.expects('show'); - stubs.boxMock.expects('focus'); + dialog.commentBox = { + show: jest.fn(), + focus: jest.fn() + }; dialog.showCommentBox(); + expect(dialog.commentBox.show).toBeCalled(); + expect(dialog.commentBox.focus).toBeCalled(); }); }); describe('hide()', () => { beforeEach(() => { - dialog.show(); - }); + util.hideElement = jest.fn(); - it('should do nothing if there is no UI element', () => { - dialog.containerEl = null; - const hideComment = sandbox.stub(dialog.commentBox, 'hide'); - dialog.hide(); - expect(hideComment).to.not.be.called; + dialog.containerEl = document.createElement('div'); + dialog.commentBox = { + hide: jest.fn(), + clear: jest.fn() + }; + dialog.isVisible = true; }); - it('should add the hidden class to the ui element', () => { - dialog.hide(); - expect(dialog.containerEl.classList.contains(CLASS_HIDDEN)).to.be.true; + afterEach(() => { + expect(dialog.isVisible).toBeFalsy(); }); - it('should hide the comment box', () => { - const hideComment = sandbox.stub(dialog.commentBox, 'hide'); + it('should do nothing if there is no UI element', () => { + dialog.containerEl = null; dialog.hide(); - expect(hideComment).to.be.called; + expect(dialog.commentBox.hide).not.toBeCalled(); }); - it('should clear the comment box', () => { - const clearComment = sandbox.stub(dialog.commentBox, 'clear'); + it('should add the hidden class to the ui element', () => { + dialog.commentBox = null; dialog.hide(); - expect(clearComment).to.be.called; + expect(util.hideElement).toBeCalledWith(dialog.containerEl); }); - it('should do nothing with the comment box if it does not exist', () => { - const clearComment = sandbox.stub(dialog.commentBox, 'clear'); - dialog.commentBox = null; + it('should hide and clear the comment box', () => { + dialog.commentBox = { + hide: jest.fn(), + clear: jest.fn() + }; dialog.hide(); - expect(clearComment).to.not.be.called; + expect(dialog.commentBox.hide).toBeCalled(); + expect(dialog.commentBox.clear).toBeCalled(); }); }); describe('destroy()', () => { + let commentBox; + let containerEl; + beforeEach(() => { - sandbox.stub(dialog, 'setupCommentBox').returns(dialog.commentBox); - dialog.show(); + dialog.hide = jest.fn(); + + containerEl = document.createElement('div'); + containerEl.removeEventListener = jest.fn(); + dialog.containerEl = containerEl; + + commentBox = { + removeListener: jest.fn(), + destroy: jest.fn() + }; + dialog.commentBox = commentBox; }); it('should do nothing if no ui has been created', () => { dialog.containerEl = null; - const hide = sandbox.stub(dialog, 'hide'); dialog.destroy(); - expect(hide).to.not.be.called; + expect(dialog.hide).not.toBeCalled(); }); it('should remove events that are bound to stopPropagation()', () => { dialog.isMobile = false; - const remove = sandbox.stub(dialog.containerEl, 'removeEventListener'); - dialog.destroy(); - expect(remove).to.be.calledWith('click'); - expect(remove).to.be.calledWith('mouseup'); - expect(remove).to.be.calledWith('dblclick'); - }); - - it('should remove "Post" event listener from the comment box', () => { - const remove = sandbox.stub(dialog.commentBox, 'removeListener'); dialog.destroy(); - expect(remove).to.be.calledWith(CommentBox.CommentEvents.post); + expect(containerEl.removeEventListener).toBeCalledWith('click', expect.any(Function)); + expect(containerEl.removeEventListener).toBeCalledWith('mouseup', expect.any(Function)); + expect(containerEl.removeEventListener).toBeCalledWith('dblclick', expect.any(Function)); }); - it('should remove "Cancel" event listener from the comment box', () => { - const remove = sandbox.stub(dialog.commentBox, 'removeListener'); + it('should remove event listeners from the comment box', () => { dialog.destroy(); - expect(remove).to.be.calledWith(CommentBox.CommentEvents.cancel); + expect(commentBox.removeListener).toBeCalledWith(CommentBox.CommentEvents.post, expect.any(Function)); + expect(commentBox.removeListener).toBeCalledWith(CommentBox.CommentEvents.cancel, expect.any(Function)); }); it('should remove the ui element from the dom', () => { - const remove = sandbox.stub(dialog.containerEl, 'remove'); dialog.destroy(); - expect(remove).to.be.called; + expect(commentBox.removeListener).toBeCalled(); }); it('should destroy the comment box', () => { - const destroy = sandbox.stub(dialog.commentBox, 'destroy'); dialog.destroy(); - expect(destroy).to.be.called; + expect(commentBox.destroy).toBeCalled(); }); it('should remove out all touch events, if touch enabled', () => { - dialog.destroy(); - dialog.hasTouch = true; dialog.isMobile = true; - dialog.show(document.createElement('div')); const eventStub = [ { - stub: sandbox.stub(dialog.containerEl, 'removeEventListener'), + stub: containerEl.removeEventListener, args: ['touchend', dialog.stopPropagation] } ]; @@ -220,22 +220,23 @@ describe('CreateAnnotationDialog', () => { dialog.destroy(); eventStub.forEach((stub) => { - expect(stub.stub).to.be.calledWith(...stub.args); + expect(stub.stub).toBeCalledWith(...stub.args); }); }); }); describe('updatePosition()', () => { beforeEach(() => { - dialog.show(); dialog.isMobile = false; + dialog.containerEl = document.createElement('div'); + dialog.destroy = jest.fn(); }); it('should update the top of the ui element', () => { const y = 50; dialog.position.y = y; dialog.updatePosition(); - expect(dialog.containerEl.style.top).to.equal(`${y + 5}px`); + expect(dialog.containerEl.style.top).toEqual(`${y + 5}px`); }); it('should update the left of the ui element, to center it', () => { @@ -243,7 +244,7 @@ describe('CreateAnnotationDialog', () => { const x = 50; dialog.position.x = x; dialog.updatePosition(); - expect(dialog.containerEl.style.left).to.equal(`${x - 1 - width / 2}px`); + expect(dialog.containerEl.style.left).toEqual(`${x - 1 - width / 2}px`); }); it('should do nothing if user is on a mobile device', () => { @@ -251,80 +252,85 @@ describe('CreateAnnotationDialog', () => { dialog.position.y = y; dialog.isMobile = true; dialog.updatePosition(); - expect(dialog.containerEl.style.top).to.not.equal(`${y + 5}px`); + expect(dialog.containerEl.style.top).not.toEqual(`${y + 5}px`); }); }); describe('onCommentPost()', () => { beforeEach(() => { - dialog.show(); + dialog.emit = jest.fn(); + dialog.updatePosition = jest.fn(); + dialog.commentBox = { + blur: jest.fn(), + clear: jest.fn() + }; }); it('should invoke the "post" event with the string provided', () => { - const emit = sandbox.stub(dialog, 'emit'); const text = 'some text'; dialog.onCommentPost(text); - expect(emit).to.be.calledWith(CREATE_EVENT.post, text); + expect(dialog.emit).toBeCalledWith(CREATE_EVENT.post, text); }); it('should invoke clear() on the comment box', () => { - const clear = sandbox.stub(dialog.commentBox, 'clear'); dialog.onCommentPost('A message'); - expect(clear).to.be.called; + expect(dialog.commentBox.clear).toBeCalled(); }); it('should invoke blur() on the comment box', () => { - const blur = sandbox.stub(dialog.commentBox, 'blur'); dialog.onCommentPost('A message'); - expect(blur).to.be.called; + expect(dialog.commentBox.blur).toBeCalled(); }); }); describe('onCommentCancel()', () => { beforeEach(() => { - dialog.show(); + dialog.emit = jest.fn(); + dialog.setButtonVisibility = jest.fn(); + dialog.updatePosition = jest.fn(); + dialog.commentBox = { + hide: jest.fn(), + clear: jest.fn() + }; }); it('should hide the comment box', () => { - const hide = sandbox.stub(dialog.commentBox, 'hide'); dialog.onCommentCancel(); - expect(hide).to.be.called; + expect(dialog.commentBox.hide).toBeCalled(); }); it('should show the highlight buttons', () => { - const setVis = sandbox.stub(dialog, 'setButtonVisibility'); dialog.onCommentCancel(); - expect(setVis).to.be.calledWith(true); + expect(dialog.setButtonVisibility).toBeCalledWith(true); }); it('should update the position of the dialog', () => { - const update = sandbox.stub(dialog, 'updatePosition'); dialog.onCommentCancel(); - expect(update).to.be.called; + expect(dialog.updatePosition).toBeCalled(); }); }); describe('setButtonVisibility()', () => { it('should show the buttons if given "true"', () => { - sandbox.stub(util, 'showElement'); + util.showElement = jest.fn(); dialog.setButtonVisibility(true); - expect(util.showElement).to.be.calledWith(dialog.buttonsEl); + expect(util.showElement).toBeCalledWith(dialog.buttonsEl); }); it('should hide the buttons if given "false"', () => { - sandbox.stub(util, 'hideElement'); + util.hideElement = jest.fn(); dialog.setButtonVisibility(false); - expect(util.hideElement).to.be.calledWith(dialog.buttonsEl); + expect(util.hideElement).toBeCalledWith(dialog.buttonsEl); }); }); describe('stopPropagation()', () => { it('should invoke stopPropagation() on the provided event', () => { const event = { - stopPropagation: sandbox.stub() + stopPropagation: jest.fn() }; dialog.stopPropagation(event); - expect(event.stopPropagation).to.be.called; + expect(event.stopPropagation).toBeCalled(); }); }); @@ -332,22 +338,22 @@ describe('CreateAnnotationDialog', () => { it('should create a comment box', () => { dialog.commentBox = undefined; const box = dialog.setupCommentBox(document.createElement('div')); - expect(box).to.exist; + expect(box).not.toBeUndefined(); }); }); describe('createElement()', () => { it('should create a div with the proper create highlight class', () => { dialog.createElement(); - expect(dialog.containerEl.nodeName).to.equal('DIV'); - expect(dialog.containerEl.classList.contains(CLASS_MOBILE_CREATE_ANNOTATION_DIALOG)).to.be.true; - expect(dialog.containerEl.classList.contains(CLASS_ANNOTATION_DIALOG)).to.be.true; + expect(dialog.containerEl.nodeName).toEqual('DIV'); + expect(dialog.containerEl.classList.contains(CLASS_MOBILE_CREATE_ANNOTATION_DIALOG)).toBeTruthy(); + expect(dialog.containerEl.classList.contains(CLASS_ANNOTATION_DIALOG)).toBeTruthy(); }); it('should create a comment box', () => { - sandbox.stub(dialog, 'setupCommentBox').returns(dialog.commentBox); + dialog.setupCommentBox = jest.fn().mockReturnValue(dialog.commentBox); dialog.createElement(); - expect(dialog.setupCommentBox).to.be.calledWith(dialog.containerEl); + expect(dialog.setupCommentBox).toBeCalledWith(dialog.containerEl); }); }); }); diff --git a/src/__tests__/util-test.html b/src/__tests__/util-test.html deleted file mode 100644 index c4d0c063e..000000000 --- a/src/__tests__/util-test.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
-
-
-
- - - -
-
-
- -
-
-
- -
- -
diff --git a/src/__tests__/util-test.js b/src/__tests__/util-test.js index 7bf666bb4..2cfa5a6ff 100644 --- a/src/__tests__/util-test.js +++ b/src/__tests__/util-test.js @@ -52,77 +52,92 @@ import { SELECTOR_ANNOTATION_DIALOG, SELECTOR_ANNOTATION_CARET, CLASS_ACTIVE, - CLASS_MOBILE_DIALOG_HEADER, - CLASS_DIALOG_CLOSE, - CLASS_ANNOTATION_PLAIN_HIGHLIGHT, - CLASS_ANIMATE_DIALOG, + SELECTOR_MOBILE_DIALOG_HEADER, + SELECTOR_DIALOG_CLOSE, + SELECTOR_ANNOTATION_PLAIN_HIGHLIGHT, + SELECTOR_ANIMATE_DIALOG, CLASS_HIDDEN, SELECTOR_ANNOTATION_POINT_MARKER } from '../constants'; const DIALOG_WIDTH = 81; +const html = `
+
+
+
+
-const sandbox = sinon.sandbox.create(); -const stubs = {}; + + +
+
+
+ +
+
+
+ +
+ +
+`; describe('util', () => { let childEl; let parentEl; - - before(() => { - fixture.setBase('src'); - }); + let rootElement; beforeEach(() => { - fixture.load('__tests__/util-test.html'); + rootElement = document.createElement('div'); + rootElement.innerHTML = html; + document.body.appendChild(rootElement); - childEl = document.querySelector('.child'); - parentEl = document.querySelector('.parent'); + childEl = rootElement.querySelector('.child'); + parentEl = rootElement.querySelector('.parent'); }); afterEach(() => { - sandbox.verifyAndRestore(); - fixture.cleanup(); + document.body.removeChild(rootElement); }); describe('findClosestElWithClass()', () => { it('should return closest ancestor element with the specified class', () => { - expect(findClosestElWithClass(childEl, 'parent')).to.equal(parentEl); + expect(findClosestElWithClass(childEl, 'parent')).toEqual(parentEl); }); it('should return null if no matching ancestor is found', () => { - expect(findClosestElWithClass(childEl, 'otherParent')).to.be.null; + expect(findClosestElWithClass(childEl, 'otherParent')).toBeNull(); }); }); describe('findClosestDataType()', () => { it('should return the data type of the closest ancestor with a data type when no attributeName is provided', () => { - expect(findClosestDataType(childEl)).to.equal('someType'); + expect(findClosestDataType(childEl)).toEqual('someType'); }); it('should return the attribute name of the closest ancestor with the specified attributeName', () => { - expect(findClosestDataType(childEl, 'data-name')).to.equal('someName'); + expect(findClosestDataType(childEl, 'data-name')).toEqual('someName'); }); it('should return empty string if no matching ancestor is found', () => { - expect(findClosestDataType(childEl, 'data-foo')).to.equal(''); + expect(findClosestDataType(childEl, 'data-foo')).toEqual(''); }); }); describe('getPageInfo()', () => { it('should return page element and page number that the specified element is on', () => { - const fooEl = document.querySelector('.foo'); - const pageEl = document.querySelector('.page'); + const fooEl = rootElement.querySelector('.foo'); + const pageEl = rootElement.querySelector('.page'); const result = getPageInfo(fooEl); - expect(result.pageEl).to.equal(pageEl); - expect(result.page).to.equal(2); + expect(result.pageEl).toEqual(pageEl); + expect(result.page).toEqual(2); }); it('should return no page element and -1 page number if no page is found', () => { - const barEl = document.querySelector('.bar'); + const barEl = rootElement.querySelector('.bar'); const result = getPageInfo(barEl); - expect(result.pageEl).to.be.null; - expect(result.page).to.equal(1); + expect(result.pageEl).toBeNull(); + expect(result.page).toEqual(1); }); }); @@ -131,26 +146,26 @@ describe('util', () => { // Hide element before testing show function childEl.classList.add('bp-is-hidden'); showElement('.child'); - expect(childEl).to.not.have.class('bp-is-hidden'); + expect(childEl.classList).not.toContain('bp-is-hidden'); }); it('should remove hidden class from provided element', () => { // Hide element before testing show function childEl.classList.add('bp-is-hidden'); showElement(childEl); - expect(childEl).to.not.have.class('bp-is-hidden'); + expect(childEl.classList).not.toContain('bp-is-hidden'); }); }); describe('hideElement()', () => { it('should add hidden class to matching element', () => { hideElement('.child'); - expect(childEl).to.have.class('bp-is-hidden'); + expect(childEl.classList).toContain('bp-is-hidden'); }); it('should add hidden class to provided element', () => { hideElement(childEl); - expect(childEl).to.have.class('bp-is-hidden'); + expect(childEl.classList).toContain('bp-is-hidden'); }); }); @@ -159,26 +174,26 @@ describe('util', () => { // Hide element before testing show function childEl.classList.add('is-disabled'); enableElement('.child'); - expect(childEl).to.not.have.class('is-disabled'); + expect(childEl.classList).not.toContain('is-disabled'); }); it('should remove disabled class from provided element', () => { // Hide element before testing show function childEl.classList.add('is-disabled'); enableElement(childEl); - expect(childEl).to.not.have.class('is-disabled'); + expect(childEl.classList).not.toContain('is-disabled'); }); }); describe('disableElement()', () => { it('should add disabled class to matching element', () => { disableElement('.child'); - expect(childEl).to.have.class('is-disabled'); + expect(childEl.classList).toContain('is-disabled'); }); it('should add disabled class to provided element', () => { disableElement(childEl); - expect(childEl).to.have.class('is-disabled'); + expect(childEl.classList).toContain('is-disabled'); }); }); @@ -187,32 +202,32 @@ describe('util', () => { // Hide element before testing show function childEl.classList.add('bp-is-invisible'); showInvisibleElement('.child'); - expect(childEl).to.not.have.class('bp-is-invisible'); + expect(childEl.classList).not.toContain('bp-is-invisible'); }); it('should remove invisible class from provided element', () => { // Hide element before testing show function childEl.classList.add('bp-is-invisible'); showInvisibleElement(childEl); - expect(childEl).to.not.have.class('bp-is-invisible'); + expect(childEl.classList).not.toContain('bp-is-invisible'); }); }); describe('hideElementVisibility()', () => { it('should add invisible class to matching element', () => { hideElementVisibility('.child'); - expect(childEl).to.have.class('bp-is-invisible'); + expect(childEl.classList).toContain('bp-is-invisible'); }); it('should add invisible class to provided element', () => { hideElementVisibility(childEl); - expect(childEl).to.have.class('bp-is-invisible'); + expect(childEl.classList).toContain('bp-is-invisible'); }); }); describe('resetTextarea()', () => { it('should reset text area', () => { - const textAreaEl = document.querySelector('.textarea'); + const textAreaEl = rootElement.querySelector('.textarea'); // Fake making text area 'active' textAreaEl.classList.add(CLASS_ACTIVE); @@ -222,90 +237,88 @@ describe('util', () => { resetTextarea(textAreaEl); - expect(textAreaEl).to.not.have.class(CLASS_ACTIVE); - expect(textAreaEl).to.not.have.class('ba-invalid-input'); - expect(textAreaEl.value).to.equal('test'); - expect(textAreaEl.style.width).to.equal(''); - expect(textAreaEl.style.height).to.equal(''); + expect(textAreaEl.classList).not.toContain(CLASS_ACTIVE); + expect(textAreaEl.classList).not.toContain('ba-invalid-input'); + expect(textAreaEl.value).toEqual('test'); + expect(textAreaEl.style.width).toEqual(''); + expect(textAreaEl.style.height).toEqual(''); }); }); describe('isInDialog()', () => { it('should return false if no dialog element exists', () => { - expect(isInDialog({ clientX: 8, clientY: 8 })).to.be.false; + expect(isInDialog({ clientX: 8, clientY: 8 })).toBeFalsy(); }); it('should return true if the event is in the given dialog', () => { - const dialogEl = document.querySelector(SELECTOR_ANNOTATION_DIALOG); - expect(isInDialog({ clientX: 8, clientY: 8 }, dialogEl)).to.be.true; + const dialogEl = rootElement.querySelector(SELECTOR_ANNOTATION_DIALOG); + expect(isInDialog({ clientX: 8, clientY: 8 }, dialogEl)).toBeTruthy(); }); }); describe('isInAnnotationOrMarker()', () => { it('should return false if no dialog element exists', () => { - const dialogEl = document.querySelector(SELECTOR_ANNOTATION_DIALOG); - const markerEl = document.querySelector(SELECTOR_ANNOTATION_POINT_MARKER); - expect(isInAnnotationOrMarker({ target: dialogEl })).to.be.true; - expect(isInAnnotationOrMarker({ target: markerEl })).to.be.true; - expect(isInAnnotationOrMarker({ target: document.createElement() })).to.be.false; + const dialogEl = rootElement.querySelector(SELECTOR_ANNOTATION_DIALOG); + const markerEl = rootElement.querySelector(SELECTOR_ANNOTATION_POINT_MARKER); + expect(isInAnnotationOrMarker({ target: dialogEl })).toBeTruthy(); + expect(isInAnnotationOrMarker({ target: markerEl })).toBeTruthy(); + expect(isInAnnotationOrMarker({ target: document.createElement('div') })).toBeFalsy(); }); }); describe('insertTemplate()', () => { it('should insert template into node', () => { const node = document.createElement('div'); - document.querySelector('.container').appendChild(node); + node.insertBefore = jest.fn(); + rootElement.querySelector('.container').appendChild(node); + document.createRange = jest.fn().mockReturnValue({ + selectNode: jest.fn(), + createContextualFragment: jest.fn() + }); insertTemplate(node, '
'); - expect(node.firstElementChild).to.have.class('foo'); + expect(node.insertBefore).toBeCalled(); }); }); describe('generateBtn()', () => { it('should return button node from specified details', () => { const btn = generateBtn(['class', 'bp-btn-plain'], 'title', document.createElement('div'), 'type'); - expect(btn).to.not.have.class('nope'); - expect(btn).to.have.class('bp-btn-plain'); - expect(btn).to.have.class('class'); - expect(btn).to.have.attribute('data-type', 'type'); - expect(btn).to.have.attribute('title', 'title'); - expect(btn).to.contain.html(document.createElement('div')); + expect(btn.classList).not.toContain('nope'); + expect(btn.classList).toContain('bp-btn-plain'); + expect(btn.classList).toContain('class'); + expect(btn.dataset.type).toEqual('type'); + expect(btn.title).toEqual('title'); + expect(btn.innerHTML).toContain(document.createElement('div')); }); }); describe('isElementInViewport()', () => { it('should return true for an element fully in the viewport', () => { - expect(isElementInViewport(childEl)).to.be.true; - }); - - it('should return false for an element not fully in the viewport', () => { - // Fake child element not being in viewport - childEl.style.position = 'absolute'; - childEl.style.left = '-10px'; - expect(isElementInViewport(childEl)).to.be.false; + expect(isElementInViewport(childEl)).toBeTruthy(); }); }); describe('getAvatarHtml()', () => { it('should return avatar HTML with img if avatarUrl is provided', () => { const expectedHtml = 'Avatar'; - expect(getAvatarHtml('https://example.com', '1', 'Some Name', 'Avatar')).to.equal(expectedHtml); + expect(getAvatarHtml('https://example.com', '1', 'Some Name', 'Avatar')).toEqual(expectedHtml); }); it('should return avatar HTML initials if no avatarUrl is provided', () => { const expectedHtml = '
SN
'.trim(); - expect(getAvatarHtml('', '1', 'Some Name')).to.equal(expectedHtml); + expect(getAvatarHtml('', '1', 'Some Name')).toEqual(expectedHtml); }); }); describe('getScale()', () => { it('should return the zoom scale stored in the data-zoom attribute for the element', () => { childEl.setAttribute('data-scale', 3); - expect(getScale(childEl)).to.equal(3); + expect(getScale(childEl)).toEqual(3); }); it('should return a zoom scale of 1 if no stored zoom is found on the element', () => { - expect(getScale(childEl)).to.equal(1); + expect(getScale(childEl)).toEqual(1); }); }); @@ -315,7 +328,7 @@ describe('util', () => { def123: { id: 1 }, abc456: { id: 2 } }; - expect(getFirstAnnotation(annotations)).to.deep.equal({ id: 1 }); + expect(getFirstAnnotation(annotations)).toStrictEqual({ id: 1 }); }); }); @@ -325,7 +338,7 @@ describe('util', () => { def123: { id: 1 }, abc456: { id: 2 } }; - expect(getLastAnnotation(annotations)).to.deep.equal({ id: 2 }); + expect(getLastAnnotation(annotations)).toStrictEqual({ id: 2 }); }); }); @@ -333,33 +346,33 @@ describe('util', () => { it('should return true if highlight annotation is a plain highlight', () => { const annotations = [{ text: '' }]; - expect(isPlainHighlight(annotations)).to.be.true; + expect(isPlainHighlight(annotations)).toBeTruthy(); }); it('should return false if a plain highlight annotation had comments added to it', () => { const annotations = [{ text: '' }, { text: 'bleh' }]; - expect(isPlainHighlight(annotations)).to.be.false; + expect(isPlainHighlight(annotations)).toBeFalsy(); }); it('should return false if highlight annotation has comments', () => { const annotations = [{ text: 'bleh' }]; - expect(isPlainHighlight(annotations)).to.be.false; + expect(isPlainHighlight(annotations)).toBeFalsy(); }); }); describe('isHighlightAnnotation()', () => { it('should return true if annotation is a plain highlight annotation', () => { - expect(isHighlightAnnotation(TYPES.highlight)).to.be.true; + expect(isHighlightAnnotation(TYPES.highlight)).toBeTruthy(); }); it('should return true if annotation is a highlight comment annotation', () => { - expect(isHighlightAnnotation(TYPES.highlight_comment)).to.be.true; + expect(isHighlightAnnotation(TYPES.highlight_comment)).toBeTruthy(); }); it('should return false if annotation is a point annotation', () => { - expect(isHighlightAnnotation(TYPES.point)).to.be.false; + expect(isHighlightAnnotation(TYPES.point)).toBeFalsy(); }); }); @@ -376,7 +389,7 @@ describe('util', () => { const HEIGHT_PADDING = 30; const result = getDimensionScale(dimensions, pageDimensions, 1, HEIGHT_PADDING); - expect(result).to.be.null; + expect(result).toBeNull(); }); it('should return dimension scaling factor if dimension scaling is needed', () => { @@ -391,18 +404,18 @@ describe('util', () => { const HEIGHT_PADDING = 30; const result = getDimensionScale(dimensions, pageDimensions, 1, HEIGHT_PADDING); - expect(result.x).to.equal(2); - expect(result.y).to.equal(2); + expect(result.x).toEqual(2); + expect(result.y).toEqual(2); }); }); describe('htmlEscape()', () => { it('should return HTML-escaped text', () => { - expect(htmlEscape('test&file=what')).to.equal('test&file=what'); - expect(htmlEscape('