@remix-run/test 0.0.0 → 0.1.0
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/LICENSE +21 -0
- package/README.md +325 -2
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +171 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/config.d.ts +60 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +152 -0
- package/dist/lib/context.d.ts +69 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/context.js +49 -0
- package/dist/lib/e2e-server.d.ts +11 -0
- package/dist/lib/e2e-server.d.ts.map +1 -0
- package/dist/lib/e2e-server.js +15 -0
- package/dist/lib/executor.d.ts +27 -0
- package/dist/lib/executor.d.ts.map +1 -0
- package/dist/lib/executor.js +123 -0
- package/dist/lib/framework.d.ts +107 -0
- package/dist/lib/framework.d.ts.map +1 -0
- package/dist/lib/framework.js +198 -0
- package/dist/lib/framework.test.d.ts +2 -0
- package/dist/lib/framework.test.d.ts.map +1 -0
- package/dist/lib/framework.test.e2e.d.ts +2 -0
- package/dist/lib/framework.test.e2e.d.ts.map +1 -0
- package/dist/lib/framework.test.e2e.js +29 -0
- package/dist/lib/framework.test.js +283 -0
- package/dist/lib/mock.d.ts +52 -0
- package/dist/lib/mock.d.ts.map +1 -0
- package/dist/lib/mock.js +61 -0
- package/dist/lib/playwright.d.ts +15 -0
- package/dist/lib/playwright.d.ts.map +1 -0
- package/dist/lib/playwright.js +84 -0
- package/dist/lib/reporters/dot.d.ts +10 -0
- package/dist/lib/reporters/dot.d.ts.map +1 -0
- package/dist/lib/reporters/dot.js +55 -0
- package/dist/lib/reporters/files.d.ts +10 -0
- package/dist/lib/reporters/files.d.ts.map +1 -0
- package/dist/lib/reporters/files.js +70 -0
- package/dist/lib/reporters/index.d.ts +14 -0
- package/dist/lib/reporters/index.d.ts.map +1 -0
- package/dist/lib/reporters/index.js +18 -0
- package/dist/lib/reporters/spec.d.ts +10 -0
- package/dist/lib/reporters/spec.d.ts.map +1 -0
- package/dist/lib/reporters/spec.js +152 -0
- package/dist/lib/reporters/tap.d.ts +10 -0
- package/dist/lib/reporters/tap.d.ts.map +1 -0
- package/dist/lib/reporters/tap.js +54 -0
- package/dist/lib/runner.d.ts +9 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +89 -0
- package/dist/lib/utils.d.ts +16 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +27 -0
- package/dist/lib/watcher.d.ts +5 -0
- package/dist/lib/watcher.d.ts.map +1 -0
- package/dist/lib/watcher.js +39 -0
- package/dist/lib/worker-e2e.d.ts +2 -0
- package/dist/lib/worker-e2e.d.ts.map +1 -0
- package/dist/lib/worker-e2e.js +48 -0
- package/dist/lib/worker.d.ts +2 -0
- package/dist/lib/worker.d.ts.map +1 -0
- package/dist/lib/worker.js +29 -0
- package/package.json +58 -5
- package/src/cli.ts +210 -0
- package/src/index.ts +15 -0
- package/src/lib/config.ts +231 -0
- package/src/lib/context.ts +126 -0
- package/src/lib/e2e-server.ts +28 -0
- package/src/lib/executor.ts +162 -0
- package/src/lib/framework.ts +251 -0
- package/src/lib/mock.ts +89 -0
- package/src/lib/playwright.ts +102 -0
- package/src/lib/reporters/dot.ts +57 -0
- package/src/lib/reporters/files.ts +76 -0
- package/src/lib/reporters/index.ts +28 -0
- package/src/lib/reporters/spec.ts +173 -0
- package/src/lib/reporters/tap.ts +58 -0
- package/src/lib/runner.ts +137 -0
- package/src/lib/utils.ts +40 -0
- package/src/lib/watcher.ts +46 -0
- package/src/lib/worker-e2e.ts +52 -0
- package/src/lib/worker.ts +30 -0
- package/tsconfig.json +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Shopify Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,326 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `test`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A test framework for Remix applications
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- `describe`/`it` test structure with `before`/`after`/`beforeEach`/`afterEach` hooks
|
|
8
|
+
- Server-side unit testing
|
|
9
|
+
- Playwright E2E testing via `t.serve`
|
|
10
|
+
- Mock functions and method spies via `t.mock.fn` / `t.mock.method`
|
|
11
|
+
- Watch mode
|
|
12
|
+
- Config file support (`remix-test.config.ts`)
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm i remix
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Write test files that import from `remix/test`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import * as assert from 'remix/assert'
|
|
26
|
+
import { describe, it } from 'remix/test'
|
|
27
|
+
|
|
28
|
+
describe('My Test Suite', () => {
|
|
29
|
+
it('tests a function', () => {
|
|
30
|
+
let result = something()
|
|
31
|
+
assert.equal(result, 42)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run tests with the CLI:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
remix-test
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
By default, `remix-test` discovers all files matching `**/*.test.{ts,tsx}`. Pass a glob as the first positional argument to override:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
remix-test "src/**/*.test.ts"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or, you may control via the `glob.test` config field/CLI arg.
|
|
49
|
+
|
|
50
|
+
### Config File
|
|
51
|
+
|
|
52
|
+
Create a `remix-test.config.ts` (or `.js`) file at the root of your project (shown with default values):
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import type { RemixTestConfig } from 'remix/test'
|
|
56
|
+
|
|
57
|
+
export default {
|
|
58
|
+
// Browser options for E2E tests
|
|
59
|
+
browser: {
|
|
60
|
+
// Echo browser console output to the terminal
|
|
61
|
+
echo: false,
|
|
62
|
+
// Open browser (via playwright `headless:false`) and keep it open after tests
|
|
63
|
+
// complete (useful for debugging)
|
|
64
|
+
open: false,
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Max number of concurrent test workers (default `os.availableParallelism()`)
|
|
68
|
+
concurrency: 2,
|
|
69
|
+
|
|
70
|
+
glob: {
|
|
71
|
+
// Glob pattern identifying all test files (default: "**/*.test?(.e2e).{ts,tsx}")
|
|
72
|
+
test: '**/*.test?(.e2e).ts',
|
|
73
|
+
// Global pattern identifying the subset of E2E test files{ts,tsx}")
|
|
74
|
+
e2e: '**/*.test.e2e.ts',
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Playwright configuration for E2E tests, or string path to an existing
|
|
78
|
+
// config file on disk
|
|
79
|
+
playwrightConfig: {
|
|
80
|
+
projects: [
|
|
81
|
+
{ name: 'chromium', use: { browserName: 'chromium' } },
|
|
82
|
+
{ name: 'firefox', use: { browserName: 'firefox' } },
|
|
83
|
+
],
|
|
84
|
+
use: {
|
|
85
|
+
navigationTimeout: 5_000,
|
|
86
|
+
actionTimeout: 5_000,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Comma-separated list of playwright projects to run E2E tests for
|
|
91
|
+
project: 'chromium',
|
|
92
|
+
|
|
93
|
+
// Test reporter ("spec", "files", "tap", "dot")
|
|
94
|
+
reporter: 'spec',
|
|
95
|
+
|
|
96
|
+
// Path to a setup module (see Setup section below)
|
|
97
|
+
setup: './test/setup.ts',
|
|
98
|
+
|
|
99
|
+
// Comma-separated list of test types to run ("server", "e2e")
|
|
100
|
+
type: 'server,e2e',
|
|
101
|
+
|
|
102
|
+
// Watch for file changes and re-run
|
|
103
|
+
watch: false,
|
|
104
|
+
} satisfies RemixTestConfig
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### CLI Options
|
|
108
|
+
|
|
109
|
+
You can point to a different config file location with the `--config` flag:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
remix-test --config ./tests/config.ts
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You may also specify any config field as a CLI flag which will take precedence over config file values:
|
|
116
|
+
|
|
117
|
+
| Flag | Short |
|
|
118
|
+
| --------------------------- | ----- |
|
|
119
|
+
| `--browser.echo` | |
|
|
120
|
+
| `--browser.open` | |
|
|
121
|
+
| `--concurrency <n>` | `-c` |
|
|
122
|
+
| `--glob.test` | |
|
|
123
|
+
| `--glob.e2e` | |
|
|
124
|
+
| `--playwrightConfig <path>` | |
|
|
125
|
+
| `--project <name>` | `-p` |
|
|
126
|
+
| `--reporter <name>` | `-r` |
|
|
127
|
+
| `--setup <path>` | `-s` |
|
|
128
|
+
| `--type <name>` | `-t` |
|
|
129
|
+
| `--watch` | `-w` |
|
|
130
|
+
|
|
131
|
+
### Setup
|
|
132
|
+
|
|
133
|
+
The `setup` option points to a module that can export `globalSetup` and/or `globalTeardown` functions, called once before and after the entire test run respectively:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// ./test/setup.ts
|
|
137
|
+
export async function globalSetup() {
|
|
138
|
+
await db.migrate()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function globalTeardown() {
|
|
142
|
+
await db.close()
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## API
|
|
147
|
+
|
|
148
|
+
### Test framework
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { beforeAll, afterAll, beforeEach, afterEach, describe, it } from 'remix/test'
|
|
152
|
+
|
|
153
|
+
beforeAll(() => {})
|
|
154
|
+
afterAll(() => {})
|
|
155
|
+
|
|
156
|
+
describe('My Test Suite', () => {
|
|
157
|
+
beforeEach(() => {})
|
|
158
|
+
afterEach(() => {})
|
|
159
|
+
|
|
160
|
+
it('tests something', () => {})
|
|
161
|
+
it('tests something else', () => {})
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`suite` and `test` are aliases for `describe` and `it`.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { suite, test } from 'remix/test'
|
|
169
|
+
|
|
170
|
+
suite('My Test Suite', () => {
|
|
171
|
+
test('tests something', () => {})
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Test Context
|
|
176
|
+
|
|
177
|
+
Each test callback receives a `TestContext` (`t`) as its first argument with helpful test utilities.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
interface TestContext {
|
|
181
|
+
// Register a cleanup function to run after the test completes
|
|
182
|
+
after(fn: () => void): void
|
|
183
|
+
|
|
184
|
+
// Mock tracker, mirroring the shape of Node's `t.mock` from `node:test`
|
|
185
|
+
mock: {
|
|
186
|
+
// Create a mock function with an optional implementation
|
|
187
|
+
fn<T extends (...args: any[]) => any>(impl?: T): MockFunction<T>
|
|
188
|
+
|
|
189
|
+
// Mock an object method with an optional implementation override
|
|
190
|
+
method<T extends object, K extends keyof T>(
|
|
191
|
+
obj: T,
|
|
192
|
+
methodName: K,
|
|
193
|
+
impl?: Function,
|
|
194
|
+
): MockFunction
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// E2E only: start a server with the given request handler, returns a Playwright Page
|
|
198
|
+
serve(handler: (req: Request) => Promise<Response>): Promise<Page>
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### Mocks and Spies
|
|
203
|
+
|
|
204
|
+
Use `t.mock.fn()`/`t.mock.method()` to set up mocks and method spies. This is preferred over the standalone `mock` import because TestContext method mocks are automatically restored after the test runs.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
it('mocks and spies', (t) => {
|
|
208
|
+
// Create a mock function
|
|
209
|
+
let fn = t.mock.fn((x: number) => x * 2)
|
|
210
|
+
fn(3)
|
|
211
|
+
fn.mock.calls[0].result // 6
|
|
212
|
+
|
|
213
|
+
// Mock an existing method
|
|
214
|
+
let spy = t.mock.method(console, 'warn')
|
|
215
|
+
console.warn('test')
|
|
216
|
+
spy.mock.calls.length // 1
|
|
217
|
+
// spy is restored automatically when the test ends
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Cleanup
|
|
222
|
+
|
|
223
|
+
You can register local test cleanup logic with `t.after()`:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
it('cleanup', (t) => {
|
|
227
|
+
let conn = db.connect()
|
|
228
|
+
t.after(() => conn.close())
|
|
229
|
+
// ...
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### E2E
|
|
234
|
+
|
|
235
|
+
In E2E test files, `t.serve()` starts an HTTP server and returns a Playwright `Page`. See [E2E Testing](#e2e-testing) for details.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
it('navigates to home', async (t) => {
|
|
239
|
+
let router = createRouter()
|
|
240
|
+
let page = await t.serve(router.fetch)
|
|
241
|
+
await page.goto('/')
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Standalone mocks (module scope)
|
|
246
|
+
|
|
247
|
+
When you need a mock outside of a test body, import `mock` directly and call `restore()` manually:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { mock } from 'remix/test'
|
|
251
|
+
|
|
252
|
+
let spy = mock.method(console, 'log')
|
|
253
|
+
// ...
|
|
254
|
+
spy.mock.restore?.()
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### E2E Testing
|
|
258
|
+
|
|
259
|
+
E2E tests use [Playwright](https://playwright.dev) and are discovered by the `**/*.test.e2e.{ts,tsx}` glob pattern (configurable via `glob.e2e`). They use the same `describe`/`it` API as unit tests.
|
|
260
|
+
|
|
261
|
+
E2E tests receive `t.serve()` on the test context, which starts an HTTP server with the given request handler and returns a Playwright [`Page`](https://playwright.dev/docs/api/class-page). The server and page are automatically closed after each test.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import * as assert from 'remix/assert'
|
|
265
|
+
import { describe, it } from 'remix/test'
|
|
266
|
+
import { createRouter } from './router.ts'
|
|
267
|
+
|
|
268
|
+
describe('checkout', () => {
|
|
269
|
+
it('adds an item to the cart', async (t) => {
|
|
270
|
+
let router = createRouter()
|
|
271
|
+
let page = await t.serve(router.fetch)
|
|
272
|
+
|
|
273
|
+
await page.goto('/')
|
|
274
|
+
await page.getByRole('button', { name: 'Add to Cart' }).click()
|
|
275
|
+
await page.getByRole('link', { name: 'Cart' }).click()
|
|
276
|
+
await page.getByRole('heading', { name: 'Shopping Cart' }).waitFor()
|
|
277
|
+
|
|
278
|
+
assert.equal(await page.locator('[data-test-cart-quantity]').innerText(), 1)
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Configure Playwright (browsers, timeouts, viewport, etc.) via `playwrightConfig` in your config file:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
export default {
|
|
287
|
+
playwrightConfig: {
|
|
288
|
+
projects: [
|
|
289
|
+
{ name: 'chromium', use: { browserName: 'chromium' } },
|
|
290
|
+
{ name: 'firefox', use: { browserName: 'firefox' } },
|
|
291
|
+
{ name: 'webkit', use: { browserName: 'webkit' } },
|
|
292
|
+
],
|
|
293
|
+
use: {
|
|
294
|
+
navigationTimeout: 5_000,
|
|
295
|
+
actionTimeout: 5_000,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
// Or, point to an existing playwright config file
|
|
300
|
+
// playwrightConfig: './playwright.config.ts'
|
|
301
|
+
} satisfies RemixTestConfig
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Set `browser.open: true` to keep the browser open after tests finish — useful for debugging failures.
|
|
305
|
+
|
|
306
|
+
### Assertions
|
|
307
|
+
|
|
308
|
+
`remix/test` re-exports `remix/assert`. See the [`@remix-run/assert` README](../assert/README.md) for full API documentation.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import * as assert from 'remix/assert'
|
|
312
|
+
|
|
313
|
+
assert.ok(value)
|
|
314
|
+
assert.equal(actual, expected)
|
|
315
|
+
assert.notEqual(actual, expected)
|
|
316
|
+
assert.deepEqual(actual, expected)
|
|
317
|
+
assert.notDeepEqual(actual, expected)
|
|
318
|
+
assert.match(string, regexp)
|
|
319
|
+
assert.throws(fn)
|
|
320
|
+
await assert.rejects(asyncFn)
|
|
321
|
+
assert.fail('message')
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## License
|
|
325
|
+
|
|
326
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fsp from 'node:fs/promises';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { tsImport } from 'tsx/esm/api';
|
|
5
|
+
import { runServerTests } from "./lib/runner.js";
|
|
6
|
+
import { createReporter } from "./lib/reporters/index.js";
|
|
7
|
+
import { createWatcher } from "./lib/watcher.js";
|
|
8
|
+
import { loadPlaywrightConfig, resolveProjects } from "./lib/playwright.js";
|
|
9
|
+
import { loadConfig } from "./lib/config.js";
|
|
10
|
+
const config = await loadConfig();
|
|
11
|
+
let hasExited = false;
|
|
12
|
+
let latestExitCode = 0;
|
|
13
|
+
let watcher;
|
|
14
|
+
let running = false;
|
|
15
|
+
let queued = false;
|
|
16
|
+
let rerunTimer;
|
|
17
|
+
process.on('SIGINT', () => cleanupAndExit(latestExitCode));
|
|
18
|
+
process.on('SIGTERM', () => cleanupAndExit(latestExitCode));
|
|
19
|
+
try {
|
|
20
|
+
await executeRun();
|
|
21
|
+
if (config.watch) {
|
|
22
|
+
console.log('Watching for changes. Press Ctrl+C to stop.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
cleanupAndExit(1);
|
|
27
|
+
}
|
|
28
|
+
async function executeRun() {
|
|
29
|
+
if (hasExited)
|
|
30
|
+
return;
|
|
31
|
+
running = true;
|
|
32
|
+
let globalTeardown;
|
|
33
|
+
try {
|
|
34
|
+
if (config.setup) {
|
|
35
|
+
let mod = await tsImport(path.resolve(process.cwd(), config.setup), {
|
|
36
|
+
parentURL: import.meta.url,
|
|
37
|
+
});
|
|
38
|
+
let globalSetup = mod.globalSetup;
|
|
39
|
+
globalTeardown = mod.globalTeardown;
|
|
40
|
+
await globalSetup?.();
|
|
41
|
+
}
|
|
42
|
+
let { files, serverFiles, e2eFiles } = await discoverTests(config);
|
|
43
|
+
if (config.watch) {
|
|
44
|
+
watcher ??= createWatcher((file) => queueRerun(file));
|
|
45
|
+
watcher.update(files);
|
|
46
|
+
}
|
|
47
|
+
let playwrightConfig = config.playwrightConfig == null || typeof config.playwrightConfig === 'string'
|
|
48
|
+
? await loadPlaywrightConfig(config.playwrightConfig)
|
|
49
|
+
: config.playwrightConfig;
|
|
50
|
+
let reporter = createReporter(config.reporter);
|
|
51
|
+
let startTime = performance.now();
|
|
52
|
+
let counts = {
|
|
53
|
+
passed: 0,
|
|
54
|
+
failed: 0,
|
|
55
|
+
skipped: 0,
|
|
56
|
+
todo: 0,
|
|
57
|
+
};
|
|
58
|
+
// Run server tests
|
|
59
|
+
if (serverFiles.length > 0) {
|
|
60
|
+
reporter.onSectionStart('\nRunning server tests:');
|
|
61
|
+
let serverResult = await runServerTests(serverFiles, reporter, config.concurrency, 'server');
|
|
62
|
+
counts.failed += serverResult.failed;
|
|
63
|
+
counts.passed += serverResult.passed;
|
|
64
|
+
counts.skipped += serverResult.skipped;
|
|
65
|
+
counts.todo += serverResult.todo;
|
|
66
|
+
}
|
|
67
|
+
// Run e2e tests for all browsers configured by the user
|
|
68
|
+
if (e2eFiles.length > 0) {
|
|
69
|
+
let projects = resolveProjects(playwrightConfig);
|
|
70
|
+
if (config.project) {
|
|
71
|
+
let projectNames = config.project.split(',').map((p) => p.trim());
|
|
72
|
+
projects = projects.filter((p) => p.name && projectNames.includes(p.name));
|
|
73
|
+
if (projects.length === 0) {
|
|
74
|
+
throw new Error(`No playwright projects found with name(s) "${config.project}"`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (let project of projects) {
|
|
78
|
+
reporter.onSectionStart(`\nRunning tests for project \`${project.name}\`:`);
|
|
79
|
+
if (config.browser?.open) {
|
|
80
|
+
if (project.playwrightUseOpts?.headless === true) {
|
|
81
|
+
let label = project.name ? ` (project "${project.name}")` : '';
|
|
82
|
+
console.warn(`Warning: browser.open is set but playwright headless is explicitly true${label} — ignoring browser.open`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
project.playwrightUseOpts = { ...project.playwrightUseOpts, headless: false };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
let e2eResult = e2eFiles.length > 0
|
|
89
|
+
? await runServerTests(e2eFiles, reporter, config.concurrency, 'e2e', {
|
|
90
|
+
open: config.browser?.open,
|
|
91
|
+
playwrightUseOpts: project.playwrightUseOpts,
|
|
92
|
+
projectName: project.name,
|
|
93
|
+
})
|
|
94
|
+
: null;
|
|
95
|
+
counts.passed += e2eResult?.passed ?? 0;
|
|
96
|
+
counts.failed += e2eResult?.failed ?? 0;
|
|
97
|
+
counts.skipped += e2eResult?.skipped ?? 0;
|
|
98
|
+
counts.todo += e2eResult?.todo ?? 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
reporter.onSummary(counts, performance.now() - startTime);
|
|
102
|
+
latestExitCode = counts.failed > 0 ? 1 : 0;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error('Error running tests:', error);
|
|
106
|
+
latestExitCode = 1;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await globalTeardown?.();
|
|
110
|
+
running = false;
|
|
111
|
+
if (queued) {
|
|
112
|
+
queued = false;
|
|
113
|
+
queueRerun('queued change');
|
|
114
|
+
}
|
|
115
|
+
else if (!config.watch) {
|
|
116
|
+
cleanupAndExit(latestExitCode);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function discoverTests(config) {
|
|
121
|
+
async function findFiles(pattern) {
|
|
122
|
+
let files = [];
|
|
123
|
+
let exclude = ['node_modules/**', '.git/**'];
|
|
124
|
+
for await (let file of fsp.glob(pattern, { cwd: process.cwd(), exclude })) {
|
|
125
|
+
files.push(path.resolve(process.cwd(), file));
|
|
126
|
+
}
|
|
127
|
+
return files;
|
|
128
|
+
}
|
|
129
|
+
let files = await findFiles(config.glob.test);
|
|
130
|
+
if (files.length === 0) {
|
|
131
|
+
console.log(`No test files found matching pattern: ${config.glob.test}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
let e2eSet = new Set(await findFiles(config.glob.e2e));
|
|
135
|
+
let types = new Set(config.type.split(','));
|
|
136
|
+
let e2eFiles = types.has('e2e') ? files.filter((f) => e2eSet.has(f)) : [];
|
|
137
|
+
let serverFiles = types.has('server') ? files.filter((f) => !e2eSet.has(f)) : [];
|
|
138
|
+
let totalFiles = serverFiles.length + e2eFiles.length;
|
|
139
|
+
if (totalFiles === 0) {
|
|
140
|
+
console.log(`No test files remain after filtering for type ${config.type}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
console.log(`Found ${totalFiles} test file(s) (${serverFiles.length} server, ${e2eFiles.length} e2e)`);
|
|
144
|
+
return {
|
|
145
|
+
files,
|
|
146
|
+
serverFiles,
|
|
147
|
+
e2eFiles,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function queueRerun(reason) {
|
|
151
|
+
if (!config.watch || hasExited)
|
|
152
|
+
return;
|
|
153
|
+
clearTimeout(rerunTimer);
|
|
154
|
+
rerunTimer = setTimeout(() => {
|
|
155
|
+
rerunTimer = undefined;
|
|
156
|
+
if (running) {
|
|
157
|
+
queued = true;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.log(`\n↻ Change detected (${reason}), re-running tests...\n`);
|
|
161
|
+
void executeRun();
|
|
162
|
+
}
|
|
163
|
+
}, 100);
|
|
164
|
+
}
|
|
165
|
+
function cleanupAndExit(code) {
|
|
166
|
+
if (hasExited)
|
|
167
|
+
return;
|
|
168
|
+
hasExited = true;
|
|
169
|
+
watcher?.close();
|
|
170
|
+
process.exit(code);
|
|
171
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { RemixTestConfig } from './lib/config.ts';
|
|
2
|
+
export { describe, it, suite, test, before, after, beforeEach, afterEach, beforeAll, afterAll, } from './lib/framework.ts';
|
|
3
|
+
export { mock } from './lib/mock.ts';
|
|
4
|
+
export type { TestContext } from './lib/context.ts';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACtD,OAAO,EACL,QAAQ,EACR,EAAE,EACF,KAAK,EACL,IAAI,EACJ,MAAM,EACN,KAAK,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,QAAQ,GACT,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACpC,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PlaywrightTestConfig } from 'playwright/test';
|
|
2
|
+
export interface RemixTestConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Options for controlling the playwright browser
|
|
5
|
+
* - `browser.echo`: Echo browser console output to stdout (--browser.echo)
|
|
6
|
+
* - `browser.open`: Open browser window and keep open after test finish (--browser.open)
|
|
7
|
+
*/
|
|
8
|
+
browser?: {
|
|
9
|
+
echo?: boolean;
|
|
10
|
+
open?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Glob patterns to identify test files
|
|
14
|
+
* - `glob.test`: Glob pattern for all test files (--glob.test)
|
|
15
|
+
* - `glob.e2e`: Glob pattern for the subset of e2e test files (--glob.e2e)
|
|
16
|
+
*/
|
|
17
|
+
glob?: {
|
|
18
|
+
test?: string;
|
|
19
|
+
e2e?: string;
|
|
20
|
+
};
|
|
21
|
+
/** Max number of concurrent test workers (--concurrency) */
|
|
22
|
+
concurrency?: number | string;
|
|
23
|
+
/**
|
|
24
|
+
* Path to a module that exports `globalSetup` and/or `globalTeardown` functions,
|
|
25
|
+
* called once before and after the test run respectively. (--setup)
|
|
26
|
+
*/
|
|
27
|
+
setup?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Playwright configuration — either a path to a playwright config file or an inline
|
|
30
|
+
* PlaywrightTestConfig object. CLI `--playwrightConfig` only accepts a file path.
|
|
31
|
+
*/
|
|
32
|
+
playwrightConfig?: string | PlaywrightTestConfig;
|
|
33
|
+
/** Filter tests to a specific playwright project or comma-separated list of projects (--project) */
|
|
34
|
+
project?: string;
|
|
35
|
+
/** Test reporter (--reporter) */
|
|
36
|
+
reporter?: string;
|
|
37
|
+
/** Comma-separated list of test types to run (--type) */
|
|
38
|
+
type?: string;
|
|
39
|
+
/** Watch mode — re-run tests on file changes (--watch) */
|
|
40
|
+
watch?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface ResolvedRemixTestConfig {
|
|
43
|
+
browser: {
|
|
44
|
+
echo?: boolean;
|
|
45
|
+
open?: boolean;
|
|
46
|
+
};
|
|
47
|
+
concurrency: number;
|
|
48
|
+
glob: {
|
|
49
|
+
test: string;
|
|
50
|
+
e2e: string;
|
|
51
|
+
};
|
|
52
|
+
playwrightConfig: string | PlaywrightTestConfig | undefined;
|
|
53
|
+
project: string | undefined;
|
|
54
|
+
reporter: string;
|
|
55
|
+
setup: string | undefined;
|
|
56
|
+
type: string;
|
|
57
|
+
watch: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare function loadConfig(): Promise<ResolvedRemixTestConfig>;
|
|
60
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAgF3D,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;KACf,CAAA;IACD;;;;OAIG;IACH,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,GAAG,CAAC,EAAE,MAAM,CAAA;KACb,CAAA;IACD,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC7B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,oBAAoB,CAAA;IAChD,oGAAoG;IACpG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4DAA0D;IAC1D,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE;QACP,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,IAAI,CAAC,EAAE,OAAO,CAAA;KACf,CAAA;IACD,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAA;QACZ,GAAG,EAAE,MAAM,CAAA;KACZ,CAAA;IACD,gBAAgB,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAAA;IAC3D,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,UAAU,qCAU/B"}
|