Skip to content

Commit 4411199

Browse files
committed
feat: support Express
1 parent 16865c8 commit 4411199

File tree

11 files changed

+1195
-51
lines changed

11 files changed

+1195
-51
lines changed

README.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ HTTP assertions for Deno made easy via <a href="https://github.com/visionmedia/s
3131
- [Getting Started](#getting-started)
3232
- [About](#about)
3333
- [Installation](#installation)
34-
- [Example](#example)
34+
- [Examples](#examples)
3535
- [Documentation](#documentation)
3636
- [API](#api)
3737
- [Notes](#notes)
@@ -87,7 +87,7 @@ a package registry for Deno on the Blockchain.
8787

8888
> Note: All examples in this README are using the unversioned form of the import URL. In production you should always use the versioned import form such as `https://deno.land/x/[email protected]/mod.ts`.
8989
90-
## Example
90+
## Examples
9191

9292
You may pass a url string,
9393
[`http.Server`](https://doc.deno.land/https/deno.land/std/http/mod.ts#Server), a
@@ -117,7 +117,7 @@ Here's an example of SuperDeno working with the Opine web framework:
117117
```ts
118118
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
119119
import { opine } from "https://deno.land/x/[email protected]/mod.ts";
120-
export { expect } from "https://deno.land/x/[email protected]/mod.ts";
120+
import { expect } from "https://deno.land/x/[email protected]/mod.ts";
121121

122122
const app = opine();
123123

@@ -129,19 +129,49 @@ Deno.test("it should support regular expressions", async () => {
129129
await superdeno(app)
130130
.get("/")
131131
.expect("Content-Type", /^application/)
132-
.end((err) => {
132+
.catch((err) => {
133133
expect(err.message).toEqual(
134134
'expected "Content-Type" matching /^application/, got "text/html; charset=utf-8"'
135135
);
136136
});
137137
});
138138
```
139139

140+
See more examples in the [Opine test suite](./test/superdeno.opine.test.ts).
141+
142+
Here's an example of SuperDeno working with the Express web framework:
143+
144+
```ts
145+
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
146+
// @deno-types="npm:@types/express@^4.17"
147+
import express from "npm:[email protected]";
148+
import { expect } from "https://deno.land/x/[email protected]/mod.ts";
149+
150+
Deno.test("it should support regular expressions", async () => {
151+
const app = express();
152+
153+
app.get("/", (_req, res) => {
154+
res.send("Hello Deno!");
155+
});
156+
157+
await superdeno(app)
158+
.get("/")
159+
.expect("Content-Type", /^application/)
160+
.catch((err) => {
161+
expect(err.message).toEqual(
162+
'expected "Content-Type" matching /^application/, got "text/html; charset=utf-8"'
163+
);
164+
});
165+
});
166+
```
167+
168+
See more examples in the [Express test suite](./test/superdeno.express.test.ts).
169+
140170
Here's an example of SuperDeno working with the Oak web framework:
141171

142172
```ts
143173
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
144-
import { Application, Router } from "https://deno.land/x/oak@v10.0.0/mod.ts";
174+
import { Application, Router } from "https://deno.land/x/oak@v12.6.2/mod.ts";
145175

146176
const router = new Router();
147177
router.get("/", (ctx) => {
@@ -171,6 +201,8 @@ Deno.test("it should support the Oak framework", () => {
171201
});
172202
```
173203

204+
See more examples in the [Oak test suite](./test/superdeno.oak.test.ts).
205+
174206
If you are using the [Oak](https://github.com/oakserver/oak/) web framework then
175207
it is recommended that you use the specialized
176208
[SuperOak](https://github.com/cmorten/superoak) assertions library for
@@ -181,7 +213,7 @@ are making use of the `app.handle()` method (for example for serverless apps)
181213
then you can write slightly less verbose tests for Oak:
182214

183215
```ts
184-
import { Application, Router } from "https://deno.land/x/oak@v10.0.0/mod.ts";
216+
import { Application, Router } from "https://deno.land/x/oak@v12.6.2/mod.ts";
185217
import { superdeno } from "https://deno.land/x/superdeno/mod.ts";
186218

187219
const router = new Router();

deps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type { StatusCode } from "https://deno.land/[email protected]/http/status.ts";
44
export { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
55
export { methods } from "https://deno.land/x/[email protected]/src/methods.ts";
66
export { mergeDescriptors } from "https://deno.land/x/[email protected]/src/utils/mergeDescriptors.ts";
7+
export { getFreePort } from "https://deno.land/x/[email protected]/mod.ts";
78

89
// TODO: upgrade to v8
910
export { default as superagent } from "https://jspm.dev/[email protected]";

src/test.ts

Lines changed: 113 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,29 @@
77
* - https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/supertest/index.d.ts
88
*/
99

10-
import type { ListenerLike, ServerLike } from "./types.ts";
11-
import { assertEquals, STATUS_TEXT, StatusCode } from "../deps.ts";
10+
import type {
11+
ExpressListenerLike,
12+
ExpressServerLike,
13+
ListenerLike,
14+
ServerLike,
15+
} from "./types.ts";
16+
import { assertEquals, getFreePort, STATUS_TEXT, StatusCode } from "../deps.ts";
1217
import { superagent } from "./superagent.ts";
1318
import { close } from "./close.ts";
14-
import { isListener, isServer, isStdNativeServer, isString } from "./utils.ts";
19+
import {
20+
isExpressListener,
21+
isExpressServer,
22+
isListener,
23+
isServer,
24+
isStdNativeServer,
25+
isString,
26+
} from "./utils.ts";
1527
import { exposeSham } from "./xhrSham.js";
1628

29+
export function random(min: number, max: number): number {
30+
return Math.round(Math.random() * (max - min)) + min;
31+
}
32+
1733
/**
1834
* Custom expectation checker.
1935
*/
@@ -201,9 +217,11 @@ export class Test extends SuperRequest {
201217
#redirects: number;
202218
#redirectList: string[];
203219
#server!: ServerLike;
220+
#serverSetupPromise: Promise<void>;
221+
#urlSetupPromise: Promise<void>;
204222

205223
public app: string | ListenerLike | ServerLike;
206-
public url: string;
224+
public url!: string;
207225

208226
constructor(
209227
app: string | ListenerLike | ServerLike,
@@ -220,8 +238,21 @@ export class Test extends SuperRequest {
220238
this.app = app;
221239
this.#asserts = [];
222240

241+
let serverSetupPromiseResolver!: () => void;
242+
let addressSetupPromiseResolver!: () => void;
243+
244+
this.#serverSetupPromise = new Promise<void>((resolve) => {
245+
serverSetupPromiseResolver = resolve;
246+
});
247+
this.#urlSetupPromise = new Promise<void>((resolve) => {
248+
addressSetupPromiseResolver = resolve;
249+
});
250+
223251
if (isString(app)) {
224252
this.url = `${app}${path}`;
253+
254+
serverSetupPromiseResolver();
255+
addressSetupPromiseResolver();
225256
} else {
226257
if (isStdNativeServer(app)) {
227258
const listenAndServePromise = app.listenAndServe().catch((err) =>
@@ -240,18 +271,60 @@ export class Test extends SuperRequest {
240271
addrs: app.addrs,
241272
async listenAndServe() {},
242273
};
274+
275+
serverSetupPromiseResolver();
276+
} else if (isExpressServer(app)) {
277+
this.#server = app as ExpressServerLike;
278+
279+
const expressResolver = async () => {
280+
await new Promise((resolve) => setTimeout(resolve, 1));
281+
serverSetupPromiseResolver();
282+
};
283+
284+
if (!this.#server.listening) {
285+
(this.#server as ExpressServerLike).once(
286+
"listening",
287+
expressResolver,
288+
);
289+
} else {
290+
expressResolver();
291+
}
243292
} else if (isServer(app)) {
244293
this.#server = app as ServerLike;
294+
295+
serverSetupPromiseResolver();
296+
} else if (isExpressListener(app)) {
297+
secure = false;
298+
299+
const expressResolver = async () => {
300+
await new Promise((resolve) => setTimeout(resolve, 1));
301+
serverSetupPromiseResolver();
302+
};
303+
304+
getFreePort(random(1024, 49151)).then(
305+
(freePort) => {
306+
this.#server = (app as ExpressListenerLike).listen(
307+
freePort,
308+
expressResolver,
309+
);
310+
},
311+
);
245312
} else if (isListener(app)) {
246313
secure = false;
314+
247315
this.#server = (app as ListenerLike).listen(":0");
316+
317+
serverSetupPromiseResolver();
248318
} else {
319+
serverSetupPromiseResolver();
320+
addressSetupPromiseResolver();
321+
249322
throw new Error(
250323
"superdeno is unable to identify or create a valid test server",
251324
);
252325
}
253326

254-
this.url = this.#serverAddress(path, host, secure);
327+
this.#setServerAddress(addressSetupPromiseResolver, path, host, secure);
255328
}
256329
}
257330

@@ -265,19 +338,28 @@ export class Test extends SuperRequest {
265338
* @returns {string} URL address
266339
* @private
267340
*/
268-
#serverAddress = (
341+
#setServerAddress = async (
342+
addressSetupPromiseResolver: () => void,
269343
path: string,
270344
host?: string,
271345
secure?: boolean,
272346
) => {
347+
await this.#serverSetupPromise;
348+
273349
const address =
274350
("addrs" in this.#server
275351
? this.#server.addrs[0]
352+
: "address" in this.#server
353+
? this.#server.address()
276354
: this.#server.listener.addr) as Deno.NetAddr;
355+
277356
const port = address.port;
278357
const protocol = secure ? "https" : "http";
358+
const url = `${protocol}://${(host || "127.0.0.1")}:${port}${path}`;
279359

280-
return `${protocol}://${(host || "127.0.0.1")}:${port}${path}`;
360+
this.url = url;
361+
362+
addressSetupPromiseResolver();
281363
};
282364

283365
/**
@@ -455,29 +537,33 @@ export class Test extends SuperRequest {
455537
* @public
456538
*/
457539
end(callback?: CallbackHandler): this {
458-
const self = this;
459-
const end = SuperRequest.prototype.end;
460-
461-
end.call(
462-
self,
463-
function (err: any, res: any) {
464-
// Before we close, ensure that we have handled all
465-
// requested redirects
466-
const redirect = isRedirect(res?.statusCode);
467-
const max: number = (self as any)._maxRedirects;
468-
469-
if (redirect && self.#redirects++ !== max) {
470-
return self.#redirect(res, callback);
471-
}
540+
Promise.allSettled([this.#serverSetupPromise, this.#urlSetupPromise]).then(
541+
() => {
542+
const self = this;
543+
const end = SuperRequest.prototype.end;
544+
545+
end.call(
546+
self,
547+
function (err: any, res: any) {
548+
// Before we close, ensure that we have handled all
549+
// requested redirects
550+
const redirect = isRedirect(res?.statusCode);
551+
const max: number = (self as any)._maxRedirects;
552+
553+
if (redirect && self.#redirects++ !== max) {
554+
return self.#redirect(res, callback);
555+
}
472556

473-
return close(self.#server, self.app, undefined, async () => {
474-
await completeXhrPromises();
557+
return close(self.#server, self.app, undefined, async () => {
558+
await completeXhrPromises();
475559

476-
// REF: https://github.com/denoland/deno/blob/987716798fb3bddc9abc7e12c25a043447be5280/ext/timers/01_timers.js#L353
477-
await new Promise((resolve) => setTimeout(resolve, 20));
560+
// REF: https://github.com/denoland/deno/blob/987716798fb3bddc9abc7e12c25a043447be5280/ext/timers/01_timers.js#L353
561+
await new Promise((resolve) => setTimeout(resolve, 20));
478562

479-
self.#assert(err, res, callback);
480-
});
563+
self.#assert(err, res, callback);
564+
});
565+
},
566+
);
481567
},
482568
);
483569

src/types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,22 @@ export interface NativeServerLike {
1818
close(): void;
1919
}
2020

21-
export type ServerLike = LegacyServerLike | NativeServerLike;
21+
export interface ExpressServerLike {
22+
address(): any;
23+
listening: boolean;
24+
close(): void;
25+
once(eventName: string, listener: () => void): void;
26+
}
27+
28+
export type ServerLike =
29+
| LegacyServerLike
30+
| NativeServerLike
31+
| ExpressServerLike;
2232

2333
export interface ListenerLike {
2434
listen(addr: string): ServerLike;
2535
}
36+
37+
export interface ExpressListenerLike {
38+
listen(port: number, callback: () => void): ServerLike;
39+
}

src/utils.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
ExpressListenerLike,
23
LegacyServerLike,
34
ListenerLike,
45
NativeServerLike,
@@ -11,6 +12,14 @@ export const isString = (thing: unknown): thing is string =>
1112
export const isListener = (thing: unknown): thing is ListenerLike =>
1213
thing instanceof Object && thing !== null && "listen" in thing;
1314

15+
export const isExpressListener = (
16+
thing: unknown,
17+
): thing is ExpressListenerLike =>
18+
thing instanceof Object && thing !== null && "locals" in thing &&
19+
"mountpath" in thing && "all" in thing && "engine" in thing &&
20+
"listen" in thing && "param" in thing && "path" in thing &&
21+
"render" in thing && "route" in thing && "set" in thing && "use" in thing;
22+
1423
const isCommonServer = (thing: unknown): thing is ServerLike =>
1524
thing instanceof Object && thing !== null && "close" in thing;
1625

@@ -22,5 +31,11 @@ export const isStdNativeServer = (thing: unknown): thing is NativeServerLike =>
2231
isCommonServer(thing) &&
2332
"addrs" in thing;
2433

34+
export const isExpressServer = (thing: unknown): thing is NativeServerLike =>
35+
isCommonServer(thing) &&
36+
"listening" in thing &&
37+
"address" in thing && typeof thing.address === "function";
38+
2539
export const isServer = (thing: unknown): thing is ServerLike =>
26-
isStdLegacyServer(thing) || isStdNativeServer(thing);
40+
isStdLegacyServer(thing) || isStdNativeServer(thing) ||
41+
isExpressServer(thing);

0 commit comments

Comments
 (0)