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 CHANGED
@@ -1,53 +1,50 @@
1
1
  Got Scripts? Test’em!
2
2
  =================
3
3
 
4
- [![Build Status](https://github.com/testem/testem/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/testem/testem/actions/workflows/ci.yml?query=branch%3Amaster) [![npm version](https://badge.fury.io/js/testem.svg)](http://badge.fury.io/js/testem)
4
+ [![Build Status](https://github.com/testem/testem/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/testem/testem/actions/workflows/ci.yml?query=branch%3Amaster) [![npm version](https://badge.fury.io/js/testem.svg)](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](http://nodejs.org/)**, [PhantomJS](http://phantomjs.org/), 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.
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 Javascript can be tedious and painful, but Testem makes it so easy that you will actually *want* to write tests.
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](http://jasmine.github.io/)
15
- - [QUnit](http://qunitjs.com/)
16
- - [Mocha](http://mochajs.org/)
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](http://nodejs.org) and [PhantomJS](http://phantomjs.org/)
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 popular CI servers like Jenkins or Teamcity
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
- - OS X
23
+ - macOS
24
24
  - Windows
25
25
  - Linux
26
26
  * Preprocessor support
27
- - CoffeeScript
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
- You need [Node](http://nodejs.org/) version 0.10+ or iojs installed on your system. Node is extremely easy to install and has a small footprint, and is really awesome otherwise too, so [just do it](http://nodejs.org/).
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
- Once you have Node installed:
40
+ ```bash
41
+ npm install testem --save-dev
42
+ npm install testem -g
43
+ ```
47
44
 
48
- npm install testem -g
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 will install the `testem` executable globally on your system.
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
- When you run `testem ci` to run tests, it outputs the results in the [TAP](http://testanything.org/) format by default, which looks like
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 16.0 - hello should say hello.
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. TAP plugins exist for popular CI servers
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://wiki.jenkins-ci.org/display/JENKINS/TAP+Plugin) - I've added [detailed instructions](https://github.com/testem/testem/blob/master/docs/use_with_jenkins.md) for setup with Jenkins.
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
- ### Example xunit reporter output
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="Wed Apr 01 2015 11:56:20 GMT+0100 (GMT Daylight Time)" time="9">
233
- <testcase classname="PhantomJS 1.9" name="myFunc returns true when input is valid" time="0"/>
234
- <testcase classname="PhantomJS 1.9" name="myFunc returns false when user tickles it" time="0"/>
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
- ### Example teamcity reporter output
247
+ </details>
248
+
249
+ <details>
250
+ <summary>Example <code>teamcity</code> reporter output</summary>
247
251
 
248
- ##teamcity[testStarted name='PhantomJS 1.9 - hello should say hello']
249
- ##teamcity[testFinished name='PhantomJS 1.9 - hello should say hello']
250
- ##teamcity[testStarted name='PhantomJS 1.9 - hello should say hello to person']
251
- ##teamcity[testFinished name='PhantomJS 1.9 - hello should say hello to person']
252
- ##teamcity[testStarted name='PhantomJS 1.9 - goodbye should say goodbye']
253
- ##teamcity[testFailed name='PhantomJS 1.9 - 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)']
254
- ##teamcity[testFinished name='PhantomJS 1.9 - goodbye should say goodbye']
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
- ##teamcity[testSuiteFinished name='mocha.suite' duration='11091']
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
- to structure your source files into separate directories, or want to have finer control over what files to include.
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 Javascript files in your custom test page, you must
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 Javascript files to be served, and then reference its `src` property to access their path (or `{{#css_files}}` for stylesheets)
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": "./node_modules/puppeteer/.local-chromium/mac-549031/chrome-mac/Chromium.app/Contents/MacOS/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](http://en.wikipedia.org/wiki/Test_Anything_Protocol) format, you can tell that to testem via the `protocol` property. For example
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
- PhantomJS
452
- ---------
459
+ Headless Chrome
460
+ ---------------
453
461
 
454
- PhantomJS is a Webkit-based headless browser. It's fast and it's awesome! Testem will pick it up if you have [PhantomJS](http://www.phantomjs.org/) installed in your system and the `phantomjs` executable is in your path. Run
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
- And verify that it's in the list.
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
- And Testem will run it before each test run. Point **`src_files`** at the sources you want
481
- watched (see **File watching** under Development Mode above).
468
+ Select it like any other launcher, for example:
482
469
 
483
- ```javascript
484
- "src_files": [
485
- "*.coffee"
486
- ]
470
+ ```json
471
+ {
472
+ "launch_in_ci": ["Headless Chrome"]
473
+ }
487
474
  ```
488
475
 
489
- 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
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
- ```javascript
492
- "serve_files": [
493
- "*.js"
494
- ]
478
+ ```json
479
+ {
480
+ "browser_args": {
481
+ "Headless Chrome": ["--remote-debugging-port=9222"]
482
+ }
483
+ }
495
484
  ```
496
485
 
497
- Testem will throw up a big ol' error dialog if the preprocessor command exits with an error code, so code checkers like jshint can be used here as well.
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
- If you need to run a command after your tests have completed (such as removing compiled `.js` files), use the `after_tests` option.
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
- ```javascript
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 Javascripts under `/js` and all the css under `/css`. You can use the "routes" option to do that
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. notification center, growl) using the `-g` option.
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/nodejitsu/node-http-proxy#options
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
- * [BrowserStack Integration](https://github.com/testem/testem/tree/master/examples/browserstack) **bleeding edge**
617
- * [SauceLabs Integration](https://github.com/testem/testem/tree/master/examples/saucelabs) **bleeding edge**
618
- * [Code Coverage with Istanbul](https://github.com/testem/testem/tree/master/examples/coverage_istanbul) **bleeding edge**
619
-
620
- Known Issues
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
- 1. On Windows, Mocha fails to run under Testem due to an [issue](https://github.com/joyent/node/issues/3871) in Node core. Until that gets resolved, I've made a [workaround](https://github.com/airportyh/mocha/tree/windowsfix) for Mocha. To install this fork of Mocha, do
652
+ Historical Screencasts
653
+ ----------------------
624
654
 
625
- npm install https://github.com/airportyh/mocha/tarball/windowsfix -g
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
- 2. If you are using prototype.js version 1.6.3 or below, you will [encounter issues](https://github.com/testem/testem/issues/130).
657
+ * **[Introductory screencast (11:39)](https://www.youtube.com/watch?v=-1mjv4yk5JM)** &mdash; TDD workflow
658
+ * **[Launchers (12:10)](https://www.youtube.com/watch?v=Up0lVjWk9Rk)** &mdash; auto-launch and running tests in **Node**
659
+ * **[CI mode (4:24)](https://www.youtube.com/watch?v=Js16Cj80HKY)** &mdash; 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](http://jasmine.github.io/)
656
- * [QUnit](http://code.google.com/p/jqunit/)
657
- * [Mocha](http://mochajs.org/)
658
- * [Node](http://nodejs.org/)
659
- * [Socket.IO](http://socket.io/)
660
- * [PhantomJS](http://www.phantomjs.org/)
661
- * [Node-Tap](https://github.com/isaacs/node-tap)
662
- * [Node-Charm](https://github.com/substack/node-charm)
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](http://expressjs.com/)
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 (testem.json, .testem.json, testem.yml, .testem.yml)
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
- let exportedConfig = require(this.resolveConfigPath(configFile));
163
- if (typeof exportedConfig === 'function') {
164
- Promise.resolve(exportedConfig()).then((obj) => {
165
- this.fileOptions = obj;
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 notifier = require('node-notifier');
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 = (passed + pending) === total;
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 = runner.get('messages').filter(function(m) {
98
- return m.get('type') === 'error';
99
- }).length > 0;
100
- self.set('allPassed',
101
- allPassed && !failCuzNoTests && !hasError);
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('visible', this.get('selected') && !appview.isPopupVisible());
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 = (passed + pending) === total;
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 = (passed + pending) === total;
237
+ equal = passed + pending === total;
237
238
  }
238
239
 
239
240
  if (results && results.get('all')) {
240
- resultsDisplay += ' ' + ((this.get('allPassed') && equal) ? Chars.success : Chars.fail);
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
- (results.get('passed') + '/' + results.get('total')) : 'finished';
271
+ let resultsDisplay = results
272
+ ? results.get('passed') + '/' + results.get('total')
273
+ : 'finished';
272
274
 
273
- notifier.notify({
274
- title: 'Test\'em',
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((firstCol ? Chars.horizontal : Chars.topLeft) +
333
- new Array(width - 1).join(Chars.horizontal) +
334
- Chars.topRight);
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 = (firstCol ? ' ' : Chars.bottomRight) +
345
- new Array(width - 1).join(' ') + Chars.bottomLeft;
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('change:isPopupVisible change:lines change:cols', function() {
384
- self.reRenderAll();
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
  });
@@ -0,0 +1,5 @@
1
+ const notifier = require('toasted-notifier');
2
+
3
+ exports.notify = function notify(opts) {
4
+ return notifier.notify(opts);
5
+ };
@@ -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! Javascript Unit testing made easy.",
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.0",
10
+ "version": "3.20.1",
11
11
  "author": "Toby Ho <airportyh@gmail.com>",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "git://github.com/testem/testem.git"
14
+ "url": "https://github.com/testem/testem.git"
15
15
  },
16
16
  "bugs": {
17
- "url": "http://github.com/testem/testem/issues"
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
- "node-notifier": "^10.0.1",
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 - defaults to testem.json or testem.yml',
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)