├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .github └── CODEOWNERS ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── api ├── commands │ ├── help.js │ └── run.js ├── index.js ├── metro │ ├── aliases │ │ ├── empty.js │ │ ├── index.js │ │ └── react-native.js │ ├── babel.js │ ├── configure.js │ ├── source.js │ └── template.js ├── server.js └── tmp │ └── .gitkeep ├── app.json ├── bin └── ekke ├── components ├── ekke.js ├── loading.js └── renderer.js ├── development.js ├── docs └── ekke-react-native-intro.gif ├── ekke.js ├── examples ├── example.mocha.js └── example.tape.js ├── index.js ├── native ├── bridge.js ├── constants.js ├── evaluator.js ├── lifecycle.js ├── screen.js ├── subway.js └── uncaught.js ├── package-lock.json ├── package.json ├── production.js ├── runners ├── index.js ├── mocha.js └── tape.js └── test ├── .babelrc ├── ekke ├── bridge.test.js ├── components │ ├── ekke.test.js │ ├── loading.test.js │ └── renderer.test.js ├── development.test.js ├── evaluator.test.js ├── production.test.js └── subway.test.js ├── mock ├── errorutils.js ├── index.js └── servers.js └── native ├── bridge.test.js ├── lifecycle.test.js ├── packagejson.test.js ├── subway.test.js └── uncaught.test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10.15.3 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn run test:nodejs 38 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | api/metro/template.js 2 | api/tmp/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['godaddy-react'], 4 | globals: { 5 | '__DEV__': 'readonly', 6 | 'ErrorUtils': 'readonly' 7 | }, 8 | rules: { 9 | 'no-process-env': 0, 10 | 'max-statements': 0, 11 | 'no-new-func': 0, 12 | 'complexity': 0, 13 | 'no-console': 0, 14 | 'no-sync': 0 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # 2 | # Default: 3 | # 4 | # These users will be automatically assigned as reviewer for everything in the 5 | # repository unless a different match is made. 6 | # 7 | * @3rd-Eden @swaagie 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # We don't want our build files to show up 64 | api/tmp/* 65 | !api/tmp/.gitkeep 66 | 67 | # The files are added using the `react-native eject` command for development 68 | ios 69 | android 70 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # We don't want our build files to show up 64 | api/tmp/* 65 | !api/tmp/.gitkeep 66 | 67 | # The files are added using the `react-native eject` command for development 68 | ios 69 | android 70 | 71 | # Above this line should be a copy of our .gitignore 72 | # Additional files that need to be ignore before publish 73 | .github 74 | .circleci 75 | test* 76 | docs 77 | examples 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ### 1.1.0 4 | 5 | - [#20] Support Windows Paths 6 | - [#19] Use a custom path for the Metro cache, this change also enables the 7 | cache by default again. So if you want to kill the cache specifically for 8 | you tests you need to use the `--reset-cache` flag for that. 9 | 10 | ### 1.0.2 11 | 12 | - [#3] Add missing repository field in package.json 13 | - [#4] Correctly reference Metro in the README 14 | 15 | ### 1.0.1 16 | 17 | - [#1] Corrected the image path in the `README` so it correctly shows up on 18 | the npm packages page: https://www.npmjs.com/package/ekke 19 | 20 | ### 1.0.0 21 | 22 | - Initial release 23 | 24 | [#1]: https://github.com/godaddy/ekke/pull/1 25 | [#3]: https://github.com/godaddy/ekke/pull/3 26 | [#4]: https://github.com/godaddy/ekke/pull/4 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at oss@godaddy.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 GoDaddy Operating Company, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `EKKE` 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/godaddy/ekke.svg)](https://greenkeeper.io/) 4 | 5 | #### [Ekke-Ekke-Ekke-Ekke][NI] PTANG Zoo Boing! Z' nourrwringmm[...][Ekke] 6 | 7 | `ekke` a unique new test runner for React-Native. Unlike other testing 8 | frameworks, it doesn't execute your tests in Node.js, with a bunch of mocks, but 9 | instead, it orchestrates the bundling and execution of tests directly **inside 10 | your React-Native application**. `ekke` allows your tests to fully access every 11 | API to the platform has to offer. 12 | 13 | ### Why should you adopt Ekke for React-Native testing? 14 | 15 | - **Platform independent** The test runner **does not contain any native code** 16 | that means that every platform that React-Native supports now, or in the 17 | future will work out of the box. 18 | - **Code confidence** Tests run in the environment of your production code. No 19 | need to rely on imperfect mocks. Tests run on devices to guarantee API's match 20 | specifications. 21 | - **Different test runners** At its core, Ekke is nothing more than an 22 | orchestration tool. We have built-in support for different test runners. 23 | - **Rendering** It's not just unit tests. Ekke provides a rendering API that 24 | allows you to mount and render components in your test suite. 25 | 26 |

27 | 28 |
29 | Ekke in action: running a test suite, streaming results back to the CLI 30 |

31 | 32 | ## Installation 33 | 34 | The module is released in the public NPM Registry and can be installed by running: 35 | 36 | ```bash 37 | npm install --save ekke 38 | ``` 39 | 40 | After installation, you can [integrate](#integration) `ekke` into your project. 41 | 42 | ## Table of Contents 43 | 44 | - [Installation](#installation) 45 | - [Integration](#integration) 46 | - [Runners](#runners) 47 | - [mocha](#mocha) 48 | - [Tape](#tape) 49 | - [API](#API) 50 | - [Component](#component) 51 | - [render](#render) 52 | - [CLI](#cli) 53 | - [run](#run) 54 | - [help](#help) 55 | - [Debugging](#debugging) 56 | - [The Ekke CLI](#the-ekke-cli) 57 | - [React Native Component](#react-native-component) 58 | 59 | ## Integration 60 | 61 | Ekke needs a host application to run. That can either be the application you are 62 | currently developing or fresh installation of `react-native init`. 63 | 64 | Not sure what to pick? 65 | 66 | - **Application developers** Using the application that you're currently 67 | developing is recommended. This allows your test suites to execute in 68 | precisely the same environment. Also, it will enable your test suites to 69 | leverage any `NativeModule` that your application might be consuming. 70 | - **Library authors** It depends on the library you're developing. If you are 71 | building a native add-on, or include `NativeModules` as dependencies, it's 72 | advised to create an example application in your project that has all native 73 | libraries linked. TODO: What is the alternative here, if there is one? 74 | 75 | Integrating is as easy as importing `ekke`, and including the [component] in 76 | your app! 77 | 78 | ```js 79 | import { Ekke } from 'ekke'; 80 | 81 | function App() { 82 | return ( 83 | <> 84 | 85 | 86 | 87 | Your App Here 88 | 89 | 90 | ) 91 | } 92 | ``` 93 | 94 | You can now run your tests by executing the [run] command of the `ekke` CLI: 95 | 96 | ```sh 97 | # Make sure that the simulator of your choice is running. 98 | react-native run-ios # or react-native run-android 99 | 100 | ekke run glob/pattern/*.test.js more/test/files.js --using mocha 101 | ``` 102 | 103 | And now watch the magic unfold on your app. 104 | 105 | > If you are worried about shipping `Ekke` in your application, the component is 106 | > using [process.env.NODE_ENV][env] to switch between [development][dev] and 107 | > [production][prod] builds. Production builds will completely remove `Ekke` 108 | > from your code. You can also conditionally include it `{ __DEV__ && }`. 109 | 110 | ## Runners 111 | 112 | At its core, Ekke is nothing more than an orchestration tool, it runs 113 | [Metro][metro] bundler with a specific configuration, executes code 114 | automatically in the React Native environment, and reports back in the CLI. To 115 | run the test, we need to know which test runner you prefer so we can bundle it 116 | with the tests. The following runners are available: 117 | 118 | - [mocha](#mocha) 119 | - [tape](#tape) 120 | 121 | ### mocha 122 | 123 | To use [Mocha](https://mochajs.org/) make sure you have the testing framework as 124 | well as an assertion framework installed in your project: 125 | 126 | ```bash 127 | npm install --save-dev mocha 128 | npm install --save-dev assume # or any other assert framework, e.g. chai 129 | ``` 130 | 131 | Once all your dependencies finish installing, you can start writing your tests. 132 | 133 | ```js 134 | import { describe, it } from 'mocha'; 135 | import { render } from 'ekke'; 136 | import assume from 'assume'; 137 | 138 | describe('The best test suite in the world', function () { 139 | it('is amazing', function () { 140 | const amazing = 'amazing'; 141 | 142 | assume(amazing).is.a('string'); 143 | assume(!!amazing).is.true(); 144 | }); 145 | }); 146 | ``` 147 | 148 | Provide `mocha` as value to the `--using` flag to select it as test runner. 149 | 150 | ```bash 151 | ekke run test.js --using mocha 152 | ``` 153 | 154 | The following Mocha options can be customized using the follow CLI flags: 155 | 156 | - `--mocha.fgrep` Only run tests containing this string. 157 | **Defaults to `''`**. 158 | - `--mocha.grep` Only run tests matching this string. 159 | **Defaults to `''`**. 160 | - `--mocha.invert` Inverts grep and fgrep matches. 161 | **Defaults to `false`**. 162 | - `--mocha.ui` Specify user interface. 163 | **Defaults to `bdd`**. 164 | - `--mocha.reporter` Specify reporter to use. 165 | **Defaults to `spec`**. 166 | - `--mocha.slow` Specify "slow" test threshold (in milliseconds). 167 | **Defaults to `75`**. 168 | - `--mocha.timeout` Specify the test timeout threshold (in milliseconds). 169 | **Defaults to `2000`**. 170 | - `--mocha.bail` Abort ("bail") after first test failure. 171 | **Defaults to `true`**. 172 | - `--mocha.color` Force-enable color output. 173 | **Defaults to `true`**. 174 | - `--mocha.inlineDiffs` Display actual/expected differences inline within each string. 175 | **Defaults to `true`**. 176 | 177 | ```bash 178 | ekke run test.js --using mocha --mocha.reporter min --mocha.timeout 5000 179 | ``` 180 | 181 | ### Tape 182 | 183 | Using [tape](https://github.com/substack/tape) as the test runner is pretty 184 | self-explanatory. You import `tape` into your test files and write your tests 185 | and assertions using provided by the framework. 186 | 187 | ```js 188 | import { render } from 'ekke'; 189 | import test from 'tape'; 190 | 191 | test('one', function (t) { 192 | t.plan(2); 193 | t.ok(true); 194 | 195 | setTimeout(function () { 196 | t.equal(1+3, 4); 197 | }, 100); 198 | }); 199 | ``` 200 | 201 | Once the tests are completed, simple tell `ekke` that you're `--using tape` to 202 | run the tests. 203 | 204 | ```bash 205 | ekke run test.js --using tape 206 | ``` 207 | 208 | The will run your tests, and output the TAP (Test. Anything. Protocol) to your 209 | terminal which you can pipe to any of the [Pretty Tap 210 | Reporters](https://github.com/substack/tape#pretty-reporters) that you might 211 | have installed. For example, if you want to use `tap-spec`: 212 | 213 | ```bash 214 | ekke run test.js --using tape | tap-spec 215 | ``` 216 | 217 | ## API 218 | 219 | The API exposes the following methods: 220 | 221 | - [component](#component) 222 | - [render](#render) 223 | 224 | ### Component 225 | 226 | ```js 227 | import { Ekke } from 'ekke'; 228 | ``` 229 | 230 | The `Ekke` component controls the orchestration, execution, and rendering of the 231 | test suite. The component can be used as a wrapper, or as a regular component 232 | and be included in your application. 233 | 234 | The component accepts the following **optional** properties: 235 | 236 | - `interval`, **String**, The component doesn't know when you are using the CLI, 237 | so it polls at a given interval, with an HTTP request, to the server that runs 238 | in the CLI to figure out if it's active and is waiting for tests to run. The 239 | lower the interval, the quicker your tests will be picked up by the component, 240 | but also the more performance it takes away from your app. 241 | **Defaults to `10 seconds`**. 242 | - `hostname`, **String**, The hostname of your machine that the CLI server is 243 | running on. 244 | **Defaults to `localhost` on iOS and `10.0.2.2` on Android**. 245 | - `port`, **Number**, The port number that the CLI server is running on. 246 | **Defaults to `1975`**. 247 | - `alive`, **Function**, Function to execute when the `Ekke` test runner is 248 | activated and is about to run the test suites. 249 | 250 | ```js 251 | // 252 | // Stand alone. 253 | // 254 | 255 | 256 | // 257 | // Or wrap your app with it, you decide what is best for your application. 258 | // 259 | 260 | 261 | 262 | ``` 263 | 264 | To see an example of the implementation, take a look at our [index.js][index] 265 | file. It's what we use to test our code. 266 | 267 | ### render 268 | 269 | ```js 270 | import { render } from 'ekke'; 271 | ``` 272 | 273 | The render method allows you to render any React-Native component on the screen 274 | of the application. 275 | 276 | ```js 277 | import { View } from 'react-native'; 278 | import { render } from 'ekke'; 279 | import React from 'react'; 280 | 281 | describe('test', function () { 282 | it('renders a red square', function () { 283 | const ref = React.createRef(); 284 | await render(); 289 | 290 | // 291 | // You can now use ref.current to access the rendered view. 292 | // Not only that, but there's big red square on your app as well. 293 | // 294 | }); 295 | }); 296 | ``` 297 | 298 | ## CLI 299 | 300 | The `ekke` CLI is automatically installed in your `node_modules` when you 301 | install `ekke` in the project as a dependency. We use the CLI to communicate 302 | between the `` component that you included in your application and the 303 | terminal. 304 | 305 | The CLI should **not be globally installed**. Instead, you directly reference 306 | the locally installed binary from your `package.json`. 307 | 308 | ```json 309 | { 310 | "name": "your-super-awesome-package", 311 | "scripts": { 312 | "test": "ekke run test/*.js --using mocha" 313 | } 314 | } 315 | ``` 316 | 317 | And run the scripts using `npm`. 318 | 319 | ```bash 320 | npm test 321 | 322 | # You can use the double dash support from npm to send additional flags: 323 | # npm test -- --watch 324 | ``` 325 | 326 | Alternatively, you can use the `npx` command to execute the commands as well 327 | without the requirement of global installation of `ekke`: 328 | 329 | ```bash 330 | npx ekke 331 | ``` 332 | 333 | The following CLI commands are available: 334 | 335 | - [run](#run) 336 | - [help](#help) 337 | 338 | ### run 339 | 340 | ```bash 341 | ekke run --flags 342 | ``` 343 | 344 | The `run` command allows you to run your specified tests on the device that 345 | included the `` React component. When you run the command, we will 346 | execute the following: 347 | 348 | - Start up the Metro bundler on the specified `hostname` and `port`. 349 | - Attach a WebSocket server on the created Metro bundler, for communication purposes. 350 | - Find all the test files you want to include based on the supplied glob. 351 | - Wait for the poll request from the `` component. 352 | - Bundle all your tests, and the specified library using Metro Bundler. 353 | - Listen to progress events that are sent by `` component over the 354 | WebSocket connection. 355 | - Proxy all the `console.*`, `process.stdout` to your terminal. 356 | - Close the CLI again with error code `0` or `1` depending on if your tests pass. 357 | 358 | The `run` command assumes that all CLI arguments after the `run` command are the 359 | test files that need to execute inside React-Native. We allow single or multiple 360 | files, a [glob][glob] pattern, or a combination of files and globs. 361 | 362 | ```bash 363 | # Executes test/1.js, test/2.js, and then test/2.js using the mocha runner. 364 | ekke run test/1.js test/2.js test/3.js --using mocha 365 | 366 | # The same above, but done using a glob pattern to fetch all .js files 367 | # from the test directory 368 | ekke run test/*.js --using mocha 369 | 370 | # Executes test/1.js and then all the .test.js files 371 | ekke run test/1.js test/*.test.js --using tape 372 | ``` 373 | 374 | You can use the following CLI flags to change the behavior of the command: 375 | 376 | - `--using {runner}` This tells Ekke which runner should be used to execute your 377 | tests. 378 | **Defaults to `mocha`** See [Runners](#runners) for all runners. 379 | - `--watch` By default, we close the process with either an exit code `0` or `1` 380 | as an indication of the test results (0 passes, 1 failure). If you do not want 381 | the process to exit, you can use the `--watch` flag to keep the CLI process 382 | alive. 383 | **Defaults to false.** 384 | - `--reset-cache` The Metro Bundler caching system is enabled by default so 385 | that tests run in a performant way and don't always rebuild. Using this 386 | flag will disable the cache. 387 | **Defaults to false.** 388 | - `--cache-location` We already made sure that the Metro Bundler cache of your 389 | test doesn't collide with your test cache, but if you like to store it 390 | somewhere else you can change it with this flag. 391 | **Defaults to `os.tempdir()/ekke-cache`.** 392 | - `--no-silent` We silence the output of the Metro bundler by default, this 393 | allows you to see the Metro bundler output again. 394 | **Defaults to false.** 395 | - `--hostname` The hostname we should attach our Metro Bundler on. The hostname 396 | should be accessible by React-Native application. 397 | **Defaults to `localhost`.** 398 | - `--port` The port number we should use for the Metro Bundler, we don't want to 399 | clash with the Metro bundler of your `react-native start` command so it should 400 | be different, but still accessible by your React-Native application. 401 | **Defaults to `1975`** (The year Monty Python and the Holy Grail got released) 402 | - `--require` Files that should be required before your test suites are required 403 | **Defaults to ¯\_(ツ)_/¯, nothing** 404 | 405 | In addition to these default flags, any flag that you prefix with the name of 406 | the runner will be considered as options and used as configuration: 407 | 408 | ```bash 409 | ekke run test.js --using mocha --mocha.timeout 3000 410 | ``` 411 | 412 | See the [Runners](#runners) for their specific configuration flags. 413 | 414 | ### help 415 | 416 | Display a list of all the available command their supported CLI flags. The help 417 | message is visible when you run `ekke` without, an unknown, or the `help` 418 | command: 419 | 420 | ``` 421 | 422 | ekke (v1.0.2) 423 | Ekke-Ekke-Ekke-Ekke-PTANG. Zoo-Boing. Z' nourrwringmm... 424 | 425 | COMMANDS: 426 | 427 | run Run the given glob of test files. 428 | --port Port number that Metro Bundler should use. 429 | --hostname Hostname that Metro Bundler should use. 430 | --using Name of the test runner to use. 431 | --watch Don't exit when the tests complete but keep listening. 432 | --no-silent Do not suppress the output of Metro. 433 | --require Require module (before tests are executed). 434 | --reset-cache Clear the Metro cache. 435 | --cache-location Change the Metro cache location. 436 | help Displays this help message. 437 | --no-color Disable colors in help message. 438 | 439 | EXAMPLES: 440 | 441 | $ ekke run ./test/*.test.js --using mocha 442 | 443 | ``` 444 | 445 | The output contains colors by default if you wish to remove those you can use 446 | the `--no-color` flag. 447 | 448 | ## Debugging 449 | 450 | Both the `CLI` and the `react-native` code bases use [diagnostics] under the 451 | hood for logging purposes. The logs are disabled by default but can be enabled 452 | by using the `DEBUG` feature flags. They both log under the `ekke:*` namespace. 453 | 454 | #### The Ekke CLI 455 | 456 | ```js 457 | DEBUG=ekke* ekke 458 | ``` 459 | 460 | #### React Native Component 461 | 462 | ```js 463 | import { AsyncStorage } from 'react-native'; 464 | 465 | AsyncStorage.setItem('debug', 'ekke*', function () { 466 | // 467 | // Reload your app, and the debug logs will now be enabled. 468 | // 469 | }); 470 | ``` 471 | 472 | For more detailed information about [diagnostics], please see their project page. 473 | 474 | ## Development 475 | 476 | - **Fork** Fork the repository to create a copy to your own GitHub account. 477 | - **Clone** Clone the newly created GitHub repo to your local machine. 478 | - **Branch** Create a fresh new branch from the master branch. 479 | - **Install** Run `npm install` to install dependencies and devDependencies. 480 | - **Setup** Run `npm run setup` to create development specific folders. 481 | - **Hack** Make your changes. Write tests covering your changes. 482 | - **Test** Run both `npm test` and `npm test:ekke` to ensure nothing got broken. 483 | - **Push** Commit and push your changes to fork. 484 | - **Pull Request** Create a pull request from your created branch to our master. 485 | - **Review** We'll review your change, and ask for updates if need. 486 | - **Merge** Virtual high fives are exchanged when your PR lands. 487 | 488 | ## License 489 | 490 | [MIT](LICENSE) 491 | 492 | [diagnostics]: https://github.com/3rd-Eden/diagnostics 493 | [metro]: https://github.com/facebook/metro 494 | [NI]: https://en.wikipedia.org/wiki/Knights_Who_Say_%22Ni!%22 495 | [Ekke]: https://youtu.be/RZvsGdJP3ng?t=17 496 | [env]: ./index.js 497 | [dev]: ./development.js 498 | [prod]: ./production.js 499 | [component]: #component 500 | [run]: #run 501 | [index]: ./index.js 502 | [glob]: https://www.npmjs.com/package/glob 503 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We take security very seriously at GoDaddy. We appreciate your efforts to 4 | responsibly disclose your findings, and will make every effort to acknowledge 5 | your contributions. 6 | 7 | ## Where should I report security issues? 8 | 9 | In order to give the community time to respond and upgrade, we strongly urge you 10 | report all security issues privately. 11 | 12 | To report a security issue in one of our Open Source projects email us directly 13 | at **oss@godaddy.com** and include the word "SECURITY" in the subject line. 14 | 15 | This mail is delivered to our Open Source Security team. 16 | 17 | After the initial reply to your report, the team will keep you informed of the 18 | progress being made towards a fix and announcement, and may ask for additional 19 | information or guidance. 20 | -------------------------------------------------------------------------------- /api/commands/help.js: -------------------------------------------------------------------------------- 1 | const { paint, stripper } = require('es-paint'); 2 | 3 | /** 4 | * Output the help information when users are exploring our CLI application. 5 | * 6 | * @param {Object} API Our command API. 7 | * @param {Object} flags CLI flags. 8 | * @public 9 | */ 10 | module.exports = async function help(API, flags) { 11 | const painter = flags.color === false ? stripper : paint; 12 | const { name, version } = require('../../package.json'); 13 | 14 | console.log(painter` 15 | ${name} (v${version}|>#00A63F) 16 | ${"Ekke-Ekke-Ekke-Ekke-PTANG. Zoo-Boing. Z' nourrwringmm..."}|>#00A63F 17 | 18 | COMMANDS: 19 | 20 | ${'run'}|>#00A63F Run the given glob of test files. 21 | ${'--port'}|>dimgray Port number that Metro Bundler should use. 22 | ${'--hostname'}|>dimgray Hostname that Metro Bundler should use. 23 | ${'--using'}|>dimgray Name of the test runner to use. 24 | ${'--watch'}|>dimgray Don't exit when the tests complete but keep listening. 25 | ${'--no-silent'}|>dimgray Do not suppress the output of Metro. 26 | ${'--require'}|>dimgray Require module (before tests are executed). 27 | ${'--reset-cache'}|>dimgray Clear the Metro cache. 28 | ${'--cache-location'}|>dimgray Change the Metro cache location. 29 | ${'help'}|>#00A63F Displays this help message. 30 | ${'--no-color'}|>dimgray Disable colors in help message. 31 | 32 | EXAMPLES: 33 | 34 | $ ${'ekke run ./test/*.test.js'}|>dimgray --using mocha 35 | `); 36 | }; 37 | -------------------------------------------------------------------------------- /api/commands/run.js: -------------------------------------------------------------------------------- 1 | const metro = require('../server'); 2 | 3 | /** 4 | * Start our servers. 5 | * 6 | * @param {Object} API Our command API. 7 | * @param {Object} flags CLI flags. 8 | * @public 9 | */ 10 | module.exports = async function run({ debug, ekke }, flags) { 11 | const { ws } = await metro(flags); 12 | 13 | ws.on('connection', (socket) => { 14 | const runner = flags.using; 15 | const opts = flags[runner] || {}; 16 | 17 | /** 18 | * Helper function providing a consistent message interface with the 19 | * Component. 20 | * 21 | * @param {String} event Name of the event. 22 | * @param {Object|Array} payload Data to transfer. 23 | * @public 24 | */ 25 | function send(event, payload) { 26 | return new Promise(function sender(resolve, reject) { 27 | socket.send(JSON.stringify({ event, payload }), function written(e) { 28 | if (e) return reject(e); 29 | 30 | resolve(); 31 | }); 32 | }); 33 | } 34 | 35 | send('run', { ...flags, opts }); 36 | socket.on('message', (message) => { 37 | let event, payload; 38 | 39 | try { 40 | ({ event, payload } = JSON.parse(message)); 41 | } catch (e) { 42 | debug('failed to decode received JSON payload', message); 43 | return; 44 | } 45 | 46 | debug('received message', message); 47 | ekke.emit(event, payload, send); 48 | 49 | if (event === 'complete') { 50 | if (payload.length) { 51 | const failure = payload[0]; 52 | 53 | // 54 | // Very specific failure, it seems like our Metro bundler 55 | // produced an error while bundling, lets output this to our 56 | // users for feedback. 57 | // 58 | if (failure && typeof failure === 'object') { 59 | if (failure.errors) { 60 | failure.errors.forEach((err) => { 61 | console.error(err.description); 62 | }); 63 | } else { 64 | console.error(failure); 65 | } 66 | } else { 67 | console.error.apply(console, payload); 68 | } 69 | 70 | if (!flags.watch) process.exit(1); 71 | } else if (!flags.watch) process.exit(0); 72 | } 73 | }); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('eventemitter3'); 2 | const diagnostics = require('diagnostics'); 3 | const cli = require('argh').argv; 4 | const os = require('os'); 5 | const path = require('path'); 6 | 7 | /** 8 | * A basic API, and CLI interface for Ekke. The only difference between 9 | * the API and CLI is that you manually need to pass in the options into 10 | * the constructor, and call our API methods. 11 | * 12 | * @constructor 13 | * @param {Object} [options] Parsed CLI arguments, or options object. 14 | * @public 15 | */ 16 | class Ekke extends EventEmitter { 17 | constructor(options = cli) { 18 | super(); 19 | 20 | const ekke = this; 21 | 22 | /** 23 | * Merge the options, with our defaults to create our API configuration. 24 | * 25 | * @returns {Object} The merged configuration. 26 | * @public 27 | */ 28 | this.config = () => ({ 29 | argv: [], 30 | ...Ekke.defaults, 31 | ...options 32 | }); 33 | 34 | this.ekke = { 35 | ekke: { 36 | ekke: { 37 | /** 38 | * If you're reading this your WTF-a-minute's must be sky rocketing 39 | * right now. Yes, I actually made an `ekke` object, with multiple 40 | * `ekke` keys, just so I could: 41 | * 42 | * ekke.ekke.ekke.ekke.PTANGZooBoingZnourrwringmm(); 43 | * 44 | * To start the CLI application. 45 | * 46 | * @public 47 | */ 48 | async PTANGZooBoingZnourrwringmm() { 49 | const flags = ekke.config(); 50 | 51 | // 52 | // Attempt to find the command we should be running. If no command or 53 | // an unknown command is given, we're just gonna assume that the user 54 | // needs help, and redirect them to our CLI help command. 55 | // 56 | // Also by using `flags.argv.shift()` we also ensure that we remove 57 | // our command name from the `argv` so the command that is called 58 | // will have a clean `flags.argv` that contains only the stuff 59 | // it needs. 60 | // 61 | let command = flags.argv.shift() || 'help'; 62 | if (typeof ekke[command] !== 'function') command = 'help'; 63 | 64 | await ekke[command](flags); 65 | } 66 | } 67 | } 68 | }; 69 | 70 | this.initialize(); 71 | } 72 | 73 | /** 74 | * Initialize our API. 75 | * 76 | * @public 77 | */ 78 | initialize() { 79 | // 80 | // Introduce our commands to the prototype. 81 | // 82 | Object.entries(Ekke.commands).forEach(([method, fn]) => { 83 | this[method] = fn.bind(this, { 84 | debug: diagnostics(`ekke:${method}`), 85 | ekke: this 86 | }); 87 | }); 88 | 89 | // 90 | // Proxy the log events to the correct console. 91 | // 92 | this.on('log', args => console.log(...args)); 93 | this.on('warn', args => console.warn(...args)); 94 | this.on('info', args => console.info(...args)); 95 | this.on('error', args => console.error(...args)); 96 | 97 | // 98 | // Generic event handling 99 | // 100 | this.on('ping', (args, send) => send('pong', args)); 101 | } 102 | } 103 | 104 | /** 105 | * The different commands that we're supporting. 106 | * 107 | * @type {Object} 108 | * @public 109 | */ 110 | Ekke.commands = { 111 | run: require('./commands/run'), 112 | help: require('./commands/help') 113 | }; 114 | 115 | /** 116 | * The default CLI flags that are being used. 117 | * 118 | * @type {Object} 119 | * @public 120 | */ 121 | Ekke.defaults = { 122 | 'hostname': 'localhost', // Hostname we should create our server upon 123 | 'port': 1975, // The port number of the created server 124 | 'silent': true, // Silence Metro bundler 125 | 'reset-cache': false, // Turn off the Metro bundler cache 126 | 'cache-location': path.join(os.tmpdir(), 'ekke-cache') // Metro bundler cacheStores location 127 | }; 128 | 129 | // 130 | // Expose our API class. 131 | // 132 | module.exports = Ekke; 133 | -------------------------------------------------------------------------------- /api/metro/aliases/empty.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /api/metro/aliases/index.js: -------------------------------------------------------------------------------- 1 | const libs = require('node-libs-react-native'); 2 | const path = require('path'); 3 | 4 | // 5 | // Provide an empty. 6 | // 7 | const empty = path.join(__dirname, 'empty.js'); 8 | module.exports = { 9 | ...libs, 10 | 11 | // 12 | // So the `node-libs-react-native` does a pretty poor job as being a polyfill 13 | // for Node.js modules as it doesn't provide good defaults and randomly 14 | // decides that some of these modules should be gone. In a future revision 15 | // we should extract this logic to a seperate module, so we don't have 16 | // to depend on `process` and `stream-browserify` our self and have 17 | // apply additional fixes to get this working. 18 | // 19 | 'fs': empty, 20 | 'child_process': empty, 21 | 'dgram': empty, 22 | 'cluster': empty, 23 | 'dns': empty, 24 | 'module': empty, 25 | 'net': empty, 26 | 'readline': empty, 27 | 'repl': empty, 28 | 'tls': empty, 29 | 'vm': empty, 30 | 'stream': require.resolve('stream-browserify'), 31 | 32 | // 33 | // We need to force mocha to the node version instead of the browser 34 | // version in order for it to compile. 35 | // 36 | 'mocha': require.resolve('mocha/lib/mocha'), 37 | 38 | // 39 | // Prevent duplicate React-Native execution, the polyfill will re-use the 40 | // existing React-Native import that is bundled with the application. This 41 | // will ensure that all NativeModules are correctly registered. 42 | // 43 | 'react-native': path.join(__dirname, 'react-native.js'), 44 | 45 | // 46 | // Needed for our own testing, we just want to require `ekke`, which 47 | // will point to the root of our repo. 48 | // 49 | 'ekke': path.join(__dirname, '..', '..', '..', 'ekke.js') 50 | }; 51 | -------------------------------------------------------------------------------- /api/metro/aliases/react-native.js: -------------------------------------------------------------------------------- 1 | const bridge = global['@ Ekke Ekke Ekke Ekke @']; 2 | const ReactNative = bridge.ReactNative; 3 | 4 | /** 5 | * Create a copy of the requireNativeComponent function as we're going 6 | * to override it when we export again. 7 | * 8 | * @type {Function} 9 | * @public 10 | */ 11 | const requireNativeComponent = ReactNative.requireNativeComponent; 12 | 13 | /** 14 | * Native components register their components using the `requireNativeComponent` 15 | * method. Unfortunately you cannot register the same component twice, and 16 | * that is what will happen when load in the JavaScript of previously bundled 17 | * native modules. So we're going to override the method, silence these 18 | * invariant errors. 19 | * 20 | * @param {String} name Unique name of the component to register. 21 | * @returns {Component} Native Component 22 | * @public 23 | */ 24 | function interceptNativeComponent(name) { 25 | try { 26 | return requireNativeComponent(name); 27 | } catch (e) { 28 | return name; 29 | } 30 | } 31 | 32 | // 33 | // All the exports of React-Native are defined using getter so they can 34 | // lazy load the components that an application requires, and insert 35 | // deprecation warnings. This also means we can't just simply override the 36 | // exports, but need to use `defineProperty` to introduce them. 37 | // 38 | Object.defineProperty(ReactNative, 'requireNativeComponent', { 39 | get: () => interceptNativeComponent 40 | }); 41 | 42 | /** 43 | * Re-Expose the React-Native that we stored in our Ekke global. 44 | * 45 | * @type {Object} 46 | * @public 47 | */ 48 | module.exports = ReactNative; 49 | -------------------------------------------------------------------------------- /api/metro/babel.js: -------------------------------------------------------------------------------- 1 | const { getCacheKey } = require('metro-react-native-babel-transformer'); 2 | const { transformSync } = require('@babel/core'); 3 | const aliases = require('./aliases'); 4 | 5 | /** 6 | * Generates all the required presets for babel transformation. 7 | * 8 | * @param {Array} existing Any presets would have existed before. 9 | * @param {Object} options Options provided to the transform. 10 | * @returns {Array} All combined presets. 11 | * @public 12 | */ 13 | function presets(existing = [], options) { 14 | const { experimentalImportSupport, ...presetOptions } = options; 15 | 16 | return [ 17 | [require('metro-react-native-babel-preset'), { 18 | ...presetOptions, 19 | 20 | disableImportExportTransform: experimentalImportSupport, 21 | enableBabelRuntime: options.enableBabelRuntime 22 | }], 23 | 24 | ...existing 25 | ]; 26 | } 27 | 28 | /** 29 | * The various of plugins that we need to make things happen. 30 | * 31 | * @param {Array} existing Any presets would have existed before. 32 | * @param {Object} options Options provided to the transform. 33 | * @returns {Array} All combined presets. 34 | * @public 35 | */ 36 | function plugins(existing = [], options) { 37 | const optional = []; 38 | 39 | if (options.inlineRequires) { 40 | optional.push(require('babel-preset-fbjs/plugins/inline-requires')); 41 | } 42 | 43 | return [ 44 | [require('babel-plugin-rewrite-require'), { 45 | throwForNonStringLiteral: true, 46 | aliases 47 | }], 48 | 49 | ...optional, 50 | ...existing 51 | ]; 52 | } 53 | 54 | /** 55 | * A custom babel transformer, this gives us finegrain control over the 56 | * transformation process, and more power than some of the metro options 57 | * that are provided. 58 | * 59 | * @param {Object} transformOptions Our transformer options. 60 | * @returns {Object} Our transformed AST. 61 | * @public 62 | */ 63 | function transform(transformOptions) { 64 | const { filename, options, src } = transformOptions; 65 | 66 | // 67 | // Development builds are controlled by the `options` that are specified 68 | // by the metro bundler, not using env variables that a user might has set 69 | // so we need to ensure that we nuke their babel-env and force it to the 70 | // right execution env. 71 | // 72 | const old = process.env.BABEL_ENV; 73 | process.env.BABEL_ENV = options.dev ? 'development' : old || 'production'; 74 | 75 | try { 76 | const result = transformSync(src, { 77 | caller: { 78 | name: 'metro', 79 | platform: options.platform 80 | }, 81 | ast: true, 82 | babelrc: !!options.enableBabelRCLookup, 83 | code: false, 84 | highlightCode: true, 85 | filename: filename, 86 | presets: presets([], options), 87 | plugins: plugins(transformOptions.plugins, options), 88 | sourceType: 'unambiguous' 89 | }); 90 | 91 | // 92 | // When a file is ignored, it doesn't return a result, so we need to 93 | // optionally return the transformed AST. 94 | // 95 | return result 96 | ? { ast: result.ast } 97 | : { ast: null }; 98 | } finally { 99 | process.env.BABEL_ENV = old; 100 | } 101 | } 102 | 103 | // 104 | // Expose the transform method. 105 | // 106 | module.exports = { 107 | getCacheKey, 108 | transform, 109 | presets, 110 | plugins 111 | }; 112 | -------------------------------------------------------------------------------- /api/metro/configure.js: -------------------------------------------------------------------------------- 1 | const { mergeConfig, loadConfig } = require('metro-config'); 2 | const { FileStore } = require('metro-cache'); 3 | const resolve = require('metro-resolver').resolve; 4 | const diagnostics = require('diagnostics'); 5 | const source = require('./source'); 6 | const path = require('path'); 7 | 8 | // 9 | // Debug logger. 10 | // 11 | const debug = diagnostics('ekke:configure'); 12 | 13 | /** 14 | * Generate the contents of the metro.config that should be used to build 15 | * the tests. 16 | * 17 | * @param {Object} flags The configuration flags of the API/CLI. 18 | * @returns {Promise} configuration. 19 | * @public 20 | */ 21 | async function configure(flags) { 22 | const reactNativePath = path.dirname(require.resolve('react-native/package.json')); 23 | const config = await loadConfig(); 24 | const custom = { 25 | resolver: {}, 26 | serializer: {}, 27 | transformer: {}, 28 | cacheStores: [ 29 | new FileStore({ 30 | root: flags['cache-location'] 31 | }) 32 | ] 33 | }; 34 | 35 | // 36 | // We need to create a fake package name that we will point to the root 37 | // of the users directory so we can resolve their requires and test files 38 | // without having to rely on `package.json` based resolve due to poor 39 | // handling of absolute and relative paths. 40 | // 41 | // See: https://github.com/facebook/react-native/issues/3099 42 | // 43 | const fake = 'ekke-ekke-ekke-ekke'; 44 | 45 | // 46 | // Check if we're asked to nuke the cache, we should. This option will 47 | // be your next best friend. 48 | // 49 | custom.resetCache = !!flags['reset-cache']; 50 | 51 | // 52 | // Prevent the Metro bundler from outputting to the console by using a 53 | // custom logger that redirects all output to our debug utility. We want 54 | // to minimize the output so the test responses can be piped to other 55 | // processes in the case of tap based test runners. 56 | // 57 | if (flags.silent) custom.reporter = { 58 | update: function log(data) { 59 | debug('status:', data); 60 | } 61 | }; 62 | 63 | // 64 | // We want to polyfill the `util`, `http` and other standard Node.js 65 | // libraries so any Node.js based code has at least a chance to get 66 | // bundled without throwing any errors. The `extraNodeModules` option 67 | // gets us there, but it doesn't work for all transforms we need to 68 | // make, so we need to go deeper, and solve it as an AST level using 69 | // babel and that requires our own custom transformer. 70 | // 71 | custom.transformer.babelTransformerPath = path.join(__dirname, 'babel.js'); 72 | 73 | custom.resolver.extraNodeModules = { 74 | [fake]: process.cwd() 75 | }; 76 | 77 | // 78 | // Mother of all hacks, we don't have a single entry point, we have multiple 79 | // as our test files are represented in a glob, that can point to an infinite 80 | // number of modules. 81 | // 82 | // Unfortunately, there isn't an API or config method available in Metro 83 | // that will allow us to inject files that need to be included AND resolve 84 | // those files dependencies. The only thing that comes close is the 85 | // `serializer.polyfillModuleNames` which will include those files, but 86 | // not their dependencies. 87 | // 88 | // That leaves us with this horrible alternative, creating a tmp.js file 89 | // with absolute paths to the files we want to import. So we're going to 90 | // intercept the `package.json` bundle operation and return the location of 91 | // our temp file so that will be required instead of the actual contents of 92 | // the `package.json` file. But then I realized, we can just put anything 93 | // we want in the URL, and it would end up here. So let's go with something 94 | // that fit our theme. 95 | // 96 | const filePath = await source({ 97 | globs: [].concat(flags.require).concat(flags.argv), 98 | runner: flags.using, 99 | fake 100 | }); 101 | 102 | custom.resolver.resolveRequest = function resolveRequest(context, file, platform) { 103 | if (file === './Ekke-Ekke-Ekke-Ekke-PTANG.Zoo-Boing.Znourrwringmm') return { 104 | type: 'sourceFile', 105 | filePath 106 | }; 107 | 108 | // 109 | // We only wanted to abuse the `resolveRequest` method to intercept a 110 | // give request and point it to a completely different, unknown file. The 111 | // last thing we've wanted to do the actual resolving, so we're going to 112 | // rely on the `metro-resolver` module for that.. 113 | // 114 | // Unfortunately, there's an issue, as we've set a `resolveRequest` 115 | // function the resolver function thinks it needs to use that resolver to 116 | // resolve the file, leading to an infinite loop, pain, suffering. So 117 | // before we can continue, we need to check if we should remove our 118 | // self from the context so the resolver works as intended again. 119 | // 120 | if (context.resolveRequest === resolveRequest) { 121 | context.resolveRequest = null; 122 | } 123 | 124 | debug(`resovling file(${file})`); 125 | return resolve(context, file, platform); 126 | }; 127 | 128 | // 129 | // It's important to understand that while Metro is branded as the JavaScript 130 | // bundler for React-Native, it's not configured out of the box to support 131 | // React-Native, all this configuration work is done by the CLI. 132 | // 133 | // That's where package resolving, and custom metro.config creation 134 | // is happening. The following fields are important for this: 135 | // 136 | // - Instructs Metro that React-Native uses a non-standard require system. 137 | // - Tell Metro to use the non-standard hasteImpl map that ships in React-Native 138 | // so it can process the @providesModule statements without blowing up on 139 | // warnOnce, or missing invariant modules. 140 | // - Instruct it to also look, and honor the dedicated `react-native` 141 | // field in package.json's 142 | // - And point to the correct AssetRegistry, also hidden in the React-Native 143 | // module. 144 | // 145 | // The `providesModuleNodeModules` and `hasteImplModulePath` are currently 146 | // not needed to correctly configure metro for Ekke as we're replacing 147 | // `react-native` with polyfill, but if we for some reason turn this off, 148 | // we don't want to research the undocumented codebase of Metro, cli, and 149 | // React-Native again to figure out how to correctly resolve and bundle 150 | // React-Native. 151 | // 152 | custom.resolver.providesModuleNodeModules = ['react-native']; 153 | custom.resolver.hasteImplModulePath = path.join(reactNativePath, 'jest/hasteImpl'); 154 | custom.resolver.resolverMainFields = ['react-native', 'browser', 'main']; 155 | custom.transformer.assetRegistryPath = path.join(reactNativePath, 'Libraries/Image/AssetRegistry'); 156 | 157 | const merged = mergeConfig(config, custom); 158 | debug('metro config', merged); 159 | 160 | return merged; 161 | } 162 | 163 | module.exports = configure; 164 | -------------------------------------------------------------------------------- /api/metro/source.js: -------------------------------------------------------------------------------- 1 | const diagnostics = require('diagnostics'); 2 | const crypto = require('crypto'); 3 | const glob = require('glob'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | 7 | // 8 | // Dedicated debugger. 9 | // 10 | const debug = diagnostics('ekke:source'); 11 | 12 | /** 13 | * Generates source file that contains all the required imports. 14 | * 15 | * @param {String[]} [globs] The glob patterns that gather the files. 16 | * @param {String} fake Fake module name of the application root. 17 | * @param {String} runner The test runner that needs to be bundled. 18 | * @returns {Promise} Location of our fake file. 19 | * @public 20 | */ 21 | async function source({ globs = [], fake, runner = 'mocha' }) { 22 | const files = []; 23 | 24 | // 25 | // It could be that we've gotten multiple glob patterns, so we need to 26 | // iterate over each. 27 | // 28 | globs.filter(Boolean).forEach(function find(file) { 29 | if (!~file.indexOf('*')) return files.push(file); 30 | 31 | Array.prototype.push.apply(files, glob.sync(file)); 32 | }); 33 | 34 | // 35 | // Map the require files to the fake package name we've created. 36 | // 37 | const requires = files 38 | .map(file => path.join(fake, file)) 39 | .map(file => file.replace(/\\/g, '\\\\')) 40 | .map(file => `require("${file}");`) 41 | .join('\n'); 42 | 43 | // 44 | // Create dummy content for the source which allows to get access to: 45 | // 46 | // - The selected test runner library, as that needs to be executed in RN. 47 | // - The suites. 48 | // 49 | // All neatly exported when we require the first moduleId, 0. 50 | // 51 | const template = fs.readFileSync(path.join(__dirname, 'template.js'), 'utf-8'); 52 | const content = template 53 | .replace('${requires}', requires) 54 | .replace('${runner}', runner) 55 | .replace('${browser}', runner === 'mocha') 56 | .replace('${__dirname}', process.cwd()); 57 | 58 | debug('compiled template', content); 59 | 60 | const ref = `${crypto.createHash('sha256').update(content).digest('hex')}.js`; 61 | const location = path.join(__dirname, '..', 'tmp', ref); 62 | 63 | debug(`generating fake source file at(${location})`, content); 64 | fs.writeFileSync(location, content); 65 | 66 | return location; 67 | } 68 | 69 | // 70 | // Expose our hack. 71 | // 72 | module.exports = source; 73 | -------------------------------------------------------------------------------- /api/metro/template.js: -------------------------------------------------------------------------------- 1 | // 2 | // The contents of this file will be replaced by the `source.js` file. 3 | // 4 | const env = process.env; 5 | const processed = require('process'); 6 | const Buffer = require('buffer').Buffer; 7 | const EventEmitter = require('eventemitter3'); 8 | 9 | // 10 | // Unfortunately the `process` polyfill, is lacking some features that 11 | // our tests usually depend upon, so we need to polyfill, our polyfill B). 12 | // 13 | processed.env = env; 14 | processed.browser = '${browser}' === 'true'; 15 | 16 | const events = new EventEmitter(); 17 | 18 | Object.keys(EventEmitter.prototype).forEach(function polyfill(key) { 19 | if (typeof events[key] !== 'function') return; 20 | 21 | processed[key] = events[key].bind(events); 22 | }); 23 | 24 | // 25 | // We want to intercept process.exit, we can act on those events. 26 | // 27 | processed.exit = events.emit.bind(events, 'process.exit'); 28 | 29 | // 30 | // Expose the libraries as globals so it's polifylled everywhere before we 31 | // require our actual test runners. 32 | // 33 | global.Buffer = Buffer; 34 | global.process = processed; 35 | 36 | global.__dirname = '${__dirname}'; 37 | global.__filename = 'not supported'; 38 | 39 | module.exports = { 40 | runner: require('${runner}'), 41 | suites: function suites() { 42 | ${requires} 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /api/server.js: -------------------------------------------------------------------------------- 1 | const configure = require('./metro/configure'); 2 | const WebSocketServer = require('ws').Server; 3 | const Metro = require('metro'); 4 | 5 | /** 6 | * Create our server infrastructure. 7 | * 8 | * @param {object} data Our API/CLI flags. 9 | * @returns {Promise} WebSocket and metro bundler 10 | * @public 11 | */ 12 | module.exports = async function metro(data) { 13 | const config = await configure(data); 14 | 15 | // 16 | // Metro inconsistencies: 17 | // 18 | // You can configure the `host` through the server config object, but not 19 | // the port number. They are extracting that from `config.server` object. 20 | // To provide consistency in the configuration we're going to forcefully 21 | // override the config.server with our own config data: 22 | // 23 | config.server.port = data.port; 24 | config.server.enableVisualizer = false; 25 | config.server.runInspectorProxy = false; 26 | 27 | const server = await Metro.runServer(config, { 28 | hmrEnabled: false, 29 | host: data.hostname 30 | }); 31 | 32 | // 33 | // Ideally we want to piggyback the WebSocket servers that Metro creates, 34 | // but unfortunately there is no way to get into their instances, and they 35 | // don't listen to responses from the server, which is something that we 36 | // need in order to stream back the test results. 37 | // 38 | const ws = new WebSocketServer({ 39 | path: '/ekke', 40 | server 41 | }); 42 | 43 | return { ws, server }; 44 | }; 45 | -------------------------------------------------------------------------------- /api/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godaddy/ekke/fe952a456f84d83db7578ee5a13525ef3cb391dd/api/tmp/.gitkeep -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ekke", 3 | "displayName": "ekke" 4 | } 5 | -------------------------------------------------------------------------------- /bin/ekke: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Ekke = require('../api'); 4 | const ekke = new Ekke(); 5 | 6 | ekke.ekke.ekke.ekke.PTANGZooBoingZnourrwringmm(); 7 | -------------------------------------------------------------------------------- /components/ekke.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import diagnostics from 'diagnostics'; 3 | import PropTypes from 'prop-types'; 4 | import { Component } from 'react'; 5 | import once from 'one-time'; 6 | 7 | // 8 | // Our dedicated component logger. 9 | // 10 | const debug = diagnostics('ekke:component'); 11 | 12 | /** 13 | * HOC: Create our Ekke component that will extract the rootTag id that was 14 | * used to render the users application, in addition to that we will extract 15 | * all the props that we applied to the component and use those as configuration 16 | * values. 17 | * 18 | * @param {Function} extract Function to call once we've obtained the rootTag. 19 | * @returns {Component} The Ekke component. 20 | * @public 21 | */ 22 | function createEkke(extract) { 23 | const expose = once(extract); 24 | 25 | /** 26 | * Story time children: 27 | * 28 | * We want Ekke to have the ability to render components in the application 29 | * so we can assert against it. We use the `AppRegistry` API to render 30 | * a different application, but this API _needs_ to have the `rootTag` 31 | * to render the app. There is no actual API inside React-Native to retrieve 32 | * this information. This specific information is however available by the 33 | * React AppContainer which wraps _every_ rendered React-Native Application 34 | * and provides this information through the legacy context API. 35 | * 36 | * So in order to get access to this information, we need to have a React 37 | * component, that uses the legacy API to extract the `rootTag` from the 38 | * `this.context` API, so we can pass it our `AppRegistry` orchestration. 39 | * And this is exactly what this component is doing. 40 | * 41 | * @constructor 42 | * @public 43 | */ 44 | class Ekke extends Component { 45 | render() { 46 | const root = this.context.rootTag; 47 | 48 | // 49 | // Additional check, for when React-Native will migrate away from the 50 | // old context API, because then this code will no longer work as 51 | // intended and most likely will end up as an undefined. 52 | // 53 | // When that happens we will rather not execute our test suite because 54 | // there will be errors everywhere. 55 | // 56 | if (typeof root === 'number') { 57 | debug('found root tag, getting ready to initialize...'); 58 | expose(root, { ...this.props }); 59 | } else { 60 | debug('the rootTag context is no longer a number, please report this bug'); 61 | } 62 | 63 | return this.props.children || null; 64 | } 65 | } 66 | 67 | /** 68 | * Specify which pieces of data we want to extract from the context API. 69 | * 70 | * @type {Object} 71 | * @public 72 | */ 73 | Ekke.contextTypes = { 74 | rootTag: PropTypes.number 75 | }; 76 | 77 | /** 78 | * The propTyes that get specified on our Ekke component are used our 79 | * primary source of configuration values. 80 | * 81 | * @type {Object} 82 | * @public 83 | */ 84 | Ekke.propTypes = { 85 | hostname: PropTypes.string.isRequired, 86 | port: PropTypes.number.isRequired, 87 | children: PropTypes.node, 88 | interval: PropTypes.string, 89 | alive: PropTypes.func, 90 | 91 | // 92 | // This is an internal property and is hidden behind the 3 sacred words: 93 | // "Ni," "Peng," and "Nee-wom"! 94 | // 95 | // It should *not* be documented and consumed by the general public. 96 | // 97 | NiPengNeeWom: PropTypes.func 98 | }; 99 | 100 | /** 101 | * Our default props, as we use the props as configuration values, 102 | * these are actually our default configuration values. 103 | * 104 | * @type {Object} 105 | * @public 106 | */ 107 | Ekke.defaultProps = { 108 | interval: '10 seconds', 109 | hostname: Platform.OS === 'android' ? '10.0.2.2' : 'localhost', 110 | port: 1975 111 | }; 112 | 113 | return Ekke; 114 | } 115 | 116 | export { 117 | createEkke as default, 118 | createEkke 119 | }; 120 | -------------------------------------------------------------------------------- /components/loading.js: -------------------------------------------------------------------------------- 1 | import { version, name } from '../package.json'; 2 | import { View, Text } from 'react-native'; 3 | import React, { Component } from 'react'; 4 | import references from 'references'; 5 | import PropTypes from 'prop-types'; 6 | 7 | const XD = { 8 | ROOT: { 9 | backgroundColor: 'black', 10 | justifyContent: 'center', 11 | alignItems: 'center', 12 | position: 'relative', 13 | flex: 1 14 | }, 15 | GRD: { 16 | position: 'absolute', 17 | fontFamily: 'Courier New', 18 | top: 0, 19 | color: 'rgba(255, 255, 255, .3)' 20 | }, 21 | TXT: { 22 | color: 'white', 23 | fontWeight: 'bold', 24 | textTransform: 'uppercase' 25 | }, 26 | BOX: { 27 | backgroundColor: '#00A63F', 28 | padding: 15 29 | }, 30 | V: { 31 | fontWeight: 'bold', 32 | color: 'rgba(0, 0, 0, .6)' 33 | } 34 | }; 35 | 36 | /** 37 | * Simple broken grid generator that somehow generates interesting 38 | * background images during render. 39 | * 40 | * @param {Object} schema Some params to tweak stuff that we never touch. 41 | * @returns {String} Some ASCII pattern. 42 | * @public 43 | */ 44 | function background({ corner = '+', spaces = 4, blocks = 12, rng = 7 } = {}) { 45 | const glitches = '▒░~%°*▓#@,`.▘'; 46 | const divider = spaces + 1; 47 | const dot = '·'; 48 | let bg = ''; 49 | 50 | /** 51 | * Generates the tile for our background. 52 | * 53 | * @returns {String} The background glitch to use. 54 | * @private 55 | */ 56 | function tile() { 57 | if (Math.floor(Math.random() * rng) + 1 !== 7) return dot; 58 | 59 | return glitches.charAt(Math.floor(Math.random() * glitches.length)); 60 | } 61 | 62 | for (let r = 0; r < blocks * spaces; r++) { 63 | for (let b = 0; b < blocks; b++) { 64 | bg += `${r % divider ? dot : corner} `; 65 | 66 | for (let s = 0; s < spaces; s++) { 67 | bg += `${tile()} `; 68 | } 69 | } 70 | bg += `${r % divider ? dot : corner}\n`; 71 | } 72 | 73 | return bg; 74 | } 75 | 76 | /** 77 | * A default loading screen for when our Ekke tests are running. 78 | * 79 | * @returns {Component} Our full screen loading component. 80 | * @public 81 | */ 82 | class Loading extends Component { 83 | constructor() { 84 | super(...arguments); 85 | 86 | this.references = references(); 87 | this.interval = null; 88 | this.state = { 89 | grd: background() 90 | }; 91 | } 92 | 93 | /** 94 | * Waiting for tests is boring, we don't really have the time to built in 95 | * a basic game like snake, space invaders, developer quiz, and what not. 96 | * If you read this feel free to come up with some crazy, open for PR's. 97 | * 98 | * So for now we're just going to animate the background by re-generating 99 | * our art on interval. 100 | * 101 | * @private 102 | */ 103 | componentDidMount() { 104 | this.interval = setInterval( 105 | function again() { 106 | this.setState({ grd: background() }); 107 | }.bind(this), 108 | this.props.interval 109 | ); 110 | } 111 | 112 | /** 113 | * Clear our component when it gets unmounted. 114 | * 115 | * @private 116 | */ 117 | componentWillUnmount() { 118 | clearInterval(this.interval); 119 | } 120 | 121 | /** 122 | * Render our loading screen while the tests are being activated. 123 | * 124 | * @returns {Component} Our default screen. 125 | * @public 126 | */ 127 | render() { 128 | const ref = this.references.create; 129 | 130 | return ( 131 | 132 | {this.state.grd} 133 | 134 | 135 | {name} 136 | {version} 137 | 138 | 139 | ); 140 | } 141 | } 142 | 143 | /** 144 | * PropType validation.. 145 | * 146 | * @type {Object} 147 | * @public 148 | */ 149 | Loading.propTypes = { 150 | interval: PropTypes.number 151 | }; 152 | 153 | /** 154 | * Default props. 155 | * 156 | * @type {Object} 157 | * @public 158 | */ 159 | Loading.defaultProps = { 160 | interval: 1500 161 | }; 162 | 163 | export { 164 | Loading as default 165 | }; 166 | -------------------------------------------------------------------------------- /components/renderer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Loading from './loading'; 3 | 4 | /** 5 | * @callback ReadyFn 6 | * @param {Component.prototype.setState} setState Bound setState function of the component. 7 | */ 8 | /** 9 | * Create a new renderer which will be used as new application root. 10 | * 11 | * @param {ReadyFn} ready Called when our component is ready for changes. 12 | * @returns {Component} Renderer. 13 | * @public 14 | */ 15 | function createRenderer(ready) { 16 | return class Renderer extends Component { 17 | constructor() { 18 | super(...arguments); 19 | // 20 | // Use state so we can dynamically change the views we are rendering. 21 | // 22 | this.state = { 23 | component: 24 | }; 25 | } 26 | 27 | /** 28 | * Call our initializer with our setState method so we can process 29 | * layout changes. 30 | * 31 | * @public 32 | */ 33 | componentDidMount() { 34 | ready(this.setState.bind(this)); 35 | } 36 | 37 | /** 38 | * Renders the actual component. 39 | * 40 | * @returns {Component} A view, with the component to render. 41 | * @public 42 | */ 43 | render() { 44 | return {this.state.component}; 45 | } 46 | }; 47 | } 48 | 49 | export { 50 | createRenderer as default, 51 | createRenderer 52 | }; 53 | -------------------------------------------------------------------------------- /development.js: -------------------------------------------------------------------------------- 1 | import { render } from './native/bridge'; 2 | import create from './components/ekke'; 3 | import Subway from './native/subway'; 4 | import Screen from './native/screen'; 5 | import Mocha from './runners/mocha'; 6 | import Tape from './runners/tape'; 7 | 8 | // 9 | // The different Runners that we support. 10 | // 11 | const RUNNERS = { 12 | mocha: Mocha, 13 | tape: Tape 14 | }; 15 | 16 | /** 17 | * Create our Ekke component that allows us to intercept the rootTag 18 | * from the application. 19 | * 20 | * @type {Component} 21 | * @public 22 | */ 23 | const Ekke = create(async function mounted(rootTag, props = {}) { 24 | // 25 | // When we want to test Ekke, in Ekke, but we don't want to execute tests 26 | // again while our tests are running. 27 | // 28 | if (typeof props.NiPengNeeWom === 'function') { 29 | return props.NiPengNeeWom(...arguments); 30 | } 31 | 32 | const screen = new Screen(rootTag); 33 | const subway = new Subway(props.hostname, props.port); 34 | 35 | subway.on('run', function run({ using = 'mocha', opts = {} }) { 36 | const Runner = props.Runner || RUNNERS[using]; 37 | const runner = new Runner({ 38 | config: { ...props, ...opts }, 39 | subway, 40 | screen 41 | }); 42 | }); 43 | 44 | // 45 | // Everything is setup correctly, we can now wait for the service to 46 | // become alive. 47 | // 48 | subway.alive(function alive() { 49 | if (props.alive) props.alive(); 50 | }, props.interval); 51 | }); 52 | 53 | // 54 | // Indication of which build is loaded. 55 | // 56 | Ekke.prod = false; 57 | Ekke.dev = true; 58 | 59 | export { 60 | Ekke as default, 61 | Ekke, 62 | render 63 | }; 64 | -------------------------------------------------------------------------------- /docs/ekke-react-native-intro.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/godaddy/ekke/fe952a456f84d83db7578ee5a13525ef3cb391dd/docs/ekke-react-native-intro.gif -------------------------------------------------------------------------------- /ekke.js: -------------------------------------------------------------------------------- 1 | // 2 | // Select the correct build version depending on the environment. 3 | // 4 | if (process.env.NODE_ENV === 'production') { 5 | module.exports = require('./production.js'); 6 | } else { 7 | module.exports = require('./development.js'); 8 | } 9 | -------------------------------------------------------------------------------- /examples/example.mocha.js: -------------------------------------------------------------------------------- 1 | import { AsyncStorage, View } from 'react-native'; 2 | import { describe, it } from 'mocha'; 3 | import { render } from 'ekke'; 4 | import assume from 'assume'; 5 | import React from 'react'; 6 | import chai from 'chai'; 7 | 8 | describe('(example) test suite', function () { 9 | this.timeout(15000); 10 | 11 | /** 12 | * A simple delay function so we can pause for a few ms. 13 | * 14 | * @param {Number} time Timeout in milliseconds 15 | * @returns {Promise} Our timeout. 16 | * @public 17 | */ 18 | function delay(time) { 19 | return new Promise(resolve => { 20 | setTimeout(resolve, time); 21 | }); 22 | } 23 | 24 | it('runs', function () { 25 | assume(describe).is.a('function'); 26 | assume(it).is.a('function'); 27 | 28 | // 29 | // Check if all globals are correctly imported. 30 | // 31 | assume(after).is.a('function'); 32 | assume(before).is.a('function'); 33 | assume(afterEach).is.a('function'); 34 | assume(beforeEach).is.a('function'); 35 | }); 36 | 37 | it('runs an async function', function (next) { 38 | setTimeout(function () { 39 | next(); 40 | }, 1500); 41 | }); 42 | 43 | it('runs an async await', async function () { 44 | await delay(1500); 45 | }); 46 | 47 | describe('#render', function () { 48 | it('is a render function', function () { 49 | assume(render).is.a('function'); 50 | }); 51 | 52 | it('renders a massive red square', async function () { 53 | const ref = React.createRef(); 54 | const square = ( 55 | 59 | ); 60 | 61 | await render(square); 62 | assume(ref.current).is.a('object'); 63 | 64 | // 65 | // This delay is not needed, but our rendering and test suite is so 66 | // quick that you barely see it happen on screen, so we give you 1500ms 67 | // see our amazing red square. 68 | // 69 | await delay(1500); 70 | }); 71 | }); 72 | 73 | describe('React-Native', function () { 74 | describe('AsyncStorage', function () { 75 | it('set and gets the data', async function () { 76 | await AsyncStorage.setItem('testing', 'values'); 77 | 78 | assume(await AsyncStorage.getItem('testing')).equals('values'); 79 | }); 80 | 81 | it('can read the previous stored data', async function () { 82 | assume(await AsyncStorage.getItem('testing')).equals('values'); 83 | }); 84 | 85 | it('can remove the previous stored data', async function () { 86 | assume(await AsyncStorage.getItem('testing')).equals('values'); 87 | 88 | await AsyncStorage.removeItem('testing'); 89 | assume(await AsyncStorage.getItem('testing')).is.a('null'); 90 | }); 91 | }); 92 | }); 93 | 94 | describe('chai', function () { 95 | it('works with the expect syntax', function () { 96 | const expect = chai.expect; 97 | const foo = 'bar'; 98 | 99 | expect(foo).to.be.a('string'); 100 | expect(foo).to.equal('bar'); 101 | expect(foo).to.have.lengthOf(3); 102 | }); 103 | 104 | it('works with assert syntax', function () { 105 | const assert = chai.assert; 106 | const foo = 'bar'; 107 | 108 | assert.typeOf(foo, 'string'); 109 | assert.equal(foo, 'bar'); 110 | assert.lengthOf(foo, 3); 111 | }); 112 | 113 | it('works with the should() syntax', function () { 114 | chai.should(); 115 | const foo = 'bar'; 116 | 117 | foo.should.be.a('string'); 118 | foo.should.equal('bar'); 119 | foo.should.have.lengthOf(3); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /examples/example.tape.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | 3 | test('one', function (t) { 4 | t.plan(2); 5 | t.ok(true); 6 | 7 | setTimeout(function () { 8 | t.equal(1 + 3, 4); 9 | }, 100); 10 | }); 11 | 12 | test('two', function (t) { 13 | t.plan(3); 14 | t.equal(5, 2 + 3); 15 | 16 | setTimeout(function () { 17 | t.equal('a'.charCodeAt(0), 97); 18 | t.ok(true); 19 | }, 50); 20 | }); 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { Text, View, AppRegistry } from 'react-native'; 2 | import React, { Component, Fragment } from 'react'; 3 | import { Ekke } from './ekke'; 4 | 5 | const styles = { 6 | container: { 7 | flex: 1, 8 | justifyContent: 'center', 9 | alignItems: 'center', 10 | backgroundColor: '#F5FCFF' 11 | }, 12 | welcome: { 13 | fontSize: 20, 14 | textAlign: 'center', 15 | margin: 10 16 | } 17 | }; 18 | 19 | class App extends Component { 20 | render() { 21 | return ( 22 | 23 | 24 | 25 | 26 | Welcome to React Native! 27 | 28 | 29 | ); 30 | } 31 | } 32 | 33 | AppRegistry.registerComponent('ekke', () => App); 34 | -------------------------------------------------------------------------------- /native/bridge.js: -------------------------------------------------------------------------------- 1 | import * as ReactNative from 'react-native'; 2 | import EventEmitter from 'eventemitter3'; 3 | 4 | // 5 | // Globals are bad, disgusting, but in this case, a necessary evil, a way 6 | // to communicate, with the new and old, the tests and the runner. 7 | // 8 | // If you read this and think, oh golly, this is a good idea, then don't, 9 | // it's not. Go back, close this file. Read a book on how to properly write 10 | // JavaScript. Because this is not it. 11 | // 12 | 13 | /** 14 | * In order to prevent potential clashes with existing globals, and globals 15 | * that accidentally get introduced using code, we need to use a key that 16 | * cannot be created as a variable. 17 | * 18 | * Using @ as prefix/suffix seems to be solution here, including the use of 19 | * spaces to cover most the use-cases mentioned above. 20 | * 21 | * @note Make sure that this key is synced it our React-Native polyfill @ CLI 22 | * @type {String} 23 | * @private 24 | */ 25 | const key = '@ Ekke Ekke Ekke Ekke @'; 26 | 27 | // 28 | // Reason 1, We need globals for Ekke: 29 | // 30 | // This scope is what will be included in the React-Native application that 31 | // includes our component, so we can execute code, the tests, and 32 | // assert their interaction with the environment they are exposed in. But 33 | // they will be executing in their own scope, with no prior knowledge of 34 | // this executing scope. 35 | // 36 | // This global allows us to include the Ekke library again in tests, so 37 | // when they execute, they will execute this code and see that bridge 38 | // has already been established, and not override it. Allowing this 39 | // single EventEmitter, to be a bridge between both execution context. 40 | // 41 | // 42 | const bridge = (global[key] = global[key] || new EventEmitter()); 43 | 44 | // 45 | // Reason 2, We can't have duplicate React-Native includes. 46 | // 47 | // React-Native's native modules are registered in the modules 48 | // 49 | bridge.ReactNative = ReactNative; 50 | 51 | /** 52 | * Transfers a given React.createElement over the bridge so it can 53 | * render it on screen during our tests. 54 | * 55 | * @param {Object} component The result of React.createElement () 56 | * @returns {Promise} Callback when the component is mounted. 57 | * @public 58 | */ 59 | function render(component) { 60 | return new Promise(function delay(resolve, reject) { 61 | bridge.emit('render', component, { 62 | resolve, 63 | reject 64 | }); 65 | }); 66 | } 67 | 68 | export { 69 | bridge as default, 70 | render, 71 | bridge 72 | }; 73 | -------------------------------------------------------------------------------- /native/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The different readyStates our runner, WebSocket connection and what not 3 | * could have. 4 | * 5 | * @type {Object} 6 | * @public 7 | */ 8 | const READYSTATE = { 9 | READY: 1, 10 | CLOSED: 2, 11 | LOADING: 3 12 | }; 13 | 14 | export { 15 | READYSTATE 16 | }; 17 | -------------------------------------------------------------------------------- /native/evaluator.js: -------------------------------------------------------------------------------- 1 | import diagnostics from 'diagnostics'; 2 | 3 | // 4 | // Create our debug instance. 5 | // 6 | const debug = diagnostics('ekke:evaluate'); 7 | 8 | /** 9 | * The evaluator allows us to maintain a history of scopes that we've 10 | * created in the app, until we figure a way to "un-evaluate". 11 | * 12 | * @constructor 13 | * @param {Subway} subway Our Metro bundler interface. 14 | * @public 15 | */ 16 | class Evaluator { 17 | constructor(subway) { 18 | this.subway = subway; 19 | this.scopes = [this.scope()]; 20 | 21 | // 22 | // Create a bunch of proxy methods for easy access to the latest scope. 23 | // 24 | Object.keys(Evaluator.externals).forEach(method => { 25 | this[method] = function proxy(...args) { 26 | return this.scopes[this.scopes.length - 1][method](...args); 27 | }.bind(this); 28 | }); 29 | } 30 | 31 | /** 32 | * Create a local, "global" for the code to execute in. It's worth noting 33 | * this of course, will not prevent lookup against actual global 34 | * variables, but only those who reference `this`, `window`, and `global` 35 | * giving a minimum layer of protection. 36 | * 37 | * But a minimum layer is still better than no layer at all. 38 | * 39 | * @returns {Object} The global. 40 | * @public 41 | */ 42 | local() { 43 | const sandbox = Object.create(null); 44 | 45 | // 46 | // React-Native does introduce a couple of globals, lets make sure we mimic 47 | // those in our local, global. 48 | // 49 | // See https://github.com/facebook/react-native/tree/df2eaa9eb69a4b79533e663fd26e8896c0089530/Libraries/Core 50 | // 51 | sandbox.process = { ...process, env: { ...process.env } }; 52 | sandbox.nativeRequire = global.nativeRequire; 53 | sandbox.navigator = { ...global.navigator }; 54 | sandbox.__DEV__ = __DEV__; 55 | sandbox.window = sandbox; 56 | sandbox.GLOBAL = sandbox; 57 | sandbox.global = sandbox; 58 | 59 | return sandbox; 60 | } 61 | 62 | /** 63 | * Create a SandBox, Proxy, so if it's not in the provided target, we can 64 | * fallback to our global. 65 | * 66 | * @param {Object} box The sandbox which should be seen as primary 67 | * @param {Object} fallback The object to fallback to when it's not in the box. 68 | * @returns {Proxy} Our sandbox proxy. 69 | * @public 70 | */ 71 | sandproxy(box = {}, fallback = global) { 72 | return new Proxy(box, { 73 | get(target, name) { 74 | if (name in target) { 75 | debug(`local read(${name})`); 76 | return target[name]; 77 | } 78 | 79 | debug(`global read(${name})`); 80 | return fallback[name]; 81 | }, 82 | 83 | set(target, name, value) { 84 | if (name in target) debug(`local write(${name})`); 85 | else debug(`global write(${name})`); 86 | 87 | target[name] = value; 88 | return value; 89 | }, 90 | 91 | has(target, name) { 92 | return name in target || name in fallback; 93 | } 94 | }); 95 | } 96 | 97 | /** 98 | * Download and execute the bundle. 99 | * 100 | * @public 101 | */ 102 | async download() { 103 | return await this.subway.bundle({ 104 | entry: this.subway.entry, 105 | 106 | // 107 | // As we're using `eval` to evaluate the code we want to make sure that 108 | // the evaluated code has the correct line numbers, so we need to inline 109 | // the sourcemaps. 110 | // 111 | inlineSourceMap: true, 112 | 113 | // 114 | // runModule, nope, we want absolute control over the execution so we 115 | // do not want to run the modules automatically when it's evaluated 116 | // 117 | runModule: false 118 | }); 119 | } 120 | 121 | /** 122 | * Create a sandbox for the code to execute in so we don't have to worry 123 | * that the test suite will actually kill any of the globals that the 124 | * app might be depending on. 125 | * 126 | * @param {String} [bundle] The bundle to compile. 127 | * @returns {Function} The compiled bundle, ready for execution. 128 | * @public 129 | */ 130 | compile(bundle = '') { 131 | const { source, map } = this.transform(bundle); 132 | return new Function('global', `with (global) { ${source} } ${map}`); 133 | } 134 | 135 | /** 136 | * Transform the received bundle. 137 | * 138 | * @param {String} bundle 139 | * @returns {Object} Source and Source Map. 140 | * @public 141 | */ 142 | transform(bundle) { 143 | // 144 | // As you might have noticed from the sourceMap check below, sourcemaps 145 | // start with comment, that means you cannot append anything behind it as 146 | // it would be seen as a comment. So in order to wrap our content, we need 147 | // to seperate the source from the sourcemap, so we can wrap our code 148 | // with our "sandbox", and re-introduce the sourcemap. 149 | // 150 | // TIL: blindly searching for the string: 151 | // 152 | // //# sourceMappingURL=data:application/json;charset=utf-8;base64' 153 | // 154 | // And splitting content based on the index works fine, until you 155 | // load in code, like your own library, and suddenly the contents of the 156 | // file contain the same string.. So a simple indexOf doesn't work. We 157 | // can however assume that it's before the last \n if we trim it, as our 158 | // code is not minified we don't have to worry about that. 159 | // 160 | const clean = bundle.trim(); 161 | const index = clean.lastIndexOf('\n'); 162 | const source = bundle.slice(0, index).trim(); 163 | const map = bundle.slice(index).trim(); 164 | 165 | return { source, map }; 166 | } 167 | 168 | /** 169 | * Execute the bundle, store references to the newly created scopes. 170 | * 171 | * @param {String} sandbox The bundle to execute. 172 | * @public 173 | */ 174 | async exec(sandbox) { 175 | const bundle = await this.download(); 176 | const source = this.compile(bundle); 177 | 178 | // 179 | // @TODO sandbox 180 | // 181 | const result = source.call(global, global); 182 | const scope = this.scope(); 183 | 184 | this.scopes.push({ result, sandbox, ...scope }); 185 | 186 | return scope; 187 | } 188 | 189 | /** 190 | * Extract the methods that were introduced by the metroRequire from 191 | * a given object. 192 | * 193 | * @param {Object} obj The source where we have to extract our scope from. 194 | * @returns {Object} The created scope. 195 | * @public 196 | */ 197 | scope(obj = global) { 198 | return Object.entries(Evaluator.externals).reduce(function (memo, [key, value]) { 199 | memo[key] = obj[value]; 200 | return memo; 201 | }, {}); 202 | } 203 | } 204 | 205 | /** 206 | * This the method to global mapping of the `metroRequire` polyfill that 207 | * is loaded in every bundle. 208 | * 209 | * @type {Object} 210 | * @public 211 | */ 212 | Evaluator.externals = { 213 | registerSegment: '__registerSegment', 214 | nativeRequire: 'nativeRequire', 215 | metroAccept: '__accept', 216 | metroRequire: '__r', 217 | define: '__d', 218 | clear: '__c' 219 | }; 220 | 221 | export { 222 | Evaluator as default, 223 | Evaluator 224 | }; 225 | -------------------------------------------------------------------------------- /native/lifecycle.js: -------------------------------------------------------------------------------- 1 | import once from 'one-time/async'; 2 | 3 | /** 4 | * Lifecyle management for when test suite is about to run. 5 | * 6 | * @async 7 | * @param {Object} Runner Our test runner internals. 8 | * @returns {Promise} The after function, that runs the clean-up. 9 | * @public 10 | */ 11 | async function before({ send }) { 12 | const ocm = ['log', 'info', 'warn', 'error']; 13 | const oc = {}; 14 | 15 | ocm.forEach(function each(method) { 16 | oc[method] = console[method]; 17 | console[method] = send.bind(send, method); 18 | }); 19 | 20 | return once(async function after() { 21 | ocm.forEach(function each(method) { 22 | console[method] = oc[method]; 23 | delete oc[method]; 24 | }); 25 | }); 26 | } 27 | 28 | export { 29 | before as default, 30 | before 31 | }; 32 | -------------------------------------------------------------------------------- /native/screen.js: -------------------------------------------------------------------------------- 1 | import createRenderer from '../components/renderer'; 2 | import { AppRegistry } from 'react-native'; 3 | import diagnostics from 'diagnostics'; 4 | import bridge from './bridge'; 5 | import Ultron from 'ultron'; 6 | 7 | // 8 | // Dedicated screen logger. 9 | // 10 | const debug = diagnostics('ekke:screen'); 11 | 12 | /** 13 | * The Screen class allows us to manage what is current presented on the 14 | * device's screen. It could be the original application that the developer 15 | * is working on, or our custom Ekke test runner. This class makes it all 16 | * possible and orchestrates all the required API calls to hack this 17 | * together. 18 | * 19 | * @constructor 20 | * @public 21 | */ 22 | class Screen { 23 | constructor(rootTag) { 24 | this.bridge = new Ultron(bridge); // Created a managed EventEmitter. 25 | this.previous = this.discover(); // Name of the current mounted app. 26 | this.rootTag = rootTag; // Reference to the rootTag 27 | 28 | // 29 | // Create our renderer reference so we 30 | // 31 | this.Renderer = createRenderer(this.manager.bind(this)); 32 | } 33 | 34 | /** 35 | * Manages the interaction between our Screen, Bridge, and Renderer. 36 | * 37 | * @param {Function} setState Update rendering. 38 | * @public 39 | */ 40 | manager(setState) { 41 | this.bridge.remove('render'); 42 | this.bridge.on('render', (component, { resolve, reject }) => { 43 | // 44 | // In the rare case where `render` is called on a component that is 45 | // no longer mounted, it could raise an exception, in that case 46 | // do not want to eternally, but reject our given promise. 47 | // 48 | try { 49 | setState({ component }, resolve); 50 | } catch (e) { 51 | debug('failed to render the component', e); 52 | reject(e); 53 | } 54 | }); 55 | } 56 | 57 | /** 58 | * Present our screen to the general public for viewing purpose. 59 | * 60 | * @public 61 | */ 62 | present() { 63 | this.render(this.Renderer, 'EkkeEkkeEkkeEkke'); 64 | } 65 | 66 | /** 67 | * Discover the application that the user has registered so we can 68 | * eventually restore control to this application once our test-suite 69 | * has stopped running. 70 | * 71 | * @returns {String} The name of the app. 72 | * @public 73 | */ 74 | discover() { 75 | const keys = AppRegistry.getAppKeys(); 76 | 77 | debug(`discovered(${keys[0]}) as active app`); 78 | return keys[0]; 79 | } 80 | 81 | /** 82 | * Restore the users application by rerunning it in the previous rootTag. 83 | * 84 | * @public 85 | */ 86 | restore() { 87 | if (this.previous) { 88 | debug('restoring previous application'); 89 | this.mount(this.previous); 90 | } 91 | } 92 | 93 | /** 94 | * Mount the given application name as current running application. 95 | * 96 | * @param {String} name The name of the component to mount. 97 | * @public 98 | */ 99 | mount(name) { 100 | debug(`mounting(${name}) as new screen`); 101 | 102 | AppRegistry.runApplication(name, { 103 | rootTag: this.rootTag, 104 | initialProps: {} 105 | }); 106 | } 107 | 108 | /** 109 | * Registers a new Application that can be mounted. 110 | * 111 | * @param {String} name Name of the app that we register. 112 | * @param {Component} App Un-initialized component. 113 | * @returns {Boolean} Indication if the application has been mounted. 114 | * @public 115 | */ 116 | register(name, App) { 117 | if (AppRegistry.getRunnable(name)) { 118 | debug(`the component(${name}) was already registered, skipping`); 119 | return false; 120 | } 121 | 122 | debug(`registering component(${name})`); 123 | AppRegistry.registerComponent(name, () => App); 124 | return true; 125 | } 126 | 127 | /** 128 | * Render a new Application on the screen. 129 | * 130 | * @param {Component} App The Component that needs to be rendered on screen. 131 | * @param {String} name The name of the component. 132 | * @public 133 | */ 134 | render(App, name) { 135 | if (!name) name = App.name; 136 | 137 | debug(`attempting to render(${name}) as new screen`); 138 | 139 | this.register(name, App); 140 | this.mount(name); 141 | } 142 | } 143 | 144 | export { 145 | Screen as default, 146 | Screen 147 | }; 148 | -------------------------------------------------------------------------------- /native/subway.js: -------------------------------------------------------------------------------- 1 | import stringify from 'json-stringify-safe'; 2 | import { READYSTATE } from './constants'; 3 | import EventEmitter from 'eventemitter3'; 4 | import { Platform } from 'react-native'; 5 | import diagnostics from 'diagnostics'; 6 | import qs from 'querystringify'; 7 | import Timers from 'tick-tock'; 8 | import failure from 'failure'; 9 | import ms from 'millisecond'; 10 | import once from 'one-time'; 11 | import yeast from 'yeast'; 12 | 13 | // 14 | // Dedicated subway debugger. 15 | // 16 | const debug = diagnostics('ekke:subway'); 17 | 18 | /** 19 | * Subway, our API client for Metro. 20 | * 21 | * @constructor 22 | * @param {String} hostname The hostname of the service. 23 | * @param {Number} port The port number of the host. 24 | * @public 25 | */ 26 | class Subway extends EventEmitter { 27 | constructor(hostname, port) { 28 | super(); 29 | 30 | // 31 | // The entry path, that stores our super secret, test-specific endpoint 32 | // on our Metro bundler. 33 | // 34 | this.entry = 'Ekke-Ekke-Ekke-Ekke-PTANG.Zoo-Boing.Znourrwringmm'; 35 | 36 | this.readyState = READYSTATE.CLOSED; // The readyState of the thing. 37 | this.timers = new Timers(); // Our timer management. 38 | this.hostname = hostname; // Hostname of the URL to hit. 39 | this.active = new Set(); // Active running XHR requests. 40 | this.port = port; // Port number of the hostname. 41 | this.socket = null; // Reference to our active socket. 42 | this.queue = []; // Message queue. 43 | 44 | // 45 | // Methods that need to be pre-bound so they can be shared between different 46 | // class's 47 | // 48 | this.send = this.send.bind(this); 49 | } 50 | 51 | /** 52 | * @typedef {object} ConnectionOptions 53 | * @prop {string} [timeout='1 minute'] Timeout for test runs 54 | * @prop {string} [namespace='ekke'] Namespace for WebSocket endpoint. 55 | */ 56 | /** 57 | * Establish a connection with the WebSocket server that we've attached 58 | * to the metro bundler so we can notify it of our test suite progress. 59 | * 60 | * @param {ConnectionOptions} options Connection configuration. 61 | * @returns {Function} Clean up function that kills the connection. 62 | * @public 63 | */ 64 | connect(options) { 65 | const { namespace, timeout } = { 66 | timeout: '1 minute', 67 | namespace: 'ekke', 68 | 69 | ...options 70 | }; 71 | 72 | const url = `ws://${this.hostname}:${this.port}/${namespace}`; 73 | const socket = new WebSocket(url); 74 | 75 | /** 76 | * The full clean-up pass that we need to do when a connection is closed. 77 | * 78 | * @type {Function} 79 | * @param {Boolean} [alive] Should we check if CLI comes back to life? 80 | * @public 81 | */ 82 | const cleanup = once(function cleanup(alive = false) { 83 | try { 84 | socket.close(); 85 | } catch (e) { 86 | debug('closing the socket failed, but at least we tried', e); 87 | } 88 | 89 | this.timers.clear('socket'); 90 | this.socket = null; 91 | 92 | if (alive) this.alive(); 93 | }.bind(this)); 94 | 95 | /** 96 | * Handle incoming messages from the socket. 97 | * 98 | * @param {MessageEvent} event The WebSocket Message Event. 99 | * @private 100 | */ 101 | socket.addEventListener('message', ({ data }) => { 102 | let event, payload; 103 | 104 | try { 105 | ({ event, payload = {} } = JSON.parse(data)); 106 | } catch (e) { 107 | debug('failed to parse the payload from the ekke CLI', data); 108 | return; 109 | } 110 | 111 | this.emit(event, payload, this.send); 112 | }); 113 | 114 | /** 115 | * The WebSocket connection successfully completed the handshake 116 | * operation with the server and is ready to send/receive events. 117 | * 118 | * @param {OpenEvent} evt The open event. 119 | * @private 120 | */ 121 | socket.addEventListener('open', evt => { 122 | debug('websocket connection is open and ready', evt); 123 | this.timers.clear('socket'); 124 | 125 | if (!this.queue.length) return; 126 | 127 | debug(`flushing queued messages(${this.queue.length})`); 128 | 129 | this.queue.forEach(payload => socket.send(payload)); 130 | this.queue.length = 0; 131 | }); 132 | 133 | /** 134 | * The WebSocket connection was closed, but this was expected as it was 135 | * either caused by the server that closed the connection or manual close. 136 | * 137 | * @private 138 | */ 139 | socket.addEventListener('close', () => { 140 | debug('the websocket connection has been closed, either by server or client'); 141 | 142 | cleanup(true); 143 | }); 144 | 145 | /** 146 | * The connection was closed due to an unexpected error. Could be WebSocket 147 | * frame/protocol miss match, broken network pipe, anything really. 148 | * 149 | * @param {Error} e The cause. 150 | * @private 151 | */ 152 | socket.addEventListener('error', e => { 153 | debug('websocket connection resulted in an error', e); 154 | 155 | cleanup(true); 156 | }); 157 | 158 | this.timers.setTimeout('socket', function timeout() { 159 | debug('failed to connection in timely manner'); 160 | cleanup(true); 161 | }, timeout); 162 | 163 | this.socket = socket; 164 | return cleanup; 165 | } 166 | 167 | /** 168 | * Send a message to the CLI. 169 | * 170 | * @param {String} event The event name. 171 | * @param {Object|Array|String} payload What ever the message is. 172 | * @public 173 | */ 174 | send(event, ...payload) { 175 | const message = stringify({ event, payload }); 176 | 177 | // 178 | // We can only send messages when we have a socket, and if the socket 179 | // is in an WebSocket.OPEN state. In any other case it would most likely 180 | // cause an error. 181 | // 182 | if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { 183 | debug('no active connection has been established yet, queueing message', message); 184 | this.queue.push(message); 185 | 186 | return; 187 | } 188 | 189 | // 190 | // While WebSocket.OPEN should give enough confidence that it's safe to 191 | // write.. It's better to be safe than sorry here. 192 | // 193 | try { 194 | this.socket.send(message); 195 | } catch (e) { 196 | debug('websocket lied, we cant write :sadface:', e); 197 | } 198 | } 199 | 200 | /** 201 | * Fetches the bundle that the metro bundler has created for us. 202 | * 203 | * @param {Object} options Optional request options. 204 | * @returns {String} The bundle that needs to be evaluated. 205 | * @public 206 | */ 207 | async bundle({ 208 | inlineSourceMap = false, // Inline the source map. 209 | platform = Platform.OS, // The platform we want to bundle for. 210 | excludeSource = false, // Don't include the source. 211 | runModule = true, // Don't execute the imports. 212 | entry = 'index', // Path of the entry file we want to load. 213 | minify = false, // Minify the source 214 | method = 'GET', // HTTP method to use. 215 | dev = __DEV__ // Debug build 216 | } = {}) { 217 | // 218 | // Construct the bundle URL that Metro bundler uses to output the bundle: 219 | // 220 | // http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false 221 | // 222 | const query = qs.stringify({ 223 | inlineSourceMap, 224 | excludeSource, 225 | runModule, 226 | platform, 227 | minify, 228 | dev 229 | }); 230 | 231 | const url = `http://${this.hostname}:${this.port}/${entry}.bundle?${query}`; 232 | 233 | debug('about to fetch a new bundle', url); 234 | return await this.request({ 235 | timeout: false, 236 | method, 237 | url 238 | }); 239 | } 240 | 241 | /** 242 | * Sends a pre-load request to the Metro bundler so it can start building 243 | * the bundle while we wait for things to setup. We don't really care about 244 | * what happens to the request, as long as it's made, and starts the bundle 245 | * operation in the CLI. 246 | * 247 | * @public 248 | */ 249 | preload() { 250 | this.bundle({ method: 'HEAD', entry: this.entry }) 251 | .then(() => { 252 | debug('preload request ended successfully'); 253 | }) 254 | .catch(e => { 255 | debug('preload request ended due to error', e); 256 | }); 257 | } 258 | 259 | /** 260 | * Checks if the Metro service that is spawned by our CLI is alive and 261 | * ready to party. 262 | * 263 | * @param {Function} fn Completion callback when service is alive. 264 | * @param {String|Number} interval How long to wait after each check. 265 | * @public 266 | */ 267 | alive(fn, interval = '10 seconds') { 268 | const subway = this; 269 | const payload = { 270 | url: `http://${this.hostname}:${this.port}/package.json.bundle`, 271 | timeout: '10 seconds', 272 | method: 'HEAD' 273 | }; 274 | 275 | (async function again() { 276 | try { 277 | await subway.request(payload); 278 | } catch (e) { 279 | debug('nope, failed to check if server is alive', e); 280 | return subway.timers.setTimeout('alive', again, ms(interval)); 281 | } 282 | 283 | subway.setup(); 284 | debug('received a reponse from our HEAD request, service is alive'); 285 | if (fn) fn(); 286 | }()); 287 | } 288 | 289 | /** 290 | * The service is considered alive, lets connect all the things. 291 | * 292 | * @public 293 | */ 294 | async setup() { 295 | this.readyState = READYSTATE.READY; 296 | 297 | this.preload(); 298 | this.connect(); 299 | } 300 | 301 | /** 302 | * As the `fetch` API lacks support for aborting and timing out requests 303 | * we have to use the regular XMLHttpRequest to make HTTP requests. 304 | * 305 | * @param {Object} options The request options. 306 | * @returns {Promise} Pinky promise. 307 | * @public 308 | */ 309 | request(options = {}) { 310 | const { method, url, timeout, headers } = { 311 | timeout: 20000, 312 | method: 'GET', 313 | 314 | ...options 315 | }; 316 | 317 | return new Promise((resolve, reject) => { 318 | const xhr = new XMLHttpRequest(); 319 | const id = yeast(); 320 | 321 | /** 322 | * Prevents execution of resolve and reject due to race conditions. 323 | * 324 | * @type {Function} 325 | * @param {Error} err Optional error. 326 | * @param {String} data The response. 327 | * @returns {Undefined} Nothing useful. 328 | * @private 329 | */ 330 | const done = once((err, data) => { 331 | this.active.delete(xhr); 332 | this.timers.clear(id); 333 | 334 | if (!err) { 335 | debug(`(${id}) successfully completed http request`); 336 | return resolve(data); 337 | } 338 | 339 | // 340 | // In case of error we want to be sure that the connection with the 341 | // server/socket was closed, so we're going to forcefuly abort. 342 | // 343 | try { 344 | xhr.abort(); 345 | } catch (e) { 346 | debug('aborting xhr failed', e); 347 | } 348 | 349 | debug(`(${id}) http request failed because of error`, err.message); 350 | reject(err); 351 | }); 352 | 353 | /** 354 | * Process readyState changes so we know when data is available. 355 | * 356 | * @returns {Undefined} Nothing. 357 | * @private 358 | */ 359 | xhr.onreadystatechange = function onreadystatechange() { 360 | if (xhr.readyState !== 4) { 361 | return; 362 | } 363 | 364 | const text = xhr.responseText; 365 | const status = xhr.status; 366 | 367 | if (!(status >= 200 && status < 300)) { 368 | debug(text); 369 | 370 | let parsed = {}; 371 | 372 | try { 373 | parsed = JSON.parse(text); 374 | } catch (e) { 375 | debug('failed to parse responseText as JSON', e); 376 | } 377 | 378 | const error = failure('Incorrect status code recieved: ' + status, { 379 | ...parsed, 380 | statusCode: status 381 | }); 382 | 383 | return done(error); 384 | } 385 | 386 | return done(null, text); 387 | }; 388 | 389 | // 390 | // When users are switching network, from wifi to 4g, or are generally 391 | // on unstable networks it could take to long for the request to 392 | // process, so we want to bail out when this happens. 393 | if (timeout) { 394 | xhr.timeout = ms(timeout); 395 | xhr.ontimeout = function ontimeout() { 396 | done(new Error('Failed to process request in a timely manner')); 397 | }; 398 | 399 | // 400 | // Fallback for when timeout actually doesn't work, this for example the 401 | // case with the xhr2 polyfill in node, and could potentially be happening 402 | // with RN as well, when we timeout in xxx, we mean it, it should just 403 | // die. 404 | // 405 | this.timers.setTimeout(id, xhr.ontimeout, timeout); 406 | } 407 | 408 | xhr.onerror = function onerror(e) { 409 | done(new Error('Failed to process request due to error: ' + e.message)); 410 | }; 411 | 412 | debug(`(${id}) starting request(${url})`); 413 | this.active.add(xhr); 414 | 415 | xhr.open(method || 'GET', url); 416 | 417 | // 418 | // We've opened the xhr requests, so we can now process our custom 419 | // headers if needed. 420 | // 421 | if (headers) { 422 | Object.keys(headers).forEach(key => { 423 | xhr.setRequestHeader(key, headers[key]); 424 | }); 425 | } 426 | 427 | xhr.send(); 428 | }); 429 | } 430 | 431 | /** 432 | * Kill anything that still runs, so we can die in peace. 433 | * 434 | * @public 435 | */ 436 | destroy() { 437 | // 438 | // If we have an established WebSocket connection, we want to close it 439 | // and clean up the references. 440 | // 441 | if (this.socket) { 442 | this.socket.close(); 443 | this.socket = null; 444 | } 445 | 446 | for (const xhr of this.active) { 447 | try { 448 | xhr.abort(); 449 | } catch (e) { 450 | debug('failed to abort the active XHR', e); 451 | } 452 | } 453 | 454 | this.active.clear(); 455 | this.timers.end(); 456 | } 457 | } 458 | 459 | export { 460 | Subway as default, 461 | Subway 462 | }; 463 | -------------------------------------------------------------------------------- /native/uncaught.js: -------------------------------------------------------------------------------- 1 | import diagnostics from 'diagnostics'; 2 | import once from 'one-time'; 3 | 4 | // 5 | // Dedicated uncaught exception logger. 6 | // 7 | const debug = diagnostics('ekke:uncaught'); 8 | 9 | /** 10 | * Adds an uncaught exception handler. 11 | * 12 | * @param {Function} fn Function to execute when an uncaughtException handles. 13 | * @returns {Function} Function to restore the error handler. 14 | * @public 15 | */ 16 | function capture(fn) { 17 | const old = ErrorUtils.getGlobalHandler(); 18 | 19 | /** 20 | * A function that will restore the error handler to it's original state 21 | * as we've found it, we only want to restore it once or we could accidentally 22 | * override another error handler. 23 | * 24 | * @type {Function} 25 | * @public 26 | */ 27 | const restore = once(function previous() { 28 | if (ErrorUtils.getGlobalHandler() !== handler) { 29 | debug('unable to restore old handler, as our current got replaced'); 30 | return; 31 | } 32 | 33 | debug('restoring previous replaced handler'); 34 | ErrorUtils.setGlobalHandler(old); 35 | }); 36 | 37 | /** 38 | * Our custom uncaughtException handler, we want to store this reference so 39 | * when we attempt to restore the error handler, we could check if the 40 | * current handler this function so we don't accidentally override a new handler. 41 | * 42 | * @type {Function} 43 | * @private 44 | */ 45 | const handler = once(function uncaughtException(...args) { 46 | debug('captured uncaught exception', args); 47 | 48 | // 49 | // Before we do anything else, we want to make sure that we restore 50 | // the previous handler, so any errors that our own error handler is 51 | // causing, is properly caught, instead of being re-directed to this 52 | // function, potentially causing an infinite loop of pain of suffering. 53 | // 54 | restore(); 55 | 56 | // 57 | // We only want to call our own error handler as this exception happened 58 | // while running the test suite we don't accidenlty want to trigger any 59 | // error reporting that shouldn't be triggered. 60 | // 61 | fn(...args); 62 | }); 63 | 64 | ErrorUtils.setGlobalHandler(handler); 65 | return restore; 66 | } 67 | 68 | export { 69 | capture as default, 70 | capture 71 | }; 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ekke", 3 | "version": "1.1.0", 4 | "description": "Ekke is a test runner that allows you to execute your tests on the actual device", 5 | "main": "api/index.js", 6 | "react-native": "ekke.js", 7 | "scripts": { 8 | "example:mocha": "./bin/ekke run examples/*.mocha.js --using mocha", 9 | "example:tape": "./bin/ekke run examples/*.tape.js --using tape", 10 | "example:tap-spec": "./bin/ekke run examples/*.tape.js --using tape | tap-spec", 11 | "ios": "react-native run-ios", 12 | "setup": "react-native eject", 13 | "start": "node node_modules/react-native/local-cli/cli.js start", 14 | "lint": "eslint-godaddy-react native/*.js examples/*.js api/*.js api/**/*.js \"api/**/*(metro|commands)*/*.js\" runners/*.js components/*.js *.js test/**/**/*.js", 15 | "test:ekke": "./bin/ekke run test/ekke/**/*.js test/ekke/*.js --using mocha", 16 | "test:examples": "npm run example:mocha && npm run example:tape && npm run example:tap-spec", 17 | "test:nodejs": "nyc --reporter=text --reporter=json-summary npm run test:runner", 18 | "test:runner": "mocha --require setup-env --require test/mock \"test/*(native|api)/*.*(test|spec).js\"", 19 | "test:watch": "npm run test:runner -- --watch", 20 | "test:all": "npm run test:examples && npm run test:ekke && npm run test:nodejs", 21 | "test": "npm run test:nodejs" 22 | }, 23 | "bin": { 24 | "ekke": "./bin/ekke" 25 | }, 26 | "author": "GoDaddy Operating Company, LLC", 27 | "contributors": [ 28 | "Arnout Kazemier (https://github.com/3rd-Eden)" 29 | ], 30 | "license": "MIT", 31 | "repository": { 32 | "type": "git", 33 | "url": "git@github.com:godaddy/ekke.git" 34 | }, 35 | "setup": { 36 | "/**/": [ 37 | "!! important !!", 38 | "The babel config for the tests is placed in a directory that cannot be", 39 | "automatically resolved by babel, this is intentional, as it prevents", 40 | "the metro bundler from using it for compilation.", 41 | "!! important !!" 42 | ], 43 | "babel": { 44 | "configFile": "./test/.babelrc" 45 | } 46 | }, 47 | "greenkeeper": { 48 | "commitMessages": { 49 | "dependencyUpdate": "[deps] Update ${dependency} to version ${version}", 50 | "devDependencyUpdate": "[deps|dev] Update ${dependency} to version ${version}", 51 | "dependencyPin": "[deps] Pin ${dependency} to ${oldVersion}", 52 | "devDependencyPin": "[deps|dev] Fix: Pin ${dependency} to ${oldVersion}" 53 | } 54 | }, 55 | "dependencies": { 56 | "argh": "^1.0.0", 57 | "babel-plugin-rewrite-require": "^1.14.5", 58 | "diagnostics": "^2.0.0", 59 | "es-paint": "^2.0.0", 60 | "eventemitter3": "^4.0.0", 61 | "failure": "^1.1.1", 62 | "glob": "^7.1.3", 63 | "json-stringify-safe": "^5.0.1", 64 | "millisecond": "^0.1.2", 65 | "node-libs-react-native": "^1.0.3", 66 | "one-time": "^1.0.0", 67 | "process": "^0.11.10", 68 | "prop-types": "^15.7.2", 69 | "querystringify": "^2.1.1", 70 | "references": "0.0.0", 71 | "stream-browserify": "^2.0.2", 72 | "tick-tock": "^1.0.0", 73 | "ultron": "^1.1.1", 74 | "yeast": "^0.1.2" 75 | }, 76 | "devDependencies": { 77 | "@babel/cli": "^7.1.5", 78 | "@babel/core": "^7.1.5", 79 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 80 | "@babel/preset-env": "^7.1.5", 81 | "@babel/preset-react": "^7.0.0", 82 | "@babel/register": "^7.0.0", 83 | "assume": "^2.2.0", 84 | "asyncstorageapi": "^1.0.2", 85 | "babel-eslint": ">=7.2.1 <11.0.0", 86 | "chai": "^4.2.0", 87 | "eslint": "^6.0.0", 88 | "eslint-config-godaddy-react": "^5.0.0", 89 | "eslint-plugin-json": "^1.4.0", 90 | "eslint-plugin-jsx-a11y": "^6.2.1", 91 | "eslint-plugin-mocha": "^6.0.0", 92 | "eslint-plugin-react": "^7.13.0", 93 | "mocha": "^6.1.4", 94 | "nock": "^10.0.6", 95 | "nyc": "^14.1.0", 96 | "react": "^16.8.6", 97 | "react-native": "^0.59.8", 98 | "require-poisoning": "^2.0.0", 99 | "setup-env": "^1.2.2", 100 | "tap-spec": "^5.0.0", 101 | "tape": "^4.10.1", 102 | "xhr2": "^0.2.0" 103 | }, 104 | "peerDependencies": { 105 | "react": "^16.8.6", 106 | "react-native": "^0.59.6" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /production.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose exactly the same interface as our development build, but without 3 | * loading our actual library, so no content except for this file would 4 | * be included in production. 5 | * 6 | * @returns {Component|Null} Children, if used as wrapping component, or nada. 7 | * @public 8 | */ 9 | function Ekke({ children }) { 10 | return children || null; 11 | } 12 | 13 | // 14 | // Indication of which build is loaded. 15 | // 16 | Ekke.prod = true; 17 | Ekke.dev = false; 18 | 19 | /** 20 | * Again, nothing but shell of it's former self. 21 | * 22 | * @returns {Promise} This should never be used in production. 23 | * @public 24 | */ 25 | function render() { 26 | return new Promise(function nope(resolve, reject) { 27 | reject(new Error('render method is disabled in production')); 28 | }); 29 | } 30 | 31 | export { 32 | Ekke, 33 | render 34 | }; 35 | -------------------------------------------------------------------------------- /runners/index.js: -------------------------------------------------------------------------------- 1 | import Evaluator from '../native/evaluator'; 2 | import uncaught from '../native/uncaught'; 3 | import before from '../native/lifecycle'; 4 | import diagnostics from 'diagnostics'; 5 | import once from 'one-time/async'; 6 | import failure from 'failure'; 7 | 8 | // 9 | // Create our test runner debugger. 10 | // 11 | const debug = diagnostics('ekke:runner'); 12 | 13 | /** 14 | * The base of our test runner, provides a clean abstraction for all other 15 | * test runners to build up without the need of duplicating functionality. 16 | * 17 | * @constructor 18 | * @param {Object} API Pre-initialized versions of screen, subway, and config 19 | * @public 20 | */ 21 | class Runner { 22 | constructor({ screen, subway, config }) { 23 | this.eva = new Evaluator(subway); 24 | this.screen = screen; 25 | this.subway = subway; 26 | this.config = config; 27 | 28 | this.cleanup = []; 29 | this.setup(); 30 | } 31 | 32 | /** 33 | * Creates a new completion handler for the current task. 34 | * 35 | * @returns {Function} Completion handler. 36 | * @public 37 | */ 38 | complete() { 39 | return once(async function completed(err) { 40 | debug('completed the run', err); 41 | 42 | if (err) { 43 | if (typeof err === 'number') { 44 | err = new Error(`Test are failing with exit code ${err}`); 45 | } 46 | 47 | this.subway.send('complete', failure(err)); 48 | } else { 49 | this.subway.send('complete'); 50 | } 51 | 52 | await this.teardown(); 53 | }.bind(this)); 54 | } 55 | 56 | /** 57 | * Kickstart the testing. 58 | * 59 | * @async 60 | * @public 61 | */ 62 | async setup() { 63 | const complete = this.complete(); 64 | const local = this.eva.local(); 65 | 66 | let suites; 67 | let runner; 68 | let scope; 69 | 70 | // 71 | // Setup our screen so the user knows magic is about to happen. 72 | // 73 | this.screen.present(); 74 | 75 | // 76 | // Check if we need to process any of the environment before we can 77 | // execute our test runner. There might be specific fixes that we need 78 | // to make in order to bend it to our will. 79 | // 80 | if (typeof this.environment === 'function') await this.environment(local); 81 | const sandbox = this.eva.sandproxy(local); 82 | 83 | // 84 | // Now that we have everything ready to evaluate we're gonna prepare 85 | // the rest of the environment and setup all our listeners. 86 | // 87 | this.cleanup.push(await before({ send: this.subway.send })); 88 | this.cleanup.push(uncaught(complete)); 89 | 90 | // 91 | // Now that we've setup our uncaught hooks, started intercepting things 92 | // like console statements, we can attempt to execute the bundle. 93 | // 94 | // The `metroRequire` method has 2 modes, it either requires modules 95 | // based on the generated moduleId that was configured in our metro.config 96 | // or by using the module names/paths. The thing is, we have no idea what 97 | // the path of the file is, as it's randomly generated based on the SH256 98 | // of it's contents, what we do know, is that it's the entry of our bundle. 99 | // 100 | // Combining that with the fact that the Metro bundler uses zero based 101 | // sequential id generation for each moduleId, we can accurately say that 102 | // module 0 is our entry file, which needs to be required. 103 | // 104 | try { 105 | scope = await this.eva.exec(sandbox); 106 | ({ runner, suites } = scope.metroRequire(1)); 107 | } catch (e) { 108 | debug('critical error: the compilation failed', e); 109 | return complete(e); 110 | } 111 | 112 | // 113 | // Prep the runner, so it can create a new instance if needed. 114 | // 115 | try { 116 | this.cleanup.push(await this.before(this.config, runner)); 117 | } catch (e) { 118 | return complete(e); 119 | } 120 | 121 | // 122 | // Execute the test suites so they are executed in our JavaScript 123 | // environment. 124 | // 125 | try { 126 | suites(); 127 | } catch (e) { 128 | return complete(e); 129 | } 130 | 131 | // 132 | // Finally, if everything went well, we can run our test suite. 133 | // 134 | await this.run(complete); 135 | } 136 | 137 | /** 138 | * Runs a clean-up operation. 139 | * 140 | * @async 141 | * @public 142 | */ 143 | async teardown() { 144 | await Promise.all( 145 | this.cleanup 146 | .filter(Boolean) // Remove possible undefineds. 147 | .map(fn => fn()) // Execute so we get promise instances. 148 | .filter(Boolean) // Clean up again, to remove non-async functions. 149 | ); 150 | 151 | this.cleanup.length = 0; 152 | this.screen.restore(); 153 | this.runner = null; 154 | } 155 | } 156 | 157 | export { 158 | Runner as default, 159 | Runner 160 | }; 161 | -------------------------------------------------------------------------------- /runners/mocha.js: -------------------------------------------------------------------------------- 1 | import Runner from './'; 2 | 3 | /** 4 | * Prepare the environment for the Mocha test runner. 5 | * 6 | * @constructor 7 | * @public 8 | */ 9 | class MochaRunner extends Runner { 10 | /** 11 | * Setup our test runner, pre-pare for test import, and execution. 12 | * 13 | * @param {Object} config The configuration of all the things. 14 | * @param {Function} Mocha The test runner. 15 | * @returns {Undefined|Function} After clean up function. 16 | * @public 17 | */ 18 | async before(config, Mocha) { 19 | const fgrep = config.fgrep || ''; 20 | const grep = config.grep || ''; 21 | 22 | const mocha = (this.runner = new Mocha({ 23 | grep: grep.length && grep, 24 | fgrep: fgrep.length && fgrep 25 | })); 26 | 27 | // 28 | // Apply all options that were given to us through the bundler process. 29 | // 30 | mocha 31 | .ui(config.ui || 'bdd') 32 | .slow(config.slow || 75) 33 | .timeout(config.timeout || 2000) 34 | .reporter(config.reporter || 'spec') 35 | .bail('bail' in config ? config.bail : true) 36 | .useColors('color' in config ? config.color : true) 37 | .useInlineDiffs('inlineDiffs' in config ? config.inlineDiffs : true); 38 | 39 | if (config.invert) mocha.invert(); 40 | 41 | // 42 | // Last but not least, we need to call this weird internal method to 43 | // initialize/inject the globals that belong to the selected test `ui` this 44 | // way when the tests are loaded they can actually access `describe` & `it`. 45 | // 46 | mocha.suite.emit('pre-require', global, '', mocha); 47 | } 48 | 49 | /** 50 | * Execute the test runner. 51 | * 52 | * @param {Function} completion Callback for when we're done. 53 | * @public 54 | */ 55 | async run(completion) { 56 | this.runner.run(completion); 57 | } 58 | } 59 | 60 | export { 61 | MochaRunner as default, 62 | MochaRunner 63 | }; 64 | -------------------------------------------------------------------------------- /runners/tape.js: -------------------------------------------------------------------------------- 1 | import Runner from './'; 2 | 3 | /** 4 | * Prepare the environment for the Mocha test runner. 5 | * 6 | * @constructor 7 | * @public 8 | */ 9 | class TapeRunner extends Runner { 10 | /** 11 | * Setup our test runner, pre-pare for test import, and execution. 12 | * 13 | * @param {Object} config The configuration of all the things. 14 | * @param {Function} tape The test runner. 15 | * @returns {Undefined|Function} After clean up function. 16 | * @public 17 | */ 18 | async before(config, tape) { 19 | this.runner = { 20 | output: tape.createStream(), 21 | harness: tape.getHarness() 22 | }; 23 | 24 | this.runner.output.on('data', line => console.log(line.trimRight())); 25 | } 26 | 27 | /** 28 | * Execute the test runner. 29 | * 30 | * @param {Function} completion Callback for when we're done. 31 | * @public 32 | */ 33 | async run(completion) { 34 | const { harness } = this.runner; 35 | 36 | (function iterate(tests) { 37 | const test = tests.shift(); 38 | 39 | if (test) { 40 | test.run(); 41 | 42 | if (!test.ended) { 43 | return test.once('end', () => iterate(tests)); 44 | } 45 | } 46 | 47 | if (!tests.length) { 48 | harness.close(); 49 | completion(harness._exitCode); 50 | } 51 | }(harness._tests.slice(0))); 52 | } 53 | } 54 | 55 | export { 56 | TapeRunner as default, 57 | TapeRunner 58 | }; 59 | -------------------------------------------------------------------------------- /test/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": 9 8 | } 9 | } 10 | ], 11 | "@babel/preset-react" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test/ekke/bridge.test.js: -------------------------------------------------------------------------------- 1 | import bridge, { render as renderBridge } from '../../native/bridge'; 2 | import { describe, it } from 'mocha'; 3 | import * as RN from 'react-native'; 4 | import { render } from 'ekke'; 5 | import assume from 'assume'; 6 | 7 | describe('(ekke) bridge', function () { 8 | it('is an eventemitter', function () { 9 | const next = assume.plan(4); 10 | 11 | assume(bridge.on).is.a('function'); 12 | assume(bridge.once).is.a('function'); 13 | assume(bridge.emit).is.a('function'); 14 | 15 | function callback(foo) { 16 | assume(foo).equals('bar'); 17 | bridge.removeListener('this is test event'); 18 | } 19 | 20 | bridge.on('this is a test event', callback); 21 | bridge.emit('this is a test event', 'bar'); 22 | 23 | next(); 24 | }); 25 | 26 | it('exposes the React-Native library', function () { 27 | // 28 | // We cannot do a deep equal check as the exports in React-Native 29 | // are defined as getter so accessing those keys will lazy load 30 | // the components. 31 | // 32 | // We can't even do an assume(RN).equals(bridge.ReactNative); 33 | // without triggering a spam of import deprecations. 34 | // 35 | // So we're just going to a quick test to make sure that 36 | // some of the "core" components are there. 37 | // 38 | assume(RN.View).equals(bridge.ReactNative.View); 39 | assume(RN.Text).equals(bridge.ReactNative.Text); 40 | assume(RN.Platform).equals(bridge.ReactNative.Platform); 41 | }); 42 | 43 | it('expose a render function', function () { 44 | assume(renderBridge).equals(render); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/ekke/components/ekke.test.js: -------------------------------------------------------------------------------- 1 | import createEkke from '../../components/ekke'; 2 | import { describe, it } from 'mocha'; 3 | import { View } from 'react-native'; 4 | import { render } from 'ekke'; 5 | import assume from 'assume'; 6 | import React from 'react'; 7 | 8 | describe('(ekke) createEkke', function () { 9 | it('is a function', function () { 10 | assume(createEkke).is.a('function'); 11 | }); 12 | 13 | it('returns a React Component', function () { 14 | const Ekke = createEkke(function () {}); 15 | 16 | assume(!!Ekke.prototype.isReactComponent).is.true(); 17 | }); 18 | 19 | it('executes the supplied callback with a rootTag & props', async function () { 20 | const next = assume.plan(4); 21 | 22 | const Ekke = createEkke(function mounted(rootTag, props) { 23 | assume(rootTag).is.a('number'); 24 | assume(props).is.a('object'); 25 | assume(props.port).equals(1975); 26 | assume(props.interval).equals('10 seconds'); 27 | }); 28 | 29 | await render(); 30 | next(); 31 | }); 32 | 33 | it('only calls the supplied callback once', async function () { 34 | let calls = 0; 35 | 36 | const Ekke = createEkke(function mounted() { 37 | calls++; 38 | }); 39 | 40 | await render(); 41 | await render(); 42 | await render(); 43 | await render(); 44 | await render(); 45 | 46 | assume(calls).equals(1); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/ekke/components/loading.test.js: -------------------------------------------------------------------------------- 1 | import Loading from '../../components/loading'; 2 | import { describe, it } from 'mocha'; 3 | import { render } from 'ekke'; 4 | import assume from 'assume'; 5 | import React from 'react'; 6 | 7 | describe('(ekke) Loading', function () { 8 | function delay(t) { 9 | return new Promise((r) => setTimeout(r, t)); 10 | } 11 | it('is a React Component', function () { 12 | assume(!!Loading.prototype.isReactComponent).is.true(); 13 | }); 14 | 15 | it('generates a different background', async function () { 16 | const ref = React.createRef(); 17 | 18 | await render(); 19 | 20 | const loading = ref.current; 21 | const grd = loading.state.grd; 22 | 23 | await delay(30); 24 | 25 | assume(loading.state.grd).does.not.equal(grd); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/ekke/components/renderer.test.js: -------------------------------------------------------------------------------- 1 | import createRenderer from '../../components/renderer'; 2 | import { describe, it } from 'mocha'; 3 | import { View } from 'react-native'; 4 | import { render } from 'ekke'; 5 | import assume from 'assume'; 6 | import React from 'react'; 7 | 8 | describe('(ekke) createRenderer', function () { 9 | it('is a function', function () { 10 | assume(createRenderer).is.a('function'); 11 | }); 12 | 13 | it('returns a React Component', function () { 14 | const Renderer = createRenderer(() => {}); 15 | assume(!!Renderer.prototype.isReactComponent).is.true(); 16 | }); 17 | 18 | it('calls the supplied callback when the component is mounted', async function () { 19 | const next = assume.plan(1); 20 | 21 | const Renderer = createRenderer((setState) => { 22 | assume(setState).is.a('function'); 23 | }); 24 | 25 | await render(); 26 | next(); 27 | }); 28 | 29 | it('can render custom components using the supplied fn', async function () { 30 | let setState; 31 | 32 | class Square extends React.Component { 33 | render() { 34 | return ( 35 | 36 | ) 37 | } 38 | } 39 | 40 | function paint(Component) { 41 | return new Promise((resolve) => { 42 | const ref = React.createRef(); 43 | 44 | setState({ component: }, function () { 45 | resolve(ref); 46 | }); 47 | }); 48 | } 49 | 50 | const Renderer = createRenderer((state) => { 51 | setState = state; 52 | }); 53 | 54 | await render(); 55 | assume(setState).is.a('function'); 56 | 57 | const one = await paint(Square); 58 | assume(one.current).exists(); 59 | 60 | const two = await paint(Square); 61 | assume(one.current).does.not.exist(); 62 | assume(two.current).exists(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/ekke/development.test.js: -------------------------------------------------------------------------------- 1 | import { render as renderDev, Ekke } from '../../development'; 2 | import { describe, it } from 'mocha'; 3 | import { View } from 'react-native'; 4 | import { render } from 'ekke'; 5 | import assume from 'assume'; 6 | import React from 'react'; 7 | 8 | describe('(ekke) development', function () { 9 | describe('#render', function () { 10 | it('is the same as our render method', function () { 11 | assume(renderDev).is.a('function'); 12 | assume(renderDev).equals(render); 13 | }); 14 | }); 15 | 16 | describe('#Ekke', function () { 17 | it('exposes the Ekke component', function () { 18 | assume(React.isValidElement()).is.true(); 19 | }); 20 | 21 | it('gets initialized', async function () { 22 | const next = assume.plan(4); 23 | 24 | function NiPengNeeWom(rootTag, props) { 25 | assume(rootTag).is.a('number'); 26 | 27 | assume(props).is.a('object'); 28 | assume(props.hostname).is.a('string'); 29 | assume(props.NiPengNeeWom).equals(NiPengNeeWom); 30 | } 31 | 32 | await render( 33 | 34 | ); 35 | 36 | next(); 37 | }); 38 | 39 | it('renders the children if provided', async function () { 40 | /** 41 | * Fail safe to ensure that Ekke's initialized is never executed 42 | * more than once. The first initialization happens in our first test. 43 | */ 44 | function nope() { 45 | throw new Error('I should never be executed'); 46 | } 47 | 48 | await render(); 49 | 50 | const ref = React.createRef(); 51 | await render( 52 | 53 | 54 | 55 | ); 56 | 57 | assume(ref.current).is.a('object'); 58 | }) 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/ekke/evaluator.test.js: -------------------------------------------------------------------------------- 1 | import Evaluator from '../../native/evaluator'; 2 | import createEkke from '../../components/ekke'; 3 | import Subway from '../../native/subway'; 4 | import { describe, it } from 'mocha'; 5 | import assume from 'assume'; 6 | 7 | describe('(ekke) Evaluator', function () { 8 | let sub; 9 | let eva; 10 | 11 | function create() { 12 | const { defaultProps } = createEkke(() => {}); 13 | 14 | sub = new Subway(defaultProps.hostname, defaultProps.port); 15 | eva = new Evaluator(sub); 16 | } 17 | 18 | beforeEach(create); 19 | 20 | describe('bundle interactions:', function () { 21 | this.timeout(5000); 22 | 23 | let bundle; 24 | 25 | before(async function () { 26 | if (!eva) create(); 27 | 28 | bundle = await eva.download(); 29 | }); 30 | 31 | describe('#download', function () { 32 | it('downloads the bundle', async function () { 33 | assume(bundle).is.a('string'); 34 | assume(bundle).includes('Anything I have write is in the bundle O_o'); 35 | }); 36 | }); 37 | 38 | describe('#transform', function () { 39 | it('separates the code from the source map', function () { 40 | const { source, map } = eva.transform(bundle); 41 | 42 | assume(source).is.a('string'); 43 | assume(map).is.a('string'); 44 | 45 | assume(source).startWith('var __DEV__=true'); 46 | assume(source).endsWith(');'); 47 | 48 | assume(map).startWith('//# sourceMappingURL=data:application/json;charset=utf-8;base64'); 49 | assume(map.split('\n')).is.length(1); 50 | }); 51 | }); 52 | 53 | describe('#compile', function () { 54 | it('transforms the bundle into a function', function () { 55 | const compiled = eva.compile(bundle); 56 | 57 | assume(compiled).is.a('function'); 58 | }); 59 | }); 60 | }); 61 | 62 | describe('#scope', function () { 63 | it('returns the current scope of global metro requires', function () { 64 | const scope = eva.scope(); 65 | 66 | assume(scope).is.a('object'); 67 | assume(scope.registerSegment).is.a('function'); 68 | assume(scope.metroRequire).is.a('function'); 69 | assume(scope.metroAccept).is.a('function'); 70 | 71 | if (scope.nativeRequire) { 72 | assume(scope.nativeRequire).is.a('function'); 73 | } 74 | 75 | assume(scope.define).is.a('function'); 76 | assume(scope.clear).is.a('function'); 77 | }); 78 | 79 | it('can use the metroRequire', function () { 80 | const scope = eva.scope(); 81 | const pkg = scope.metroRequire('package.json'); 82 | 83 | assume(pkg).is.a('object'); 84 | assume(pkg.name).equals('ekke'); 85 | }); 86 | 87 | it('reads out the global scope by default', function () { 88 | const globalScope = eva.scope(global); 89 | const scope = eva.scope(); 90 | 91 | Object.keys(scope).forEach((x) => assume(scope[x]).equals(globalScope[x])); 92 | }); 93 | 94 | it('reads the scope from passed object', function () { 95 | const scope = eva.scope({ __r: 'fake' }); 96 | 97 | assume(scope.metroRequire); 98 | }); 99 | 100 | it('each scope is directly accessible from the instance', function () { 101 | const scope = eva.scope(); 102 | Object.keys(scope).forEach((x) => assume(eva[x]).is.a('function')); 103 | 104 | const pkg = eva.metroRequire('package.json'); 105 | assume(pkg).is.a('object'); 106 | assume(pkg.name).equals('ekke'); 107 | }) 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/ekke/production.test.js: -------------------------------------------------------------------------------- 1 | import { render as renderProd, Ekke } from '../../production'; 2 | import { describe, it } from 'mocha'; 3 | import { View } from 'react-native'; 4 | import { render } from 'ekke'; 5 | import assume from 'assume'; 6 | import React from 'react'; 7 | 8 | describe('(ekke) production', function () { 9 | it('is not the same as our development render', function () { 10 | assume(renderProd).is.a('function'); 11 | assume(render).is.a('function'); 12 | assume(render).does.not.equal(renderProd); 13 | }); 14 | 15 | describe('#render', function () { 16 | it('throw an error, as it doesnt work in prod', async function () { 17 | const done = assume.plan(2); 18 | 19 | try { await renderProd(); } 20 | catch (e) { 21 | assume(e).is.a('error'); 22 | assume(e.message).equals('render method is disabled in production'); 23 | } 24 | 25 | done(); 26 | }); 27 | }); 28 | 29 | describe('#Ekke', function () { 30 | it('it can still render as React component', async function () { 31 | await render(); 32 | 33 | const ref = React.createRef(); 34 | await render( 35 | 36 | 37 | 38 | ); 39 | 40 | assume(ref.current).is.a('object'); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/ekke/subway.test.js: -------------------------------------------------------------------------------- 1 | import createEkke from '../../components/ekke'; 2 | import Subway from '../../native/subway'; 3 | import EventEmitter from 'eventemitter3'; 4 | import pkg from '../../package.json'; 5 | import { describe, it } from 'mocha'; 6 | import * as RN from 'react-native'; 7 | import { render } from 'ekke'; 8 | import ms from 'millisecond'; 9 | import assume from 'assume'; 10 | import yeast from 'yeast'; 11 | 12 | describe('(ekke) Subway', function () { 13 | let sub; 14 | 15 | beforeEach(function () { 16 | // 17 | // We want to re-use our defaults to establish a connection. 18 | // 19 | const { defaultProps } = createEkke(() => {}); 20 | sub = new Subway(defaultProps.hostname, defaultProps.port); 21 | }); 22 | 23 | it('is an EventEmitter', function () { 24 | assume(sub).is.instanceOf(EventEmitter); 25 | }); 26 | 27 | describe('#bundle', function () { 28 | this.timeout(ms('2 minutes')); 29 | let bundle; 30 | 31 | before(async function () { 32 | bundle = await sub.bundle({ entry: 'package.json' }); 33 | }); 34 | 35 | it('requests the package.json as bundle', async function () { 36 | assume(bundle).includes(pkg.description); 37 | assume(bundle).includes(pkg.scripts.test); 38 | }); 39 | 40 | it('can request any file from the project root', async function () { 41 | const test = await sub.bundle({ entry: 'test/ekke/subway.test.js' }); 42 | 43 | assume(test).includes('yeah its going to include what ever we put in this assertion random words like this'); 44 | }); 45 | 46 | it('can minify the bundle', async function () { 47 | const test = await sub.bundle({ entry: 'package.json', minify: true }); 48 | 49 | assume(test).does.not.equal(bundle); 50 | assume(bundle).does.includes(`\n\n`); 51 | assume(test).does.not.include(`\n\n`); 52 | }); 53 | 54 | it('can download development and prod builds', async function () { 55 | const test = await sub.bundle({ entry: 'package.json', dev: false }); 56 | 57 | assume(test).includes('__DEV__=false'); 58 | assume(bundle).includes('__DEV__=true'); 59 | }); 60 | }); 61 | 62 | describe('communication', function () { 63 | it('it flushes the message queue on connect', function (next) { 64 | const id = yeast(); 65 | 66 | sub.once('pong', function (args, send) { 67 | assume(sub.queue).is.length(0); 68 | 69 | assume(args).is.a('array'); 70 | assume(args[0]).equals(id); 71 | assume(send).equals(sub.send); 72 | 73 | disconnect(); 74 | next(); 75 | }); 76 | 77 | assume(sub.queue).is.length(0); 78 | sub.send('ping', id); 79 | assume(sub.queue).is.length(1); 80 | 81 | const disconnect = sub.connect(); 82 | }); 83 | 84 | it('does not queue messages after connection', function (next) { 85 | const disconnect = sub.connect(); 86 | 87 | sub.socket.addEventListener('open', function () { 88 | sub.once('pong', function (args) { 89 | assume(args[0]).equals('yeet'); 90 | 91 | disconnect(); 92 | next(); 93 | }); 94 | 95 | assume(sub.queue).is.length(0); 96 | sub.send('ping', 'yeet'); 97 | assume(sub.queue).is.length(0); 98 | }); 99 | }); 100 | }); 101 | 102 | describe('#alive', function () { 103 | it('calls the setup function', function (next) { 104 | sub.setup = next; 105 | sub.alive(); 106 | }); 107 | 108 | it('calls the supplied callback', function (next) { 109 | sub.setup = () => {}; 110 | sub.alive(next); 111 | }) 112 | }); 113 | 114 | describe('#request', function () { 115 | it('makes a HTTP request', async function () { 116 | this.timeout(5000); 117 | 118 | const resp = await sub.request({ 119 | url: 'https://google.com', 120 | method: 'GET' 121 | }); 122 | 123 | assume(resp).is.a('string'); 124 | assume(resp).includes('google.com'); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/mock/errorutils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The exception handler. 3 | * 4 | * @type {Function} 5 | * @public 6 | */ 7 | let handler; 8 | 9 | /** 10 | * Returns the current exception handler. 11 | * 12 | * @returns {Function|Undefined} Handler. 13 | * @public 14 | */ 15 | function getGlobalHandler() { 16 | return handler; 17 | } 18 | 19 | /** 20 | * Set a new exception handler. 21 | * 22 | * @param {Function} fn The new handler. 23 | * @public 24 | */ 25 | function setGlobalHandler(fn) { 26 | handler = fn; 27 | } 28 | 29 | /** 30 | * Simulate an uncaught exception by triggering the handler. 31 | * 32 | * @param {Error} err Error that caused the exception. 33 | * @param {Boolean} fatal Was the error fatal. 34 | * @private 35 | */ 36 | function simulate(err, fatal = false) { 37 | if (handler) handler.call(handler, err, fatal); 38 | } 39 | 40 | export { 41 | getGlobalHandler, 42 | setGlobalHandler, 43 | simulate 44 | }; 45 | -------------------------------------------------------------------------------- /test/mock/index.js: -------------------------------------------------------------------------------- 1 | import * as ErrorUtils from './errorutils.js'; 2 | import AsyncStorage from 'asyncstorageapi'; 3 | import poison from 'require-poisoning'; 4 | import './servers'; 5 | 6 | // 7 | // ErrorUtils: While introduced by React-Native, is a global, not an actual 8 | // module that needs to be imported. 9 | // 10 | // XMLHttpRequest: It's 2019, and this is still not a thing. 11 | // 12 | // WebSocket: Yup, also still not a thing in Node. 13 | // 14 | global.ErrorUtils = ErrorUtils; 15 | global.XMLHttpRequest = require('xhr2'); 16 | global.WebSocket = require('ws'); 17 | 18 | const mocks = { 19 | AsyncStorage, 20 | Platform: { 21 | OS: 'ios' 22 | } 23 | }; 24 | 25 | // 26 | // We're just gonna polyfill the whole React-Native API with a proxy 27 | // so we have absolute control over the API's and methods that we 28 | // want to mock. 29 | // 30 | poison('react-native', new Proxy(Object.create(null), { 31 | get: function getter(target, name) { 32 | return mocks[name]; 33 | } 34 | })); 35 | -------------------------------------------------------------------------------- /test/mock/servers.js: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | 3 | const example = nock('http://example.com'); 4 | 5 | example.persist().get('/foo').reply(200, 'this is a reply'); 6 | example.persist().get('/404').reply(404, 'Missing'); 7 | example.persist().get('/500').reply(500, 'Error'); 8 | 9 | example 10 | .persist() 11 | .get('/error') 12 | .replyWithError('All the pipes broke'); 13 | 14 | example 15 | .persist() 16 | .get('/timeout') 17 | .reply(200, (uri, body, fn) => { 18 | setTimeout(fn.bind(fn, null, [200, 'Done']), 2000); 19 | }); 20 | -------------------------------------------------------------------------------- /test/native/bridge.test.js: -------------------------------------------------------------------------------- 1 | import bridge, { render } from '../../native/bridge'; 2 | import EventEmitter from 'eventemitter3'; 3 | import { describe, it } from 'mocha'; 4 | import assume from 'assume'; 5 | 6 | describe('(native) bridge', function () { 7 | it('exposes a render method', function () { 8 | assume(render).is.a('function'); 9 | }); 10 | 11 | it('exposes the eventemitter', function () { 12 | assume(bridge).is.instanceOf(EventEmitter); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/native/lifecycle.test.js: -------------------------------------------------------------------------------- 1 | import before from '../../native/lifecycle'; 2 | import { describe, it } from 'mocha'; 3 | import assume from 'assume'; 4 | 5 | describe('(native) lifecycle', function () { 6 | const api = { 7 | send: () => {} 8 | }; 9 | 10 | it('is an async function', function () { 11 | assume(before).is.a('asyncfunction'); 12 | }); 13 | 14 | it('returns an async function', async function () { 15 | const after = await before({ ...api }); 16 | 17 | assume(after).is.a('asyncfunction'); 18 | await after(); 19 | }); 20 | 21 | it('intercepts console[info|warn|log|error]', async function () { 22 | const asserts = [ 23 | { method: 'warn', payload: ['what', 'is', 'up'] }, 24 | { method: 'log', payload: ['im just loggin', { data: 'here' }] }, 25 | { method: 'error', payload: ['error here'] }, 26 | { method: 'info', payload: ['works as intended'] } 27 | ]; 28 | 29 | const send = (method, ...payload) => { 30 | assume({ method, payload }).deep.equals(asserts.shift()); 31 | }; 32 | 33 | const after = await before({ send }); 34 | 35 | console.warn('what', 'is', 'up'); 36 | console.log('im just loggin', { data: 'here' }); 37 | console.error('error here'); 38 | console.info('works as intended'); 39 | 40 | await after(); 41 | console.log(' V This log message is intended, please ignore in test output'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/native/packagejson.test.js: -------------------------------------------------------------------------------- 1 | import pkg from '../../package.json'; 2 | import { describe, it } from 'mocha'; 3 | import assume from 'assume'; 4 | 5 | describe('(ekke) package.json', function () { 6 | const ekke = '../../ekke.js'; 7 | 8 | beforeEach(function () { 9 | const key = require.resolve(ekke); 10 | delete require.cache[key]; 11 | }); 12 | 13 | function prod() { 14 | process.env.NODE_ENV = 'production'; 15 | return require(ekke); 16 | } 17 | 18 | function dev() { 19 | process.env.NODE_ENV = 'asfasdfas'; 20 | return require(ekke); 21 | } 22 | 23 | it('has a different react-native entry point', function () { 24 | assume(pkg.main).does.not.equal(pkg['react-native']); 25 | 26 | assume(pkg.main).equals('api/index.js'); 27 | assume(pkg['react-native']).equals('ekke.js'); 28 | }); 29 | 30 | describe('production/development builds', function () { 31 | it('loads `production` for NODE_ENV=production', function () { 32 | const { Ekke, render } = prod(); 33 | 34 | assume(Ekke.prod).is.true(); 35 | assume(Ekke.dev).is.false(); 36 | 37 | assume(render).is.a('function'); 38 | }); 39 | 40 | it('loads `development` for NODE_ENV= the rest', function () { 41 | const { Ekke, render } = dev(); 42 | 43 | assume(Ekke.prod).is.false(); 44 | assume(Ekke.dev).is.true(); 45 | 46 | assume(render).is.a('function'); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/native/subway.test.js: -------------------------------------------------------------------------------- 1 | import Subway from '../../native/subway.js'; 2 | import { describe, it } from 'mocha'; 3 | import assume from 'assume'; 4 | 5 | describe('(native) subway', function () { 6 | let sub; 7 | 8 | beforeEach(function () { 9 | sub = new Subway('localhost', 1975); 10 | }); 11 | 12 | afterEach(function () { 13 | sub.destroy(); 14 | }); 15 | 16 | describe('#send', function () { 17 | it('is a function', function () { 18 | assume(sub.send).is.a('function'); 19 | }); 20 | 21 | it('queues the message (we have no connection)', function () { 22 | assume(sub.queue).is.a('array'); 23 | assume(sub.queue).is.length(0); 24 | 25 | sub.send('event', 'message', 'included', 'in', 'payload'); 26 | 27 | assume(sub.queue).is.length(1); 28 | assume(sub.queue[0]).is.a('string'); 29 | assume(sub.queue[0]).equals(JSON.stringify({ 30 | event: 'event', 31 | payload: ['message', 'included', 'in', 'payload'] 32 | })); 33 | }); 34 | }); 35 | 36 | describe('#request', function () { 37 | it('makes a http request', async function () { 38 | const res = await sub.request({ 39 | url: 'http://example.com/foo', 40 | method: 'GET' 41 | }); 42 | 43 | assume(res).is.a('string'); 44 | assume(res).equals('this is a reply'); 45 | }); 46 | 47 | it('throws an error when a non 200 status code is received', async function () { 48 | const done = assume.plan(4); 49 | 50 | try { 51 | await sub.request({ 52 | url: 'http://example.com/404', 53 | method: 'GET' 54 | }); 55 | } catch (e) { 56 | assume(e).is.a('error'); 57 | assume(e.message).equals('Incorrect status code recieved: 404'); 58 | } 59 | 60 | try { 61 | await sub.request({ 62 | url: 'http://example.com/500', 63 | method: 'GET' 64 | }); 65 | } catch (e) { 66 | assume(e).is.a('error'); 67 | assume(e.message).equals('Incorrect status code recieved: 500'); 68 | } 69 | 70 | done(); 71 | }); 72 | 73 | it('throws an error on timeout', async function () { 74 | const done = assume.plan(2); 75 | 76 | try { 77 | await sub.request({ 78 | url: 'http://example.com/timeout', 79 | timeout: '1 second', 80 | method: 'GET' 81 | }); 82 | } catch (e) { 83 | assume(e).is.a('error'); 84 | assume(e.message).equals('Failed to process request in a timely manner'); 85 | } 86 | 87 | done(); 88 | }); 89 | 90 | it('throws an error when shit breaks', async function () { 91 | const done = assume.plan(1); 92 | 93 | try { 94 | await sub.request({ 95 | url: 'http://example.com/error', 96 | method: 'GET' 97 | }); 98 | } catch (e) { 99 | assume(e).is.a('error'); 100 | } 101 | 102 | done(); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/native/uncaught.test.js: -------------------------------------------------------------------------------- 1 | import uncaught from '../../native/uncaught'; 2 | import { describe, it } from 'mocha'; 3 | import assume from 'assume'; 4 | 5 | describe('(native) uncaught', function () { 6 | function fixture() {} 7 | 8 | beforeEach(function () { 9 | ErrorUtils.setGlobalHandler(fixture); 10 | assume(ErrorUtils.getGlobalHandler()).equals(fixture); 11 | }); 12 | 13 | it('is exported as a function', function () { 14 | assume(uncaught).is.a('function'); 15 | }); 16 | 17 | it('returns a destroy function when called', function () { 18 | const destroy = uncaught(() => {}); 19 | 20 | assume(destroy).is.a('function'); 21 | }); 22 | 23 | it('restores the previous callback when destroyed', function () { 24 | const destroy = uncaught(() => {}); 25 | assume(ErrorUtils.getGlobalHandler()).does.not.equals(fixture); 26 | 27 | destroy(); 28 | assume(ErrorUtils.getGlobalHandler()).equals(fixture); 29 | }); 30 | 31 | it('does not restore previous callback when overriden', function () { 32 | const destroy = uncaught(() => {}); 33 | 34 | function nope() {} 35 | ErrorUtils.setGlobalHandler(nope); 36 | 37 | destroy(); 38 | assume(ErrorUtils.getGlobalHandler()).equals(nope); 39 | }); 40 | 41 | it('can call the destroy method multiple times', function () { 42 | function one() {} 43 | function two() {} 44 | 45 | ErrorUtils.setGlobalHandler(one); 46 | ErrorUtils.setGlobalHandler(two); 47 | 48 | const destroy = uncaught(() => {}); 49 | 50 | destroy(); 51 | assume(ErrorUtils.getGlobalHandler()).equals(two); 52 | 53 | destroy(); 54 | assume(ErrorUtils.getGlobalHandler()).equals(two); 55 | 56 | destroy(); 57 | assume(ErrorUtils.getGlobalHandler()).equals(two); 58 | }); 59 | 60 | it('executes the callback when an exception is thrown', function (next) { 61 | uncaught(function (e, fatal) { 62 | assume(e).is.a('error'); 63 | assume(e.message).equals('custom error'); 64 | assume(fatal).is.false(); 65 | 66 | next(); 67 | }); 68 | 69 | ErrorUtils.simulate(new Error('custom error')); 70 | }); 71 | 72 | it('restores the previous listener on callback execution', function (next) { 73 | uncaught(function (e, fatal) { 74 | assume(e).is.a('error'); 75 | assume(e.message).equals('custom error'); 76 | assume(fatal).is.false(); 77 | 78 | next(); 79 | }); 80 | 81 | ErrorUtils.simulate(new Error('custom error')); 82 | assume(ErrorUtils.getGlobalHandler()).equals(fixture); 83 | }); 84 | }); 85 | --------------------------------------------------------------------------------