remix 3.0.0-beta.4 → 3.0.0-beta.6
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 +4 -2
- package/dist/assets/types/hmr.d.ts +2 -0
- package/dist/cli-entry.js +1 -1
- package/dist/data-table/cli.d.ts +2 -0
- package/dist/data-table/cli.d.ts.map +1 -0
- package/dist/{ui/scroll-lock.js → data-table/cli.js} +1 -1
- package/dist/node-hmr/runtime.d.ts +2 -0
- package/dist/node-hmr/runtime.d.ts.map +1 -0
- package/dist/node-hmr/runtime.js +2 -0
- package/dist/node-hmr/types.d.ts +2 -0
- package/dist/node-hmr.d.ts +2 -0
- package/dist/node-hmr.d.ts.map +1 -0
- package/dist/{ui/glyph.js → node-hmr.js} +1 -1
- package/dist/ui/accordion/primitives.d.ts +2 -0
- package/dist/ui/accordion/primitives.d.ts.map +1 -0
- package/dist/ui/accordion/primitives.js +2 -0
- package/dist/ui/button.d.ts +1 -0
- package/dist/ui/button.d.ts.map +1 -1
- package/dist/ui/button.js +1 -0
- package/dist/ui/checkbox.d.ts +3 -0
- package/dist/ui/checkbox.d.ts.map +1 -0
- package/dist/ui/checkbox.js +3 -0
- package/dist/ui/combobox/primitives.d.ts +2 -0
- package/dist/ui/combobox/primitives.d.ts.map +1 -0
- package/dist/ui/combobox/primitives.js +2 -0
- package/dist/ui/dev/refresh.d.ts +2 -0
- package/dist/ui/dev/refresh.d.ts.map +1 -0
- package/dist/ui/dev/refresh.js +2 -0
- package/dist/ui/input.d.ts +3 -0
- package/dist/ui/input.d.ts.map +1 -0
- package/dist/ui/input.js +3 -0
- package/dist/ui/menu/primitives.d.ts +2 -0
- package/dist/ui/menu/primitives.d.ts.map +1 -0
- package/dist/ui/menu/primitives.js +2 -0
- package/dist/ui/radio.d.ts +3 -0
- package/dist/ui/radio.d.ts.map +1 -0
- package/dist/ui/radio.js +3 -0
- package/dist/ui/select/primitives.d.ts +2 -0
- package/dist/ui/select/primitives.d.ts.map +1 -0
- package/dist/ui/select/primitives.js +2 -0
- package/dist/ui/tabs/primitives.d.ts +2 -0
- package/dist/ui/tabs/primitives.d.ts.map +1 -0
- package/dist/ui/tabs/primitives.js +2 -0
- package/dist/ui/tabs.d.ts +2 -0
- package/dist/ui/tabs.d.ts.map +1 -0
- package/{src/ui/theme.ts → dist/ui/tabs.js} +1 -1
- package/dist/ui/toggle/primitives.d.ts +2 -0
- package/dist/ui/toggle/primitives.d.ts.map +1 -0
- package/dist/ui/toggle/primitives.js +2 -0
- package/dist/ui/toggle.d.ts +3 -0
- package/dist/ui/toggle.d.ts.map +1 -0
- package/dist/ui/toggle.js +3 -0
- package/dist/ui-hmr/assets.d.ts +2 -0
- package/dist/ui-hmr/assets.d.ts.map +1 -0
- package/dist/ui-hmr/assets.js +2 -0
- package/dist/ui-hmr/node.d.ts +3 -0
- package/dist/ui-hmr/node.d.ts.map +1 -0
- package/dist/ui-hmr/node.js +3 -0
- package/dist/ui-hmr/runtime/browser.d.ts +2 -0
- package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
- package/dist/ui-hmr/runtime/browser.js +2 -0
- package/dist/ui-hmr/runtime/server.d.ts +2 -0
- package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
- package/dist/ui-hmr/runtime/server.js +2 -0
- package/dist/ui-hmr.d.ts +2 -0
- package/dist/ui-hmr.d.ts.map +1 -0
- package/{src/ui/glyph.ts → dist/ui-hmr.js} +1 -1
- package/package.json +122 -142
- package/src/assets/README.md +322 -56
- package/src/assets/types/hmr.d.ts +2 -0
- package/src/cli/README.md +105 -1
- package/src/cookie/README.md +4 -4
- package/src/data-table/README.md +202 -68
- package/src/data-table/cli.ts +2 -0
- package/src/data-table-mysql/README.md +46 -17
- package/src/data-table-postgres/README.md +39 -13
- package/src/data-table-sqlite/README.md +38 -20
- package/src/fetch-proxy/README.md +25 -0
- package/src/form-data-parser/README.md +4 -4
- package/src/mime/README.md +8 -1
- package/src/node-fetch-server/README.md +39 -13
- package/src/node-hmr/README.md +307 -0
- package/src/node-hmr/runtime.ts +2 -0
- package/src/node-hmr/types.d.ts +2 -0
- package/{dist/ui/theme.js → src/node-hmr.ts} +1 -1
- package/src/route-pattern/README.md +141 -13
- package/src/session/README.md +1 -1
- package/src/session-middleware/README.md +9 -7
- package/src/test/README.md +161 -115
- package/src/ui/README.md +116 -157
- package/src/ui/accordion/README.md +50 -14
- package/src/ui/accordion/primitives/README.md +202 -0
- package/src/ui/accordion/primitives.ts +2 -0
- package/src/ui/anchor/README.md +37 -2
- package/src/ui/breadcrumbs/README.md +4 -4
- package/src/ui/button/README.md +26 -26
- package/src/ui/button.ts +1 -0
- package/src/ui/checkbox/README.md +59 -0
- package/src/ui/checkbox.ts +3 -0
- package/src/ui/combobox/README.md +58 -9
- package/src/ui/combobox/primitives/README.md +194 -0
- package/src/ui/combobox/primitives.ts +2 -0
- package/src/ui/dev/refresh.ts +2 -0
- package/src/ui/input/README.md +52 -0
- package/src/ui/input.ts +3 -0
- package/src/ui/listbox/README.md +9 -41
- package/src/ui/menu/README.md +55 -14
- package/src/ui/menu/primitives/README.md +161 -0
- package/src/ui/menu/primitives.ts +2 -0
- package/src/ui/popover/README.md +20 -39
- package/src/ui/radio/README.md +53 -0
- package/src/ui/radio.ts +3 -0
- package/src/ui/select/README.md +29 -19
- package/src/ui/select/primitives/README.md +117 -0
- package/src/ui/select/primitives.ts +2 -0
- package/src/ui/tabs/README.md +141 -0
- package/src/ui/tabs/primitives/README.md +141 -0
- package/src/ui/tabs/primitives.ts +2 -0
- package/src/ui/tabs.ts +2 -0
- package/src/ui/test/README.md +151 -60
- package/src/ui/toggle/README.md +56 -0
- package/src/ui/toggle/primitives/README.md +56 -0
- package/src/ui/toggle/primitives.ts +2 -0
- package/src/ui/toggle.ts +3 -0
- package/src/ui-hmr/README.md +119 -0
- package/{dist/ui/separator.js → src/ui-hmr/assets.ts} +1 -1
- package/src/ui-hmr/node.ts +3 -0
- package/src/ui-hmr/runtime/browser.ts +2 -0
- package/src/ui-hmr/runtime/server.ts +2 -0
- package/src/ui-hmr.ts +2 -0
- package/dist/ui/glyph.d.ts +0 -2
- package/dist/ui/glyph.d.ts.map +0 -1
- package/dist/ui/scroll-lock.d.ts +0 -2
- package/dist/ui/scroll-lock.d.ts.map +0 -1
- package/dist/ui/separator.d.ts +0 -2
- package/dist/ui/separator.d.ts.map +0 -1
- package/dist/ui/theme.d.ts +0 -2
- package/dist/ui/theme.d.ts.map +0 -1
- package/src/ui/glyph/README.md +0 -72
- package/src/ui/scroll-lock/README.md +0 -33
- package/src/ui/scroll-lock.ts +0 -2
- package/src/ui/separator.ts +0 -2
- package/src/ui/theme/README.md +0 -103
package/src/test/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A test framework for JavaScript and TypeScript projects.
|
|
|
12
12
|
- Per-test and hook timeouts with `t.signal` abort support
|
|
13
13
|
- Unified code coverage reporting across unit and E2E tests
|
|
14
14
|
- Watch mode
|
|
15
|
-
-
|
|
15
|
+
- Static CLI configuration through `remix.json`
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -42,127 +42,145 @@ Run tests with the CLI:
|
|
|
42
42
|
remix test
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
By default, `remix test` discovers all files matching `**/*.test{,.e2e}.{ts,tsx}`. Pass one or more globs as positional arguments to override:
|
|
45
|
+
By default, `remix test` discovers all files matching `**/*.test{,.browser,.e2e}.{ts,tsx}`. Pass one or more globs as positional arguments to override:
|
|
46
46
|
|
|
47
47
|
```sh
|
|
48
48
|
remix test "src/**/*.test.ts"
|
|
49
49
|
remix test "src/**/*.test.ts" "tests/**/*.test.tsx"
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
You may also repeat the `--glob.*` flags. Positional globs take precedence over `--glob.test`.
|
|
53
53
|
|
|
54
54
|
### Config File
|
|
55
55
|
|
|
56
|
-
Create
|
|
56
|
+
Create an optional `remix.json` at the root of your project. It is parsed as JSONC, so comments and
|
|
57
|
+
trailing commas are allowed:
|
|
58
|
+
|
|
59
|
+
```jsonc
|
|
60
|
+
{
|
|
61
|
+
"$schema": "https://remix.run/schemas/remix.json",
|
|
62
|
+
"test": {
|
|
63
|
+
"files": ["**/*.test{,.browser,.e2e}.{ts,tsx}"],
|
|
64
|
+
"browserFiles": ["**/*.test.browser.{ts,tsx}"],
|
|
65
|
+
"e2eFiles": ["**/*.test.e2e.{ts,tsx}"],
|
|
66
|
+
"exclude": ["node_modules/**", "dist/**"],
|
|
67
|
+
"type": ["server", "browser", "e2e"],
|
|
68
|
+
"only": ["/checkout/i"],
|
|
69
|
+
|
|
70
|
+
"concurrency": 2,
|
|
71
|
+
"pool": "forks",
|
|
72
|
+
"setup": "./test/setup.ts",
|
|
73
|
+
"watch": false,
|
|
74
|
+
|
|
75
|
+
"playwright": {
|
|
76
|
+
"echo": false,
|
|
77
|
+
"open": false,
|
|
78
|
+
"configFile": "./playwright.config.ts",
|
|
79
|
+
"projects": ["chromium", "firefox"],
|
|
80
|
+
},
|
|
57
81
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
"reporter": "spec",
|
|
83
|
+
"quiet": false,
|
|
84
|
+
|
|
85
|
+
"coverage": {
|
|
86
|
+
"enabled": true,
|
|
87
|
+
"dir": ".coverage",
|
|
88
|
+
"include": ["src/**"],
|
|
89
|
+
"exclude": ["src/**/*.test.ts"],
|
|
90
|
+
"statements": 80,
|
|
91
|
+
"lines": 80,
|
|
92
|
+
"branches": 80,
|
|
93
|
+
"functions": 80,
|
|
94
|
+
},
|
|
69
95
|
},
|
|
96
|
+
}
|
|
97
|
+
```
|
|
70
98
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Pool for server and E2E test files ("forks", "threads")
|
|
75
|
-
pool: 'forks',
|
|
76
|
-
|
|
77
|
-
// Code coverage options
|
|
78
|
-
coverage: {
|
|
79
|
-
// Enable coverage reporting
|
|
80
|
-
enabled: true,
|
|
81
|
-
// Output directory (default: ".coverage")
|
|
82
|
-
dir: '.coverage',
|
|
83
|
-
// Glob pattern(s) to include/exclude
|
|
84
|
-
include: 'src/**',
|
|
85
|
-
exclude: 'src/**/*.test.ts',
|
|
86
|
-
// Minimum thresholds (%)
|
|
87
|
-
statements: 80,
|
|
88
|
-
lines: 80,
|
|
89
|
-
branches: 80,
|
|
90
|
-
functions: 80,
|
|
91
|
-
},
|
|
99
|
+
Every field is optional. Relative paths and globs resolve from the directory containing the config
|
|
100
|
+
file. Explicit CLI flags and positional globs take precedence. Repeatable flags replace configured
|
|
101
|
+
arrays, and nested Playwright and coverage values merge by field.
|
|
92
102
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Browser test files (default: "**/*.test.browser.{ts,tsx}")
|
|
98
|
-
browser: '**/*.test.browser.ts',
|
|
99
|
-
// E2E test files (default: "**/*.test.e2e.{ts,tsx}")
|
|
100
|
-
e2e: '**/*.test.e2e.ts',
|
|
101
|
-
},
|
|
103
|
+
`remix-test.config.ts` and `remix-test.config.js` are no longer discovered. Move their static values
|
|
104
|
+
under `remix.json#test`. Move inline Playwright configuration into `playwright.config.ts` and point
|
|
105
|
+
`playwright.configFile` at it. Use slash-delimited strings for regular expressions, and use CLI flags
|
|
106
|
+
or package scripts for environment-specific overrides.
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
// config file on disk
|
|
105
|
-
playwrightConfig: {
|
|
106
|
-
projects: [
|
|
107
|
-
{ name: 'chromium', use: { browserName: 'chromium' } },
|
|
108
|
-
{ name: 'firefox', use: { browserName: 'firefox' } },
|
|
109
|
-
],
|
|
110
|
-
use: {
|
|
111
|
-
navigationTimeout: 5_000,
|
|
112
|
-
actionTimeout: 5_000,
|
|
113
|
-
},
|
|
114
|
-
},
|
|
108
|
+
### CLI Options
|
|
115
109
|
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
Use the global `--config` flag to select another Remix config file. The flag path resolves from the
|
|
111
|
+
current working directory:
|
|
118
112
|
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
```sh
|
|
114
|
+
remix test --config ./config/remix.ci.json
|
|
115
|
+
```
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
You may specify any test setting as a CLI flag. Boolean settings have negative forms so configured
|
|
118
|
+
`true` values can be disabled explicitly:
|
|
119
|
+
|
|
120
|
+
| Flag | Short |
|
|
121
|
+
| --------------------------- | ----- |
|
|
122
|
+
| `--browser.echo` | |
|
|
123
|
+
| `--no-browser.echo` | |
|
|
124
|
+
| `--browser.open` | |
|
|
125
|
+
| `--no-browser.open` | |
|
|
126
|
+
| `--concurrency <n>` | `-c` |
|
|
127
|
+
| `--coverage` | |
|
|
128
|
+
| `--no-coverage` | |
|
|
129
|
+
| `--coverage.dir <path>` | |
|
|
130
|
+
| `--coverage.include` | |
|
|
131
|
+
| `--coverage.exclude` | |
|
|
132
|
+
| `--coverage.statements` | |
|
|
133
|
+
| `--coverage.lines` | |
|
|
134
|
+
| `--coverage.branches` | |
|
|
135
|
+
| `--coverage.functions` | |
|
|
136
|
+
| `--glob.test` | |
|
|
137
|
+
| `--glob.browser` | |
|
|
138
|
+
| `--glob.e2e` | |
|
|
139
|
+
| `--glob.exclude` | |
|
|
140
|
+
| `--playwrightConfig <path>` | |
|
|
141
|
+
| `--only <pattern>` | |
|
|
142
|
+
| `--pool <forks\|threads>` | |
|
|
143
|
+
| `--project <name>` | `-p` |
|
|
144
|
+
| `--quiet` | `-q` |
|
|
145
|
+
| `--no-quiet` | |
|
|
146
|
+
| `--reporter <name>` | `-r` |
|
|
147
|
+
| `--setup <path>` | `-s` |
|
|
148
|
+
| `--type <name>` | `-t` |
|
|
149
|
+
| `--watch` | `-w` |
|
|
150
|
+
| `--no-watch` | |
|
|
151
|
+
|
|
152
|
+
The standalone `remix-test` executable is no longer installed. Update scripts to use the main Remix CLI:
|
|
153
|
+
|
|
154
|
+
```diff
|
|
155
|
+
- "test": "remix-test --type server"
|
|
156
|
+
+ "test": "remix test --type server"
|
|
157
|
+
```
|
|
124
158
|
|
|
125
|
-
|
|
126
|
-
type: ['server', 'browser', 'e2e'],
|
|
159
|
+
### Focusing Tests
|
|
127
160
|
|
|
128
|
-
|
|
129
|
-
watch: false,
|
|
130
|
-
} satisfies RemixTestConfig
|
|
131
|
-
```
|
|
161
|
+
Use `.only` to focus a suite or test while developing:
|
|
132
162
|
|
|
133
|
-
|
|
163
|
+
```ts
|
|
164
|
+
describe.only('Cart routes', () => {
|
|
165
|
+
it('loads cart items', () => {})
|
|
166
|
+
})
|
|
134
167
|
|
|
135
|
-
|
|
168
|
+
describe('Checkout routes', () => {
|
|
169
|
+
it.only('redirects anonymous users', () => {})
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use `--only <pattern>` to focus tests from the CLI without editing source. Plain patterns are case-insensitive JavaScript regular expressions matched against suite names and full test names:
|
|
136
174
|
|
|
137
175
|
```sh
|
|
138
|
-
remix test --
|
|
176
|
+
remix test --only 'Cart routes'
|
|
177
|
+
remix test --only 'Checkout routes > redirects anonymous users'
|
|
178
|
+
remix test --only '/anonymous users$/'
|
|
139
179
|
```
|
|
140
180
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
| --------------------------- | --------- | --- |
|
|
145
|
-
| `--browser.echo` | |
|
|
146
|
-
| `--browser.open` | |
|
|
147
|
-
| `--concurrency <n>` | `-c` |
|
|
148
|
-
| `--coverage` | |
|
|
149
|
-
| `--coverage.dir <path>` | |
|
|
150
|
-
| `--coverage.include` | |
|
|
151
|
-
| `--coverage.exclude` | |
|
|
152
|
-
| `--coverage.statements` | |
|
|
153
|
-
| `--coverage.lines` | |
|
|
154
|
-
| `--coverage.branches` | |
|
|
155
|
-
| `--coverage.functions` | |
|
|
156
|
-
| `--glob.test` | |
|
|
157
|
-
| `--glob.browser` | |
|
|
158
|
-
| `--glob.e2e` | |
|
|
159
|
-
| `--playwrightConfig <path>` | |
|
|
160
|
-
| `--pool <forks | threads>` | |
|
|
161
|
-
| `--project <name>` | `-p` |
|
|
162
|
-
| `--reporter <name>` | `-r` |
|
|
163
|
-
| `--setup <path>` | `-s` |
|
|
164
|
-
| `--type <name>` | `-t` |
|
|
165
|
-
| `--watch` | `-w` |
|
|
181
|
+
Slash-delimited CLI and `remix.json` patterns preserve their flags, so `/pattern/` is case-sensitive and `/pattern/i` is case-insensitive. Plain string patterns are case-insensitive. Programmatic callers may also pass `RegExp` values.
|
|
182
|
+
|
|
183
|
+
Full test names join nested `describe` names and the test name with `>`. For example, `describe('Cart routes', () => describe('loader', () => it('loads cart items', ...)))` has the full test name `Cart routes > loader > loads cart items`.
|
|
166
184
|
|
|
167
185
|
### Setup
|
|
168
186
|
|
|
@@ -221,12 +239,19 @@ suite('My Test Suite', () => {
|
|
|
221
239
|
import { runRemixTest } from 'remix/test/cli'
|
|
222
240
|
|
|
223
241
|
let exitCode = await runRemixTest({
|
|
224
|
-
|
|
242
|
+
concurrency: 1,
|
|
225
243
|
cwd: process.cwd(),
|
|
244
|
+
glob: { test: 'src/**/*.test.ts' },
|
|
245
|
+
type: ['server'],
|
|
226
246
|
})
|
|
227
247
|
```
|
|
228
248
|
|
|
229
|
-
|
|
249
|
+
The programmatic runner accepts structured options only; it does not discover or load configuration
|
|
250
|
+
files. Programmatic callers may pass richer values such as an inline Playwright config or `RegExp`
|
|
251
|
+
test-name filters. The main Remix CLI owns `remix.json` loading and passes the resolved options to the
|
|
252
|
+
runner.
|
|
253
|
+
|
|
254
|
+
`runRemixTest()` does not read `process.argv` or terminate the process; it returns the runner exit code. The main `remix` executable owns argument parsing and passes the final code to `process.exit()` so open workers, browsers, or project handles cannot keep the CLI alive. `@remix-run/test` does not install a standalone executable.
|
|
230
255
|
|
|
231
256
|
### Test Context
|
|
232
257
|
|
|
@@ -426,28 +451,49 @@ describe('checkout', () => {
|
|
|
426
451
|
})
|
|
427
452
|
```
|
|
428
453
|
|
|
429
|
-
Configure Playwright
|
|
454
|
+
Configure Playwright's browsers, timeouts, viewport, and other executable settings in
|
|
455
|
+
`playwright.config.ts`:
|
|
430
456
|
|
|
431
457
|
```ts
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
458
|
+
import { defineConfig } from 'playwright/test'
|
|
459
|
+
|
|
460
|
+
export default defineConfig({
|
|
461
|
+
projects: [
|
|
462
|
+
{ name: 'chromium', use: { browserName: 'chromium' } },
|
|
463
|
+
{ name: 'firefox', use: { browserName: 'firefox' } },
|
|
464
|
+
{ name: 'webkit', use: { browserName: 'webkit' } },
|
|
465
|
+
],
|
|
466
|
+
use: {
|
|
467
|
+
navigationTimeout: 5_000,
|
|
468
|
+
actionTimeout: 5_000,
|
|
443
469
|
},
|
|
470
|
+
})
|
|
471
|
+
```
|
|
444
472
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
473
|
+
Reference it from `remix.json` when it is not at the default location:
|
|
474
|
+
|
|
475
|
+
```jsonc
|
|
476
|
+
{
|
|
477
|
+
"test": {
|
|
478
|
+
"playwright": {
|
|
479
|
+
"configFile": "./playwright.config.ts",
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
}
|
|
448
483
|
```
|
|
449
484
|
|
|
450
|
-
Set `
|
|
485
|
+
Set `test.playwright.open` to `true`, or pass `--browser.open`, to keep the browser open after tests
|
|
486
|
+
finish—useful for debugging failures.
|
|
487
|
+
|
|
488
|
+
## Related Packages
|
|
489
|
+
|
|
490
|
+
- [`assert`](https://github.com/remix-run/remix/tree/main/packages/assert) provides assertions that work in server and browser tests.
|
|
491
|
+
- [`ui`](https://github.com/remix-run/remix/tree/main/packages/ui) provides the `remix/ui/test` browser rendering utilities.
|
|
492
|
+
|
|
493
|
+
## Related Work
|
|
494
|
+
|
|
495
|
+
- [Playwright](https://playwright.dev) provides the browser automation used by browser and E2E tests.
|
|
496
|
+
- [Node.js test runner](https://nodejs.org/api/test.html) provides prior art for the test framework API and reporting model.
|
|
451
497
|
|
|
452
498
|
## License
|
|
453
499
|
|
package/src/ui/README.md
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
# ui
|
|
2
2
|
|
|
3
|
-
Runtime UI primitives for Remix apps, including the component runtime, server rendering, frame hydration, reusable mixins, first-party
|
|
3
|
+
Runtime UI primitives for Remix apps, including the component runtime, server rendering, frame hydration, reusable mixins, and headless first-party behavior primitives.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- Component runtime APIs for rendering, hydration, frame navigation, and JSX
|
|
7
|
+
- Component runtime APIs for rendering, hydration, link and form frame navigation, and JSX
|
|
8
8
|
- Server rendering APIs for streaming Remix UI trees and frames
|
|
9
9
|
- `mix` composition with event, ref, CSS, and animation helpers
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- `createTheme()` and `createGlyphSheet()` utilities for shared app styling and glyphs
|
|
10
|
+
- Headless behavior primitives for controls such as menus, listboxes, popovers, selects, and comboboxes
|
|
11
|
+
- Lower-level utilities for keyboard events, typeahead search, refs, attributes, and CSS transition timing
|
|
13
12
|
|
|
14
13
|
## Installation
|
|
15
14
|
|
|
@@ -19,188 +18,148 @@ npm i remix
|
|
|
19
18
|
|
|
20
19
|
## Usage
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
Compose behavior primitives with your own markup and styles:
|
|
23
22
|
|
|
24
23
|
```tsx
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
sm: '4px',
|
|
33
|
-
md: '8px',
|
|
34
|
-
lg: '12px',
|
|
35
|
-
xl: '16px',
|
|
36
|
-
xxl: '24px',
|
|
37
|
-
},
|
|
38
|
-
radius: {
|
|
39
|
-
none: '0px',
|
|
40
|
-
sm: '4px',
|
|
41
|
-
md: '8px',
|
|
42
|
-
lg: '12px',
|
|
43
|
-
xl: '16px',
|
|
44
|
-
full: '9999px',
|
|
45
|
-
},
|
|
46
|
-
fontSize: {
|
|
47
|
-
xxxs: '10px',
|
|
48
|
-
xxs: '11px',
|
|
49
|
-
xs: '12px',
|
|
50
|
-
sm: '14px',
|
|
51
|
-
md: '16px',
|
|
52
|
-
lg: '18px',
|
|
53
|
-
xl: '20px',
|
|
54
|
-
xxl: '28px',
|
|
55
|
-
},
|
|
56
|
-
lineHeight: {
|
|
57
|
-
tight: '1.2',
|
|
58
|
-
normal: '1.5',
|
|
59
|
-
relaxed: '1.7',
|
|
60
|
-
},
|
|
61
|
-
fontWeight: {
|
|
62
|
-
normal: '400',
|
|
63
|
-
medium: '500',
|
|
64
|
-
semibold: '600',
|
|
65
|
-
bold: '700',
|
|
66
|
-
},
|
|
67
|
-
shadow: {
|
|
68
|
-
xs: '0 1px 2px rgb(0 0 0 / 0.05)',
|
|
69
|
-
sm: '0 1px 3px rgb(0 0 0 / 0.10)',
|
|
70
|
-
md: '0 4px 10px rgb(0 0 0 / 0.12)',
|
|
71
|
-
lg: '0 10px 30px rgb(0 0 0 / 0.16)',
|
|
72
|
-
xl: '0 20px 50px rgb(0 0 0 / 0.20)',
|
|
73
|
-
},
|
|
74
|
-
zIndex: {
|
|
75
|
-
dropdown: '1000',
|
|
76
|
-
popover: '1100',
|
|
77
|
-
sticky: '1200',
|
|
78
|
-
overlay: '1300',
|
|
79
|
-
modal: '1400',
|
|
80
|
-
toast: '1500',
|
|
81
|
-
tooltip: '1600',
|
|
82
|
-
},
|
|
83
|
-
surface: {
|
|
84
|
-
lvl0: '#ffffff',
|
|
85
|
-
lvl1: '#f8fafc',
|
|
86
|
-
lvl2: '#f1f5f9',
|
|
87
|
-
lvl3: '#e5edf7',
|
|
88
|
-
lvl4: '#dbe6f4',
|
|
89
|
-
},
|
|
90
|
-
colors: {
|
|
91
|
-
text: {
|
|
92
|
-
primary: '#111827',
|
|
93
|
-
secondary: '#374151',
|
|
94
|
-
muted: '#6b7280',
|
|
95
|
-
link: '#2563eb',
|
|
96
|
-
},
|
|
97
|
-
border: {
|
|
98
|
-
subtle: '#e5e7eb',
|
|
99
|
-
default: '#d1d5db',
|
|
100
|
-
strong: '#9ca3af',
|
|
101
|
-
},
|
|
102
|
-
focus: {
|
|
103
|
-
ring: '#3b82f6',
|
|
104
|
-
},
|
|
105
|
-
overlay: {
|
|
106
|
-
scrim: 'rgb(0 0 0 / 0.45)',
|
|
107
|
-
},
|
|
108
|
-
action: {
|
|
109
|
-
primary: {
|
|
110
|
-
background: '#2563eb',
|
|
111
|
-
backgroundHover: '#1d4ed8',
|
|
112
|
-
backgroundActive: '#1e40af',
|
|
113
|
-
foreground: '#ffffff',
|
|
114
|
-
border: '#2563eb',
|
|
115
|
-
},
|
|
116
|
-
secondary: {
|
|
117
|
-
background: '#ffffff',
|
|
118
|
-
backgroundHover: '#f8fafc',
|
|
119
|
-
backgroundActive: '#f1f5f9',
|
|
120
|
-
foreground: '#111827',
|
|
121
|
-
border: '#d1d5db',
|
|
122
|
-
},
|
|
123
|
-
danger: {
|
|
124
|
-
background: '#dc2626',
|
|
125
|
-
backgroundHover: '#b91c1c',
|
|
126
|
-
backgroundActive: '#991b1b',
|
|
127
|
-
foreground: '#ffffff',
|
|
128
|
-
border: '#dc2626',
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
},
|
|
24
|
+
import { css } from 'remix/ui'
|
|
25
|
+
import * as popover from 'remix/ui/popover'
|
|
26
|
+
|
|
27
|
+
let triggerCss = css({
|
|
28
|
+
border: '1px solid #d1d5db',
|
|
29
|
+
borderRadius: '6px',
|
|
30
|
+
padding: '6px 10px',
|
|
132
31
|
})
|
|
133
|
-
```
|
|
134
32
|
|
|
135
|
-
|
|
33
|
+
let surfaceCss = css({
|
|
34
|
+
background: 'white',
|
|
35
|
+
border: '1px solid #d1d5db',
|
|
36
|
+
borderRadius: '6px',
|
|
37
|
+
padding: '8px',
|
|
38
|
+
})
|
|
136
39
|
|
|
137
|
-
|
|
138
|
-
|
|
40
|
+
function ViewOptions() {
|
|
41
|
+
let open = false
|
|
139
42
|
|
|
140
|
-
function Layout(handle: Handle<{ children: RemixNode }>) {
|
|
141
43
|
return () => (
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
44
|
+
<popover.Context>
|
|
45
|
+
<button
|
|
46
|
+
mix={[triggerCss, popover.anchor({ placement: 'bottom-end' }), popover.focusOnHide()]}
|
|
47
|
+
onClick={() => {
|
|
48
|
+
open = true
|
|
49
|
+
}}
|
|
50
|
+
type="button"
|
|
51
|
+
>
|
|
52
|
+
View options
|
|
53
|
+
</button>
|
|
54
|
+
<div
|
|
55
|
+
mix={[
|
|
56
|
+
surfaceCss,
|
|
57
|
+
popover.surface({
|
|
58
|
+
open,
|
|
59
|
+
onHide() {
|
|
60
|
+
open = false
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
]}
|
|
64
|
+
>
|
|
65
|
+
Panel content
|
|
66
|
+
</div>
|
|
67
|
+
</popover.Context>
|
|
148
68
|
)
|
|
149
69
|
}
|
|
150
70
|
```
|
|
151
71
|
|
|
152
|
-
|
|
72
|
+
Button styling is available as a composable mixin:
|
|
153
73
|
|
|
154
74
|
```tsx
|
|
155
|
-
import
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
75
|
+
import button from 'remix/ui/button'
|
|
76
|
+
|
|
77
|
+
function Actions() {
|
|
78
|
+
return () => <button mix={button({ tone: 'primary' })}>Create project</button>
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Frame Navigation
|
|
83
|
+
|
|
84
|
+
Configure `resolveFrame` once to progressively enhance same-origin links and forms. The resolver owns the fetch request and receives native submission metadata for non-GET forms:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import type { ResolveFrameOptions } from 'remix/ui'
|
|
88
|
+
import { run } from 'remix/ui'
|
|
89
|
+
|
|
90
|
+
let app = run({
|
|
91
|
+
async loadModule(moduleUrl, exportName) {
|
|
92
|
+
let mod = await import(moduleUrl)
|
|
93
|
+
return mod[exportName]
|
|
94
|
+
},
|
|
95
|
+
async resolveFrame(src, options) {
|
|
96
|
+
return fetch(src, {
|
|
97
|
+
headers: { Accept: 'text/html', 'X-Remix-Frame': 'true' },
|
|
98
|
+
method: options?.method,
|
|
99
|
+
body: getRequestBody(options),
|
|
100
|
+
signal: options?.signal,
|
|
101
|
+
})
|
|
102
|
+
},
|
|
165
103
|
})
|
|
166
104
|
|
|
167
|
-
|
|
105
|
+
function getRequestBody(options?: ResolveFrameOptions): BodyInit | undefined {
|
|
106
|
+
let formData = options?.formData
|
|
107
|
+
if (!formData) return
|
|
108
|
+
if (options.encType !== 'application/x-www-form-urlencoded') return formData
|
|
109
|
+
|
|
110
|
+
let body = new URLSearchParams()
|
|
111
|
+
for (let [name, value] of formData) {
|
|
112
|
+
body.append(name, typeof value === 'string' ? value : value.name)
|
|
113
|
+
}
|
|
114
|
+
return body
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await app.ready()
|
|
168
118
|
```
|
|
169
119
|
|
|
170
|
-
|
|
120
|
+
Forms remain ordinary HTML forms before the runtime starts. Add `rmx-target` to reload a named frame, or `rmx-document` to require a full-document submission:
|
|
171
121
|
|
|
172
122
|
```tsx
|
|
173
|
-
import
|
|
174
|
-
import { Button } from 'remix/ui/button'
|
|
175
|
-
import { Glyph } from 'remix/ui/glyph'
|
|
176
|
-
import { RMX_01, RMX_01_GLYPHS } from 'remix/ui/theme'
|
|
123
|
+
import { Frame } from 'remix/ui'
|
|
177
124
|
|
|
178
|
-
function
|
|
125
|
+
function AccountPage() {
|
|
179
126
|
return () => (
|
|
180
|
-
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
</Button>
|
|
189
|
-
{handle.props.children}
|
|
190
|
-
</body>
|
|
191
|
-
</html>
|
|
127
|
+
<>
|
|
128
|
+
<Frame name="account" src="/account/edit" />
|
|
129
|
+
<form action="/account/edit" method="post" rmx-target="account">
|
|
130
|
+
<label for="display-name">Display name</label>
|
|
131
|
+
<input id="display-name" name="displayName" required />
|
|
132
|
+
<button type="submit">Save</button>
|
|
133
|
+
</form>
|
|
134
|
+
</>
|
|
192
135
|
)
|
|
193
136
|
}
|
|
194
137
|
```
|
|
195
138
|
|
|
139
|
+
Native constraint validation and submitter overrides still apply. GET form values arrive in `src`; non-GET forms provide `formData`, `method`, and `encType` to the resolver. See [Frames](https://github.com/remix-run/remix/blob/main/packages/ui/docs/frames.md#form-navigation) for targeting, history behavior, request encoding, opt-outs, and server response guidance.
|
|
140
|
+
|
|
141
|
+
Use `rmx-history="push|replace"` on an enhanced anchor or form to control how the navigation updates history. This can override the automatic replacement used for non-GET form submissions to the current URL.
|
|
142
|
+
|
|
143
|
+
## Preserving Client-Owned DOM
|
|
144
|
+
|
|
145
|
+
Use `rmx-preserve-dom` on the smallest element whose live DOM should belong to client code after initial render, such as a custom element or third-party widget:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
<pagefind-ui data-key="search" rmx-preserve-dom>
|
|
149
|
+
<button type="button">Search</button>
|
|
150
|
+
</pagefind-ui>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Remix UI still renders the element's children during SSR and still hydrates any initial client entries inside it. On later frame reloads, matched `rmx-preserve-dom` elements keep their current attributes and children instead of accepting incoming DOM updates. See [Preserving client-owned DOM](https://github.com/remix-run/remix/blob/main/packages/ui/docs/frames.md#preserving-client-owned-dom) for guidance and caveats.
|
|
154
|
+
|
|
196
155
|
## Cascade Layers
|
|
197
156
|
|
|
198
|
-
Remix UI emits
|
|
157
|
+
Remix UI emits generated `css(...)` rules under the `rmx` cascade layer. Unlayered CSS outranks layered CSS, so use explicit layer order when mixing Remix UI with global styles.
|
|
199
158
|
|
|
200
|
-
Put layers that should lose to Remix UI before `rmx
|
|
159
|
+
Put layers that should lose to Remix UI before `rmx`:
|
|
201
160
|
|
|
202
161
|
```css
|
|
203
|
-
@layer base, rmx
|
|
162
|
+
@layer base, rmx;
|
|
204
163
|
|
|
205
164
|
@layer base {
|
|
206
165
|
button,
|