mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 13:07:21 +00:00
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
273 lines
10 KiB
C#
273 lines
10 KiB
C#
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);
|
|
Assert.Multiple(() =>
|
|
{
|
|
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];
|
|
Assert.Multiple(() =>
|
|
{
|
|
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()
|
|
{
|
|
Assert.Multiple(() =>
|
|
{
|
|
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" } ] }""";
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
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<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);
|
|
Assert.Multiple(() =>
|
|
{
|
|
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);
|
|
Assert.Multiple(() =>
|
|
{
|
|
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<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.");
|
|
}
|
|
} |