├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ ├── docs-build.yml │ └── packages-ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── .vuepress │ ├── config.js │ ├── public │ │ └── assets │ │ │ ├── 6d308bd9gw1f6wsibnfldg20nk0gr7kg.gif │ │ │ └── 6d308bd9gw1f6wsic5dmxj20rl0qqtbi.jpg │ └── styles │ │ └── palette.styl ├── README.md ├── guide │ ├── README.md │ ├── advanced.md │ ├── install.md │ └── usage.md └── zh │ ├── README.md │ └── guide │ ├── README.md │ ├── advanced.md │ ├── install.md │ └── usage.md ├── index.js ├── lib ├── commands │ ├── browser-events.js │ ├── index.js │ ├── macaca-datahub.js │ └── page-manager.js ├── mocha │ ├── browser │ │ ├── progress.js │ │ └── tty.js │ ├── context.js │ ├── hook.js │ ├── interfaces │ │ ├── bdd.js │ │ ├── common.js │ │ ├── exports.js │ │ ├── index.js │ │ ├── qunit.js │ │ └── tdd.js │ ├── mocha.js │ ├── ms.js │ ├── pending.js │ ├── reporters │ │ ├── base.js │ │ ├── doc.js │ │ ├── dot.js │ │ ├── html.js │ │ ├── index.js │ │ ├── json-stream.js │ │ ├── json.js │ │ ├── landing.js │ │ ├── list.js │ │ ├── markdown.js │ │ ├── min.js │ │ ├── nyan.js │ │ ├── progress.js │ │ ├── spec.js │ │ ├── tap.js │ │ └── xunit.js │ ├── runnable.js │ ├── runner.js │ ├── suite.js │ ├── template.html │ ├── test.js │ └── utils.js ├── playwright.js ├── uitest.js └── utils │ └── index.js ├── mocha.entry.js ├── package.json ├── packages └── gulp-uitest │ ├── .eslintignore │ ├── .eslintrc.js │ ├── README.md │ ├── index.js │ ├── lib │ └── gulp-uitest.js │ ├── package.json │ └── test │ ├── gulp-uitest.test.js │ └── mocha.opts ├── rollup.config.js ├── test ├── case-sample │ ├── .eslintrc.js │ ├── fileChoose.js │ ├── keyboard.js │ ├── mouse.js │ ├── page.js │ ├── retries.js │ ├── sample1.js │ └── sample2.js ├── ci.sh ├── index.html ├── mocha.opts ├── uitest.test.js └── uitls.test.js └── uitest-mocha-shim.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | docs_dist 4 | mocha.js 5 | uitest-mocha-shim.js 6 | lib/mocha 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | extends: 'eslint-config-egg', 6 | parserOptions: { 7 | ecmaVersion: 2020, 8 | }, 9 | plugins: [], 10 | rules: { 11 | 'valid-jsdoc': 0, 12 | 'no-script-url': 0, 13 | 'no-multi-spaces': 0, 14 | 'default-case': 0, 15 | 'no-case-declarations': 0, 16 | 'one-var-declaration-per-line': 0, 17 | 'no-restricted-syntax': 0, 18 | 'jsdoc/require-param': 0, 19 | 'jsdoc/check-param-names': 0, 20 | 'jsdoc/require-param-description': 0, 21 | 'arrow-parens': 0, 22 | 'prefer-promise-reject-errors': 0, 23 | 'no-control-regex': 0, 24 | 'no-use-before-define': 0, 25 | 'array-callback-return': 0, 26 | 'no-bitwise': 0, 27 | 'no-self-compare': 0, 28 | 'one-var': 0, 29 | 'no-trailing-spaces': [ 'warn', { skipBlankLines: true }], 30 | 'no-return-await': 0, 31 | }, 32 | globals: { 33 | window: true, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | push: 7 | branches: 8 | - '**' 9 | 10 | jobs: 11 | Runner: 12 | timeout-minutes: 10 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout git source 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup docker 19 | uses: docker-practice/actions-setup-docker@1.0.9 20 | 21 | - name: Setup node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: '16' 25 | 26 | - name: Install dependencies 27 | run: | 28 | npm i --force 29 | 30 | - name: Continuous integration 31 | run: | 32 | npm run lint 33 | docker run -i --entrypoint=bash -v `pwd`:/root/tmp --rm mcr.microsoft.com/playwright:v1.26.0-focal -c "cd /root/tmp && ./test/ci.sh" 34 | 35 | - name: Code coverage 36 | uses: codecov/codecov-action@v3.0.0 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/docs-build.yml: -------------------------------------------------------------------------------- 1 | name: Docs Build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | docs-build: 12 | timeout-minutes: 10 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout git source 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Setup node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: '16' 25 | 26 | - name: Install dependencies 27 | run: | 28 | npm i npm@6 -g 29 | npm i vuepress macaca-ecosystem -D 30 | 31 | - name: Build docs 32 | run: npm run docs:build 33 | 34 | - name: Deploy to GitHub Pages 35 | if: success() 36 | uses: peaceiris/actions-gh-pages@v3 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | publish_dir: ./docs_dist 40 | -------------------------------------------------------------------------------- /.github/workflows/packages-ci.yml: -------------------------------------------------------------------------------- 1 | name: Packages CI 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | push: 7 | branches: 8 | - '**' 9 | 10 | jobs: 11 | Runner: 12 | timeout-minutes: 10 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout git source 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: '16' 22 | 23 | - name: Install dependencies 24 | run: | 25 | cd ./packages/gulp-uitest/ 26 | npm i npm@6 -g 27 | npm i 28 | 29 | - name: Continuous integration 30 | run: | 31 | cd ./packages/gulp-uitest/ 32 | npm run lint 33 | npm run test 34 | 35 | - name: Code coverage 36 | uses: codecov/codecov-action@v3.0.0 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | docs_dist/ 4 | coverage/ 5 | reports/ 6 | *.sw* 7 | *.un~ 8 | mocha.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) 2017 Alibaba Group Holding Limited and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uitest 2 | 3 | --- 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![CI][CI-image]][CI-url] 7 | [![Test coverage][coveralls-image]][coveralls-url] 8 | [![npm download][download-image]][download-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/uitest.svg 11 | [npm-url]: https://npmjs.org/package/uitest 12 | [CI-image]: https://github.com/macacajs/uitest/actions/workflows/ci.yml/badge.svg 13 | [CI-url]: https://github.com/macacajs/uitest/actions/workflows/ci.yml 14 | [coveralls-image]: https://img.shields.io/coveralls/macacajs/uitest.svg 15 | [coveralls-url]: https://coveralls.io/r/macacajs/uitest?branch=master 16 | [download-image]: https://img.shields.io/npm/dm/uitest.svg 17 | [download-url]: https://npmjs.org/package/uitest 18 | 19 | > Run mocha in a browser environment. 20 | 21 | 22 | 23 | ## Contributors 24 | 25 | |[
xudafeng](https://github.com/xudafeng)
|[
zivyangll](https://github.com/zivyangll)
|[
meowtec](https://github.com/meowtec)
|[
ilimei](https://github.com/ilimei)
|[
paradite](https://github.com/paradite)
|[
snapre](https://github.com/snapre)
| 26 | | :---: | :---: | :---: | :---: | :---: | :---: | 27 | 28 | 29 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Wed Feb 15 2023 14:41:53 GMT+0800`. 30 | 31 | 32 | 33 | ## Installation 34 | 35 | ```bash 36 | $ npm i uitest --save-dev 37 | ``` 38 | 39 | For more help, please visite: [macacajs.github.io/uitest](//macacajs.github.io/uitest) 40 | 41 | ## License 42 | 43 | The MIT License (MIT) 44 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const macacaEcosystem = require('macaca-ecosystem'); 4 | const traceFragment = require('macaca-ecosystem/lib/trace-fragment'); 5 | 6 | const name = 'uitest'; 7 | 8 | const title = 'Macaca UITest'; 9 | 10 | module.exports = { 11 | dest: 'docs_dist', 12 | base: `/${name}/`, 13 | 14 | locales: { 15 | '/': { 16 | lang: 'en-US', 17 | title, 18 | description: 'Run mocha in a browser environment.', 19 | }, 20 | '/zh/': { 21 | lang: 'zh-CN', 22 | title, 23 | description: '在浏览器环境中运行测试。', 24 | }, 25 | }, 26 | head: [ 27 | ['link', { 28 | rel: 'icon', 29 | href: 'https://macacajs.github.io/assets/favicon.ico' 30 | }], 31 | ['script', { 32 | async: true, 33 | src: 'https://www.googletagmanager.com/gtag/js?id=UA-49226133-2', 34 | }, ''], 35 | ['script', {}, ` 36 | window.dataLayer = window.dataLayer || []; 37 | function gtag(){dataLayer.push(arguments);} 38 | gtag('js', new Date()); 39 | gtag('config', 'UA-49226133-2'); 40 | `], 41 | ['script', {}, traceFragment], 42 | ['style', {}, ` 43 | img { 44 | width: 100%; 45 | } 46 | `] 47 | ], 48 | serviceWorker: true, 49 | themeConfig: { 50 | repo: `macacajs/${name}`, 51 | editLinks: true, 52 | docsDir: 'docs', 53 | locales: { 54 | '/': { 55 | label: 'English', 56 | selectText: 'Languages', 57 | editLinkText: 'Edit this page on GitHub', 58 | lastUpdated: 'Last Updated', 59 | serviceWorker: { 60 | updatePopup: { 61 | message: 'New content is available.', 62 | buttonText: 'Refresh', 63 | }, 64 | }, 65 | nav: [ 66 | { 67 | text: 'Guide', 68 | link: '/guide/' 69 | }, 70 | macacaEcosystem.en, 71 | ], 72 | sidebar: { 73 | '/guide/': genSidebarConfig('Guide', 'Usage', 'Advanced'), 74 | }, 75 | }, 76 | '/zh/': { 77 | label: '简体中文', 78 | selectText: '选择语言', 79 | editLinkText: '在 GitHub 上编辑此页', 80 | lastUpdated: '上次更新', 81 | serviceWorker: { 82 | updatePopup: { 83 | message: '发现新内容可用', 84 | buttonText: '刷新', 85 | }, 86 | }, 87 | nav: [ 88 | { 89 | text: '指南', 90 | link: '/zh/guide/' 91 | }, 92 | macacaEcosystem.zh, 93 | ], 94 | sidebar: { 95 | '/zh/guide/': genSidebarConfig('指南'), 96 | }, 97 | }, 98 | }, 99 | }, 100 | }; 101 | 102 | function genSidebarConfig(guide) { 103 | return [ 104 | { 105 | title: guide, 106 | collapsable: false, 107 | children: [ 108 | '', 109 | 'install', 110 | 'usage', 111 | 'advanced', 112 | ], 113 | }, 114 | ]; 115 | } 116 | -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/6d308bd9gw1f6wsibnfldg20nk0gr7kg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macacajs/uitest/01372ad0ae75077b1a50baacd92706085a9ed6b4/docs/.vuepress/public/assets/6d308bd9gw1f6wsibnfldg20nk0gr7kg.gif -------------------------------------------------------------------------------- /docs/.vuepress/public/assets/6d308bd9gw1f6wsic5dmxj20rl0qqtbi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macacajs/uitest/01372ad0ae75077b1a50baacd92706085a9ed6b4/docs/.vuepress/public/assets/6d308bd9gw1f6wsic5dmxj20rl0qqtbi.jpg -------------------------------------------------------------------------------- /docs/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | $textColor = #2c3e50 2 | $borderColor = #eaecef 3 | $accentColor = #ee6723 4 | $codeBgColor = #282c34 5 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | home: true 4 | heroImage: https://macacajs.github.io/logo/macaca.svg 5 | actionText: Try it Out → 6 | actionLink: /guide/ 7 | footer: MIT Licensed | Copyright © 2015-present Macaca 8 | 9 | --- 10 | 11 | ## Quick Start 12 | 13 | ```bash 14 | # install UITest 15 | $ npm i uitest --save-dev 16 | ``` 17 | 18 | --- 19 | 20 | ::: tip Browser 21 | UITest support running in the browser. 22 | ::: 23 | 24 | ![](/uitest/assets/6d308bd9gw1f6wsic5dmxj20rl0qqtbi.jpg) 25 | 26 | ::: tip Headless 27 | Of course, UITest also supports running in the command-line environment. 28 | ::: 29 | 30 | ![](/uitest/assets/6d308bd9gw1f6wsibnfldg20nk0gr7kg.gif) 31 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | UITest is a test framework that make your code runs in the browser. It is mainly for browsers and is a member of the [Macaca](//macacajs.github.io) ecosystem. 4 | 5 | UITest mainly solves unit test scenarios that require the real environment of the Chromium, such as running JavaScript in V8, verifying whether the rendering is successful in the rendering engine, providing screenshots, coverage, and the corresponding server CI solution. You can now easily test any JavaScript app in a real Chromium without hassling solution. 6 | -------------------------------------------------------------------------------- /docs/guide/advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced 2 | 3 | ## Configuration downgrade 4 | 5 | If you do not want the page to display in retina mode, set `hidpi` to false. 6 | 7 | For more options, see [Electron BrowserWindow options](http://electron.atom.io/docs/api/browser-window/#new-browserwindowoptions). 8 | 9 | ## Run with Travis 10 | 11 | Your `.travis.yml` need the configuration below to run UITest on Travis: 12 | 13 | ```yaml 14 | addons: 15 | apt: 16 | packages: 17 | - xvfb 18 | install: 19 | - export DISPLAY=':99.0' 20 | - Xvfb :99 -screen 0 1366x768x24 > /dev/null 2>&1 & 21 | ``` 22 | 23 | ## Run with Docker 24 | 25 | You can use Macaca Electron [Docker Image](//github.com/macacajs/macaca-electron-docker). 26 | -------------------------------------------------------------------------------- /docs/guide/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Requirements 4 | 5 | To install the UITest, [Node.js](https://nodejs.org) environment is required. 6 | 7 | ## Install from NPM 8 | 9 | ```bash 10 | $ npm i uitest --save-dev 11 | ``` 12 | 13 | ## Sample 14 | 15 | ```bash 16 | $ git clone https://github.com/macaca-sample/uitest-sample.git --depth=1 17 | $ cd uitest-sample 18 | $ npm i 19 | $ npm run test 20 | ``` 21 | 22 | ## More 23 | 24 | - [Game framework Hilo test sample](https://github.com/hiloteam/Hilo) 25 | - [Canvas framework monitor.js test sample](https://github.com/pillowjs/monitor.js) 26 | -------------------------------------------------------------------------------- /docs/guide/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | You should configure your entry HTML by including `uitest-mocha-shim.js`. 4 | 5 | Here is an example `test.html` 6 | 7 | ```html 8 | 9 | 10 | 11 | macaca mocha test 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 28 | 39 | 42 | 43 | 44 | ``` 45 | 46 | ## Start with Node.js 47 | 48 | Your can start uitest using Node API: 49 | 50 | ```javascript 51 | const uitest = require('uitest'); 52 | 53 | uitest({ 54 | url: 'file:///Users/name/path/index.html', 55 | width: 600, 56 | height: 480, 57 | hidpi: false, 58 | useContentSize: true, 59 | show: false, 60 | }).then(() => { 61 | console.log('uitest success') 62 | }).catch(() => { 63 | console.log('uitest error') 64 | }); 65 | ``` 66 | 67 | ![](/uitest/assets/6d308bd9gw1f6wsic5dmxj20rl0qqtbi.jpg) 68 | 69 | ![](/uitest/assets/6d308bd9gw1f6wsibnfldg20nk0gr7kg.gif) 70 | 71 | ## Use with Gulp 72 | 73 | ```bash 74 | $ npm i gulp-uitest --save-dev 75 | ``` 76 | 77 | ```javascript 78 | const uitest = require('gulp-uitest'); 79 | //test 80 | gulp.task('test', function() { 81 | return gulp 82 | .src('test/html/index.html') 83 | .pipe(uitest({ 84 | width: 600, 85 | height: 480, 86 | hidpi: false, 87 | useContentSize: true, 88 | show: false, 89 | })); 90 | }); 91 | 92 | ``` 93 | 94 | ## Use Screenshots 95 | 96 | ```javascript 97 | _macaca_uitest.screenshot(name[String], cb[Function]); 98 | ``` 99 | 100 | ## Coverage 101 | 102 | UITest will generate the coverage file if `window.__coverage__` is existed. 103 | 104 | process.env.MACACA_COVERAGE_IGNORE_REG support coverage ignore rule, for example: 105 | 106 | MACACA_COVERAGE_IGNORE_REG='test/' means all files in the `./test` directory are ignored. 107 | -------------------------------------------------------------------------------- /docs/zh/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | home: true 4 | heroImage: https://macacajs.github.io/logo/macaca.svg 5 | actionText: 使用文档 6 | actionLink: /zh/guide/ 7 | footer: MIT Licensed | Copyright © 2015-present Macaca 8 | 9 | --- 10 | 11 | ## 安装简单 12 | 13 | ```bash 14 | $ npm i uitest --save-dev 15 | ``` 16 | 17 | --- 18 | 19 | ::: tip 浏览器运行 20 | UITest 支持在浏览器中运行。 21 | ::: 22 | 23 | ![](/uitest/assets/6d308bd9gw1f6wsic5dmxj20rl0qqtbi.jpg) 24 | 25 | ::: tip 命令行运行 26 | 当然,UITest 也支持在命令行环境运行。 27 | ::: 28 | 29 | ![](/uitest/assets/6d308bd9gw1f6wsibnfldg20nk0gr7kg.gif) 30 | -------------------------------------------------------------------------------- /docs/zh/guide/README.md: -------------------------------------------------------------------------------- 1 | # 简单介绍 2 | 3 | UITest 是一款运行在浏览器中的测试框架,主要面向场景是浏览器单测,是 [Macaca](//macacajs.github.io) 生态成员之一。 4 | 5 | UITest 主要解决需要浏览器真实环境的单测场景,比如在 V8 中运行 JavaScript,在渲染引擎中验证渲染是否成功,提供了截图、覆盖率等功能和对应的服务器 CI 方案。 6 | -------------------------------------------------------------------------------- /docs/zh/guide/advanced.md: -------------------------------------------------------------------------------- 1 | # 进阶 2 | 3 | ## 设置降级 4 | 5 | 如果你不想在 Retina 模式中展示页面,可以设置 `hidpi` 为 false。 6 | 7 | 更多配置可以参考 [Electron BrowserWindow 配置](http://electron.atom.io/docs/api/browser-window/#new-browserwindowoptions)。 8 | 9 | ## 持续集成 10 | 11 | 如果你使用 Travis CI 运行测试,你需要在 `.travis.yml` 文件中添加如下配置: 12 | 13 | ```yaml 14 | addons: 15 | apt: 16 | packages: 17 | - xvfb 18 | install: 19 | - export DISPLAY=':99.0' 20 | - Xvfb :99 -screen 0 1366x768x24 > /dev/null 2>&1 & 21 | ``` 22 | 23 | ## Docker 运行 24 | 25 | 你也可以使用 Macaca Electron [Docker 镜像](//github.com/macacajs/macaca-electron-docker) 来运行。 26 | -------------------------------------------------------------------------------- /docs/zh/guide/install.md: -------------------------------------------------------------------------------- 1 | # 安装 2 | 3 | ## 环境需要 4 | 5 | 要安装 UITest, 你需要首先安装 [Node.js](https://nodejs.org),中国用户可以安装 [cnpm](https://npm.taobao.org/) 加快 NPM 模块安装速度。 6 | 7 | ## 命令行安装 8 | 9 | ```bash 10 | $ npm i uitest --save-dev 11 | ``` 12 | 13 | ## 运行示例 14 | 15 | ```bash 16 | $ git clone https://github.com/macaca-sample/uitest-sample.git --depth=1 17 | $ cd uitest-sample 18 | $ npm i 19 | $ npm run test 20 | ``` 21 | 22 | ## 更多示例 23 | 24 | - [在大型游戏引擎框架 Hilo 中使用](https://github.com/hiloteam/Hilo) 25 | - [在 Canvas 框架 monitor.js 中使用](https://github.com/pillowjs/monitor.js) 26 | -------------------------------------------------------------------------------- /docs/zh/guide/usage.md: -------------------------------------------------------------------------------- 1 | # 如何使用 2 | 3 | 安装之后可以通过在 html runner 中添加 `uitest-mocha-shim.js` 来进行 UITest 配置。 4 | 5 | 下面是一个 `test.html` 示例: 6 | 7 | ```html 8 | 9 | 10 | 11 | macaca mocha test 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 28 | 39 | 43 | 44 | 45 | ``` 46 | 47 | ## 通过 Node.js 运行 48 | 49 | 可以通过 Node.js 来启动 UITest: 50 | 51 | ```javascript 52 | uitest({ 53 | url: 'file:///Users/name/path/index.html', 54 | width: 600, 55 | height: 480, 56 | hidpi: false, 57 | useContentSize: true, 58 | show: false, 59 | }).then(() => { 60 | console.log('uitest success') 61 | }).catch(() => { 62 | console.log('uitest error') 63 | }); 64 | ``` 65 | 66 | 效果如下: 67 | 68 | ![](/uitest/assets/6d308bd9gw1f6wsic5dmxj20rl0qqtbi.jpg) 69 | 70 | ![](/uitest/assets/6d308bd9gw1f6wsibnfldg20nk0gr7kg.gif) 71 | 72 | ## 使用 Gulp 73 | 74 | 在 Gulp 中使用 UITest: 75 | 76 | ```bash 77 | $ npm i gulp-uitest --save-dev 78 | ``` 79 | 80 | ```javascript 81 | const uitest = require('gulp-uitest'); 82 | //test 83 | gulp.task('test', function() { 84 | return gulp 85 | .src('test/html/index.html') 86 | .pipe(uitest({ 87 | width: 600, 88 | height: 480, 89 | hidpi: false, 90 | useContentSize: true, 91 | show: false, 92 | })); 93 | }); 94 | 95 | 同样的,UITest 运行触发比较灵活,你可以与其他脚本和 pipeline 集成。 96 | 97 | ``` 98 | 99 | ## 使用截图 100 | 101 | ```javascript 102 | _macaca_uitest.screenshot(name[String], cb[Function]); 103 | ``` 104 | 105 | ## 覆盖率 106 | 107 | 当浏览器上下文中有 `window.__coverage__` 将自动生成覆盖率报告。 108 | 109 | process.env.MACACA_COVERAGE_IGNORE_REG 可以传入 coverage 忽略规则,例如: 110 | 111 | MACACA_COVERAGE_IGNORE_REG='test/' 会忽略所有 `./test` 目录下的文件。 -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/uitest'); 4 | -------------------------------------------------------------------------------- /lib/commands/browser-events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | keyboard: async (context, type, key, opt) => { 5 | const { page } = context; 6 | if (page.keyboard[type]) { 7 | await page.keyboard[type](key, opt); 8 | } 9 | return true; 10 | }, 11 | mouse: async (context, type, x, y, opt) => { 12 | const { page } = context; 13 | if (page.mouse[type]) { 14 | await page.mouse[type](x, y, opt); 15 | } 16 | return true; 17 | }, 18 | fileChooser: async (context, filePath) => { 19 | const { page } = context; 20 | const fileChooser = await page.waitForEvent('filechooser'); 21 | await fileChooser.setFiles(filePath); 22 | return true; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/commands/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const { createThenableFunction } = require('../utils'); 7 | 8 | const exitForWait = createThenableFunction(); 9 | 10 | const commands = Object.assign({}, 11 | require('./macaca-datahub'), 12 | require('./browser-events'), 13 | require('./page-manager'), 14 | { 15 | screenshot: async ({ context }, dir) => { 16 | return await context.getScreenshot(context, { dir }); 17 | }, 18 | getVideoName: async ({ context }) => { 19 | const filePath = await context.getScreenshot(context, { video: true }); 20 | return filePath && path.basename(filePath); 21 | }, 22 | exit: async ({ context }, failData) => { 23 | await context.stopDevice(); 24 | exitForWait(failData.failedCount || 0); 25 | }, 26 | saveCoverage: async (_, data) => { 27 | if (data) { 28 | const coverageDir = path.join(process.cwd(), 'coverage'); 29 | try { 30 | if (!fs.existsSync(path.join(coverageDir))) { 31 | fs.mkdirSync(path.join(coverageDir)); 32 | } 33 | if (!fs.existsSync(path.join(coverageDir, '.temp'))) { 34 | fs.mkdirSync(path.join(coverageDir, '.temp')); 35 | } 36 | } catch (e) { 37 | return false; 38 | } 39 | const file = path.join(coverageDir, '.temp', `${+new Date()}_coverage.json`); 40 | // ignore tests 41 | const coverageIgnore = process.env.MACACA_COVERAGE_IGNORE_REG; 42 | if (coverageIgnore) { 43 | const ignoreReg = new RegExp(coverageIgnore, 'i'); 44 | for (const k in data) { 45 | if (ignoreReg.test(k)) { 46 | delete data[k]; 47 | } 48 | } 49 | } 50 | fs.writeFileSync(file, JSON.stringify(data, null, 2)); 51 | console.log(`coverage file created at: ${file}`); 52 | } 53 | return true; 54 | }, 55 | saveReport: async (_, output) => { 56 | try { 57 | const reportsDir = path.join(process.cwd(), 'reports'); 58 | 59 | if (!(fs.existsSync(reportsDir) && fs.statSync(reportsDir).isDirectory())) { 60 | fs.mkdirSync(reportsDir); 61 | } 62 | 63 | const reportsFile = path.join(reportsDir, 'json-final.json'); 64 | fs.writeFileSync(reportsFile, JSON.stringify(output, null, 2), 'utf8'); 65 | console.log(`reports file created at: ${reportsFile}`); 66 | } catch (e) { 67 | console.error(e); 68 | } 69 | }, 70 | } 71 | ); 72 | 73 | async function setupCommands(context) { 74 | const { browserContext } = context; 75 | 76 | await browserContext.exposeBinding('__execCommand', async (ctx, name, ...args) => { 77 | if (typeof name !== 'string') throw new Error(`invalid command name ${name}`); 78 | if (!commands[name]) throw new Error(`unknown command name ${name}`); 79 | return await commands[name]({ ...ctx, context }, ...args); 80 | }, { handle: false }); 81 | } 82 | 83 | module.exports = { 84 | setupCommands, 85 | exitForWait, 86 | }; 87 | -------------------------------------------------------------------------------- /lib/commands/macaca-datahub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DataHubSDK = require('datahub-nodejs-sdk'); 4 | 5 | const datahubClient = new DataHubSDK(); 6 | 7 | module.exports = { 8 | switchScene: async (_, argData) => { 9 | try { 10 | await datahubClient.switchScene(argData); 11 | } catch (e) { 12 | return false; 13 | } 14 | return true; 15 | }, 16 | switchAllScenes: async (_, argData) => { 17 | try { 18 | await datahubClient.switchAllScenes(argData); 19 | } catch (e) { 20 | return false; 21 | } 22 | return true; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/commands/page-manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | 4 | const shimContent = fs.readFileSync(__dirname + '/../../uitest-mocha-shim.js').toString(); 5 | const pages = []; 6 | 7 | module.exports = { 8 | newPage: async ({ context }, url) => { 9 | const { browserContext } = context; 10 | const page = await browserContext.newPage(); 11 | await page.goto(url, { 12 | waitUntil: 'load' || 'networkidle', 13 | }); 14 | await page.addScriptTag({ 15 | content: shimContent, 16 | }); 17 | const redirectConsole = require('macaca-playwright/dist/lib/redirect-console'); 18 | await redirectConsole({ page }); 19 | return pages.push(page) - 1; 20 | }, 21 | closePage: async (_, id) => { 22 | const page = pages[id]; 23 | if (page) { 24 | page.close({ runBeforeUnload: true }); 25 | pages[id] = null; 26 | } 27 | return false; 28 | }, 29 | runInPage: async (_, id, funcString) => { 30 | const page = pages[id]; 31 | if (page) { 32 | return await page.evaluate(funcString); 33 | } 34 | return false; 35 | }, 36 | waitForSelector: async (_, id, selector) => { 37 | const page = pages[id]; 38 | if (page) { 39 | return await page.waitForSelector(selector); 40 | } 41 | return false; 42 | }, 43 | waitForEvent: async (_, id, eventName) => { 44 | const page = pages[id]; 45 | if (page) { 46 | return await new Promise(resolve => { 47 | const loaded = () => { 48 | page.removeListener(eventName, loaded); 49 | resolve(true); 50 | }; 51 | page.on(eventName, loaded); 52 | }); 53 | } 54 | return false; 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /lib/mocha/browser/progress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Expose `Progress`. 5 | */ 6 | 7 | module.exports = Progress; 8 | 9 | /** 10 | * Initialize a new `Progress` indicator. 11 | */ 12 | function Progress() { 13 | this.percent = 0; 14 | this.size(0); 15 | this.fontSize(11); 16 | this.font('helvetica, arial, sans-serif'); 17 | } 18 | 19 | /** 20 | * Set progress size to `size`. 21 | * 22 | * @api public 23 | * @param {number} size 24 | * @return {Progress} Progress instance. 25 | */ 26 | Progress.prototype.size = function(size) { 27 | this._size = size; 28 | return this; 29 | }; 30 | 31 | /** 32 | * Set text to `text`. 33 | * 34 | * @api public 35 | * @param {string} text 36 | * @return {Progress} Progress instance. 37 | */ 38 | Progress.prototype.text = function(text) { 39 | this._text = text; 40 | return this; 41 | }; 42 | 43 | /** 44 | * Set font size to `size`. 45 | * 46 | * @api public 47 | * @param {number} size 48 | * @return {Progress} Progress instance. 49 | */ 50 | Progress.prototype.fontSize = function(size) { 51 | this._fontSize = size; 52 | return this; 53 | }; 54 | 55 | /** 56 | * Set font to `family`. 57 | * 58 | * @param {string} family 59 | * @return {Progress} Progress instance. 60 | */ 61 | Progress.prototype.font = function(family) { 62 | this._font = family; 63 | return this; 64 | }; 65 | 66 | /** 67 | * Update percentage to `n`. 68 | * 69 | * @param {number} n 70 | * @return {Progress} Progress instance. 71 | */ 72 | Progress.prototype.update = function(n) { 73 | this.percent = n; 74 | return this; 75 | }; 76 | 77 | /** 78 | * Draw on `ctx`. 79 | * 80 | * @param {CanvasRenderingContext2d} ctx 81 | * @return {Progress} Progress instance. 82 | */ 83 | Progress.prototype.draw = function(ctx) { 84 | try { 85 | const percent = Math.min(this.percent, 100); 86 | const size = this._size; 87 | const half = size / 2; 88 | const x = half; 89 | const y = half; 90 | const rad = half - 1; 91 | const fontSize = this._fontSize; 92 | 93 | ctx.font = fontSize + 'px ' + this._font; 94 | 95 | const angle = Math.PI * 2 * (percent / 100); 96 | ctx.clearRect(0, 0, size, size); 97 | 98 | // outer circle 99 | ctx.strokeStyle = '#9f9f9f'; 100 | ctx.beginPath(); 101 | ctx.arc(x, y, rad, 0, angle, false); 102 | ctx.stroke(); 103 | 104 | // inner circle 105 | ctx.strokeStyle = '#eee'; 106 | ctx.beginPath(); 107 | ctx.arc(x, y, rad - 1, 0, angle, true); 108 | ctx.stroke(); 109 | 110 | // text 111 | // eslint-disable-next-line no-bitwise 112 | const text = this._text || (percent | 0) + '%'; 113 | const w = ctx.measureText(text).width; 114 | 115 | ctx.fillText(text, x - w / 2 + 1, y + fontSize / 2 - 1); 116 | } catch (ignore) { 117 | // don't fail if we can't render progress 118 | } 119 | return this; 120 | }; 121 | -------------------------------------------------------------------------------- /lib/mocha/browser/tty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.isatty = function isatty() { 4 | return true; 5 | }; 6 | 7 | exports.getWindowSize = function getWindowSize() { 8 | if ('innerHeight' in global) { 9 | return [global.innerHeight, global.innerWidth]; 10 | } 11 | // In a Web Worker, the DOM Window is not available. 12 | return [640, 480]; 13 | }; 14 | -------------------------------------------------------------------------------- /lib/mocha/context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module Context 4 | */ 5 | /** 6 | * Expose `Context`. 7 | */ 8 | 9 | module.exports = Context; 10 | 11 | /** 12 | * Initialize a new `Context`. 13 | * 14 | * @api private 15 | */ 16 | function Context() {} 17 | 18 | /** 19 | * Set or get the context `Runnable` to `runnable`. 20 | * 21 | * @api private 22 | * @param {Runnable} runnable 23 | * @return {Context} context 24 | */ 25 | Context.prototype.runnable = function(runnable) { 26 | if (!arguments.length) { 27 | return this._runnable; 28 | } 29 | this.test = this._runnable = runnable; 30 | return this; 31 | }; 32 | 33 | /** 34 | * Set or get test timeout `ms`. 35 | * 36 | * @api private 37 | * @param {number} ms 38 | * @return {Context} self 39 | */ 40 | Context.prototype.timeout = function(ms) { 41 | if (!arguments.length) { 42 | return this.runnable().timeout(); 43 | } 44 | this.runnable().timeout(ms); 45 | return this; 46 | }; 47 | 48 | /** 49 | * Set test timeout `enabled`. 50 | * 51 | * @api private 52 | * @param {boolean} enabled 53 | * @return {Context} self 54 | */ 55 | Context.prototype.enableTimeouts = function(enabled) { 56 | if (!arguments.length) { 57 | return this.runnable().enableTimeouts(); 58 | } 59 | this.runnable().enableTimeouts(enabled); 60 | return this; 61 | }; 62 | 63 | /** 64 | * Set or get test slowness threshold `ms`. 65 | * 66 | * @api private 67 | * @param {number} ms 68 | * @return {Context} self 69 | */ 70 | Context.prototype.slow = function(ms) { 71 | if (!arguments.length) { 72 | return this.runnable().slow(); 73 | } 74 | this.runnable().slow(ms); 75 | return this; 76 | }; 77 | 78 | /** 79 | * Mark a test as skipped. 80 | * 81 | * @api private 82 | * @throws Pending 83 | */ 84 | Context.prototype.skip = function() { 85 | this.runnable().skip(); 86 | }; 87 | 88 | /** 89 | * Set or get a number of allowed retries on failed tests 90 | * 91 | * @api private 92 | * @param {number} n 93 | * @return {Context} self 94 | */ 95 | Context.prototype.retries = function(n) { 96 | if (!arguments.length) { 97 | return this.runnable().retries(); 98 | } 99 | this.runnable().retries(n); 100 | return this; 101 | }; 102 | -------------------------------------------------------------------------------- /lib/mocha/hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Runnable = require('./runnable'); 4 | const inherits = require('./utils').inherits; 5 | 6 | /** 7 | * Expose `Hook`. 8 | */ 9 | 10 | module.exports = Hook; 11 | 12 | /** 13 | * Initialize a new `Hook` with the given `title` and callback `fn` 14 | * 15 | * @class 16 | * @augments Runnable 17 | * @param {String} title 18 | * @param {Function} fn 19 | */ 20 | function Hook(title, fn) { 21 | Runnable.call(this, title, fn); 22 | this.type = 'hook'; 23 | } 24 | 25 | /** 26 | * Inherit from `Runnable.prototype`. 27 | */ 28 | inherits(Hook, Runnable); 29 | 30 | /** 31 | * Get or set the test `err`. 32 | * 33 | * @memberof Hook 34 | * @public 35 | * @param {Error} err 36 | * @return {Error} 37 | */ 38 | Hook.prototype.error = function(err) { 39 | if (!arguments.length) { 40 | err = this._error; 41 | this._error = null; 42 | return err; 43 | } 44 | 45 | this._error = err; 46 | }; 47 | -------------------------------------------------------------------------------- /lib/mocha/interfaces/bdd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Test = require('../test'); 4 | 5 | /** 6 | * BDD-style interface: 7 | * 8 | * describe('Array', function() { 9 | * describe('#indexOf()', function() { 10 | * it('should return -1 when not present', function() { 11 | * // ... 12 | * }); 13 | * 14 | * it('should return the index when present', function() { 15 | * // ... 16 | * }); 17 | * }); 18 | * }); 19 | * 20 | * @param {Suite} suite Root suite. 21 | */ 22 | module.exports = function bddInterface(suite) { 23 | const suites = [suite]; 24 | 25 | suite.on('pre-require', function(context, file, mocha) { 26 | const common = require('./common')(suites, context, mocha); 27 | 28 | context.before = common.before; 29 | context.after = common.after; 30 | context.beforeEach = common.beforeEach; 31 | context.afterEach = common.afterEach; 32 | context.run = mocha.options.delay && common.runWithSuite(suite); 33 | /** 34 | * Describe a "suite" with the given `title` 35 | * and callback `fn` containing nested suites 36 | * and/or tests. 37 | */ 38 | 39 | context.describe = context.context = function(title, fn) { 40 | return common.suite.create({ 41 | title, 42 | file, 43 | fn 44 | }); 45 | }; 46 | 47 | /** 48 | * Pending describe. 49 | */ 50 | 51 | context.xdescribe = context.xcontext = context.describe.skip = function( 52 | title, 53 | fn 54 | ) { 55 | return common.suite.skip({ 56 | title, 57 | file, 58 | fn 59 | }); 60 | }; 61 | 62 | /** 63 | * Exclusive suite. 64 | */ 65 | 66 | context.describe.only = function(title, fn) { 67 | return common.suite.only({ 68 | title, 69 | file, 70 | fn 71 | }); 72 | }; 73 | 74 | /** 75 | * Describe a specification or test-case 76 | * with the given `title` and callback `fn` 77 | * acting as a thunk. 78 | */ 79 | 80 | context.it = context.specify = function(title, fn) { 81 | const suite = suites[0]; 82 | if (suite.isPending()) { 83 | fn = null; 84 | } 85 | const test = new Test(title, fn); 86 | test.file = file; 87 | suite.addTest(test); 88 | return test; 89 | }; 90 | 91 | /** 92 | * Exclusive test-case. 93 | */ 94 | 95 | context.it.only = function(title, fn) { 96 | return common.test.only(mocha, context.it(title, fn)); 97 | }; 98 | 99 | /** 100 | * Pending test case. 101 | */ 102 | 103 | context.xit = context.xspecify = context.it.skip = function(title) { 104 | return context.it(title); 105 | }; 106 | 107 | /** 108 | * times of attempts to retry. 109 | */ 110 | context.it.retries = function(times, title, fn) { 111 | const suite = suites[0]; 112 | if (suite.isPending()) { 113 | fn = null; 114 | } 115 | const test = new Test(title, fn); 116 | test.file = file; 117 | suite.addTest(test); 118 | test.retries(times); 119 | return test; 120 | }; 121 | }); 122 | }; 123 | -------------------------------------------------------------------------------- /lib/mocha/interfaces/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Suite = require('../suite'); 4 | 5 | /** 6 | * Functions common to more than one interface. 7 | * 8 | * @param {Suite[]} suites 9 | * @param {Context} context 10 | * @return {Object} An object containing common functions. 11 | */ 12 | module.exports = function(suites, context) { 13 | return { 14 | /** 15 | * This is only present if flag --delay is passed into Mocha. It triggers 16 | * root suite execution. 17 | * 18 | * @param {Suite} suite The root suite. 19 | * @return {Function} A function which runs the root suite 20 | */ 21 | runWithSuite: function runWithSuite(suite) { 22 | return function run() { 23 | suite.run(); 24 | }; 25 | }, 26 | 27 | /** 28 | * Execute before running tests. 29 | * 30 | * @param {string} name 31 | * @param {Function} fn 32 | */ 33 | before(name, fn) { 34 | suites[0].beforeAll(name, fn); 35 | }, 36 | 37 | /** 38 | * Execute after running tests. 39 | * 40 | * @param {string} name 41 | * @param {Function} fn 42 | */ 43 | after(name, fn) { 44 | suites[0].afterAll(name, fn); 45 | }, 46 | 47 | /** 48 | * Execute before each test case. 49 | * 50 | * @param {string} name 51 | * @param {Function} fn 52 | */ 53 | beforeEach(name, fn) { 54 | suites[0].beforeEach(name, fn); 55 | }, 56 | 57 | /** 58 | * Execute after each test case. 59 | * 60 | * @param {string} name 61 | * @param {Function} fn 62 | */ 63 | afterEach(name, fn) { 64 | suites[0].afterEach(name, fn); 65 | }, 66 | 67 | suite: { 68 | /** 69 | * Create an exclusive Suite; convenience function 70 | * See docstring for create() below. 71 | * 72 | * @param {Object} opts 73 | * @return {Suite} 74 | */ 75 | only: function only(opts) { 76 | opts.isOnly = true; 77 | return this.create(opts); 78 | }, 79 | 80 | /** 81 | * Create a Suite, but skip it; convenience function 82 | * See docstring for create() below. 83 | * 84 | * @param {Object} opts 85 | * @return {Suite} 86 | */ 87 | skip: function skip(opts) { 88 | opts.pending = true; 89 | return this.create(opts); 90 | }, 91 | 92 | /** 93 | * Creates a suite. 94 | * @param {Object} opts Options 95 | * @param {string} opts.title Title of Suite 96 | * @param {Function} [opts.fn] Suite Function (not always applicable) 97 | * @param {boolean} [opts.pending] Is Suite pending? 98 | * @param {string} [opts.file] Filepath where this Suite resides 99 | * @param {boolean} [opts.isOnly] Is Suite exclusive? 100 | * @return {Suite} 101 | */ 102 | create: function create(opts) { 103 | const suite = Suite.create(suites[0], opts.title); 104 | suite.pending = Boolean(opts.pending); 105 | suite.file = opts.file; 106 | suites.unshift(suite); 107 | if (opts.isOnly) { 108 | suite.parent._onlySuites = suite.parent._onlySuites.concat(suite); 109 | } 110 | if (typeof opts.fn === 'function') { 111 | opts.fn.call(suite); 112 | suites.shift(); 113 | } else if (typeof opts.fn === 'undefined' && !suite.pending) { 114 | throw new Error( 115 | 'Suite "' + 116 | suite.fullTitle() + 117 | '" was defined but no callback was supplied. Supply a callback or explicitly skip the suite.' 118 | ); 119 | } else if (!opts.fn && suite.pending) { 120 | suites.shift(); 121 | } 122 | 123 | return suite; 124 | } 125 | }, 126 | 127 | test: { 128 | /** 129 | * Exclusive test-case. 130 | * 131 | * @param {Object} mocha 132 | * @param {Function} test 133 | * @return {*} 134 | */ 135 | only(mocha, test) { 136 | test.parent._onlyTests = test.parent._onlyTests.concat(test); 137 | return test; 138 | }, 139 | 140 | /** 141 | * Pending test case. 142 | * 143 | * @param {string} title 144 | */ 145 | skip(title) { 146 | context.test(title); 147 | }, 148 | 149 | /** 150 | * Number of retry attempts 151 | * 152 | * @param {number} n 153 | */ 154 | retries(n) { 155 | context.retries(n); 156 | } 157 | } 158 | }; 159 | }; 160 | -------------------------------------------------------------------------------- /lib/mocha/interfaces/exports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Suite = require('../suite'); 3 | const Test = require('../test'); 4 | 5 | /** 6 | * Exports-style (as Node.js module) interface: 7 | * 8 | * exports.Array = { 9 | * '#indexOf()': { 10 | * 'should return -1 when the value is not present': function() { 11 | * 12 | * }, 13 | * 14 | * 'should return the correct index when the value is present': function() { 15 | * 16 | * } 17 | * } 18 | * }; 19 | * 20 | * @param {Suite} suite Root suite. 21 | */ 22 | module.exports = function(suite) { 23 | const suites = [suite]; 24 | 25 | suite.on('require', visit); 26 | 27 | function visit(obj, file) { 28 | let suite; 29 | for (const key in obj) { 30 | if (typeof obj[key] === 'function') { 31 | const fn = obj[key]; 32 | switch (key) { 33 | case 'before': 34 | suites[0].beforeAll(fn); 35 | break; 36 | case 'after': 37 | suites[0].afterAll(fn); 38 | break; 39 | case 'beforeEach': 40 | suites[0].beforeEach(fn); 41 | break; 42 | case 'afterEach': 43 | suites[0].afterEach(fn); 44 | break; 45 | default: 46 | // eslint-disable-next-line no-var 47 | var test = new Test(key, fn); 48 | test.file = file; 49 | suites[0].addTest(test); 50 | } 51 | } else { 52 | suite = Suite.create(suites[0], key); 53 | suites.unshift(suite); 54 | visit(obj[key], file); 55 | suites.shift(); 56 | } 57 | } 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /lib/mocha/interfaces/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.bdd = require('./bdd'); 4 | exports.tdd = require('./tdd'); 5 | exports.qunit = require('./qunit'); 6 | exports.exports = require('./exports'); 7 | -------------------------------------------------------------------------------- /lib/mocha/interfaces/qunit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Test = require('../test'); 4 | 5 | /** 6 | * QUnit-style interface: 7 | * 8 | * suite('Array'); 9 | * 10 | * test('#length', function() { 11 | * var arr = [1,2,3]; 12 | * ok(arr.length == 3); 13 | * }); 14 | * 15 | * test('#indexOf()', function() { 16 | * var arr = [1,2,3]; 17 | * ok(arr.indexOf(1) == 0); 18 | * ok(arr.indexOf(2) == 1); 19 | * ok(arr.indexOf(3) == 2); 20 | * }); 21 | * 22 | * suite('String'); 23 | * 24 | * test('#length', function() { 25 | * ok('foo'.length == 3); 26 | * }); 27 | * 28 | * @param {Suite} suite Root suite. 29 | */ 30 | module.exports = function qUnitInterface(suite) { 31 | const suites = [suite]; 32 | 33 | suite.on('pre-require', function(context, file, mocha) { 34 | const common = require('./common')(suites, context, mocha); 35 | 36 | context.before = common.before; 37 | context.after = common.after; 38 | context.beforeEach = common.beforeEach; 39 | context.afterEach = common.afterEach; 40 | context.run = mocha.options.delay && common.runWithSuite(suite); 41 | /** 42 | * Describe a "suite" with the given `title`. 43 | */ 44 | 45 | context.suite = function(title) { 46 | if (suites.length > 1) { 47 | suites.shift(); 48 | } 49 | return common.suite.create({ 50 | title, 51 | file, 52 | fn: false 53 | }); 54 | }; 55 | 56 | /** 57 | * Exclusive Suite. 58 | */ 59 | 60 | context.suite.only = function(title) { 61 | if (suites.length > 1) { 62 | suites.shift(); 63 | } 64 | return common.suite.only({ 65 | title, 66 | file, 67 | fn: false 68 | }); 69 | }; 70 | 71 | /** 72 | * Describe a specification or test-case 73 | * with the given `title` and callback `fn` 74 | * acting as a thunk. 75 | */ 76 | 77 | context.test = function(title, fn) { 78 | const test = new Test(title, fn); 79 | test.file = file; 80 | suites[0].addTest(test); 81 | return test; 82 | }; 83 | 84 | /** 85 | * Exclusive test-case. 86 | */ 87 | 88 | context.test.only = function(title, fn) { 89 | return common.test.only(mocha, context.test(title, fn)); 90 | }; 91 | 92 | context.test.skip = common.test.skip; 93 | context.test.retries = common.test.retries; 94 | }); 95 | }; 96 | -------------------------------------------------------------------------------- /lib/mocha/interfaces/tdd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Test = require('../test'); 4 | 5 | /** 6 | * TDD-style interface: 7 | * 8 | * suite('Array', function() { 9 | * suite('#indexOf()', function() { 10 | * suiteSetup(function() { 11 | * 12 | * }); 13 | * 14 | * test('should return -1 when not present', function() { 15 | * 16 | * }); 17 | * 18 | * test('should return the index when present', function() { 19 | * 20 | * }); 21 | * 22 | * suiteTeardown(function() { 23 | * 24 | * }); 25 | * }); 26 | * }); 27 | * 28 | * @param {Suite} suite Root suite. 29 | */ 30 | module.exports = function(suite) { 31 | const suites = [suite]; 32 | 33 | suite.on('pre-require', function(context, file, mocha) { 34 | const common = require('./common')(suites, context, mocha); 35 | 36 | context.setup = common.beforeEach; 37 | context.teardown = common.afterEach; 38 | context.suiteSetup = common.before; 39 | context.suiteTeardown = common.after; 40 | context.run = mocha.options.delay && common.runWithSuite(suite); 41 | 42 | /** 43 | * Describe a "suite" with the given `title` and callback `fn` containing 44 | * nested suites and/or tests. 45 | */ 46 | context.suite = function(title, fn) { 47 | return common.suite.create({ 48 | title, 49 | file, 50 | fn 51 | }); 52 | }; 53 | 54 | /** 55 | * Pending suite. 56 | */ 57 | context.suite.skip = function(title, fn) { 58 | return common.suite.skip({ 59 | title, 60 | file, 61 | fn 62 | }); 63 | }; 64 | 65 | /** 66 | * Exclusive test-case. 67 | */ 68 | context.suite.only = function(title, fn) { 69 | return common.suite.only({ 70 | title, 71 | file, 72 | fn 73 | }); 74 | }; 75 | 76 | /** 77 | * Describe a specification or test-case with the given `title` and 78 | * callback `fn` acting as a thunk. 79 | */ 80 | context.test = function(title, fn) { 81 | const suite = suites[0]; 82 | if (suite.isPending()) { 83 | fn = null; 84 | } 85 | const test = new Test(title, fn); 86 | test.file = file; 87 | suite.addTest(test); 88 | return test; 89 | }; 90 | 91 | /** 92 | * Exclusive test-case. 93 | */ 94 | 95 | context.test.only = function(title, fn) { 96 | return common.test.only(mocha, context.test(title, fn)); 97 | }; 98 | 99 | context.test.skip = common.test.skip; 100 | context.test.retries = common.test.retries; 101 | }); 102 | }; 103 | -------------------------------------------------------------------------------- /lib/mocha/mocha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* ! 4 | * mocha 5 | * Copyright(c) 2011 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | const escapeRe = require('escape-string-regexp'); 10 | const path = require('path'); 11 | const reporters = require('./reporters'); 12 | const utils = require('./utils'); 13 | 14 | exports = module.exports = Mocha; 15 | 16 | /** 17 | * To require local UIs and reporters when running in node. 18 | */ 19 | 20 | if (!process.browser) { 21 | const cwd = process.cwd(); 22 | module.paths.push(cwd, path.join(cwd, 'node_modules')); 23 | } 24 | 25 | /** 26 | * Expose internals. 27 | */ 28 | 29 | /** 30 | * @public 31 | * @class utils 32 | * @memberof Mocha 33 | */ 34 | exports.utils = utils; 35 | exports.interfaces = require('./interfaces'); 36 | /** 37 | * 38 | * @memberof Mocha 39 | * @public 40 | */ 41 | exports.reporters = reporters; 42 | exports.Runnable = require('./runnable'); 43 | exports.Context = require('./context'); 44 | /** 45 | * 46 | * @memberof Mocha 47 | */ 48 | exports.Runner = require('./runner'); 49 | exports.Suite = require('./suite'); 50 | exports.Hook = require('./hook'); 51 | exports.Test = require('./test'); 52 | 53 | /** 54 | * Set up mocha with `options`. 55 | * 56 | * Options: 57 | * 58 | * - `ui` name "bdd", "tdd", "exports" etc 59 | * - `reporter` reporter instance, defaults to `mocha.reporters.spec` 60 | * - `globals` array of accepted globals 61 | * - `timeout` timeout in milliseconds 62 | * - `retries` number of times to retry failed tests 63 | * - `bail` bail on the first test failure 64 | * - `slow` milliseconds to wait before considering a test slow 65 | * - `ignoreLeaks` ignore global leaks 66 | * - `fullTrace` display the full stack-trace on failing 67 | * - `grep` string or regexp to filter tests with 68 | * 69 | * @class Mocha 70 | * @param {Object} options 71 | */ 72 | function Mocha(options) { 73 | options = options || {}; 74 | this.files = []; 75 | this.options = options; 76 | if (options.grep) { 77 | this.grep(new RegExp(options.grep)); 78 | } 79 | if (options.fgrep) { 80 | this.fgrep(options.fgrep); 81 | } 82 | this.suite = new exports.Suite('', new exports.Context()); 83 | this.ui(options.ui); 84 | this.bail(options.bail); 85 | this.reporter(options.reporter, options.reporterOptions); 86 | if (typeof options.timeout !== 'undefined' && options.timeout !== null) { 87 | this.timeout(options.timeout); 88 | } 89 | if (typeof options.retries !== 'undefined' && options.retries !== null) { 90 | this.retries(options.retries); 91 | } 92 | this.useColors(options.useColors); 93 | if (options.enableTimeouts !== null) { 94 | this.enableTimeouts(options.enableTimeouts); 95 | } 96 | if (options.slow) { 97 | this.slow(options.slow); 98 | } 99 | } 100 | 101 | /** 102 | * Enable or disable bailing on the first failure. 103 | * 104 | * @public 105 | * @api public 106 | * @param {boolean} [bail] 107 | */ 108 | Mocha.prototype.bail = function(bail) { 109 | if (!arguments.length) { 110 | bail = true; 111 | } 112 | this.suite.bail(bail); 113 | return this; 114 | }; 115 | 116 | /** 117 | * Add test `file`. 118 | * 119 | * @public 120 | * @api public 121 | * @param {string} file 122 | */ 123 | Mocha.prototype.addFile = function(file) { 124 | this.files.push(file); 125 | return this; 126 | }; 127 | 128 | /** 129 | * Set reporter to `reporter`, defaults to "spec". 130 | * 131 | * @public 132 | * @param {String|Function} reporter name or constructor 133 | * @param {Object} reporterOptions optional options 134 | * @api public 135 | * @param {string|Function} reporter name or constructor 136 | * @param {Object} reporterOptions optional options 137 | */ 138 | Mocha.prototype.reporter = function(reporter, reporterOptions) { 139 | if (typeof reporter === 'function') { 140 | this._reporter = reporter; 141 | } else { 142 | reporter = reporter || 'spec'; 143 | let _reporter; 144 | // Try to load a built-in reporter. 145 | if (reporters[reporter]) { 146 | _reporter = reporters[reporter]; 147 | } 148 | // Try to load reporters from process.cwd() and node_modules 149 | if (!_reporter) { 150 | try { 151 | _reporter = require(reporter); 152 | } catch (err) { 153 | if (err.message.indexOf('Cannot find module') !== -1) { 154 | // Try to load reporters from a path (absolute or relative) 155 | try { 156 | _reporter = require(path.resolve(process.cwd(), reporter)); 157 | } catch (_err) { 158 | err.message.indexOf('Cannot find module') !== -1 159 | ? console.warn('"' + reporter + '" reporter not found') 160 | : console.warn( 161 | '"' + 162 | reporter + 163 | '" reporter blew up with error:\n' + 164 | err.stack 165 | ); 166 | } 167 | } else { 168 | console.warn( 169 | '"' + reporter + '" reporter blew up with error:\n' + err.stack 170 | ); 171 | } 172 | } 173 | } 174 | if (!_reporter && reporter === 'teamcity') { 175 | console.warn( 176 | 'The Teamcity reporter was moved to a package named ' + 177 | 'mocha-teamcity-reporter ' + 178 | '(https://npmjs.org/package/mocha-teamcity-reporter).' 179 | ); 180 | } 181 | if (!_reporter) { 182 | throw new Error('invalid reporter "' + reporter + '"'); 183 | } 184 | this._reporter = _reporter; 185 | } 186 | this.options.reporterOptions = reporterOptions; 187 | return this; 188 | }; 189 | 190 | /** 191 | * Set test UI `name`, defaults to "bdd". 192 | * @public 193 | * @api public 194 | * @param {string} bdd 195 | */ 196 | Mocha.prototype.ui = function(name) { 197 | name = name || 'bdd'; 198 | this._ui = exports.interfaces[name]; 199 | if (!this._ui) { 200 | try { 201 | this._ui = require(name); 202 | } catch (err) { 203 | throw new Error('invalid interface "' + name + '"'); 204 | } 205 | } 206 | this._ui = this._ui(this.suite); 207 | 208 | this.suite.on('pre-require', function(context) { 209 | exports.afterEach = context.afterEach || context.teardown; 210 | exports.after = context.after || context.suiteTeardown; 211 | exports.beforeEach = context.beforeEach || context.setup; 212 | exports.before = context.before || context.suiteSetup; 213 | exports.describe = context.describe || context.suite; 214 | exports.it = context.it || context.test; 215 | exports.xit = context.xit || context.test.skip; 216 | exports.setup = context.setup || context.beforeEach; 217 | exports.suiteSetup = context.suiteSetup || context.before; 218 | exports.suiteTeardown = context.suiteTeardown || context.after; 219 | exports.suite = context.suite || context.describe; 220 | exports.teardown = context.teardown || context.afterEach; 221 | exports.test = context.test || context.it; 222 | exports.run = context.run; 223 | }); 224 | 225 | return this; 226 | }; 227 | 228 | /** 229 | * Load registered files. 230 | * 231 | * @api private 232 | */ 233 | Mocha.prototype.loadFiles = function(fn) { 234 | const self = this; 235 | const suite = this.suite; 236 | this.files.forEach(function(file) { 237 | file = path.resolve(file); 238 | suite.emit('pre-require', global, file, self); 239 | suite.emit('require', require(file), file, self); 240 | suite.emit('post-require', global, file, self); 241 | }); 242 | fn && fn(); 243 | }; 244 | 245 | /** 246 | * Escape string and add it to grep as a regexp. 247 | * 248 | * @public 249 | * @api public 250 | * @param str 251 | * @return {Mocha} 252 | */ 253 | Mocha.prototype.fgrep = function(str) { 254 | return this.grep(new RegExp(escapeRe(str))); 255 | }; 256 | 257 | /** 258 | * Add regexp to grep, if `re` is a string it is escaped. 259 | * 260 | * @public 261 | * @param {RegExp|String} re 262 | * @return {Mocha} 263 | * @api public 264 | * @param {RegExp|string} re 265 | * @return {Mocha} 266 | */ 267 | Mocha.prototype.grep = function(re) { 268 | if (utils.isString(re)) { 269 | // extract args if it's regex-like, i.e: [string, pattern, flag] 270 | const arg = re.match(/^\/(.*)\/(g|i|)$|.*/); 271 | this.options.grep = new RegExp(arg[1] || arg[0], arg[2]); 272 | } else { 273 | this.options.grep = re; 274 | } 275 | return this; 276 | }; 277 | /** 278 | * Invert `.grep()` matches. 279 | * 280 | * @public 281 | * @return {Mocha} 282 | * @api public 283 | */ 284 | Mocha.prototype.invert = function() { 285 | this.options.invert = true; 286 | return this; 287 | }; 288 | 289 | /** 290 | * Ignore global leaks. 291 | * 292 | * @public 293 | * @param {Boolean} ignore 294 | * @return {Mocha} 295 | * @api public 296 | * @param {boolean} ignore 297 | * @return {Mocha} 298 | */ 299 | Mocha.prototype.ignoreLeaks = function(ignore) { 300 | this.options.ignoreLeaks = Boolean(ignore); 301 | return this; 302 | }; 303 | 304 | /** 305 | * Enable global leak checking. 306 | * 307 | * @return {Mocha} 308 | * @api public 309 | * @public 310 | */ 311 | Mocha.prototype.checkLeaks = function() { 312 | this.options.ignoreLeaks = false; 313 | return this; 314 | }; 315 | 316 | /** 317 | * Display long stack-trace on failing 318 | * 319 | * @return {Mocha} 320 | * @api public 321 | * @public 322 | */ 323 | Mocha.prototype.fullTrace = function() { 324 | this.options.fullStackTrace = true; 325 | return this; 326 | }; 327 | 328 | /** 329 | * Enable growl support. 330 | * 331 | * @return {Mocha} 332 | * @api public 333 | * @public 334 | */ 335 | Mocha.prototype.growl = function() { 336 | this.options.growl = true; 337 | return this; 338 | }; 339 | 340 | /** 341 | * Ignore `globals` array or string. 342 | * 343 | * @param {Array|String} globals 344 | * @return {Mocha} 345 | * @api public 346 | * @public 347 | * @param {Array|string} globals 348 | * @return {Mocha} 349 | */ 350 | Mocha.prototype.globals = function(globals) { 351 | this.options.globals = (this.options.globals || []).concat(globals); 352 | return this; 353 | }; 354 | 355 | /** 356 | * Emit color output. 357 | * 358 | * @param {Boolean} colors 359 | * @return {Mocha} 360 | * @api public 361 | * @public 362 | * @param {boolean} colors 363 | * @return {Mocha} 364 | */ 365 | Mocha.prototype.useColors = function(colors) { 366 | if (colors !== undefined) { 367 | this.options.useColors = colors; 368 | } 369 | return this; 370 | }; 371 | 372 | /** 373 | * Use inline diffs rather than +/-. 374 | * 375 | * @param {Boolean} inlineDiffs 376 | * @return {Mocha} 377 | * @api public 378 | * @public 379 | * @param {boolean} inlineDiffs 380 | * @return {Mocha} 381 | */ 382 | Mocha.prototype.useInlineDiffs = function(inlineDiffs) { 383 | this.options.useInlineDiffs = inlineDiffs !== undefined && inlineDiffs; 384 | return this; 385 | }; 386 | 387 | /** 388 | * Do not show diffs at all. 389 | * 390 | * @param {Boolean} hideDiff 391 | * @return {Mocha} 392 | * @api public 393 | * @public 394 | * @param {boolean} hideDiff 395 | * @return {Mocha} 396 | */ 397 | Mocha.prototype.hideDiff = function(hideDiff) { 398 | this.options.hideDiff = hideDiff !== undefined && hideDiff; 399 | return this; 400 | }; 401 | 402 | /** 403 | * Set the timeout in milliseconds. 404 | * 405 | * @param {Number} timeout 406 | * @return {Mocha} 407 | * @api public 408 | * @public 409 | * @param {number} timeout 410 | * @return {Mocha} 411 | */ 412 | Mocha.prototype.timeout = function(timeout) { 413 | this.suite.timeout(timeout); 414 | return this; 415 | }; 416 | 417 | /** 418 | * Set the number of times to retry failed tests. 419 | * 420 | * @param {Number} retry times 421 | * @return {Mocha} 422 | * @api public 423 | * @public 424 | */ 425 | Mocha.prototype.retries = function(n) { 426 | this.suite.retries(n); 427 | return this; 428 | }; 429 | 430 | /** 431 | * Set slowness threshold in milliseconds. 432 | * 433 | * @param {Number} slow 434 | * @return {Mocha} 435 | * @api public 436 | * @public 437 | * @param {number} slow 438 | * @return {Mocha} 439 | */ 440 | Mocha.prototype.slow = function(slow) { 441 | this.suite.slow(slow); 442 | return this; 443 | }; 444 | 445 | /** 446 | * Enable timeouts. 447 | * 448 | * @param {Boolean} enabled 449 | * @return {Mocha} 450 | * @api public 451 | * @public 452 | * @param {boolean} enabled 453 | * @return {Mocha} 454 | */ 455 | Mocha.prototype.enableTimeouts = function(enabled) { 456 | this.suite.enableTimeouts( 457 | arguments.length && enabled !== undefined ? enabled : true 458 | ); 459 | return this; 460 | }; 461 | 462 | /** 463 | * Makes all tests async (accepting a callback) 464 | * 465 | * @return {Mocha} 466 | * @api public 467 | * @public 468 | */ 469 | Mocha.prototype.asyncOnly = function() { 470 | this.options.asyncOnly = true; 471 | return this; 472 | }; 473 | 474 | /** 475 | * Disable syntax highlighting (in browser). 476 | * 477 | * @api public 478 | * @public 479 | */ 480 | Mocha.prototype.noHighlighting = function() { 481 | this.options.noHighlighting = true; 482 | return this; 483 | }; 484 | 485 | /** 486 | * Enable uncaught errors to propagate (in browser). 487 | * 488 | * @return {Mocha} 489 | * @api public 490 | * @public 491 | */ 492 | Mocha.prototype.allowUncaught = function() { 493 | this.options.allowUncaught = true; 494 | return this; 495 | }; 496 | 497 | /** 498 | * Delay root suite execution. 499 | * @return {Mocha} 500 | */ 501 | Mocha.prototype.delay = function delay() { 502 | this.options.delay = true; 503 | return this; 504 | }; 505 | 506 | /** 507 | * Tests marked only fail the suite 508 | * @return {Mocha} 509 | */ 510 | Mocha.prototype.forbidOnly = function() { 511 | this.options.forbidOnly = true; 512 | return this; 513 | }; 514 | 515 | /** 516 | * Pending tests and tests marked skip fail the suite 517 | * @return {Mocha} 518 | */ 519 | Mocha.prototype.forbidPending = function() { 520 | this.options.forbidPending = true; 521 | return this; 522 | }; 523 | 524 | /** 525 | * Run tests and invoke `fn()` when complete. 526 | * 527 | * Note that `loadFiles` relies on Node's `require` to execute 528 | * the test interface functions and will be subject to the 529 | * cache - if the files are already in the `require` cache, 530 | * they will effectively be skipped. Therefore, to run tests 531 | * multiple times or to run tests in files that are already 532 | * in the `require` cache, make sure to clear them from the 533 | * cache first in whichever manner best suits your needs. 534 | * 535 | * @api public 536 | * @public 537 | * @param {Function} fn 538 | * @return {Runner} 539 | */ 540 | Mocha.prototype.run = function(fn) { 541 | if (this.files.length) { 542 | this.loadFiles(); 543 | } 544 | const suite = this.suite; 545 | const options = this.options; 546 | options.files = this.files; 547 | const runner = new exports.Runner(suite, options.delay); 548 | const reporter = new this._reporter(runner, options); 549 | runner.ignoreLeaks = options.ignoreLeaks !== false; 550 | runner.fullStackTrace = options.fullStackTrace; 551 | runner.asyncOnly = options.asyncOnly; 552 | runner.allowUncaught = options.allowUncaught; 553 | runner.forbidOnly = options.forbidOnly; 554 | runner.forbidPending = options.forbidPending; 555 | if (options.grep) { 556 | runner.grep(options.grep, options.invert); 557 | } 558 | if (options.globals) { 559 | runner.globals(options.globals); 560 | } 561 | if (options.growl) { 562 | this._growl(runner, reporter); 563 | } 564 | if (options.useColors !== undefined) { 565 | exports.reporters.Base.useColors = options.useColors; 566 | } 567 | exports.reporters.Base.inlineDiffs = options.useInlineDiffs; 568 | exports.reporters.Base.hideDiff = options.hideDiff; 569 | 570 | function done(failures) { 571 | if (reporter.done) { 572 | reporter.done(failures, fn); 573 | } else { 574 | fn && fn(failures); 575 | } 576 | } 577 | 578 | return runner.run(done); 579 | }; 580 | -------------------------------------------------------------------------------- /lib/mocha/ms.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module milliseconds 4 | */ 5 | /** 6 | * Helpers. 7 | */ 8 | 9 | const s = 1000; 10 | const m = s * 60; 11 | const h = m * 60; 12 | const d = h * 24; 13 | const y = d * 365.25; 14 | 15 | /** 16 | * Parse or format the given `val`. 17 | * 18 | * @memberof Mocha 19 | * @public 20 | * @api public 21 | * @param {string|number} val 22 | * @return {string|number} 23 | */ 24 | module.exports = function(val) { 25 | if (typeof val === 'string') { 26 | return parse(val); 27 | } 28 | return format(val); 29 | }; 30 | 31 | /** 32 | * Parse the given `str` and return milliseconds. 33 | * 34 | * @api private 35 | * @param {string} str 36 | * @return {number} 37 | */ 38 | function parse(str) { 39 | const match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec( 40 | str 41 | ); 42 | if (!match) { 43 | return; 44 | } 45 | const n = parseFloat(match[1]); 46 | const type = (match[2] || 'ms').toLowerCase(); 47 | switch (type) { 48 | case 'years': 49 | case 'year': 50 | case 'y': 51 | return n * y; 52 | case 'days': 53 | case 'day': 54 | case 'd': 55 | return n * d; 56 | case 'hours': 57 | case 'hour': 58 | case 'h': 59 | return n * h; 60 | case 'minutes': 61 | case 'minute': 62 | case 'm': 63 | return n * m; 64 | case 'seconds': 65 | case 'second': 66 | case 's': 67 | return n * s; 68 | case 'ms': 69 | return n; 70 | default: 71 | // No default case 72 | } 73 | } 74 | 75 | /** 76 | * Format for `ms`. 77 | * 78 | * @api private 79 | * @param {number} ms 80 | * @return {string} 81 | */ 82 | function format(ms) { 83 | if (ms >= d) { 84 | return Math.round(ms / d) + 'd'; 85 | } 86 | if (ms >= h) { 87 | return Math.round(ms / h) + 'h'; 88 | } 89 | if (ms >= m) { 90 | return Math.round(ms / m) + 'm'; 91 | } 92 | if (ms >= s) { 93 | return Math.round(ms / s) + 's'; 94 | } 95 | return ms + 'ms'; 96 | } 97 | -------------------------------------------------------------------------------- /lib/mocha/pending.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = Pending; 4 | 5 | /** 6 | * Initialize a new `Pending` error with the given message. 7 | * 8 | * @param {string} message 9 | */ 10 | function Pending(message) { 11 | this.message = message; 12 | } 13 | -------------------------------------------------------------------------------- /lib/mocha/reporters/base.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-concat */ 2 | 'use strict'; 3 | /** 4 | * @module Base 5 | */ 6 | /** 7 | * Module dependencies. 8 | */ 9 | 10 | const tty = require('tty'); 11 | const diff = require('diff'); 12 | const ms = require('../ms'); 13 | const utils = require('../utils'); 14 | const supportsColor = process.browser ? null : require('supports-color'); 15 | 16 | /** 17 | * Expose `Base`. 18 | */ 19 | 20 | exports = module.exports = Base; 21 | 22 | /** 23 | * Save timer references to avoid Sinon interfering. 24 | * See: https://github.com/mochajs/mocha/issues/237 25 | */ 26 | 27 | /* eslint-disable no-unused-vars, no-native-reassign */ 28 | const Date = global.Date; 29 | const setTimeout = global.setTimeout; 30 | const setInterval = global.setInterval; 31 | const clearTimeout = global.clearTimeout; 32 | const clearInterval = global.clearInterval; 33 | /* eslint-enable no-unused-vars, no-native-reassign */ 34 | 35 | /** 36 | * Check if both stdio streams are associated with a tty. 37 | */ 38 | 39 | const isatty = tty.isatty(1) && tty.isatty(2); 40 | 41 | /** 42 | * Enable coloring by default, except in the browser interface. 43 | */ 44 | 45 | exports.useColors = 46 | !process.browser && 47 | (supportsColor.stdout || process.env.MOCHA_COLORS !== undefined); 48 | 49 | /** 50 | * Inline diffs instead of +/- 51 | */ 52 | 53 | exports.inlineDiffs = false; 54 | 55 | /** 56 | * Default color map. 57 | */ 58 | 59 | exports.colors = { 60 | pass: 90, 61 | fail: 31, 62 | 'bright pass': 92, 63 | 'bright fail': 91, 64 | 'bright yellow': 93, 65 | pending: 36, 66 | suite: 0, 67 | 'error title': 0, 68 | 'error message': 31, 69 | 'error stack': 90, 70 | checkmark: 32, 71 | fast: 90, 72 | medium: 33, 73 | slow: 31, 74 | green: 32, 75 | light: 90, 76 | 'diff gutter': 90, 77 | 'diff added': 32, 78 | 'diff removed': 31 79 | }; 80 | 81 | /** 82 | * Default symbol map. 83 | */ 84 | 85 | exports.symbols = { 86 | ok: '✓', 87 | err: '✖', 88 | dot: '․', 89 | comma: ',', 90 | bang: '!' 91 | }; 92 | 93 | // With node.js on Windows: use symbols available in terminal default fonts 94 | if (process.platform === 'win32') { 95 | exports.symbols.ok = '\u221A'; 96 | exports.symbols.err = '\u00D7'; 97 | exports.symbols.dot = '.'; 98 | } 99 | 100 | /** 101 | * Color `str` with the given `type`, 102 | * allowing colors to be disabled, 103 | * as well as user-defined color 104 | * schemes. 105 | * 106 | * @param {string} type 107 | * @param {string} str 108 | * @return {string} 109 | * @api private 110 | */ 111 | const color = (exports.color = function(type, str) { 112 | if (!exports.useColors) { 113 | return String(str); 114 | } 115 | return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m'; 116 | }); 117 | 118 | /** 119 | * Expose term window size, with some defaults for when stderr is not a tty. 120 | */ 121 | 122 | exports.window = { 123 | width: 75 124 | }; 125 | 126 | if (isatty) { 127 | exports.window.width = process.stdout.getWindowSize 128 | ? process.stdout.getWindowSize(1)[0] 129 | : tty.getWindowSize()[1]; 130 | } 131 | 132 | /** 133 | * Expose some basic cursor interactions that are common among reporters. 134 | */ 135 | 136 | exports.cursor = { 137 | hide() { 138 | isatty && process.stdout.write('\u001b[?25l'); 139 | }, 140 | 141 | show() { 142 | isatty && process.stdout.write('\u001b[?25h'); 143 | }, 144 | 145 | deleteLine() { 146 | isatty && process.stdout.write('\u001b[2K'); 147 | }, 148 | 149 | beginningOfLine() { 150 | isatty && process.stdout.write('\u001b[0G'); 151 | }, 152 | 153 | CR() { 154 | if (isatty) { 155 | exports.cursor.deleteLine(); 156 | exports.cursor.beginningOfLine(); 157 | } else { 158 | process.stdout.write('\r'); 159 | } 160 | } 161 | }; 162 | 163 | function showDiff(err) { 164 | return ( 165 | err && 166 | err.showDiff !== false && 167 | sameType(err.actual, err.expected) && 168 | err.expected !== undefined 169 | ); 170 | } 171 | 172 | function stringifyDiffObjs(err) { 173 | if (!utils.isString(err.actual) || !utils.isString(err.expected)) { 174 | err.actual = utils.stringify(err.actual); 175 | err.expected = utils.stringify(err.expected); 176 | } 177 | } 178 | 179 | /** 180 | * Returns a diff between 2 strings with coloured ANSI output. 181 | * 182 | * The diff will be either inline or unified dependant on the value 183 | * of `Base.inlineDiff`. 184 | * 185 | * @param {string} actual 186 | * @param {string} expected 187 | * @return {string} Diff 188 | */ 189 | const generateDiff = (exports.generateDiff = function(actual, expected) { 190 | return exports.inlineDiffs 191 | ? inlineDiff(actual, expected) 192 | : unifiedDiff(actual, expected); 193 | }); 194 | 195 | /** 196 | * Output the given `failures` as a list. 197 | * 198 | * @public 199 | * @memberof Mocha.reporters.Base 200 | * @variation 1 201 | * @param {Array} failures 202 | * @api public 203 | */ 204 | 205 | exports.list = function(failures) { 206 | console.log(); 207 | failures.forEach(function(test, i) { 208 | // format 209 | let fmt = 210 | color('error title', ' %s) %s:\n') + 211 | color('error message', ' %s') + 212 | color('error stack', '\n%s\n'); 213 | 214 | // msg 215 | let msg; 216 | const err = test.err; 217 | let message; 218 | if (err.message && typeof err.message.toString === 'function') { 219 | message = err.message + ''; 220 | } else if (typeof err.inspect === 'function') { 221 | message = err.inspect() + ''; 222 | } else { 223 | message = ''; 224 | } 225 | let stack = err.stack || message; 226 | let index = message ? stack.indexOf(message) : -1; 227 | 228 | if (index === -1) { 229 | msg = message; 230 | } else { 231 | index += message.length; 232 | msg = stack.slice(0, index); 233 | // remove msg from stack 234 | stack = stack.slice(index + 1); 235 | } 236 | 237 | // uncaught 238 | if (err.uncaught) { 239 | msg = 'Uncaught ' + msg; 240 | } 241 | // explicitly show diff 242 | if (!exports.hideDiff && showDiff(err)) { 243 | stringifyDiffObjs(err); 244 | fmt = 245 | color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n'); 246 | const match = message.match(/^([^:]+): expected/); 247 | msg = '\n ' + color('error message', match ? match[1] : msg); 248 | 249 | msg += generateDiff(err.actual, err.expected); 250 | } 251 | 252 | // indent stack trace 253 | stack = stack.replace(/^/gm, ' '); 254 | 255 | // indented test title 256 | let testTitle = ''; 257 | test.titlePath().forEach(function(str, index) { 258 | if (index !== 0) { 259 | testTitle += '\n '; 260 | } 261 | for (let i = 0; i < index; i++) { 262 | testTitle += ' '; 263 | } 264 | testTitle += str; 265 | }); 266 | 267 | console.log(fmt, i + 1, testTitle, msg, stack); 268 | }); 269 | }; 270 | 271 | /** 272 | * Initialize a new `Base` reporter. 273 | * 274 | * All other reporters generally 275 | * inherit from this reporter, providing 276 | * stats such as test duration, number 277 | * of tests passed / failed etc. 278 | * 279 | * @memberof Mocha.reporters 280 | * @public 281 | * @class 282 | * @param {Runner} runner 283 | * @api public 284 | */ 285 | 286 | function Base(runner) { 287 | const stats = (this.stats = { 288 | suites: 0, 289 | tests: 0, 290 | passes: 0, 291 | pending: 0, 292 | failures: 0 293 | }); 294 | const failures = (this.failures = []); 295 | 296 | if (!runner) { 297 | return; 298 | } 299 | this.runner = runner; 300 | 301 | runner.stats = stats; 302 | 303 | runner.on('start', function() { 304 | stats.start = new Date(); 305 | }); 306 | 307 | runner.on('suite', function(suite) { 308 | stats.suites = stats.suites || 0; 309 | suite.root || stats.suites++; 310 | }); 311 | 312 | runner.on('test end', function() { 313 | stats.tests = stats.tests || 0; 314 | stats.tests++; 315 | }); 316 | 317 | runner.on('pass', function(test) { 318 | stats.passes = stats.passes || 0; 319 | 320 | if (test.duration > test.slow()) { 321 | test.speed = 'slow'; 322 | } else if (test.duration > test.slow() / 2) { 323 | test.speed = 'medium'; 324 | } else { 325 | test.speed = 'fast'; 326 | } 327 | 328 | stats.passes++; 329 | }); 330 | 331 | runner.on('fail', function(test, err) { 332 | stats.failures = stats.failures || 0; 333 | stats.failures++; 334 | if (showDiff(err)) { 335 | stringifyDiffObjs(err); 336 | } 337 | test.err = err; 338 | failures.push(test); 339 | }); 340 | 341 | runner.once('end', function() { 342 | stats.end = new Date(); 343 | stats.duration = stats.end - stats.start; 344 | }); 345 | 346 | runner.on('pending', function() { 347 | stats.pending++; 348 | }); 349 | } 350 | 351 | /** 352 | * Output common epilogue used by many of 353 | * the bundled reporters. 354 | * 355 | * @memberof Mocha.reporters.Base 356 | * @public 357 | * @api public 358 | */ 359 | Base.prototype.epilogue = function() { 360 | const stats = this.stats; 361 | let fmt; 362 | 363 | console.log(); 364 | 365 | // passes 366 | fmt = 367 | color('bright pass', ' ') + 368 | color('green', ' %d passing') + 369 | color('light', ' (%s)'); 370 | 371 | console.log(fmt, stats.passes || 0, ms(stats.duration)); 372 | 373 | // pending 374 | if (stats.pending) { 375 | fmt = color('pending', ' ') + color('pending', ' %d pending'); 376 | 377 | console.log(fmt, stats.pending); 378 | } 379 | 380 | // failures 381 | if (stats.failures) { 382 | fmt = color('fail', ' %d failing'); 383 | 384 | console.log(fmt, stats.failures); 385 | 386 | Base.list(this.failures); 387 | console.log(); 388 | } 389 | 390 | console.log(); 391 | }; 392 | 393 | /** 394 | * Pad the given `str` to `len`. 395 | * 396 | * @api private 397 | * @param {string} str 398 | * @param {string} len 399 | * @return {string} 400 | */ 401 | function pad(str, len) { 402 | str = String(str); 403 | return Array(len - str.length + 1).join(' ') + str; 404 | } 405 | 406 | /** 407 | * Returns an inline diff between 2 strings with coloured ANSI output. 408 | * 409 | * @api private 410 | * @param {String} actual 411 | * @param {String} expected 412 | * @return {string} Diff 413 | */ 414 | function inlineDiff(actual, expected) { 415 | let msg = errorDiff(actual, expected); 416 | 417 | // linenos 418 | const lines = msg.split('\n'); 419 | if (lines.length > 4) { 420 | const width = String(lines.length).length; 421 | msg = lines 422 | .map(function(str, i) { 423 | return pad(++i, width) + ' |' + ' ' + str; 424 | }) 425 | .join('\n'); 426 | } 427 | 428 | // legend 429 | msg = [ 430 | '\n', 431 | color('diff removed', 'actual'), 432 | ' ', 433 | color('diff added', 'expected'), 434 | '\n\n', 435 | msg, 436 | '\n'].join(''); 437 | 438 | // indent 439 | msg = msg.replace(/^/gm, ' '); 440 | return msg; 441 | } 442 | 443 | /** 444 | * Returns a unified diff between two strings with coloured ANSI output. 445 | * 446 | * @api private 447 | * @param {String} actual 448 | * @param {String} expected 449 | * @return {string} The diff. 450 | */ 451 | function unifiedDiff(actual, expected) { 452 | const indent = ' '; 453 | function cleanUp(line) { 454 | if (line[0] === '+') { 455 | return indent + colorLines('diff added', line); 456 | } 457 | if (line[0] === '-') { 458 | return indent + colorLines('diff removed', line); 459 | } 460 | if (line.match(/@@/)) { 461 | return '--'; 462 | } 463 | if (line.match(/\\ No newline/)) { 464 | return null; 465 | } 466 | return indent + line; 467 | } 468 | function notBlank(line) { 469 | return typeof line !== 'undefined' && line !== null; 470 | } 471 | const msg = diff.createPatch('string', actual, expected); 472 | const lines = msg.split('\n').splice(5); 473 | return ( 474 | '\n ' + 475 | colorLines('diff added', '+ expected') + 476 | ' ' + 477 | colorLines('diff removed', '- actual') + 478 | '\n\n' + 479 | lines 480 | .map(cleanUp) 481 | .filter(notBlank) 482 | .join('\n') 483 | ); 484 | } 485 | 486 | /** 487 | * Return a character diff for `err`. 488 | * 489 | * @api private 490 | * @param {String} actual 491 | * @param {String} expected 492 | * @return {string} the diff 493 | */ 494 | function errorDiff(actual, expected) { 495 | return diff 496 | .diffWordsWithSpace(actual, expected) 497 | .map(function(str) { 498 | if (str.added) { 499 | return colorLines('diff added', str.value); 500 | } 501 | if (str.removed) { 502 | return colorLines('diff removed', str.value); 503 | } 504 | return str.value; 505 | }) 506 | .join(''); 507 | } 508 | 509 | /** 510 | * Color lines for `str`, using the color `name`. 511 | * 512 | * @api private 513 | * @param {string} name 514 | * @param {string} str 515 | * @return {string} 516 | */ 517 | function colorLines(name, str) { 518 | return str 519 | .split('\n') 520 | .map(function(str) { 521 | return color(name, str); 522 | }) 523 | .join('\n'); 524 | } 525 | 526 | /** 527 | * Object#toString reference. 528 | */ 529 | const objToString = Object.prototype.toString; 530 | 531 | /** 532 | * Check that a / b have the same type. 533 | * 534 | * @api private 535 | * @param {Object} a 536 | * @param {Object} b 537 | * @return {boolean} 538 | */ 539 | function sameType(a, b) { 540 | return objToString.call(a) === objToString.call(b); 541 | } 542 | -------------------------------------------------------------------------------- /lib/mocha/reporters/doc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module Doc 4 | */ 5 | /** 6 | * Module dependencies. 7 | */ 8 | 9 | const Base = require('./base'); 10 | const utils = require('../utils'); 11 | 12 | /** 13 | * Expose `Doc`. 14 | */ 15 | 16 | exports = module.exports = Doc; 17 | 18 | /** 19 | * Initialize a new `Doc` reporter. 20 | * 21 | * @class 22 | * @memberof Mocha.reporters 23 | * @augments {Base} 24 | * @public 25 | * @param {Runner} runner 26 | * @api public 27 | */ 28 | function Doc(runner) { 29 | Base.call(this, runner); 30 | 31 | let indents = 2; 32 | 33 | function indent() { 34 | return Array(indents).join(' '); 35 | } 36 | 37 | runner.on('suite', function(suite) { 38 | if (suite.root) { 39 | return; 40 | } 41 | ++indents; 42 | console.log('%s
', indent()); 43 | ++indents; 44 | console.log('%s

%s

', indent(), utils.escape(suite.title)); 45 | console.log('%s
', indent()); 46 | }); 47 | 48 | runner.on('suite end', function(suite) { 49 | if (suite.root) { 50 | return; 51 | } 52 | console.log('%s
', indent()); 53 | --indents; 54 | console.log('%s
', indent()); 55 | --indents; 56 | }); 57 | 58 | runner.on('pass', function(test) { 59 | console.log('%s
%s
', indent(), utils.escape(test.title)); 60 | const code = utils.escape(utils.clean(test.body)); 61 | console.log('%s
%s
', indent(), code); 62 | }); 63 | 64 | runner.on('fail', function(test, err) { 65 | console.log( 66 | '%s
%s
', 67 | indent(), 68 | utils.escape(test.title) 69 | ); 70 | const code = utils.escape(utils.clean(test.body)); 71 | console.log( 72 | '%s
%s
', 73 | indent(), 74 | code 75 | ); 76 | console.log('%s
%s
', indent(), utils.escape(err)); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /lib/mocha/reporters/dot.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | 'use strict'; 3 | /** 4 | * @module Dot 5 | */ 6 | /** 7 | * Module dependencies. 8 | */ 9 | 10 | const Base = require('./base'); 11 | const inherits = require('../utils').inherits; 12 | const color = Base.color; 13 | 14 | /** 15 | * Expose `Dot`. 16 | */ 17 | 18 | exports = module.exports = Dot; 19 | 20 | /** 21 | * Initialize a new `Dot` matrix test reporter. 22 | * 23 | * @class 24 | * @memberof Mocha.reporters 25 | * @augments Mocha.reporters.Base 26 | * @public 27 | * @api public 28 | * @param {Runner} runner 29 | */ 30 | function Dot(runner) { 31 | Base.call(this, runner); 32 | 33 | const self = this; 34 | const width = (Base.window.width * 0.75) | 0; 35 | let n = -1; 36 | 37 | runner.on('start', function() { 38 | process.stdout.write('\n'); 39 | }); 40 | 41 | runner.on('pending', function() { 42 | if (++n % width === 0) { 43 | process.stdout.write('\n '); 44 | } 45 | process.stdout.write(color('pending', Base.symbols.comma)); 46 | }); 47 | 48 | runner.on('pass', function(test) { 49 | if (++n % width === 0) { 50 | process.stdout.write('\n '); 51 | } 52 | if (test.speed === 'slow') { 53 | process.stdout.write(color('bright yellow', Base.symbols.dot)); 54 | } else { 55 | process.stdout.write(color(test.speed, Base.symbols.dot)); 56 | } 57 | }); 58 | 59 | runner.on('fail', function() { 60 | if (++n % width === 0) { 61 | process.stdout.write('\n '); 62 | } 63 | process.stdout.write(color('fail', Base.symbols.bang)); 64 | }); 65 | 66 | runner.once('end', function() { 67 | console.log(); 68 | self.epilogue(); 69 | }); 70 | } 71 | 72 | /** 73 | * Inherit from `Base.prototype`. 74 | */ 75 | inherits(Dot, Base); 76 | -------------------------------------------------------------------------------- /lib/mocha/reporters/html.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | 'use strict'; 3 | 4 | /* eslint-env browser */ 5 | /** 6 | * @module HTML 7 | */ 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | const Base = require('./base'); 13 | const utils = require('../utils'); 14 | const Progress = require('../browser/progress'); 15 | const escapeRe = require('escape-string-regexp'); 16 | const escape = utils.escape; 17 | 18 | /** 19 | * Save timer references to avoid Sinon interfering (see GH-237). 20 | */ 21 | 22 | /* eslint-disable no-unused-vars, no-native-reassign */ 23 | const Date = global.Date; 24 | const setTimeout = global.setTimeout; 25 | const setInterval = global.setInterval; 26 | const clearTimeout = global.clearTimeout; 27 | const clearInterval = global.clearInterval; 28 | /* eslint-enable no-unused-vars, no-native-reassign */ 29 | 30 | /** 31 | * Expose `HTML`. 32 | */ 33 | 34 | exports = module.exports = HTML; 35 | 36 | /** 37 | * Stats template. 38 | */ 39 | 40 | const statsTemplate = 41 | ''; 47 | 48 | const playIcon = '‣'; 49 | 50 | /** 51 | * Initialize a new `HTML` reporter. 52 | * 53 | * @public 54 | * @class 55 | * @memberof Mocha.reporters 56 | * @augments Mocha.reporters.Base 57 | * @api public 58 | * @param {Runner} runner 59 | */ 60 | function HTML(runner) { 61 | Base.call(this, runner); 62 | 63 | const self = this; 64 | const stats = this.stats; 65 | const stat = fragment(statsTemplate); 66 | const items = stat.getElementsByTagName('li'); 67 | const passes = items[1].getElementsByTagName('em')[0]; 68 | const passesLink = items[1].getElementsByTagName('a')[0]; 69 | const failures = items[2].getElementsByTagName('em')[0]; 70 | const failuresLink = items[2].getElementsByTagName('a')[0]; 71 | const duration = items[3].getElementsByTagName('em')[0]; 72 | const canvas = stat.getElementsByTagName('canvas')[0]; 73 | const report = fragment(''); 74 | const stack = [report]; 75 | let progress; 76 | let ctx; 77 | const root = document.getElementById('mocha'); 78 | 79 | if (canvas.getContext) { 80 | const ratio = window.devicePixelRatio || 1; 81 | canvas.style.width = canvas.width; 82 | canvas.style.height = canvas.height; 83 | canvas.width *= ratio; 84 | canvas.height *= ratio; 85 | ctx = canvas.getContext('2d'); 86 | ctx.scale(ratio, ratio); 87 | progress = new Progress(); 88 | } 89 | 90 | if (!root) { 91 | return error('#mocha div missing, add it to your document'); 92 | } 93 | 94 | // pass toggle 95 | on(passesLink, 'click', function(evt) { 96 | evt.preventDefault(); 97 | unhide(); 98 | const name = /pass/.test(report.className) ? '' : ' pass'; 99 | report.className = report.className.replace(/fail|pass/g, '') + name; 100 | if (report.className.trim()) { 101 | hideSuitesWithout('test pass'); 102 | } 103 | }); 104 | 105 | // failure toggle 106 | on(failuresLink, 'click', function(evt) { 107 | evt.preventDefault(); 108 | unhide(); 109 | const name = /fail/.test(report.className) ? '' : ' fail'; 110 | report.className = report.className.replace(/fail|pass/g, '') + name; 111 | if (report.className.trim()) { 112 | hideSuitesWithout('test fail'); 113 | } 114 | }); 115 | 116 | root.appendChild(stat); 117 | root.appendChild(report); 118 | 119 | if (progress) { 120 | progress.size(40); 121 | } 122 | 123 | runner.on('suite', function(suite) { 124 | if (suite.root) { 125 | return; 126 | } 127 | 128 | // suite 129 | const url = self.suiteURL(suite); 130 | const el = fragment( 131 | '
  • %s

  • ', 132 | url, 133 | escape(suite.title) 134 | ); 135 | 136 | // container 137 | stack[0].appendChild(el); 138 | stack.unshift(document.createElement('ul')); 139 | el.appendChild(stack[0]); 140 | }); 141 | 142 | runner.on('suite end', function(suite) { 143 | if (suite.root) { 144 | updateStats(); 145 | return; 146 | } 147 | stack.shift(); 148 | }); 149 | 150 | runner.on('pass', function(test) { 151 | const url = self.testURL(test); 152 | const markup = 153 | '
  • %e%ems ' + 154 | '' + 155 | playIcon + 156 | '

  • '; 157 | const el = fragment(markup, test.speed, test.title, test.duration, url); 158 | self.addCodeToggle(el, test.body); 159 | appendToStack(el); 160 | updateStats(); 161 | }); 162 | 163 | runner.on('fail', function(test) { 164 | const el = fragment( 165 | '
  • %e ' + 166 | playIcon + 167 | '

  • ', 168 | test.title, 169 | self.testURL(test) 170 | ); 171 | let stackString; // Note: Includes leading newline 172 | let message = test.err.toString(); 173 | 174 | // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we 175 | // check for the result of the stringifying. 176 | if (message === '[object Error]') { 177 | message = test.err.message; 178 | } 179 | 180 | if (test.err.stack) { 181 | const indexOfMessage = test.err.stack.indexOf(test.err.message); 182 | if (indexOfMessage === -1) { 183 | stackString = test.err.stack; 184 | } else { 185 | stackString = test.err.stack.substr( 186 | test.err.message.length + indexOfMessage 187 | ); 188 | } 189 | } else if (test.err.sourceURL && test.err.line !== undefined) { 190 | // Safari doesn't give you a stack. Let's at least provide a source line. 191 | stackString = '\n(' + test.err.sourceURL + ':' + test.err.line + ')'; 192 | } 193 | 194 | stackString = stackString || ''; 195 | 196 | if (test.err.htmlMessage && stackString) { 197 | el.appendChild( 198 | fragment( 199 | '
    %s\n
    %e
    ', 200 | test.err.htmlMessage, 201 | stackString 202 | ) 203 | ); 204 | } else if (test.err.htmlMessage) { 205 | el.appendChild( 206 | fragment('
    %s
    ', test.err.htmlMessage) 207 | ); 208 | } else { 209 | el.appendChild( 210 | fragment('
    %e%e
    ', message, stackString) 211 | ); 212 | } 213 | 214 | self.addCodeToggle(el, test.body); 215 | appendToStack(el); 216 | updateStats(); 217 | }); 218 | 219 | runner.on('pending', function(test) { 220 | const el = fragment( 221 | '
  • %e

  • ', 222 | test.title 223 | ); 224 | appendToStack(el); 225 | updateStats(); 226 | }); 227 | 228 | function appendToStack(el) { 229 | // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack. 230 | if (stack[0]) { 231 | stack[0].appendChild(el); 232 | } 233 | } 234 | 235 | function updateStats() { 236 | // TODO: add to stats 237 | const percent = (stats.tests / runner.total * 100) | 0; 238 | if (progress) { 239 | progress.update(percent).draw(ctx); 240 | } 241 | 242 | // update stats 243 | const ms = new Date() - stats.start; 244 | text(passes, stats.passes); 245 | text(failures, stats.failures); 246 | text(duration, (ms / 1000).toFixed(2)); 247 | } 248 | } 249 | 250 | /** 251 | * Makes a URL, preserving querystring ("search") parameters. 252 | * 253 | * @param {string} s 254 | * @return {string} A new URL. 255 | */ 256 | function makeUrl(s) { 257 | let search = window.location.search; 258 | 259 | // Remove previous grep query parameter if present 260 | if (search) { 261 | search = search.replace(/[?&]grep=[^&\s]*/g, '').replace(/^&/, '?'); 262 | } 263 | 264 | return ( 265 | window.location.pathname + 266 | (search ? search + '&' : '?') + 267 | 'grep=' + 268 | encodeURIComponent(escapeRe(s)) 269 | ); 270 | } 271 | 272 | /** 273 | * Provide suite URL. 274 | * 275 | * @param {Object} [suite] 276 | */ 277 | HTML.prototype.suiteURL = function(suite) { 278 | return makeUrl(suite.fullTitle()); 279 | }; 280 | 281 | /** 282 | * Provide test URL. 283 | * 284 | * @param {Object} [test] 285 | */ 286 | HTML.prototype.testURL = function(test) { 287 | return makeUrl(test.fullTitle()); 288 | }; 289 | 290 | /** 291 | * Adds code toggle functionality for the provided test's list element. 292 | * 293 | * @param {HTMLLIElement} el 294 | * @param {string} contents 295 | */ 296 | HTML.prototype.addCodeToggle = function(el, contents) { 297 | const h2 = el.getElementsByTagName('h2')[0]; 298 | let pre; 299 | 300 | on(h2, 'click', function() { 301 | pre.style.display = pre.style.display === 'none' ? 'block' : 'none'; 302 | }); 303 | 304 | pre = fragment('
    %e
    ', utils.clean(contents)); 305 | el.appendChild(pre); 306 | pre.style.display = 'none'; 307 | }; 308 | 309 | /** 310 | * Display error `msg`. 311 | * 312 | * @param {string} msg 313 | */ 314 | function error(msg) { 315 | document.body.appendChild(fragment('
    %s
    ', msg)); 316 | } 317 | 318 | /** 319 | * Return a DOM fragment from `html`. 320 | * 321 | * @param {string} html 322 | */ 323 | function fragment(html) { 324 | const args = arguments; 325 | const div = document.createElement('div'); 326 | let i = 1; 327 | 328 | div.innerHTML = html.replace(/%([se])/g, function(_, type) { 329 | switch (type) { 330 | case 's': 331 | return String(args[i++]); 332 | case 'e': 333 | return escape(args[i++]); 334 | // no default 335 | } 336 | }); 337 | 338 | return div.firstChild; 339 | } 340 | 341 | /** 342 | * Check for suites that do not have elements 343 | * with `classname`, and hide them. 344 | * 345 | * @param {text} classname 346 | */ 347 | function hideSuitesWithout(classname) { 348 | const suites = document.getElementsByClassName('suite'); 349 | for (let i = 0; i < suites.length; i++) { 350 | const els = suites[i].getElementsByClassName(classname); 351 | if (!els.length) { 352 | suites[i].className += ' hidden'; 353 | } 354 | } 355 | } 356 | 357 | /** 358 | * Unhide .hidden suites. 359 | */ 360 | function unhide() { 361 | const els = document.getElementsByClassName('suite hidden'); 362 | for (let i = 0; i < els.length; ++i) { 363 | els[i].className = els[i].className.replace('suite hidden', 'suite'); 364 | } 365 | } 366 | 367 | /** 368 | * Set an element's text contents. 369 | * 370 | * @param {HTMLElement} el 371 | * @param {string} contents 372 | */ 373 | function text(el, contents) { 374 | if (el.textContent) { 375 | el.textContent = contents; 376 | } else { 377 | el.innerText = contents; 378 | } 379 | } 380 | 381 | /** 382 | * Listen on `event` with callback `fn`. 383 | */ 384 | function on(el, event, fn) { 385 | if (el.addEventListener) { 386 | el.addEventListener(event, fn, false); 387 | } else { 388 | el.attachEvent('on' + event, fn); 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /lib/mocha/reporters/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Alias exports to a their normalized format Mocha#reporter to prevent a need 4 | // for dynamic (try/catch) requires, which Browserify doesn't handle. 5 | exports.Base = exports.base = require('./base'); 6 | exports.Dot = exports.dot = require('./dot'); 7 | exports.Doc = exports.doc = require('./doc'); 8 | exports.TAP = exports.tap = require('./tap'); 9 | exports.JSON = exports.json = require('./json'); 10 | exports.HTML = exports.html = require('./html'); 11 | exports.List = exports.list = require('./list'); 12 | exports.Min = exports.min = require('./min'); 13 | exports.Spec = exports.spec = require('./spec'); 14 | exports.Nyan = exports.nyan = require('./nyan'); 15 | exports.XUnit = exports.xunit = require('./xunit'); 16 | exports.Markdown = exports.markdown = require('./markdown'); 17 | exports.Progress = exports.progress = require('./progress'); 18 | exports.Landing = exports.landing = require('./landing'); 19 | exports.JSONStream = exports['json-stream'] = require('./json-stream'); 20 | -------------------------------------------------------------------------------- /lib/mocha/reporters/json-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module JSONStream 4 | */ 5 | /** 6 | * Module dependencies. 7 | */ 8 | 9 | const Base = require('./base'); 10 | 11 | /** 12 | * Expose `List`. 13 | */ 14 | 15 | exports = module.exports = List; 16 | 17 | /** 18 | * Initialize a new `JSONStream` test reporter. 19 | * 20 | * @public 21 | * @name JSONStream 22 | * @class JSONStream 23 | * @memberof Mocha.reporters 24 | * @augments Mocha.reporters.Base 25 | * @api public 26 | * @param {Runner} runner 27 | */ 28 | function List(runner) { 29 | Base.call(this, runner); 30 | 31 | const self = this; 32 | const total = runner.total; 33 | 34 | runner.on('start', function() { 35 | console.log(JSON.stringify(['start', {total}])); 36 | }); 37 | 38 | runner.on('pass', function(test) { 39 | console.log(JSON.stringify(['pass', clean(test)])); 40 | }); 41 | 42 | runner.on('fail', function(test, err) { 43 | test = clean(test); 44 | test.err = err.message; 45 | test.stack = err.stack || null; 46 | console.log(JSON.stringify(['fail', test])); 47 | }); 48 | 49 | runner.once('end', function() { 50 | process.stdout.write(JSON.stringify(['end', self.stats])); 51 | }); 52 | } 53 | 54 | /** 55 | * Return a plain-object representation of `test` 56 | * free of cyclic properties etc. 57 | * 58 | * @api private 59 | * @param {Object} test 60 | * @return {Object} 61 | */ 62 | function clean(test) { 63 | return { 64 | title: test.title, 65 | fullTitle: test.fullTitle(), 66 | duration: test.duration, 67 | currentRetry: test.currentRetry() 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /lib/mocha/reporters/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module JSON 4 | */ 5 | /** 6 | * Module dependencies. 7 | */ 8 | 9 | const Base = require('./base'); 10 | 11 | /** 12 | * Expose `JSON`. 13 | */ 14 | 15 | exports = module.exports = JSONReporter; 16 | 17 | /** 18 | * Initialize a new `JSON` reporter. 19 | * 20 | * @public 21 | * @class JSON 22 | * @memberof Mocha.reporters 23 | * @augments Mocha.reporters.Base 24 | * @api public 25 | * @param {Runner} runner 26 | */ 27 | function JSONReporter(runner) { 28 | Base.call(this, runner); 29 | 30 | const self = this; 31 | const tests = []; 32 | const pending = []; 33 | const failures = []; 34 | const passes = []; 35 | 36 | runner.on('test end', function(test) { 37 | tests.push(test); 38 | }); 39 | 40 | runner.on('pass', function(test) { 41 | passes.push(test); 42 | }); 43 | 44 | runner.on('fail', function(test) { 45 | failures.push(test); 46 | }); 47 | 48 | runner.on('pending', function(test) { 49 | pending.push(test); 50 | }); 51 | 52 | runner.once('end', function() { 53 | const obj = { 54 | stats: self.stats, 55 | tests: tests.map(clean), 56 | pending: pending.map(clean), 57 | failures: failures.map(clean), 58 | passes: passes.map(clean) 59 | }; 60 | 61 | runner.testResults = obj; 62 | 63 | process.stdout.write(JSON.stringify(obj, null, 2)); 64 | }); 65 | } 66 | 67 | /** 68 | * Return a plain-object representation of `test` 69 | * free of cyclic properties etc. 70 | * 71 | * @api private 72 | * @param {Object} test 73 | * @return {Object} 74 | */ 75 | function clean(test) { 76 | let err = test.err || {}; 77 | if (err instanceof Error) { 78 | err = errorJSON(err); 79 | } 80 | 81 | return { 82 | title: test.title, 83 | fullTitle: test.fullTitle(), 84 | duration: test.duration, 85 | currentRetry: test.currentRetry(), 86 | err: cleanCycles(err) 87 | }; 88 | } 89 | 90 | /** 91 | * Replaces any circular references inside `obj` with '[object Object]' 92 | * 93 | * @api private 94 | * @param {Object} obj 95 | * @return {Object} 96 | */ 97 | function cleanCycles(obj) { 98 | const cache = []; 99 | return JSON.parse( 100 | JSON.stringify(obj, function(key, value) { 101 | if (typeof value === 'object' && value !== null) { 102 | if (cache.indexOf(value) !== -1) { 103 | // Instead of going in a circle, we'll print [object Object] 104 | return '' + value; 105 | } 106 | cache.push(value); 107 | } 108 | 109 | return value; 110 | }) 111 | ); 112 | } 113 | 114 | /** 115 | * Transform an Error object into a JSON object. 116 | * 117 | * @api private 118 | * @param {Error} err 119 | * @return {Object} 120 | */ 121 | function errorJSON(err) { 122 | const res = {}; 123 | Object.getOwnPropertyNames(err).forEach(function(key) { 124 | res[key] = err[key]; 125 | }, err); 126 | return res; 127 | } 128 | -------------------------------------------------------------------------------- /lib/mocha/reporters/landing.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | 'use strict'; 3 | /** 4 | * @module Landing 5 | */ 6 | /** 7 | * Module dependencies. 8 | */ 9 | 10 | const Base = require('./base'); 11 | const inherits = require('../utils').inherits; 12 | const cursor = Base.cursor; 13 | const color = Base.color; 14 | 15 | /** 16 | * Expose `Landing`. 17 | */ 18 | 19 | exports = module.exports = Landing; 20 | 21 | /** 22 | * Airplane color. 23 | */ 24 | 25 | Base.colors.plane = 0; 26 | 27 | /** 28 | * Airplane crash color. 29 | */ 30 | 31 | Base.colors['plane crash'] = 31; 32 | 33 | /** 34 | * Runway color. 35 | */ 36 | 37 | Base.colors.runway = 90; 38 | 39 | /** 40 | * Initialize a new `Landing` reporter. 41 | * 42 | * @public 43 | * @class 44 | * @memberof Mocha.reporters 45 | * @augments Mocha.reporters.Base 46 | * @api public 47 | * @param {Runner} runner 48 | */ 49 | function Landing(runner) { 50 | Base.call(this, runner); 51 | 52 | const self = this; 53 | const width = (Base.window.width * 0.75) | 0; 54 | const total = runner.total; 55 | const stream = process.stdout; 56 | let plane = color('plane', '✈'); 57 | let crashed = -1; 58 | let n = 0; 59 | 60 | function runway() { 61 | const buf = Array(width).join('-'); 62 | return ' ' + color('runway', buf); 63 | } 64 | 65 | runner.on('start', function() { 66 | stream.write('\n\n\n '); 67 | cursor.hide(); 68 | }); 69 | 70 | runner.on('test end', function(test) { 71 | // check if the plane crashed 72 | const col = crashed === -1 ? (width * ++n / total) | 0 : crashed; 73 | 74 | // show the crash 75 | if (test.state === 'failed') { 76 | plane = color('plane crash', '✈'); 77 | crashed = col; 78 | } 79 | 80 | // render landing strip 81 | stream.write('\u001b[' + (width + 1) + 'D\u001b[2A'); 82 | stream.write(runway()); 83 | stream.write('\n '); 84 | stream.write(color('runway', Array(col).join('⋅'))); 85 | stream.write(plane); 86 | stream.write(color('runway', Array(width - col).join('⋅') + '\n')); 87 | stream.write(runway()); 88 | stream.write('\u001b[0m'); 89 | }); 90 | 91 | runner.once('end', function() { 92 | cursor.show(); 93 | console.log(); 94 | self.epilogue(); 95 | }); 96 | } 97 | 98 | /** 99 | * Inherit from `Base.prototype`. 100 | */ 101 | inherits(Landing, Base); 102 | -------------------------------------------------------------------------------- /lib/mocha/reporters/list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module List 4 | */ 5 | /** 6 | * Module dependencies. 7 | */ 8 | 9 | const Base = require('./base'); 10 | const inherits = require('../utils').inherits; 11 | const color = Base.color; 12 | const cursor = Base.cursor; 13 | 14 | /** 15 | * Expose `List`. 16 | */ 17 | 18 | exports = module.exports = List; 19 | 20 | /** 21 | * Initialize a new `List` test reporter. 22 | * 23 | * @public 24 | * @class 25 | * @memberof Mocha.reporters 26 | * @augments Mocha.reporters.Base 27 | * @api public 28 | * @param {Runner} runner 29 | */ 30 | function List(runner) { 31 | Base.call(this, runner); 32 | 33 | const self = this; 34 | let n = 0; 35 | 36 | runner.on('start', function() { 37 | console.log(); 38 | }); 39 | 40 | runner.on('test', function(test) { 41 | process.stdout.write(color('pass', ' ' + test.fullTitle() + ': ')); 42 | }); 43 | 44 | runner.on('pending', function(test) { 45 | const fmt = color('checkmark', ' -') + color('pending', ' %s'); 46 | console.log(fmt, test.fullTitle()); 47 | }); 48 | 49 | runner.on('pass', function(test) { 50 | const fmt = 51 | color('checkmark', ' ' + Base.symbols.ok) + 52 | color('pass', ' %s: ') + 53 | color(test.speed, '%dms'); 54 | cursor.CR(); 55 | console.log(fmt, test.fullTitle(), test.duration); 56 | }); 57 | 58 | runner.on('fail', function(test) { 59 | cursor.CR(); 60 | console.log(color('fail', ' %d) %s'), ++n, test.fullTitle()); 61 | }); 62 | 63 | runner.once('end', self.epilogue.bind(self)); 64 | } 65 | 66 | /** 67 | * Inherit from `Base.prototype`. 68 | */ 69 | inherits(List, Base); 70 | -------------------------------------------------------------------------------- /lib/mocha/reporters/markdown.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module Markdown 4 | */ 5 | /** 6 | * Module dependencies. 7 | */ 8 | 9 | const Base = require('./base'); 10 | const utils = require('../utils'); 11 | 12 | /** 13 | * Constants 14 | */ 15 | 16 | const SUITE_PREFIX = '$'; 17 | 18 | /** 19 | * Expose `Markdown`. 20 | */ 21 | 22 | exports = module.exports = Markdown; 23 | 24 | /** 25 | * Initialize a new `Markdown` reporter. 26 | * 27 | * @public 28 | * @class 29 | * @memberof Mocha.reporters 30 | * @augments Mocha.reporters.Base 31 | * @api public 32 | * @param {Runner} runner 33 | */ 34 | function Markdown(runner) { 35 | Base.call(this, runner); 36 | 37 | let level = 0; 38 | let buf = ''; 39 | 40 | function title(str) { 41 | return Array(level).join('#') + ' ' + str; 42 | } 43 | 44 | function mapTOC(suite, obj) { 45 | const ret = obj; 46 | const key = SUITE_PREFIX + suite.title; 47 | 48 | obj = obj[key] = obj[key] || {suite}; 49 | suite.suites.forEach(function(suite) { 50 | mapTOC(suite, obj); 51 | }); 52 | 53 | return ret; 54 | } 55 | 56 | function stringifyTOC(obj, level) { 57 | ++level; 58 | let buf = ''; 59 | let link; 60 | for (const key in obj) { 61 | if (key === 'suite') { 62 | continue; 63 | } 64 | if (key !== SUITE_PREFIX) { 65 | link = ' - [' + key.substring(1) + ']'; 66 | link += '(#' + utils.slug(obj[key].suite.fullTitle()) + ')\n'; 67 | buf += Array(level).join(' ') + link; 68 | } 69 | buf += stringifyTOC(obj[key], level); 70 | } 71 | return buf; 72 | } 73 | 74 | function generateTOC(suite) { 75 | const obj = mapTOC(suite, {}); 76 | return stringifyTOC(obj, 0); 77 | } 78 | 79 | generateTOC(runner.suite); 80 | 81 | runner.on('suite', function(suite) { 82 | ++level; 83 | const slug = utils.slug(suite.fullTitle()); 84 | // eslint-disable-next-line no-useless-concat 85 | buf += '' + '\n'; 86 | buf += title(suite.title) + '\n'; 87 | }); 88 | 89 | runner.on('suite end', function() { 90 | --level; 91 | }); 92 | 93 | runner.on('pass', function(test) { 94 | const code = utils.clean(test.body); 95 | buf += test.title + '.\n'; 96 | buf += '\n```js\n'; 97 | buf += code + '\n'; 98 | buf += '```\n\n'; 99 | }); 100 | 101 | runner.once('end', function() { 102 | process.stdout.write('# TOC\n'); 103 | process.stdout.write(generateTOC(runner.suite)); 104 | process.stdout.write(buf); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /lib/mocha/reporters/min.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module Min 4 | */ 5 | /** 6 | * Module dependencies. 7 | */ 8 | 9 | const Base = require('./base'); 10 | const inherits = require('../utils').inherits; 11 | 12 | /** 13 | * Expose `Min`. 14 | */ 15 | 16 | exports = module.exports = Min; 17 | 18 | /** 19 | * Initialize a new `Min` minimal test reporter (best used with --watch). 20 | * 21 | * @public 22 | * @class 23 | * @memberof Mocha.reporters 24 | * @augments Mocha.reporters.Base 25 | * @api public 26 | * @param {Runner} runner 27 | */ 28 | function Min(runner) { 29 | Base.call(this, runner); 30 | 31 | runner.on('start', function() { 32 | // clear screen 33 | process.stdout.write('\u001b[2J'); 34 | // set cursor position 35 | process.stdout.write('\u001b[1;3H'); 36 | }); 37 | 38 | runner.once('end', this.epilogue.bind(this)); 39 | } 40 | 41 | /** 42 | * Inherit from `Base.prototype`. 43 | */ 44 | inherits(Min, Base); 45 | -------------------------------------------------------------------------------- /lib/mocha/reporters/nyan.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | 'use strict'; 3 | /** 4 | * @module Nyan 5 | */ 6 | /** 7 | * Module dependencies. 8 | */ 9 | 10 | const Base = require('./base'); 11 | const inherits = require('../utils').inherits; 12 | 13 | /** 14 | * Expose `Dot`. 15 | */ 16 | 17 | exports = module.exports = NyanCat; 18 | 19 | /** 20 | * Initialize a new `Dot` matrix test reporter. 21 | * 22 | * @param {Runner} runner 23 | * @api public 24 | * @public 25 | * @class Nyan 26 | * @memberof Mocha.reporters 27 | * @extends Mocha.reporters.Base 28 | */ 29 | 30 | function NyanCat(runner) { 31 | Base.call(this, runner); 32 | 33 | const self = this; 34 | const width = (Base.window.width * 0.75) | 0; 35 | const nyanCatWidth = (this.nyanCatWidth = 11); 36 | 37 | this.colorIndex = 0; 38 | this.numberOfLines = 4; 39 | this.rainbowColors = self.generateColors(); 40 | this.scoreboardWidth = 5; 41 | this.tick = 0; 42 | this.trajectories = [[], [], [], []]; 43 | this.trajectoryWidthMax = width - nyanCatWidth; 44 | 45 | runner.on('start', function() { 46 | Base.cursor.hide(); 47 | self.draw(); 48 | }); 49 | 50 | runner.on('pending', function() { 51 | self.draw(); 52 | }); 53 | 54 | runner.on('pass', function() { 55 | self.draw(); 56 | }); 57 | 58 | runner.on('fail', function() { 59 | self.draw(); 60 | }); 61 | 62 | runner.once('end', function() { 63 | Base.cursor.show(); 64 | for (let i = 0; i < self.numberOfLines; i++) { 65 | write('\n'); 66 | } 67 | self.epilogue(); 68 | }); 69 | } 70 | 71 | /** 72 | * Inherit from `Base.prototype`. 73 | */ 74 | inherits(NyanCat, Base); 75 | 76 | /** 77 | * Draw the nyan cat 78 | * 79 | * @api private 80 | */ 81 | 82 | NyanCat.prototype.draw = function() { 83 | this.appendRainbow(); 84 | this.drawScoreboard(); 85 | this.drawRainbow(); 86 | this.drawNyanCat(); 87 | this.tick = !this.tick; 88 | }; 89 | 90 | /** 91 | * Draw the "scoreboard" showing the number 92 | * of passes, failures and pending tests. 93 | * 94 | * @api private 95 | */ 96 | 97 | NyanCat.prototype.drawScoreboard = function() { 98 | const stats = this.stats; 99 | 100 | function draw(type, n) { 101 | write(' '); 102 | write(Base.color(type, n)); 103 | write('\n'); 104 | } 105 | 106 | draw('green', stats.passes); 107 | draw('fail', stats.failures); 108 | draw('pending', stats.pending); 109 | write('\n'); 110 | 111 | this.cursorUp(this.numberOfLines); 112 | }; 113 | 114 | /** 115 | * Append the rainbow. 116 | * 117 | * @api private 118 | */ 119 | 120 | NyanCat.prototype.appendRainbow = function() { 121 | const segment = this.tick ? '_' : '-'; 122 | const rainbowified = this.rainbowify(segment); 123 | 124 | for (let index = 0; index < this.numberOfLines; index++) { 125 | const trajectory = this.trajectories[index]; 126 | if (trajectory.length >= this.trajectoryWidthMax) { 127 | trajectory.shift(); 128 | } 129 | trajectory.push(rainbowified); 130 | } 131 | }; 132 | 133 | /** 134 | * Draw the rainbow. 135 | * 136 | * @api private 137 | */ 138 | 139 | NyanCat.prototype.drawRainbow = function() { 140 | const self = this; 141 | 142 | this.trajectories.forEach(function(line) { 143 | write('\u001b[' + self.scoreboardWidth + 'C'); 144 | write(line.join('')); 145 | write('\n'); 146 | }); 147 | 148 | this.cursorUp(this.numberOfLines); 149 | }; 150 | 151 | /** 152 | * Draw the nyan cat 153 | * 154 | * @api private 155 | */ 156 | NyanCat.prototype.drawNyanCat = function() { 157 | const self = this; 158 | const startWidth = this.scoreboardWidth + this.trajectories[0].length; 159 | const dist = '\u001b[' + startWidth + 'C'; 160 | let padding = ''; 161 | 162 | write(dist); 163 | write('_,------,'); 164 | write('\n'); 165 | 166 | write(dist); 167 | padding = self.tick ? ' ' : ' '; 168 | write('_|' + padding + '/\\_/\\ '); 169 | write('\n'); 170 | 171 | write(dist); 172 | padding = self.tick ? '_' : '__'; 173 | const tail = self.tick ? '~' : '^'; 174 | write(tail + '|' + padding + this.face() + ' '); 175 | write('\n'); 176 | 177 | write(dist); 178 | padding = self.tick ? ' ' : ' '; 179 | write(padding + '"" "" '); 180 | write('\n'); 181 | 182 | this.cursorUp(this.numberOfLines); 183 | }; 184 | 185 | /** 186 | * Draw nyan cat face. 187 | * 188 | * @api private 189 | * @return {string} 190 | */ 191 | 192 | NyanCat.prototype.face = function() { 193 | const stats = this.stats; 194 | if (stats.failures) { 195 | return '( x .x)'; 196 | } else if (stats.pending) { 197 | return '( o .o)'; 198 | } else if (stats.passes) { 199 | return '( ^ .^)'; 200 | } 201 | return '( - .-)'; 202 | }; 203 | 204 | /** 205 | * Move cursor up `n`. 206 | * 207 | * @api private 208 | * @param {number} n 209 | */ 210 | 211 | NyanCat.prototype.cursorUp = function(n) { 212 | write('\u001b[' + n + 'A'); 213 | }; 214 | 215 | /** 216 | * Move cursor down `n`. 217 | * 218 | * @api private 219 | * @param {number} n 220 | */ 221 | 222 | NyanCat.prototype.cursorDown = function(n) { 223 | write('\u001b[' + n + 'B'); 224 | }; 225 | 226 | /** 227 | * Generate rainbow colors. 228 | * 229 | * @api private 230 | * @return {Array} 231 | */ 232 | NyanCat.prototype.generateColors = function() { 233 | const colors = []; 234 | 235 | for (let i = 0; i < 6 * 7; i++) { 236 | const pi3 = Math.floor(Math.PI / 3); 237 | const n = i * (1.0 / 6); 238 | const r = Math.floor(3 * Math.sin(n) + 3); 239 | const g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3); 240 | const b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3); 241 | colors.push(36 * r + 6 * g + b + 16); 242 | } 243 | 244 | return colors; 245 | }; 246 | 247 | /** 248 | * Apply rainbow to the given `str`. 249 | * 250 | * @api private 251 | * @param {string} str 252 | * @return {string} 253 | */ 254 | NyanCat.prototype.rainbowify = function(str) { 255 | if (!Base.useColors) { 256 | return str; 257 | } 258 | const color = this.rainbowColors[this.colorIndex % this.rainbowColors.length]; 259 | this.colorIndex += 1; 260 | return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m'; 261 | }; 262 | 263 | /** 264 | * Stdout helper. 265 | * 266 | * @param {string} string A message to write to stdout. 267 | */ 268 | function write(string) { 269 | process.stdout.write(string); 270 | } 271 | -------------------------------------------------------------------------------- /lib/mocha/reporters/progress.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | 'use strict'; 3 | /** 4 | * @module Progress 5 | */ 6 | /** 7 | * Module dependencies. 8 | */ 9 | 10 | const Base = require('./base'); 11 | const inherits = require('../utils').inherits; 12 | const color = Base.color; 13 | const cursor = Base.cursor; 14 | 15 | /** 16 | * Expose `Progress`. 17 | */ 18 | 19 | exports = module.exports = Progress; 20 | 21 | /** 22 | * General progress bar color. 23 | */ 24 | 25 | Base.colors.progress = 90; 26 | 27 | /** 28 | * Initialize a new `Progress` bar test reporter. 29 | * 30 | * @public 31 | * @class 32 | * @memberof Mocha.reporters 33 | * @augments Mocha.reporters.Base 34 | * @api public 35 | * @param {Runner} runner 36 | * @param {Object} options 37 | */ 38 | function Progress(runner, options) { 39 | Base.call(this, runner); 40 | 41 | const self = this; 42 | const width = (Base.window.width * 0.5) | 0; 43 | const total = runner.total; 44 | let complete = 0; 45 | let lastN = -1; 46 | 47 | // default chars 48 | options = options || {}; 49 | const reporterOptions = options.reporterOptions || {}; 50 | 51 | options.open = reporterOptions.open || '['; 52 | options.complete = reporterOptions.complete || '▬'; 53 | options.incomplete = reporterOptions.incomplete || Base.symbols.dot; 54 | options.close = reporterOptions.close || ']'; 55 | options.verbose = reporterOptions.verbose || false; 56 | 57 | // tests started 58 | runner.on('start', function() { 59 | console.log(); 60 | cursor.hide(); 61 | }); 62 | 63 | // tests complete 64 | runner.on('test end', function() { 65 | complete++; 66 | 67 | const percent = complete / total; 68 | const n = (width * percent) | 0; 69 | const i = width - n; 70 | 71 | if (n === lastN && !options.verbose) { 72 | // Don't re-render the line if it hasn't changed 73 | return; 74 | } 75 | lastN = n; 76 | 77 | cursor.CR(); 78 | process.stdout.write('\u001b[J'); 79 | process.stdout.write(color('progress', ' ' + options.open)); 80 | process.stdout.write(Array(n).join(options.complete)); 81 | process.stdout.write(Array(i).join(options.incomplete)); 82 | process.stdout.write(color('progress', options.close)); 83 | if (options.verbose) { 84 | process.stdout.write(color('progress', ' ' + complete + ' of ' + total)); 85 | } 86 | }); 87 | 88 | // tests are complete, output some stats 89 | // and the failures if any 90 | runner.once('end', function() { 91 | cursor.show(); 92 | console.log(); 93 | self.epilogue(); 94 | }); 95 | } 96 | 97 | /** 98 | * Inherit from `Base.prototype`. 99 | */ 100 | inherits(Progress, Base); 101 | -------------------------------------------------------------------------------- /lib/mocha/reporters/spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | /** 4 | * @module Spec 5 | */ 6 | /** 7 | * Module dependencies. 8 | */ 9 | 10 | const Base = require('./base'); 11 | const inherits = require('../utils').inherits; 12 | const color = Base.color; 13 | 14 | /** 15 | * Expose `Spec`. 16 | */ 17 | 18 | exports = module.exports = Spec; 19 | 20 | /** 21 | * support macaca-reporter 22 | */ 23 | function stringify(obj, replacer, spaces, cycleReplacer) { 24 | return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces) 25 | } 26 | 27 | function serializer(replacer, cycleReplacer) { 28 | const stack = [], keys = [] 29 | 30 | if (cycleReplacer == null) cycleReplacer = function (key, value) { 31 | if (stack[0] === value) return "[Circular ~]" 32 | return "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]" 33 | } 34 | 35 | return function (key, value) { 36 | if (stack.length > 0) { 37 | const thisPos = stack.indexOf(this) 38 | ~thisPos ? stack.splice(thisPos + 1) : stack.push(this) 39 | ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key) 40 | if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value) 41 | } 42 | else stack.push(value) 43 | 44 | return replacer == null ? value : replacer.call(this, key, value) 45 | } 46 | } 47 | 48 | const totalTests = { 49 | total: 0 50 | }; 51 | 52 | function uuid() { 53 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 54 | const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 55 | return v.toString(16); 56 | }); 57 | } 58 | 59 | function transferCode(data) { 60 | data = data 61 | .replace(/\r\n?|[\n\u2028\u2029]/g, '\n') 62 | .replace(/^\uFEFF/, '') 63 | .replace(/^function\s*\(.*\)\s*{|\(.*\)\s*=>\s*{?/, '') 64 | .replace(/\s*\}$/, '') 65 | .replace(/"/g, ''); 66 | 67 | const spaces = data.match(/^\n?( *)/)[1].length; 68 | const tabs = data.match(/^\n?(\t*)/)[1].length; 69 | const reg = new RegExp(`^\n?${tabs ? '\t' : ' '}{${tabs || spaces}}`, 'gm'); 70 | 71 | return data 72 | .replace(reg, '') 73 | .replace(/^\s+|\s+$/g, '') 74 | .replace(/\)\./g, '\)\n\.'); 75 | } 76 | 77 | function transferTest(test, suite, parentTitle) { 78 | const code = test.fn ? test.fn.toString() : test.body; 79 | 80 | const cleaned = { 81 | title: test.title, 82 | fullTitle: parentTitle + ' -- ' + suite.title + ' -- ' + test.title, 83 | duration: test.duration || 0, 84 | state: test.state, 85 | pass: test.state === 'passed', 86 | fail: test.state === 'failed', 87 | pending: test.pending, 88 | context: require('../utils').stringify(test.context), 89 | code: code && transferCode(code), 90 | uuid: test.uuid || uuid() 91 | }; 92 | 93 | cleaned.skipped = !cleaned.pass && !cleaned.fail && !cleaned.pending && !cleaned.isHook; 94 | return cleaned; 95 | } 96 | 97 | function transferSuite(suite, totalTests, totalTime, parentTitle) { 98 | suite.uuid = uuid(); 99 | const cleanTmpTests = suite.tests.map(test => test.state && transferTest(test, suite, parentTitle)); 100 | const cleanTests = cleanTmpTests.filter(test => !!test); 101 | 102 | const passingTests = cleanTests.filter(item => item.state === 'passed'); 103 | 104 | const failingTests = cleanTests.filter(item => item.state === 'failed'); 105 | const pendingTests = cleanTests.filter(item => item.pending); 106 | const skippedTests = cleanTests.filter(item => item.skipped); 107 | let duration = 0; 108 | 109 | cleanTests.forEach(test => { 110 | duration += test.duration; 111 | totalTime.time += test.duration; 112 | }); 113 | 114 | suite.tests = cleanTests; 115 | suite.fullFile = suite.file || ''; 116 | suite.file = suite.file ? suite.file.replace(process.cwd(), '') : ''; 117 | suite.passes = passingTests; 118 | suite.failures = failingTests; 119 | suite.pending = pendingTests; 120 | suite.skipped = skippedTests; 121 | suite.totalTests = suite.tests.length; 122 | suite.totalPasses = passingTests.length; 123 | suite.totalFailures = failingTests.length; 124 | suite.totalPending = pendingTests.length; 125 | suite.totalSkipped = skippedTests.length; 126 | suite.duration = duration; 127 | } 128 | 129 | function getSuite(suite, totalTests) { 130 | const totalTime = { 131 | time: 0 132 | }; 133 | const queue = []; 134 | const result = JSON.parse(suite.replace(/"/g, '')); 135 | let next = result; 136 | totalTests.total++; 137 | while (next) { 138 | 139 | if (next.root) { 140 | transferSuite(next, totalTests, totalTime); 141 | } 142 | 143 | if (next.suites && next.suites.length) { 144 | next.suites.forEach(nextSuite => { 145 | transferSuite(nextSuite, totalTests, totalTime, next.title); 146 | queue.push(nextSuite); 147 | }); 148 | } 149 | next = queue.shift(); 150 | } 151 | result._totalTime = totalTime.time; 152 | return stringify(result); 153 | } 154 | 155 | function done(output, config, failures, exit) { 156 | try { 157 | window.__execCommand('saveReport', output) 158 | .finally(() => exit && exit(failures ? 1 : 0)); 159 | } catch (e) { 160 | console.log(e); 161 | } 162 | } 163 | 164 | /** 165 | * Initialize a new `Spec` test reporter. 166 | * 167 | * @public 168 | * @class 169 | * @memberof Mocha.reporters 170 | * @augments Mocha.reporters.Base 171 | * @api public 172 | * @param {Runner} runner 173 | */ 174 | function Spec(runner) { 175 | Base.call(this, runner); 176 | 177 | const self = this; 178 | let indents = 0; 179 | let n = 0; 180 | 181 | function indent() { 182 | return Array(indents).join(' '); 183 | } 184 | 185 | const getSuiteData = (isEnd) => { 186 | const result = getSuite(stringify(this._originSuiteData), totalTests); 187 | const obj = { 188 | stats: this.stats, 189 | suites: JSON.parse(result) 190 | }; 191 | 192 | const { 193 | passes, 194 | failures, 195 | pending, 196 | tests 197 | } = obj.stats; 198 | 199 | const passPercentage = Math.round((passes / (totalTests.total - pending)) * 1000) / 10; 200 | const pendingPercentage = Math.round((pending / totalTests.total) * 1000) / 10; 201 | 202 | if (!isEnd) { 203 | obj.stats.passPercent = passPercentage; 204 | obj.stats.pendingPercent = pendingPercentage; 205 | obj.stats.other = passes + failures + pending - tests; 206 | obj.stats.hasOther = obj.stats.other > 0; 207 | obj.stats.skipped = totalTests.total - tests; 208 | obj.stats.hasSkipped = obj.stats.skipped > 0; 209 | obj.stats.failures -= obj.stats.other; 210 | } 211 | return obj; 212 | }; 213 | 214 | const handleTestEnd = (isEnd) => { 215 | const obj = getSuiteData(isEnd); 216 | obj.stats.duration = obj.suites._totalTime || 0; 217 | this.output = obj; 218 | }; 219 | 220 | this.done = (failures, exit) => done(this.output, this.config, failures, exit); 221 | 222 | runner.on('start', () => { 223 | this._originSuiteData = runner.suite; 224 | }); 225 | 226 | runner.on('test end', test => { 227 | test.uuid = uuid(); 228 | handleTestEnd(); 229 | }); 230 | 231 | runner.once('end', () => { 232 | handleTestEnd(true); 233 | self.epilogue(); 234 | }); 235 | 236 | runner.on('suite', function (suite) { 237 | ++indents; 238 | console.log(color('suite', '%s%s'), indent(), suite.title); 239 | }); 240 | 241 | runner.on('suite end', function () { 242 | --indents; 243 | if (indents === 1) { 244 | console.log(); 245 | } 246 | }); 247 | 248 | runner.on('pending', function (test) { 249 | test.uuid = uuid(); 250 | const fmt = indent() + color('pending', ' - %s'); 251 | console.log(fmt, test.title); 252 | }); 253 | 254 | runner.on('pass', function (test) { 255 | let fmt; 256 | if (test.speed === 'fast') { 257 | fmt = 258 | indent() + 259 | color('checkmark', ' ' + Base.symbols.ok) + 260 | color('pass', ' %s'); 261 | console.log(fmt, test.title); 262 | } else { 263 | fmt = 264 | indent() + 265 | color('checkmark', ' ' + Base.symbols.ok) + 266 | color('pass', ' %s') + 267 | color(test.speed, ' (%dms)'); 268 | console.log(fmt, test.title, test.duration); 269 | } 270 | }); 271 | 272 | runner.on('fail', function (test, err) { 273 | console.log(indent() + color('fail', ' %d) %s'), ++n, test.title); 274 | console.log(indent(), color('fail', require('../utils').escape(err))); 275 | }); 276 | } 277 | 278 | /** 279 | * Inherit from `Base.prototype`. 280 | */ 281 | inherits(Spec, Base); 282 | -------------------------------------------------------------------------------- /lib/mocha/reporters/tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module TAP 4 | */ 5 | /** 6 | * Module dependencies. 7 | */ 8 | 9 | const Base = require('./base'); 10 | 11 | /** 12 | * Expose `TAP`. 13 | */ 14 | 15 | exports = module.exports = TAP; 16 | 17 | /** 18 | * Initialize a new `TAP` reporter. 19 | * 20 | * @public 21 | * @class 22 | * @memberof Mocha.reporters 23 | * @augments Mocha.reporters.Base 24 | * @api public 25 | * @param {Runner} runner 26 | */ 27 | function TAP(runner) { 28 | Base.call(this, runner); 29 | 30 | let n = 1; 31 | let passes = 0; 32 | let failures = 0; 33 | 34 | runner.on('start', function() { 35 | const total = runner.grepTotal(runner.suite); 36 | console.log('%d..%d', 1, total); 37 | }); 38 | 39 | runner.on('test end', function() { 40 | ++n; 41 | }); 42 | 43 | runner.on('pending', function(test) { 44 | console.log('ok %d %s # SKIP -', n, title(test)); 45 | }); 46 | 47 | runner.on('pass', function(test) { 48 | passes++; 49 | console.log('ok %d %s', n, title(test)); 50 | }); 51 | 52 | runner.on('fail', function(test, err) { 53 | failures++; 54 | console.log('not ok %d %s', n, title(test)); 55 | if (err.stack) { 56 | console.log(err.stack.replace(/^/gm, ' ')); 57 | } 58 | }); 59 | 60 | runner.once('end', function() { 61 | console.log('# tests ' + (passes + failures)); 62 | console.log('# pass ' + passes); 63 | console.log('# fail ' + failures); 64 | }); 65 | } 66 | 67 | /** 68 | * Return a TAP-safe title of `test` 69 | * 70 | * @api private 71 | * @param {Object} test 72 | * @return {String} 73 | */ 74 | function title(test) { 75 | return test.fullTitle().replace(/#/g, ''); 76 | } 77 | -------------------------------------------------------------------------------- /lib/mocha/reporters/xunit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module XUnit 4 | */ 5 | /** 6 | * Module dependencies. 7 | */ 8 | 9 | const Base = require('./base'); 10 | const utils = require('../utils'); 11 | const inherits = utils.inherits; 12 | const fs = require('fs'); 13 | const escape = utils.escape; 14 | const mkdirp = require('mkdirp'); 15 | const path = require('path'); 16 | 17 | /** 18 | * Save timer references to avoid Sinon interfering (see GH-237). 19 | */ 20 | 21 | /* eslint-disable no-unused-vars, no-native-reassign */ 22 | const Date = global.Date; 23 | const setTimeout = global.setTimeout; 24 | const setInterval = global.setInterval; 25 | const clearTimeout = global.clearTimeout; 26 | const clearInterval = global.clearInterval; 27 | /* eslint-enable no-unused-vars, no-native-reassign */ 28 | 29 | /** 30 | * Expose `XUnit`. 31 | */ 32 | 33 | exports = module.exports = XUnit; 34 | 35 | /** 36 | * Initialize a new `XUnit` reporter. 37 | * 38 | * @public 39 | * @class 40 | * @memberof Mocha.reporters 41 | * @augments Mocha.reporters.Base 42 | * @api public 43 | * @param {Runner} runner 44 | */ 45 | function XUnit(runner, options) { 46 | Base.call(this, runner); 47 | 48 | const stats = this.stats; 49 | const tests = []; 50 | const self = this; 51 | 52 | // the name of the test suite, as it will appear in the resulting XML file 53 | let suiteName; 54 | 55 | // the default name of the test suite if none is provided 56 | const DEFAULT_SUITE_NAME = 'Mocha Tests'; 57 | 58 | if (options && options.reporterOptions) { 59 | if (options.reporterOptions.output) { 60 | if (!fs.createWriteStream) { 61 | throw new Error('file output not supported in browser'); 62 | } 63 | 64 | mkdirp.sync(path.dirname(options.reporterOptions.output)); 65 | self.fileStream = fs.createWriteStream(options.reporterOptions.output); 66 | } 67 | 68 | // get the suite name from the reporter options (if provided) 69 | suiteName = options.reporterOptions.suiteName; 70 | } 71 | 72 | // fall back to the default suite name 73 | suiteName = suiteName || DEFAULT_SUITE_NAME; 74 | 75 | runner.on('pending', function(test) { 76 | tests.push(test); 77 | }); 78 | 79 | runner.on('pass', function(test) { 80 | tests.push(test); 81 | }); 82 | 83 | runner.on('fail', function(test) { 84 | tests.push(test); 85 | }); 86 | 87 | runner.once('end', function() { 88 | self.write( 89 | tag( 90 | 'testsuite', 91 | { 92 | name: suiteName, 93 | tests: stats.tests, 94 | failures: stats.failures, 95 | errors: stats.failures, 96 | skipped: stats.tests - stats.failures - stats.passes, 97 | timestamp: new Date().toUTCString(), 98 | time: stats.duration / 1000 || 0 99 | }, 100 | false 101 | ) 102 | ); 103 | 104 | tests.forEach(function(t) { 105 | self.test(t); 106 | }); 107 | 108 | self.write(''); 109 | }); 110 | } 111 | 112 | /** 113 | * Inherit from `Base.prototype`. 114 | */ 115 | inherits(XUnit, Base); 116 | 117 | /** 118 | * Override done to close the stream (if it's a file). 119 | * 120 | * @param failures 121 | * @param {Function} fn 122 | */ 123 | XUnit.prototype.done = function(failures, fn) { 124 | if (this.fileStream) { 125 | this.fileStream.end(function() { 126 | fn(failures); 127 | }); 128 | } else { 129 | fn(failures); 130 | } 131 | }; 132 | 133 | /** 134 | * Write out the given line. 135 | * 136 | * @param {string} line 137 | */ 138 | XUnit.prototype.write = function(line) { 139 | if (this.fileStream) { 140 | this.fileStream.write(line + '\n'); 141 | } else if (typeof process === 'object' && process.stdout) { 142 | process.stdout.write(line + '\n'); 143 | } else { 144 | console.log(line); 145 | } 146 | }; 147 | 148 | /** 149 | * Output tag for the given `test.` 150 | * 151 | * @param {Test} test 152 | */ 153 | XUnit.prototype.test = function(test) { 154 | const attrs = { 155 | classname: test.parent.fullTitle(), 156 | name: test.title, 157 | time: test.duration / 1000 || 0 158 | }; 159 | 160 | if (test.state === 'failed') { 161 | const err = test.err; 162 | this.write( 163 | tag( 164 | 'testcase', 165 | attrs, 166 | false, 167 | tag( 168 | 'failure', 169 | {}, 170 | false, 171 | escape(err.message) + '\n' + escape(err.stack) 172 | ) 173 | ) 174 | ); 175 | } else if (test.isPending()) { 176 | this.write(tag('testcase', attrs, false, tag('skipped', {}, true))); 177 | } else { 178 | this.write(tag('testcase', attrs, true)); 179 | } 180 | }; 181 | 182 | /** 183 | * HTML tag helper. 184 | * 185 | * @param name 186 | * @param attrs 187 | * @param close 188 | * @param content 189 | * @return {string} 190 | */ 191 | function tag(name, attrs, close, content) { 192 | const end = close ? '/>' : '>'; 193 | const pairs = []; 194 | let tag; 195 | 196 | for (const key in attrs) { 197 | if (Object.prototype.hasOwnProperty.call(attrs, key)) { 198 | pairs.push(key + '="' + escape(attrs[key]) + '"'); 199 | } 200 | } 201 | 202 | tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end; 203 | if (content) { 204 | tag += content + ' Math.pow(2, 31)) { 66 | this._enableTimeouts = false; 67 | } 68 | if (typeof ms === 'string') { 69 | ms = milliseconds(ms); 70 | } 71 | debug('timeout %d', ms); 72 | this._timeout = ms; 73 | if (this.timer) { 74 | this.resetTimeout(); 75 | } 76 | return this; 77 | }; 78 | 79 | /** 80 | * Set or get slow `ms`. 81 | * 82 | * @api private 83 | * @param {number|string} ms 84 | * @return {Runnable|number} ms or Runnable instance. 85 | */ 86 | Runnable.prototype.slow = function(ms) { 87 | if (!arguments.length || typeof ms === 'undefined') { 88 | return this._slow; 89 | } 90 | if (typeof ms === 'string') { 91 | ms = milliseconds(ms); 92 | } 93 | debug('slow %d', ms); 94 | this._slow = ms; 95 | return this; 96 | }; 97 | 98 | /** 99 | * Set and get whether timeout is `enabled`. 100 | * 101 | * @api private 102 | * @param {boolean} enabled 103 | * @return {Runnable|boolean} enabled or Runnable instance. 104 | */ 105 | Runnable.prototype.enableTimeouts = function(enabled) { 106 | if (!arguments.length) { 107 | return this._enableTimeouts; 108 | } 109 | debug('enableTimeouts %s', enabled); 110 | this._enableTimeouts = enabled; 111 | return this; 112 | }; 113 | 114 | /** 115 | * Halt and mark as pending. 116 | * 117 | * @memberof Mocha.Runnable 118 | * @public 119 | * @api public 120 | */ 121 | Runnable.prototype.skip = function() { 122 | throw new Pending('sync skip'); 123 | }; 124 | 125 | /** 126 | * Check if this runnable or its parent suite is marked as pending. 127 | * 128 | * @api private 129 | */ 130 | Runnable.prototype.isPending = function() { 131 | return this.pending || (this.parent && this.parent.isPending()); 132 | }; 133 | 134 | /** 135 | * Return `true` if this Runnable has failed. 136 | * @return {boolean} 137 | * @private 138 | */ 139 | Runnable.prototype.isFailed = function() { 140 | return !this.isPending() && this.state === 'failed'; 141 | }; 142 | 143 | /** 144 | * Return `true` if this Runnable has passed. 145 | * @return {boolean} 146 | * @private 147 | */ 148 | Runnable.prototype.isPassed = function() { 149 | return !this.isPending() && this.state === 'passed'; 150 | }; 151 | 152 | /** 153 | * Set or get number of retries. 154 | * 155 | * @api private 156 | */ 157 | Runnable.prototype.retries = function(n) { 158 | if (!arguments.length) { 159 | return this._retries; 160 | } 161 | this._retries = n; 162 | }; 163 | 164 | /** 165 | * Set or get current retry 166 | * 167 | * @api private 168 | */ 169 | Runnable.prototype.currentRetry = function(n) { 170 | if (!arguments.length) { 171 | return this._currentRetry; 172 | } 173 | this._currentRetry = n; 174 | }; 175 | 176 | /** 177 | * Return the full title generated by recursively concatenating the parent's 178 | * full title. 179 | * 180 | * @memberof Mocha.Runnable 181 | * @public 182 | * @api public 183 | * @return {string} 184 | */ 185 | Runnable.prototype.fullTitle = function() { 186 | return this.titlePath().join(' '); 187 | }; 188 | 189 | /** 190 | * Return the title path generated by concatenating the parent's title path with the title. 191 | * 192 | * @memberof Mocha.Runnable 193 | * @public 194 | * @api public 195 | * @return {string} 196 | */ 197 | Runnable.prototype.titlePath = function() { 198 | return this.parent.titlePath().concat([this.title]); 199 | }; 200 | 201 | /** 202 | * Clear the timeout. 203 | * 204 | * @api private 205 | */ 206 | Runnable.prototype.clearTimeout = function() { 207 | clearTimeout(this.timer); 208 | }; 209 | 210 | /** 211 | * Inspect the runnable void of private properties. 212 | * 213 | * @api private 214 | * @return {string} 215 | */ 216 | Runnable.prototype.inspect = function() { 217 | return JSON.stringify( 218 | this, 219 | function(key, val) { 220 | if (key[0] === '_') { 221 | return; 222 | } 223 | if (key === 'parent') { 224 | return '#'; 225 | } 226 | if (key === 'ctx') { 227 | return '#'; 228 | } 229 | return val; 230 | }, 231 | 2 232 | ); 233 | }; 234 | 235 | /** 236 | * Reset the timeout. 237 | * 238 | * @api private 239 | */ 240 | Runnable.prototype.resetTimeout = function() { 241 | const self = this; 242 | const ms = this.timeout() || 1e9; 243 | 244 | if (!this._enableTimeouts) { 245 | return; 246 | } 247 | this.clearTimeout(); 248 | this.timer = setTimeout(function() { 249 | if (!self._enableTimeouts) { 250 | return; 251 | } 252 | self.callback(self._timeoutError(ms)); 253 | self.timedOut = true; 254 | }, ms); 255 | }; 256 | 257 | /** 258 | * Set or get a list of whitelisted globals for this test run. 259 | * 260 | * @api private 261 | * @param {string[]} globals 262 | */ 263 | Runnable.prototype.globals = function(globals) { 264 | if (!arguments.length) { 265 | return this._allowedGlobals; 266 | } 267 | this._allowedGlobals = globals; 268 | }; 269 | 270 | /** 271 | * Run the test and invoke `fn(err)`. 272 | * 273 | * @param {Function} fn 274 | * @api private 275 | */ 276 | Runnable.prototype.run = function(fn) { 277 | const self = this; 278 | const start = new Date(); 279 | const ctx = this.ctx; 280 | let finished; 281 | let emitted; 282 | 283 | // Sometimes the ctx exists, but it is not runnable 284 | if (ctx && ctx.runnable) { 285 | ctx.runnable(this); 286 | } 287 | 288 | // called multiple times 289 | function multiple(err) { 290 | if (emitted) { 291 | return; 292 | } 293 | emitted = true; 294 | const msg = 'done() called multiple times'; 295 | if (err && err.message) { 296 | err.message += " (and Mocha's " + msg + ')'; 297 | self.emit('error', err); 298 | } else { 299 | self.emit('error', new Error(msg)); 300 | } 301 | } 302 | 303 | // finished 304 | function done(err) { 305 | const ms = self.timeout(); 306 | if (self.timedOut) { 307 | return; 308 | } 309 | 310 | if (finished) { 311 | return multiple(err); 312 | } 313 | 314 | self.clearTimeout(); 315 | self.duration = new Date() - start; 316 | finished = true; 317 | if (!err && self.duration > ms && self._enableTimeouts) { 318 | err = self._timeoutError(ms); 319 | } 320 | fn(err); 321 | } 322 | 323 | // for .resetTimeout() 324 | this.callback = done; 325 | 326 | // explicit async with `done` argument 327 | if (this.async) { 328 | this.resetTimeout(); 329 | 330 | // allows skip() to be used in an explicit async context 331 | this.skip = function asyncSkip() { 332 | done(new Pending('async skip call')); 333 | // halt execution. the Runnable will be marked pending 334 | // by the previous call, and the uncaught handler will ignore 335 | // the failure. 336 | throw new Pending('async skip; aborting execution'); 337 | }; 338 | 339 | if (this.allowUncaught) { 340 | return callFnAsync(this.fn); 341 | } 342 | try { 343 | callFnAsync(this.fn); 344 | } catch (err) { 345 | emitted = true; 346 | done(utils.getError(err)); 347 | } 348 | return; 349 | } 350 | 351 | if (this.allowUncaught) { 352 | if (this.isPending()) { 353 | done(); 354 | } else { 355 | callFn(this.fn); 356 | } 357 | return; 358 | } 359 | 360 | // sync or promise-returning 361 | try { 362 | if (this.isPending()) { 363 | done(); 364 | } else { 365 | callFn(this.fn); 366 | } 367 | } catch (err) { 368 | emitted = true; 369 | done(utils.getError(err)); 370 | } 371 | 372 | function callFn(fn) { 373 | const result = fn.call(ctx); 374 | if (result && typeof result.then === 'function') { 375 | self.resetTimeout(); 376 | result.then( 377 | function() { 378 | done(); 379 | // Return null so libraries like bluebird do not warn about 380 | // subsequently constructed Promises. 381 | return null; 382 | }, 383 | function(reason) { 384 | done(reason || new Error('Promise rejected with no or falsy reason')); 385 | } 386 | ); 387 | } else { 388 | if (self.asyncOnly) { 389 | return done( 390 | new Error( 391 | '--async-only option in use without declaring `done()` or returning a promise' 392 | ) 393 | ); 394 | } 395 | 396 | done(); 397 | } 398 | } 399 | 400 | function callFnAsync(fn) { 401 | var result = fn.call(ctx, function(err) { 402 | if (err instanceof Error || toString.call(err) === '[object Error]') { 403 | return done(err); 404 | } 405 | if (err) { 406 | if (Object.prototype.toString.call(err) === '[object Object]') { 407 | return done( 408 | new Error('done() invoked with non-Error: ' + JSON.stringify(err)) 409 | ); 410 | } 411 | return done(new Error('done() invoked with non-Error: ' + err)); 412 | } 413 | if (result && utils.isPromise(result)) { 414 | return done( 415 | new Error( 416 | 'Resolution method is overspecified. Specify a callback *or* return a Promise; not both.' 417 | ) 418 | ); 419 | } 420 | 421 | done(); 422 | }); 423 | } 424 | }; 425 | 426 | /** 427 | * Instantiates a "timeout" error 428 | * 429 | * @param {number} ms - Timeout (in milliseconds) 430 | * @return {Error} a "timeout" error 431 | * @private 432 | */ 433 | Runnable.prototype._timeoutError = function(ms) { 434 | let msg = 435 | 'Timeout of ' + 436 | ms + 437 | 'ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.'; 438 | if (this.file) { 439 | msg += ' (' + this.file + ')'; 440 | } 441 | return new Error(msg); 442 | }; 443 | -------------------------------------------------------------------------------- /lib/mocha/suite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module Suite 4 | */ 5 | 6 | /** 7 | * Module dependencies. 8 | */ 9 | const EventEmitter = require('events').EventEmitter; 10 | const Hook = require('./hook'); 11 | const utils = require('./utils'); 12 | const inherits = utils.inherits; 13 | const debug = require('debug')('mocha:suite'); 14 | const milliseconds = require('./ms'); 15 | 16 | /** 17 | * Expose `Suite`. 18 | */ 19 | 20 | exports = module.exports = Suite; 21 | 22 | /** 23 | * Create a new `Suite` with the given `title` and parent `Suite`. When a suite 24 | * with the same title is already present, that suite is returned to provide 25 | * nicer reporter and more flexible meta-testing. 26 | * 27 | * @memberof Mocha 28 | * @public 29 | * @api public 30 | * @param {Suite} parent 31 | * @param {string} title 32 | * @return {Suite} 33 | */ 34 | exports.create = function(parent, title) { 35 | const suite = new Suite(title, parent.ctx); 36 | suite.parent = parent; 37 | title = suite.fullTitle(); 38 | parent.addSuite(suite); 39 | return suite; 40 | }; 41 | 42 | /** 43 | * Initialize a new `Suite` with the given `title` and `ctx`. Derived from [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) 44 | * 45 | * @memberof Mocha 46 | * @public 47 | * @class 48 | * @param {string} title 49 | * @param {Context} parentContext 50 | */ 51 | function Suite(title, parentContext) { 52 | if (!utils.isString(title)) { 53 | throw new Error( 54 | 'Suite `title` should be a "string" but "' + 55 | typeof title + 56 | '" was given instead.' 57 | ); 58 | } 59 | this.title = title; 60 | function Context() {} 61 | Context.prototype = parentContext; 62 | this.ctx = new Context(); 63 | this.suites = []; 64 | this.tests = []; 65 | this.pending = false; 66 | this._beforeEach = []; 67 | this._beforeAll = []; 68 | this._afterEach = []; 69 | this._afterAll = []; 70 | this.root = !title; 71 | this._timeout = 2000; 72 | this._enableTimeouts = true; 73 | this._slow = 75; 74 | this._bail = false; 75 | this._retries = -1; 76 | this._onlyTests = []; 77 | this._onlySuites = []; 78 | this.delayed = false; 79 | } 80 | 81 | /** 82 | * Inherit from `EventEmitter.prototype`. 83 | */ 84 | inherits(Suite, EventEmitter); 85 | 86 | /** 87 | * Return a clone of this `Suite`. 88 | * 89 | * @api private 90 | * @return {Suite} 91 | */ 92 | Suite.prototype.clone = function() { 93 | const suite = new Suite(this.title); 94 | debug('clone'); 95 | suite.ctx = this.ctx; 96 | suite.timeout(this.timeout()); 97 | suite.retries(this.retries()); 98 | suite.enableTimeouts(this.enableTimeouts()); 99 | suite.slow(this.slow()); 100 | suite.bail(this.bail()); 101 | return suite; 102 | }; 103 | 104 | /** 105 | * Set or get timeout `ms` or short-hand such as "2s". 106 | * 107 | * @api private 108 | * @param {number|string} ms 109 | * @return {Suite|number} for chaining 110 | */ 111 | Suite.prototype.timeout = function(ms) { 112 | if (!arguments.length) { 113 | return this._timeout; 114 | } 115 | if (ms.toString() === '0') { 116 | this._enableTimeouts = false; 117 | } 118 | if (typeof ms === 'string') { 119 | ms = milliseconds(ms); 120 | } 121 | debug('timeout %d', ms); 122 | this._timeout = parseInt(ms, 10); 123 | return this; 124 | }; 125 | 126 | /** 127 | * Set or get number of times to retry a failed test. 128 | * 129 | * @api private 130 | * @param {number|string} n 131 | * @return {Suite|number} for chaining 132 | */ 133 | Suite.prototype.retries = function(n) { 134 | if (!arguments.length) { 135 | return this._retries; 136 | } 137 | debug('retries %d', n); 138 | this._retries = parseInt(n, 10) || 0; 139 | return this; 140 | }; 141 | 142 | /** 143 | * Set or get timeout to `enabled`. 144 | * 145 | * @api private 146 | * @param {boolean} enabled 147 | * @return {Suite|boolean} self or enabled 148 | */ 149 | Suite.prototype.enableTimeouts = function(enabled) { 150 | if (!arguments.length) { 151 | return this._enableTimeouts; 152 | } 153 | debug('enableTimeouts %s', enabled); 154 | this._enableTimeouts = enabled; 155 | return this; 156 | }; 157 | 158 | /** 159 | * Set or get slow `ms` or short-hand such as "2s". 160 | * 161 | * @api private 162 | * @param {number|string} ms 163 | * @return {Suite|number} for chaining 164 | */ 165 | Suite.prototype.slow = function(ms) { 166 | if (!arguments.length) { 167 | return this._slow; 168 | } 169 | if (typeof ms === 'string') { 170 | ms = milliseconds(ms); 171 | } 172 | debug('slow %d', ms); 173 | this._slow = ms; 174 | return this; 175 | }; 176 | 177 | /** 178 | * Set or get whether to bail after first error. 179 | * 180 | * @api private 181 | * @param {boolean} bail 182 | * @return {Suite|number} for chaining 183 | */ 184 | Suite.prototype.bail = function(bail) { 185 | if (!arguments.length) { 186 | return this._bail; 187 | } 188 | debug('bail %s', bail); 189 | this._bail = bail; 190 | return this; 191 | }; 192 | 193 | /** 194 | * Check if this suite or its parent suite is marked as pending. 195 | * 196 | * @api private 197 | */ 198 | Suite.prototype.isPending = function() { 199 | return this.pending || (this.parent && this.parent.isPending()); 200 | }; 201 | 202 | /** 203 | * Generic hook-creator. 204 | * @private 205 | * @param {string} title - Title of hook 206 | * @param {Function} fn - Hook callback 207 | * @return {Hook} A new hook 208 | */ 209 | Suite.prototype._createHook = function(title, fn) { 210 | const hook = new Hook(title, fn); 211 | hook.parent = this; 212 | hook.timeout(this.timeout()); 213 | hook.retries(this.retries()); 214 | hook.enableTimeouts(this.enableTimeouts()); 215 | hook.slow(this.slow()); 216 | hook.ctx = this.ctx; 217 | hook.file = this.file; 218 | return hook; 219 | }; 220 | 221 | /** 222 | * Run `fn(test[, done])` before running tests. 223 | * 224 | * @api private 225 | * @param {string} title 226 | * @param {Function} fn 227 | * @return {Suite} for chaining 228 | */ 229 | Suite.prototype.beforeAll = function(title, fn) { 230 | if (this.isPending()) { 231 | return this; 232 | } 233 | if (typeof title === 'function') { 234 | fn = title; 235 | title = fn.name; 236 | } 237 | title = '"before all" hook' + (title ? ': ' + title : ''); 238 | 239 | const hook = this._createHook(title, fn); 240 | this._beforeAll.push(hook); 241 | this.emit('beforeAll', hook); 242 | return this; 243 | }; 244 | 245 | /** 246 | * Run `fn(test[, done])` after running tests. 247 | * 248 | * @api private 249 | * @param {string} title 250 | * @param {Function} fn 251 | * @return {Suite} for chaining 252 | */ 253 | Suite.prototype.afterAll = function(title, fn) { 254 | if (this.isPending()) { 255 | return this; 256 | } 257 | if (typeof title === 'function') { 258 | fn = title; 259 | title = fn.name; 260 | } 261 | title = '"after all" hook' + (title ? ': ' + title : ''); 262 | 263 | const hook = this._createHook(title, fn); 264 | this._afterAll.push(hook); 265 | this.emit('afterAll', hook); 266 | return this; 267 | }; 268 | 269 | /** 270 | * Run `fn(test[, done])` before each test case. 271 | * 272 | * @api private 273 | * @param {string} title 274 | * @param {Function} fn 275 | * @return {Suite} for chaining 276 | */ 277 | Suite.prototype.beforeEach = function(title, fn) { 278 | if (this.isPending()) { 279 | return this; 280 | } 281 | if (typeof title === 'function') { 282 | fn = title; 283 | title = fn.name; 284 | } 285 | title = '"before each" hook' + (title ? ': ' + title : ''); 286 | 287 | const hook = this._createHook(title, fn); 288 | this._beforeEach.push(hook); 289 | this.emit('beforeEach', hook); 290 | return this; 291 | }; 292 | 293 | /** 294 | * Run `fn(test[, done])` after each test case. 295 | * 296 | * @api private 297 | * @param {string} title 298 | * @param {Function} fn 299 | * @return {Suite} for chaining 300 | */ 301 | Suite.prototype.afterEach = function(title, fn) { 302 | if (this.isPending()) { 303 | return this; 304 | } 305 | if (typeof title === 'function') { 306 | fn = title; 307 | title = fn.name; 308 | } 309 | title = '"after each" hook' + (title ? ': ' + title : ''); 310 | 311 | const hook = this._createHook(title, fn); 312 | this._afterEach.push(hook); 313 | this.emit('afterEach', hook); 314 | return this; 315 | }; 316 | 317 | /** 318 | * Add a test `suite`. 319 | * 320 | * @api private 321 | * @param {Suite} suite 322 | * @return {Suite} for chaining 323 | */ 324 | Suite.prototype.addSuite = function(suite) { 325 | suite.parent = this; 326 | suite.timeout(this.timeout()); 327 | suite.retries(this.retries()); 328 | suite.enableTimeouts(this.enableTimeouts()); 329 | suite.slow(this.slow()); 330 | suite.bail(this.bail()); 331 | this.suites.push(suite); 332 | this.emit('suite', suite); 333 | return this; 334 | }; 335 | 336 | /** 337 | * Add a `test` to this suite. 338 | * 339 | * @api private 340 | * @param {Test} test 341 | * @return {Suite} for chaining 342 | */ 343 | Suite.prototype.addTest = function(test) { 344 | test.parent = this; 345 | test.timeout(this.timeout()); 346 | test.retries(this.retries()); 347 | test.enableTimeouts(this.enableTimeouts()); 348 | test.slow(this.slow()); 349 | test.ctx = this.ctx; 350 | this.tests.push(test); 351 | this.emit('test', test); 352 | return this; 353 | }; 354 | 355 | /** 356 | * Return the full title generated by recursively concatenating the parent's 357 | * full title. 358 | * 359 | * @memberof Mocha.Suite 360 | * @public 361 | * @api public 362 | * @return {string} 363 | */ 364 | Suite.prototype.fullTitle = function() { 365 | return this.titlePath().join(' '); 366 | }; 367 | 368 | /** 369 | * Return the title path generated by recursively concatenating the parent's 370 | * title path. 371 | * 372 | * @memberof Mocha.Suite 373 | * @public 374 | * @api public 375 | * @return {string} 376 | */ 377 | Suite.prototype.titlePath = function() { 378 | let result = []; 379 | if (this.parent) { 380 | result = result.concat(this.parent.titlePath()); 381 | } 382 | if (!this.root) { 383 | result.push(this.title); 384 | } 385 | return result; 386 | }; 387 | 388 | /** 389 | * Return the total number of tests. 390 | * 391 | * @memberof Mocha.Suite 392 | * @public 393 | * @api public 394 | * @return {number} 395 | */ 396 | Suite.prototype.total = function() { 397 | return ( 398 | this.suites.reduce(function(sum, suite) { 399 | return sum + suite.total(); 400 | }, 0) + this.tests.length 401 | ); 402 | }; 403 | 404 | /** 405 | * Iterates through each suite recursively to find all tests. Applies a 406 | * function in the format `fn(test)`. 407 | * 408 | * @api private 409 | * @param {Function} fn 410 | * @return {Suite} 411 | */ 412 | Suite.prototype.eachTest = function(fn) { 413 | this.tests.forEach(fn); 414 | this.suites.forEach(function(suite) { 415 | suite.eachTest(fn); 416 | }); 417 | return this; 418 | }; 419 | 420 | /** 421 | * This will run the root suite if we happen to be running in delayed mode. 422 | */ 423 | Suite.prototype.run = function run() { 424 | if (this.root) { 425 | this.emit('run'); 426 | } 427 | }; 428 | -------------------------------------------------------------------------------- /lib/mocha/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha 5 | 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/mocha/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Runnable = require('./runnable'); 3 | const utils = require('./utils'); 4 | const isString = utils.isString; 5 | 6 | module.exports = Test; 7 | 8 | /** 9 | * Initialize a new `Test` with the given `title` and callback `fn`. 10 | * 11 | * @class 12 | * @augments Runnable 13 | * @param {String} title 14 | * @param {Function} fn 15 | */ 16 | function Test(title, fn) { 17 | if (!isString(title)) { 18 | throw new Error( 19 | 'Test `title` should be a "string" but "' + 20 | typeof title + 21 | '" was given instead.' 22 | ); 23 | } 24 | Runnable.call(this, title, fn); 25 | this.pending = !fn; 26 | this.type = 'test'; 27 | } 28 | 29 | /** 30 | * Inherit from `Runnable.prototype`. 31 | */ 32 | utils.inherits(Test, Runnable); 33 | 34 | Test.prototype.clone = function() { 35 | const test = new Test(this.title, this.fn); 36 | test.timeout(this.timeout()); 37 | test.slow(this.slow()); 38 | test.enableTimeouts(this.enableTimeouts()); 39 | test.retries(this.retries()); 40 | test.currentRetry(this.currentRetry()); 41 | test.globals(this.globals()); 42 | test.parent = this.parent; 43 | test.file = this.file; 44 | test.ctx = this.ctx; 45 | return test; 46 | }; 47 | -------------------------------------------------------------------------------- /lib/mocha/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | /** 5 | * @module 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | const debug = require('debug')('mocha:watch'); 13 | const fs = require('fs'); 14 | const glob = require('glob'); 15 | const path = require('path'); 16 | const join = path.join; 17 | const he = require('he'); 18 | 19 | /** 20 | * Ignored directories. 21 | */ 22 | 23 | const ignore = ['node_modules', '.git']; 24 | 25 | exports.inherits = require('util').inherits; 26 | 27 | /** 28 | * Escape special characters in the given string of html. 29 | * 30 | * @api private 31 | * @param {string} html 32 | * @return {string} 33 | */ 34 | exports.escape = function(html) { 35 | return he.encode(String(html), {useNamedReferences: false}); 36 | }; 37 | 38 | /** 39 | * Test if the given obj is type of string. 40 | * 41 | * @api private 42 | * @param {Object} obj 43 | * @return {boolean} 44 | */ 45 | exports.isString = function(obj) { 46 | return typeof obj === 'string'; 47 | }; 48 | 49 | /** 50 | * Watch the given `files` for changes 51 | * and invoke `fn(file)` on modification. 52 | * 53 | * @api private 54 | * @param {Array} files 55 | * @param {Function} fn 56 | */ 57 | exports.watch = function(files, fn) { 58 | const options = {interval: 100}; 59 | files.forEach(function(file) { 60 | debug('file %s', file); 61 | fs.watchFile(file, options, function(curr, prev) { 62 | if (prev.mtime < curr.mtime) { 63 | fn(file); 64 | } 65 | }); 66 | }); 67 | }; 68 | 69 | /** 70 | * Ignored files. 71 | * 72 | * @api private 73 | * @param {string} path 74 | * @return {boolean} 75 | */ 76 | function ignored(path) { 77 | return !~ignore.indexOf(path); 78 | } 79 | 80 | /** 81 | * Lookup files in the given `dir`. 82 | * 83 | * @api private 84 | * @param {string} dir 85 | * @param {string[]} [ext=['.js']] 86 | * @param {Array} [ret=[]] 87 | * @return {Array} 88 | */ 89 | exports.files = function(dir, ext, ret) { 90 | ret = ret || []; 91 | ext = ext || ['js']; 92 | 93 | const re = new RegExp('\\.(' + ext.join('|') + ')$'); 94 | 95 | fs 96 | .readdirSync(dir) 97 | .filter(ignored) 98 | .forEach(function(path) { 99 | path = join(dir, path); 100 | if (fs.lstatSync(path).isDirectory()) { 101 | exports.files(path, ext, ret); 102 | } else if (path.match(re)) { 103 | ret.push(path); 104 | } 105 | }); 106 | 107 | return ret; 108 | }; 109 | 110 | /** 111 | * Compute a slug from the given `str`. 112 | * 113 | * @api private 114 | * @param {string} str 115 | * @return {string} 116 | */ 117 | exports.slug = function(str) { 118 | return str 119 | .toLowerCase() 120 | .replace(/ +/g, '-') 121 | .replace(/[^-\w]/g, ''); 122 | }; 123 | 124 | /** 125 | * Strip the function definition from `str`, and re-indent for pre whitespace. 126 | * 127 | * @param {string} str 128 | * @return {string} 129 | */ 130 | exports.clean = function(str) { 131 | str = str 132 | .replace(/\r\n?|[\n\u2028\u2029]/g, '\n') 133 | .replace(/^\uFEFF/, '') 134 | // (traditional)-> space/name parameters body (lambda)-> parameters body multi-statement/single keep body content 135 | .replace( 136 | /^function(?:\s*|\s+[^(]*)\([^)]*\)\s*\{((?:.|\n)*?)\s*\}$|^\([^)]*\)\s*=>\s*(?:\{((?:.|\n)*?)\s*\}|((?:.|\n)*))$/, 137 | '$1$2$3' 138 | ); 139 | 140 | const spaces = str.match(/^\n?( *)/)[1].length; 141 | const tabs = str.match(/^\n?(\t*)/)[1].length; 142 | const re = new RegExp( 143 | '^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs || spaces) + '}', 144 | 'gm' 145 | ); 146 | 147 | str = str.replace(re, ''); 148 | 149 | return str.trim(); 150 | }; 151 | 152 | /** 153 | * Parse the given `qs`. 154 | * 155 | * @api private 156 | * @param {string} qs 157 | * @return {Object} 158 | */ 159 | exports.parseQuery = function(qs) { 160 | return qs 161 | .replace('?', '') 162 | .split('&') 163 | .reduce(function(obj, pair) { 164 | let i = pair.indexOf('='); 165 | const key = pair.slice(0, i); 166 | const val = pair.slice(++i); 167 | 168 | // Due to how the URLSearchParams API treats spaces 169 | obj[key] = decodeURIComponent(val.replace(/\+/g, '%20')); 170 | 171 | return obj; 172 | }, {}); 173 | }; 174 | 175 | /** 176 | * Highlight the given string of `js`. 177 | * 178 | * @api private 179 | * @param {string} js 180 | * @return {string} 181 | */ 182 | function highlight(js) { 183 | return js 184 | .replace(//g, '>') 186 | .replace(/\/\/(.*)/gm, '//$1') 187 | .replace(/('.*?')/gm, '$1') 188 | .replace(/(\d+\.\d+)/gm, '$1') 189 | .replace(/(\d+)/gm, '$1') 190 | .replace( 191 | /\bnew[ \t]+(\w+)/gm, 192 | 'new $1' 193 | ) 194 | .replace( 195 | /\b(function|new|throw|return|var|if|else)\b/gm, 196 | '$1' 197 | ); 198 | } 199 | 200 | /** 201 | * Highlight the contents of tag `name`. 202 | * 203 | * @api private 204 | * @param {string} name 205 | */ 206 | exports.highlightTags = function(name) { 207 | const code = document.getElementById('mocha').getElementsByTagName(name); 208 | for (let i = 0, len = code.length; i < len; ++i) { 209 | code[i].innerHTML = highlight(code[i].innerHTML); 210 | } 211 | }; 212 | 213 | /** 214 | * If a value could have properties, and has none, this function is called, 215 | * which returns a string representation of the empty value. 216 | * 217 | * Functions w/ no properties return `'[Function]'` 218 | * Arrays w/ length === 0 return `'[]'` 219 | * Objects w/ no properties return `'{}'` 220 | * All else: return result of `value.toString()` 221 | * 222 | * @api private 223 | * @param {*} value The value to inspect. 224 | * @param {string} typeHint The type of the value 225 | * @return {string} 226 | */ 227 | function emptyRepresentation(value, typeHint) { 228 | switch (typeHint) { 229 | case 'function': 230 | return '[Function]'; 231 | case 'object': 232 | return '{}'; 233 | case 'array': 234 | return '[]'; 235 | default: 236 | return value.toString(); 237 | } 238 | } 239 | 240 | /** 241 | * Takes some variable and asks `Object.prototype.toString()` what it thinks it 242 | * is. 243 | * 244 | * @api private 245 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString 246 | * @param {*} value The value to test. 247 | * @return {string} Computed type 248 | * @example 249 | * type({}) // 'object' 250 | * type([]) // 'array' 251 | * type(1) // 'number' 252 | * type(false) // 'boolean' 253 | * type(Infinity) // 'number' 254 | * type(null) // 'null' 255 | * type(new Date()) // 'date' 256 | * type(/foo/) // 'regexp' 257 | * type('type') // 'string' 258 | * type(global) // 'global' 259 | * type(new String('foo') // 'object' 260 | */ 261 | const type = (exports.type = function type(value) { 262 | if (value === undefined) { 263 | return 'undefined'; 264 | } else if (value === null) { 265 | return 'null'; 266 | } else if (Buffer.isBuffer(value)) { 267 | return 'buffer'; 268 | } 269 | return Object.prototype.toString 270 | .call(value) 271 | .replace(/^\[.+\s(.+?)]$/, '$1') 272 | .toLowerCase(); 273 | }); 274 | 275 | /** 276 | * Stringify `value`. Different behavior depending on type of value: 277 | * 278 | * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively. 279 | * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes. 280 | * - If `value` is an *empty* object, function, or array, return result of function 281 | * {@link emptyRepresentation}. 282 | * - If `value` has properties, call {@link exports.canonicalize} on it, then return result of 283 | * JSON.stringify(). 284 | * 285 | * @api private 286 | * @see exports.type 287 | * @param {*} value 288 | * @return {string} 289 | */ 290 | exports.stringify = function(value) { 291 | let typeHint = type(value); 292 | 293 | if (!~['object', 'array', 'function'].indexOf(typeHint)) { 294 | if (typeHint === 'buffer') { 295 | const json = Buffer.prototype.toJSON.call(value); 296 | // Based on the toJSON result 297 | return jsonStringify( 298 | json.data && json.type ? json.data : json, 299 | 2 300 | ).replace(/,(\n|$)/g, '$1'); 301 | } 302 | 303 | // IE7/IE8 has a bizarre String constructor; needs to be coerced 304 | // into an array and back to obj. 305 | if (typeHint === 'string' && typeof value === 'object') { 306 | value = value.split('').reduce(function(acc, char, idx) { 307 | acc[idx] = char; 308 | return acc; 309 | }, {}); 310 | typeHint = 'object'; 311 | } else { 312 | return jsonStringify(value); 313 | } 314 | } 315 | 316 | for (const prop in value) { 317 | if (Object.prototype.hasOwnProperty.call(value, prop)) { 318 | return jsonStringify( 319 | exports.canonicalize(value, null, typeHint), 320 | 2 321 | ).replace(/,(\n|$)/g, '$1'); 322 | } 323 | } 324 | 325 | return emptyRepresentation(value, typeHint); 326 | }; 327 | 328 | /** 329 | * like JSON.stringify but more sense. 330 | * 331 | * @api private 332 | * @param {Object} object 333 | * @param {number=} spaces 334 | * @param {number=} depth 335 | * @return {*} 336 | */ 337 | function jsonStringify(object, spaces, depth) { 338 | if (typeof spaces === 'undefined') { 339 | // primitive types 340 | return _stringify(object); 341 | } 342 | 343 | depth = depth || 1; 344 | let space = spaces * depth; 345 | let str = Array.isArray(object) ? '[' : '{'; 346 | const end = Array.isArray(object) ? ']' : '}'; 347 | let length = 348 | typeof object.length === 'number' 349 | ? object.length 350 | : Object.keys(object).length; 351 | // `.repeat()` polyfill 352 | function repeat(s, n) { 353 | return new Array(n).join(s); 354 | } 355 | 356 | function _stringify(val) { 357 | switch (type(val)) { 358 | case 'null': 359 | case 'undefined': 360 | val = '[' + val + ']'; 361 | break; 362 | case 'array': 363 | case 'object': 364 | val = jsonStringify(val, spaces, depth + 1); 365 | break; 366 | case 'boolean': 367 | case 'regexp': 368 | case 'symbol': 369 | case 'number': 370 | val = 371 | val === 0 && 1 / val === -Infinity // `-0` 372 | ? '-0' 373 | : val.toString(); 374 | break; 375 | case 'date': 376 | var sDate = isNaN(val.getTime()) ? val.toString() : val.toISOString(); 377 | val = '[Date: ' + sDate + ']'; 378 | break; 379 | case 'buffer': 380 | var json = val.toJSON(); 381 | // Based on the toJSON result 382 | json = json.data && json.type ? json.data : json; 383 | val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']'; 384 | break; 385 | default: 386 | val = 387 | val === '[Function]' || val === '[Circular]' 388 | ? val 389 | : JSON.stringify(val); // string 390 | } 391 | return val; 392 | } 393 | 394 | for (const i in object) { 395 | if (!Object.prototype.hasOwnProperty.call(object, i)) { 396 | continue; // not my business 397 | } 398 | --length; 399 | str += 400 | '\n ' + 401 | repeat(' ', space) + 402 | (Array.isArray(object) ? '' : '"' + i + '": ') + // key 403 | _stringify(object[i]) + // value 404 | (length ? ',' : ''); // comma 405 | } 406 | 407 | return ( 408 | str + 409 | // [], {} 410 | (str.length !== 1 ? '\n' + repeat(' ', --space) + end : end) 411 | ); 412 | } 413 | 414 | /** 415 | * Return a new Thing that has the keys in sorted order. Recursive. 416 | * 417 | * If the Thing... 418 | * - has already been seen, return string `'[Circular]'` 419 | * - is `undefined`, return string `'[undefined]'` 420 | * - is `null`, return value `null` 421 | * - is some other primitive, return the value 422 | * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method 423 | * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again. 424 | * - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()` 425 | * 426 | * @api private 427 | * @see {@link exports.stringify} 428 | * @param {*} value Thing to inspect. May or may not have properties. 429 | * @param {Array} [stack=[]] Stack of seen values 430 | * @param {string} [typeHint] Type hint 431 | * @return {(Object|Array|Function|string|undefined)} 432 | */ 433 | exports.canonicalize = function canonicalize(value, stack, typeHint) { 434 | let canonicalizedObj; 435 | /* eslint-disable no-unused-vars */ 436 | let prop; 437 | /* eslint-enable no-unused-vars */ 438 | typeHint = typeHint || type(value); 439 | function withStack(value, fn) { 440 | stack.push(value); 441 | fn(); 442 | stack.pop(); 443 | } 444 | 445 | stack = stack || []; 446 | 447 | if (stack.indexOf(value) !== -1) { 448 | return '[Circular]'; 449 | } 450 | 451 | switch (typeHint) { 452 | case 'undefined': 453 | case 'buffer': 454 | case 'null': 455 | canonicalizedObj = value; 456 | break; 457 | case 'array': 458 | withStack(value, function() { 459 | canonicalizedObj = value.map(function(item) { 460 | return exports.canonicalize(item, stack); 461 | }); 462 | }); 463 | break; 464 | case 'function': 465 | /* eslint-disable */ 466 | for (prop in value) { 467 | canonicalizedObj = {}; 468 | break; 469 | } 470 | /* eslint-enable guard-for-in */ 471 | if (!canonicalizedObj) { 472 | canonicalizedObj = emptyRepresentation(value, typeHint); 473 | break; 474 | } 475 | /* falls through */ 476 | case 'object': 477 | canonicalizedObj = canonicalizedObj || {}; 478 | withStack(value, function() { 479 | Object.keys(value) 480 | .sort() 481 | .forEach(function(key) { 482 | canonicalizedObj[key] = exports.canonicalize(value[key], stack); 483 | }); 484 | }); 485 | break; 486 | case 'date': 487 | case 'number': 488 | case 'regexp': 489 | case 'boolean': 490 | case 'symbol': 491 | canonicalizedObj = value; 492 | break; 493 | default: 494 | canonicalizedObj = value + ''; 495 | } 496 | 497 | return canonicalizedObj; 498 | }; 499 | 500 | /** 501 | * Lookup file names at the given `path`. 502 | * 503 | * @memberof Mocha.utils 504 | * @public 505 | * @api public 506 | * @param {string} filepath Base path to start searching from. 507 | * @param {string[]} extensions File extensions to look for. 508 | * @param {boolean} recursive Whether or not to recurse into subdirectories. 509 | * @return {string[]} An array of paths. 510 | */ 511 | exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) { 512 | let files = []; 513 | 514 | if (!fs.existsSync(filepath)) { 515 | if (fs.existsSync(filepath + '.js')) { 516 | filepath += '.js'; 517 | } else { 518 | files = glob.sync(filepath); 519 | if (!files.length) { 520 | throw new Error("cannot resolve path (or pattern) '" + filepath + "'"); 521 | } 522 | return files; 523 | } 524 | } 525 | 526 | try { 527 | const stat = fs.statSync(filepath); 528 | if (stat.isFile()) { 529 | return filepath; 530 | } 531 | } catch (err) { 532 | // ignore error 533 | return; 534 | } 535 | 536 | fs.readdirSync(filepath).forEach(function(file) { 537 | file = path.join(filepath, file); 538 | try { 539 | var stat = fs.statSync(file); 540 | if (stat.isDirectory()) { 541 | if (recursive) { 542 | files = files.concat(lookupFiles(file, extensions, recursive)); 543 | } 544 | return; 545 | } 546 | } catch (err) { 547 | // ignore error 548 | return; 549 | } 550 | if (!extensions) { 551 | throw new Error( 552 | 'extensions parameter required when filepath is a directory' 553 | ); 554 | } 555 | const re = new RegExp('\\.(?:' + extensions.join('|') + ')$'); 556 | if (!stat.isFile() || !re.test(file) || path.basename(file)[0] === '.') { 557 | return; 558 | } 559 | files.push(file); 560 | }); 561 | 562 | return files; 563 | }; 564 | 565 | /** 566 | * Generate an undefined error with a message warning the user. 567 | * 568 | * @return {Error} 569 | */ 570 | 571 | exports.undefinedError = function() { 572 | return new Error( 573 | 'Caught undefined error, did you throw without specifying what?' 574 | ); 575 | }; 576 | 577 | /** 578 | * Generate an undefined error if `err` is not defined. 579 | * 580 | * @param {Error} err 581 | * @return {Error} 582 | */ 583 | 584 | exports.getError = function(err) { 585 | return err || exports.undefinedError(); 586 | }; 587 | 588 | /** 589 | * @summary 590 | * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`) 591 | * @description 592 | * When invoking this function you get a filter function that get the Error.stack as an input, 593 | * and return a prettify output. 594 | * (i.e: strip Mocha and internal node functions from stack trace). 595 | * @return {Function} 596 | */ 597 | exports.stackTraceFilter = function() { 598 | // TODO: Replace with `process.browser` 599 | const is = typeof document === 'undefined' ? {node: true} : {browser: true}; 600 | let slash = path.sep; 601 | let cwd; 602 | if (is.node) { 603 | cwd = process.cwd() + slash; 604 | } else { 605 | cwd = (typeof location === 'undefined' 606 | ? window.location 607 | : location 608 | ).href.replace(/\/[^/]*$/, '/'); 609 | slash = '/'; 610 | } 611 | 612 | function isMochaInternal(line) { 613 | return ( 614 | ~line.indexOf('node_modules' + slash + 'mocha' + slash) || 615 | ~line.indexOf('node_modules' + slash + 'mocha.js') || 616 | ~line.indexOf('bower_components' + slash + 'mocha.js') || 617 | ~line.indexOf(slash + 'mocha.js') 618 | ); 619 | } 620 | 621 | function isNodeInternal(line) { 622 | return ( 623 | ~line.indexOf('(timers.js:') || 624 | ~line.indexOf('(events.js:') || 625 | ~line.indexOf('(node.js:') || 626 | ~line.indexOf('(module.js:') || 627 | ~line.indexOf('GeneratorFunctionPrototype.next (native)') || 628 | false 629 | ); 630 | } 631 | 632 | return function(stack) { 633 | stack = stack.split('\n'); 634 | 635 | stack = stack.reduce(function(list, line) { 636 | if (isMochaInternal(line)) { 637 | return list; 638 | } 639 | 640 | if (is.node && isNodeInternal(line)) { 641 | return list; 642 | } 643 | 644 | // Clean up cwd(absolute) 645 | if (/\(?.+:\d+:\d+\)?$/.test(line)) { 646 | line = line.replace('(' + cwd, '('); 647 | } 648 | 649 | list.push(line); 650 | return list; 651 | }, []); 652 | 653 | return stack.join('\n'); 654 | }; 655 | }; 656 | 657 | /** 658 | * Crude, but effective. 659 | * @api 660 | * @param {*} value 661 | * @return {boolean} Whether or not `value` is a Promise 662 | */ 663 | exports.isPromise = function isPromise(value) { 664 | return typeof value === 'object' && typeof value.then === 'function'; 665 | }; 666 | 667 | /** 668 | * It's a noop. 669 | * @api 670 | */ 671 | exports.noop = function() {}; 672 | -------------------------------------------------------------------------------- /lib/playwright.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Playwright = require('macaca-playwright'); 4 | 5 | const { setupCommands, exitForWait } = require('./commands'); 6 | 7 | module.exports = async options => { 8 | const playwright = new Playwright(); 9 | const opts = Object.assign({ 10 | debug: true, 11 | redirectConsole: true, 12 | }, options); 13 | console.log(JSON.stringify(opts, null, 2)); 14 | 15 | await playwright.startDevice(opts); 16 | await setupCommands(playwright); 17 | await playwright.get(options.url); 18 | 19 | const failCount = await exitForWait; 20 | 21 | if (failCount > 0) { 22 | throw new Error('test failed'); 23 | } 24 | return true; 25 | }; 26 | -------------------------------------------------------------------------------- /lib/uitest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async options => { 4 | return await require('./playwright')(options); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * return a thenable function can be use for await and function to call 5 | * use 6 | * ```js 7 | * // demo 8 | * const fn = createThenableFunction(); 9 | * setTimeout(fn, 1000, 1); 10 | * const ret = await fn; // ret === 1 11 | * ``` 12 | * @return {{ 13 | * (..args: any[]) => void; 14 | * then: cb => void; 15 | * }} thenable function 16 | */ 17 | function createThenableFunction() { 18 | let recordCallback; 19 | let resultCache; 20 | 21 | const fn = (...args) => { 22 | resultCache = args; 23 | if (recordCallback) { 24 | recordCallback(...args); 25 | } 26 | }; 27 | 28 | fn.then = callback => { 29 | if (resultCache) { 30 | callback(...resultCache); 31 | } else { 32 | recordCallback = callback; 33 | } 34 | }; 35 | 36 | return fn; 37 | } 38 | 39 | module.exports = { 40 | createThenableFunction, 41 | }; 42 | -------------------------------------------------------------------------------- /mocha.entry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-unused-vars: off */ 4 | /* eslint-env commonjs */ 5 | 6 | /** 7 | * Shim process.stdout. 8 | */ 9 | process.stdout = require('browser-stdout')({ level: false }); 10 | 11 | const Mocha = require('./lib/mocha/mocha'); 12 | 13 | /** 14 | * Create a Mocha instance. 15 | * 16 | * @return {undefined} 17 | */ 18 | 19 | const mocha = new Mocha({ reporter: 'html' }); 20 | 21 | /** 22 | * Save timer references to avoid Sinon interfering (see GH-237). 23 | */ 24 | 25 | const Date = global.Date; 26 | const setTimeout = global.setTimeout; 27 | const setInterval = global.setInterval; 28 | const clearTimeout = global.clearTimeout; 29 | const clearInterval = global.clearInterval; 30 | 31 | const uncaughtExceptionHandlers = []; 32 | 33 | const originalOnerrorHandler = global.onerror; 34 | 35 | /** 36 | * Remove uncaughtException listener. 37 | * Revert to original onerror handler if previously defined. 38 | */ 39 | 40 | process.removeListener = function(e, fn) { 41 | if (e === 'uncaughtException') { 42 | if (originalOnerrorHandler) { 43 | global.onerror = originalOnerrorHandler; 44 | } else { 45 | global.onerror = function() {}; 46 | } 47 | const i = uncaughtExceptionHandlers.indexOf(fn); 48 | if (i !== -1) { 49 | uncaughtExceptionHandlers.splice(i, 1); 50 | } 51 | } 52 | }; 53 | 54 | /** 55 | * Implements uncaughtException listener. 56 | */ 57 | 58 | process.on = function(e, fn) { 59 | if (e === 'uncaughtException') { 60 | global.onerror = function(err, url, line) { 61 | fn(new Error(err + ' (' + url + ':' + line + ')')); 62 | return !mocha.allowUncaught; 63 | }; 64 | uncaughtExceptionHandlers.push(fn); 65 | } 66 | }; 67 | 68 | // The BDD UI is registered by default, but no UI will be functional in the 69 | // browser without an explicit call to the overridden `mocha.ui` (see below). 70 | // Ensure that this default UI does not expose its methods to the global scope. 71 | mocha.suite.removeAllListeners('pre-require'); 72 | 73 | const immediateQueue = []; 74 | let immediateTimeout; 75 | 76 | function timeslice() { 77 | const immediateStart = new Date().getTime(); 78 | while (immediateQueue.length && new Date().getTime() - immediateStart < 100) { 79 | immediateQueue.shift()(); 80 | } 81 | if (immediateQueue.length) { 82 | immediateTimeout = setTimeout(timeslice, 0); 83 | } else { 84 | immediateTimeout = null; 85 | } 86 | } 87 | 88 | /** 89 | * High-performance override of Runner.immediately. 90 | */ 91 | 92 | Mocha.Runner.immediately = function(callback) { 93 | immediateQueue.push(callback); 94 | if (!immediateTimeout) { 95 | immediateTimeout = setTimeout(timeslice, 0); 96 | } 97 | }; 98 | 99 | /** 100 | * Function to allow assertion libraries to throw errors directly into mocha. 101 | * This is useful when running tests in a browser because window.onerror will 102 | * only receive the 'message' attribute of the Error. 103 | * @param {Error} err the error 104 | */ 105 | mocha.throwError = function(err) { 106 | uncaughtExceptionHandlers.forEach(function(fn) { 107 | fn(err); 108 | }); 109 | throw err; 110 | }; 111 | 112 | /** 113 | * Override ui to ensure that the ui functions are initialized. 114 | * Normally this would happen in Mocha.prototype.loadFiles. 115 | */ 116 | 117 | mocha.ui = function(ui) { 118 | Mocha.prototype.ui.call(this, ui); 119 | this.suite.emit('pre-require', global, null, this); 120 | return this; 121 | }; 122 | 123 | /** 124 | * Setup mocha with the given setting options. 125 | */ 126 | 127 | mocha.setup = function(opts) { 128 | if (typeof opts === 'string') { 129 | opts = { ui: opts }; 130 | } 131 | for (const opt in opts) { 132 | if (opts.hasOwnProperty(opt)) { 133 | this[opt](opts[opt]); 134 | } 135 | } 136 | return this; 137 | }; 138 | 139 | /** 140 | * Run mocha, returning the Runner. 141 | */ 142 | 143 | mocha.run = function(fn) { 144 | const options = mocha.options; 145 | mocha.globals('location'); 146 | 147 | const query = Mocha.utils.parseQuery(global.location.search || ''); 148 | if (query.grep) { 149 | mocha.grep(query.grep); 150 | } 151 | if (query.fgrep) { 152 | mocha.fgrep(query.fgrep); 153 | } 154 | if (query.invert) { 155 | mocha.invert(); 156 | } 157 | 158 | return Mocha.prototype.run.call(mocha, function(err) { 159 | // The DOM Document is not available in Web Workers. 160 | const document = global.document; 161 | if ( 162 | document && 163 | document.getElementById('mocha') && 164 | options.noHighlighting !== true 165 | ) { 166 | Mocha.utils.highlightTags('code'); 167 | } 168 | if (fn) { 169 | fn(err); 170 | } 171 | }); 172 | }; 173 | 174 | /** 175 | * Expose the process shim. 176 | * https://github.com/mochajs/mocha/pull/916 177 | */ 178 | 179 | Mocha.process = process; 180 | 181 | /** 182 | * Expose mocha. 183 | */ 184 | 185 | global.Mocha = Mocha; 186 | global.mocha = mocha; 187 | 188 | // this allows test/acceptance/required-tokens.js to pass; thus, 189 | // you can now do `const describe = require('mocha').describe` in a 190 | // browser context (assuming browserification). should fix #880 191 | module.exports = mocha; 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uitest", 3 | "version": "20.0.3", 4 | "description": "Run mocha in a browser environment.", 5 | "keywords": [ 6 | "uitest" 7 | ], 8 | "main": "index.js", 9 | "files": [ 10 | "lib/**/*.js", 11 | "*.js" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:macacajs/uitest.git" 16 | }, 17 | "dependencies": { 18 | "datahub-nodejs-sdk": "2", 19 | "macaca-playwright": "1" 20 | }, 21 | "devDependencies": { 22 | "@babel/eslint-parser": "^7.18.2", 23 | "@rollup/plugin-commonjs": "^22.0.0", 24 | "@rollup/plugin-node-resolve": "^13.3.0", 25 | "@types/mocha": "^10.0.1", 26 | "chai": "^4.2.0", 27 | "eslint": "7", 28 | "eslint-config-airbnb": "^19.0.4", 29 | "eslint-config-egg": "^7.1.0", 30 | "eslint-config-prettier": "^4.1.0", 31 | "eslint-plugin-mocha": "^10.1.0", 32 | "git-contributor": "1", 33 | "husky": "^1.3.1", 34 | "ipv4": "1", 35 | "macaca-ecosystem": "*", 36 | "mocha": "*", 37 | "nyc": "^15.1.0", 38 | "rollup": "^2.75.7", 39 | "rollup-plugin-node-globals": "^1.4.0", 40 | "rollup-plugin-polyfill-node": "^0.9.0", 41 | "vuepress": "^1.5.2" 42 | }, 43 | "scripts": { 44 | "prepublishOnly": "rollup -c", 45 | "test": "mocha", 46 | "cov": "nyc --reporter=lcov --reporter=text mocha", 47 | "lint": "eslint --fix .", 48 | "docs:dev": "vuepress dev docs", 49 | "docs:build": "vuepress build docs", 50 | "contributor": "git-contributor" 51 | }, 52 | "husky": { 53 | "hooks": { 54 | "pre-commit": "npm run lint" 55 | } 56 | }, 57 | "license": "MIT" 58 | } 59 | -------------------------------------------------------------------------------- /packages/gulp-uitest/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /packages/gulp-uitest/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | extends: 'eslint-config-egg', 6 | parserOptions: { 7 | ecmaVersion: 2020, 8 | }, 9 | plugins: [], 10 | rules: { 11 | 'valid-jsdoc': 0, 12 | 'no-script-url': 0, 13 | 'no-multi-spaces': 0, 14 | 'default-case': 0, 15 | 'no-case-declarations': 0, 16 | 'one-var-declaration-per-line': 0, 17 | 'no-restricted-syntax': 0, 18 | 'jsdoc/require-param': 0, 19 | 'jsdoc/check-param-names': 0, 20 | 'jsdoc/require-param-description': 0, 21 | 'arrow-parens': 0, 22 | 'prefer-promise-reject-errors': 0, 23 | 'no-control-regex': 0, 24 | 'no-use-before-define': 0, 25 | 'array-callback-return': 0, 26 | 'no-bitwise': 0, 27 | 'no-self-compare': 0, 28 | 'one-var': 0, 29 | 'no-trailing-spaces': [ 'warn', { skipBlankLines: true }], 30 | 'no-return-await': 0, 31 | }, 32 | globals: { 33 | window: true, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /packages/gulp-uitest/README.md: -------------------------------------------------------------------------------- 1 | # gulp-uitest 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![CI][CI-image]][CI-url] 5 | [![Test coverage][coveralls-image]][coveralls-url] 6 | [![node version][node-image]][node-url] 7 | [![npm download][download-image]][download-url] 8 | 9 | [npm-image]: https://img.shields.io/npm/v/gulp-uitest.svg 10 | [npm-url]: https://npmjs.org/package/gulp-uitest 11 | [CI-image]: https://github.com/macacajs/uitest/actions/workflows/packages-ci.yml/badge.svg 12 | [CI-url]: https://github.com/macacajs/uitest/actions/workflows/packages-ci.yml 13 | [coveralls-image]: https://img.shields.io/coveralls/xudafeng/gulp-uitest.svg 14 | [coveralls-url]: https://coveralls.io/r/xudafeng/gulp-uitest?branch=master 15 | [node-image]: https://img.shields.io/badge/node.js-%3E=_7-green.svg 16 | [node-url]: http://nodejs.org/download/ 17 | [download-image]: https://img.shields.io/npm/dm/gulp-uitest.svg 18 | [download-url]: https://npmjs.org/package/gulp-uitest 19 | 20 | > gulp uitest 21 | 22 | 23 | 24 | ## Contributors 25 | 26 | |[
    xudafeng](https://github.com/xudafeng)
    |[
    06wj](https://github.com/06wj)
    |[
    meowtec](https://github.com/meowtec)
    | 27 | | :---: | :---: | :---: | 28 | 29 | 30 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Wed Mar 30 2022 12:08:37 GMT+0800`. 31 | 32 | 33 | 34 | ## Installment 35 | 36 | ```bash 37 | $ npm i gulp-uitest --save-dev 38 | ``` 39 | 40 | ## Usage 41 | 42 | ```javascript 43 | const uitest = require('gulp-uitest'); 44 | 45 | gulp.task('test', [], function() { 46 | return gulp 47 | .src('path/to/index.html') 48 | .pipe(uitest({ 49 | width: 600, 50 | height: 480, 51 | hidpi: false, 52 | useContentSize: true, 53 | show: false 54 | })); 55 | }); 56 | ``` 57 | 58 | ## License 59 | 60 | The MIT License (MIT) 61 | -------------------------------------------------------------------------------- /packages/gulp-uitest/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/gulp-uitest'); 4 | -------------------------------------------------------------------------------- /packages/gulp-uitest/lib/gulp-uitest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const uitest = require('uitest'); 4 | const through = require('through2'); 5 | 6 | module.exports = function(options) { 7 | return through.obj(function(file, env, cb) { 8 | uitest(Object.assign({ 9 | url: 'file://' + file.path, 10 | }, options)) 11 | .then(cb, cb); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/gulp-uitest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-uitest", 3 | "version": "17.0.0", 4 | "description": "gulp uitest", 5 | "keywords": [ 6 | "gulp" 7 | ], 8 | "main": "index.js", 9 | "files": [ 10 | "index.js", 11 | "lib/*.js" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/macacajs/uitest.git" 16 | }, 17 | "dependencies": { 18 | "through2": "^2.0.1", 19 | "uitest": "17" 20 | }, 21 | "devDependencies": { 22 | "eslint": "7", 23 | "eslint-config-egg": "^7.1.0", 24 | "eslint-plugin-mocha": "^10.1.0", 25 | "git-contributor": "1", 26 | "husky": "*", 27 | "mocha": "*", 28 | "nyc": "^15.0.0" 29 | }, 30 | "husky": { 31 | "hooks": { 32 | "pre-commit": "npm run lint" 33 | } 34 | }, 35 | "scripts": { 36 | "test": "nyc --reporter=lcov --reporter=text mocha", 37 | "lint": "eslint --fix .", 38 | "contributor": "git-contributor" 39 | }, 40 | "license": "MIT" 41 | } 42 | -------------------------------------------------------------------------------- /packages/gulp-uitest/test/gulp-uitest.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const gulpUitest = require('..'); 5 | 6 | describe('test', () => { 7 | it('should be ok', () => { 8 | assert(gulpUitest); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/gulp-uitest/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const commonjs = require('@rollup/plugin-commonjs'); 4 | const nodePolyfills = require('rollup-plugin-polyfill-node'); 5 | const { nodeResolve } = require('@rollup/plugin-node-resolve'); 6 | const nodeGlobal = require('rollup-plugin-node-globals'); 7 | 8 | module.exports = { 9 | input: 'mocha.entry.js', 10 | context: 'globalThis', 11 | output: { 12 | file: 'mocha.js', 13 | name: 'mocha', 14 | format: 'umd', 15 | }, 16 | plugins: [ 17 | commonjs(), 18 | nodeGlobal(), 19 | nodePolyfills(), 20 | nodeResolve({ browser: true, preferBuiltins: true }), 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /test/case-sample/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'eslint-config-airbnb', 3 | env: { 4 | browser: true, 5 | es6: true, 6 | }, 7 | parser: '@babel/eslint-parser', 8 | parserOptions: { 9 | ecmaVersion: 6, 10 | sourceType: 'module', 11 | requireConfigFile: false, 12 | }, 13 | rules: { 14 | camelcase: 0, 15 | 'no-restricted-syntax': 0, 16 | 'no-plusplus': 0, 17 | 'no-underscore-dangle': 0, 18 | 'no-useless-escape': 0, 19 | 'no-unused-vars': ['error', { args: 'none' }], 20 | 'no-prototype-builtins': 0, 21 | 'max-len': 0, 22 | 'class-methods-use-this': 0, 23 | 'function-paren-newline': 0, 24 | 'no-await-in-loop': 0, 25 | 'comma-dangle': ['error', { 26 | arrays: 'always-multiline', 27 | objects: 'always-multiline', 28 | imports: 'always-multiline', 29 | exports: 'always-multiline', 30 | functions: 'ignore', 31 | }], 32 | 'no-param-reassign': 0, 33 | 'consistent-return': 0, 34 | 'object-curly-newline': ['error', { consistent: true, minProperties: 6 }], 35 | 'arrow-parens': 0, 36 | 'arrow-body-style': ['warn'], 37 | 'prefer-destructuring': ['error', { 38 | VariableDeclarator: { 39 | array: false, 40 | object: true, 41 | }, 42 | AssignmentExpression: { 43 | array: false, 44 | object: false, 45 | }, 46 | }], 47 | 'import/named': 0, 48 | 'import/no-extraneous-dependencies': 0, 49 | 'no-mixed-operators': 0, 50 | 'no-shadow': 0, 51 | 'no-useless-constructor': 0, 52 | 'no-return-assign': 0, 53 | 'prefer-rest-params': 0, 54 | 'no-restricted-globals': [0, 'location'], 55 | 'prefer-promise-reject-errors': 0, 56 | 'no-bitwise': 0, 57 | 'no-return-await': 0, 58 | 'import/prefer-default-export': 0, 59 | 'func-names': 0, 60 | 'no-use-before-define': 0, 61 | 'global-require': 0, 62 | 'no-else-return': 0, 63 | 'no-lonely-if': 0, 64 | 'guard-for-in': 0, 65 | 'one-var': 0, 66 | 'one-var-declaration-per-line': 0, 67 | 'no-console': 0, 68 | }, 69 | globals: { 70 | assert: false, 71 | chai: false, 72 | _macaca_uitest: false, 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /test/case-sample/fileChoose.js: -------------------------------------------------------------------------------- 1 | describe('test/case-sample/fileChoose.js', () => { 2 | afterEach(() => { 3 | document.querySelector('#testForKeyboard').innerHTML = ''; 4 | }); 5 | 6 | it('file choose should be ok', async () => { 7 | if (!_macaca_uitest.fileChooser) return; 8 | document.querySelector('#testForKeyboard').innerHTML = ''; 9 | const inputDOM = document.querySelector('#input'); 10 | assert(inputDOM); 11 | const rect = inputDOM.getBoundingClientRect(); 12 | await Promise.all([ 13 | _macaca_uitest.fileChooser('README.md'), 14 | _macaca_uitest.mouse.click(rect.left + 1, rect.top + 1), 15 | ]); 16 | 17 | assert.strictEqual(inputDOM.files[0].name, 'README.md'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/case-sample/keyboard.js: -------------------------------------------------------------------------------- 1 | describe('test/case-sample/keyboard.js', () => { 2 | afterEach(() => { 3 | document.querySelector('#testForKeyboard').innerHTML = ''; 4 | }); 5 | 6 | it('keyboard insertText should be ok', async () => { 7 | if (!_macaca_uitest.keyboard) return; 8 | document.querySelector('#testForKeyboard').innerHTML = ''; 9 | const inputDOM = document.querySelector('#input'); 10 | assert(inputDOM); 11 | inputDOM.focus(); 12 | await _macaca_uitest.keyboard.insertText('😂'); 13 | assert.equal('😂', inputDOM.value); 14 | }); 15 | 16 | it('keyboard should be ok', async () => { 17 | if (!_macaca_uitest.keyboard) return; 18 | document.querySelector('#testForKeyboard').innerHTML = ''; 19 | const inputDOM = document.querySelector('#input'); 20 | assert(inputDOM); 21 | inputDOM.focus(); 22 | await _macaca_uitest.keyboard.type('Hello World!'); 23 | await _macaca_uitest.keyboard.press('ArrowLeft'); 24 | 25 | await _macaca_uitest.keyboard.down('Shift'); 26 | for (let i = 0; i < ' World'.length; i++) { await _macaca_uitest.keyboard.press('ArrowLeft'); } 27 | await _macaca_uitest.keyboard.up('Shift'); 28 | 29 | await _macaca_uitest.keyboard.press('Backspace'); 30 | assert.equal('Hello!', inputDOM.value); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/case-sample/mouse.js: -------------------------------------------------------------------------------- 1 | describe('test/case-sample/mouse.js', () => { 2 | afterEach(() => { 3 | document.querySelector('#testForMouse').innerHTML = ''; 4 | }); 5 | 6 | it('mouse should be ok', async () => { 7 | if (!_macaca_uitest.mouse) return; 8 | document.querySelector('#testForMouse').innerHTML = ''; 9 | const btn = document.querySelector('#btn'); 10 | assert(btn); 11 | let result = ''; 12 | btn.onmousemove = () => { 13 | result += '1'; 14 | }; 15 | btn.onmousedown = () => { 16 | result += '2'; 17 | }; 18 | btn.onmouseup = () => { 19 | result += '3'; 20 | }; 21 | btn.onclick = () => { 22 | result += '4'; 23 | }; 24 | btn.ondblclick = () => { 25 | result += '5'; 26 | }; 27 | // 1234123412342345 28 | const { x, y } = btn.getBoundingClientRect(); 29 | await _macaca_uitest.mouse.move(x + 1, y + 1); 30 | await _macaca_uitest.mouse.down(); 31 | await _macaca_uitest.mouse.up(); // 连续触发鼠标down和up 会触发click 32 | result += ','; 33 | await _macaca_uitest.mouse.click(x + 1, y + 1); // 触发mousemove mousedown mouseup click 34 | result += ','; 35 | await _macaca_uitest.mouse.dblclick(x + 2, y + 2); // 触发mousemove mousedown mouseup click mousedown mouseup dblclick 36 | assert.equal(result, '1234,1234,12342345'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/case-sample/page.js: -------------------------------------------------------------------------------- 1 | describe('test/case-sample/page.js', () => { 2 | it('page should be ok', async () => { 3 | if (!_macaca_uitest.page) return; 4 | const { page } = _macaca_uitest; 5 | const pageId = await page.newPage('https://google.com'); 6 | let title = await page.exec(pageId, async () => document.title); 7 | assert.equal(title, 'Google'); 8 | await page.exec(pageId, async () => { 9 | await _macaca_uitest.keyboard.insertText('😂'); 10 | await _macaca_uitest.keyboard.press('Enter'); 11 | return false; 12 | }); 13 | await page.waitForEvent(pageId, 'load'); 14 | title = await page.exec(pageId, async () => document.title); 15 | assert.equal(title, '😂 - Google 搜索'); 16 | await page.close(pageId); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/case-sample/retries.js: -------------------------------------------------------------------------------- 1 | describe('test/case-sample/sample2.js', () => { 2 | const collections = []; 3 | 4 | beforeEach(async () => { 5 | collections.push(1); 6 | }); 7 | 8 | it.retries(3, 'retries should be work', async () => { 9 | if (collections.length !== 2) throw new Error('error'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/case-sample/sample1.js: -------------------------------------------------------------------------------- 1 | describe('test/case-sample/sample1.js', () => { 2 | afterEach(async function () { 3 | window.__coverage__ = { test: {} }; 4 | await _macaca_uitest.saveScreenshot(this); 5 | }); 6 | 7 | it('1should be true', done => { 8 | _macaca_uitest.screenshot('aa.png', () => { 9 | console.log('1diff'); 10 | // throw Error(); 11 | done(); 12 | }); 13 | }); 14 | 15 | it('2should be true', done => { 16 | _macaca_uitest.screenshot('aa1.png', () => { 17 | console.log('2diff'); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('3should be true', done => { 23 | _macaca_uitest.screenshot('aa123.png', () => { 24 | console.log('3diff'); 25 | done(); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/case-sample/sample2.js: -------------------------------------------------------------------------------- 1 | const { assert } = chai; 2 | 3 | describe('test/case-sample/sample2.js', () => { 4 | before(async () => { 5 | const p = new Promise((resolve, reject) => { 6 | resolve('res'); 7 | }); 8 | const res = await p; 9 | console.log('before', res); 10 | }); 11 | 12 | beforeEach(async () => { 13 | const p = new Promise((resolve, reject) => { 14 | resolve('res'); 15 | }); 16 | const res = await p; 17 | console.log('beforeEach', res); 18 | }); 19 | 20 | after(() => { 21 | console.log('after'); 22 | }); 23 | 24 | it('should be ok', async () => { 25 | const p = new Promise((resolve, reject) => { 26 | resolve('res'); 27 | }); 28 | const res = await p; 29 | assert.equal(res, 'res'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/ci.sh: -------------------------------------------------------------------------------- 1 | Xvfb -ac -screen scrn 1280x2000x24 :9.0 & 2 | 3 | export DISPLAY=:9.0 4 | 5 | sleep 3 6 | 7 | npm run test 8 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | UITest 5 | 6 | 7 | 8 | 9 | 10 |
    11 |
    12 |
    13 | 14 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --timeout 300000 3 | -------------------------------------------------------------------------------- /test/uitest.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const uitest = require('..'); 5 | 6 | describe('test/uitest.test.js', function() { 7 | this.timeout(60 * 1000); 8 | it('run test should be ok', async () => { 9 | const url = path.join(__dirname, 'index.html'); 10 | await uitest({ 11 | url: `file://${url}`, 12 | width: 800, 13 | height: 600, 14 | }); 15 | }); 16 | }); 17 | 18 | -------------------------------------------------------------------------------- /test/uitls.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { createThenableFunction } = require('../lib/utils'); 4 | 5 | describe('test/exit.test.js', () => { 6 | it('createThenableFunction should be ok', async () => { 7 | const randomRet = Math.random(); 8 | const fn = createThenableFunction(); 9 | 10 | setTimeout(() => { 11 | fn(randomRet); 12 | }, 10); 13 | 14 | const ret = await fn; 15 | 16 | if (ret !== randomRet) { 17 | throw new Error('failed'); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /uitest-mocha-shim.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | ; (function () { 5 | 6 | function getUUID() { 7 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' 8 | .replace(/[xy]/g, function (c) { 9 | const r = Math.random() * 16 | 0; 10 | const v = c === 'x' ? r : (r & 0x3 | 0x8); 11 | return v.toString(16); 12 | }); 13 | } 14 | 15 | if (!window.__execCommand) { 16 | window.__execCommand = async () => { }; 17 | } 18 | 19 | window._macaca_uitest = { 20 | mouse: { 21 | click(x, y, opt) { 22 | return window.__execCommand('mouse', 'click', x, y, opt); 23 | }, 24 | dblclick(x, y, opt) { 25 | return window.__execCommand('mouse', 'dblclick', x, y, opt); 26 | }, 27 | move(x, y, opt) { 28 | return window.__execCommand('mouse', 'move', x, y, opt); 29 | }, 30 | down(opt) { 31 | return window.__execCommand('mouse', 'down', opt); 32 | }, 33 | up(opt) { 34 | return window.__execCommand('mouse', 'up', opt); 35 | }, 36 | wheel(opt) { 37 | return window.__execCommand('mouse', 'wheel', x, y, opt); 38 | } 39 | }, 40 | keyboard: { 41 | type(str, opt) { 42 | return window.__execCommand('keyboard', 'type', str, opt); 43 | }, 44 | down(key) { 45 | return window.__execCommand('keyboard', 'down', key); 46 | }, 47 | up(key) { 48 | return window.__execCommand('keyboard', 'up', key); 49 | }, 50 | insertText(text) { 51 | return window.__execCommand('keyboard', 'insertText', text); 52 | }, 53 | press(key, opt) { 54 | return window.__execCommand('keyboard', 'press', key, opt); 55 | } 56 | }, 57 | page: { 58 | newPage(url) { 59 | return window.__execCommand('newPage', url); 60 | }, 61 | close(pageId) { 62 | return window.__execCommand('closePage', pageId); 63 | }, 64 | waitForSelector(pageId, selector) { 65 | return window.__execCommand('waitForSelector', pageId, selector); 66 | }, 67 | waitForEvent(pageId, eventName) { 68 | return window.__execCommand('waitForEvent', pageId, eventName); 69 | }, 70 | exec(pageId, func) { 71 | return window.__execCommand('runInPage', pageId, `(${func.toString()})()`); 72 | }, 73 | }, 74 | fileChooser(filePath) { 75 | return window.__execCommand('fileChooser', filePath); 76 | }, 77 | switchScene() { 78 | const args = Array.prototype.slice.call(arguments); 79 | return window.__execCommand('switchScene', args[0]); 80 | }, 81 | 82 | switchAllScenes() { 83 | const args = Array.prototype.slice.call(arguments); 84 | return window.__execCommand('switchAllScenes', args[0]); 85 | }, 86 | 87 | saveVideo(context) { 88 | return new Promise((resolve, reject) => { 89 | window.__execCommand('getVideoName').then(name => { 90 | // 失败后直接返回 91 | if (!name) return resolve(name); 92 | const filePath = `./screenshots/${name}`; 93 | this.appendToContext(context, filePath); 94 | resolve(filePath); 95 | }).catch(e => resolve(null)); 96 | }); 97 | }, 98 | 99 | saveScreenshot(context) { 100 | return new Promise((resolve, reject) => { 101 | const name = `${getUUID()}.png`; 102 | const filePath = `./screenshots/${name}`; 103 | this.appendToContext(context, filePath); 104 | resolve(this.screenshot(name)); 105 | }); 106 | }, 107 | 108 | screenshot(name) { 109 | const filePath = `./reports/screenshots/${name}`; 110 | return window.__execCommand('screenshot', filePath); 111 | }, 112 | 113 | appendToContext(mocha, content) { 114 | try { 115 | const test = mocha.currentTest || mocha.test; 116 | if (!test.context) { 117 | test.context = content; 118 | } else if (Array.isArray(test.context)) { 119 | test.context.push(content); 120 | } else { 121 | test.context = [test.context]; 122 | test.context.push(content); 123 | } 124 | } catch (e) { 125 | console.log('error', e); 126 | } 127 | }, 128 | 129 | setup(options) { 130 | let mochaOptions = options; 131 | 132 | mochaOptions = Object.assign({}, options, { 133 | reporter: 'spec', 134 | useColors: true 135 | }); 136 | 137 | return mocha.setup(mochaOptions); 138 | }, 139 | 140 | run() { 141 | return mocha.run(function (failedCount) { 142 | const __coverage__ = window.__coverage__; 143 | 144 | if (__coverage__) { 145 | window.__execCommand('saveCoverage', __coverage__); 146 | } 147 | 148 | // delay to exit 149 | setTimeout(() => { 150 | window.__execCommand('exit', { failedCount }); 151 | }, 200); 152 | }); 153 | } 154 | }; 155 | 156 | })(); 157 | 158 | --------------------------------------------------------------------------------