|
1 | | -Unit test runner for NativeScript |
2 | | -================================= |
| 1 | +# @nativescript/unit-test-runner |
3 | 2 |
|
4 | | -Refer to the documentation of NativeScript CLI's `ns test init` command for usage. |
| 3 | +Run [Vitest](https://vitest.dev) unit **and UI** tests inside real NativeScript |
| 4 | +runtimes on Android, iOS, and visionOS. |
5 | 5 |
|
6 | | -If you encounter an issue, please log it at https://github.com/NativeScript/nativescript-cli/ |
| 6 | +Vitest stays on your machine as the orchestrator — configuration, CLI, |
| 7 | +reporters, `--ui`, coverage, and editor integrations all work as usual — while |
| 8 | +your specs execute on device/emulator inside the actual V8/JSC runtimes, with |
| 9 | +full access to native APIs and the NativeScript UI layer. |
7 | 10 |
|
8 | | -### Troubleshooting |
| 11 | +> Version 5 is a complete rewrite. Karma-based testing (v4 and below) is |
| 12 | +> deprecated; see the [migration guide](./docs/migrating-from-karma.md). |
9 | 13 |
|
10 | | -If you see an error like this: |
| 14 | +## Quick start |
11 | 15 |
|
| 16 | +```bash |
| 17 | +ns test init --framework vitest |
| 18 | +ns test ios # or: ns test android / ns test visionos |
12 | 19 | ``` |
13 | | -Error: connect ECONNREFUSED ::1:9876 |
14 | | - at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1195:16) |
| 20 | + |
| 21 | +Or run Vitest directly (what editor extensions and CI use): |
| 22 | + |
| 23 | +```bash |
| 24 | +NS_PLATFORM=ios npx vitest run |
| 25 | +``` |
| 26 | + |
| 27 | +## How it works |
| 28 | + |
| 29 | +- A Vitest plugin installs a custom pool that forwards Vitest's standard |
| 30 | + worker protocol over a WebSocket bridge (bound to `127.0.0.1`). |
| 31 | +- The plugin launches your app via `ns run <platform> --no-hmr |
| 32 | + --env.unitTesting`; the webpack helper (auto-discovered from this package) |
| 33 | + swaps the bundle entry to your `test.ts` for test builds only. |
| 34 | +- On device, a coordinator connects back to the host and executes specs: |
| 35 | + - **Main-thread context (default):** specs run on the UI thread, so they can |
| 36 | + create Views, navigate Frames, and use every NativeScript API. |
| 37 | + - **Worker contexts (opt-in):** additional isolated NativeScript `Worker` |
| 38 | + runtimes for parallel, non-UI specs. |
| 39 | +- Results flow through Vitest's normal RPC, so every reporter works unchanged. |
| 40 | + |
| 41 | +## Configuration |
| 42 | + |
| 43 | +```ts |
| 44 | +// vitest.config.mts |
| 45 | +import { defineConfig } from 'vitest/config'; |
| 46 | +import { nativeScript } from '@nativescript/unit-test-runner'; |
| 47 | + |
| 48 | +export default defineConfig({ |
| 49 | + plugins: [ |
| 50 | + nativeScript({ |
| 51 | + platform: process.env.NS_PLATFORM || 'ios', // 'android' | 'ios' | 'visionos' |
| 52 | + device: process.env.NS_DEVICE || undefined, |
| 53 | + // workers: 2, // extra Worker runtimes for non-UI specs |
| 54 | + // mainThread: false, // disable the UI-capable slot (workers only) |
| 55 | + // port: 17878, |
| 56 | + // launch: false, // attach to an app you run yourself |
| 57 | + }), |
| 58 | + ], |
| 59 | +}); |
| 60 | +``` |
| 61 | + |
| 62 | +Per-platform setups are possible with [Vitest projects](https://vitest.dev/guide/projects) |
| 63 | +— give each project its own `nativeScript({ platform })` plugin instance. |
| 64 | + |
| 65 | +## UI testing |
| 66 | + |
| 67 | +```ts |
| 68 | +import { describe, expect, it } from 'vitest'; |
| 69 | +import { Button } from '@nativescript/core'; |
| 70 | +import { mount, tap } from '@nativescript/unit-test-runner/testing'; |
| 71 | + |
| 72 | +describe('counter button', () => { |
| 73 | + it('increments on tap', async () => { |
| 74 | + let count = 0; |
| 75 | + const { view } = await mount(() => { |
| 76 | + const button = new Button(); |
| 77 | + button.text = 'Count'; |
| 78 | + button.on('tap', () => (count += 1)); |
| 79 | + return button; |
| 80 | + }); |
| 81 | + |
| 82 | + await tap(view); |
| 83 | + expect(count).toBe(1); |
| 84 | + expect(view.isLayoutValid).toBe(true); |
| 85 | + }); |
| 86 | +}); |
15 | 87 | ``` |
16 | 88 |
|
17 | | -When using node 17 or higher, make sure your `karma.conf.js` contains a server hostname setting, for example: |
| 89 | +`mount()` attaches the view to the host page scaffolded in your `test.ts`, |
| 90 | +waits for `loaded` + a real layout pass, and auto-unmounts when the test |
| 91 | +finishes. Also available from `./testing`: `tap`, `doubleTap`, `longPress`, |
| 92 | +`enterText`, `returnPress`, `waitForLayout`, `waitUntil`, `nextRenderPass`. |
| 93 | + |
| 94 | +UI specs require the main-thread context (the default). Files routed to |
| 95 | +Worker contexts must not touch Views. |
18 | 96 |
|
| 97 | +## Coverage |
| 98 | + |
| 99 | +```bash |
| 100 | +ns test ios --env.codeCoverage |
| 101 | +# or: NS_PLATFORM=ios npx vitest run --coverage |
19 | 102 | ``` |
20 | | -// web server hostname (ensure this is present) |
21 | | -hostname: '127.0.0.1', |
22 | 103 |
|
23 | | -// web server port |
24 | | -port: 9876, |
| 104 | +Use the `istanbul` provider — device runtimes do not expose V8 coverage: |
| 105 | + |
| 106 | +```ts |
| 107 | +test: { |
| 108 | + coverage: { provider: 'istanbul', reporter: ['text', 'lcov'] }, |
| 109 | +}, |
25 | 110 | ``` |
26 | | -See [here](https://github.com/NativeScript/nativescript-cli/commit/81cb9c37cdd4e24115be79b24b68dfbaf8cdcfd2) for changeset in CLI which adds that to all newly initialized unit test setups. |
| 111 | + |
| 112 | +## Devices and networking |
| 113 | + |
| 114 | +| Target | Transport | |
| 115 | +| --- | --- | |
| 116 | +| iOS / visionOS simulator | host loopback (`127.0.0.1`) | |
| 117 | +| Android emulator | `10.0.2.2` → host loopback | |
| 118 | +| Physical Android | USB via automatic `adb reverse` | |
| 119 | +| Physical iOS / Apple Vision Pro | pass a LAN-reachable `url` to the coordinator in `test.ts` | |
| 120 | + |
| 121 | +The host server binds `127.0.0.1` by default. Test builds on Android may need |
| 122 | +a scoped cleartext exception for `10.0.2.2`/`127.0.0.1`; on iOS and visionOS, |
| 123 | +`NSAllowsLocalNetworking`. |
| 124 | + |
| 125 | +## Support matrix |
| 126 | + |
| 127 | +| Feature | Status | |
| 128 | +| --- | --- | |
| 129 | +| `describe` / `it` / hooks / `expect` | ✅ | |
| 130 | +| Reporters, `vitest --ui`, JUnit output | ✅ | |
| 131 | +| Istanbul coverage | ✅ | |
| 132 | +| UI testing (`mount`, gestures) | ✅ main-thread context | |
| 133 | +| `vi.fn` / `vi.spyOn` / fake timers | 🚧 planned | |
| 134 | +| Snapshots | 🚧 planned | |
| 135 | +| Watch mode | 🚧 planned (one-shot `vitest run` today) | |
| 136 | +| `vi.mock` module mocking | ❌ not supported (webpack static bundle) — prefer DI | |
| 137 | + |
| 138 | +## Credits |
| 139 | + |
| 140 | +The host↔device bridge design originates from |
| 141 | +[`@cross-code/vitest-ns`](https://github.com/listepo/cross-code) by |
| 142 | +[@listepo](https://github.com/listepo) (MIT). Thank you! |
| 143 | + |
| 144 | +## License |
| 145 | + |
| 146 | +Apache-2.0 |
0 commit comments