@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.8
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-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-ZOEPKBVF.js → chunk-SPQUX3C7.js} +1347 -428
- package/dist/chunk-SPQUX3C7.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,264 @@
|
|
|
1
|
+
# Directory structure
|
|
2
|
+
|
|
3
|
+
A new Oriel app has Laravel's layout: your code in `app`, your settings in `config`, your routes in `routes`, and a
|
|
4
|
+
few folders that Oriel reads by itself.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
The layout is a starting point, not a rule: your files import each other by relative path, so a class can live
|
|
9
|
+
wherever suits your app. A few folders are the exception. Oriel looks in them by name to find your models, your
|
|
10
|
+
commands, your listeners and your policies, as [Folders Oriel discovers](#folders-oriel-discovers) lists.
|
|
11
|
+
|
|
12
|
+
Every starter kit is built from layers: `base`, which every app has, then the layers of its kit. The
|
|
13
|
+
[starter kits' layers](#the-starter-kits-layers) section says which folders each layer adds.
|
|
14
|
+
|
|
15
|
+
## The root directory
|
|
16
|
+
|
|
17
|
+
### The app directory
|
|
18
|
+
|
|
19
|
+
`app` holds the core code of your app: its controllers, models, providers and most of its other classes. The
|
|
20
|
+
[app directory](#the-app-directory-1) section looks inside it.
|
|
21
|
+
|
|
22
|
+
### The bootstrap directory
|
|
23
|
+
|
|
24
|
+
`bootstrap/app.ts` creates your app: it names your route files, configures middleware and error handling, and lists your
|
|
25
|
+
[service providers](providers.md) through `bootstrap/providers.ts`. Your server, your `oriel` command and your tests all
|
|
26
|
+
start from it.
|
|
27
|
+
|
|
28
|
+
The Inertia kits with server-side rendering build their SSR bundle into `bootstrap/ssr`, which git ignores. There is
|
|
29
|
+
no `bootstrap/cache`: Oriel has no route or config cache to store.
|
|
30
|
+
|
|
31
|
+
### The config directory
|
|
32
|
+
|
|
33
|
+
`config` holds your app's [configuration](configuration.md) files, one TypeScript module per part of the framework.
|
|
34
|
+
|
|
35
|
+
### The database directory
|
|
36
|
+
|
|
37
|
+
`database` holds your migrations (`database/migrations`), your model factories (`database/factories`) and your seeders
|
|
38
|
+
(`database/seeders`). A new app keeps its SQLite database here too, in `database/database.sqlite`, which git ignores.
|
|
39
|
+
|
|
40
|
+
### The public directory
|
|
41
|
+
|
|
42
|
+
`public` holds the files the server sends as they are, such as `favicon.svg`, and `public/index.ts`, the entry point
|
|
43
|
+
that runs your app in production (see [Deployment](deployment.md)). A request for a file that exists in `public` gets
|
|
44
|
+
the file before your routes run. TypeScript files and dotfiles there are never sent.
|
|
45
|
+
|
|
46
|
+
Vite builds your scripts and styles into `public/build`, and while `npm run dev` runs it writes `public/hot`, which
|
|
47
|
+
tells your pages to load their assets from Vite. `node oriel storage:link` adds `public/storage`, a link to
|
|
48
|
+
`storage/app/public`. Git ignores all three.
|
|
49
|
+
|
|
50
|
+
### The resources directory
|
|
51
|
+
|
|
52
|
+
`resources` holds your views and the uncompiled sources of your pages:
|
|
53
|
+
|
|
54
|
+
- `resources/views`: your [views](views.md), written in TSX, such as `welcome.tsx`. In the Inertia kits, `app.tsx`
|
|
55
|
+
there is the root view every page is rendered into.
|
|
56
|
+
- `resources/css/app.css`: the kits' stylesheet, with Tailwind and the Oriel theme.
|
|
57
|
+
- `resources/js`: your pages' scripts. In the Inertia kits, the pages are in `resources/js/pages`, with their
|
|
58
|
+
`components` and `layouts` beside them.
|
|
59
|
+
- `resources/js/oriel`: the route and page types that `route:types` and `inertia:types` write, which git ignores.
|
|
60
|
+
|
|
61
|
+
The API kit has no `resources` folder until `npm run typecheck` writes `resources/js/oriel/routes.ts`.
|
|
62
|
+
|
|
63
|
+
### The routes directory
|
|
64
|
+
|
|
65
|
+
`routes` holds your route files. A new app has two:
|
|
66
|
+
|
|
67
|
+
- `routes/web.ts`: the routes of your pages, in the `web` middleware group, which gives them the session, cookie
|
|
68
|
+
encryption and [CSRF protection](csrf.md).
|
|
69
|
+
- `routes/console.ts`: your closure commands, such as the `inspire` command a new app comes with, and your scheduled
|
|
70
|
+
tasks.
|
|
71
|
+
|
|
72
|
+
The kits with pages add `routes/auth.ts`, for registration, signing in and out, password resets and email
|
|
73
|
+
verification, and `routes/settings.ts`, for the settings pages. The API kit has `routes/api.ts` in place of
|
|
74
|
+
`routes/web.ts`: its routes are stateless, their URIs start with `/api`, and their users sign in with tokens.
|
|
75
|
+
`node oriel install:api` adds a `routes/api.ts` to another app. `bootstrap/app.ts` lists the files to load (see
|
|
76
|
+
[Routing](routing.md#the-default-route-files)).
|
|
77
|
+
|
|
78
|
+
### The storage directory
|
|
79
|
+
|
|
80
|
+
`storage` holds the files your app and the framework write:
|
|
81
|
+
|
|
82
|
+
- `storage/app/private`: the `local` disk, for files your app keeps to itself.
|
|
83
|
+
- `storage/app/public`: the `public` disk, for files anyone may download, such as avatars. `node oriel storage:link`
|
|
84
|
+
links `public/storage` to it.
|
|
85
|
+
- `storage/framework/cache/data`: the `file` cache store.
|
|
86
|
+
- `storage/framework/sessions`: the `file` session driver's sessions.
|
|
87
|
+
- `storage/framework/views`: kept from Laravel's layout. Oriel's views are TSX modules, so nothing compiles into it.
|
|
88
|
+
- `storage/logs`: your logs, `oriel.log`, and `mail.log` when mail is logged to its own channel.
|
|
89
|
+
|
|
90
|
+
Git keeps the folders and ignores what is written into them.
|
|
91
|
+
|
|
92
|
+
### The tests directory
|
|
93
|
+
|
|
94
|
+
`tests` holds your tests: `tests/Feature` for tests that send requests to your app or run its commands, and `tests/Unit`
|
|
95
|
+
for the rest, with `tests/TestCase.ts`, the class they boot the app with. A test file's name ends in `.test.ts`, or
|
|
96
|
+
`.test.tsx`. Run them with `node oriel test`, or with `npm test`, and on Bun with `npm run test:bun`.
|
|
97
|
+
|
|
98
|
+
### The types directory
|
|
99
|
+
|
|
100
|
+
`types` holds the type declarations Oriel writes for your app:
|
|
101
|
+
|
|
102
|
+
- `types/models.d.ts`: your models' columns, written by `node oriel model:types` from your database. `migrate`,
|
|
103
|
+
`migrate:fresh` and `migrate:refresh` write it again when they finish, outside production. It is in git, so your
|
|
104
|
+
app type-checks on a fresh clone.
|
|
105
|
+
- `types/routes.d.ts`: your route names and parameters, written by `node oriel route:types`. Git ignores it.
|
|
106
|
+
|
|
107
|
+
### The node_modules directory
|
|
108
|
+
|
|
109
|
+
`node_modules` holds your dependencies, as `vendor` does in a Laravel app. Your package manager writes it, and git
|
|
110
|
+
ignores it.
|
|
111
|
+
|
|
112
|
+
### The files at the root
|
|
113
|
+
|
|
114
|
+
| File | What it is |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `oriel` | Your app's command line: `node oriel <command>`, or `bun oriel <command>` |
|
|
117
|
+
| `.env`, `.env.example` | Your environment's values, and the list of the variables your app reads (see [Configuration](configuration.md#environment-configuration)) |
|
|
118
|
+
| `package.json` | Your dependencies and scripts, such as `dev`, `build`, `start`, `test`, `test:bun` and `typecheck` |
|
|
119
|
+
| `tsconfig.json` | TypeScript's settings for your server code; the kits' pages have their own in `resources/js` |
|
|
120
|
+
| `vite.config.ts` | Vite's settings, with the `oriel()` plugin (not in the API kit) |
|
|
121
|
+
| `vitest.config.ts` | Vitest's settings for your tests |
|
|
122
|
+
| `README.md` | How to run the app, written for your kit |
|
|
123
|
+
| `.gitignore` | What git leaves out |
|
|
124
|
+
| `.github/workflows/ci.yml` | The CI workflow the installer writes for your package manager |
|
|
125
|
+
|
|
126
|
+
## The app directory
|
|
127
|
+
|
|
128
|
+
Most of your app lives in `app`. A new app has `app/Http`, with its base `Controller`, `app/Models`, with the `User`
|
|
129
|
+
model, and `app/Providers`, with the `AppServiceProvider`. The other folders appear when a `make:` command writes a
|
|
130
|
+
class into them: `app/Console` doesn't exist until you run `make:command`. `node oriel list` shows every `make:`
|
|
131
|
+
command.
|
|
132
|
+
|
|
133
|
+
### The Console directory
|
|
134
|
+
|
|
135
|
+
`app/Console/Commands` holds your [console commands](console.md#writing-commands), written by `make:command`. Oriel
|
|
136
|
+
finds them by itself.
|
|
137
|
+
|
|
138
|
+
### The Events directory
|
|
139
|
+
|
|
140
|
+
`app/Events` holds your [event](events.md) classes, written by `make:event`.
|
|
141
|
+
|
|
142
|
+
### The Http directory
|
|
143
|
+
|
|
144
|
+
`app/Http` holds what handles a request: `Controllers` (`make:controller`), `Middleware` (`make:middleware`), form
|
|
145
|
+
`Requests` (`make:request`) and API `Resources` (`make:resource`).
|
|
146
|
+
|
|
147
|
+
### The Jobs directory
|
|
148
|
+
|
|
149
|
+
`app/Jobs` holds your [queued jobs](queues.md), written by `make:job`.
|
|
150
|
+
|
|
151
|
+
### The Listeners directory
|
|
152
|
+
|
|
153
|
+
`app/Listeners` holds the classes that handle your events, written by `make:listener`. Oriel finds each one by itself,
|
|
154
|
+
and registers it for the events its `static listensTo` names.
|
|
155
|
+
|
|
156
|
+
### The Live directory
|
|
157
|
+
|
|
158
|
+
`app/Live` holds the [Live](live.md) kit's components, and the ones `make:live` writes. Each component's name comes from
|
|
159
|
+
its path: `app/Live/Auth/Login.tsx` is `auth.login`.
|
|
160
|
+
|
|
161
|
+
### The Mail directory
|
|
162
|
+
|
|
163
|
+
`app/Mail` holds your [mail](mail.md) classes, written by `make:mail`, which also writes the mail's view in
|
|
164
|
+
`resources/views/mail`.
|
|
165
|
+
|
|
166
|
+
### The Models directory
|
|
167
|
+
|
|
168
|
+
`app/Models` holds your [models](eloquent.md), written by `make:model`. Oriel finds them by itself: a route parameter
|
|
169
|
+
named after a model receives it, and `model:types` writes their columns.
|
|
170
|
+
|
|
171
|
+
### The Notifications directory
|
|
172
|
+
|
|
173
|
+
`app/Notifications` holds your [notifications](notifications.md), written by `make:notification`.
|
|
174
|
+
|
|
175
|
+
### The Policies directory
|
|
176
|
+
|
|
177
|
+
`app/Policies` holds your [authorization policies](authorization.md#creating-policies), written by `make:policy`. A
|
|
178
|
+
policy named after a model, as `PostPolicy` for `Post`, applies to that model without being registered.
|
|
179
|
+
|
|
180
|
+
### The Providers directory
|
|
181
|
+
|
|
182
|
+
`app/Providers` holds your [service providers](providers.md). A new app has `AppServiceProvider`. `make:provider` writes
|
|
183
|
+
a new one there, which you then add to the list in `bootstrap/providers.ts`.
|
|
184
|
+
|
|
185
|
+
### The Rules directory
|
|
186
|
+
|
|
187
|
+
`app/Rules` holds your [validation rule objects](validation.md#custom-validation-rules), written by `make:rule`.
|
|
188
|
+
|
|
189
|
+
### The other directories
|
|
190
|
+
|
|
191
|
+
`make:cast` writes into `app/Casts` and `make:observer` into `app/Observers`. The kits with pages also have
|
|
192
|
+
`app/Support/GettingStarted.ts`, which builds the dashboard's checklist.
|
|
193
|
+
|
|
194
|
+
> [!NOTE]
|
|
195
|
+
> Laravel's `app/Exceptions` and `app/Broadcasting` folders, and their `make:exception` and `make:channel` commands,
|
|
196
|
+
> aren't in Oriel: errors are configured in `bootstrap/app.ts`, and Oriel has no broadcasting.
|
|
197
|
+
|
|
198
|
+
## Folders Oriel discovers
|
|
199
|
+
|
|
200
|
+
A TypeScript module runs only when something imports it, so Oriel imports these folders itself, and registers what
|
|
201
|
+
it finds there:
|
|
202
|
+
|
|
203
|
+
| Folder | What Oriel registers | When |
|
|
204
|
+
| --- | --- | --- |
|
|
205
|
+
| `app/Models` | Every exported model, for route model binding by name, `model:types`, and the queue and Live, which store a model as its class and key | When the app boots |
|
|
206
|
+
| `app/Policies` | Each `<Model>Policy` file's policy, for the model of that name (only the folder's top level) | When the app boots |
|
|
207
|
+
| `app/Listeners` | Every listener with a `static listensTo`, for the events it names | When the app boots |
|
|
208
|
+
| `app/Live` | Every component, under the name its path gives it | When the app boots |
|
|
209
|
+
| `app/Console/Commands` | Every command class | When an `oriel` command or a test runs |
|
|
210
|
+
| `database/factories` | Every factory, for its model's `factory()` (only the folder's top level) | When an `oriel` command or a test runs |
|
|
211
|
+
| `app/Jobs`, `app/Events`, `app/Listeners`, `app/Mail`, `app/Notifications` | The classes a queued job carries, so the worker can rebuild them | When `queue:work` starts |
|
|
212
|
+
|
|
213
|
+
Each folder is read with the folders inside it, except `app/Policies` and `database/factories`. A class in another
|
|
214
|
+
folder works, but isn't discovered: register it yourself, as `Event.listen()` does for a listener or `Gate.policy()`
|
|
215
|
+
for a policy.
|
|
216
|
+
|
|
217
|
+
Other folders are read when they are needed: `config` when the app is created, `resources/views` for each view,
|
|
218
|
+
`database/migrations` by `migrate`, and `database/seeders` by `db:seed`.
|
|
219
|
+
|
|
220
|
+
## The starter kits' layers
|
|
221
|
+
|
|
222
|
+
Each kit is the `base` layer and its own layers, applied in order, a later layer's file replacing an earlier one's:
|
|
223
|
+
|
|
224
|
+
| Kit | Layers |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| React, Vue, Svelte, Solid, Angular | `base`, `web`, `inertia`, then `react`, `vue`, `svelte`, `solid` or `angular` |
|
|
227
|
+
| Live | `base`, `web`, `live` |
|
|
228
|
+
| API | `base`, `api` |
|
|
229
|
+
|
|
230
|
+
What each layer adds:
|
|
231
|
+
|
|
232
|
+
| Layer | Adds |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `base` | The skeleton of every app: `app/Http/Controllers/Controller.ts`, `app/Models/User.ts`, `app/Providers`, `bootstrap`, `config`, `database`, `public`, `resources/views/welcome.tsx`, `routes/web.ts` and `routes/console.ts`, `storage`, `tests`, `types`, and the root files |
|
|
235
|
+
| `web` | What the kits with pages share: the email verification controller, the `HandleAppearance` middleware, `app/Support/GettingStarted.ts`, `resources/css/app.css`, the browser scripts in `resources/js` (`appearance.ts`, `checklist.ts`, `menus.ts`, `names.ts`) and `resources/views/partials/appearance.tsx` |
|
|
236
|
+
| `inertia` | The Inertia kits' server side: the auth, dashboard and settings controllers, `HandleInertiaRequests`, the form requests, `config/inertia.ts`, `bootstrap/app.ts`, and `routes/web.ts`, `routes/auth.ts` and `routes/settings.ts` |
|
|
237
|
+
| `react`, `vue`, `svelte`, `solid`, `angular` | The pages, layouts and components in `resources/js`, the browser entry and, except in Angular, the SSR entry, the root view `resources/views/app.tsx`, `vite.config.ts` and the kit's `README.md` |
|
|
238
|
+
| `live` | The components in `app/Live`, their views and view components in `resources/views`, `resources/js/app.ts`, `bootstrap/app.ts`, the three route files, `vite.config.ts` and the kit's `README.md` |
|
|
239
|
+
| `api` | The token auth controllers, the login request, `bootstrap/app.ts`, `routes/api.ts` and the kit's `README.md`; it removes `routes/web.ts`, `resources/views` and the welcome page's test |
|
|
240
|
+
|
|
241
|
+
Each layer but `inertia` also adds to `package.json` (`web` its dev dependencies only), and all but the five framework
|
|
242
|
+
layers add their tests.
|
|
243
|
+
|
|
244
|
+
[Starter kits](starter-kits.md) describes what the kits do with them.
|
|
245
|
+
|
|
246
|
+
## Generated and ignored files
|
|
247
|
+
|
|
248
|
+
Some files are written by a command rather than by you. Don't edit them: run the command again.
|
|
249
|
+
|
|
250
|
+
| File | Written by | In git |
|
|
251
|
+
| --- | --- | --- |
|
|
252
|
+
| `types/models.d.ts` | `model:types`, and `migrate` outside production | Yes |
|
|
253
|
+
| `types/routes.d.ts` | `route:types` | No |
|
|
254
|
+
| `resources/js/oriel/routes.ts` | `route:types`, and the Inertia kits' Vite plugin | No |
|
|
255
|
+
| `resources/js/oriel/pages.d.ts` | `inertia:types`, and the Inertia kits' Vite plugin | No |
|
|
256
|
+
| `public/build` | `npm run build` | No |
|
|
257
|
+
| `public/hot` | `npm run dev`, while it runs | No |
|
|
258
|
+
| `bootstrap/ssr` | `npm run build` in the kits with server-side rendering | No |
|
|
259
|
+
| `public/storage` | `storage:link` | No |
|
|
260
|
+
| `database/database.sqlite` | The first connection to it, such as `migrate` | No |
|
|
261
|
+
| `.env` | The installer, from `.env.example` | No |
|
|
262
|
+
|
|
263
|
+
Git also ignores `node_modules`, the files under `storage`, `.env.backup`, `.DS_Store` and Yarn's
|
|
264
|
+
`.yarn/install-state.gz`.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Testing: getting started
|
|
2
|
+
|
|
3
|
+
Oriel apps are tested with Vitest. A new app has tests ready to run, a `TestCase` that gives each test a fresh
|
|
4
|
+
application, and helpers to send requests, run commands and check the database.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
`tests` has two folders: `tests/Feature` for tests that send requests to your app or run its commands, and
|
|
9
|
+
`tests/Unit` for tests of one piece of code on its own. A test file's name ends in `.test.ts`, or `.test.tsx` for one
|
|
10
|
+
that renders TSX.
|
|
11
|
+
|
|
12
|
+
`tests/TestCase.ts` is the class your tests boot the app with, and `vitest.config.ts` sets Vitest up for your app. A
|
|
13
|
+
test file boots the test case once, at the top, and uses it in each test:
|
|
14
|
+
|
|
15
|
+
```ts title="tests/Feature/Example.test.ts"
|
|
16
|
+
import { test } from 'vitest'
|
|
17
|
+
import { TestCase } from '../TestCase'
|
|
18
|
+
|
|
19
|
+
const t = TestCase.boot()
|
|
20
|
+
|
|
21
|
+
test('the application returns a successful response', async () => {
|
|
22
|
+
const response = await t.get('/')
|
|
23
|
+
|
|
24
|
+
response.assertOk()
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`TestCase.boot()` registers Vitest's `beforeEach` and `afterEach` hooks: before each test, it creates and boots a new
|
|
29
|
+
application; after it, it tears that application down. No state survives from one test to the next.
|
|
30
|
+
|
|
31
|
+
## Environment
|
|
32
|
+
|
|
33
|
+
Before each application is created, the test case sets these variables, unless they are already set:
|
|
34
|
+
|
|
35
|
+
| Variable | Value |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `APP_ENV` | `testing` |
|
|
38
|
+
| `BCRYPT_ROUNDS` | `4` |
|
|
39
|
+
| `CACHE_STORE` | `array` |
|
|
40
|
+
| `DB_CONNECTION` | `sqlite` |
|
|
41
|
+
| `DB_DATABASE` | `:memory:` |
|
|
42
|
+
| `MAIL_MAILER` | `array` |
|
|
43
|
+
| `QUEUE_CONNECTION` | `sync` |
|
|
44
|
+
| `SESSION_DRIVER` | `array` |
|
|
45
|
+
|
|
46
|
+
So your tests send no mail, run jobs at once, and each test gets its own empty in-memory database. A new app's
|
|
47
|
+
`vitest.config.ts` sets the same values as Vitest's `env`, because Bun reads `.env` before the tests start. Vitest's
|
|
48
|
+
`env` replaces what your shell or your CI sets, so to run your tests with another value, such as a MySQL database,
|
|
49
|
+
change it in `vitest.config.ts`.
|
|
50
|
+
|
|
51
|
+
With `APP_ENV` set to `testing`, a `.env.testing` file, when you add one, is read in place of `.env`. For the other
|
|
52
|
+
variables, what you set in your shell or your CI wins over both files.
|
|
53
|
+
|
|
54
|
+
To change the app before it boots, override `defineEnvironment()` in your `TestCase`:
|
|
55
|
+
|
|
56
|
+
```ts title="tests/TestCase.ts"
|
|
57
|
+
import type { Application } from '@orieljs/framework'
|
|
58
|
+
import { ConfigRepository } from '@orieljs/framework/contracts'
|
|
59
|
+
import { TestCase as BaseTestCase } from '@orieljs/framework/testing'
|
|
60
|
+
import { afterEach, beforeEach } from 'vitest'
|
|
61
|
+
import { createApplication } from '../bootstrap/app'
|
|
62
|
+
|
|
63
|
+
export class TestCase extends BaseTestCase {
|
|
64
|
+
static override hooks = { beforeEach, afterEach }
|
|
65
|
+
|
|
66
|
+
override createApplication(): Promise<Application> {
|
|
67
|
+
return createApplication()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected override defineEnvironment(app: Application): void {
|
|
71
|
+
app.make(ConfigRepository).set('mail.default', 'array')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Creating tests
|
|
77
|
+
|
|
78
|
+
`make:test` writes a test in `tests/Feature`, and with `--unit` in `tests/Unit`:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
node oriel make:test UserTest
|
|
82
|
+
|
|
83
|
+
node oriel make:test UserTest --unit
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The file is named after the test without its `Test`: `tests/Feature/User.test.ts`. Both write the same Vitest test, with
|
|
87
|
+
`test()`, `describe()` and `expect()`; for a feature test, import your `TestCase` and add `const t = TestCase.boot()`. A
|
|
88
|
+
unit test that doesn't need the app leaves it out:
|
|
89
|
+
|
|
90
|
+
```ts title="tests/Unit/Example.test.ts"
|
|
91
|
+
import { expect, test } from 'vitest'
|
|
92
|
+
|
|
93
|
+
test('true is true', () => {
|
|
94
|
+
expect(true).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`t.app` is the current test's application, inside a test: `t.app.make(Service)` resolves a service from it.
|
|
99
|
+
|
|
100
|
+
> [!NOTE]
|
|
101
|
+
> Laravel's PHPUnit tests are classes, with a method for each test. Oriel's are Vitest files, as Pest's are: each
|
|
102
|
+
> `test()` is one test, and the test case `TestCase.boot()` returns gives every test the helpers Laravel's `$this`
|
|
103
|
+
> has.
|
|
104
|
+
|
|
105
|
+
## Running tests
|
|
106
|
+
|
|
107
|
+
`oriel test` runs your tests with Vitest:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
node oriel test
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`npm test` does the same, and `npm run test:bun` runs them on Bun. Name files or folders to run only those, and
|
|
114
|
+
`--filter` runs the tests whose names match:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
node oriel test tests/Feature/User.test.ts
|
|
118
|
+
|
|
119
|
+
node oriel test --filter="the application returns"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Anything after `--` goes to Vitest as it is, such as `node oriel test -- --coverage` for a coverage report (with
|
|
123
|
+
`@vitest/coverage-v8` installed).
|
|
124
|
+
|
|
125
|
+
### Running tests in parallel
|
|
126
|
+
|
|
127
|
+
Vitest runs your test files at the same time, in several workers. With the default in-memory SQLite database, each
|
|
128
|
+
test has its own database, so tests can't see each other's rows.
|
|
129
|
+
|
|
130
|
+
> [!WARNING]
|
|
131
|
+
> On a database in a file, or a MySQL or PostgreSQL server (set in `vitest.config.ts`), every worker uses the same
|
|
132
|
+
> database. Run those tests one
|
|
133
|
+
> file at a time, with `node oriel test -- --no-file-parallelism`, or give each worker its own database.
|
|
134
|
+
|
|
135
|
+
## Test traits
|
|
136
|
+
|
|
137
|
+
A trait adds setup to the tests of a file. `RefreshDatabase` gives each test a migrated database:
|
|
138
|
+
|
|
139
|
+
```ts title="tests/Feature/Posts.test.ts"
|
|
140
|
+
import { RefreshDatabase } from '@orieljs/framework/testing'
|
|
141
|
+
import { test } from 'vitest'
|
|
142
|
+
import { TestCase } from '../TestCase'
|
|
143
|
+
|
|
144
|
+
const t = TestCase.boot({ uses: [RefreshDatabase] })
|
|
145
|
+
|
|
146
|
+
test('posts can be listed', async () => {
|
|
147
|
+
;(await t.get('/posts')).assertOk()
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
[Database testing](database-testing.md) covers it and the other database traits.
|
|
152
|
+
|
|
153
|
+
## What the test case gives you
|
|
154
|
+
|
|
155
|
+
| Helper | Page |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| `t.get()`, `t.post()`, `t.actingAs()` and the response's assertions | [HTTP tests](http-tests.md) |
|
|
158
|
+
| `t.artisan()` | [Testing commands](console.md#testing-commands) |
|
|
159
|
+
| `Live.test()` | [Live tests](live-tests.md) |
|
|
160
|
+
| `Mail.fake()`, `Queue.fake()` and the other fakes | [Fakes](mocking.md) |
|
|
161
|
+
| `t.assertDatabaseHas()` and the database traits | [Database testing](database-testing.md) |
|
|
162
|
+
| `t.travelTo()`, `t.freezeTime()` | [Time travel](time-travel.md) |
|
|
163
|
+
|
|
164
|
+
While your tests run, the app runs as a console app in the `testing` environment: CSRF tokens aren't checked, so your
|
|
165
|
+
tests can post forms without one.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Time travel
|
|
2
|
+
|
|
3
|
+
Code that depends on the time, such as an expiring link, a cache entry or a trial that ends after 30 days, is tested
|
|
4
|
+
by moving the clock. The test case's time travel moves the clock your code reads, and puts it back after each test.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
`t.travel(5).days()` moves the clock five days on, and `t.travelTo(date)` sets it to a date:
|
|
9
|
+
|
|
10
|
+
```ts title="tests/Feature/Trial.test.ts"
|
|
11
|
+
import { expect, test } from 'vitest'
|
|
12
|
+
import { TestCase } from '../TestCase'
|
|
13
|
+
|
|
14
|
+
const t = TestCase.boot()
|
|
15
|
+
|
|
16
|
+
test('time can be manipulated', () => {
|
|
17
|
+
t.travelTo(new Date('2026-01-01T00:00:00Z'))
|
|
18
|
+
|
|
19
|
+
t.travel(5).days()
|
|
20
|
+
|
|
21
|
+
expect(new Date().toISOString()).toBe('2026-01-06T00:00:00.000Z')
|
|
22
|
+
|
|
23
|
+
t.travelBack()
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
While you travel, the clock stands still: each `new Date()` gives the same time until you travel again. `travelBack()`
|
|
28
|
+
returns to the real time, and the test case does it after each test.
|
|
29
|
+
|
|
30
|
+
## Travelling
|
|
31
|
+
|
|
32
|
+
`travel(n)` moves the clock by `n` of a unit, from where it stands, and a negative `n` moves it back:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { TestCase } from './tests/TestCase'
|
|
36
|
+
const t = TestCase.boot()
|
|
37
|
+
// ---cut---
|
|
38
|
+
t.travel(5).milliseconds()
|
|
39
|
+
t.travel(5).seconds()
|
|
40
|
+
t.travel(5).minutes()
|
|
41
|
+
t.travel(5).hours()
|
|
42
|
+
t.travel(5).days()
|
|
43
|
+
t.travel(5).weeks()
|
|
44
|
+
t.travel(5).months()
|
|
45
|
+
t.travel(5).years()
|
|
46
|
+
|
|
47
|
+
t.travel(-5).hours()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Each unit has a singular form too, as `t.travel(1).day()`. Months and years overflow as Laravel's do: 31 January plus a
|
|
51
|
+
month is 3 March. The units count in UTC.
|
|
52
|
+
|
|
53
|
+
`travelTo()` takes a `Date`, an ISO string, or milliseconds since 1970:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { TestCase } from './tests/TestCase'
|
|
57
|
+
const t = TestCase.boot()
|
|
58
|
+
// ---cut---
|
|
59
|
+
t.travelTo('2026-03-04T10:00:00Z')
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Travelling for one function
|
|
63
|
+
|
|
64
|
+
Give a function, and the clock travels while it runs. After it, or after its promise settles, the clock returns to
|
|
65
|
+
the real time, as `travelBack()` does, even when you had travelled before. The function's result is returned:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { TestCase } from './tests/TestCase'
|
|
69
|
+
import { User } from './app/Models/User'
|
|
70
|
+
const t = TestCase.boot()
|
|
71
|
+
// ---cut---
|
|
72
|
+
const user = await t.travelTo('2026-01-01T00:00:00Z', () => User.factory().create())
|
|
73
|
+
|
|
74
|
+
await t.travel(5).days(async () => {
|
|
75
|
+
// The clock is five days on here...
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Freezing time
|
|
80
|
+
|
|
81
|
+
`freezeTime()` holds the clock where it is now, and returns that time. `freezeSecond()` does the same at the start of
|
|
82
|
+
the current second, which helps when your database keeps seconds only:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { TestCase } from './tests/TestCase'
|
|
86
|
+
const t = TestCase.boot()
|
|
87
|
+
// ---cut---
|
|
88
|
+
const frozen = t.freezeTime()
|
|
89
|
+
|
|
90
|
+
const second = t.freezeSecond()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Both also take a function, and freeze the clock only while it runs, then return to the real time.
|
|
94
|
+
|
|
95
|
+
## What the clock moves
|
|
96
|
+
|
|
97
|
+
Time travel changes JavaScript's `Date`: `new Date()`, `Date()` and `Date.now()` read the travelled time, and so does
|
|
98
|
+
everything in your app that reads them:
|
|
99
|
+
|
|
100
|
+
- `now()`, model timestamps and the dates your casts write
|
|
101
|
+
- Expiry in the `array`, `file` and `database` cache stores, cache locks and rate limiters
|
|
102
|
+
- Signed URLs, password reset tokens, API tokens, and the expiry dates of the cookies your app sets
|
|
103
|
+
- Job delays and retry times
|
|
104
|
+
|
|
105
|
+
It doesn't change what runs outside JavaScript's `Date`:
|
|
106
|
+
|
|
107
|
+
- Timers: `setTimeout()` and `setInterval()` still wait in real time
|
|
108
|
+
- `performance.now()`
|
|
109
|
+
- Redis, whose keys expire on the server's clock, so Redis cache entries and locks don't expire when you travel
|
|
110
|
+
- Your database's own clock: `CURRENT_TIMESTAMP` and `now()` in SQL
|
|
111
|
+
|
|
112
|
+
A test's session doesn't end either: the test client carries it from request to request, so travelling past
|
|
113
|
+
`session.lifetime` doesn't sign the user out.
|
|
114
|
+
|
|
115
|
+
> [!NOTE]
|
|
116
|
+
> Vitest's `vi.useFakeTimers()` fakes `Date` and the timers too. Use one or the other in a test: both replace `Date`,
|
|
117
|
+
> and the last one wins.
|
|
118
|
+
|
|
119
|
+
## Outside a test case
|
|
120
|
+
|
|
121
|
+
`travelTo()`, `travel()`, `travelBack()`, `freezeTime()` and `freezeSecond()` are also functions, from
|
|
122
|
+
`@orieljs/framework/testing`, for a test that doesn't boot the test case. They work the same, but nothing travels back
|
|
123
|
+
for you: call `travelBack()` in an `afterEach()`, or pass a function.
|
|
124
|
+
|
|
125
|
+
```ts title="tests/Unit/Trial.test.ts"
|
|
126
|
+
import { travelBack, travelTo } from '@orieljs/framework/testing'
|
|
127
|
+
import { afterEach, expect, test } from 'vitest'
|
|
128
|
+
|
|
129
|
+
afterEach(() => {
|
|
130
|
+
travelBack()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('the clock can be set', () => {
|
|
134
|
+
travelTo('2026-01-01T00:00:00Z')
|
|
135
|
+
|
|
136
|
+
expect(Date.now()).toBe(Date.parse('2026-01-01T00:00:00Z'))
|
|
137
|
+
})
|
|
138
|
+
```
|