2026-10-25 18:52:39 +01:00
using AIStudio.Tools.PowerPoint ;
using P = DocumentFormat . OpenXml . Presentation ;
namespace AIStudio.Tests.Tools.PowerPoint ;
/// <summary>
/// Checks how the slide deck is read out of a model answer, and how it is shown as an outline.
/// </summary>
[TestFixture]
public sealed class SlideDeckSpecTests
{
[Test]
public void TheJsonIsReadOutOfProseAndACodeFence ()
{
const string ANSWER = """
Here is your presentation :
``` json
{ "title" : "Mars" , "slides" : [ { "layout" : "L3" , "title" : "Why Mars" , "texts" : [[ "Close to Earth" , { "text" : "Two years per trip" , "level" : 1 }]] } ] }
```
""";
Assert . That ( SlideDeckSpec . TryParse ( ANSWER , out var spec , out var issue ), Is . True , issue );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-25 18:52:39 +01:00
{
Assert . That ( spec . Title , Is . EqualTo ( "Mars" ));
Assert . That ( spec . Slides , Has . Count . EqualTo ( 1 ));
Assert . That ( spec . Slides [ 0 ]. Layout , Is . EqualTo ( "L3" ));
Assert . That ( spec . Slides [ 0 ]. Title , Is . EqualTo ( "Why Mars" ));
Assert . That ( spec . Slides [ 0 ]. Texts [ 0 ]. Paragraphs , Is . EqualTo ( new [] { new SlideParagraph ( "Close to Earth" , 0 ), new SlideParagraph ( "Two years per trip" , 1 ) }));
2026-11-01 20:32:16 +01:00
}
2026-10-25 18:52:39 +01:00
}
[Test]
public void WhatModelsWriteInSeveralWaysIsAccepted ()
{
const string ANSWER = """
{
"title" : "Mars" ,
"slides" : [
{
"layout" : "L6" ,
"starts_chapter" : true ,
"title" : "Costs" ,
"subtitle" : null ,
"texts" : [ "- First point\n- Second point" ],
"labels" : [ null , "Source: NASA" ],
"pictures" : [ "2" ],
"table" : { "headers" : [ "Year" , "Missions" ], "rows" : [[ 2024 , 3 ], [ "2025" , null ]] },
},
],
}
""";
Assert . That ( SlideDeckSpec . TryParse ( ANSWER , out var spec , out var issue ), Is . True , issue );
var slide = spec . Slides [ 0 ];
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-25 18:52:39 +01:00
{
Assert . That ( slide . StartsChapter , Is . True );
Assert . That ( slide . Subtitle , Is . Empty , "A null is as good as a missing value." );
Assert . That ( slide . Texts [ 0 ]. Paragraphs , Is . EqualTo ( new [] { new SlideParagraph ( "First point" , 0 ), new SlideParagraph ( "Second point" , 0 ) }), "One string with line breaks is one paragraph per line, and the template draws the bullets." );
Assert . That ( slide . Labels , Is . EqualTo ( new [] { string . Empty , "Source: NASA" }), "An empty label keeps its place, so that the second one still lands in the second slot." );
Assert . That ( slide . Pictures , Is . EqualTo ( new [] { 2 }));
Assert . That ( slide . Table ?. Headers , Is . EqualTo ( new [] { "Year" , "Missions" }));
Assert . That ( slide . Table ?. Rows , Is . EqualTo ( new [] { new [] { "2024" , "3" }, new [] { "2025" , string . Empty } }), "Numbers in a table are text on a slide." );
2026-11-01 20:32:16 +01:00
}
2026-10-25 18:52:39 +01:00
}
[Test]
public void AnAnswerWithoutJsonIsReported ()
{
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-25 18:52:39 +01:00
{
Assert . That ( SlideDeckSpec . TryParse ( "I cannot help with that." , out _ , out var issue ), Is . False );
Assert . That ( issue , Does . Contain ( "no complete JSON object" ));
2026-11-01 20:32:16 +01:00
}
2026-10-25 18:52:39 +01:00
}
[Test]
public void BrokenJsonIsReported ()
{
const string ANSWER = """{ " title ": " Mars ", " slides ": [ { " layout ": " L1 ",, " title ": " Mars " } ] }""" ;
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-25 18:52:39 +01:00
{
Assert . That ( SlideDeckSpec . TryParse ( ANSWER , out _ , out var issue ), Is . False );
Assert . That ( issue , Does . StartWith ( "The JSON object cannot be read" ), "The reader's own message follows, which tells the model where the JSON breaks." );
2026-11-01 20:32:16 +01:00
}
2026-10-25 18:52:39 +01:00
}
[Test]
public void TheOutlineFollowsTheReadingOrderOfEachLayout ()
{
var deck = new SlideDeckSpec ( "Mars" ,
[
new SlideSpec("L2", true, "Getting there", "Part one", [] , [], [], null ),
new SlideSpec ( "L5" , false , "The rocket" , string . Empty , [ new SlideTextBlock ([ new SlideParagraph ( "Big" , 0 ), new SlideParagraph ( "Very big" , 1 )])], [], [ 1 ], null ),
new SlideSpec ( "L6" , false , "Costs" , string . Empty , [], [], [], new SlideTableSpec ([ "Item" , "Cost" ], [[ "Fuel" , "10" ]])),
]);
const string EXPECTED = """
# Mars
***
## Getting there
_Part one_
***
### The rocket
- Big
- Very big
* Picture : rocket . png *
***
### Costs
| Item | Cost |
| --- | --- |
| Fuel | 10 |
""";
// The literal takes the line breaks of this file, which depend on the checkout:
Assert . That ( deck . ToMarkdownOutline ( PresentationTemplate . CreateDefault (), new Dictionary < int , string > { [ 1 ] = "rocket.png" }), Is . EqualTo ( EXPECTED . ReplaceLineEndings ( "\n" )));
}
[Test]
public void TheTemplateSlideAnEntryRefersToIsRead ()
{
const string ANSWER = """
{ "title" : "Weekly" , "slides" : [
{ "template_slide" : 1 },
{ "template_slide" : "2" , "title" : "News" , "texts" : [[ "A new release" ]] },
{ "layout" : "L3" , "title" : "Example" }
] }
""";
Assert . That ( SlideDeckSpec . TryParse ( ANSWER , out var spec , out var issue ), Is . True , issue );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-25 18:52:39 +01:00
{
Assert . That ( spec . Slides . Select ( slide => slide . TemplateSlide ), Is . EqualTo ( new int? [] { 1 , 2 , null }));
Assert . That ( spec . Slides . Select ( slide => slide . HasContent ), Is . EqualTo ( new [] { false , true , true }), "Only content of its own makes a slide more than a reference." );
Assert . That ( spec . Slides [ 0 ]. Layout , Is . Empty );
2026-11-01 20:32:16 +01:00
}
2026-10-25 18:52:39 +01:00
}
[Test]
public void WhatAnEditedSlideRemovesIsRead ()
{
const string ANSWER = """
{ "title" : "Weekly" , "slides" : [
{ "template_slide" : 1 , "remove" : [ "element 2" , " label 1 " , 3 , "Element 2" , "" ] },
{ "template_slide" : 2 , "title" : "News" , "remove" : "element 1" },
{ "template_slide" : 3 }
] }
""";
Assert . That ( SlideDeckSpec . TryParse ( ANSWER , out var spec , out var issue ), Is . True , issue );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-25 18:52:39 +01:00
{
Assert . That ( spec . Slides [ 0 ]. Removals , Is . EqualTo ( new [] { "element 2" , "label 1" , "3" }), "Every name counts once, whatever its case." );
Assert . That ( spec . Slides [ 1 ]. Removals , Is . EqualTo ( new [] { "element 1" }), "A single name is as good as a list." );
Assert . That ( spec . Slides [ 2 ]. Removals , Is . Empty );
Assert . That ( spec . Slides . Select ( slide => slide . HasContent ), Is . EqualTo ( new [] { true , true , false }), "Removing something changes a template slide as well." );
2026-11-01 20:32:16 +01:00
}
2026-10-25 18:52:39 +01:00
}
[Test]
public void TheOutlineShowsWhatTheTemplateSlidesKeep ()
{
using var file = TestTemplate . Create (
new TestTemplateOptions ( Slides :
[
new TestSlide(0, (TestPlaceholder.Title(), "Consultation hour"), (new TestPlaceholder(P.PlaceholderValues.SubTitle, 1), "Every Monday")),
new TestSlide(1, (TestPlaceholder.Title(), "News"), (new TestPlaceholder(null, 1), "AI Studio:\nEINSTEIN:")),
new TestSlide(1, (TestPlaceholder.Title(), "Questions"), (new TestPlaceholder(null, 1), "Ask us")),
] ),
new TestLayout ( "Title" , TestPlaceholder . Title (), TestPlaceholder . Subtitle ( new SlotBounds ( 838_200 , 3_602_038 , 10_515_600 , 1_655_762 ))),
new TestLayout ( "Content" , TestPlaceholder . Title (), TestPlaceholder . Text ( 1 , TestTemplate . MASTER_BODY_BOUNDS )));
var deck = new SlideDeckSpec ( "Weekly" ,
[
new SlideSpec(string.Empty, false, string.Empty, string.Empty, [] , [], [], null , 1 ),
new SlideSpec ( string . Empty , false , string . Empty , string . Empty , [ new SlideTextBlock ([ new SlideParagraph ( "A new release" , 0 )])], [], [], null , 2 ),
new SlideSpec ( string . Empty , false , "Your questions" , string . Empty , [], [], [], null , 3 ),
]);
const string EXPECTED = """
# Weekly
***
### Consultation hour
* Slide 1 of the template , unchanged *
_Every Monday_
***
### News
* Slide 2 of the template , edited *
- A new release
***
### Your questions
* Slide 3 of the template , edited *
- Ask us
""";
Assert . That ( deck . ToMarkdownOutline ( PresentationTemplate . Read ( file ), new Dictionary < int , string >()), Is . EqualTo ( EXPECTED . ReplaceLineEndings ( "\n" )));
}
[Test]
public void TheOutlineLeavesOutWhatAnEditedSlideRemoves ()
{
using var file = TestTemplate . Create (
new TestTemplateOptions ( Slides : [ new TestSlide ( 0 , ( TestPlaceholder . Title (), "Questions" ), ( new TestPlaceholder ( null , 1 ), "Lorem ipsum" ))]),
new TestLayout ( "Content" , TestPlaceholder . Title (), TestPlaceholder . Text ( 1 , TestTemplate . MASTER_BODY_BOUNDS )));
var deck = new SlideDeckSpec ( "Weekly" , [ new SlideSpec ( string . Empty , false , "Your questions" , string . Empty , [], [], [], null , 1 ) { Removals = [ "text 1" ] }]);
const string EXPECTED = """
# Weekly
***
### Your questions
* Slide 1 of the template , edited *
""";
Assert . That ( deck . ToMarkdownOutline ( PresentationTemplate . Read ( file ), new Dictionary < int , string >()), Is . EqualTo ( EXPECTED . ReplaceLineEndings ( "\n" )));
}
[Test]
public void TheOutlineShowsTheCopiesOfSlotsInReadingOrder ()
{
using var file = TestTemplate . Create ( new TestTemplateOptions ( Slides : [ StackedHeadings . Slide ()]), StackedHeadings . Layout ());
var deck = new SlideDeckSpec ( "Missions" , [ new SlideSpec ( string . Empty , false , "Our missions" , string . Empty , [ new SlideTextBlock ([ new SlideParagraph ( "To Mars" , 0 )])], [ "Mars" ], [], null , 1 ) { Removals = [ "label 3" , "text 3" ] }]);
const string EXPECTED = """
# Missions
***
### Our missions
* Slide 1 of the template , edited *
_Mars_
- To Mars
_Lorem_
- Duis autem
""";
Assert . That ( deck . ToMarkdownOutline ( PresentationTemplate . Read ( file ), new Dictionary < int , string >()), Is . EqualTo ( EXPECTED . ReplaceLineEndings ( "\n" )), "The second heading and its text keep the content of the template; the third ones are removed." );
}
}