@thegetty/quire-cli 1.0.0-rc.35 → 1.0.0-rc.36

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.
Files changed (158) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -0
  3. package/bin/cli.js +19 -1
  4. package/package.json +21 -8
  5. package/patches/README.md +19 -0
  6. package/patches/install-npm-version+1.0.9.patch +12119 -0
  7. package/schemas/objects.schema.json +12 -38
  8. package/schemas/publication.schema.json +0 -22
  9. package/schemas/references.schema.json +0 -1
  10. package/src/Command.js +26 -6
  11. package/src/Command.spec.js +99 -0
  12. package/src/commands/README.md +213 -122
  13. package/src/commands/build.js +46 -37
  14. package/src/commands/build.spec.js +113 -0
  15. package/src/commands/build.test.js +402 -0
  16. package/src/commands/clean.js +25 -19
  17. package/src/commands/clean.spec.js +108 -0
  18. package/src/commands/clean.test.js +260 -0
  19. package/src/commands/config.js +251 -0
  20. package/src/commands/config.spec.js +107 -0
  21. package/src/commands/config.test.js +715 -0
  22. package/src/commands/create.js +42 -14
  23. package/src/commands/create.spec.js +112 -0
  24. package/src/commands/create.test.js +415 -0
  25. package/src/commands/epub.js +59 -30
  26. package/src/commands/epub.spec.js +114 -0
  27. package/src/commands/epub.test.js +503 -0
  28. package/src/commands/index.js +9 -2
  29. package/src/commands/info.js +18 -24
  30. package/src/commands/info.spec.js +64 -0
  31. package/src/commands/info.test.js +415 -0
  32. package/src/commands/pdf.js +58 -84
  33. package/src/commands/pdf.spec.js +114 -0
  34. package/src/commands/pdf.test.js +464 -0
  35. package/src/commands/preview.js +26 -31
  36. package/src/commands/preview.spec.js +97 -0
  37. package/src/commands/preview.test.js +250 -0
  38. package/src/commands/use.js +56 -0
  39. package/src/commands/use.spec.js +61 -0
  40. package/src/commands/use.test.js +280 -0
  41. package/src/commands/validate.js +31 -19
  42. package/src/commands/validate.spec.js +82 -0
  43. package/src/commands/validate.test.js +234 -0
  44. package/src/commands/workflows.js +70 -0
  45. package/src/errors/build/build-failed-error.js +19 -0
  46. package/src/errors/build/config-field-missing-error.js +20 -0
  47. package/src/errors/build/config-file-not-found-error.js +20 -0
  48. package/src/errors/build/index.js +11 -0
  49. package/src/errors/index.js +48 -0
  50. package/src/errors/install/dependency-install-error.js +19 -0
  51. package/src/errors/install/directory-not-empty-error.js +25 -0
  52. package/src/errors/install/index.js +12 -0
  53. package/src/errors/install/invalid-path-error.js +31 -0
  54. package/src/errors/install/invalid-starter-error.js +27 -0
  55. package/src/errors/install/version-not-found-error.js +35 -0
  56. package/src/errors/output/epub-generation-error.js +19 -0
  57. package/src/errors/output/index.js +14 -0
  58. package/src/errors/output/invalid-epub-library-error.js +20 -0
  59. package/src/errors/output/invalid-pdf-library-error.js +20 -0
  60. package/src/errors/output/missing-build-output-error.js +21 -0
  61. package/src/errors/output/pdf-generation-error.js +24 -0
  62. package/src/errors/output/tool-not-found-error.js +37 -0
  63. package/src/errors/project/index.js +10 -0
  64. package/src/errors/project/not-in-project-error.js +20 -0
  65. package/src/errors/project/project-create-error.js +21 -0
  66. package/src/errors/quire-error.js +27 -0
  67. package/src/errors/validation/validation-error.js +20 -11
  68. package/src/helpers/clean.js +1 -1
  69. package/src/helpers/docs-url.js +32 -0
  70. package/src/helpers/test-cwd.js +5 -6
  71. package/src/helpers/test-cwd.test.js +192 -0
  72. package/src/helpers/which.js +10 -4
  73. package/src/lib/11ty/README.md +135 -19
  74. package/src/lib/11ty/api.js +176 -93
  75. package/src/lib/11ty/cli.js +77 -35
  76. package/src/lib/11ty/index.js +64 -5
  77. package/src/lib/11ty/index.test.js +655 -0
  78. package/src/lib/README.md +275 -0
  79. package/src/lib/commander/index.js +100 -0
  80. package/src/lib/commander/index.test.js +86 -0
  81. package/src/lib/commander/options.js +195 -0
  82. package/src/lib/commander/options.test.js +109 -0
  83. package/src/lib/conf/README.md +84 -73
  84. package/src/lib/conf/config.js +5 -3
  85. package/src/lib/conf/config.test.js +281 -0
  86. package/src/lib/conf/defaults.js +44 -0
  87. package/src/lib/conf/format.js +60 -0
  88. package/src/lib/conf/format.test.js +106 -0
  89. package/src/lib/conf/helpers.js +91 -0
  90. package/src/lib/conf/helpers.test.js +136 -0
  91. package/src/lib/conf/index.js +22 -0
  92. package/src/lib/conf/schema.js +53 -8
  93. package/src/lib/epub/README.md +133 -2
  94. package/src/lib/epub/engines.js +46 -0
  95. package/src/lib/epub/epub.js +36 -11
  96. package/src/lib/epub/index.js +111 -21
  97. package/src/lib/epub/index.test.js +518 -0
  98. package/src/lib/epub/pandoc.js +33 -4
  99. package/src/lib/epub/pandoc.test.js +122 -0
  100. package/src/lib/epub/schema.js +21 -0
  101. package/src/lib/error/README.md +170 -0
  102. package/src/lib/error/handler.js +107 -0
  103. package/src/lib/git/README.md +151 -2
  104. package/src/lib/git/index.js +217 -13
  105. package/src/lib/git/index.spec.js +80 -0
  106. package/src/lib/git/index.test.js +453 -0
  107. package/src/lib/installer/index.js +309 -0
  108. package/src/lib/installer/index.spec.js +83 -0
  109. package/src/lib/installer/index.test.js +545 -0
  110. package/src/lib/logger/README.md +424 -0
  111. package/src/lib/logger/debug.js +90 -0
  112. package/src/lib/logger/debug.spec.js +130 -0
  113. package/src/lib/logger/index.js +228 -0
  114. package/src/lib/logger/index.spec.js +131 -0
  115. package/src/lib/logger/index.test.js +477 -0
  116. package/src/lib/npm/README.md +127 -0
  117. package/src/lib/npm/index.js +198 -0
  118. package/src/lib/npm/index.spec.js +60 -0
  119. package/src/lib/npm/index.test.js +355 -0
  120. package/src/lib/pdf/README.md +131 -0
  121. package/src/lib/pdf/engines.js +46 -0
  122. package/src/lib/pdf/index.js +124 -21
  123. package/src/lib/pdf/index.test.js +708 -0
  124. package/src/lib/pdf/paged.js +100 -52
  125. package/src/lib/pdf/paged.test.js +366 -0
  126. package/src/lib/pdf/prince.js +115 -37
  127. package/src/lib/pdf/prince.test.js +202 -0
  128. package/src/lib/pdf/schema.js +21 -0
  129. package/src/lib/pdf/split.js +61 -33
  130. package/src/lib/pdf/split.test.js +445 -0
  131. package/src/lib/process/manager.js +110 -0
  132. package/src/lib/process/manager.test.js +55 -0
  133. package/src/lib/project/build.js +143 -0
  134. package/src/lib/project/build.test.js +253 -0
  135. package/src/lib/project/config.js +48 -0
  136. package/src/lib/project/config.test.js +134 -0
  137. package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
  138. package/src/lib/project/detect.test.js +157 -0
  139. package/src/lib/project/index.js +40 -0
  140. package/src/lib/project/paths.js +224 -0
  141. package/src/lib/project/version.js +110 -0
  142. package/src/lib/project/version.test.js +350 -0
  143. package/src/lib/reporter/README.md +211 -2
  144. package/src/lib/reporter/index.js +512 -0
  145. package/src/lib/reporter/index.test.js +593 -0
  146. package/src/main.js +134 -47
  147. package/src/main.spec.js +61 -0
  148. package/src/main.test.js +347 -0
  149. package/src/validators/utils.js +2 -1
  150. package/src/commands/conf.js +0 -43
  151. package/src/commands/version.js +0 -43
  152. package/src/lib/11ty/paths.js +0 -103
  153. package/src/lib/i18n/README.md +0 -3
  154. package/src/lib/i18n/config.js +0 -53
  155. package/src/lib/i18n/index.js +0 -43
  156. package/src/lib/i18n/localeService.js +0 -58
  157. package/src/lib/quire/README.md +0 -19
  158. package/src/lib/quire/index.js +0 -350
@@ -4,25 +4,39 @@ import fs from 'fs-extra'
4
4
  import path from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
6
 
7
+ import processManager from '#lib/process/manager.js'
8
+ import reporter from '#lib/reporter/index.js'
7
9
  import { splitPdf } from './split.js'
10
+ import { PdfGenerationError } from '#src/errors/index.js'
11
+ import ENGINES from './engines.js'
12
+ import createDebug from '#debug'
8
13
 
9
14
  const __filename = fileURLToPath(import.meta.url)
10
15
  const __dirname = path.dirname(__filename)
11
16
 
12
- // FIXME: This module swallows errors currently.
17
+ const debug = createDebug('lib:pdf:paged')
18
+
19
+ /** Re-export engine metadata from central registry */
20
+ export const metadata = ENGINES.pagedjs
13
21
 
14
22
  /**
15
- * A façade module for interacting with Paged.js and pagedjs-cli
23
+ * A façade module for interacting with Paged.js and pagedjs-cli
16
24
  * @see https://gitlab.coko.foundation/pagedjs/
25
+ *
26
+ * @param {string} publicationInput - Path to the publication HTML file
27
+ * @param {string} coversInput - Path to the covers HTML file
28
+ * @param {string} pdfPath - Path where the PDF should be written
29
+ * @param {Object} [options={}] - Generation options
30
+ * @returns {Promise<string>} Path to the generated PDF file
17
31
  */
18
- export default async (publicationInput, coversInput, output, options = {}) => {
32
+ export default async (publicationInput, coversInput, pdfPath, options = {}) => {
19
33
  /**
20
34
  * Configure the Paged.js Printer options
21
35
  * @see https://gitlab.coko.foundation/pagedjs/pagedjs-cli/-/blob/main/src/cli.js
22
36
  */
23
37
 
24
38
  let additionalScripts = []
25
-
39
+
26
40
  const { pdfConfig } = options
27
41
 
28
42
  additionalScripts.push( path.join(__dirname, 'pagedPlugin.js') )
@@ -35,25 +49,35 @@ export default async (publicationInput, coversInput, output, options = {}) => {
35
49
  additionalScripts,
36
50
  }
37
51
 
38
- if (options.debug) {
39
- const optionsOutput = JSON.stringify(printerOptions, null, 2)
40
- console.debug(`[CLI:lib/pdf/pagedjs] Printer options\n${optionsOutput}`)
41
- }
52
+ debug('printer options: %O', printerOptions)
42
53
 
43
54
  let printer = new Printer(printerOptions)
44
55
 
45
- printer.on('page', (page,pageElement,breakToken) => {
56
+ // Register cleanup handler for graceful shutdown
57
+ processManager.onShutdown('pagedjs', () => {
58
+ debug('closing printer')
59
+ printer.close()
60
+ })
61
+
62
+ printer.on('page', (page, pageElement, breakToken) => {
46
63
  if (page.position === 0) {
47
- console.info(`[CLI:lib/pdf/pagedjs] loaded`)
48
- }
64
+ debug('paged.js loaded')
65
+ }
49
66
  })
50
67
 
51
68
  printer.on('rendered', (msg) => {
52
- console.info(`[CLI:lib/pdf/pagedjs] ${msg}`)
69
+ debug('rendered: %s', msg)
53
70
  })
54
71
 
55
72
  printer.on('postprocessing', () => {
56
- console.info(`[CLI:lib/pdf/pagedjs] post-processing`)
73
+ debug('post-processing')
74
+ })
75
+
76
+ // Capture browser console messages for debugging
77
+ printer.on('console', (type, ...args) => {
78
+ if (type === 'warning' || type === 'error') {
79
+ debug('browser %s: %s', type, args.join(' '))
80
+ }
57
81
  })
58
82
 
59
83
  /**
@@ -61,29 +85,31 @@ export default async (publicationInput, coversInput, output, options = {}) => {
61
85
  * @see
62
86
  *
63
87
  * @todo verify options before setting them
64
- * @todo catch errors thrown by the printer
65
88
  */
66
89
  const pdfOptions = {
67
90
  outlineTags: options.outlineTags || ['h1'],
68
91
  }
69
92
 
70
- if (options.debug) {
71
- const optionsOutput = JSON.stringify(pdfOptions, null, 2)
72
- console.debug(`[CLI:lib/pdf/pagedjs] PDF options\n${optionsOutput}`)
73
- }
93
+ debug('pdf options: %O', pdfOptions)
74
94
 
75
- try {
76
- console.info(`[CLI:lib/pdf/pagedjs] printing ${publicationInput}`)
95
+ debug('printing %s', publicationInput)
96
+ reporter.update('Rendering PDF...')
77
97
 
78
- const file = await printer.pdf(publicationInput, pdfOptions)
79
- .catch((error) => console.error(error))
98
+ let file
99
+ try {
100
+ file = await printer.pdf(publicationInput, pdfOptions)
101
+ } catch (error) {
102
+ throw new PdfGenerationError('Paged.js', 'PDF rendering', error.message)
103
+ }
80
104
 
81
- let pageMap
105
+ let pageMap
82
106
 
83
- // Now it's printed, create the pageMap by running JS in the printer's context
84
- let coversFile
107
+ // Now it's printed, create the pageMap by running JS in the printer's context
108
+ let coversFile
85
109
 
86
- console.info(`[CLI:lib/pdf/pagedjs] generating page map`)
110
+ debug('extracting page map')
111
+ reporter.update('Extracting page map...')
112
+ try {
87
113
  const pages = await printer.browser.pages()
88
114
 
89
115
  if (pages.length > 0) {
@@ -92,15 +118,24 @@ export default async (publicationInput, coversInput, output, options = {}) => {
92
118
  return window.pageMap ?? {} // eslint-disable-line no-undef
93
119
  })
94
120
  }
121
+ } catch (error) {
122
+ throw new PdfGenerationError('Paged.js', 'page map extraction', error.message)
123
+ }
95
124
 
96
- if ( pdfConfig?.pagePDF?.coverPage===true && fs.existsSync(coversInput) ) {
97
- console.info(`[CLI:lib/pdf/pagedjs] printing ${coversInput}`)
125
+ if ( pdfConfig?.pagePDF?.coverPage===true && fs.existsSync(coversInput) ) {
126
+ debug('printing covers %s', coversInput)
127
+ reporter.update('Rendering cover pages...')
98
128
 
99
- const coverPrinter = new Printer(printerOptions)
129
+ const coverPrinter = new Printer(printerOptions)
100
130
 
131
+ try {
101
132
  coversFile = await coverPrinter.pdf(coversInput, pdfOptions)
102
- .catch((error) => console.error(error))
133
+ } catch (error) {
134
+ coverPrinter.close()
135
+ throw new PdfGenerationError('Paged.js', 'cover PDF rendering', error.message)
136
+ }
103
137
 
138
+ try {
104
139
  const coverPages = await coverPrinter.browser.pages()
105
140
 
106
141
  if (coverPages.length > 0) {
@@ -111,40 +146,53 @@ export default async (publicationInput, coversInput, output, options = {}) => {
111
146
 
112
147
  Object.values(coversMap).forEach( cov => {
113
148
  if (cov.id in pageMap) {
114
- pageMap[cov.id].coverPage = cov.startPage
149
+ pageMap[cov.id].coverPage = cov.startPage
115
150
  }
116
151
  })
117
-
118
152
  }
119
-
153
+ } catch (error) {
120
154
  coverPrinter.close()
121
- }
122
-
123
- // Leave the printer open for debug logs
124
- if (!options.debug) {
125
- printer.close()
155
+ throw new PdfGenerationError('Paged.js', 'cover page map extraction', error.message)
126
156
  }
127
157
 
128
- if (file && output) {
129
- console.info(`[CLI:lib/pdf/pagedjs] writing file(s)`)
158
+ coverPrinter.close()
159
+ }
160
+
161
+ // Leave the printer open for debug logs
162
+ if (!options.debug) {
163
+ printer.close()
164
+ }
165
+
166
+ // Unregister cleanup handler
167
+ processManager.onShutdownComplete('pagedjs')
130
168
 
131
- const { dir } = path.parse(output)
132
- if (!fs.existsSync(dir)) {
169
+ if (file && pdfPath) {
170
+ debug('writing files')
171
+ reporter.update('Writing PDF files...')
172
+
173
+ try {
174
+ const { dir } = path.parse(pdfPath)
175
+ if (!fs.existsSync(dir)) {
133
176
  fs.mkdirsSync(dir)
134
177
  }
135
178
 
136
- await fs.promises.writeFile(output, file)
137
- .catch((error) => console.error(error))
179
+ await fs.promises.writeFile(pdfPath, file)
180
+ } catch (error) {
181
+ throw new PdfGenerationError('Paged.js', 'PDF file write', error.message)
182
+ }
138
183
 
139
- const files = await splitPdf(file,coversFile,pageMap,options.pdfConfig)
184
+ try {
185
+ const files = await splitPdf(file, coversFile, pageMap, options.pdfConfig)
140
186
 
141
- Object.entries(files).forEach( async ([filePath,pagePdf]) => {
142
- await fs.promises.writeFile(filePath,pagePdf)
143
- .catch((error) => console.error(error))
144
- })
187
+ await Promise.all(
188
+ Object.entries(files).map(([filePath, pagePdf]) =>
189
+ fs.promises.writeFile(filePath, pagePdf)
190
+ )
191
+ )
192
+ } catch (error) {
193
+ throw new PdfGenerationError('Paged.js', 'PDF splitting', error.message)
145
194
  }
146
-
147
- } catch (ERR_FILE_NOT_FOUND) {
148
- console.error(`[CLI:lib/pdf/pagedjs] file not found ${publicationInput}`)
149
195
  }
196
+
197
+ return pdfPath
150
198
  }
@@ -0,0 +1,366 @@
1
+ import esmock from 'esmock'
2
+ import path from 'node:path'
3
+ import sinon from 'sinon'
4
+ import test from 'ava'
5
+
6
+ /**
7
+ * Paged.js PDF Engine Integration Tests
8
+ *
9
+ * Tests error handling paths that are difficult to trigger in E2E tests.
10
+ * The happy path is covered by E2E tests with real Paged.js/Puppeteer.
11
+ */
12
+
13
+ /**
14
+ * Create a cross-platform absolute path for testing
15
+ * @param {...string} segments - Path segments to join
16
+ * @returns {string} Platform-appropriate absolute path
17
+ */
18
+ function testPath(...segments) {
19
+ const root = process.platform === 'win32' ? 'C:\\' : '/'
20
+ return path.join(root, ...segments)
21
+ }
22
+
23
+ test.beforeEach((t) => {
24
+ t.context.sandbox = sinon.createSandbox()
25
+
26
+ // Mock reporter
27
+ t.context.mockReporter = {
28
+ update: t.context.sandbox.stub(),
29
+ warn: t.context.sandbox.stub()
30
+ }
31
+
32
+ // Mock processManager
33
+ t.context.mockProcessManager = {
34
+ onShutdown: t.context.sandbox.stub(),
35
+ onShutdownComplete: t.context.sandbox.stub()
36
+ }
37
+
38
+ // Mock fs-extra
39
+ t.context.mockFs = {
40
+ existsSync: t.context.sandbox.stub().returns(true),
41
+ mkdirsSync: t.context.sandbox.stub(),
42
+ promises: {
43
+ writeFile: t.context.sandbox.stub().resolves()
44
+ }
45
+ }
46
+ })
47
+
48
+ test.afterEach.always((t) => {
49
+ t.context.sandbox.restore()
50
+ })
51
+
52
+ /**
53
+ * Create a mock Printer instance
54
+ */
55
+ function createMockPrinter(sandbox, options = {}) {
56
+ const mockPrinter = {
57
+ on: sandbox.stub(),
58
+ close: sandbox.stub(),
59
+ pdf: options.pdf || sandbox.stub().resolves(Buffer.from([1, 2, 3])),
60
+ browser: {
61
+ pages: options.pages || sandbox.stub().resolves([{
62
+ evaluate: sandbox.stub().resolves({})
63
+ }])
64
+ }
65
+ }
66
+ return mockPrinter
67
+ }
68
+
69
+ // ─────────────────────────────────────────────────────────────────────────────
70
+ // Error handling tests
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+
73
+ test('throws PdfGenerationError when PDF rendering fails', async (t) => {
74
+ const { sandbox, mockReporter, mockProcessManager, mockFs } = t.context
75
+
76
+ const mockPrinter = createMockPrinter(sandbox, {
77
+ pdf: sandbox.stub().rejects(new Error('Puppeteer crashed: out of memory'))
78
+ })
79
+
80
+ const MockPrinter = sandbox.stub().returns(mockPrinter)
81
+
82
+ const paged = await esmock('./paged.js', {
83
+ 'pagedjs-cli': MockPrinter,
84
+ 'fs-extra': mockFs,
85
+ '#lib/reporter/index.js': { default: mockReporter },
86
+ '#lib/process/manager.js': { default: mockProcessManager }
87
+ })
88
+
89
+ const error = await t.throwsAsync(() =>
90
+ paged.default(testPath('input.html'), testPath('covers.html'), testPath('output.pdf'), {})
91
+ )
92
+
93
+ t.is(error.code, 'PDF_FAILED')
94
+ t.true(error.message.includes('Paged.js'))
95
+ t.true(error.message.includes('PDF rendering'))
96
+ t.true(error.message.includes('out of memory'))
97
+ })
98
+
99
+ test('throws PdfGenerationError when page map extraction fails', async (t) => {
100
+ const { sandbox, mockReporter, mockProcessManager, mockFs } = t.context
101
+
102
+ const mockPrinter = createMockPrinter(sandbox, {
103
+ pages: sandbox.stub().rejects(new Error('Browser context destroyed'))
104
+ })
105
+
106
+ const MockPrinter = sandbox.stub().returns(mockPrinter)
107
+
108
+ const paged = await esmock('./paged.js', {
109
+ 'pagedjs-cli': MockPrinter,
110
+ 'fs-extra': mockFs,
111
+ '#lib/reporter/index.js': { default: mockReporter },
112
+ '#lib/process/manager.js': { default: mockProcessManager }
113
+ })
114
+
115
+ const error = await t.throwsAsync(() =>
116
+ paged.default(testPath('input.html'), testPath('covers.html'), testPath('output.pdf'), {})
117
+ )
118
+
119
+ t.is(error.code, 'PDF_FAILED')
120
+ t.true(error.message.includes('Paged.js'))
121
+ t.true(error.message.includes('page map extraction'))
122
+ t.true(error.message.includes('Browser context destroyed'))
123
+ })
124
+
125
+ test('throws PdfGenerationError when cover PDF rendering fails', async (t) => {
126
+ const { sandbox, mockReporter, mockProcessManager, mockFs } = t.context
127
+
128
+ // Main printer succeeds
129
+ const mainPrinter = createMockPrinter(sandbox)
130
+
131
+ // Cover printer fails
132
+ const coverPrinter = createMockPrinter(sandbox, {
133
+ pdf: sandbox.stub().rejects(new Error('Cover page timeout'))
134
+ })
135
+
136
+ let printerCallCount = 0
137
+ const MockPrinter = sandbox.stub().callsFake(() => {
138
+ printerCallCount++
139
+ return printerCallCount === 1 ? mainPrinter : coverPrinter
140
+ })
141
+
142
+ const paged = await esmock('./paged.js', {
143
+ 'pagedjs-cli': MockPrinter,
144
+ 'fs-extra': mockFs,
145
+ '#lib/reporter/index.js': { default: mockReporter },
146
+ '#lib/process/manager.js': { default: mockProcessManager }
147
+ })
148
+
149
+ // Enable cover page generation
150
+ const options = {
151
+ pdfConfig: { pagePDF: { coverPage: true } }
152
+ }
153
+
154
+ const error = await t.throwsAsync(() =>
155
+ paged.default(testPath('input.html'), testPath('covers.html'), testPath('output.pdf'), options)
156
+ )
157
+
158
+ t.is(error.code, 'PDF_FAILED')
159
+ t.true(error.message.includes('Paged.js'))
160
+ t.true(error.message.includes('cover PDF rendering'))
161
+ t.true(error.message.includes('Cover page timeout'))
162
+ // Verify cover printer was closed on error
163
+ t.true(coverPrinter.close.called)
164
+ })
165
+
166
+ test('throws PdfGenerationError when cover page map extraction fails', async (t) => {
167
+ const { sandbox, mockReporter, mockProcessManager, mockFs } = t.context
168
+
169
+ // Main printer succeeds
170
+ const mainPrinter = createMockPrinter(sandbox)
171
+
172
+ // Cover printer PDF succeeds but pages() fails
173
+ const coverPrinter = createMockPrinter(sandbox, {
174
+ pages: sandbox.stub().rejects(new Error('Cover browser crashed'))
175
+ })
176
+
177
+ let printerCallCount = 0
178
+ const MockPrinter = sandbox.stub().callsFake(() => {
179
+ printerCallCount++
180
+ return printerCallCount === 1 ? mainPrinter : coverPrinter
181
+ })
182
+
183
+ const paged = await esmock('./paged.js', {
184
+ 'pagedjs-cli': MockPrinter,
185
+ 'fs-extra': mockFs,
186
+ '#lib/reporter/index.js': { default: mockReporter },
187
+ '#lib/process/manager.js': { default: mockProcessManager }
188
+ })
189
+
190
+ const options = {
191
+ pdfConfig: { pagePDF: { coverPage: true } }
192
+ }
193
+
194
+ const error = await t.throwsAsync(() =>
195
+ paged.default(testPath('input.html'), testPath('covers.html'), testPath('output.pdf'), options)
196
+ )
197
+
198
+ t.is(error.code, 'PDF_FAILED')
199
+ t.true(error.message.includes('Paged.js'))
200
+ t.true(error.message.includes('cover page map extraction'))
201
+ t.true(error.message.includes('Cover browser crashed'))
202
+ t.true(coverPrinter.close.called)
203
+ })
204
+
205
+ test('throws PdfGenerationError when PDF file write fails', async (t) => {
206
+ const { sandbox, mockReporter, mockProcessManager } = t.context
207
+
208
+ const mockPrinter = createMockPrinter(sandbox)
209
+ const MockPrinter = sandbox.stub().returns(mockPrinter)
210
+
211
+ const mockFs = {
212
+ existsSync: sandbox.stub().returns(false),
213
+ mkdirsSync: sandbox.stub().throws(new Error('EACCES: permission denied')),
214
+ promises: { writeFile: sandbox.stub() }
215
+ }
216
+
217
+ const paged = await esmock('./paged.js', {
218
+ 'pagedjs-cli': MockPrinter,
219
+ 'fs-extra': mockFs,
220
+ '#lib/reporter/index.js': { default: mockReporter },
221
+ '#lib/process/manager.js': { default: mockProcessManager }
222
+ })
223
+
224
+ const error = await t.throwsAsync(() =>
225
+ paged.default(testPath('input.html'), testPath('covers.html'), testPath('restricted', 'output.pdf'), {})
226
+ )
227
+
228
+ t.is(error.code, 'PDF_FAILED')
229
+ t.true(error.message.includes('Paged.js'))
230
+ t.true(error.message.includes('PDF file write'))
231
+ t.true(error.message.includes('permission denied'))
232
+ })
233
+
234
+ test('closes printer on successful completion when not in debug mode', async (t) => {
235
+ const { sandbox, mockReporter, mockProcessManager, mockFs } = t.context
236
+
237
+ const mockPrinter = createMockPrinter(sandbox)
238
+ const MockPrinter = sandbox.stub().returns(mockPrinter)
239
+
240
+ const paged = await esmock('./paged.js', {
241
+ 'pagedjs-cli': MockPrinter,
242
+ 'fs-extra': mockFs,
243
+ '#lib/reporter/index.js': { default: mockReporter },
244
+ '#lib/process/manager.js': { default: mockProcessManager },
245
+ './split.js': { splitPdf: sandbox.stub().resolves({}) }
246
+ })
247
+
248
+ await paged.default(testPath('input.html'), testPath('covers.html'), testPath('output.pdf'), { debug: false })
249
+
250
+ t.true(mockPrinter.close.called)
251
+ t.true(mockProcessManager.onShutdownComplete.calledWith('pagedjs'))
252
+ })
253
+
254
+ test('keeps printer open when in debug mode', async (t) => {
255
+ const { sandbox, mockReporter, mockProcessManager, mockFs } = t.context
256
+
257
+ const mockPrinter = createMockPrinter(sandbox)
258
+ const MockPrinter = sandbox.stub().returns(mockPrinter)
259
+
260
+ const paged = await esmock('./paged.js', {
261
+ 'pagedjs-cli': MockPrinter,
262
+ 'fs-extra': mockFs,
263
+ '#lib/reporter/index.js': { default: mockReporter },
264
+ '#lib/process/manager.js': { default: mockProcessManager },
265
+ './split.js': { splitPdf: sandbox.stub().resolves({}) }
266
+ })
267
+
268
+ await paged.default(testPath('input.html'), testPath('covers.html'), testPath('output.pdf'), { debug: true })
269
+
270
+ t.false(mockPrinter.close.called)
271
+ })
272
+
273
+ test('registers console event handler for browser warnings and errors', async (t) => {
274
+ const { sandbox, mockReporter, mockProcessManager, mockFs } = t.context
275
+
276
+ const mockPrinter = createMockPrinter(sandbox)
277
+ const MockPrinter = sandbox.stub().returns(mockPrinter)
278
+
279
+ const mockDebug = sandbox.stub()
280
+ const mockCreateDebug = sandbox.stub().returns(mockDebug)
281
+
282
+ const paged = await esmock('./paged.js', {
283
+ 'pagedjs-cli': MockPrinter,
284
+ 'fs-extra': mockFs,
285
+ '#lib/reporter/index.js': { default: mockReporter },
286
+ '#lib/process/manager.js': { default: mockProcessManager },
287
+ '#debug': { default: mockCreateDebug },
288
+ './split.js': { splitPdf: sandbox.stub().resolves({}) }
289
+ })
290
+
291
+ await paged.default(testPath('input.html'), testPath('covers.html'), testPath('output.pdf'), {})
292
+
293
+ // Verify console handler was registered
294
+ const consoleHandler = mockPrinter.on.getCalls().find(call => call.args[0] === 'console')
295
+ t.truthy(consoleHandler, 'console event handler should be registered')
296
+
297
+ // Simulate console warning
298
+ const handler = consoleHandler.args[1]
299
+ handler('warning', 'CSS property not supported')
300
+
301
+ t.true(mockDebug.calledWith('browser %s: %s', 'warning', 'CSS property not supported'))
302
+ })
303
+
304
+ test('logs browser errors via debug', async (t) => {
305
+ const { sandbox, mockReporter, mockProcessManager, mockFs } = t.context
306
+
307
+ const mockPrinter = createMockPrinter(sandbox)
308
+ const MockPrinter = sandbox.stub().returns(mockPrinter)
309
+
310
+ const mockDebug = sandbox.stub()
311
+ const mockCreateDebug = sandbox.stub().returns(mockDebug)
312
+
313
+ const paged = await esmock('./paged.js', {
314
+ 'pagedjs-cli': MockPrinter,
315
+ 'fs-extra': mockFs,
316
+ '#lib/reporter/index.js': { default: mockReporter },
317
+ '#lib/process/manager.js': { default: mockProcessManager },
318
+ '#debug': { default: mockCreateDebug },
319
+ './split.js': { splitPdf: sandbox.stub().resolves({}) }
320
+ })
321
+
322
+ await paged.default(testPath('input.html'), testPath('covers.html'), testPath('output.pdf'), {})
323
+
324
+ // Get console handler
325
+ const consoleHandler = mockPrinter.on.getCalls().find(call => call.args[0] === 'console')
326
+ const handler = consoleHandler.args[1]
327
+
328
+ // Simulate console error
329
+ handler('error', 'Script error:', 'undefined is not a function')
330
+
331
+ t.true(mockDebug.calledWith('browser %s: %s', 'error', 'Script error: undefined is not a function'))
332
+ })
333
+
334
+ test('ignores non-warning/error console messages', async (t) => {
335
+ const { sandbox, mockReporter, mockProcessManager, mockFs } = t.context
336
+
337
+ const mockPrinter = createMockPrinter(sandbox)
338
+ const MockPrinter = sandbox.stub().returns(mockPrinter)
339
+
340
+ const mockDebug = sandbox.stub()
341
+ const mockCreateDebug = sandbox.stub().returns(mockDebug)
342
+
343
+ const paged = await esmock('./paged.js', {
344
+ 'pagedjs-cli': MockPrinter,
345
+ 'fs-extra': mockFs,
346
+ '#lib/reporter/index.js': { default: mockReporter },
347
+ '#lib/process/manager.js': { default: mockProcessManager },
348
+ '#debug': { default: mockCreateDebug },
349
+ './split.js': { splitPdf: sandbox.stub().resolves({}) }
350
+ })
351
+
352
+ await paged.default(testPath('input.html'), testPath('covers.html'), testPath('output.pdf'), {})
353
+
354
+ // Get console handler
355
+ const consoleHandler = mockPrinter.on.getCalls().find(call => call.args[0] === 'console')
356
+ const handler = consoleHandler.args[1]
357
+
358
+ // Reset debug stub to check only calls after handler invocation
359
+ mockDebug.resetHistory()
360
+
361
+ // Simulate console log (should be ignored)
362
+ handler('log', 'Debug info')
363
+
364
+ // Should not have logged this message (info/log messages are ignored)
365
+ t.false(mockDebug.calledWith('browser %s: %s', 'log', 'Debug info'))
366
+ })