@zerotal/testing 1.10.0 → 1.11.0
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/CHANGELOG.md +20 -0
- package/api-surface.md +1 -0
- package/package.json +6 -6
- package/src/TestResponse.ts +82 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,26 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.11.0] — 2026-08-31
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **`assertRedirect` compares the path exactly**, where it used to use `includes()`.
|
|
16
|
+
The loose form made the assertion mean less than it looks like it means:
|
|
17
|
+
`assertRedirect("/login")` was satisfied by `/login-as-someone-else` and by
|
|
18
|
+
`/admin?next=/login` — the two cases a test about a login redirect exists to rule
|
|
19
|
+
out. An absolute `Location` still matches a relative expectation, and naming a query
|
|
20
|
+
string compares that too. `assertRedirectContains()` is the old behaviour, for the
|
|
21
|
+
cases that want it (a signed URL with an unpredictable token).
|
|
22
|
+
|
|
23
|
+
### Documented
|
|
24
|
+
|
|
25
|
+
- **How to authenticate a test when identity is not a row.** `withSession()` already
|
|
26
|
+
did it, and an app with no users table — an IMAP login _is_ the identity — reached
|
|
27
|
+
past it to the session driver instead, guessing `driver.write()`. Reaching for the
|
|
28
|
+
driver is the wrong layer and does not work; the doc now says so and shows the form
|
|
29
|
+
that does.
|
|
30
|
+
|
|
11
31
|
## [1.10.0] — 2026-08-30
|
|
12
32
|
|
|
13
33
|
### Added
|
package/api-surface.md
CHANGED
|
@@ -197,6 +197,7 @@ class TestResponse = {
|
|
|
197
197
|
assertNotFound: () => TestResponse
|
|
198
198
|
assertOk: () => TestResponse
|
|
199
199
|
assertRedirect: (url: string) => TestResponse
|
|
200
|
+
assertRedirectContains: (fragment: string) => TestResponse
|
|
200
201
|
assertSee: (needle: string) => TestResponse
|
|
201
202
|
assertSeeText: (needle: string) => TestResponse
|
|
202
203
|
assertServerError: () => TestResponse
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/testing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"typecheck": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zerotal/core": "1.
|
|
36
|
-
"@zerotal/orm": "1.
|
|
37
|
-
"@zerotal/queue": "1.
|
|
38
|
-
"@zerotal/notifications": "1.
|
|
35
|
+
"@zerotal/core": "1.11.0",
|
|
36
|
+
"@zerotal/orm": "1.11.0",
|
|
37
|
+
"@zerotal/queue": "1.11.0",
|
|
38
|
+
"@zerotal/notifications": "1.11.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.8.0",
|
|
42
|
-
"@zerotal/session": "1.
|
|
42
|
+
"@zerotal/session": "1.11.0"
|
|
43
43
|
},
|
|
44
44
|
"description": "Testing utilities for Zerotal — an in-process test app, HTTP helpers, and database refresh.",
|
|
45
45
|
"keywords": [
|
package/src/TestResponse.ts
CHANGED
|
@@ -179,22 +179,74 @@ export class TestResponse {
|
|
|
179
179
|
return this;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* Assert the response is a redirect to `url`.
|
|
184
|
+
*
|
|
185
|
+
* The comparison is on the `Location` header's **path**, exactly. A substring
|
|
186
|
+
* match — which this used to do — makes the assertion mean less than it looks
|
|
187
|
+
* like it means: `assertRedirect("/login")` was satisfied by
|
|
188
|
+
* `/login-as-someone-else` and by `/admin?next=/login`, so a redirect to the
|
|
189
|
+
* wrong place passed a test written to catch exactly that.
|
|
190
|
+
*
|
|
191
|
+
* A `url` carrying a query string or a fragment is compared whole, so you can
|
|
192
|
+
* still pin one when it matters. For anything looser, use
|
|
193
|
+
* {@link assertRedirectContains}.
|
|
194
|
+
*
|
|
195
|
+
* @param url - Expected `Location`, or just its path.
|
|
196
|
+
*/
|
|
183
197
|
assertRedirect(url: string): this {
|
|
184
|
-
|
|
198
|
+
const location = this._assertIsRedirect();
|
|
199
|
+
|
|
200
|
+
// Compare paths unless the expectation names a query or a fragment — an
|
|
201
|
+
// absolute `Location` and a relative expectation are the same redirect.
|
|
202
|
+
const wantsMore = url.includes("?") || url.includes("#");
|
|
203
|
+
const actual = wantsMore ? _withoutOrigin(location) : _pathOf(location);
|
|
204
|
+
const expected = wantsMore ? _withoutOrigin(url) : _pathOf(url);
|
|
205
|
+
|
|
206
|
+
if (actual !== expected) {
|
|
185
207
|
throw new Error(
|
|
186
|
-
this._decorate(
|
|
208
|
+
this._decorate(
|
|
209
|
+
`Expected redirect to "${url}" but Location was "${location}".` +
|
|
210
|
+
(location.includes(url)
|
|
211
|
+
? `\n (It contains the expected value but is not equal to it. ` +
|
|
212
|
+
`Use assertRedirectContains() if that is what you meant.)`
|
|
213
|
+
: ""),
|
|
214
|
+
),
|
|
187
215
|
);
|
|
188
216
|
}
|
|
189
|
-
|
|
190
|
-
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Assert the response is a redirect whose `Location` *contains* `fragment`.
|
|
222
|
+
*
|
|
223
|
+
* The old behaviour of {@link assertRedirect}, kept for the cases where it is
|
|
224
|
+
* genuinely what you want — a signed URL with an unpredictable token, say.
|
|
225
|
+
*
|
|
226
|
+
* @param fragment - Substring the `Location` must contain.
|
|
227
|
+
*/
|
|
228
|
+
assertRedirectContains(fragment: string): this {
|
|
229
|
+
const location = this._assertIsRedirect();
|
|
230
|
+
if (!location.includes(fragment)) {
|
|
191
231
|
throw new Error(
|
|
192
|
-
this._decorate(
|
|
232
|
+
this._decorate(
|
|
233
|
+
`Expected redirect containing "${fragment}" but Location was "${location}".`,
|
|
234
|
+
),
|
|
193
235
|
);
|
|
194
236
|
}
|
|
195
237
|
return this;
|
|
196
238
|
}
|
|
197
239
|
|
|
240
|
+
/** The `Location` of a response that is a redirect, or a failure saying it is not. */
|
|
241
|
+
private _assertIsRedirect(): string {
|
|
242
|
+
if (this._res.status < 300 || this._res.status > 399) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
this._decorate(`Expected a redirect but got HTTP ${this._res.status}.`, { body: true }),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return this._res.headers.get("Location") ?? "";
|
|
248
|
+
}
|
|
249
|
+
|
|
198
250
|
/**
|
|
199
251
|
* Assert the response is a redirect a browser running Inertia will actually follow.
|
|
200
252
|
*
|
|
@@ -1006,3 +1058,27 @@ function _indent(text: string): string {
|
|
|
1006
1058
|
.map((line) => ` ${line}`)
|
|
1007
1059
|
.join("\n");
|
|
1008
1060
|
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* A URL's path, so an absolute `Location` and a relative expectation compare equal.
|
|
1064
|
+
*
|
|
1065
|
+
* A redirect to `https://app.test/dashboard` and an expectation of `/dashboard` are
|
|
1066
|
+
* the same redirect, and a test should not have to know which form the handler used.
|
|
1067
|
+
*/
|
|
1068
|
+
function _pathOf(url: string): string {
|
|
1069
|
+
try {
|
|
1070
|
+
return new URL(url, "http://localhost").pathname;
|
|
1071
|
+
} catch {
|
|
1072
|
+
return url;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** A URL without its origin — path, query and fragment — for an exact comparison. */
|
|
1077
|
+
function _withoutOrigin(url: string): string {
|
|
1078
|
+
try {
|
|
1079
|
+
const parsed = new URL(url, "http://localhost");
|
|
1080
|
+
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
1081
|
+
} catch {
|
|
1082
|
+
return url;
|
|
1083
|
+
}
|
|
1084
|
+
}
|