chromiumly 2.0.1 → 2.0.4
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/README.md +9 -1
- package/dist/pdf-engines/utils/engine.utils.js +7 -1
- package/dist/pdf-engines/utils/engine.utils.js.map +1 -1
- package/package.json +10 -4
- package/src/chromium/converters/tests/html.converter.test.ts +95 -0
- package/src/chromium/converters/tests/markdown.converter.test.ts +105 -0
- package/src/chromium/converters/tests/url.converter.test.ts +82 -0
- package/src/chromium/utils/tests/converter.utils.test.ts +101 -0
- package/src/common/tests/gotenberg.utils.test.ts +71 -0
- package/src/libre-office/utils/test/libre-office.utils.test.ts +85 -0
- package/src/pdf-engines/tests/pdf.engine.test.ts +84 -0
- package/src/pdf-engines/utils/engine.utils.ts +6 -1
- package/src/pdf-engines/utils/tests/engine.utils.test.ts +59 -0
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Chromiumly
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+
[](https://codecov.io/gh/cherfia/chromiumly)
|
|
5
|
+
[](https://snyk.io/test/github/cherfia/chromiumly?targetFile=package.json)
|
|
6
|
+
[](https://codeclimate.com/github/cherfia/chromiumly/maintainability)
|
|
7
|
+
[](https://npmjs.org/package/chromiumly)
|
|
8
|
+
[](https://npm-stat.com/charts.html?package=chromiumly)
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
A lightweight Typescript library that interacts with [Gotenberg](https://gotenberg.dev/)'s different modules to convert a variety of document formats to PDF files.
|
|
4
12
|
|
|
5
13
|
## Install
|
|
6
14
|
|
|
@@ -11,7 +11,13 @@ class PDFEngineUtils {
|
|
|
11
11
|
try {
|
|
12
12
|
yield fs_1.promises.access(file, fs_1.constants.R_OK);
|
|
13
13
|
const filename = path_1.default.basename(file.toString());
|
|
14
|
-
|
|
14
|
+
const extension = path_1.default.extname(filename);
|
|
15
|
+
if (extension === ".pdf") {
|
|
16
|
+
data.append(filename, (0, fs_1.createReadStream)(file));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
throw new Error(`${extension} is not supported`);
|
|
20
|
+
}
|
|
15
21
|
}
|
|
16
22
|
catch (error) {
|
|
17
23
|
throw error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.utils.js","sourceRoot":"","sources":["../../../src/pdf-engines/utils/engine.utils.ts"],"names":[],"mappings":";;;;AAAA,2BAAqE;AACrE,wDAAwB;AAIxB,MAAa,cAAc;IAClB,MAAM,CAAO,WAAW,CAAC,KAAiB,EAAE,IAAc;;YAC/D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE;gBACxB,IAAI;oBACF,MAAM,aAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,cAAS,CAAC,IAAI,CAAC,CAAC;oBAC5C,MAAM,QAAQ,GAAG,cAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;oBAChD,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAA,qBAAgB,EAAC,IAAI,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"engine.utils.js","sourceRoot":"","sources":["../../../src/pdf-engines/utils/engine.utils.ts"],"names":[],"mappings":";;;;AAAA,2BAAqE;AACrE,wDAAwB;AAIxB,MAAa,cAAc;IAClB,MAAM,CAAO,WAAW,CAAC,KAAiB,EAAE,IAAc;;YAC/D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE;gBACxB,IAAI;oBACF,MAAM,aAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,cAAS,CAAC,IAAI,CAAC,CAAC;oBAC5C,MAAM,QAAQ,GAAG,cAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;oBAChD,MAAM,SAAS,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;oBACzC,IAAI,SAAS,KAAK,MAAM,EAAE;wBACxB,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAA,qBAAgB,EAAC,IAAI,CAAC,CAAC,CAAC;qBAC/C;yBAAM;wBACL,MAAM,IAAI,KAAK,CAAC,GAAG,SAAS,mBAAmB,CAAC,CAAC;qBAClD;iBACF;gBAAC,OAAO,KAAK,EAAE;oBACd,MAAM,KAAK,CAAC;iBACb;aACF;QACH,CAAC;KAAA;CACF;AAjBD,wCAiBC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chromiumly",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "A lightweight
|
|
3
|
+
"version": "2.0.4",
|
|
4
|
+
"description": "A lightweight Typescript library that interacts with Gotenberg's different modules to convert a variety of document formats to PDF files.",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"types": "dist/main.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -28,19 +28,25 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"clean": "rm -rf dist build",
|
|
30
30
|
"lint": "eslint src/ --ext .js,.ts",
|
|
31
|
-
"build": "yarn clean && tsc -p tsconfig.json"
|
|
31
|
+
"build": "yarn clean && tsc -p tsconfig.json",
|
|
32
|
+
"test": "jest --runInBand --ci --coverage --reporters=default --reporters=jest-junit"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
35
|
+
"@babel/preset-typescript": "^7.16.7",
|
|
34
36
|
"@types/config": "^0.0.41",
|
|
35
37
|
"@types/dotenv": "^8.2.0",
|
|
36
38
|
"@types/form-data": "^2.5.0",
|
|
39
|
+
"@types/jest": "^27.4.1",
|
|
37
40
|
"@types/node": "^17.0.22",
|
|
38
41
|
"@types/node-fetch": "2",
|
|
39
42
|
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
|
40
43
|
"@typescript-eslint/parser": "^5.16.0",
|
|
41
44
|
"eslint": "^8.11.0",
|
|
45
|
+
"jest": "^27.5.1",
|
|
46
|
+
"jest-junit": "^13.0.0",
|
|
47
|
+
"ts-jest": "^27.1.4",
|
|
42
48
|
"ts-node": "^10.7.0",
|
|
43
|
-
"typescript": "
|
|
49
|
+
"typescript": "4.6.3"
|
|
44
50
|
},
|
|
45
51
|
"dependencies": {
|
|
46
52
|
"config": "^3.3.7",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { promises, createReadStream } from "fs";
|
|
2
|
+
|
|
3
|
+
import fetch from "node-fetch";
|
|
4
|
+
import FormData from "form-data";
|
|
5
|
+
|
|
6
|
+
import { PdfFormat } from "../../../common";
|
|
7
|
+
import { HtmlConverter } from "../html.converter";
|
|
8
|
+
|
|
9
|
+
const { Response } = jest.requireActual("node-fetch");
|
|
10
|
+
jest.mock("node-fetch", () => jest.fn());
|
|
11
|
+
|
|
12
|
+
describe("HtmlConverter", () => {
|
|
13
|
+
const mockPromisesAccess = jest.spyOn(promises, "access");
|
|
14
|
+
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
|
|
15
|
+
const mockFormDataAppend = jest.spyOn(FormData.prototype, "append");
|
|
16
|
+
|
|
17
|
+
const converter = new HtmlConverter();
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
(createReadStream as jest.Mock) = jest
|
|
20
|
+
.fn()
|
|
21
|
+
.mockImplementation((file) => file);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
jest.resetAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("endpoint", () => {
|
|
29
|
+
it("should route to Chromium HTML route", () => {
|
|
30
|
+
expect(converter.endpoint).toEqual(
|
|
31
|
+
"http://localhost:3000/forms/chromium/convert/html"
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("convert", () => {
|
|
37
|
+
describe("when file exists", () => {
|
|
38
|
+
it("should return a buffer", async () => {
|
|
39
|
+
mockPromisesAccess.mockResolvedValue();
|
|
40
|
+
mockFetch.mockResolvedValue(new Response("content"));
|
|
41
|
+
const buffer = await converter.convert({ html: "path/to/index.html" });
|
|
42
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("when pdf format parameter is passed", () => {
|
|
47
|
+
it("should return a buffer", async () => {
|
|
48
|
+
mockPromisesAccess.mockResolvedValue();
|
|
49
|
+
mockFetch.mockResolvedValue(new Response("content"));
|
|
50
|
+
const buffer = await converter.convert({
|
|
51
|
+
html: "path/to/index.html",
|
|
52
|
+
pdfFormat: PdfFormat.A_1a,
|
|
53
|
+
});
|
|
54
|
+
expect(mockFormDataAppend).toBeCalledTimes(2);
|
|
55
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("when page properties parameter is passed", () => {
|
|
60
|
+
it("should return a buffer", async () => {
|
|
61
|
+
mockPromisesAccess.mockResolvedValue();
|
|
62
|
+
mockFetch.mockResolvedValue(new Response("content"));
|
|
63
|
+
const buffer = await converter.convert({
|
|
64
|
+
html: "path/to/index.html",
|
|
65
|
+
properties: { size: { width: 8.3, height: 11.7 } },
|
|
66
|
+
});
|
|
67
|
+
expect(mockFormDataAppend).toBeCalledTimes(3);
|
|
68
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("when file does not exist", () => {
|
|
73
|
+
it("should throw an error", async () => {
|
|
74
|
+
const errorMessage =
|
|
75
|
+
"ENOENT: no such file or directory, access 'path/to/index.html'";
|
|
76
|
+
mockPromisesAccess.mockRejectedValue(new Error(errorMessage));
|
|
77
|
+
await expect(() =>
|
|
78
|
+
converter.convert({ html: "path/to/index.html" })
|
|
79
|
+
).rejects.toThrow(errorMessage);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("when fetch request fails", () => {
|
|
84
|
+
it("should throw an error", async () => {
|
|
85
|
+
const errorMessage =
|
|
86
|
+
"FetchError: request to http://localhost:3000/forms/chromium/convert/html failed";
|
|
87
|
+
mockPromisesAccess.mockResolvedValue();
|
|
88
|
+
mockFetch.mockRejectedValue(new Error(errorMessage));
|
|
89
|
+
await expect(() =>
|
|
90
|
+
converter.convert({ html: "path/to/index.html" })
|
|
91
|
+
).rejects.toThrow(errorMessage);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createReadStream, promises } from "fs";
|
|
2
|
+
|
|
3
|
+
import fetch from "node-fetch";
|
|
4
|
+
import FormData from "form-data";
|
|
5
|
+
|
|
6
|
+
import { PdfFormat } from "../../../common";
|
|
7
|
+
import { MarkdownConverter } from "../markdown.converter";
|
|
8
|
+
|
|
9
|
+
const { Response } = jest.requireActual("node-fetch");
|
|
10
|
+
jest.mock("node-fetch", () => jest.fn());
|
|
11
|
+
|
|
12
|
+
describe("MarkdownConverter", () => {
|
|
13
|
+
const mockPromisesAccess = jest.spyOn(promises, "access");
|
|
14
|
+
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
|
|
15
|
+
const mockFormDataAppend = jest.spyOn(FormData.prototype, "append");
|
|
16
|
+
|
|
17
|
+
const converter = new MarkdownConverter();
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.resetAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("endpoint", () => {
|
|
23
|
+
it("should route to Chromium Markdown route", () => {
|
|
24
|
+
expect(converter.endpoint).toEqual(
|
|
25
|
+
"http://localhost:3000/forms/chromium/convert/markdown"
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("convert", () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
(createReadStream as jest.Mock) = jest.fn().mockImplementation(() => {});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("when file exists", () => {
|
|
36
|
+
it("should return a buffer", async () => {
|
|
37
|
+
mockPromisesAccess.mockResolvedValue();
|
|
38
|
+
mockFetch.mockResolvedValue(new Response("content"));
|
|
39
|
+
const buffer = await converter.convert({
|
|
40
|
+
html: "path/to/index.html",
|
|
41
|
+
markdown: "path/to/file.md",
|
|
42
|
+
});
|
|
43
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("when pdf format parameter is passed", () => {
|
|
48
|
+
it("should return a buffer", async () => {
|
|
49
|
+
mockPromisesAccess.mockResolvedValue();
|
|
50
|
+
mockFetch.mockResolvedValue(new Response("content"));
|
|
51
|
+
const buffer = await converter.convert({
|
|
52
|
+
html: "path/to/index.html",
|
|
53
|
+
markdown: "path/to/file.md",
|
|
54
|
+
pdfFormat: PdfFormat.A_2b,
|
|
55
|
+
});
|
|
56
|
+
expect(mockFormDataAppend).toBeCalledTimes(3);
|
|
57
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("when page properties parameter is passed", () => {
|
|
62
|
+
it("should return a buffer", async () => {
|
|
63
|
+
mockPromisesAccess.mockResolvedValue();
|
|
64
|
+
mockFetch.mockResolvedValue(new Response("content"));
|
|
65
|
+
const buffer = await converter.convert({
|
|
66
|
+
html: "path/to/index.html",
|
|
67
|
+
markdown: "path/to/file.md",
|
|
68
|
+
properties: { size: { width: 8.3, height: 11.7 } },
|
|
69
|
+
});
|
|
70
|
+
expect(mockFormDataAppend).toBeCalledTimes(4);
|
|
71
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("when file does not exist", () => {
|
|
76
|
+
it("should throw an error", async () => {
|
|
77
|
+
const errorMessage =
|
|
78
|
+
"ENOENT: no such file or directory, access 'path/to/index.html'";
|
|
79
|
+
mockPromisesAccess.mockRejectedValue(new Error(errorMessage));
|
|
80
|
+
|
|
81
|
+
await expect(() =>
|
|
82
|
+
converter.convert({
|
|
83
|
+
html: "path/to/index.html",
|
|
84
|
+
markdown: "path/to/file.md",
|
|
85
|
+
})
|
|
86
|
+
).rejects.toThrow(errorMessage);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("when fetch request fails", () => {
|
|
91
|
+
it("should throw an error", async () => {
|
|
92
|
+
const errorMessage =
|
|
93
|
+
"FetchError: request to http://localhost:3000/forms/chromium/convert/html failed";
|
|
94
|
+
mockPromisesAccess.mockResolvedValue();
|
|
95
|
+
mockFetch.mockRejectedValue(new Error(errorMessage));
|
|
96
|
+
await expect(() =>
|
|
97
|
+
converter.convert({
|
|
98
|
+
html: "path/to/index.html",
|
|
99
|
+
markdown: "path/to/file.md",
|
|
100
|
+
})
|
|
101
|
+
).rejects.toThrow(errorMessage);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
import FormData from "form-data";
|
|
3
|
+
|
|
4
|
+
import { PdfFormat } from "../../../common";
|
|
5
|
+
import { UrlConverter } from "../url.converter";
|
|
6
|
+
|
|
7
|
+
const { Response } = jest.requireActual("node-fetch");
|
|
8
|
+
jest.mock("node-fetch", () => jest.fn());
|
|
9
|
+
|
|
10
|
+
describe("HtmlConverter", () => {
|
|
11
|
+
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
|
|
12
|
+
const mockFormDataAppend = jest.spyOn(FormData.prototype, "append");
|
|
13
|
+
|
|
14
|
+
const converter = new UrlConverter();
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
jest.resetAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("endpoint", () => {
|
|
21
|
+
it("should route to Chromium HTML route", () => {
|
|
22
|
+
expect(converter.endpoint).toEqual(
|
|
23
|
+
"http://localhost:3000/forms/chromium/convert/url"
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("convert", () => {
|
|
29
|
+
describe("when URL is valid", () => {
|
|
30
|
+
it("should return a buffer", async () => {
|
|
31
|
+
mockFetch.mockResolvedValueOnce(new Response("content"));
|
|
32
|
+
const buffer = await converter.convert({
|
|
33
|
+
url: "http://www.example.com/",
|
|
34
|
+
});
|
|
35
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("when pdf format parameter is passed", () => {
|
|
40
|
+
it("should return a buffer", async () => {
|
|
41
|
+
mockFetch.mockResolvedValueOnce(new Response("content"));
|
|
42
|
+
const buffer = await converter.convert({
|
|
43
|
+
url: "http://www.example.com/",
|
|
44
|
+
pdfFormat: PdfFormat.A_3b,
|
|
45
|
+
});
|
|
46
|
+
expect(mockFormDataAppend).toBeCalledTimes(2);
|
|
47
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("when page properties parameter is passed", () => {
|
|
52
|
+
it("should return a buffer", async () => {
|
|
53
|
+
mockFetch.mockResolvedValueOnce(new Response("content"));
|
|
54
|
+
const buffer = await converter.convert({
|
|
55
|
+
url: "http://www.example.com/",
|
|
56
|
+
properties: { size: { width: 8.3, height: 11.7 } },
|
|
57
|
+
});
|
|
58
|
+
expect(mockFormDataAppend).toBeCalledTimes(3);
|
|
59
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("when URL is invalid", () => {
|
|
64
|
+
it("should throw an error", async () => {
|
|
65
|
+
await expect(() =>
|
|
66
|
+
converter.convert({ url: "invalid url" })
|
|
67
|
+
).rejects.toThrow("Invalid URL: invalid url");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("when fetch request fails", () => {
|
|
72
|
+
it("should throw an error", async () => {
|
|
73
|
+
const errorMessage =
|
|
74
|
+
"FetchError: request to http://localhost:3000/forms/chromium/convert/html failed";
|
|
75
|
+
mockFetch.mockRejectedValueOnce(new Error(errorMessage));
|
|
76
|
+
await expect(() =>
|
|
77
|
+
converter.convert({ url: "http://www.example.com/" })
|
|
78
|
+
).rejects.toThrow(errorMessage);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import FormData from "form-data";
|
|
2
|
+
import { ConverterUtils } from "../converter.utils";
|
|
3
|
+
|
|
4
|
+
describe("ConverterUtils", () => {
|
|
5
|
+
const mockFormDataAppend = jest.spyOn(FormData.prototype, "append");
|
|
6
|
+
const data = new FormData();
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
jest.resetAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("injectPageProperties", () => {
|
|
13
|
+
describe("Page size", () => {
|
|
14
|
+
describe("when page size is valid", () => {
|
|
15
|
+
it("should append page size to data", () => {
|
|
16
|
+
ConverterUtils.injectPageProperties(data, {
|
|
17
|
+
size: { width: 8.3, height: 11.7 },
|
|
18
|
+
});
|
|
19
|
+
expect(mockFormDataAppend).toBeCalledTimes(2);
|
|
20
|
+
expect(data.append).toHaveBeenCalledWith("paperWidth", 8.3);
|
|
21
|
+
expect(data.append).toHaveBeenNthCalledWith(2, "paperHeight", 11.7);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("Page margins", () => {
|
|
27
|
+
describe("when page margins are valid", () => {
|
|
28
|
+
it("should append page margins to data", () => {
|
|
29
|
+
ConverterUtils.injectPageProperties(data, {
|
|
30
|
+
margins: { top: 0.5, bottom: 0.5, left: 1, right: 1 },
|
|
31
|
+
});
|
|
32
|
+
expect(mockFormDataAppend).toBeCalledTimes(4);
|
|
33
|
+
expect(data.append).toHaveBeenCalledWith("marginTop", 0.5);
|
|
34
|
+
expect(data.append).toHaveBeenNthCalledWith(2, "marginBottom", 0.5);
|
|
35
|
+
expect(data.append).toHaveBeenNthCalledWith(3, "marginLeft", 1);
|
|
36
|
+
expect(data.append).toHaveBeenNthCalledWith(4, "marginRight", 1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("Page css size", () => {
|
|
42
|
+
describe("when preferCssPageSize parameter is set", () => {
|
|
43
|
+
it("should append preferCssPageSize to data", () => {
|
|
44
|
+
ConverterUtils.injectPageProperties(data, {
|
|
45
|
+
preferCssPageSize: true,
|
|
46
|
+
});
|
|
47
|
+
expect(mockFormDataAppend).toBeCalledTimes(1);
|
|
48
|
+
expect(data.append).toHaveBeenCalledWith("preferCssPageSize", "true");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("Page background", () => {
|
|
54
|
+
describe("when printBackground parameter is set", () => {
|
|
55
|
+
it("should append printBackground to data", () => {
|
|
56
|
+
ConverterUtils.injectPageProperties(data, {
|
|
57
|
+
printBackground: true,
|
|
58
|
+
});
|
|
59
|
+
expect(mockFormDataAppend).toBeCalledTimes(1);
|
|
60
|
+
expect(data.append).toHaveBeenCalledWith("printBackground", "true");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("Page landscape", () => {
|
|
66
|
+
describe("when landscape parameter is set", () => {
|
|
67
|
+
it("should append landscape to data", () => {
|
|
68
|
+
ConverterUtils.injectPageProperties(data, {
|
|
69
|
+
landscape: true,
|
|
70
|
+
});
|
|
71
|
+
expect(mockFormDataAppend).toBeCalledTimes(1);
|
|
72
|
+
expect(data.append).toHaveBeenCalledWith("landscape", "true");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("Page scale", () => {
|
|
78
|
+
describe("when page scale is valid", () => {
|
|
79
|
+
it("should append scale to data", () => {
|
|
80
|
+
ConverterUtils.injectPageProperties(data, {
|
|
81
|
+
scale: 1.5,
|
|
82
|
+
});
|
|
83
|
+
expect(mockFormDataAppend).toBeCalledTimes(1);
|
|
84
|
+
expect(data.append).toHaveBeenCalledWith("scale", 1.5);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("Page ranges", () => {
|
|
90
|
+
describe("when nativePageRanges is valid", () => {
|
|
91
|
+
it("should append nativePageRanges to data", () => {
|
|
92
|
+
ConverterUtils.injectPageProperties(data, {
|
|
93
|
+
nativePageRanges: { from: 1, to: 6 },
|
|
94
|
+
});
|
|
95
|
+
expect(mockFormDataAppend).toBeCalledTimes(1);
|
|
96
|
+
expect(data.append).toHaveBeenCalledWith("nativePageRanges", "1-6");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
import FormData from "form-data";
|
|
3
|
+
|
|
4
|
+
import { GotenbergUtils } from "./../gotenberg.utils";
|
|
5
|
+
|
|
6
|
+
const { Response, FetchError } = jest.requireActual("node-fetch");
|
|
7
|
+
|
|
8
|
+
jest.mock("node-fetch", () => jest.fn());
|
|
9
|
+
|
|
10
|
+
describe("GotenbergUtils", () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
jest.resetAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("assert", () => {
|
|
16
|
+
const errorMessage = "error message";
|
|
17
|
+
describe("when condition is true", () => {
|
|
18
|
+
it("should pass", () => {
|
|
19
|
+
expect(() => GotenbergUtils.assert(true, errorMessage)).not.toThrow();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("when condition is false", () => {
|
|
23
|
+
it("should return error message", () => {
|
|
24
|
+
expect(() => GotenbergUtils.assert(false, errorMessage)).toThrow(
|
|
25
|
+
errorMessage
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("fetch", () => {
|
|
32
|
+
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
|
|
33
|
+
const data = new FormData();
|
|
34
|
+
const endpoint = "http://localhost:3000/forms/chromium/convert/html";
|
|
35
|
+
|
|
36
|
+
describe("when fetch request succeeds", () => {
|
|
37
|
+
it("should return a buffer", async () => {
|
|
38
|
+
mockFetch.mockResolvedValueOnce(new Response("content"));
|
|
39
|
+
const response = await GotenbergUtils.fetch(endpoint, data);
|
|
40
|
+
await expect(response).toEqual(Buffer.from("content"));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("when fetch request fails", () => {
|
|
45
|
+
describe("when there is a known error", () => {
|
|
46
|
+
it("should throw an error", async () => {
|
|
47
|
+
const errorMessage =
|
|
48
|
+
"FetchError: request to http://localhost:3000/forms/chromium/convert/html failed";
|
|
49
|
+
mockFetch.mockRejectedValueOnce(new FetchError(errorMessage));
|
|
50
|
+
await expect(() =>
|
|
51
|
+
GotenbergUtils.fetch(endpoint, data)
|
|
52
|
+
).rejects.toThrow(errorMessage);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("when there is an unknown error", () => {
|
|
57
|
+
it("should throw an error", async () => {
|
|
58
|
+
mockFetch.mockResolvedValueOnce(
|
|
59
|
+
new Response(
|
|
60
|
+
{},
|
|
61
|
+
{ status: 500, statusText: "Internal server error" }
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
await expect(() =>
|
|
65
|
+
GotenbergUtils.fetch(endpoint, data)
|
|
66
|
+
).rejects.toThrow("500 Internal server error");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { LibreOfficeUtils } from "../libre-office.utils";
|
|
2
|
+
import { promises, createReadStream } from "fs";
|
|
3
|
+
|
|
4
|
+
import FormData from "form-data";
|
|
5
|
+
|
|
6
|
+
describe("LibreOfficeUtils", () => {
|
|
7
|
+
const mockPromisesAccess = jest.spyOn(promises, "access");
|
|
8
|
+
const mockFormDataAppend = jest.spyOn(FormData.prototype, "append");
|
|
9
|
+
const data = new FormData();
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
(createReadStream as jest.Mock) = jest
|
|
13
|
+
.fn()
|
|
14
|
+
.mockImplementation((file) => file);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
jest.resetAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("injectFiles", () => {
|
|
22
|
+
describe("when files exist", () => {
|
|
23
|
+
it("should append each file to data", async () => {
|
|
24
|
+
mockPromisesAccess.mockResolvedValue();
|
|
25
|
+
await LibreOfficeUtils.injectFiles(
|
|
26
|
+
["path/to/file.docx", "path/to/file.bib"],
|
|
27
|
+
data
|
|
28
|
+
);
|
|
29
|
+
expect(mockFormDataAppend).toBeCalledTimes(2);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("when one of the files has unsupported format", () => {
|
|
34
|
+
it("should throw an error", async () => {
|
|
35
|
+
mockPromisesAccess.mockResolvedValue();
|
|
36
|
+
await expect(() =>
|
|
37
|
+
LibreOfficeUtils.injectFiles(
|
|
38
|
+
["path/to/file.rar", "path/to/file.pdf"],
|
|
39
|
+
data
|
|
40
|
+
)
|
|
41
|
+
).rejects.toThrow(".rar is not supported");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("when one of the files does not exist", () => {
|
|
46
|
+
it("should throw an error", async () => {
|
|
47
|
+
const errorMessage =
|
|
48
|
+
"ENOENT: no such file or directory, access 'path/to/index.html'";
|
|
49
|
+
mockPromisesAccess.mockRejectedValue(new Error(errorMessage));
|
|
50
|
+
await expect(() =>
|
|
51
|
+
LibreOfficeUtils.injectFiles(
|
|
52
|
+
["path/to/file.pdf", "path/to/another-file.pdf"],
|
|
53
|
+
data
|
|
54
|
+
)
|
|
55
|
+
).rejects.toThrow(errorMessage);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("injectPageProperties", () => {
|
|
61
|
+
describe("Page landscape", () => {
|
|
62
|
+
describe("when landscape parameter is set", () => {
|
|
63
|
+
it("should append landscape to data", () => {
|
|
64
|
+
LibreOfficeUtils.injectPageProperties(data, {
|
|
65
|
+
landscape: true,
|
|
66
|
+
});
|
|
67
|
+
expect(mockFormDataAppend).toBeCalledTimes(1);
|
|
68
|
+
expect(data.append).toHaveBeenCalledWith("landscape", "true");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("Page ranges", () => {
|
|
74
|
+
describe("when nativePageRanges is valid", () => {
|
|
75
|
+
it("should append nativePageRanges to data", () => {
|
|
76
|
+
LibreOfficeUtils.injectPageProperties(data, {
|
|
77
|
+
nativePageRanges: { from: 1, to: 6 },
|
|
78
|
+
});
|
|
79
|
+
expect(mockFormDataAppend).toBeCalledTimes(1);
|
|
80
|
+
expect(data.append).toHaveBeenCalledWith("nativePageRanges", "1-6");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
import { PDFEngine } from "./../pdf.engine";
|
|
4
|
+
import { PdfFormat } from "../../common";
|
|
5
|
+
|
|
6
|
+
import { promises, createReadStream } from "fs";
|
|
7
|
+
|
|
8
|
+
import fetch from "node-fetch";
|
|
9
|
+
import FormData from "form-data";
|
|
10
|
+
|
|
11
|
+
const { Response } = jest.requireActual("node-fetch");
|
|
12
|
+
jest.mock("node-fetch", () => jest.fn());
|
|
13
|
+
|
|
14
|
+
describe("PDFEngine", () => {
|
|
15
|
+
const mockProcessCwd = jest.spyOn(process, "cwd");
|
|
16
|
+
const mockPromisesAccess = jest.spyOn(promises, "access");
|
|
17
|
+
const mockPromisesMkDir = jest.spyOn(promises, "mkdir");
|
|
18
|
+
const mockPromisesWriteFile = jest.spyOn(promises, "writeFile");
|
|
19
|
+
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
|
|
20
|
+
const mockFormDataAppend = jest.spyOn(FormData.prototype, "append");
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
(createReadStream as jest.Mock) = jest
|
|
24
|
+
.fn()
|
|
25
|
+
.mockImplementation((file) => file);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
jest.resetAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("convert", () => {
|
|
33
|
+
describe("when no properties are passed", () => {
|
|
34
|
+
it("should return a buffer", async () => {
|
|
35
|
+
mockPromisesAccess.mockResolvedValue();
|
|
36
|
+
mockFetch.mockResolvedValue(new Response("content"));
|
|
37
|
+
const buffer = await PDFEngine.convert({
|
|
38
|
+
files: ["path/to/file.docx", "path/to/file.bib"],
|
|
39
|
+
});
|
|
40
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
41
|
+
expect(mockFormDataAppend).toBeCalledTimes(2);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("when properties are passed", () => {
|
|
46
|
+
it("should return a buffer", async () => {
|
|
47
|
+
mockPromisesAccess.mockResolvedValue();
|
|
48
|
+
mockFetch.mockResolvedValue(new Response("content"));
|
|
49
|
+
const buffer = await PDFEngine.convert({
|
|
50
|
+
files: ["path/to/file.docx", "path/to/file.bib"],
|
|
51
|
+
properties: { landscape: true },
|
|
52
|
+
pdfFormat: PdfFormat.A_1a,
|
|
53
|
+
merge: true,
|
|
54
|
+
});
|
|
55
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
56
|
+
expect(mockFormDataAppend).toBeCalledTimes(5);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("merge", () => {
|
|
62
|
+
it("should return a buffer", async () => {
|
|
63
|
+
mockPromisesAccess.mockResolvedValue();
|
|
64
|
+
mockFetch.mockResolvedValue(new Response("content"));
|
|
65
|
+
const buffer = await PDFEngine.merge({
|
|
66
|
+
files: ["path/to/file.pdf", "path/to/another-file.pdf"],
|
|
67
|
+
});
|
|
68
|
+
expect(buffer).toEqual(Buffer.from("content"));
|
|
69
|
+
expect(mockFormDataAppend).toBeCalledTimes(2);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("generate", () => {
|
|
74
|
+
it("should generate a PDF file", async () => {
|
|
75
|
+
mockProcessCwd.mockReturnValue("path/to/");
|
|
76
|
+
mockPromisesMkDir.mockResolvedValue("__generated__");
|
|
77
|
+
await PDFEngine.generate("file.pdf", Buffer.from("content"));
|
|
78
|
+
expect(mockPromisesWriteFile).toBeCalledWith(
|
|
79
|
+
path.resolve("path/to/__generated__/file.pdf"),
|
|
80
|
+
Buffer.from("content")
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -9,7 +9,12 @@ export class PDFEngineUtils {
|
|
|
9
9
|
try {
|
|
10
10
|
await promises.access(file, constants.R_OK);
|
|
11
11
|
const filename = path.basename(file.toString());
|
|
12
|
-
|
|
12
|
+
const extension = path.extname(filename);
|
|
13
|
+
if (extension === ".pdf") {
|
|
14
|
+
data.append(filename, createReadStream(file));
|
|
15
|
+
} else {
|
|
16
|
+
throw new Error(`${extension} is not supported`);
|
|
17
|
+
}
|
|
13
18
|
} catch (error) {
|
|
14
19
|
throw error;
|
|
15
20
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PDFEngineUtils } from "./../engine.utils";
|
|
2
|
+
import { promises, createReadStream } from "fs";
|
|
3
|
+
|
|
4
|
+
import FormData from "form-data";
|
|
5
|
+
|
|
6
|
+
describe("PDFEngineUtils", () => {
|
|
7
|
+
const mockPromisesAccess = jest.spyOn(promises, "access");
|
|
8
|
+
const mockFormDataAppend = jest.spyOn(FormData.prototype, "append");
|
|
9
|
+
const data = new FormData();
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
(createReadStream as jest.Mock) = jest
|
|
13
|
+
.fn()
|
|
14
|
+
.mockImplementation((file) => file);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
jest.resetAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("injectFiles", () => {
|
|
22
|
+
describe("when files exist", () => {
|
|
23
|
+
it("should append each file to data", async () => {
|
|
24
|
+
mockPromisesAccess.mockResolvedValue();
|
|
25
|
+
await PDFEngineUtils.injectFiles(
|
|
26
|
+
["path/to/file.pdf", "path/to/another-file.pdf"],
|
|
27
|
+
data
|
|
28
|
+
);
|
|
29
|
+
expect(mockFormDataAppend).toBeCalledTimes(2);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("when one of the files is not PDF", () => {
|
|
34
|
+
it("should throw an error", async () => {
|
|
35
|
+
mockPromisesAccess.mockResolvedValue();
|
|
36
|
+
await expect(() =>
|
|
37
|
+
PDFEngineUtils.injectFiles(
|
|
38
|
+
["path/to/file.docx", "path/to/file.pdf"],
|
|
39
|
+
data
|
|
40
|
+
)
|
|
41
|
+
).rejects.toThrow(".docx is not supported");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("when one of the files does not exist", () => {
|
|
46
|
+
it("should throw an error", async () => {
|
|
47
|
+
const errorMessage =
|
|
48
|
+
"ENOENT: no such file or directory, access 'path/to/index.html'";
|
|
49
|
+
mockPromisesAccess.mockRejectedValue(new Error(errorMessage));
|
|
50
|
+
await expect(() =>
|
|
51
|
+
PDFEngineUtils.injectFiles(
|
|
52
|
+
["path/to/file.pdf", "path/to/another-file.pdf"],
|
|
53
|
+
data
|
|
54
|
+
)
|
|
55
|
+
).rejects.toThrow(errorMessage);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|