11import { existsSync , mkdtempSync , rmSync } from 'node:fs' ;
22import { tmpdir } from 'node:os' ;
33import { join } from 'node:path' ;
4- import { afterAll , beforeAll , describe , expect , it , vi } from 'vitest' ;
4+ import { afterAll , beforeAll , beforeEach , describe , expect , it , vi } from 'vitest' ;
55import { exportPptx , extractSlides } from './pptx' ;
66
77const pngBytes = Buffer . from (
@@ -17,6 +17,8 @@ const screenshotMock = vi.fn();
1717const closeMock = vi . fn ( ) ;
1818const sectionBoundingBoxMock = vi . fn ( ) ;
1919const querySectionsMock = vi . fn ( ) ;
20+ const defaultFallbackSlideSelector =
21+ '[data-slide], [data-pptx-slide], [data-slide-container], .slide' ;
2022
2123vi . mock ( 'puppeteer-core' , ( ) => ( {
2224 default : { launch : launchMock } ,
@@ -30,6 +32,10 @@ let tempDir = '';
3032
3133beforeAll ( ( ) => {
3234 tempDir = mkdtempSync ( join ( tmpdir ( ) , 'codesign-pptx-test-' ) ) ;
35+ } ) ;
36+
37+ beforeEach ( ( ) => {
38+ vi . clearAllMocks ( ) ;
3339 launchMock . mockResolvedValue ( {
3440 newPage : newPageMock ,
3541 close : closeMock ,
@@ -51,6 +57,12 @@ afterAll(() => {
5157 rmSync ( tempDir , { recursive : true , force : true } ) ;
5258} ) ;
5359
60+ function mockPaginationPageCount ( pageCount : number ) : void {
61+ evaluateMock . mockImplementation ( async ( source : unknown ) =>
62+ typeof source === 'string' && source . includes ( 'pageCount' ) ? { pageCount } : undefined ,
63+ ) ;
64+ }
65+
5466describe ( 'extractSlides' , ( ) => {
5567 it ( 'treats each <section> as a slide and pulls the heading + bullets' , ( ) => {
5668 const html = `
@@ -131,6 +143,87 @@ describe('exportPptx', () => {
131143 ) ;
132144 } ) ;
133145
146+ it ( 'uses slide-like containers when section elements are absent' , async ( ) => {
147+ querySectionsMock . mockImplementation ( async ( selector : string ) =>
148+ selector === 'section' ? [ ] : [ { boundingBox : sectionBoundingBoxMock } ] ,
149+ ) ;
150+ const dest = join ( tempDir , 'slide-class-visual.pptx' ) ;
151+
152+ await exportPptx ( '<div class="slide"><h1>Visual</h1></div>' , dest ) ;
153+
154+ expect ( querySectionsMock ) . toHaveBeenNthCalledWith ( 1 , 'section' ) ;
155+ expect ( querySectionsMock ) . toHaveBeenNthCalledWith ( 2 , defaultFallbackSlideSelector ) ;
156+ expect ( screenshotMock ) . toHaveBeenCalledWith (
157+ expect . objectContaining ( { type : 'png' , clip : expect . any ( Object ) } ) ,
158+ ) ;
159+ } ) ;
160+
161+ it ( 'uses a caller-provided fallback slide selector' , async ( ) => {
162+ querySectionsMock . mockImplementation ( async ( selector : string ) =>
163+ selector === '[data-slide-container]' ? [ { boundingBox : sectionBoundingBoxMock } ] : [ ] ,
164+ ) ;
165+ const dest = join ( tempDir , 'custom-slide-selector.pptx' ) ;
166+
167+ await exportPptx ( '<article data-slide-container><h1>Visual</h1></article>' , dest , {
168+ slideSelector : '[data-slide-container]' ,
169+ } ) ;
170+
171+ expect ( querySectionsMock ) . toHaveBeenNthCalledWith ( 1 , 'section' ) ;
172+ expect ( querySectionsMock ) . toHaveBeenNthCalledWith ( 2 , '[data-slide-container]' ) ;
173+ expect ( screenshotMock ) . toHaveBeenCalledTimes ( 1 ) ;
174+ } ) ;
175+
176+ it ( 'paginates sectionless documents into viewport-sized screenshots' , async ( ) => {
177+ querySectionsMock . mockResolvedValue ( [ ] ) ;
178+ mockPaginationPageCount ( 3 ) ;
179+ const dest = join ( tempDir , 'sectionless-visual.pptx' ) ;
180+
181+ await exportPptx ( '<main><h1>Long artifact</h1></main>' , dest ) ;
182+
183+ expect ( screenshotMock ) . toHaveBeenCalledTimes ( 3 ) ;
184+ expect ( screenshotMock ) . toHaveBeenNthCalledWith ( 1 , {
185+ type : 'png' ,
186+ clip : { x : 0 , y : 0 , width : 1280 , height : 720 } ,
187+ } ) ;
188+ expect ( screenshotMock ) . toHaveBeenNthCalledWith ( 2 , {
189+ type : 'png' ,
190+ clip : { x : 0 , y : 720 , width : 1280 , height : 720 } ,
191+ } ) ;
192+ expect ( screenshotMock ) . toHaveBeenNthCalledWith ( 3 , {
193+ type : 'png' ,
194+ clip : { x : 0 , y : 1440 , width : 1280 , height : 720 } ,
195+ } ) ;
196+ expect ( screenshotMock ) . not . toHaveBeenCalledWith ( expect . objectContaining ( { fullPage : true } ) ) ;
197+ } ) ;
198+
199+ it ( 'keeps short sectionless documents to one viewport-sized screenshot' , async ( ) => {
200+ querySectionsMock . mockResolvedValue ( [ ] ) ;
201+ mockPaginationPageCount ( 1 ) ;
202+ const dest = join ( tempDir , 'short-sectionless-visual.pptx' ) ;
203+
204+ await exportPptx ( '<main><h1>Short artifact</h1></main>' , dest ) ;
205+
206+ expect ( screenshotMock ) . toHaveBeenCalledTimes ( 1 ) ;
207+ expect ( screenshotMock ) . toHaveBeenCalledWith ( {
208+ type : 'png' ,
209+ clip : { x : 0 , y : 0 , width : 1280 , height : 720 } ,
210+ } ) ;
211+ } ) ;
212+
213+ it ( 'exports an empty sectionless document as one screenshot slide' , async ( ) => {
214+ querySectionsMock . mockResolvedValue ( [ ] ) ;
215+ mockPaginationPageCount ( 1 ) ;
216+ const dest = join ( tempDir , 'empty-sectionless-visual.pptx' ) ;
217+
218+ await exportPptx ( '' , dest ) ;
219+
220+ expect ( screenshotMock ) . toHaveBeenCalledTimes ( 1 ) ;
221+ expect ( screenshotMock ) . toHaveBeenCalledWith ( {
222+ type : 'png' ,
223+ clip : { x : 0 , y : 0 , width : 1280 , height : 720 } ,
224+ } ) ;
225+ } ) ;
226+
134227 it ( 'wraps JSX source before screenshotting PPTX slides' , async ( ) => {
135228 setContentMock . mockClear ( ) ;
136229 const dest = join ( tempDir , 'jsx-visual.pptx' ) ;
0 commit comments