diff --git a/CHANGELOG.md b/CHANGELOG.md index 032b814..7c0808a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project are documented here. This project follows ## [Unreleased] +### Changed + +- The extended HTTP assertions (`expectClientError`, `expectJsonSchema`, + `expectJsonContains`, `expectCookie`, `expectSorted`, and the rest) are now + chainable on the request builder too, not only on a resolved response. So + `api.get("/x").expectStatus(200).expectJsonSchema(schema)` works. `expectValue` + stays response-only since it returns an `expect()`. + ### Added - **BDD layer** (`two-go/bdd`): runner-agnostic `given` / `when` / `then` / diff --git a/src/client.d.ts b/src/client.d.ts index 6bdf5e4..5fd23e5 100644 --- a/src/client.d.ts +++ b/src/client.d.ts @@ -61,6 +61,30 @@ export declare class RequestBuilder implements PromiseLike { expectTimeBelow(ms: number): this; check(fn: (response: GoResponse) => unknown): this; + // --- extended HTTP assertions, also chainable here (added by http-assertions.js) --- + expectClientError(): this; + expectServerError(): this; + expectRedirect(): this; + expectCreated(): this; + expectAccepted(): this; + expectNoContent(): this; + expectBadRequest(): this; + expectUnauthorized(): this; + expectForbidden(): this; + expectNotFound(): this; + expectContentType(type: string): this; + expectHeaderContains(name: string, substr: string): this; + expectHeaderAbsent(name: string): this; + expectJsonSchema(schema: unknown): this; + expectJsonLength(path: string, n: number): this; + expectJsonContains(path: string, value: unknown): this; + expectArrayLength(path: string, n: number): this; + expectSorted(path: string, options?: { key?: string; order?: "asc" | "desc" }): this; + expectCookie(name: string, matcher?: unknown): this; + expectBodyContains(substr: string): this; + expectEmpty(): this; + expectNotEmpty(): this; + // --- run + thenable --- run(): Promise; diff --git a/src/http-assertions.js b/src/http-assertions.js index 6608b25..cebef8d 100644 --- a/src/http-assertions.js +++ b/src/http-assertions.js @@ -6,6 +6,7 @@ // with messages formatted as `${this.method} ${this.url} -> `. import { GoResponse } from "./response.js"; +import { RequestBuilder } from "./client.js"; import { AssertionError, resolvePath, matches } from "./assertions.js"; import { validate } from "./schema.js"; import { expect } from "./expect.js"; @@ -411,3 +412,22 @@ for (const [name, fn] of Object.entries(methods)) { configurable: true, }); } + +// Also make these assertions chainable on the (thenable) RequestBuilder, so you +// can write api.get("/x").expectStatus(200).expectJsonSchema(schema) instead of +// awaiting first. They are queued and replayed on the resolved GoResponse, just +// like the core assertions. expectValue is excluded because it returns an +// Expectation rather than the response, so it only makes sense post-await. +for (const name of Object.keys(methods)) { + if (name === "expectValue") continue; + if (typeof RequestBuilder.prototype[name] === "function") continue; + Object.defineProperty(RequestBuilder.prototype, name, { + value: function (...args) { + this._assertions.push({ name, args }); + return this; + }, + writable: true, + enumerable: false, + configurable: true, + }); +} diff --git a/test/unit/builder-assertions.test.mjs b/test/unit/builder-assertions.test.mjs new file mode 100644 index 0000000..975fe62 --- /dev/null +++ b/test/unit/builder-assertions.test.mjs @@ -0,0 +1,52 @@ +// The extended HTTP assertions should be chainable on the (thenable) builder, +// not only on a resolved GoResponse. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import http from "node:http"; +import { go } from "../../src/index.js"; + +let server; +let base; + +test.before(async () => { + server = http.createServer((req, res) => { + if (req.url === "/users") { + res.writeHead(201, { "content-type": "application/json", "set-cookie": "sid=abc" }); + res.end(JSON.stringify({ data: [{ id: 1 }, { id: 2 }], count: 2 })); + } else { + res.writeHead(404, { "content-type": "application/json" }); + res.end(JSON.stringify({ error: "not found" })); + } + }); + await new Promise((r) => server.listen(0, r)); + base = `http://localhost:${server.address().port}`; +}); + +test.after(() => server.close()); + +test("extended assertions chain on the builder and replay on the response", async () => { + await go(base) + .post("/users") + .json({ name: "Ada" }) + .expectStatus(201) + .expectCreated() + .expectContentType("json") + .expectJsonSchema({ type: "object", required: ["data", "count"] }) + .expectJsonLength("data", 2) + .expectJsonContains("data", { id: 2 }) + .expectCookie("sid"); +}); + +test("a queued extended assertion still fails when it should", async () => { + await assert.rejects( + () => go(base).get("/users").expectNotFound(), + /404/ + ); +}); + +test("expectValue is not queued on the builder (only on the response)", async () => { + const builder = go(base).get("/users"); + assert.equal(typeof builder.expectValue, "undefined"); + const res = await builder; + res.expectValue("count").toBe(2); +});