@remix-run/test 0.0.0 → 0.2.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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +430 -2
  3. package/dist/app/client/entry.d.ts +2 -0
  4. package/dist/app/client/entry.d.ts.map +1 -0
  5. package/dist/app/client/entry.js +324 -0
  6. package/dist/app/client/iframe.d.ts +2 -0
  7. package/dist/app/client/iframe.d.ts.map +1 -0
  8. package/dist/app/client/iframe.js +22 -0
  9. package/dist/app/server.d.ts +6 -0
  10. package/dist/app/server.d.ts.map +1 -0
  11. package/dist/app/server.js +303 -0
  12. package/dist/cli-entry.d.ts +3 -0
  13. package/dist/cli-entry.d.ts.map +1 -0
  14. package/dist/cli-entry.js +14 -0
  15. package/dist/cli.d.ts +8 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +305 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +2 -0
  21. package/dist/lib/colors.d.ts +2 -0
  22. package/dist/lib/colors.d.ts.map +1 -0
  23. package/dist/lib/colors.js +2 -0
  24. package/dist/lib/config.d.ts +91 -0
  25. package/dist/lib/config.d.ts.map +1 -0
  26. package/dist/lib/config.js +255 -0
  27. package/dist/lib/context.d.ts +93 -0
  28. package/dist/lib/context.d.ts.map +1 -0
  29. package/dist/lib/context.js +65 -0
  30. package/dist/lib/coverage-loader.d.ts +16 -0
  31. package/dist/lib/coverage-loader.d.ts.map +1 -0
  32. package/dist/lib/coverage-loader.js +20 -0
  33. package/dist/lib/coverage.d.ts +28 -0
  34. package/dist/lib/coverage.d.ts.map +1 -0
  35. package/dist/lib/coverage.js +212 -0
  36. package/dist/lib/executor.d.ts +4 -0
  37. package/dist/lib/executor.d.ts.map +1 -0
  38. package/dist/lib/executor.js +128 -0
  39. package/dist/lib/fake-timers.d.ts +6 -0
  40. package/dist/lib/fake-timers.d.ts.map +1 -0
  41. package/dist/lib/fake-timers.js +45 -0
  42. package/dist/lib/framework.d.ts +107 -0
  43. package/dist/lib/framework.d.ts.map +1 -0
  44. package/dist/lib/framework.js +198 -0
  45. package/dist/lib/import-module.d.ts +2 -0
  46. package/dist/lib/import-module.d.ts.map +1 -0
  47. package/dist/lib/import-module.js +29 -0
  48. package/dist/lib/mock.d.ts +52 -0
  49. package/dist/lib/mock.d.ts.map +1 -0
  50. package/dist/lib/mock.js +61 -0
  51. package/dist/lib/normalize.d.ts +2 -0
  52. package/dist/lib/normalize.d.ts.map +1 -0
  53. package/dist/lib/normalize.js +18 -0
  54. package/dist/lib/playwright.d.ts +15 -0
  55. package/dist/lib/playwright.d.ts.map +1 -0
  56. package/dist/lib/playwright.js +81 -0
  57. package/dist/lib/reporters/dot.d.ts +9 -0
  58. package/dist/lib/reporters/dot.d.ts.map +1 -0
  59. package/dist/lib/reporters/dot.js +56 -0
  60. package/dist/lib/reporters/files.d.ts +9 -0
  61. package/dist/lib/reporters/files.d.ts.map +1 -0
  62. package/dist/lib/reporters/files.js +71 -0
  63. package/dist/lib/reporters/index.d.ts +13 -0
  64. package/dist/lib/reporters/index.d.ts.map +1 -0
  65. package/dist/lib/reporters/index.js +18 -0
  66. package/dist/lib/reporters/results.d.ts +30 -0
  67. package/dist/lib/reporters/results.d.ts.map +1 -0
  68. package/dist/lib/reporters/results.js +1 -0
  69. package/dist/lib/reporters/spec.d.ts +9 -0
  70. package/dist/lib/reporters/spec.d.ts.map +1 -0
  71. package/dist/lib/reporters/spec.js +153 -0
  72. package/dist/lib/reporters/tap.d.ts +9 -0
  73. package/dist/lib/reporters/tap.d.ts.map +1 -0
  74. package/dist/lib/reporters/tap.js +54 -0
  75. package/dist/lib/runner-browser.d.ts +21 -0
  76. package/dist/lib/runner-browser.d.ts.map +1 -0
  77. package/dist/lib/runner-browser.js +117 -0
  78. package/dist/lib/runner.d.ts +14 -0
  79. package/dist/lib/runner.d.ts.map +1 -0
  80. package/dist/lib/runner.js +118 -0
  81. package/dist/lib/runtime.d.ts +2 -0
  82. package/dist/lib/runtime.d.ts.map +1 -0
  83. package/dist/lib/runtime.js +2 -0
  84. package/dist/lib/ts-transform.d.ts +4 -0
  85. package/dist/lib/ts-transform.d.ts.map +1 -0
  86. package/dist/lib/ts-transform.js +29 -0
  87. package/dist/lib/watcher.d.ts +5 -0
  88. package/dist/lib/watcher.d.ts.map +1 -0
  89. package/dist/lib/watcher.js +39 -0
  90. package/dist/lib/worker-e2e.d.ts +2 -0
  91. package/dist/lib/worker-e2e.d.ts.map +1 -0
  92. package/dist/lib/worker-e2e.js +49 -0
  93. package/dist/lib/worker.d.ts +2 -0
  94. package/dist/lib/worker.d.ts.map +1 -0
  95. package/dist/lib/worker.js +57 -0
  96. package/dist/test/coverage/fixture.d.ts +5 -0
  97. package/dist/test/coverage/fixture.d.ts.map +1 -0
  98. package/dist/test/coverage/fixture.js +32 -0
  99. package/dist/test/coverage/test-browser.d.ts +2 -0
  100. package/dist/test/coverage/test-browser.d.ts.map +1 -0
  101. package/dist/test/coverage/test-browser.js +24 -0
  102. package/dist/test/coverage/test-e2e.d.ts +2 -0
  103. package/dist/test/coverage/test-e2e.d.ts.map +1 -0
  104. package/dist/test/coverage/test-e2e.js +60 -0
  105. package/dist/test/coverage/test-unit.d.ts +2 -0
  106. package/dist/test/coverage/test-unit.d.ts.map +1 -0
  107. package/dist/test/coverage/test-unit.js +27 -0
  108. package/dist/test/framework.test.browser.d.ts +2 -0
  109. package/dist/test/framework.test.browser.d.ts.map +1 -0
  110. package/dist/test/framework.test.browser.js +107 -0
  111. package/dist/test/framework.test.e2e.d.ts +2 -0
  112. package/dist/test/framework.test.e2e.d.ts.map +1 -0
  113. package/dist/test/framework.test.e2e.js +34 -0
  114. package/package.json +79 -5
  115. package/src/app/client/entry.ts +353 -0
  116. package/src/app/client/iframe.ts +18 -0
  117. package/src/app/server.ts +336 -0
  118. package/src/cli-entry.ts +15 -0
  119. package/src/cli.ts +384 -0
  120. package/src/index.ts +16 -0
  121. package/src/lib/colors.ts +3 -0
  122. package/src/lib/config.ts +377 -0
  123. package/src/lib/context.ts +168 -0
  124. package/src/lib/coverage-loader.ts +31 -0
  125. package/src/lib/coverage.ts +320 -0
  126. package/src/lib/executor.ts +145 -0
  127. package/src/lib/fake-timers.ts +64 -0
  128. package/src/lib/framework.ts +251 -0
  129. package/src/lib/import-module.ts +29 -0
  130. package/src/lib/mock.ts +89 -0
  131. package/src/lib/normalize.ts +22 -0
  132. package/src/lib/playwright.ts +100 -0
  133. package/src/lib/reporters/dot.ts +58 -0
  134. package/src/lib/reporters/files.ts +77 -0
  135. package/src/lib/reporters/index.ts +27 -0
  136. package/src/lib/reporters/results.ts +29 -0
  137. package/src/lib/reporters/spec.ts +174 -0
  138. package/src/lib/reporters/tap.ts +58 -0
  139. package/src/lib/runner-browser.ts +165 -0
  140. package/src/lib/runner.ts +189 -0
  141. package/src/lib/runtime.ts +2 -0
  142. package/src/lib/ts-transform.ts +36 -0
  143. package/src/lib/watcher.ts +46 -0
  144. package/src/lib/worker-e2e.ts +54 -0
  145. package/src/lib/worker.ts +50 -0
  146. package/src/test/coverage/fixture.ts +34 -0
  147. package/src/test/coverage/test-browser.ts +29 -0
  148. package/src/test/coverage/test-e2e.ts +70 -0
  149. package/src/test/coverage/test-unit.ts +32 -0
  150. package/tsconfig.json +16 -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,431 @@
1
- # Placeholder Package
1
+ # `test`
2
2
 
3
- This package is a placeholder published at `0.0.0` to reserve the npm name and configure CI publish permissions.
3
+ A test framework for JavaScript and TypeScript projects.
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
+ - In-browser component testing (pair with `render` from `remix/ui/test`)
11
+ - Mock functions and method spies via `t.mock.fn` / `t.mock.method`
12
+ - Unified code coverage reporting across unit and E2E tests
13
+ - Watch mode
14
+ - Config file support (`remix-test.config.ts`)
15
+
16
+ ## Installation
17
+
18
+ ```sh
19
+ npm i remix
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ Write test files that import from `remix/test`:
25
+
26
+ ```ts
27
+ import * as assert from 'remix/assert'
28
+ import { describe, it } from 'remix/test'
29
+
30
+ describe('My Test Suite', () => {
31
+ it('tests a function', () => {
32
+ let result = something()
33
+ assert.equal(result, 42)
34
+ })
35
+ })
36
+ ```
37
+
38
+ Run tests with the CLI:
39
+
40
+ ```sh
41
+ remix test
42
+ ```
43
+
44
+ By default, `remix test` discovers all files matching `**/*.test{,.e2e}.{ts,tsx}`. Pass a glob as the first positional argument to override:
45
+
46
+ ```sh
47
+ remix test "src/**/*.test.ts"
48
+ ```
49
+
50
+ Or, you may control via the `glob.test` config field/CLI arg.
51
+
52
+ If you install `@remix-run/test` directly instead of the umbrella `remix` package, the same runner is available as `remix-test`:
53
+
54
+ ```sh
55
+ npm i @remix-run/test
56
+ remix-test
57
+ ```
58
+
59
+ ### Config File
60
+
61
+ Create a `remix-test.config.ts` (or `.js`) file at the root of your project (shown with default values):
62
+
63
+ ```ts
64
+ import type { RemixTestConfig } from 'remix/test'
65
+
66
+ export default {
67
+ // Browser options for E2E tests
68
+ browser: {
69
+ // Echo browser console output to the terminal
70
+ echo: false,
71
+ // Open browser (via playwright `headless:false`) and keep it open after tests
72
+ // complete (useful for debugging)
73
+ open: false,
74
+ },
75
+
76
+ // Max number of concurrent test workers (default `os.availableParallelism()`)
77
+ concurrency: 2,
78
+
79
+ // Code coverage options
80
+ coverage: {
81
+ // Enable coverage reporting
82
+ enabled: true,
83
+ // Output directory (default: ".coverage")
84
+ dir: '.coverage',
85
+ // Glob patterns to include/exclude
86
+ include: ['src/**'],
87
+ exclude: ['src/**/*.test.ts'],
88
+ // Minimum thresholds (%)
89
+ statements: 80,
90
+ lines: 80,
91
+ branches: 80,
92
+ functions: 80,
93
+ },
94
+
95
+ glob: {
96
+ // Glob pattern identifying all test files (default: "**/*.test{,.browser,.e2e}.{ts,tsx}")
97
+ test: '**/*.test{,.browser,.e2e}.ts',
98
+ // Glob pattern identifying browser test files (default: "**/*.test.browser.{ts,tsx}")
99
+ browser: '**/*.test.browser.ts',
100
+ // Glob pattern identifying E2E test files (default: "**/*.test.e2e.{ts,tsx}")
101
+ e2e: '**/*.test.e2e.ts',
102
+ },
103
+
104
+ // Playwright configuration for E2E tests, or string path to an existing
105
+ // config file on disk
106
+ playwrightConfig: {
107
+ projects: [
108
+ { name: 'chromium', use: { browserName: 'chromium' } },
109
+ { name: 'firefox', use: { browserName: 'firefox' } },
110
+ ],
111
+ use: {
112
+ navigationTimeout: 5_000,
113
+ actionTimeout: 5_000,
114
+ },
115
+ },
116
+
117
+ // Comma-separated list of playwright projects to run E2E tests for
118
+ project: 'chromium',
119
+
120
+ // Test reporter ("spec", "files", "tap", "dot")
121
+ reporter: 'spec',
122
+
123
+ // Path to a setup module (see Setup section below)
124
+ setup: './test/setup.ts',
125
+
126
+ // Comma-separated list of test types to run ("server", "browser", "e2e")
127
+ type: 'server,browser,e2e',
128
+
129
+ // Watch for file changes and re-run
130
+ watch: false,
131
+ } satisfies RemixTestConfig
132
+ ```
133
+
134
+ ### CLI Options
135
+
136
+ You can point to a different config file location with the `--config` flag:
137
+
138
+ ```sh
139
+ remix test --config ./tests/config.ts
140
+ ```
141
+
142
+ You may also specify any config field as a CLI flag which will take precedence over config file values:
143
+
144
+ | Flag | Short |
145
+ | --------------------------- | ----- |
146
+ | `--browser.echo` | |
147
+ | `--browser.open` | |
148
+ | `--concurrency <n>` | `-c` |
149
+ | `--coverage` | |
150
+ | `--coverage.dir <path>` | |
151
+ | `--coverage.include` | |
152
+ | `--coverage.exclude` | |
153
+ | `--coverage.statements` | |
154
+ | `--coverage.lines` | |
155
+ | `--coverage.branches` | |
156
+ | `--coverage.functions` | |
157
+ | `--glob.test` | |
158
+ | `--glob.browser` | |
159
+ | `--glob.e2e` | |
160
+ | `--playwrightConfig <path>` | |
161
+ | `--project <name>` | `-p` |
162
+ | `--reporter <name>` | `-r` |
163
+ | `--setup <path>` | `-s` |
164
+ | `--type <name>` | `-t` |
165
+ | `--watch` | `-w` |
166
+
167
+ ### Setup
168
+
169
+ 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:
170
+
171
+ ```ts
172
+ // ./test/setup.ts
173
+ export async function globalSetup() {
174
+ await db.migrate()
175
+ }
176
+
177
+ export async function globalTeardown() {
178
+ await db.close()
179
+ }
180
+ ```
181
+
182
+ ## API
183
+
184
+ ### Test framework
185
+
186
+ ```ts
187
+ import { beforeAll, afterAll, beforeEach, afterEach, describe, it } from 'remix/test'
188
+
189
+ beforeAll(() => {})
190
+ afterAll(() => {})
191
+
192
+ describe('My Test Suite', () => {
193
+ beforeEach(() => {})
194
+ afterEach(() => {})
195
+
196
+ it('tests something', () => {})
197
+ it('tests something else', () => {})
198
+ })
199
+ ```
200
+
201
+ `suite` and `test` are aliases for `describe` and `it`.
202
+
203
+ ```ts
204
+ import { suite, test } from 'remix/test'
205
+
206
+ suite('My Test Suite', () => {
207
+ test('tests something', () => {})
208
+ })
209
+ ```
210
+
211
+ ### Programmatic runner
212
+
213
+ `@remix-run/test/cli` exports `runRemixTest()` for tools that want to run the test runner without
214
+ exiting the current process:
215
+
216
+ ```ts
217
+ import { runRemixTest } from '@remix-run/test/cli'
218
+
219
+ let exitCode = await runRemixTest({
220
+ argv: ['--type', 'server'],
221
+ cwd: process.cwd(),
222
+ })
223
+ ```
224
+
225
+ `runRemixTest()` returns the runner exit code. The `remix test` and `remix-test` bin wrappers call
226
+ `process.exit()` with that code when the run finishes so open workers, browsers, or project handles
227
+ cannot keep the CLI alive.
228
+
229
+ ### Test Context
230
+
231
+ Each test callback receives a `TestContext` (`t`) as its first argument with helpful test utilities.
232
+
233
+ ```ts
234
+ // from 'remix/test'
235
+ interface TestContext {
236
+ // Register a cleanup function to run after the test completes
237
+ after(fn: () => void): void
238
+
239
+ // Mock tracker, mirroring the shape of Node's `t.mock` from `node:test`
240
+ mock: {
241
+ // Create a mock function with an optional implementation
242
+ fn<T extends (...args: any[]) => any>(impl?: T): MockFunction<T>
243
+
244
+ // Mock an object method with an optional implementation override
245
+ method<T extends object, K extends keyof T>(
246
+ obj: T,
247
+ methodName: K,
248
+ impl?: Function,
249
+ ): MockFunction
250
+ }
251
+
252
+ // Replace global timer functions with controllable fakes
253
+ useFakeTimers(): FakeTimers
254
+
255
+ // E2E only: connect a running test server to a Playwright Page
256
+ serve(server: { baseUrl: string; close(): Promise<void> }): Promise<Page>
257
+ }
258
+ ```
259
+
260
+ #### Mocks and Spies
261
+
262
+ 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.
263
+
264
+ ```ts
265
+ it('mocks and spies', (t) => {
266
+ // Create a mock function
267
+ let fn = t.mock.fn((x: number) => x * 2)
268
+ fn(3)
269
+ fn.mock.calls[0].result // 6
270
+
271
+ // Mock an existing method
272
+ let spy = t.mock.method(console, 'warn')
273
+ console.warn('test')
274
+ spy.mock.calls.length // 1
275
+ // spy is restored automatically when the test ends
276
+ })
277
+ ```
278
+
279
+ #### Cleanup
280
+
281
+ You can register local test cleanup logic with `t.after()`:
282
+
283
+ ```ts
284
+ it('cleanup', (t) => {
285
+ let conn = db.connect()
286
+ t.after(() => conn.close())
287
+ // ...
288
+ })
289
+ ```
290
+
291
+ #### Fake Timers
292
+
293
+ `t.useFakeTimers()` replaces the global timer functions (`setTimeout`, `setInterval`, etc.) with controllable fakes that are automatically restored after the test. It works in any test environment — server unit tests, browser tests, or E2E setup code.
294
+
295
+ ```ts
296
+ it('debounces a callback', (t) => {
297
+ let timers = t.useFakeTimers()
298
+ let calls = 0
299
+ let debounced = debounce(() => calls++, 300)
300
+
301
+ debounced()
302
+ timers.advance(299)
303
+ assert.equal(calls, 0)
304
+ timers.advance(1)
305
+ assert.equal(calls, 1)
306
+ })
307
+ ```
308
+
309
+ | Method | Description |
310
+ | ------------- | --------------------------------------------------------------------------- |
311
+ | `advance(ms)` | Advance the clock by `ms` milliseconds, firing any elapsed timers |
312
+ | `restore()` | Restore the original timer functions (called automatically after each test) |
313
+
314
+ #### E2E
315
+
316
+ In E2E test files, `t.serve()` connects a running test server to a Playwright `Page`. See [E2E Testing](#e2e-testing) for details.
317
+
318
+ ```ts
319
+ import { createTestServer } from 'remix/node-fetch-server/test'
320
+
321
+ it('navigates to home', async (t) => {
322
+ let router = createRouter()
323
+ let server = await createTestServer(router.fetch)
324
+ let page = await t.serve(server)
325
+ await page.goto('/')
326
+ })
327
+ ```
328
+
329
+ ### Standalone mocks (module scope)
330
+
331
+ When you need a mock outside of a test body, import `mock` directly and call `restore()` manually:
332
+
333
+ ```ts
334
+ import { mock } from 'remix/test'
335
+
336
+ let spy = mock.method(console, 'log')
337
+ // ...
338
+ spy.mock.restore?.()
339
+ ```
340
+
341
+ ### Browser Testing
342
+
343
+ Browser tests run components in an actual browser environment via Playwright and are discovered by the `**/*.test.browser.{ts,tsx}` glob pattern (configurable via `glob.browser`). They use the same `describe`/`it` API as unit tests. Each in-browser test suite runs in an isolated `iframe` so it has access to its own `document` instance.
344
+
345
+ #### `render()`
346
+
347
+ `render`, exported from `remix/ui/test`, mounts a component into the DOM and returns a `RenderResult`:
348
+
349
+ ```ts
350
+ import * as assert from 'remix/assert'
351
+ import { describe, it } from 'remix/test'
352
+ import { render } from 'remix/ui/test'
353
+ import { Counter } from './counter.tsx'
354
+
355
+ describe('Counter', () => {
356
+ it('increments on click', async (t) => {
357
+ let { $, act, cleanup } = render(<Counter />)
358
+ t.after(cleanup)
359
+
360
+ assert.equal($('[data-count]')?.textContent, '0')
361
+ await act(() => $('[data-action="increment"]')?.click())
362
+ assert.equal($('[data-count]')?.textContent, '1')
363
+ })
364
+ })
365
+ ```
366
+
367
+ `RenderResult` provides:
368
+
369
+ | Property/Method | Description |
370
+ | --------------- | ----------------------------------------------------------------------- |
371
+ | `container` | The `HTMLElement` the component is mounted into |
372
+ | `root` | The Remix `VirtualRoot` the component is rendered in |
373
+ | `$(selector)` | Alias for `container.querySelector()` |
374
+ | `$$(selector)` | Alias for `container.querySelectorAll()` |
375
+ | `act(fn)` | Runs `fn` and flushes pending component updates |
376
+ | `cleanup()` | Unmounts and removes the container (pass to `t.after` for auto-cleanup) |
377
+
378
+ ### E2E Testing
379
+
380
+ End-to-end (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.
381
+
382
+ E2E tests receive `t.serve()` on the test context, which accepts a running test server and returns a Playwright [`Page`](https://playwright.dev/docs/api/class-page) whose `baseURL` points at that server. The server and page are automatically closed after each test.
383
+
384
+ ```ts
385
+ import * as assert from 'remix/assert'
386
+ import { createTestServer } from 'remix/node-fetch-server/test'
387
+ import { describe, it } from 'remix/test'
388
+ import { createRouter } from './router.ts'
389
+
390
+ describe('checkout', () => {
391
+ it('adds an item to the cart', async (t) => {
392
+ let router = createRouter()
393
+ let server = await createTestServer(router.fetch)
394
+ let page = await t.serve(server)
395
+
396
+ await page.goto('/')
397
+ await page.getByRole('button', { name: 'Add to Cart' }).click()
398
+ await page.getByRole('link', { name: 'Cart' }).click()
399
+ await page.getByRole('heading', { name: 'Shopping Cart' }).waitFor()
400
+
401
+ assert.equal(await page.locator('[data-test-cart-quantity]').innerText(), 1)
402
+ })
403
+ })
404
+ ```
405
+
406
+ Configure Playwright (browsers, timeouts, viewport, etc.) via `playwrightConfig` in your config file:
407
+
408
+ ```ts
409
+ export default {
410
+ playwrightConfig: {
411
+ projects: [
412
+ { name: 'chromium', use: { browserName: 'chromium' } },
413
+ { name: 'firefox', use: { browserName: 'firefox' } },
414
+ { name: 'webkit', use: { browserName: 'webkit' } },
415
+ ],
416
+ use: {
417
+ navigationTimeout: 5_000,
418
+ actionTimeout: 5_000,
419
+ },
420
+ },
421
+
422
+ // Or, point to an existing playwright config file
423
+ // playwrightConfig: './playwright.config.ts'
424
+ } satisfies RemixTestConfig
425
+ ```
426
+
427
+ Set `browser.open: true` to keep the browser open after tests finish — useful for debugging failures.
428
+
429
+ ## License
430
+
431
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../../src/app/client/entry.ts"],"names":[],"mappings":""}