testem 3.20.0 → 3.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -132
- package/lib/api.js +1 -1
- package/lib/cli-local-delegation.js +93 -0
- package/lib/config.js +52 -11
- package/lib/reporters/dev/runner_tabs.js +52 -47
- package/lib/reporters/dev/toast_notify.js +5 -0
- package/lib/server/index.js +8 -2
- package/package.json +5 -7
- package/testem.js +6 -1
package/README.md
CHANGED
|
@@ -1,53 +1,50 @@
|
|
|
1
1
|
Got Scripts? Test’em!
|
|
2
2
|
=================
|
|
3
3
|
|
|
4
|
-
[](https://github.com/testem/testem/actions/workflows/ci.yml?query=branch%3Amaster) [](
|
|
4
|
+
[](https://github.com/testem/testem/actions/workflows/ci.yml?query=branch%3Amaster) [](https://badge.fury.io/js/testem)
|
|
5
5
|
|
|
6
|
-
Testem is a **JavaScript test runner** that runs your tests in **real desktop browsers**—Chrome, Firefox, Safari, Edge, and others you launch—so your specs execute in the same browser engines and DOM your users get, not a pretend environment. It also runs tests in **[Node](
|
|
6
|
+
Testem is a **JavaScript test runner** that runs your tests in **real desktop browsers**—Chrome, Firefox, Safari, Edge, and others you launch—so your specs execute in the same browser engines and DOM your users get, not a pretend environment. It also runs tests in **[Node](https://nodejs.org/)**, **Chrome** (including **headless** runs via `browser_args`, e.g. `--headless`), or any launcher you configure. It is **framework-agnostic** and aimed at **any kind of tests** you want to run: unit, integration, end-to-end style suites, or custom setups—you pick the style; Testem wires it to the browser or process.
|
|
7
7
|
|
|
8
|
-
Unit testing in
|
|
8
|
+
Unit testing in JavaScript can be tedious and painful, but Testem makes it so easy that you will actually *want* to write tests.
|
|
9
9
|
|
|
10
10
|
Features
|
|
11
11
|
--------
|
|
12
12
|
|
|
13
13
|
* Test-framework agnostic — designed for **any kind of tests** that fit your project (unit, integration, custom runners, etc.), not a single prescribed style. Support for
|
|
14
|
-
- [Jasmine](
|
|
15
|
-
- [QUnit](
|
|
16
|
-
- [Mocha](
|
|
14
|
+
- [Jasmine](https://jasmine.github.io/)
|
|
15
|
+
- [QUnit](https://qunitjs.com/)
|
|
16
|
+
- [Mocha](https://mochajs.org/)
|
|
17
17
|
- Others, through custom test framework adapters.
|
|
18
|
-
* Run tests in **all** major **real** browsers (your tests load and run in the actual browser) as well as [Node](
|
|
18
|
+
* Run tests in **all** major **real** browsers (your tests load and run in the actual browser) as well as [Node](https://nodejs.org) and **Chrome** (use `browser_args` with `--headless` for headless runs—see `docs/browser_args.md`)
|
|
19
19
|
* Two distinct use-cases:
|
|
20
20
|
- Test-Driven-Development(TDD) — designed to streamline the TDD workflow
|
|
21
|
-
- Continuous Integration(CI) — designed to work well with
|
|
21
|
+
- Continuous Integration(CI) — designed to work well with **GitHub Actions** and other CI systems, including Jenkins and TeamCity
|
|
22
22
|
* Cross-platform support
|
|
23
|
-
-
|
|
23
|
+
- macOS
|
|
24
24
|
- Windows
|
|
25
25
|
- Linux
|
|
26
26
|
* Preprocessor support
|
|
27
|
-
-
|
|
27
|
+
- Babel
|
|
28
|
+
- TypeScript
|
|
28
29
|
- Browserify
|
|
29
|
-
- JSHint/JSLint
|
|
30
|
+
- ESLint/JSHint/JSLint
|
|
31
|
+
- CoffeeScript
|
|
30
32
|
- everything else
|
|
31
33
|
|
|
32
|
-
**Internet Explorer** and **PhantomJS** are still supported as launchers, but we document them as **deprecated** targets: keeping them viable through transpilation and polyfills is likely to get more difficult over time, so prefer evergreen browsers or Node for new projects. See the [configuration reference](docs/config_file.md) for how we categorize browsers.
|
|
33
|
-
|
|
34
|
-
Screencasts
|
|
35
|
-
-----------
|
|
36
|
-
|
|
37
|
-
* Watch this **[introductory screencast (11:39)](http://www.youtube.com/watch?v=-1mjv4yk5JM)** to see it in action! This one demonstrates the TDD workflow.
|
|
38
|
-
* [Launchers (12:10)](http://www.youtube.com/watch?v=Up0lVjWk9Rk) — more detail about launchers: how to specify what to auto-launch and how to configure one yourself to run tests in **Node**.
|
|
39
|
-
* [Continuous Integration (CI) Mode (4:24)](http://www.youtube.com/watch?v=Js16Cj80HKY) — details about how CI mode works.
|
|
40
|
-
* [Making JavaScript Testing Fun With Testem (22:53)](http://net.tutsplus.com/tutorials/javascript-ajax/make-javascript-testing-fun-with-testem/) — a thorough screencast by NetTuts+'s Jeffery Way covering the basics, Jasmine, Mocha/Chai, CoffeeScript and more!
|
|
41
|
-
|
|
42
34
|
Installation
|
|
43
35
|
------------
|
|
44
|
-
|
|
36
|
+
Testem needs a supported **[Node.js](https://nodejs.org/)** runtime. The required range is defined in [`package.json`](package.json) under `engines` (currently **^20.19.0**, **^22.12.0**, or **>=24.0.0**).
|
|
37
|
+
|
|
38
|
+
**Recommended:** install Testem **both** as a **dev dependency** (so your project pins a version) **and** **globally** (so the `testem` command is always available on your `PATH`):
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
```bash
|
|
41
|
+
npm install testem --save-dev
|
|
42
|
+
npm install testem -g
|
|
43
|
+
```
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
When you run the global `testem` inside a project directory that has a local `testem` install, the CLI **automatically re-runs the local copy** so the version stays in sync with `package.json`. Set **`TESTEM_USE_GLOBAL=1`** if you ever need to force the global binary only.
|
|
49
46
|
|
|
50
|
-
This
|
|
47
|
+
This README uses the `testem` command in examples; add it to npm scripts or invoke it as `testem` from a shell after the installs above.
|
|
51
48
|
|
|
52
49
|
Usage
|
|
53
50
|
-----
|
|
@@ -146,6 +143,8 @@ To use Testem for continuous integration
|
|
|
146
143
|
|
|
147
144
|
testem ci
|
|
148
145
|
|
|
146
|
+
**GitHub Actions** is a common way to run Testem in CI: add a workflow job that runs `testem ci` (often with the **Headless Chrome** or **Chromium** launcher). This project’s own workflow is in [`.github/workflows/ci.yml`](https://github.com/testem/testem/blob/master/.github/workflows/ci.yml).
|
|
147
|
+
|
|
149
148
|
In CI mode, Testem runs your tests on all the browsers that are available on the system one after another.
|
|
150
149
|
|
|
151
150
|
You can run multiple browsers in parallel in CI mode by specifying the `--parallel` (or `-P`) option to be the number of concurrent running browsers.
|
|
@@ -166,11 +165,12 @@ Will print them out. The output might look like
|
|
|
166
165
|
Safari
|
|
167
166
|
Safari Technology Preview
|
|
168
167
|
Opera
|
|
169
|
-
PhantomJS
|
|
170
168
|
|
|
171
|
-
|
|
169
|
+
Your machine may list other launchers too. For **headless** runs, prefer **Chrome** with `browser_args` (for example `--headless`) rather than the deprecated PhantomJS launcher—see `docs/browser_args.md`.
|
|
170
|
+
|
|
171
|
+
When you run `testem ci` to run tests, it outputs the results in the [TAP](https://testanything.org/) format by default, which looks like
|
|
172
172
|
|
|
173
|
-
ok 1 Chrome
|
|
173
|
+
ok 1 Chrome 130.0 - hello should say hello.
|
|
174
174
|
|
|
175
175
|
1..1
|
|
176
176
|
# tests 1
|
|
@@ -178,9 +178,9 @@ When you run `testem ci` to run tests, it outputs the results in the [TAP](http:
|
|
|
178
178
|
|
|
179
179
|
# ok
|
|
180
180
|
|
|
181
|
-
TAP is a human-readable and language-agnostic test result format.
|
|
181
|
+
TAP is a human-readable and language-agnostic test result format. On **GitHub Actions**, a typical pattern is a step that runs `testem ci` and relies on the exit code to fail the job (see [`.github/workflows/ci.yml`](https://github.com/testem/testem/blob/master/.github/workflows/ci.yml) in this repository). For **Jenkins** and **TeamCity**, use TAP plugins:
|
|
182
182
|
|
|
183
|
-
* [Jenkins TAP plugin](https://
|
|
183
|
+
* [Jenkins TAP plugin](https://plugins.jenkins.io/tap/) - I've added [detailed instructions](https://github.com/testem/testem/blob/master/docs/use_with_jenkins.md) for setup with Jenkins.
|
|
184
184
|
* [TeamCity TAP plugin](https://github.com/pavelsher/teamcity-tap-parser)
|
|
185
185
|
|
|
186
186
|
## TAP Options
|
|
@@ -225,13 +225,14 @@ Testem has other test reporters besides TAP: `dot`, `xunit` and `teamcity`. You
|
|
|
225
225
|
|
|
226
226
|
You can also [add your own reporter](docs/custom_reporter.md).
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
<details>
|
|
229
|
+
<summary>Example <code>xunit</code> reporter output</summary>
|
|
229
230
|
|
|
230
231
|
Note that the real output is not pretty printed.
|
|
231
232
|
```xml
|
|
232
|
-
<testsuite name="Testem Tests" tests="4" failures="1" timestamp="
|
|
233
|
-
<testcase classname="
|
|
234
|
-
<testcase classname="
|
|
233
|
+
<testsuite name="Testem Tests" tests="4" failures="1" timestamp="2026-04-25T10:00:00.000Z" time="9">
|
|
234
|
+
<testcase classname="Firefox 128" name="myFunc returns true when input is valid" time="0"/>
|
|
235
|
+
<testcase classname="Firefox 128" name="myFunc returns false when user tickles it" time="0"/>
|
|
235
236
|
<testcase classname="Chrome" name="myFunc returns true when input is valid" time="0"/>
|
|
236
237
|
<testcase classname="Chrome" name="myFunc returns false when user tickles it" time="0">
|
|
237
238
|
<failure name="myFunc returns false when user tickles it" message="function is not ticklish">
|
|
@@ -243,17 +244,24 @@ Note that the real output is not pretty printed.
|
|
|
243
244
|
</testsuite>
|
|
244
245
|
```
|
|
245
246
|
|
|
246
|
-
|
|
247
|
+
</details>
|
|
248
|
+
|
|
249
|
+
<details>
|
|
250
|
+
<summary>Example <code>teamcity</code> reporter output</summary>
|
|
247
251
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
252
|
+
```text
|
|
253
|
+
##teamcity[testStarted name='Firefox 128 - hello should say hello']
|
|
254
|
+
##teamcity[testFinished name='Firefox 128 - hello should say hello']
|
|
255
|
+
##teamcity[testStarted name='Firefox 128 - hello should say hello to person']
|
|
256
|
+
##teamcity[testFinished name='Firefox 128 - hello should say hello to person']
|
|
257
|
+
##teamcity[testStarted name='Firefox 128 - goodbye should say goodbye']
|
|
258
|
+
##teamcity[testFailed name='Firefox 128 - goodbye should say goodbye' message='expected |'hello world|' to equal |'goodbye world|'' details='AssertionError: expected |'hello world|' to equal |'goodbye world|'|n at http://localhost:7357/testem/chai.js:873|n at assertEqual (http://localhost:7357/testem/chai.js:1386)|n at http://localhost:7357/testem/chai.js:3627|n at http://localhost:7357/hello_spec.js:14|n at callFn (http://localhost:7357/testem/mocha.js:4338)|n at http://localhost:7357/testem/mocha.js:4331|n at http://localhost:7357/testem/mocha.js:4728|n at http://localhost:7357/testem/mocha.js:4819|n at next (http://localhost:7357/testem/mocha.js:4653)|n at http://localhost:7357/testem/mocha.js:4663|n at next (http://localhost:7357/testem/mocha.js:4601)|n at http://localhost:7357/testem/mocha.js:4630|n at timeslice (http://localhost:7357/testem/mocha.js:5761)']
|
|
259
|
+
##teamcity[testFinished name='Firefox 128 - goodbye should say goodbye']
|
|
255
260
|
|
|
256
|
-
|
|
261
|
+
##teamcity[testSuiteFinished name='mocha.suite' duration='11091']
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
</details>
|
|
257
265
|
|
|
258
266
|
### Command line options
|
|
259
267
|
|
|
@@ -264,9 +272,9 @@ To see all command line options for CI
|
|
|
264
272
|
Configuration File
|
|
265
273
|
------------------
|
|
266
274
|
|
|
267
|
-
For the simplest JavaScript projects, the TDD workflow described above will work fine. There are times when you want
|
|
268
|
-
|
|
269
|
-
This calls for the `testem.json` configuration file (you can also alternatively use the YAML format with a `testem.yml` file). It looks like
|
|
275
|
+
For the simplest JavaScript projects, the TDD workflow described above will work fine. There are times when you want to structure your source files into separate directories, or want to have finer control over what files to include.
|
|
276
|
+
|
|
277
|
+
This calls for the `testem.json` configuration file (you can also alternatively use the YAML format with a `testem.yml` file or return json from a javascript file `testem.js`). It looks like
|
|
270
278
|
|
|
271
279
|
```json
|
|
272
280
|
{
|
|
@@ -334,10 +342,10 @@ Or if you are using require.js or another loader, just make sure you load `/test
|
|
|
334
342
|
|
|
335
343
|
### Dynamic Substitution
|
|
336
344
|
|
|
337
|
-
To enable dynamic substitutions within the
|
|
345
|
+
To enable dynamic substitutions within the JavaScript files in your custom test page, you must
|
|
338
346
|
|
|
339
347
|
1. name your test page using `.mustache` as the extension
|
|
340
|
-
2. use `{{#serve_files}}` to loop over the set of
|
|
348
|
+
2. use `{{#serve_files}}` to loop over the set of JavaScript files to be served, and then reference its `src` property to access their path (or `{{#css_files}}` for stylesheets)
|
|
341
349
|
|
|
342
350
|
Example:
|
|
343
351
|
|
|
@@ -391,14 +399,14 @@ You can add your own custom paths to browser binaries by including `browser_path
|
|
|
391
399
|
|
|
392
400
|
```javascript
|
|
393
401
|
"browser_paths": {
|
|
394
|
-
"Chromium": "./
|
|
402
|
+
"Chromium": "./path/to/chromium"
|
|
395
403
|
}
|
|
396
404
|
"browser_exes": {
|
|
397
405
|
"Chromium": "chrome-custom-binary"
|
|
398
406
|
}
|
|
399
407
|
```
|
|
400
408
|
|
|
401
|
-
Adding a browser_path for a browser will override all default places for testem to look for the browser. So if the browser doesn't exist at the path you provided, you will get failures.
|
|
409
|
+
Set `Chromium` to a real browser binary path, for example the Chromium or Chrome under your Puppeteer install (the exact `path/to/chromium` depends on your platform and Puppeteer version). Adding a browser_path for a browser will override all default places for testem to look for the browser. So if the browser doesn't exist at the path you provided, you will get failures.
|
|
402
410
|
|
|
403
411
|
Customizing Browser Arguments
|
|
404
412
|
-----------------------------
|
|
@@ -435,7 +443,7 @@ When you run `testem`, it will auto-launch the mocha process based on the specif
|
|
|
435
443
|
Processes with TAP Output
|
|
436
444
|
-------------------------
|
|
437
445
|
|
|
438
|
-
If your process outputs test results in [TAP](
|
|
446
|
+
If your process outputs test results in [TAP](https://en.wikipedia.org/wiki/Test_Anything_Protocol) format, you can tell that to testem via the `protocol` property. For example
|
|
439
447
|
|
|
440
448
|
```javascript
|
|
441
449
|
"launchers": {
|
|
@@ -448,61 +456,38 @@ If your process outputs test results in [TAP](http://en.wikipedia.org/wiki/Test_
|
|
|
448
456
|
|
|
449
457
|
When this is done, Testem will read in the process's stdout and parse it as TAP, and then display the test results in Testem's normal format. It will also hide the process's stdout output from the console log panel, although it will still display the stderr.
|
|
450
458
|
|
|
451
|
-
|
|
452
|
-
|
|
459
|
+
Headless Chrome
|
|
460
|
+
---------------
|
|
453
461
|
|
|
454
|
-
|
|
462
|
+
Testem ships a **Headless Chrome** launcher that runs [Google Chrome](https://www.google.com/chrome/) (or Chromium where that is the detected binary) with **`--headless`**, **`--disable-gpu`**, and other defaults suitable for CI and unattended runs. You need Chrome installed; run
|
|
455
463
|
|
|
456
464
|
testem launchers
|
|
457
465
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
If you want to debug tests in PhantomJS, include the `phantomjs_debug_port` option in your testem configuration, referencing an available port number. Once testem has started PhantomJS, navigate (with a traditional browser) to http://localhost:<port> and attach to one of PhantomJS's browser tabs (probably the second one in the list). `debugger` statements will now break in the debugging console.
|
|
461
|
-
|
|
462
|
-
If you want to use any of the [PhantomJS command line options](http://phantomjs.org/api/command-line.html), include the `phantomjs_args` option in your testem configuration. For example:
|
|
463
|
-
|
|
464
|
-
```javascript
|
|
465
|
-
"phantomjs_args": [
|
|
466
|
-
"--ignore-ssl-errors=true"
|
|
467
|
-
]
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
You can also customize the phantomjs launcher file by specifying the `phantomjs_launch_script` option.
|
|
471
|
-
In this launcher you can change options like the `viewPortSize`. See `assets/phantom.js` for the default launcher.
|
|
472
|
-
|
|
473
|
-
Preprocessors (CoffeeScript, LESS, Sass, Browserify, etc)
|
|
474
|
-
---------------------------------------------------------
|
|
475
|
-
|
|
476
|
-
If you need to run a preprocessor (or indeed any shell command before the start of the tests) use the `before_tests` option, such as
|
|
477
|
-
|
|
478
|
-
"before_tests": "coffee -c *.coffee"
|
|
466
|
+
and confirm **Headless Chrome** appears.
|
|
479
467
|
|
|
480
|
-
|
|
481
|
-
watched (see **File watching** under Development Mode above).
|
|
468
|
+
Select it like any other launcher, for example:
|
|
482
469
|
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
"
|
|
486
|
-
|
|
470
|
+
```json
|
|
471
|
+
{
|
|
472
|
+
"launch_in_ci": ["Headless Chrome"]
|
|
473
|
+
}
|
|
487
474
|
```
|
|
488
475
|
|
|
489
|
-
|
|
476
|
+
To add or override flags, use **`browser_args`** (see [`docs/browser_args.md`](docs/browser_args.md)). Testem merges these with the built-in headless arguments—for example a fixed remote-debugging port for attaching DevTools:
|
|
490
477
|
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
"
|
|
494
|
-
]
|
|
478
|
+
```json
|
|
479
|
+
{
|
|
480
|
+
"browser_args": {
|
|
481
|
+
"Headless Chrome": ["--remote-debugging-port=9222"]
|
|
482
|
+
}
|
|
483
|
+
}
|
|
495
484
|
```
|
|
496
485
|
|
|
497
|
-
|
|
486
|
+
**Remote debugging:** By default the headless launcher passes **`--remote-debugging-port=0`**, so Chrome chooses a random port—set a **fixed** port in `browser_args` (as in the example above) so you can connect reliably. With Testem running, open a regular Chrome window to **`chrome://inspect`**, use **Configure…** under “Discover network targets” to add **`localhost:9222`** (or whatever port you set), find your test page under **Remote Target**, and click **inspect**. You can also open **`http://localhost:9222`** and follow the links to a target page. **`debugger`** statements and breakpoints work in the DevTools **Sources** panel.
|
|
498
487
|
|
|
499
|
-
|
|
488
|
+
**Headless Chrome Beta** is available when the Chrome Beta channel is installed. For legacy **PhantomJS**-specific options (`phantomjs_args`, `phantomjs_debug_port`, etc.), see [`docs/config_file.md`](docs/config_file.md).
|
|
500
489
|
|
|
501
|
-
|
|
502
|
-
"after_tests": "rm *.js"
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
If you would prefer simply to clean up when Testem exits, you can use the `on_exit` option.
|
|
490
|
+
**Internet Explorer** and the legacy **PhantomJS** launcher are still available, but we document them as **deprecated** targets: keeping them viable through transpilation and polyfills is likely to get more difficult over time, so prefer evergreen browsers, **Chrome** with `--headless` for headless automation, or Node for new projects. See the [configuration reference](docs/config_file.md) for how we categorize browsers.
|
|
506
491
|
|
|
507
492
|
Running browser code after tests complete
|
|
508
493
|
-------------
|
|
@@ -540,7 +525,7 @@ Sometimes you may want to re-map a URL to a different directory on the file syst
|
|
|
540
525
|
+ public
|
|
541
526
|
+ tests.html
|
|
542
527
|
|
|
543
|
-
Let's say you want to serve `tests.html` at the top level url `/tests.html`, all the
|
|
528
|
+
Let's say you want to serve `tests.html` at the top level url `/tests.html`, all the JavaScript files under `/js` and all the css under `/css`. You can use the "routes" option to do that
|
|
544
529
|
|
|
545
530
|
```javascript
|
|
546
531
|
"routes": {
|
|
@@ -566,7 +551,7 @@ And then make sure you include the adapter code in your test suite and you are r
|
|
|
566
551
|
Native notifications
|
|
567
552
|
--------------------------------
|
|
568
553
|
|
|
569
|
-
If you'd prefer not to be looking at the terminal while developing, you can enable native notifications (e.g.
|
|
554
|
+
If you'd prefer not to be looking at the terminal while developing, you can enable native notifications (e.g. Notification Center on macOS) using the `-g` option.
|
|
570
555
|
|
|
571
556
|
API Proxy
|
|
572
557
|
--------------------------------
|
|
@@ -591,77 +576,110 @@ Simply add a `proxies` section to the `testem.json` configuration file.
|
|
|
591
576
|
```
|
|
592
577
|
|
|
593
578
|
This functionality is implemented as a *transparent proxy*, hence a request to
|
|
594
|
-
`http://localhost:7357/api/posts.json` will be proxied to `http://localhost:4200/api/posts.json` without removing the `/api` prefix. Setting the `secure` option to `false` as in the above `/xmlapi` configuration block will ignore TLS certificate validation and allow tests to successfully reach that URL even if testem was launched over HTTP. Other available options can be found here: https://github.com/
|
|
579
|
+
`http://localhost:7357/api/posts.json` will be proxied to `http://localhost:4200/api/posts.json` without removing the `/api` prefix. Setting the `secure` option to `false` as in the above `/xmlapi` configuration block will ignore TLS certificate validation and allow tests to successfully reach that URL even if testem was launched over HTTP. Other available options can be found here: https://github.com/http-party/node-http-proxy#options
|
|
595
580
|
|
|
596
581
|
To limit the functionality to only certain content types, use "onlyContentTypes".
|
|
597
582
|
|
|
583
|
+
Preprocessors (Babel, TypeScript, LESS, Sass, Browserify, CoffeeScript, etc)
|
|
584
|
+
---------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
If you need to run a preprocessor (or indeed any shell command before the start of the tests) use the `before_tests` option, such as
|
|
587
|
+
|
|
588
|
+
"before_tests": "coffee -c *.coffee"
|
|
589
|
+
|
|
590
|
+
or, with Babel (see the [Babel example](https://github.com/testem/testem/tree/master/examples/babel)):
|
|
591
|
+
|
|
592
|
+
"before_tests": "babel src --out-dir ."
|
|
593
|
+
|
|
594
|
+
or, with a `tsconfig.json` that emits JavaScript next to your project (see the [TypeScript example](https://github.com/testem/testem/tree/master/examples/typescript)):
|
|
595
|
+
|
|
596
|
+
"before_tests": "tsc"
|
|
597
|
+
|
|
598
|
+
And Testem will run it before each test run. Point **`src_files`** at the sources you want
|
|
599
|
+
watched (see **File watching** under Development Mode above).
|
|
600
|
+
|
|
601
|
+
```javascript
|
|
602
|
+
"src_files": [
|
|
603
|
+
"*.coffee"
|
|
604
|
+
]
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Since you want to be serving the `.js` files that are generated and not the `.coffee` files, you want to specify the `serve_files` option to tell it that
|
|
608
|
+
|
|
609
|
+
```javascript
|
|
610
|
+
"serve_files": [
|
|
611
|
+
"*.js"
|
|
612
|
+
]
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Testem will throw up a big ol' error dialog if the preprocessor command exits with an error code, so code checkers like JSHint or ESLint can be used here as well.
|
|
616
|
+
|
|
617
|
+
If you need to run a command after your tests have completed (such as removing compiled `.js` files), use the `after_tests` option.
|
|
618
|
+
|
|
619
|
+
```javascript
|
|
620
|
+
"after_tests": "rm *.js"
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
If you would prefer simply to clean up when Testem exits, you can use the `on_exit` option.
|
|
624
|
+
|
|
598
625
|
Example Projects
|
|
599
626
|
----------------
|
|
600
627
|
|
|
601
628
|
I've created [examples](https://github.com/testem/testem/tree/master/examples/) for various setups
|
|
602
629
|
|
|
630
|
+
* [Vite (middleware + plugin)](https://github.com/testem/testem/tree/master/examples/vite)
|
|
631
|
+
* [Electron](https://github.com/testem/testem/tree/master/examples/electron)
|
|
632
|
+
* [TypeScript Project](https://github.com/testem/testem/tree/master/examples/typescript)
|
|
633
|
+
* [ESLint Example](https://github.com/testem/testem/tree/master/examples/eslint)
|
|
634
|
+
* [Babel Project](https://github.com/testem/testem/tree/master/examples/babel)
|
|
603
635
|
* [Simple QUnit project](https://github.com/testem/testem/tree/master/examples/qunit_simple)
|
|
604
636
|
* [Simple Jasmine project](https://github.com/testem/testem/tree/master/examples/jasmine_simple)
|
|
605
637
|
* [Jasmine 2](https://github.com/testem/testem/tree/master/examples/jasmine2)
|
|
606
638
|
* [Custom Jasmine project](https://github.com/testem/testem/tree/master/examples/jasmine_custom)
|
|
607
|
-
* [Custom Jasmine project using Require.js](https://github.com/testem/testem/tree/master/examples/jasmine_requirejs)
|
|
608
639
|
* [Simple Mocha Project](https://github.com/testem/testem/tree/master/examples/mocha_simple)
|
|
609
640
|
* [Mocha + Chai](https://github.com/testem/testem/tree/master/examples/mocha_chai_simple)
|
|
610
641
|
* [Hybrid Project](https://github.com/testem/testem/tree/master/examples/hybrid_simple) - Mocha tests running in both the browser and Node.
|
|
611
|
-
* [Coffeescript Project](https://github.com/testem/testem/tree/master/examples/coffeescript)
|
|
612
|
-
* [Browserify Project](https://github.com/testem/testem/tree/master/examples/browserify)
|
|
613
|
-
* [JSHint Example](https://github.com/testem/testem/tree/master/examples/jshint)
|
|
614
642
|
* [Custom Test Framework](https://github.com/testem/testem/tree/master/examples/custom_adapter)
|
|
615
643
|
* [Tape Example](https://github.com/testem/testem/tree/master/examples/tape_example)
|
|
616
|
-
* [
|
|
617
|
-
* [
|
|
618
|
-
* [
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
644
|
+
* [Browserify Project](https://github.com/testem/testem/tree/master/examples/browserify)
|
|
645
|
+
* [JSHint Example](https://github.com/testem/testem/tree/master/examples/jshint)
|
|
646
|
+
* [Coffeescript Project](https://github.com/testem/testem/tree/master/examples/coffeescript)
|
|
647
|
+
* [Custom Jasmine project using Require.js](https://github.com/testem/testem/tree/master/examples/jasmine_requirejs)
|
|
648
|
+
* [BrowserStack Integration](https://github.com/testem/testem/tree/master/examples/browserstack)
|
|
649
|
+
* [SauceLabs Integration](https://github.com/testem/testem/tree/master/examples/saucelabs)
|
|
650
|
+
* [Code Coverage with Istanbul](https://github.com/testem/testem/tree/master/examples/coverage_istanbul)
|
|
622
651
|
|
|
623
|
-
|
|
652
|
+
Historical Screencasts
|
|
653
|
+
----------------------
|
|
624
654
|
|
|
625
|
-
|
|
655
|
+
These YouTube screencasts are from around **2012** and may not match the current UI or features, but they still illustrate the core ideas:
|
|
626
656
|
|
|
627
|
-
|
|
657
|
+
* **[Introductory screencast (11:39)](https://www.youtube.com/watch?v=-1mjv4yk5JM)** — TDD workflow
|
|
658
|
+
* **[Launchers (12:10)](https://www.youtube.com/watch?v=Up0lVjWk9Rk)** — auto-launch and running tests in **Node**
|
|
659
|
+
* **[CI mode (4:24)](https://www.youtube.com/watch?v=Js16Cj80HKY)** — continuous integration
|
|
628
660
|
|
|
629
661
|
Contributing
|
|
630
662
|
------------
|
|
631
663
|
|
|
632
664
|
If you want to [contribute to the project](https://github.com/testem/testem/blob/master/CONTRIBUTING.md), I am going to do my best to stay out of your way.
|
|
633
665
|
|
|
634
|
-
Roadmap
|
|
635
|
-
-------
|
|
636
|
-
|
|
637
|
-
1. [BrowserStack](http://www.browserstack.com/user/dashboard) integration - following [Bunyip](http://www.thecssninja.com/javascript/bunyip)'s example
|
|
638
|
-
2. Figure out a happy path for testing on mobile browsers (maybe BrowserStack).
|
|
639
|
-
|
|
640
666
|
Core Maintainer(s)
|
|
641
667
|
------------------
|
|
642
668
|
|
|
643
669
|
* [Johannes Würbach](https://github.com/johanneswuerbach)
|
|
644
670
|
|
|
645
|
-
Community
|
|
646
|
-
---------
|
|
647
|
-
|
|
648
|
-
* **Mailing list**: <https://groups.google.com/forum/?fromgroups#!forum/testem-users>
|
|
649
|
-
|
|
650
671
|
Credits
|
|
651
672
|
-------
|
|
652
673
|
|
|
653
674
|
Testem depends on the following great software
|
|
654
675
|
|
|
655
|
-
* [Jasmine](
|
|
656
|
-
* [QUnit](
|
|
657
|
-
* [Mocha](
|
|
658
|
-
* [Node](
|
|
659
|
-
* [Socket.IO](
|
|
660
|
-
* [
|
|
661
|
-
* [
|
|
662
|
-
* [
|
|
663
|
-
* [Node Commander](http://tjholowaychuk.com/post/9103188408/commander-js-nodejs-command-line-interfaces-made-easy)
|
|
676
|
+
* [Jasmine](https://jasmine.github.io/)
|
|
677
|
+
* [QUnit](https://qunitjs.com/)
|
|
678
|
+
* [Mocha](https://mochajs.org/)
|
|
679
|
+
* [Node](https://nodejs.org/)
|
|
680
|
+
* [Socket.IO](https://socket.io/)
|
|
681
|
+
* [tap-parser](https://github.com/tapjs/tap-parser)
|
|
682
|
+
* [Charm](https://github.com/aheckmann/charm)
|
|
683
|
+
* [Commander.js](https://github.com/tj/commander.js)
|
|
664
684
|
* [JS-Yaml](https://github.com/nodeca/js-yaml)
|
|
665
|
-
* [Express](
|
|
666
|
-
* [jQuery](http://jquery.com/)
|
|
667
|
-
* [Backbone](http://backbonejs.org/)
|
|
685
|
+
* [Express](https://expressjs.com/)
|
package/lib/api.js
CHANGED
|
@@ -10,7 +10,7 @@ const Server = require('./server');
|
|
|
10
10
|
/*
|
|
11
11
|
CLI-level options:
|
|
12
12
|
|
|
13
|
-
file: [String] configuration file (
|
|
13
|
+
file: [String] configuration file path (JSON, YAML, JS: testem.js / .cjs / .mjs and dotted variants)
|
|
14
14
|
host: [String] server host to use (localhost)
|
|
15
15
|
port: [Number] server port to use (7357)
|
|
16
16
|
launch: [Array] list of launchers to use for current runs (defaults to current mode)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Walks upward from `startDir` looking for `node_modules/testem/testem.js`.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} startDir
|
|
9
|
+
* @returns {string | null}
|
|
10
|
+
*/
|
|
11
|
+
function findLocalTestemScript(startDir) {
|
|
12
|
+
let dir = path.resolve(startDir);
|
|
13
|
+
const { root } = path.parse(dir);
|
|
14
|
+
while (true) {
|
|
15
|
+
const candidate = path.join(dir, 'node_modules', 'testem', 'testem.js');
|
|
16
|
+
if (fs.existsSync(candidate)) {
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
if (dir === root) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
dir = path.dirname(dir);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* If a local install exists and is not the same file as the running CLI (e.g. global),
|
|
29
|
+
* returns the path to the local `testem.js`. Otherwise returns null.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} currentFile - `__filename` of the running CLI
|
|
32
|
+
* @param {string} cwd
|
|
33
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
34
|
+
* @returns {string | null}
|
|
35
|
+
*/
|
|
36
|
+
function resolveLocalTestemCliOrNull(currentFile, cwd, env = process.env) {
|
|
37
|
+
if (env.TESTEM_USE_GLOBAL === '1') {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const local = findLocalTestemScript(cwd);
|
|
41
|
+
if (!local) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
let cur;
|
|
45
|
+
let loc;
|
|
46
|
+
try {
|
|
47
|
+
cur = fs.realpathSync(currentFile);
|
|
48
|
+
loc = fs.realpathSync(local);
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (cur === loc) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return local;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* When the globally installed `testem` is run inside a project that has a local
|
|
60
|
+
* `testem` dependency, re-exec that local copy so CLI and project stay in sync.
|
|
61
|
+
*
|
|
62
|
+
* Exits the process when delegation runs; otherwise returns normally.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} currentFile
|
|
65
|
+
* @param {string[]} argv
|
|
66
|
+
* @param {string} cwd
|
|
67
|
+
* @param {NodeJS.ProcessEnv} env
|
|
68
|
+
*/
|
|
69
|
+
function tryDelegateToLocalTestem(currentFile, argv, cwd, env) {
|
|
70
|
+
const local = resolveLocalTestemCliOrNull(currentFile, cwd, env);
|
|
71
|
+
if (!local) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const result = spawnSync(process.execPath, [local, ...argv.slice(2)], {
|
|
75
|
+
stdio: 'inherit',
|
|
76
|
+
env,
|
|
77
|
+
windowsHide: true,
|
|
78
|
+
});
|
|
79
|
+
if (result.error) {
|
|
80
|
+
console.error(result.error);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
if (result.signal) {
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
process.exit(result.status === null ? 0 : result.status);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
findLocalTestemScript,
|
|
91
|
+
resolveLocalTestemCliOrNull,
|
|
92
|
+
tryDelegateToLocalTestem,
|
|
93
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -14,6 +14,7 @@ const yaml = require('js-yaml');
|
|
|
14
14
|
const log = require('./log');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const url = require('url');
|
|
17
|
+
const { pathToFileURL } = url;
|
|
17
18
|
const querystring = require('querystring');
|
|
18
19
|
const { filter, reduce } = require('./utils/promises');
|
|
19
20
|
|
|
@@ -76,6 +77,8 @@ class Config {
|
|
|
76
77
|
'testem.yml',
|
|
77
78
|
'testem.js',
|
|
78
79
|
'.testem.js',
|
|
80
|
+
'testem.mjs',
|
|
81
|
+
'.testem.mjs',
|
|
79
82
|
'testem.cjs',
|
|
80
83
|
'.testem.cjs',
|
|
81
84
|
];
|
|
@@ -144,7 +147,7 @@ class Config {
|
|
|
144
147
|
if (callback) {
|
|
145
148
|
callback.call(this);
|
|
146
149
|
}
|
|
147
|
-
} else if (configFile.match(/\.c?js$/)) {
|
|
150
|
+
} else if (configFile.match(/\.(c?js|mjs)$/)) {
|
|
148
151
|
this.readJS(configFile, callback);
|
|
149
152
|
} else if (configFile.match(/\.json$/)) {
|
|
150
153
|
this.readJSON(configFile, callback);
|
|
@@ -158,21 +161,59 @@ class Config {
|
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
unwrapJsExport(exported) {
|
|
165
|
+
if (
|
|
166
|
+
exported &&
|
|
167
|
+
typeof exported === 'object' &&
|
|
168
|
+
exported.__esModule === true &&
|
|
169
|
+
Object.prototype.hasOwnProperty.call(exported, 'default')
|
|
170
|
+
) {
|
|
171
|
+
return exported.default;
|
|
172
|
+
}
|
|
173
|
+
return exported;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
loadJsExportedConfig(absPath) {
|
|
177
|
+
const ext = path.extname(absPath);
|
|
178
|
+
if (ext === '.cjs') {
|
|
179
|
+
return Promise.resolve(this.unwrapJsExport(require(absPath)));
|
|
180
|
+
}
|
|
181
|
+
if (ext === '.mjs') {
|
|
182
|
+
return import(pathToFileURL(absPath).href).then((mod) =>
|
|
183
|
+
mod.default !== undefined ? mod.default : mod,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
return Promise.resolve(this.unwrapJsExport(require(absPath)));
|
|
188
|
+
} catch (e) {
|
|
189
|
+
if (e.code === 'ERR_REQUIRE_ESM') {
|
|
190
|
+
return import(pathToFileURL(absPath).href).then((mod) =>
|
|
191
|
+
mod.default !== undefined ? mod.default : mod,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
throw e;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
161
198
|
readJS(configFile, callback) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
199
|
+
const absPath = this.resolveConfigPath(configFile);
|
|
200
|
+
this.loadJsExportedConfig(absPath)
|
|
201
|
+
.then((exportedConfig) => {
|
|
202
|
+
if (typeof exportedConfig === 'function') {
|
|
203
|
+
return Promise.resolve(exportedConfig()).then((obj) => {
|
|
204
|
+
this.fileOptions = obj;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
this.fileOptions = exportedConfig;
|
|
208
|
+
})
|
|
209
|
+
.catch((err) => {
|
|
210
|
+
log.error(err);
|
|
211
|
+
})
|
|
212
|
+
.then(() => {
|
|
166
213
|
if (callback) {
|
|
167
214
|
callback.call(this);
|
|
168
215
|
}
|
|
169
216
|
});
|
|
170
|
-
} else {
|
|
171
|
-
this.fileOptions = exportedConfig;
|
|
172
|
-
if (callback) {
|
|
173
|
-
callback.call(this);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
217
|
}
|
|
177
218
|
|
|
178
219
|
readYAML(configFile, callback) {
|
|
@@ -8,22 +8,21 @@ When the tab is not selected, it hides the associated log panel.
|
|
|
8
8
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
const SplitLogPanel = require('./split_log_panel');
|
|
13
12
|
const View = require('./view');
|
|
14
13
|
const Backbone = require('backbone');
|
|
15
14
|
const pad = require('../../utils/strutils').pad;
|
|
16
15
|
const Chars = require('../../utils/chars');
|
|
17
16
|
const Screen = require('./screen');
|
|
18
|
-
const
|
|
17
|
+
const toastNotify = require('./toast_notify');
|
|
19
18
|
const constants = require('./constants');
|
|
20
19
|
const TabWidth = constants.TabWidth;
|
|
21
20
|
const TabStartLine = constants.TabStartLine;
|
|
22
21
|
const TabHeight = constants.TabHeight;
|
|
23
22
|
const TabStartCol = constants.TabStartCol;
|
|
24
|
-
const RunnerTab = exports.RunnerTab = View.extend({
|
|
23
|
+
const RunnerTab = (exports.RunnerTab = View.extend({
|
|
25
24
|
defaults: {
|
|
26
|
-
allPassed: true
|
|
25
|
+
allPassed: true,
|
|
27
26
|
},
|
|
28
27
|
col: TabStartCol,
|
|
29
28
|
line: TabStartLine,
|
|
@@ -46,7 +45,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
46
45
|
runner: runner,
|
|
47
46
|
appview: appview,
|
|
48
47
|
visible: visible,
|
|
49
|
-
screen: this.get('screen')
|
|
48
|
+
screen: this.get('screen'),
|
|
50
49
|
});
|
|
51
50
|
|
|
52
51
|
this.spinnerIdx = 0;
|
|
@@ -56,7 +55,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
this.observe(appview, {
|
|
59
|
-
'change:currentTab': handleCurrentTab
|
|
58
|
+
'change:currentTab': handleCurrentTab,
|
|
60
59
|
});
|
|
61
60
|
this.observe(runner, {
|
|
62
61
|
'change:name'() {
|
|
@@ -77,7 +76,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
77
76
|
},
|
|
78
77
|
'change:allPassed'(model, value) {
|
|
79
78
|
self.set('allPassed', value);
|
|
80
|
-
}
|
|
79
|
+
},
|
|
81
80
|
});
|
|
82
81
|
|
|
83
82
|
if (results) {
|
|
@@ -91,19 +90,19 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
91
90
|
let total = results.get('total');
|
|
92
91
|
let pending = results.get('pending');
|
|
93
92
|
|
|
94
|
-
let allPassed =
|
|
93
|
+
let allPassed = passed + pending === total;
|
|
95
94
|
let hasTests = total > 0;
|
|
96
95
|
let failCuzNoTests = !hasTests && config.get('fail_on_zero_tests');
|
|
97
|
-
let hasError =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
let hasError =
|
|
97
|
+
runner.get('messages').filter(function (m) {
|
|
98
|
+
return m.get('type') === 'error';
|
|
99
|
+
}).length > 0;
|
|
100
|
+
self.set('allPassed', allPassed && !failCuzNoTests && !hasError);
|
|
102
101
|
}
|
|
103
102
|
},
|
|
104
103
|
'change:all'() {
|
|
105
104
|
self.renderResults();
|
|
106
|
-
}
|
|
105
|
+
},
|
|
107
106
|
});
|
|
108
107
|
}
|
|
109
108
|
|
|
@@ -123,7 +122,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
123
122
|
self.renderRunnerName();
|
|
124
123
|
self.renderResults();
|
|
125
124
|
});
|
|
126
|
-
}
|
|
125
|
+
},
|
|
127
126
|
});
|
|
128
127
|
this.render();
|
|
129
128
|
|
|
@@ -132,7 +131,10 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
132
131
|
|
|
133
132
|
updateSplitPanelVisibility() {
|
|
134
133
|
let appview = this.get('appview');
|
|
135
|
-
this.splitPanel.set(
|
|
134
|
+
this.splitPanel.set(
|
|
135
|
+
'visible',
|
|
136
|
+
this.get('selected') && !appview.isPopupVisible(),
|
|
137
|
+
);
|
|
136
138
|
},
|
|
137
139
|
|
|
138
140
|
color() {
|
|
@@ -147,7 +149,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
147
149
|
let passed = results.get('passed');
|
|
148
150
|
pending = results.get('pending');
|
|
149
151
|
let total = results.get('total');
|
|
150
|
-
equal =
|
|
152
|
+
equal = passed + pending === total;
|
|
151
153
|
hasTests = total > 0;
|
|
152
154
|
}
|
|
153
155
|
let failCuzNoTests = !hasTests && config.get('fail_on_zero_tests');
|
|
@@ -199,8 +201,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
199
201
|
let runnerName = runner.get('name');
|
|
200
202
|
|
|
201
203
|
// write line 1
|
|
202
|
-
screen
|
|
203
|
-
.foreground(this.color());
|
|
204
|
+
screen.foreground(this.color());
|
|
204
205
|
|
|
205
206
|
if (this.get('selected')) {
|
|
206
207
|
screen.display('bright');
|
|
@@ -233,11 +234,12 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
233
234
|
let passed = results.get('passed');
|
|
234
235
|
let pending = results.get('pending');
|
|
235
236
|
resultsDisplay = passed + '/' + total;
|
|
236
|
-
equal =
|
|
237
|
+
equal = passed + pending === total;
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
if (results && results.get('all')) {
|
|
240
|
-
resultsDisplay +=
|
|
241
|
+
resultsDisplay +=
|
|
242
|
+
' ' + (this.get('allPassed') && equal ? Chars.success : Chars.fail);
|
|
241
243
|
} else if (!results && runner.get('allPassed') !== undefined) {
|
|
242
244
|
resultsDisplay = runner.get('allPassed') ? Chars.success : Chars.fail;
|
|
243
245
|
} else {
|
|
@@ -250,8 +252,7 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
250
252
|
resultsDisplay = pad(resultsDisplay, width - 4, ' ', 2);
|
|
251
253
|
|
|
252
254
|
// write line 1
|
|
253
|
-
screen
|
|
254
|
-
.foreground(this.color());
|
|
255
|
+
screen.foreground(this.color());
|
|
255
256
|
|
|
256
257
|
if (this.get('selected')) {
|
|
257
258
|
screen.display('bright');
|
|
@@ -267,12 +268,13 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
267
268
|
let runner = this.get('runner');
|
|
268
269
|
let results = runner.get('results');
|
|
269
270
|
let name = runner.get('name');
|
|
270
|
-
let resultsDisplay = results
|
|
271
|
-
|
|
271
|
+
let resultsDisplay = results
|
|
272
|
+
? results.get('passed') + '/' + results.get('total')
|
|
273
|
+
: 'finished';
|
|
272
274
|
|
|
273
|
-
|
|
274
|
-
title:
|
|
275
|
-
message: name + ' : ' + resultsDisplay
|
|
275
|
+
toastNotify.notify({
|
|
276
|
+
title: "Test'em",
|
|
277
|
+
message: name + ' : ' + resultsDisplay,
|
|
276
278
|
});
|
|
277
279
|
},
|
|
278
280
|
|
|
@@ -329,9 +331,11 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
329
331
|
let firstCol = index === 0;
|
|
330
332
|
screen.position(col, line);
|
|
331
333
|
|
|
332
|
-
screen.write(
|
|
333
|
-
|
|
334
|
-
Chars.
|
|
334
|
+
screen.write(
|
|
335
|
+
(firstCol ? Chars.horizontal : Chars.topLeft) +
|
|
336
|
+
new Array(width - 1).join(Chars.horizontal) +
|
|
337
|
+
Chars.topRight,
|
|
338
|
+
);
|
|
335
339
|
for (let i = 1; i < height - 1; i++) {
|
|
336
340
|
if (!firstCol) {
|
|
337
341
|
screen.position(col, line + i);
|
|
@@ -341,8 +345,10 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
341
345
|
screen.write(Chars.vertical);
|
|
342
346
|
}
|
|
343
347
|
|
|
344
|
-
let bottomLine =
|
|
345
|
-
|
|
348
|
+
let bottomLine =
|
|
349
|
+
(firstCol ? ' ' : Chars.bottomRight) +
|
|
350
|
+
new Array(width - 1).join(' ') +
|
|
351
|
+
Chars.bottomLeft;
|
|
346
352
|
screen.position(col, line + height - 1);
|
|
347
353
|
screen.write(bottomLine);
|
|
348
354
|
},
|
|
@@ -351,8 +357,8 @@ const RunnerTab = exports.RunnerTab = View.extend({
|
|
|
351
357
|
this.stopSpinner();
|
|
352
358
|
this.splitPanel.destroy();
|
|
353
359
|
View.prototype.destroy.call(this);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
360
|
+
},
|
|
361
|
+
}));
|
|
356
362
|
|
|
357
363
|
// View container for all the tabs. It'll handle clean up of removed tabs and draw
|
|
358
364
|
// the edge for where there are no tabs.
|
|
@@ -366,12 +372,12 @@ exports.RunnerTabs = Backbone.Collection.extend({
|
|
|
366
372
|
let currentTab = self.appview.get('currentTab');
|
|
367
373
|
if (currentTab >= self.length) {
|
|
368
374
|
currentTab--;
|
|
369
|
-
self.appview.set('currentTab', currentTab, {silent: true});
|
|
375
|
+
self.appview.set('currentTab', currentTab, { silent: true });
|
|
370
376
|
}
|
|
371
377
|
self.forEach((runner, idx) => {
|
|
372
378
|
runner.set({
|
|
373
379
|
index: idx,
|
|
374
|
-
selected: idx === currentTab
|
|
380
|
+
selected: idx === currentTab,
|
|
375
381
|
});
|
|
376
382
|
});
|
|
377
383
|
self.eraseLast();
|
|
@@ -380,9 +386,12 @@ exports.RunnerTabs = Backbone.Collection.extend({
|
|
|
380
386
|
self.blankOutBackground();
|
|
381
387
|
}
|
|
382
388
|
});
|
|
383
|
-
this.appview.on(
|
|
384
|
-
|
|
385
|
-
|
|
389
|
+
this.appview.on(
|
|
390
|
+
'change:isPopupVisible change:lines change:cols',
|
|
391
|
+
function () {
|
|
392
|
+
self.reRenderAll();
|
|
393
|
+
},
|
|
394
|
+
);
|
|
386
395
|
},
|
|
387
396
|
|
|
388
397
|
reRenderAll() {
|
|
@@ -397,9 +406,7 @@ exports.RunnerTabs = Backbone.Collection.extend({
|
|
|
397
406
|
let screen = this.screen;
|
|
398
407
|
let cols = this.appview.get('cols');
|
|
399
408
|
for (let i = 0; i < TabHeight; i++) {
|
|
400
|
-
screen
|
|
401
|
-
.position(0, TabStartLine + i)
|
|
402
|
-
.write(pad('', cols, ' ', 1));
|
|
409
|
+
screen.position(0, TabStartLine + i).write(pad('', cols, ' ', 1));
|
|
403
410
|
}
|
|
404
411
|
},
|
|
405
412
|
|
|
@@ -439,9 +446,7 @@ exports.RunnerTabs = Backbone.Collection.extend({
|
|
|
439
446
|
let col = TabStartCol + index * width;
|
|
440
447
|
|
|
441
448
|
for (let i = 0; i < height - 1; i++) {
|
|
442
|
-
screen
|
|
443
|
-
.position(col, line + i)
|
|
444
|
-
.write(new Array(width + 1).join(' '));
|
|
449
|
+
screen.position(col, line + i).write(new Array(width + 1).join(' '));
|
|
445
450
|
}
|
|
446
451
|
|
|
447
452
|
let bottomLine = new Array(width + 1).join(Chars.horizontal);
|
|
@@ -452,5 +457,5 @@ exports.RunnerTabs = Backbone.Collection.extend({
|
|
|
452
457
|
isPopupVisible() {
|
|
453
458
|
let appview = this.appview;
|
|
454
459
|
return appview && appview.isPopupVisible();
|
|
455
|
-
}
|
|
460
|
+
},
|
|
456
461
|
});
|
package/lib/server/index.js
CHANGED
|
@@ -399,6 +399,12 @@ class Server extends EventEmitter {
|
|
|
399
399
|
this.emit('file-requested', filePath);
|
|
400
400
|
});
|
|
401
401
|
} else {
|
|
402
|
+
// `dotfiles: 'allow'` restores the Express 4 (send@0.19) behavior of
|
|
403
|
+
// serving paths that contain intermediate dot-prefixed segments — common
|
|
404
|
+
// in pnpm projects, whose entire dependency tree lives under
|
|
405
|
+
// `node_modules/.pnpm/...`. send@1.x defaults to `'ignore'` and 404s any
|
|
406
|
+
// such path.
|
|
407
|
+
var sendOptions = { dotfiles: 'allow' };
|
|
402
408
|
fs.promises.stat(filePath)
|
|
403
409
|
.then(stat => {
|
|
404
410
|
this.emit('file-requested', filePath);
|
|
@@ -408,11 +414,11 @@ class Server extends EventEmitter {
|
|
|
408
414
|
res.render(dirListingPage, { files: files });
|
|
409
415
|
});
|
|
410
416
|
}
|
|
411
|
-
res.sendFile(filePath);
|
|
417
|
+
res.sendFile(filePath, sendOptions);
|
|
412
418
|
})
|
|
413
419
|
.catch(() => {
|
|
414
420
|
this.emit('file-requested', filePath);
|
|
415
|
-
res.sendFile(filePath);
|
|
421
|
+
res.sendFile(filePath, sendOptions);
|
|
416
422
|
});
|
|
417
423
|
}
|
|
418
424
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testem",
|
|
3
|
-
"description": "Test'em 'scripts!
|
|
3
|
+
"description": "Test'em 'scripts! JavaScript unit testing made easy.",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"javascript",
|
|
6
6
|
"testing",
|
|
7
7
|
"unittest",
|
|
8
8
|
"browser"
|
|
9
9
|
],
|
|
10
|
-
"version": "3.20.
|
|
10
|
+
"version": "3.20.1",
|
|
11
11
|
"author": "Toby Ho <airportyh@gmail.com>",
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "
|
|
14
|
+
"url": "https://github.com/testem/testem.git"
|
|
15
15
|
},
|
|
16
16
|
"bugs": {
|
|
17
|
-
"url": "
|
|
17
|
+
"url": "https://github.com/testem/testem/issues"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"minimatch": "^10.2.5",
|
|
38
38
|
"mkdirp": "^3.0.1",
|
|
39
39
|
"mustache": "^4.2.0",
|
|
40
|
-
"
|
|
40
|
+
"toasted-notifier": "^10.1.0",
|
|
41
41
|
"printf": "^0.6.1",
|
|
42
42
|
"proc-log": "^6.1.0",
|
|
43
43
|
"rimraf": "^6.1.3",
|
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
"views"
|
|
57
57
|
],
|
|
58
58
|
"scripts": {
|
|
59
|
-
"install:all": "npm install && npm install phantomjs-prebuilt",
|
|
60
59
|
"test": "mocha tests/*_tests.js tests/**/*_tests.js",
|
|
61
60
|
"test:verbose": "TESTEM_VERBOSE=1 mocha tests/*_tests.js tests/**/*_tests.js",
|
|
62
61
|
"cover": "cover run ./node_modules/.bin/_mocha tests/*_tests.js tests/**/*_tests.js; cover report html; open cover_html/index.html",
|
|
@@ -66,7 +65,6 @@
|
|
|
66
65
|
"lint": "eslint ."
|
|
67
66
|
},
|
|
68
67
|
"devDependencies": {
|
|
69
|
-
"@cypress/request": "^3.0.10",
|
|
70
68
|
"@eslint/eslintrc": "^3.3.5",
|
|
71
69
|
"@eslint/js": "^10.0.1",
|
|
72
70
|
"chai": "^6.2.2",
|
package/testem.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const { tryDelegateToLocalTestem } = require('./lib/cli-local-delegation');
|
|
4
|
+
if (require.main === module) {
|
|
5
|
+
tryDelegateToLocalTestem(__filename, process.argv, process.cwd(), process.env);
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
const { Command } = require('commander');
|
|
4
9
|
const Config = require('./lib/config');
|
|
5
10
|
const Api = require('./lib/api');
|
|
@@ -43,7 +48,7 @@ function parseArgs(argv, { exitOverride = false } = {}) {
|
|
|
43
48
|
.usage('[options]')
|
|
44
49
|
.option(
|
|
45
50
|
'-f, --file [file]',
|
|
46
|
-
'config file
|
|
51
|
+
'config file path (JSON, YAML, or JS: testem.js / testem.cjs / testem.mjs); when omitted, picks the first match among defaults (see docs)',
|
|
47
52
|
)
|
|
48
53
|
.option('-p, --port [num]', 'server port - defaults to 7357', Number)
|
|
49
54
|
.option('--host [hostname]', 'host name - defaults to localhost', String)
|