bits-to-dead-trees 1.5.1 → 2.0.1

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.
@@ -0,0 +1,249 @@
1
+ import { chromium } from "playwright-core";
2
+ import { afterEach, beforeEach, describe, expect, it, vi, Mock } from "vitest";
3
+
4
+ import build, {
5
+ defaultBrowserContextOptions,
6
+ defaultGotoOptions,
7
+ } from "../app";
8
+ const app = build();
9
+
10
+ describe("GET /health", () => {
11
+ it("provides a simple health check", async () => {
12
+ const response = await app.inject({
13
+ method: "GET",
14
+ url: "/health",
15
+ });
16
+
17
+ expect(response.statusCode).toEqual(200);
18
+ });
19
+ });
20
+
21
+ vi.mock("playwright-core", () => {
22
+ const playwright = {
23
+ chromium: {
24
+ launch: vi.fn(),
25
+ },
26
+ };
27
+ return playwright;
28
+ });
29
+
30
+ describe("POST /pdf", () => {
31
+ afterEach(() => {
32
+ vi.resetAllMocks();
33
+ });
34
+
35
+ let gotoMethod = vi.fn();
36
+ let pdf = vi.fn();
37
+ let newPage = vi.fn();
38
+ let newContext = vi.fn();
39
+ let close = vi.fn();
40
+
41
+ beforeEach(() => {
42
+ gotoMethod = vi.fn().mockResolvedValue({
43
+ ok() {
44
+ return true;
45
+ },
46
+ status() {
47
+ return 200;
48
+ },
49
+ });
50
+ pdf = vi.fn().mockResolvedValue(Buffer.from("pdf"));
51
+ newPage = vi.fn().mockResolvedValue({ goto: gotoMethod, pdf });
52
+ newContext = vi.fn().mockResolvedValue({ newPage });
53
+
54
+ (chromium.launch as Mock<any[], any>).mockResolvedValue({
55
+ newContext,
56
+ close,
57
+ });
58
+ });
59
+
60
+ it("sends a buffer of the PDF as response", async () => {
61
+ const response = await app.inject({
62
+ method: "POST",
63
+ url: "/pdf",
64
+ payload: { url: "/foobar" },
65
+ });
66
+
67
+ expect(gotoMethod).toHaveBeenCalledTimes(1);
68
+ expect(gotoMethod).toHaveBeenLastCalledWith("/foobar", defaultGotoOptions);
69
+
70
+ expect(response.statusCode).toEqual(200);
71
+ expect(response.payload).toEqual("pdf");
72
+ });
73
+
74
+ it("allows to overwrite gotoOptions but merges them with the defaults", async () => {
75
+ await app.inject({
76
+ method: "POST",
77
+ url: "/pdf",
78
+ payload: { url: "/foobar", gotoOptions: { timeout: 300 } },
79
+ });
80
+
81
+ expect(gotoMethod).toHaveBeenCalledWith("/foobar", {
82
+ timeout: 300,
83
+ waitUntil: "networkidle",
84
+ });
85
+ });
86
+
87
+ it("uses the default browserContextOptions if not passed or empty", async () => {
88
+ await app.inject({
89
+ method: "POST",
90
+ url: "/pdf",
91
+ payload: { url: "/foobar", browserContextOptions: {} },
92
+ });
93
+
94
+ expect(newContext).toHaveBeenCalledWith(defaultBrowserContextOptions);
95
+ });
96
+
97
+ it("allows to overwrite browserContextOptions", async () => {
98
+ await app.inject({
99
+ method: "POST",
100
+ url: "/pdf",
101
+ payload: {
102
+ url: "/foobar",
103
+ browserContextOptions: { ignoreHTTPSErrors: false },
104
+ },
105
+ });
106
+
107
+ expect(newContext).toHaveBeenCalledWith({
108
+ ignoreHTTPSErrors: false,
109
+ });
110
+ });
111
+
112
+ it("returns validation errors", async () => {
113
+ const response = await app.inject({
114
+ method: "POST",
115
+ url: "/pdf",
116
+ payload: {
117
+ url: "/foobar",
118
+ gotoOptions: { doesNot: "exist" },
119
+ },
120
+ });
121
+
122
+ expect(response.statusCode).toEqual(400);
123
+
124
+ const parsedResponse = JSON.parse(response.body);
125
+
126
+ expect(parsedResponse.error).toBeTruthy();
127
+ expect(parsedResponse.message).toContain("body/gotoOptions");
128
+ });
129
+
130
+ it("returns error if page could not load", async () => {
131
+ gotoMethod.mockResolvedValue({
132
+ ok() {
133
+ return false;
134
+ },
135
+
136
+ status() {
137
+ return 404;
138
+ },
139
+
140
+ statusText() {
141
+ return "Not found";
142
+ },
143
+
144
+ async text() {
145
+ return "We tried very hard, but could not find the page.";
146
+ },
147
+ });
148
+
149
+ const response = await app.inject({
150
+ method: "POST",
151
+ url: "/pdf",
152
+ payload: {
153
+ url: "/foobar",
154
+ },
155
+ });
156
+
157
+ expect(response.statusCode).toEqual(404);
158
+ const error = JSON.parse(response.body);
159
+
160
+ expect(error).toMatchObject({
161
+ error: true,
162
+ status: 404,
163
+ statusText: "Not found",
164
+ message: "We tried very hard, but could not find the page.",
165
+ });
166
+ });
167
+
168
+ it("allows to pass options to the pdf call", async () => {
169
+ await app.inject({
170
+ method: "POST",
171
+ url: "/pdf",
172
+ payload: {
173
+ url: "/foobar",
174
+ pdfOptions: {
175
+ headerTemplate: "moin",
176
+ },
177
+ },
178
+ });
179
+
180
+ expect(pdf).toHaveBeenCalledWith({ headerTemplate: "moin" });
181
+ });
182
+
183
+ it("returns 500 if something broke", async () => {
184
+ pdf.mockRejectedValue(new Error("OH NO"));
185
+
186
+ const response = await app.inject({
187
+ method: "POST",
188
+ url: "/pdf",
189
+ payload: {
190
+ url: "/foobar",
191
+ },
192
+ });
193
+
194
+ expect(response.statusCode).toEqual(500);
195
+ expect(JSON.parse(response.body)).toMatchObject({
196
+ statusCode: 500,
197
+ error: "Internal Server Error",
198
+ message: "OH NO",
199
+ });
200
+ });
201
+
202
+ it("allows null as empty for options", async () => {
203
+ await app.inject({
204
+ method: "POST",
205
+ url: "/pdf",
206
+ payload: { url: "/foobar", gotoOptions: null },
207
+ });
208
+
209
+ expect(gotoMethod).toHaveBeenCalledWith("/foobar", defaultGotoOptions);
210
+ });
211
+
212
+ it("closes the browser", async () => {
213
+ pdf.mockRejectedValue("OH NO");
214
+
215
+ await app.inject({
216
+ method: "POST",
217
+ url: "/pdf",
218
+ payload: {
219
+ url: "/foobar",
220
+ },
221
+ });
222
+
223
+ expect(close).toHaveBeenCalled();
224
+ });
225
+
226
+ it("generates the pdf before closing the browser", async () => {
227
+ let i = 0;
228
+
229
+ pdf.mockImplementation(async () => {
230
+ await new Promise((r) => setTimeout(r, 100));
231
+
232
+ return i++;
233
+ });
234
+
235
+ close.mockImplementation(() => {
236
+ return i++;
237
+ });
238
+
239
+ await app.inject({
240
+ method: "POST",
241
+ url: "/pdf",
242
+ payload: {
243
+ url: "https://google.com",
244
+ },
245
+ });
246
+
247
+ expect(pdf.mock.results[0].value).toBeLessThan(close.mock.results[0].value);
248
+ });
249
+ });
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import build from "../app";
4
+ const app = build();
5
+
6
+ describe("Smoke tests", () => {
7
+ it("sends the PDF on the success case", async () => {
8
+ const response = await app.inject({
9
+ method: "POST",
10
+ url: "/pdf",
11
+ payload: { url: "https://coding-robin.de" },
12
+ });
13
+
14
+ expect(response.payload.startsWith("%PDF")).toBeTruthy();
15
+ });
16
+
17
+ it("returns an error object when the page could not be loaded", async () => {
18
+ const response = await app.inject({
19
+ method: "POST",
20
+ url: "/pdf",
21
+ payload: {
22
+ url: "http://httpbin.org/status/404",
23
+ },
24
+ });
25
+
26
+ expect(response.statusCode).toEqual(404);
27
+ const error = JSON.parse(response.body);
28
+
29
+ expect(error).toMatchObject({
30
+ error: true,
31
+ status: 404,
32
+ statusText: "NOT FOUND",
33
+ message: "",
34
+ });
35
+ });
36
+ });
package/tsconfig.json CHANGED
@@ -7,6 +7,8 @@
7
7
  "skipLibCheck": true,
8
8
  "forceConsistentCasingInFileNames": true,
9
9
  "resolveJsonModule": true,
10
+ "esModuleInterop": true,
11
+ "module": "ESNext",
10
12
 
11
13
  "checkJs": true
12
14
  }
package/types/schemas.ts CHANGED
@@ -1,7 +1,10 @@
1
- import { Page } from "playwright-core";
1
+ import { BrowserContextOptions, Page } from "playwright-core";
2
2
 
3
3
  export type PdfOptions = Parameters<Page["pdf"]>[0];
4
+ export type GotoOptions = Parameters<Page["goto"]>[1];
4
5
  export type PdfRequestBody = {
5
6
  url: string;
6
- options?: PdfOptions;
7
+ pdfOptions?: PdfOptions | null;
8
+ gotoOptions?: GotoOptions | null;
9
+ browserContextOptions?: BrowserContextOptions | null;
7
10
  };
package/index.js DELETED
@@ -1,95 +0,0 @@
1
- // @ts-check
2
-
3
- /**
4
- * @typedef {Parameters<import("playwright-core").Page["pdf"]>[0]} PdfOptions
5
- */
6
-
7
- /**
8
- * @typedef {Object} PdfRequestBody
9
- * @property {string} url
10
- * @property {PdfOptions} options
11
- */
12
-
13
- const { fastify } = require("fastify");
14
- const playwright = require("playwright-core");
15
- const PdfRequestBodySchema = require("./pdf_request_body.json");
16
-
17
- const SERVER_PORT = process.env["SERVER_PORT"] ?? 8000;
18
- const SERVER_ADDRESS = process.env["SERVER_ADDRESS"] ?? "localhost";
19
-
20
- const server = fastify({
21
- logger: {
22
- transport:
23
- process.env["NODE_ENV"] !== "production"
24
- ? {
25
- target: "pino-pretty",
26
- options: {
27
- translateTime: "HH:MM:ss Z",
28
- ignore: "pid,hostname",
29
- },
30
- }
31
- : undefined,
32
- },
33
- });
34
-
35
- /**
36
- *
37
- * @param {string} url
38
- * @param {PdfOptions} options
39
- * @returns Buffer
40
- */
41
- const createPDF = async (url, options = {}) => {
42
- const browser = await playwright.chromium.launch();
43
- const context = await browser.newContext({ ignoreHTTPSErrors: true });
44
- const page = await context.newPage();
45
- await page.goto(url, { timeout: 60000 });
46
- await page.waitForLoadState("networkidle");
47
-
48
- const pdf = await page.pdf(options);
49
- await browser.close();
50
-
51
- return pdf;
52
- };
53
-
54
- server.post(
55
- "/pdf",
56
- { schema: { body: PdfRequestBodySchema }, attachValidation: true },
57
- /**
58
- *
59
- * @param {import('fastify').FastifyRequest<{Body: PdfRequestBody}>} request
60
- * @param {import('fastify').FastifyReply} response
61
- */
62
- async (request, response) => {
63
- if (request.validationError) {
64
- response.code(400);
65
-
66
- return {
67
- error: true,
68
- message: request.validationError.message,
69
- };
70
- }
71
-
72
- try {
73
- return createPDF(request.body.url, request.body.options);
74
- } catch (error) {
75
- response.code(500);
76
-
77
- return {
78
- error,
79
- };
80
- }
81
- }
82
- );
83
-
84
- server.get("/health", async (_, response) => response.code(200).send());
85
-
86
- const startServer = async () => {
87
- try {
88
- await server.listen({ port: +SERVER_PORT, host: SERVER_ADDRESS });
89
- } catch (error) {
90
- console.error(error);
91
- process.exit(1);
92
- }
93
- };
94
-
95
- startServer();