Skip to content

Commit 3dea229

Browse files
authored
fix: details not opened in PDF (#203)
* fix: details not opened in PDF * test: fix unit test
1 parent a73abad commit 3dea229

File tree

6 files changed

+93
-8
lines changed

6 files changed

+93
-8
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# built by typescript
22
/lib/*
3+
/examples/*

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ lib/
55
.vscode
66
.DS_Store
77
coverage
8-
*.tgz
8+
*.tgz
9+
examples/

docs-to-pdf.pdf

87.8 KB
Binary file not shown.

src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ program
8080
'--restrictPaths',
8181
'only the paths in the --initialDocURLs will be included in the PDF',
8282
)
83+
.option('--openDetail', 'open details elements in the PDF, default is open')
8384

8485
.action((options: GeneratePDFOptions) => {
8586
if (options.pdfFormat) {

src/utils.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface GeneratePDFOptions {
3232
baseUrl: string;
3333
excludePaths: Array<string>;
3434
restrictPaths: boolean;
35+
openDetail: boolean;
3536
}
3637

3738
/* c8 ignore start */
@@ -58,6 +59,7 @@ export async function generatePDF({
5859
baseUrl,
5960
excludePaths,
6061
restrictPaths,
62+
openDetail = true,
6163
}: GeneratePDFOptions): Promise<void> {
6264
const execPath =
6365
process.env.PUPPETEER_EXECUTABLE_PATH ?? puppeteer.executablePath('chrome');
@@ -115,6 +117,10 @@ export async function generatePDF({
115117
restrictPaths,
116118
)
117119
) {
120+
// Open all <details> elements on the page
121+
if (openDetail) {
122+
await openDetails(page);
123+
}
118124
// Get the HTML string of the content section.
119125
contentHTML += await getHtmlContent(page, contentSelector);
120126
console.log(chalk.green('Success'));
@@ -241,7 +247,7 @@ export async function getHtmlContent(page: puppeteer.Page, selector: string) {
241247

242248
/**
243249
* Retrieves the HTML content of an element matching the specified selector.
244-
* Adds page break styling and opens <details> tags for PDF generation.
250+
* Adds page break styling for PDF generation.
245251
*
246252
* @param selector - The CSS selector of the element to retrieve.
247253
* @returns The HTML content of the matched element, or an empty string if no element is found.
@@ -252,18 +258,42 @@ export function getHtmlFromSelector(selector: string): string {
252258
// Add pageBreak for PDF
253259
element.style.pageBreakAfter = 'always';
254260

255-
// Open <details> tag
256-
const detailsArray = element.getElementsByTagName('details');
257-
Array.from(detailsArray).forEach((element) => {
258-
element.open = true;
259-
});
260-
261261
return element.outerHTML;
262262
} else {
263263
return '';
264264
}
265265
}
266266

267+
type ClickFunction = (element: puppeteer.ElementHandle) => Promise<void>;
268+
type WaitFunction = (milliseconds: number) => Promise<void>;
269+
270+
/**
271+
* Recursively opens all <details> elements on a page.
272+
*
273+
* @param page - The Puppeteer page instance.
274+
* @param clickFunction - A function to click the summary element of a <details> element.
275+
* @param waitFunction - A function to wait for a specified number of milliseconds.
276+
*/
277+
export async function openDetails(
278+
page: puppeteer.Page,
279+
clickFunction?: ClickFunction,
280+
waitFunction?: WaitFunction
281+
) {
282+
const detailsHandles = await page.$$('details');
283+
284+
console.debug(`Found ${detailsHandles.length} elements`);
285+
286+
for (const detailsHandle of detailsHandles) {
287+
const summaryHandle = await detailsHandle.$('summary');
288+
if (summaryHandle){
289+
console.debug(`Clicking summary: ${await summaryHandle.evaluate((node) => node.textContent)}`);
290+
await (clickFunction ? clickFunction(summaryHandle) : summaryHandle.click());
291+
await (waitFunction ? waitFunction(800) : new Promise((r) => setTimeout(r, 800)));
292+
}
293+
294+
}
295+
}
296+
267297
/**
268298
* Finds the URL of the next page based on a CSS selector using Puppeteer.
269299
* @param page - The Puppeteer page instance.

tests/utils.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
replaceHeader,
1212
matchKeyword,
1313
isPageKept,
14+
openDetails,
1415
} from '../src/utils';
1516

1617
const execPath =
@@ -454,3 +455,54 @@ describe('isPageKept function', () => {
454455
expect(result).toBe(true);
455456
});
456457
});
458+
459+
describe('openDetails function', () => {
460+
let page: puppeteer.Page;
461+
let browser: puppeteer.Browser;
462+
463+
beforeAll(async () => {
464+
browser = await puppeteer.launch({
465+
headless: 'new',
466+
executablePath: execPath,
467+
});
468+
page = await browser.newPage();
469+
}, 30000);
470+
471+
afterAll(async () => {
472+
await browser.close();
473+
});
474+
475+
it('should open all details elements recursively', async () => {
476+
// Mock the click and wait functions
477+
const clickFunction = jest.fn(async () => {});
478+
const waitFunction = jest.fn(async () => {});
479+
480+
// Mock a simple HTML page with nested <details> elements
481+
await page.setContent(`
482+
<details>
483+
<summary>Toggle me!</summary>
484+
<div>
485+
<div>This is the detailed content</div>
486+
<br/>
487+
<details>
488+
<summary>
489+
Nested toggle! Some surprise inside...
490+
</summary>
491+
<div>😲😲😲😲😲</div>
492+
</details>
493+
</div>
494+
</details>
495+
`);
496+
497+
// Call the recursive function to open details elements
498+
await openDetails(page, clickFunction, waitFunction);
499+
500+
// Assertions based on the mock functions
501+
expect(clickFunction).toHaveBeenCalledTimes(2);
502+
expect(waitFunction).toHaveBeenCalledTimes(2);
503+
expect(waitFunction).toHaveBeenCalledWith(800);
504+
505+
// Close the browser
506+
await browser.close();
507+
});
508+
});

0 commit comments

Comments
 (0)