using AIStudio.Tools.PowerPoint; using P = DocumentFormat.OpenXml.Presentation; namespace AIStudio.Tests.Tools.PowerPoint; /// /// Checks how the slide deck is read out of a model answer, and how it is shown as an outline. /// [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); using (Assert.EnterMultipleScope()) { 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) })); } } [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]; using (Assert.EnterMultipleScope()) { 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."); } } [Test] public void AnAnswerWithoutJsonIsReported() { using (Assert.EnterMultipleScope()) { Assert.That(SlideDeckSpec.TryParse("I cannot help with that.", out _, out var issue), Is.False); Assert.That(issue, Does.Contain("no complete JSON object")); } } [Test] public void BrokenJsonIsReported() { const string ANSWER = """{ "title": "Mars", "slides": [ { "layout": "L1",, "title": "Mars" } ] }"""; using (Assert.EnterMultipleScope()) { 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."); } } [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 { [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); using (Assert.EnterMultipleScope()) { 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); } } [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); using (Assert.EnterMultipleScope()) { 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."); } } [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()), 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()), 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()), Is.EqualTo(EXPECTED.ReplaceLineEndings("\n")), "The second heading and its text keep the content of the template; the third ones are removed."); } }