@@ -2664,6 +2664,181 @@ def slide_2():
26642664 sys .modules .pop ("playwright.async_api" , None )
26652665
26662666
2667+ @pytest .mark .skipif (
2668+ not DependencyManager .nbformat .has (),
2669+ reason = "nbformat not installed" ,
2670+ )
2671+ async def test_export_as_slides_pdf_with_png_fallbacks (
2672+ session_view : SessionView ,
2673+ ) -> None :
2674+ """Test slides PDF injection of PNG fallbacks alongside virtual file inlining."""
2675+ app = App ()
2676+
2677+ @app .cell ()
2678+ def slide_1 ():
2679+ return "slide 1"
2680+
2681+ file_manager = AppFileManager .from_app (InternalApp (app ))
2682+ exporter = Exporter ()
2683+
2684+ mock_slides_exporter_instance = MagicMock ()
2685+ mock_slides_exporter_instance .from_notebook_node .return_value = (
2686+ "<html>mock slides html</html>" ,
2687+ {},
2688+ )
2689+ mock_slides_exporter_cls = MagicMock (
2690+ return_value = mock_slides_exporter_instance
2691+ )
2692+
2693+ mock_page = AsyncMock ()
2694+ mock_page .pdf .return_value = b"mock_slides_pdf_data"
2695+ mock_browser = AsyncMock ()
2696+ mock_browser .new_page .return_value = mock_page
2697+ mock_playwright_instance = AsyncMock ()
2698+ mock_playwright_instance .chromium .launch .return_value = mock_browser
2699+ mock_playwright_cm = AsyncMock ()
2700+ mock_playwright_cm .__aenter__ .return_value = mock_playwright_instance
2701+ mock_playwright_cm .__aexit__ .return_value = False
2702+ mock_async_playwright = MagicMock (return_value = mock_playwright_cm )
2703+
2704+ mock_nbconvert = MagicMock ()
2705+ mock_nbconvert .SlidesExporter = mock_slides_exporter_cls
2706+ mock_playwright_async = MagicMock ()
2707+ mock_playwright_async .async_playwright = mock_async_playwright
2708+
2709+ orig_nbconvert = sys .modules .get ("nbconvert" )
2710+ orig_playwright = sys .modules .get ("playwright.async_api" )
2711+
2712+ try :
2713+ sys .modules ["nbconvert" ] = mock_nbconvert
2714+ sys .modules ["playwright.async_api" ] = mock_playwright_async
2715+
2716+ png_fallbacks = {
2717+ "HbolJK" : "data:image/png;base64,iVBORw0KGgo=" ,
2718+ }
2719+
2720+ with (
2721+ patch .object (
2722+ DependencyManager .nbconvert , "has" , return_value = True
2723+ ),
2724+ patch .object (
2725+ DependencyManager .playwright , "has" , return_value = True
2726+ ),
2727+ ):
2728+ events : list [PDFExportStatusEvent ] = []
2729+ result = await exporter .export_as_slides_pdf (
2730+ _pdf_export_request (
2731+ app = file_manager .app ,
2732+ session_view = session_view ,
2733+ png_fallbacks = png_fallbacks ,
2734+ status_callback = events .append ,
2735+ preset = "slides" ,
2736+ )
2737+ )
2738+
2739+ assert result == b"mock_slides_pdf_data"
2740+ assert [(event .phase , event .message ) for event in events ] == [
2741+ ("render" , "rendering slides PDF..." ),
2742+ ]
2743+
2744+ # Verify the notebook passed to SlidesExporter has the injected
2745+ # PNG fallback in its cell outputs.
2746+ notebook = (
2747+ mock_slides_exporter_instance .from_notebook_node .call_args [0 ][
2748+ 0
2749+ ]
2750+ )
2751+ found_png = False
2752+ for cell in notebook .cells :
2753+ for output in cell .get ("outputs" , []):
2754+ data = output .get ("data" , {})
2755+ if "image/png" in data :
2756+ found_png = True
2757+ break
2758+ assert found_png , (
2759+ "Expected PNG fallback data in notebook cell outputs"
2760+ )
2761+
2762+ finally :
2763+ if orig_nbconvert is not None :
2764+ sys .modules ["nbconvert" ] = orig_nbconvert
2765+ else :
2766+ sys .modules .pop ("nbconvert" , None )
2767+ if orig_playwright is not None :
2768+ sys .modules ["playwright.async_api" ] = orig_playwright
2769+ else :
2770+ sys .modules .pop ("playwright.async_api" , None )
2771+
2772+ @pytest .mark .skipif (
2773+ not DependencyManager .nbformat .has (),
2774+ reason = "nbformat not installed" ,
2775+ )
2776+ def test_inline_virtual_files_in_notebook () -> None :
2777+ """Test that _inline_virtual_files_in_notebook resolves virtual file
2778+ references in cell output HTML to data URIs."""
2779+ import nbformat
2780+
2781+ notebook = nbformat .v4 .new_notebook ()
2782+ cell = nbformat .v4 .new_code_cell (
2783+ "print('hi')" ,
2784+ id = "cell-1" ,
2785+ )
2786+ cell .outputs = [
2787+ nbformat .v4 .new_output (
2788+ "display_data" ,
2789+ data = {
2790+ "text/html" : '<img src="./@file/12345-test.png" alt="plot" />' ,
2791+ "text/plain" : "test" ,
2792+ },
2793+ ),
2794+ ]
2795+ notebook .cells = [cell ]
2796+
2797+ # The virtual file doesn't exist on disk, so the replacement will
2798+ # log a warning and leave the original value. We verify no crash.
2799+ Exporter ._inline_virtual_files_in_notebook (notebook )
2800+
2801+ output_data = notebook .cells [0 ].outputs [0 ]["data" ]
2802+ assert "text/html" in output_data
2803+ assert "text/plain" in output_data
2804+
2805+
2806+ @pytest .mark .skipif (
2807+ not DependencyManager .nbformat .has (),
2808+ reason = "nbformat not installed" ,
2809+ )
2810+ def test_inline_virtual_files_in_notebook_skips_non_html () -> None :
2811+ """Test that non-HTML outputs (image/png, text/plain) are never
2812+ sent through the HTML parser, preserving their raw content."""
2813+ import nbformat
2814+
2815+ notebook = nbformat .v4 .new_notebook ()
2816+ cell = nbformat .v4 .new_code_cell (
2817+ "print('hi')" ,
2818+ id = "cell-1" ,
2819+ )
2820+ cell .outputs = [
2821+ nbformat .v4 .new_output (
2822+ "display_data" ,
2823+ data = {
2824+ "image/png" : "iVBORw0KGgo=" ,
2825+ "text/plain" : "plain text with ./@file/ in content" ,
2826+ },
2827+ ),
2828+ ]
2829+ notebook .cells = [cell ]
2830+ original_png = cell .outputs [0 ]["data" ]["image/png" ]
2831+ original_text = cell .outputs [0 ]["data" ]["text/plain" ]
2832+
2833+ Exporter ._inline_virtual_files_in_notebook (notebook )
2834+
2835+ output_data = notebook .cells [0 ].outputs [0 ]["data" ]
2836+ # image/png must remain untouched
2837+ assert output_data ["image/png" ] == original_png
2838+ # text/plain with "./@file/" must remain untouched (not text/html)
2839+ assert output_data ["text/plain" ] == original_text
2840+
2841+
26672842@pytest .mark .skipif (
26682843 sys .platform == "win32" ,
26692844 reason = "Unix permission bits not supported on Windows" ,
0 commit comments