@remix-run/cli 0.3.1 → 0.3.2
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/dist/lib/bootstrap-project.js +7 -4
- package/package.json +3 -3
- package/src/lib/bootstrap-project.ts +9 -6
- package/template/.agents/skills/remix/SKILL.md +115 -262
- package/template/.agents/skills/remix/references/animate-elements.md +8 -17
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +14 -33
- package/template/.agents/skills/remix/references/auth-and-sessions.md +10 -31
- package/template/.agents/skills/remix/references/component-model.md +11 -26
- package/template/.agents/skills/remix/references/create-mixins.md +4 -9
- package/template/.agents/skills/remix/references/data-and-validation.md +54 -77
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +23 -56
- package/template/.agents/skills/remix/references/middleware-and-server.md +17 -38
- package/template/.agents/skills/remix/references/mixins-styling-events.md +14 -32
- package/template/.agents/skills/remix/references/routing-and-controllers.md +17 -43
- package/template/.agents/skills/remix/references/testing-patterns.md +13 -32
- package/template/app/ui/document.tsx +22 -17
- package/template/app/ui/scaffold-home-page.tsx +56 -47
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What This Covers
|
|
4
4
|
|
|
5
|
-
How to test the two layers most Remix code lives in: HTTP behavior and DOM behavior. Read this when
|
|
6
|
-
the task involves:
|
|
5
|
+
How to test the two layers most Remix code lives in: HTTP behavior and DOM behavior. Read this when the task involves:
|
|
7
6
|
|
|
8
7
|
- Driving the router with `router.fetch(new Request(...))` and asserting on the returned `Response`
|
|
9
8
|
- Building a fresh router per test for session, storage, or database isolation
|
|
@@ -12,24 +11,18 @@ the task involves:
|
|
|
12
11
|
- Using adjacent CLI checks such as `remix routes`, `remix doctor`, and `remix version`
|
|
13
12
|
- Choosing which layer to test for a given behavior
|
|
14
13
|
|
|
15
|
-
For session and auth test setup, see `auth-and-sessions.md`. For component lifecycle, see
|
|
16
|
-
`component-model.md`.
|
|
14
|
+
For session and auth test setup, see `auth-and-sessions.md`. For component lifecycle, see `component-model.md`.
|
|
17
15
|
|
|
18
16
|
## Two Shapes
|
|
19
17
|
|
|
20
|
-
Remix tests run with `remix test`, use `remix/test` for the test framework, and use
|
|
21
|
-
`remix/assert` for assertions. Two main shapes:
|
|
18
|
+
Remix tests run with `remix test`, use `remix/test` for the test framework, and use `remix/assert` for assertions. Two main shapes:
|
|
22
19
|
|
|
23
|
-
- **Server / router tests** — drive the router with `router.fetch(new Request(...))` and assert
|
|
24
|
-
|
|
25
|
-
- **Component tests** — render a component into a real DOM `Element` with `render(...)`, or use
|
|
26
|
-
`createRoot(...)` directly when you need lower-level root control.
|
|
20
|
+
- **Server / router tests** — drive the router with `router.fetch(new Request(...))` and assert on the returned `Response`. No DOM, no browser harness.
|
|
21
|
+
- **Component tests** — render a component into a real DOM `Element` with `render(...)`, or use `createRoot(...)` directly when you need lower-level root control.
|
|
27
22
|
|
|
28
23
|
## Server / Router Tests
|
|
29
24
|
|
|
30
|
-
Treat the router as a pure `(Request) => Promise<Response>` function. Build a fresh app router
|
|
31
|
-
per test (or per suite) so middleware state — sessions, in-memory storage, the database — stays
|
|
32
|
-
isolated.
|
|
25
|
+
Treat the router as a pure `(Request) => Promise<Response>` function. Build a fresh app router per test (or per suite) so middleware state — sessions, in-memory storage, the database — stays isolated.
|
|
33
26
|
|
|
34
27
|
```ts
|
|
35
28
|
import * as assert from 'remix/assert'
|
|
@@ -49,10 +42,7 @@ describe('home', () => {
|
|
|
49
42
|
})
|
|
50
43
|
```
|
|
51
44
|
|
|
52
|
-
Use `routes.<name>.href(...)` to build URLs in tests so they stay in sync with the route
|
|
53
|
-
definition. For form-style POSTs, attach a `FormData` body to the `Request`. For tests that need
|
|
54
|
-
a known session, swap in `createMemorySessionStorage()` and a test cookie when constructing the
|
|
55
|
-
router.
|
|
45
|
+
Use `routes.<name>.href(...)` to build URLs in tests so they stay in sync with the route definition. For form-style POSTs, attach a `FormData` body to the `Request`. For tests that need a known session, swap in `createMemorySessionStorage()` and a test cookie when constructing the router.
|
|
56
46
|
|
|
57
47
|
```ts
|
|
58
48
|
import { createMemorySessionStorage } from 'remix/session-storage/memory'
|
|
@@ -64,8 +54,7 @@ let router = createBookstoreRouter({
|
|
|
64
54
|
})
|
|
65
55
|
```
|
|
66
56
|
|
|
67
|
-
Use `createTestServer` from `remix/node-fetch-server/test` when the behavior depends on a real
|
|
68
|
-
HTTP origin, redirects, streaming, cookies through a network boundary, or browser-style `fetch`:
|
|
57
|
+
Use `createTestServer` from `remix/node-fetch-server/test` when the behavior depends on a real HTTP origin, redirects, streaming, cookies through a network boundary, or browser-style `fetch`:
|
|
69
58
|
|
|
70
59
|
```ts
|
|
71
60
|
import { createTestServer } from 'remix/node-fetch-server/test'
|
|
@@ -102,16 +91,11 @@ export default {
|
|
|
102
91
|
}
|
|
103
92
|
```
|
|
104
93
|
|
|
105
|
-
Use `remix test --coverage` to enable coverage with defaults. Use `glob.exclude` when discovery
|
|
106
|
-
would otherwise enter generated output, symlinked workspaces, or other paths that should not
|
|
107
|
-
produce tests.
|
|
94
|
+
Use `remix test --coverage` to enable coverage with defaults. Use `glob.exclude` when discovery would otherwise enter generated output, symlinked workspaces, or other paths that should not produce tests.
|
|
108
95
|
|
|
109
96
|
## Component Tests
|
|
110
97
|
|
|
111
|
-
Use `render(...)` from `remix/ui/test` for most component tests. It creates a real DOM container,
|
|
112
|
-
flushes the initial render, and returns `act(...)` so interactions can flush pending updates before
|
|
113
|
-
assertions. Use `createRoot(container)` from `remix/ui` directly when a test needs explicit control
|
|
114
|
-
over root rendering, flushing, or disposal.
|
|
98
|
+
Use `render(...)` from `remix/ui/test` for most component tests. It creates a real DOM container, flushes the initial render, and returns `act(...)` so interactions can flush pending updates before assertions. Use `createRoot(container)` from `remix/ui` directly when a test needs explicit control over root rendering, flushing, or disposal.
|
|
115
99
|
|
|
116
100
|
### Basic pattern
|
|
117
101
|
|
|
@@ -130,8 +114,7 @@ result.cleanup()
|
|
|
130
114
|
|
|
131
115
|
### Why act / flush
|
|
132
116
|
|
|
133
|
-
- **After initial render** — ensures event listeners are attached and the DOM is ready for
|
|
134
|
-
interaction.
|
|
117
|
+
- **After initial render** — ensures event listeners are attached and the DOM is ready for interaction.
|
|
135
118
|
- **After interactions** — applies updates from `handle.update()` calls triggered by events.
|
|
136
119
|
- **After async work resolves** — applies updates from resolved `queueTask(...)` callbacks.
|
|
137
120
|
|
|
@@ -152,8 +135,7 @@ assert.equal(result.container.textContent, 'Expected data')
|
|
|
152
135
|
|
|
153
136
|
### Component removal
|
|
154
137
|
|
|
155
|
-
Use `result.cleanup()` or `root.dispose()` to remove the component tree and verify cleanup
|
|
156
|
-
behavior:
|
|
138
|
+
Use `result.cleanup()` or `root.dispose()` to remove the component tree and verify cleanup behavior:
|
|
157
139
|
|
|
158
140
|
```tsx
|
|
159
141
|
let result = render(<MyComponent />)
|
|
@@ -168,5 +150,4 @@ assert.throws(() => result.$('.content'), /cleaned up/)
|
|
|
168
150
|
|
|
169
151
|
- Prefer real DOM interactions over mocking framework behavior.
|
|
170
152
|
- Avoid testing implementation-only markers unless they are the only stable synchronization point.
|
|
171
|
-
- One representative flow proving a behavior is better than repeating the same assertion across many
|
|
172
|
-
paths.
|
|
153
|
+
- One representative flow proving a behavior is better than repeating the same assertion across many paths.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Handle, RemixNode } from 'remix/ui'
|
|
2
|
+
import { css } from 'remix/ui'
|
|
2
3
|
|
|
3
4
|
import { routes } from '../routes.ts'
|
|
4
5
|
|
|
@@ -10,22 +11,26 @@ export interface DocumentProps {
|
|
|
10
11
|
|
|
11
12
|
const DEFAULT_TITLE = readAppDisplayName('%%RMX_APP_DISPLAY_NAME_URI_COMPONENT%%')
|
|
12
13
|
|
|
13
|
-
export function Document() {
|
|
14
|
-
return (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
export function Document(handle: Handle<DocumentProps>) {
|
|
15
|
+
return () => {
|
|
16
|
+
let { children, head, title = DEFAULT_TITLE } = handle.props
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<html lang="en">
|
|
20
|
+
<head>
|
|
21
|
+
<meta charSet="utf-8" />
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
23
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
24
|
+
<title>{title}</title>
|
|
25
|
+
{head}
|
|
26
|
+
</head>
|
|
27
|
+
<body mix={css({ margin: 0 })}>
|
|
28
|
+
{children}
|
|
29
|
+
<script type="module" src={routes.assets.href({ path: 'app/assets/entry.ts' })}></script>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
function readAppDisplayName(value: string): string {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Delete this file and put your own home page in app/actions/controller.tsx
|
|
2
|
-
import {
|
|
2
|
+
import type { Handle, RemixNode } from 'remix/ui'
|
|
3
|
+
import { css } from 'remix/ui'
|
|
3
4
|
|
|
4
5
|
import { PromptButton } from '../assets/prompt-button.tsx'
|
|
5
6
|
import { Document } from './document.tsx'
|
|
@@ -203,54 +204,62 @@ function CodingWithAiCard() {
|
|
|
203
204
|
)
|
|
204
205
|
}
|
|
205
206
|
|
|
206
|
-
function CardLink() {
|
|
207
|
-
return (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
background: '
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
207
|
+
function CardLink(handle: Handle<{ href: string; icon: RemixNode; label: string }>) {
|
|
208
|
+
return () => {
|
|
209
|
+
let { href, icon, label } = handle.props
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<a
|
|
213
|
+
href={href}
|
|
214
|
+
mix={css({
|
|
215
|
+
display: 'flex',
|
|
216
|
+
gap: '16px',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
padding: '16px',
|
|
219
|
+
borderRadius: '12px',
|
|
220
|
+
color: 'var(--text-primary)',
|
|
221
|
+
textDecoration: 'none',
|
|
222
|
+
background: 'transparent',
|
|
223
|
+
transition: 'background-color 150ms ease, color 150ms ease',
|
|
224
|
+
'&:hover, &:focus-visible': {
|
|
225
|
+
background: 'var(--surface-4)',
|
|
226
|
+
color: 'var(--brand-blue)',
|
|
227
|
+
outline: 'none',
|
|
228
|
+
},
|
|
229
|
+
})}
|
|
230
|
+
>
|
|
231
|
+
<IconSlot>{icon}</IconSlot>
|
|
232
|
+
<span mix={css({ fontSize: '14px', lineHeight: 1.5, whiteSpace: 'nowrap' })}>{label}</span>
|
|
233
|
+
</a>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
231
236
|
}
|
|
232
237
|
|
|
233
|
-
function IconSlot() {
|
|
234
|
-
return (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
238
|
+
function IconSlot(handle: Handle<{ children: RemixNode; rotated?: boolean }>) {
|
|
239
|
+
return () => {
|
|
240
|
+
let { children, rotated = false } = handle.props
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<span
|
|
244
|
+
aria-hidden="true"
|
|
245
|
+
mix={css({
|
|
246
|
+
flex: '0 0 24px',
|
|
247
|
+
width: '24px',
|
|
248
|
+
display: 'flex',
|
|
249
|
+
alignItems: 'center',
|
|
250
|
+
justifyContent: rotated ? 'center' : 'flex-start',
|
|
251
|
+
'& svg': {
|
|
252
|
+
width: '20px',
|
|
253
|
+
height: '20px',
|
|
254
|
+
display: 'block',
|
|
255
|
+
...(rotated ? { transform: 'rotate(180deg)' } : {}),
|
|
256
|
+
},
|
|
257
|
+
})}
|
|
258
|
+
>
|
|
259
|
+
{children}
|
|
260
|
+
</span>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
254
263
|
}
|
|
255
264
|
|
|
256
265
|
function Footer() {
|