@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.7
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/NOTICE +4 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
- package/dist/chunk-4MWG3B7G.js.map +1 -0
- package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +515 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +964 -152
- package/dist/index.js +4279 -866
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/docs/authentication.md +487 -0
- package/docs/authorization.md +435 -0
- package/docs/cache.md +647 -0
- package/docs/collections.md +2270 -0
- package/docs/configuration.md +334 -0
- package/docs/console.md +651 -0
- package/docs/container.md +430 -0
- package/docs/controllers.md +406 -0
- package/docs/csrf.md +126 -0
- package/docs/database-testing.md +144 -0
- package/docs/database.md +236 -0
- package/docs/deployment.md +276 -0
- package/docs/eloquent-collections.md +218 -0
- package/docs/eloquent-factories.md +255 -0
- package/docs/eloquent-mutators.md +254 -0
- package/docs/eloquent-observers.md +145 -0
- package/docs/eloquent-relationships.md +557 -0
- package/docs/eloquent.md +494 -0
- package/docs/encryption.md +104 -0
- package/docs/errors.md +318 -0
- package/docs/events.md +336 -0
- package/docs/facades.md +278 -0
- package/docs/hashing.md +118 -0
- package/docs/helpers.md +1281 -0
- package/docs/http-tests.md +386 -0
- package/docs/inertia.md +485 -0
- package/docs/installation.md +209 -0
- package/docs/lifecycle.md +234 -0
- package/docs/live-tests.md +226 -0
- package/docs/live.md +910 -0
- package/docs/logging.md +255 -0
- package/docs/mail.md +619 -0
- package/docs/middleware.md +384 -0
- package/docs/migrations.md +416 -0
- package/docs/mocking.md +264 -0
- package/docs/navigation.json +273 -0
- package/docs/notifications.md +225 -0
- package/docs/pagination.md +247 -0
- package/docs/passwords.md +207 -0
- package/docs/providers.md +187 -0
- package/docs/queries.md +600 -0
- package/docs/queues.md +330 -0
- package/docs/requests.md +533 -0
- package/docs/responses.md +444 -0
- package/docs/routing.md +738 -0
- package/docs/scheduling.md +277 -0
- package/docs/seeding.md +122 -0
- package/docs/session.md +409 -0
- package/docs/starter-kits.md +396 -0
- package/docs/storage.md +192 -0
- package/docs/structure.md +264 -0
- package/docs/testing.md +165 -0
- package/docs/time-travel.md +138 -0
- package/docs/validation.md +1792 -0
- package/docs/verification.md +152 -0
- package/docs/views.md +566 -0
- package/docs/vite.md +322 -0
- package/package.json +34 -32
- package/dist/chunk-I63ZFMX6.js.map +0 -1
- package/dist/chunk-MFTZQMZM.js.map +0 -1
- package/dist/chunk-POEP4B2M.js.map +0 -1
- package/dist/chunk-ZOEPKBVF.js.map +0 -1
- package/dist/index.d-BJyJx6U1.d.ts +0 -319
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Request lifecycle
|
|
2
|
+
|
|
3
|
+
Oriel builds your app once, when its process starts, and answers every request from it, until the process ends and
|
|
4
|
+
the app shuts down.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
You can build an app without knowing how Oriel works inside. Knowing the order in which it does things tells you
|
|
9
|
+
where your own code goes: what a service provider can rely on, when your routes exist, and what lasts longer than one
|
|
10
|
+
request.
|
|
11
|
+
|
|
12
|
+
## Lifecycle overview
|
|
13
|
+
|
|
14
|
+
### First steps
|
|
15
|
+
|
|
16
|
+
Your app has two entry points. `public/index.ts` starts the web server, as `node --import tsx public/index.ts` or
|
|
17
|
+
`bun public/index.ts`, and `node oriel serve` starts the same server while you work. The `oriel` file runs a command,
|
|
18
|
+
as `node oriel migrate`. Both get the application from `createApplication()` in `bootstrap/app.ts`:
|
|
19
|
+
|
|
20
|
+
```ts title="public/index.ts"
|
|
21
|
+
// @filename: bootstrap/app.ts
|
|
22
|
+
export declare function createApplication(): Promise<import('@orieljs/framework').Application>
|
|
23
|
+
// ---cut---
|
|
24
|
+
import { serve } from '@orieljs/framework'
|
|
25
|
+
import { createApplication } from '../bootstrap/app'
|
|
26
|
+
|
|
27
|
+
// Production entry: `node --import tsx public/index.ts`, or `bun public/index.ts`.
|
|
28
|
+
await serve(await createApplication())
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`Application.configure()` creates the application, which is also the [service container](container.md). The methods
|
|
32
|
+
after it record what you give them, and `create()` puts the app together:
|
|
33
|
+
|
|
34
|
+
```ts title="bootstrap/app.ts"
|
|
35
|
+
import { dirname } from 'node:path'
|
|
36
|
+
import { Application } from '@orieljs/framework'
|
|
37
|
+
import providers from './providers'
|
|
38
|
+
|
|
39
|
+
/** Builds the application. Each call returns a fresh app (tests need one per test). */
|
|
40
|
+
export function createApplication(): Promise<Application> {
|
|
41
|
+
return Application.configure({ basePath: dirname(import.meta.dirname) })
|
|
42
|
+
.withRouting({ web: 'routes/web.ts', console: 'routes/console.ts', health: '/up' })
|
|
43
|
+
.withProviders(providers)
|
|
44
|
+
.create()
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`create()` loads your `.env` file and every file in `config/` ([Configuration](configuration.md) covers both), then
|
|
49
|
+
registers the service providers. Each call makes a new application, so your tests can have a fresh one for every test.
|
|
50
|
+
|
|
51
|
+
### HTTP / console kernels
|
|
52
|
+
|
|
53
|
+
A request goes to the HTTP kernel, and a command to the console kernel.
|
|
54
|
+
|
|
55
|
+
`serve()` answers a `GET` or `HEAD` request for a file in `public/` itself, and hands every other request to
|
|
56
|
+
`app.handle()`, which passes it to the HTTP kernel and returns the response. The kernel takes a web-standard `Request`
|
|
57
|
+
and returns a `Response`: think of it as a black box that stands for your whole app. It wraps the request in Oriel's
|
|
58
|
+
request object, applies the trusted proxies and the body limits of `config/http.ts` (see
|
|
59
|
+
[Requests](requests.md#body-size-limits)), reads the body, and sends the request through the global middleware to the
|
|
60
|
+
router.
|
|
61
|
+
|
|
62
|
+
`node oriel` gives its arguments to the console kernel. The command runs, the app terminates as it does after a
|
|
63
|
+
request, then it shuts down, and the process exits with the command's exit code.
|
|
64
|
+
|
|
65
|
+
### Service providers
|
|
66
|
+
|
|
67
|
+
Service providers set up every part of the framework: the database, the queue, validation, routing and the rest.
|
|
68
|
+
`create()` calls each provider's `register()`, in order. The framework has 22, registered first, in this order:
|
|
69
|
+
Pipeline, Encryption, Cookie, Session, Hashing, Log, Event, Cache, Filesystem, Database, Queue, Bus, Mail,
|
|
70
|
+
Notifications, Validation, Auth, View, Inertia, Live, Routing, Http and Console (each one's class is its name followed
|
|
71
|
+
by `ServiceProvider`). Each may rely on the ones before it. Yours come next, in the order `bootstrap/providers.ts`
|
|
72
|
+
lists them.
|
|
73
|
+
|
|
74
|
+
`create()` registers the providers but doesn't boot them. The app boots once, the first time something needs it:
|
|
75
|
+
`serve()` before it starts listening, `node oriel` before it runs its command, your tests before each test, or the
|
|
76
|
+
first request the app handles. Booting runs, in this order:
|
|
77
|
+
|
|
78
|
+
1. the `booting()` callbacks;
|
|
79
|
+
2. each provider's `boot()`, in the order they were registered. A provider registered while the app boots is booted in
|
|
80
|
+
the same pass, and one registered after it has booted is booted at once;
|
|
81
|
+
3. the `booted()` callbacks, in the order they were added: those you give the builder first, then the framework's,
|
|
82
|
+
which add the health route, load the route files of `withRouting()` (the `web` files in the `web` group, then the
|
|
83
|
+
`api` files in the `api` group), apply your `withMiddleware()` callback and, in the console only, find your
|
|
84
|
+
commands; last, those your providers add in `boot()`.
|
|
85
|
+
|
|
86
|
+
Your route files run once every provider has booted, so a route pattern, a binding or a gate that a provider sets up
|
|
87
|
+
in `boot()` is ready for them. The builder takes callbacks of its own:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
Application.configure({ basePath: '.' })
|
|
91
|
+
.booting((app) => {
|
|
92
|
+
// Before the first provider boots...
|
|
93
|
+
})
|
|
94
|
+
.booted((app) => {
|
|
95
|
+
// Once every provider has booted, before your routes load...
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> [!NOTE]
|
|
100
|
+
> Laravel bootstraps the application again for every request: a PHP process handles one request, then forgets it.
|
|
101
|
+
> An Oriel app runs in a long-lived Node or Bun process, so it registers and boots its providers once, and serves
|
|
102
|
+
> every request from the same application. A singleton lasts as long as the process; what belongs to one request lives
|
|
103
|
+
> in that request's [scope](container.md#request-scopes).
|
|
104
|
+
|
|
105
|
+
### Routing
|
|
106
|
+
|
|
107
|
+
`app.handle()` opens a request scope, and the kernel binds the request in it: everything resolved for this request,
|
|
108
|
+
such as the request, its session and its cookie jar, lives in the scope and is dropped with it. The global middleware
|
|
109
|
+
run first: `TrimStrings`, `ConvertEmptyStringsToNull`, and those you add with
|
|
110
|
+
[`withMiddleware()`](middleware.md#global-middleware).
|
|
111
|
+
|
|
112
|
+
The router then finds the route. A path no route matches is a 404, and a path whose routes don't take the request's
|
|
113
|
+
method is a 405 with an `Allow` header. The router records the route as the current one, then runs the route's
|
|
114
|
+
middleware: those of its groups, outer group first, then its own. The routes in `routes/web.ts` are in the `web` group,
|
|
115
|
+
which gives them their cookies, their session and CSRF protection:
|
|
116
|
+
|
|
117
|
+
| The `web` group | What it does |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| `EncryptCookies` | Decrypts the request's cookies and encrypts the response's |
|
|
120
|
+
| `AddQueuedCookiesToResponse` | Adds the cookies queued on the cookie jar to the response |
|
|
121
|
+
| `StartSession` | Loads the session, and saves it once the response is ready |
|
|
122
|
+
| `VerifyCsrfToken` | Refuses a form without the CSRF token |
|
|
123
|
+
|
|
124
|
+
The `web` and `api` groups' middleware, `auth` and `guest` run before the route's other middleware, then route model
|
|
125
|
+
binding replaces the parameters that name a model, then the rest run ([Sorting
|
|
126
|
+
middleware](middleware.md#sorting-middleware) has the rule). Last, the action runs: a closure is called with the
|
|
127
|
+
route's parameters, in URI order, and a controller is built by the container for this request. Whatever the action
|
|
128
|
+
returns becomes the response, as [Responses](responses.md) describes.
|
|
129
|
+
|
|
130
|
+
### Finishing up
|
|
131
|
+
|
|
132
|
+
The response goes back out through the route's middleware, then the global middleware, each of which can examine or
|
|
133
|
+
change it. An error thrown at any stage becomes a response at that stage, rendered by the exception handler (see
|
|
134
|
+
[Error handling](errors.md)), so a middleware further out always receives a response. The response to a `HEAD`
|
|
135
|
+
request loses its body here.
|
|
136
|
+
|
|
137
|
+
The server writes the response, and then the app terminates the request, still inside its scope:
|
|
138
|
+
|
|
139
|
+
1. `terminate()` runs on each [terminable middleware](middleware.md#terminable-middleware), the global ones first,
|
|
140
|
+
then the route's;
|
|
141
|
+
2. the `terminating()` callbacks run, in the order they were added. One of them is the framework's: it runs the jobs
|
|
142
|
+
you dispatched with [`dispatchAfterResponse()`](queues.md#running-a-job-now).
|
|
143
|
+
|
|
144
|
+
On Node, this starts once the response is written; on Bun, right after the response is handed to the server.
|
|
145
|
+
|
|
146
|
+
Register a terminating callback in a service provider. It receives no arguments:
|
|
147
|
+
|
|
148
|
+
```ts title="app/Providers/AppServiceProvider.ts"
|
|
149
|
+
// @filename: app/Services/Metrics.ts
|
|
150
|
+
export declare class Metrics {
|
|
151
|
+
flush(): Promise<void>
|
|
152
|
+
}
|
|
153
|
+
// ---cut---
|
|
154
|
+
import { ServiceProvider } from '@orieljs/framework'
|
|
155
|
+
import { Metrics } from '../Services/Metrics'
|
|
156
|
+
|
|
157
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
158
|
+
override register(): void {
|
|
159
|
+
this.app.singleton(Metrics)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
override boot(): void {
|
|
163
|
+
this.app.terminating(async () => {
|
|
164
|
+
await this.app.make(Metrics).flush()
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
> [!WARNING]
|
|
171
|
+
> The terminating callbacks also run after each `node oriel` command, and after the server sends a file from
|
|
172
|
+
> `public/`, which never reaches the app. They then run outside any request, where `request()` throws. A terminating
|
|
173
|
+
> callback that throws stops the ones after it, and its error is printed to the console rather than reported; after a
|
|
174
|
+
> command, `node oriel` then exits with code 1.
|
|
175
|
+
|
|
176
|
+
## Shutting down
|
|
177
|
+
|
|
178
|
+
A Node or Bun process keeps its database pools, its Redis clients and its log files open from one request to the next.
|
|
179
|
+
When the process ends, `app.shutdown()` closes them: it runs the callbacks registered with `app.onShutdown()`, once.
|
|
180
|
+
|
|
181
|
+
- The server calls it when it receives `SIGINT` or `SIGTERM`. It stops taking new connections, waits for the requests
|
|
182
|
+
in flight (closing the connections that are still open after 10 seconds), shuts the app down, and exits with 0.
|
|
183
|
+
- `node oriel` calls it after its command, also when the command throws.
|
|
184
|
+
- A script that makes the app itself must call it before it exits, or the last log entries are lost (see [Flushing
|
|
185
|
+
the log](logging.md#flushing-the-log)).
|
|
186
|
+
|
|
187
|
+
The callbacks run newest first. Yours were registered after the framework's, so they run before the framework closes
|
|
188
|
+
what they might use. Then the framework closes the queue's Redis connections, the database pools, the cache's Redis
|
|
189
|
+
clients, the log files (writing out what is waiting) and the session's Redis connection, each only if your app opened
|
|
190
|
+
it:
|
|
191
|
+
|
|
192
|
+
```ts title="app/Providers/AppServiceProvider.ts"
|
|
193
|
+
// @filename: app/Services/Metrics.ts
|
|
194
|
+
export declare class Metrics {
|
|
195
|
+
close(): Promise<void>
|
|
196
|
+
}
|
|
197
|
+
// ---cut---
|
|
198
|
+
import { ServiceProvider } from '@orieljs/framework'
|
|
199
|
+
import { Metrics } from '../Services/Metrics'
|
|
200
|
+
|
|
201
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
202
|
+
override register(): void {
|
|
203
|
+
this.app.singleton(Metrics)
|
|
204
|
+
|
|
205
|
+
this.app.onShutdown(async () => {
|
|
206
|
+
if (this.app.resolved(Metrics)) await this.app.make(Metrics).close()
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
A callback that throws is printed with `console.error`, and the others still run. If the callbacks haven't finished
|
|
213
|
+
after 10 seconds, the rest are given up on, with "The shutdown callbacks did not finish within 10 seconds; carrying on
|
|
214
|
+
with the exit.", so the process can still exit. `shutdown()` never rejects, and calling it again returns the same
|
|
215
|
+
promise.
|
|
216
|
+
|
|
217
|
+
| | `terminating()` | `onShutdown()` |
|
|
218
|
+
| --- | --- | --- |
|
|
219
|
+
| Runs | After each response, and after each `node oriel` command | Once, when the process ends |
|
|
220
|
+
| Inside | The request's scope, after a request | No request |
|
|
221
|
+
| Order | The order they were added | Newest first |
|
|
222
|
+
| For | Work that waits until the response is sent | Closing pools, clients and files |
|
|
223
|
+
|
|
224
|
+
> [!NOTE]
|
|
225
|
+
> Laravel has no `onShutdown()`: PHP frees what a request opened when the request ends. An Oriel process serves many
|
|
226
|
+
> requests, so the handles it opens stay open until the process ends, and `shutdown()` closes them before it exits.
|
|
227
|
+
|
|
228
|
+
## Focus on service providers
|
|
229
|
+
|
|
230
|
+
Service providers are the key to how an Oriel app starts: the application is created, the providers are registered and
|
|
231
|
+
booted, and the requests are handed to the booted app. Your own providers live in `app/Providers`. A new app's
|
|
232
|
+
`AppServiceProvider` is empty: it is the place for your app's bindings and its setup at boot. A larger app can split
|
|
233
|
+
them over several providers, each listed in `bootstrap/providers.ts`. [Service providers](providers.md) explains how
|
|
234
|
+
to write them.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Live tests
|
|
2
|
+
|
|
3
|
+
`Live.test()` mounts a Live component in a test, calls its actions and checks its state and HTML. It goes through the
|
|
4
|
+
same steps as the browser's updates, without a browser.
|
|
5
|
+
|
|
6
|
+
## Creating your first test
|
|
7
|
+
|
|
8
|
+
`make:live --test` writes a test with the component:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
node oriel make:live Counter --test
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The test, in `tests/Feature/Live`, mounts the component and checks its HTML:
|
|
15
|
+
|
|
16
|
+
```ts title="tests/Feature/Live/Counter.test.ts"
|
|
17
|
+
// @filename: app/Live/Counter.tsx
|
|
18
|
+
import { Component } from '@orieljs/framework'
|
|
19
|
+
export default class Counter extends Component {
|
|
20
|
+
count = 0
|
|
21
|
+
increment() {
|
|
22
|
+
this.count++
|
|
23
|
+
}
|
|
24
|
+
render() {
|
|
25
|
+
return <div>{this.count}</div>
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// ---cut---
|
|
29
|
+
import { Live } from '@orieljs/framework'
|
|
30
|
+
import { expect, test } from 'vitest'
|
|
31
|
+
import Counter from '../../../app/Live/Counter'
|
|
32
|
+
import { TestCase } from '../../TestCase'
|
|
33
|
+
|
|
34
|
+
TestCase.boot()
|
|
35
|
+
|
|
36
|
+
test('counter renders', async () => {
|
|
37
|
+
const component = await Live.test(Counter)
|
|
38
|
+
expect(component.html()).toContain('<div')
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`Live.test()` needs the test's application, so boot the test case in the file.
|
|
43
|
+
|
|
44
|
+
## Testing actions and properties
|
|
45
|
+
|
|
46
|
+
`set()` sets a property, as an `o:model` input does, and `call()` calls an action, as `o:click` does. Both send an
|
|
47
|
+
update, so `await` each one; the assertions that follow are synchronous and chain:
|
|
48
|
+
|
|
49
|
+
```ts title="tests/Feature/Live/Counter.test.ts"
|
|
50
|
+
// @filename: app/Live/Counter.tsx
|
|
51
|
+
import { Component } from '@orieljs/framework'
|
|
52
|
+
export default class Counter extends Component {
|
|
53
|
+
count = 0
|
|
54
|
+
increment() {
|
|
55
|
+
this.count++
|
|
56
|
+
}
|
|
57
|
+
render() {
|
|
58
|
+
return <div>{this.count}</div>
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// ---cut---
|
|
62
|
+
import { Live } from '@orieljs/framework'
|
|
63
|
+
import { test } from 'vitest'
|
|
64
|
+
import Counter from '../../../app/Live/Counter'
|
|
65
|
+
import { TestCase } from '../../TestCase'
|
|
66
|
+
|
|
67
|
+
TestCase.boot()
|
|
68
|
+
|
|
69
|
+
test('the counter goes up', async () => {
|
|
70
|
+
const component = await Live.test(Counter)
|
|
71
|
+
|
|
72
|
+
await component.set('count', 5)
|
|
73
|
+
await component.call('increment')
|
|
74
|
+
|
|
75
|
+
component.assertSet('count', 6).assertSee('6')
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`set()` also takes several properties at once, `set({ title: 'Hello', body: 'World' })`, and `call()` the action's
|
|
80
|
+
arguments after its name: `call('delete', 5)`. `toggle('open')` flips a boolean, `refresh()` renders the component
|
|
81
|
+
again, and `dispatch('post-created', { id: 1 })` sends it an event, which its `@On` methods receive.
|
|
82
|
+
|
|
83
|
+
The second argument of `Live.test()` is the mount parameters, as `<Live>` props would be:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// @filename: app/Live/Counter.tsx
|
|
87
|
+
import { Component } from '@orieljs/framework'
|
|
88
|
+
export default class Counter extends Component {
|
|
89
|
+
count = 0
|
|
90
|
+
render() {
|
|
91
|
+
return <div>{this.count}</div>
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ---cut---
|
|
95
|
+
import Counter from './app/Live/Counter'
|
|
96
|
+
|
|
97
|
+
const component = await Live.test(Counter, { count: 4 })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`get('count')` reads a property, `html()` the last HTML, `instance()` the component itself, and `effects()` what the
|
|
101
|
+
last update asks of the browser, such as a redirect.
|
|
102
|
+
|
|
103
|
+
### Authenticating
|
|
104
|
+
|
|
105
|
+
`Live.test()` runs in the test's requests, so a user you gave `actingAs()` is signed in for the component:
|
|
106
|
+
|
|
107
|
+
```ts title="tests/Feature/Live/Password.test.ts"
|
|
108
|
+
// @filename: app/Live/Settings/Password.tsx
|
|
109
|
+
import { Component } from '@orieljs/framework'
|
|
110
|
+
export default class Password extends Component {
|
|
111
|
+
current_password = ''
|
|
112
|
+
password = ''
|
|
113
|
+
password_confirmation = ''
|
|
114
|
+
updatePassword() {}
|
|
115
|
+
render() {
|
|
116
|
+
return <form />
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ---cut---
|
|
120
|
+
import { Hash, Live } from '@orieljs/framework'
|
|
121
|
+
import { RefreshDatabase } from '@orieljs/framework/testing'
|
|
122
|
+
import { expect, test } from 'vitest'
|
|
123
|
+
import Password from '../../../app/Live/Settings/Password'
|
|
124
|
+
import { User } from '../../../app/Models/User'
|
|
125
|
+
import { TestCase } from '../../TestCase'
|
|
126
|
+
|
|
127
|
+
const t = TestCase.boot({ uses: [RefreshDatabase] })
|
|
128
|
+
|
|
129
|
+
test('password can be updated', async () => {
|
|
130
|
+
const user = await User.factory().create()
|
|
131
|
+
|
|
132
|
+
t.actingAs(user)
|
|
133
|
+
const component = await Live.test(Password)
|
|
134
|
+
await component.set({ current_password: 'password', password: 'new-password', password_confirmation: 'new-password' })
|
|
135
|
+
await component.call('updatePassword')
|
|
136
|
+
|
|
137
|
+
component.assertHasNoErrors().assertSet('password', '').assertDispatched('password-updated')
|
|
138
|
+
|
|
139
|
+
expect(await Hash.check('new-password', (await user.fresh())!.password)).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
What the component changes in the session, such as a sign-in, carries on to the test's next request, and the
|
|
144
|
+
other way round.
|
|
145
|
+
|
|
146
|
+
## Testing validation
|
|
147
|
+
|
|
148
|
+
A failed validation leaves its errors on the component, which `assertHasErrors()` checks:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import type { Testable } from '@orieljs/framework/testing'
|
|
152
|
+
declare const component: Testable
|
|
153
|
+
// ---cut---
|
|
154
|
+
await component.set('title', '')
|
|
155
|
+
await component.call('save')
|
|
156
|
+
|
|
157
|
+
component.assertHasErrors(['title'])
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
With no keys, `assertHasErrors()` passes when there is any error, and `assertHasNoErrors()` when there is none.
|
|
161
|
+
`instance().errors.first('title')` reads a message.
|
|
162
|
+
|
|
163
|
+
## Testing authorization
|
|
164
|
+
|
|
165
|
+
An action that calls `abort(403)`, or whose policy check fails, throws in the test as it would end the request:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import type { Testable } from '@orieljs/framework/testing'
|
|
169
|
+
import { expect } from 'vitest'
|
|
170
|
+
declare const component: Testable
|
|
171
|
+
// ---cut---
|
|
172
|
+
await expect(component.call('delete', 5)).rejects.toMatchObject({ status: 403 })
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A page route's `can` middleware runs on the page's own request, so check it with an [HTTP test](http-tests.md) of the
|
|
176
|
+
page.
|
|
177
|
+
|
|
178
|
+
## Testing redirects and events
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import type { Testable } from '@orieljs/framework/testing'
|
|
182
|
+
declare const component: Testable
|
|
183
|
+
// ---cut---
|
|
184
|
+
await component.call('save')
|
|
185
|
+
|
|
186
|
+
component.assertRedirect('/posts').assertDispatched('post-created', { title: 'Hello' })
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`assertRedirect()` with no URL passes on any redirect. `effects().redirectUsingNavigate` is `true` when the action
|
|
190
|
+
asked for `{ navigate: true }`.
|
|
191
|
+
|
|
192
|
+
## Testing file uploads
|
|
193
|
+
|
|
194
|
+
In a `WithFileUploads` component, `set()` with a `FakeFile` uploads it as the browser would: through the upload rules,
|
|
195
|
+
to the temporary disk, and into the property as a `TemporaryUploadedFile`:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { FakeFile, type Testable } from '@orieljs/framework/testing'
|
|
199
|
+
declare const component: Testable
|
|
200
|
+
// ---cut---
|
|
201
|
+
await component.set('photo', FakeFile.image('photo.jpg'))
|
|
202
|
+
await component.call('save')
|
|
203
|
+
|
|
204
|
+
component.assertHasNoErrors()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
An array of files sets a `multiple` input's property. A file that fails `live.temporary_file_upload.rules` leaves the
|
|
208
|
+
upload's error on the property. Use `Storage.fake()` to check where `store()` puts the file.
|
|
209
|
+
|
|
210
|
+
## Available assertions
|
|
211
|
+
|
|
212
|
+
| Assertion | Passes when |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| `assertSet(name, value)`, `assertNotSet(name, value)` | The property has the value, or hasn't |
|
|
215
|
+
| `assertSee(...texts)`, `assertDontSee(...texts)` | The HTML has each text, escaped as the HTML writes it |
|
|
216
|
+
| `assertSeeHtml(...html)` | The HTML has each piece of HTML, as it is |
|
|
217
|
+
| `assertSeeText(...texts)`, `assertDontSeeText(...texts)` | The text, with the tags taken out, has each text |
|
|
218
|
+
| `assertHasErrors(keys?)`, `assertHasNoErrors(keys?)` | The component has errors for the keys, or has none |
|
|
219
|
+
| `assertDispatched(name, params?)`, `assertNotDispatched(name)` | The last update dispatched the event, with those parameters |
|
|
220
|
+
| `assertRedirect(url?)`, `assertNoRedirect()` | The last update redirects, to that URL |
|
|
221
|
+
| `assertReturned(value)` | The last action returned the value |
|
|
222
|
+
|
|
223
|
+
> [!NOTE]
|
|
224
|
+
> Livewire's `assertSeeLivewire()`, `assertViewHas()`, `assertCount()` and `withQueryParams()` aren't in Oriel. To
|
|
225
|
+
> check that a page has a component, check the page's HTML with an HTTP test; `get()` and Vitest's `expect()` check
|
|
226
|
+
> the rest.
|