@orieljs/framework 0.1.0-alpha.5 → 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.
Files changed (87) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-WNJP46B7.js → chunk-4MWG3B7G.js} +1785 -626
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/chunk-7ZQZYDOC.js +3253 -0
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-BSQKOLSK.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-A5XRLEWM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +535 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +2202 -5952
  16. package/dist/index.js +4900 -944
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-WKTl3EEw.d.ts → jsx-runtime-6um4Mwfb.d.ts} +97 -51
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BErengYY.d.ts → testing-CKwfWJGn.d.ts} +313 -994
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +19 -5
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-A5XRLEWM.js.map +0 -1
  83. package/dist/chunk-BSQKOLSK.js.map +0 -1
  84. package/dist/chunk-PM5YTG57.js +0 -880
  85. package/dist/chunk-PM5YTG57.js.map +0 -1
  86. package/dist/chunk-WNJP46B7.js.map +0 -1
  87. package/dist/index.d-DOlTeG-8.d.ts +0 -99
@@ -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`.
@@ -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
+ ```