@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.
Files changed (86) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
  5. package/dist/chunk-4MWG3B7G.js.map +1 -0
  6. package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
  7. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  8. package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
  9. package/dist/chunk-GVNLZZTN.js.map +1 -0
  10. package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +515 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +964 -152
  16. package/dist/index.js +4279 -866
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +3 -3
  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-I63ZFMX6.js.map +0 -1
  83. package/dist/chunk-MFTZQMZM.js.map +0 -1
  84. package/dist/chunk-POEP4B2M.js.map +0 -1
  85. package/dist/chunk-ZOEPKBVF.js.map +0 -1
  86. package/dist/index.d-BJyJx6U1.d.ts +0 -319
@@ -0,0 +1,386 @@
1
+ # HTTP tests
2
+
3
+ Your tests send requests to your app and check the responses. The requests run inside the test, through your routes
4
+ and middleware, with no server and no network.
5
+
6
+ ## Introduction
7
+
8
+ `t.get()` sends a request and returns a `TestResponse`, whose assertions check the status, the headers, the content,
9
+ the session and more:
10
+
11
+ ```ts title="tests/Feature/Example.test.ts"
12
+ import { test } from 'vitest'
13
+ import { TestCase } from '../TestCase'
14
+
15
+ const t = TestCase.boot()
16
+
17
+ test('the application returns a successful response', async () => {
18
+ const response = await t.get('/')
19
+
20
+ response.assertOk()
21
+ })
22
+ ```
23
+
24
+ Await the request; the assertions that follow are synchronous, and each returns the response, so you can chain them.
25
+
26
+ ## Making requests
27
+
28
+ `t.get()`, `t.post()`, `t.put()`, `t.patch()`, `t.delete()`, `t.options()` and `t.head()` send a request with that
29
+ method. The ones with a body take the data as their second argument, sent as a form:
30
+
31
+ ```ts
32
+ import { TestCase } from './tests/TestCase'
33
+ const t = TestCase.boot()
34
+ // ---cut---
35
+ const response = await t.post('/posts', { title: 'Hello', body: 'My first post' })
36
+
37
+ response.assertRedirect('/posts')
38
+ ```
39
+
40
+ `call(method, uri, data)` sends any method. Within one test, the requests share their cookies and session, as one
41
+ browser's would: a user who signs in on one request is signed in on the next.
42
+
43
+ ### Customizing request headers
44
+
45
+ `withHeaders()` adds headers to every request that follows in the test, and `withHeader()` one:
46
+
47
+ ```ts
48
+ import { TestCase } from './tests/TestCase'
49
+ const t = TestCase.boot()
50
+ // ---cut---
51
+ const response = await t.withHeaders({ 'X-Header': 'Value' }).post('/user', { name: 'Sally' })
52
+
53
+ response.assertCreated()
54
+ ```
55
+
56
+ `withToken('secret')` sends `Authorization: Bearer secret`, `withBasicAuth(username, password)` a basic one, and
57
+ `withoutToken()` takes it out again.
58
+
59
+ ### Cookies
60
+
61
+ `withCookie()` and `withCookies()` send cookies, encrypted as your app's own cookies are; `withUnencryptedCookie()`
62
+ sends one as it is:
63
+
64
+ ```ts
65
+ import { TestCase } from './tests/TestCase'
66
+ const t = TestCase.boot()
67
+ // ---cut---
68
+ const response = await t.withCookie('color', 'blue').get('/')
69
+ ```
70
+
71
+ ### Session and authentication
72
+
73
+ `withSession()` puts data in the session before a request, and `flushSession()` empties it. `actingAs()` signs a user
74
+ in for every request that follows:
75
+
76
+ ```ts title="tests/Feature/Dashboard.test.ts"
77
+ import { RefreshDatabase } from '@orieljs/framework/testing'
78
+ import { test } from 'vitest'
79
+ import { User } from '../../app/Models/User'
80
+ import { TestCase } from '../TestCase'
81
+
82
+ const t = TestCase.boot({ uses: [RefreshDatabase] })
83
+
84
+ test('signed-in users can see the dashboard', async () => {
85
+ const user = await User.factory().create()
86
+
87
+ const response = await t.actingAs(user).withSession({ banned: false }).get('/dashboard')
88
+
89
+ response.assertOk()
90
+ })
91
+ ```
92
+
93
+ `actingAs(user, 'token')` signs the user in on another guard, here the `token` guard, which becomes the default for the
94
+ requests.
95
+
96
+ ### Redirects and the previous page
97
+
98
+ After `followingRedirects()`, each request of the test follows its redirects, with `GET`, and returns the last response.
99
+ `from(url)` sets the page the request comes from, so a `back()` redirect goes there:
100
+
101
+ ```ts
102
+ import { TestCase } from './tests/TestCase'
103
+ const t = TestCase.boot()
104
+ // ---cut---
105
+ const response = await t.from('/posts/create').post('/posts', {})
106
+
107
+ response.assertRedirect('/posts/create')
108
+ ```
109
+
110
+ ### Debugging responses
111
+
112
+ `dump()` prints the response's content, or with a path, a value of its JSON. `response.content` is the body,
113
+ `response.json()` the decoded JSON, and `response.status` and `response.headers` what they say.
114
+
115
+ ### Exception handling
116
+
117
+ An error your code throws is rendered by your app's exception handler, as it would be for a visitor, so a test can
118
+ check the 500 or 404 it gets. `response.exception` is the error. `withoutExceptionHandling()` lets errors escape from
119
+ the request instead, so the test fails with the error and its stack:
120
+
121
+ ```ts
122
+ import { TestCase } from './tests/TestCase'
123
+ const t = TestCase.boot()
124
+ // ---cut---
125
+ const response = await t.withoutExceptionHandling().get('/')
126
+ ```
127
+
128
+ ## Testing JSON APIs
129
+
130
+ `getJson()`, `postJson()`, `putJson()`, `patchJson()`, `deleteJson()` and `json(method, uri, data)` ask for JSON back,
131
+ and the ones with a body send it as JSON:
132
+
133
+ ```ts title="tests/Feature/Api.test.ts"
134
+ import { test } from 'vitest'
135
+ import { TestCase } from '../TestCase'
136
+
137
+ const t = TestCase.boot()
138
+
139
+ test('making an API request', async () => {
140
+ const response = await t.postJson('/api/user', { name: 'Sally' })
141
+
142
+ response.assertCreated().assertJson({ created: true })
143
+ })
144
+ ```
145
+
146
+ `assertJson()` passes when the response's JSON contains the data, whatever else it has. `assertExactJson()` wants
147
+ the JSON to be exactly the data, and `assertJsonPath()` checks one value:
148
+
149
+ ```ts
150
+ import type { TestResponse } from '@orieljs/framework/testing'
151
+ declare const response: TestResponse
152
+ // ---cut---
153
+ response.assertJsonPath('team.owner.name', 'Darian')
154
+ ```
155
+
156
+ `response.json('team.owner.name')` reads a value, to check it with Vitest's `expect()`.
157
+
158
+ ### Fluent JSON testing
159
+
160
+ `assertJson()` also takes a function, which checks the JSON one key at a time:
161
+
162
+ ```ts
163
+ import type { TestResponse } from '@orieljs/framework/testing'
164
+ declare const response: TestResponse
165
+ // ---cut---
166
+ response.assertJson((json) =>
167
+ json
168
+ .where('id', 1)
169
+ .where('name', 'Victoria Faith')
170
+ .where('email', (email: string) => email.endsWith('@example.com'))
171
+ .whereNot('status', 'pending')
172
+ .missing('password')
173
+ .etc(),
174
+ )
175
+ ```
176
+
177
+ Each key of the JSON must be checked, or the assertion fails with "Unexpected properties were found on the root
178
+ level.". `etc()` allows the keys you don't check.
179
+
180
+ | Method | Checks |
181
+ | --- | --- |
182
+ | `has(key)`, `has(key, count)` | The key is there, with that many items |
183
+ | `hasAll(keys)`, `hasAny(keys)` | Every key is there, or one of them is |
184
+ | `missing(key)`, `missingAll(keys)` | The key isn't there |
185
+ | `count(key, n)`, `countBetween(key, min, max)` | The number of items |
186
+ | `where(key, value)`, `whereNot(key, value)` | The value is that value, or isn't; with a function, it returns `true`, or `false` |
187
+ | `whereNull(key)`, `whereNotNull(key)` | The value is `null`, or isn't |
188
+ | `whereAll(values)` | Several values at once |
189
+ | `whereType(key, type)`, `whereAllType(types)` | The type: `'string'`, `'integer'`, `'number'`, `'boolean'`, `'array'` (a list or an object, as in Laravel), `'object'` or `'null'`, or a list of them |
190
+ | `whereContains(key, value)` | A list or string contains the value |
191
+
192
+ A key can be a dotted path: `where('user.name', 'Sally')`.
193
+
194
+ #### Asserting against JSON collections
195
+
196
+ `has(3)` checks the root's size, and `first()` scopes the checks to its first item:
197
+
198
+ ```ts
199
+ import type { TestResponse } from '@orieljs/framework/testing'
200
+ declare const response: TestResponse
201
+ // ---cut---
202
+ response.assertJson((json) =>
203
+ json.has(3).first((user) => user.where('id', 1).where('name', 'Victoria Faith').etc()),
204
+ )
205
+ ```
206
+
207
+ `has(3)` checks the root is a list of 3. `has('users', 3, (user) => ...)` checks the `users` list and its first item,
208
+ `first()` the first item, and `each()` every item.
209
+
210
+ #### Scoping JSON assertions
211
+
212
+ `has()` with a function scopes the checks to one key:
213
+
214
+ ```ts
215
+ import type { TestResponse } from '@orieljs/framework/testing'
216
+ declare const response: TestResponse
217
+ // ---cut---
218
+ response.assertJson((json) =>
219
+ json.has('meta').has('users', 3, (user) => user.where('id', 1).missing('password').etc()),
220
+ )
221
+ ```
222
+
223
+ A scope must check each of its keys too, unless it calls `etc()`.
224
+
225
+ ## Testing Inertia pages
226
+
227
+ `assertInertia()` checks that a response is an Inertia page, and checks its component and props with the same fluent
228
+ methods:
229
+
230
+ ```ts title="tests/Feature/Posts.test.ts"
231
+ import { RefreshDatabase } from '@orieljs/framework/testing'
232
+ import { test } from 'vitest'
233
+ import { User } from '../../app/Models/User'
234
+ import { TestCase } from '../TestCase'
235
+
236
+ const t = TestCase.boot({ uses: [RefreshDatabase] })
237
+
238
+ test('the posts page lists the posts', async () => {
239
+ const user = await User.factory().create()
240
+
241
+ const response = await t.actingAs(user).get('/posts')
242
+
243
+ response.assertOk().assertInertia((page) => page.component('Posts/Index').has('posts', 0).where('auth.user.id', user.id))
244
+ })
245
+ ```
246
+
247
+ It works on a first visit, whose page is in the HTML, and on an Inertia visit, whose page is JSON. `component()` also
248
+ checks that the page's file exists in `resources/js/pages`; `inertia.testing.ensure_pages_exist` set to `false` turns
249
+ that off.
250
+
251
+ | Method | Checks |
252
+ | --- | --- |
253
+ | `component(name)` | The page component |
254
+ | `url(url)`, `version(version)` | The page's URL and asset version |
255
+ | `hasFlash(key, value?)`, `missingFlash(key)` | A flashed value, in `page.flash` |
256
+ | `reload(callback)`, `reloadOnly(props, callback)`, `reloadExcept(props, callback)` | Requests the page again, as a partial reload, and checks it |
257
+ | `loadDeferredProps(callback)`, `loadDeferredProps(groups, callback)` | Loads the deferred props, and checks them |
258
+
259
+ The reloads are requests, so the callback that uses them is async, and `assertInertia()` returns a promise to await:
260
+
261
+ ```ts
262
+ import type { TestResponse } from '@orieljs/framework/testing'
263
+ declare const response: TestResponse
264
+ // ---cut---
265
+ await response.assertInertia((page) => page.component('Posts/Index').loadDeferredProps((reloaded) => reloaded.has('stats')))
266
+ ```
267
+
268
+ `response.assertInertiaFlash('status', 'Saved.')` checks a flashed value without a callback.
269
+
270
+ ## Testing file uploads
271
+
272
+ `FakeFile` makes files to upload. Give one in the request's data, and the request is sent as `multipart/form-data`:
273
+
274
+ ```ts title="tests/Feature/Avatar.test.ts"
275
+ import { Storage } from '@orieljs/framework'
276
+ import { FakeFile } from '@orieljs/framework/testing'
277
+ import { test } from 'vitest'
278
+ import { TestCase } from '../TestCase'
279
+
280
+ const t = TestCase.boot()
281
+
282
+ test('avatars can be uploaded', async () => {
283
+ const disk = Storage.fake('avatars')
284
+
285
+ const file = FakeFile.image('avatar.jpg')
286
+
287
+ const response = await t.post('/avatar', { avatar: file })
288
+
289
+ response.assertRedirect()
290
+
291
+ await disk.assertExists('avatar.jpg')
292
+ })
293
+ ```
294
+
295
+ The route here stores the file under its own name. `FakeFile.image(name, width, height)` makes a real image, in the
296
+ format its name's extension asks for: PNG, JPEG, GIF, WebP or BMP, so the `image`, `mimes` and `dimensions` rules pass.
297
+ `FakeFile.create(name, kilobytes)` makes a file of that size, typed from its name, or from a MIME type you give as a
298
+ third argument:
299
+
300
+ ```ts
301
+ import { FakeFile } from '@orieljs/framework/testing'
302
+
303
+ const document = FakeFile.create('document.pdf', 500)
304
+
305
+ const sheet = FakeFile.create('data.csv', 10, 'text/csv')
306
+ ```
307
+
308
+ ## Testing views
309
+
310
+ A response rendered from a view knows it: `assertViewIs()` checks its name, and `assertViewHas()` its data:
311
+
312
+ ```ts
313
+ import { TestCase } from './tests/TestCase'
314
+ const t = TestCase.boot()
315
+ // ---cut---
316
+ const response = await t.get('/')
317
+
318
+ response.assertViewIs('welcome').assertViewHas('name', 'Taylor')
319
+ ```
320
+
321
+ `assertViewHas()` takes a key, a key and value, a function that checks the value, or several keys and values.
322
+ `response.viewData('name')` reads the view's data, and `response.original` is the view.
323
+
324
+ > [!NOTE]
325
+ > Laravel's `$this->view()` and `$this->blade()` render a view without a request. In Oriel, render it with
326
+ > `await view('welcome', data).render()` and check the HTML, or send the request that shows it.
327
+
328
+ ## Available assertions
329
+
330
+ ### Response assertions
331
+
332
+ | Assertion | Passes when |
333
+ | --- | --- |
334
+ | `assertStatus(code)` | The response has that status |
335
+ | `assertOk()`, `assertCreated()`, `assertAccepted()`, `assertNoContent()` | 200, 201, 202, or 204 with no body |
336
+ | `assertMovedPermanently()`, `assertFound()`, `assertNotModified()` | 301, 302, 304 |
337
+ | `assertBadRequest()`, `assertUnauthorized()`, `assertPaymentRequired()`, `assertForbidden()`, `assertNotFound()` | 400, 401, 402, 403, 404 |
338
+ | `assertMethodNotAllowed()`, `assertNotAcceptable()`, `assertRequestTimeout()`, `assertConflict()`, `assertGone()` | 405, 406, 408, 409, 410 |
339
+ | `assertUnsupportedMediaType()`, `assertUnprocessable()`, `assertTooManyRequests()` | 415, 422, 429 |
340
+ | `assertInternalServerError()`, `assertServiceUnavailable()` | 500, 503 |
341
+ | `assertSuccessful()`, `assertClientError()`, `assertServerError()` | A 2xx, 4xx or 5xx status |
342
+ | `assertRedirect(uri?)`, `assertRedirectContains(text)`, `assertRedirectToRoute(name, params?)` | A redirect, to that URI or route |
343
+ | `assertLocation(uri)` | The `Location` header is that URI |
344
+ | `assertHeader(name, value?)`, `assertHeaderMissing(name)` | The header is there, with that value, or isn't |
345
+ | `assertDownload(filename?)` | The response is a file download |
346
+ | `assertContent(text)` | The body is exactly the text |
347
+ | `assertSee(text)`, `assertDontSee(text)`, `assertSeeInOrder(texts)` | The body has the text, escaped unless you pass `false` |
348
+ | `assertSeeText(text)`, `assertDontSeeText(text)`, `assertSeeTextInOrder(texts)` | The same, with the HTML tags taken out |
349
+ | `assertJson(data)`, `assertExactJson(data)`, `assertJsonPath(path, value)`, `assertJsonMissingPath(path)` | The JSON |
350
+ | `assertJsonFragment(data)`, `assertJsonMissing(data)` | The data appears somewhere in the JSON, or doesn't |
351
+ | `assertJsonStructure(structure)`, `assertJsonCount(count, key?)` | The JSON's keys, or the number of items |
352
+ | `assertCookie(name, value?)`, `assertPlainCookie(name, value?)` | The response sets the cookie, encrypted or not |
353
+ | `assertCookieExpired(name)`, `assertCookieNotExpired(name)`, `assertCookieMissing(name)` | The cookie |
354
+ | `assertViewIs(name)`, `assertViewHas(key, value?)`, `assertViewHasAll(data)`, `assertViewMissing(key)` | The view |
355
+
356
+ The [Session](session.md#testing) page lists the session assertions: `assertSessionHas()`, `assertSessionMissing()`,
357
+ `assertSessionHasInput()` and the others.
358
+
359
+ ### Authentication assertions
360
+
361
+ These are on the test case, and check the user after the requests so far. They are async, so `await` them:
362
+
363
+ | Assertion | Passes when |
364
+ | --- | --- |
365
+ | `await t.assertAuthenticated(guard?)` | A user is signed in |
366
+ | `await t.assertGuest(guard?)` | No user is signed in |
367
+ | `await t.assertAuthenticatedAs(user, guard?)` | That user is signed in |
368
+
369
+ ### Validation assertions
370
+
371
+ | Assertion | Passes when |
372
+ | --- | --- |
373
+ | `assertValid(keys?)` | The response has no validation errors, for those keys or at all |
374
+ | `assertInvalid(errors)` | It has errors for the keys, or with the messages: `{ name: 'The name field is required.' }` |
375
+ | `assertSessionHasErrors(keys)`, `assertSessionHasNoErrors()` | The session has the errors a form's redirect carries |
376
+ | `assertJsonValidationErrors(keys)`, `assertJsonMissingValidationErrors(keys?)` | A JSON response has the errors |
377
+
378
+ `assertValid()` and `assertInvalid()` read the errors from the session, or from a JSON response's `errors`, so they work
379
+ for both kinds of request.
380
+
381
+ ```ts
382
+ import type { TestResponse } from '@orieljs/framework/testing'
383
+ declare const response: TestResponse
384
+ // ---cut---
385
+ response.assertInvalid({ email: 'The email field must be a valid email address.' })
386
+ ```