diff --git a/documentation/documentation/using/result_document.md b/documentation/documentation/using/result_document.md new file mode 100644 index 000000000..c2a2639e4 --- /dev/null +++ b/documentation/documentation/using/result_document.md @@ -0,0 +1,40 @@ + + +The resulting HTML document from running Storyteller is a self-contained report with all CSS and Javascript packaged inside the HTML file. The **HtmlDocument** itself is assembled by **HtmlDocumentBuilder** out of a collection of builder object **IDocumentPartBuilder**. + +## Default Behavior: DefaultDocumentBuilder / HtmlDocumentBuilder + +**DefaulDocumentBuilder** the default implementation of **HtmlDocumentBuilder** assembles the report from the following internal assembly resources: + +* JS : StoryTeller.batch-bundle.js +* CSS : Storyteller.stylesheets.bootstrap.min.css (v 3.3.2) +* CSS : StoryTeller.stylesheets.storyteller.css +* CSS : StoryTeller.stylesheets.fixed-data-table.min.css + +The best way to modify the generated report is to append additional builders to the **DefaultDocumentBuilder** using the **Add** method with any **IDocumentPartBuilder**. + +## Document Modification: IDocumentPartBuilder + +When the report is being assembled, a **HtmlDocument** and **BatchRunResponse** is passed to each **IDocumentPartBuilder** registered with the **HtmlDocumentBuilder** being executed. Because the **HtmlDocument** is available, any coded behavior could be defined inside a **IDocumentPartBuilder**, including behaviors that replace/modify previous builder results. + +### The build-in builders are: + +* ReportPartBuilder - (*Required*) Creates the report data and container elements in the reports. +* StoryTellerTitleBuilder - Updates the **HtmlDocument** Title property. +* StyleTagBuilder - Creates a style tag from loaded content. +* ScriptTagBuilder - Creates a script tag from loaded content or uri. +* LinkTagBuilder - Creates a link tag from a uri. +* HtmlTagBuilder - A base level class for building self-appending **IDocumentPartBuilder** that are also **HtmlTag** implementations. **StyleTagBuilder**, **ScriptTagBuilder** and **LinkTagBuilder** are examples of this. + + +## Content Resolution: IDocumentPartLoader + +Many of the built-in **IDocumentPartBuilder** classes require some kind of content to be wrapped inside an **HtmlTag**. The content is often stored in files either on the local file system or embedded into the assembly. The **IDocumentPartLoader** exists to bridge the gap between these sources by expecting a **Read** method implementation that results in a content string. + +### The build-in loaders are: + +* VirtualFileLoader - loads a given string as the loaded content, replaces needing a content file to load. +* LocalFileLoader - loads a file from the file system as the loaded content. +* ResourceFileLoader - loads an embedded resource from a given assembly as the loaded content. +* StoryTellerResourceLoader - loads an embedded resource from the storyteller assembly as the loaded content. + diff --git a/src/StoryTeller.Testing/Results/CompoundResourceLoaderTester.cs b/src/StoryTeller.Testing/Results/CompoundResourceLoaderTester.cs new file mode 100644 index 000000000..6c2e74bbb --- /dev/null +++ b/src/StoryTeller.Testing/Results/CompoundResourceLoaderTester.cs @@ -0,0 +1,42 @@ +using Shouldly; +using StoryTeller.Results; +using Xunit; + +namespace StoryTeller.Testing.Results +{ + public class CompoundResourceLoaderTester + { + [Fact] + public void empty_builder_list_returns_an_empty_string() + { + var loader = new CompoundResourceLoader(); + loader.Read().ShouldBeEmpty(); + } + + [Fact] + public void single_builder_returns_the_single_builder_result() + { + var loader = new CompoundResourceLoader() + .AddLoader(new VirtualFileLoader("test")); + loader.Read().ShouldBe("test"); + } + + [Fact] + public void multiple_builder_returns_the_builder_joined_by_default_delimiter() + { + var loader = new CompoundResourceLoader() + .AddLoader(new VirtualFileLoader("test")) + .AddLoader(new VirtualFileLoader("test")); + loader.Read().ShouldBe("test\n\ntest"); + } + + [Fact] + public void multiple_builder_returns_the_builder_joined_by_set_delimiter() + { + var loader = new CompoundResourceLoader("\n") + .AddLoader(new VirtualFileLoader("test")) + .AddLoader(new VirtualFileLoader("test")); + loader.Read().ShouldBe("test\ntest"); + } + } +} diff --git a/src/StoryTeller.Testing/Results/DefaultHtmlDocumentBuilderTester.cs b/src/StoryTeller.Testing/Results/DefaultHtmlDocumentBuilderTester.cs new file mode 100644 index 000000000..4a4c69309 --- /dev/null +++ b/src/StoryTeller.Testing/Results/DefaultHtmlDocumentBuilderTester.cs @@ -0,0 +1,115 @@ +using System.Linq; +using Shouldly; +using StoryTeller.Engine; +using StoryTeller.Results; +using StoryTeller.Util; +using Xunit; + +namespace StoryTeller.Testing.Results +{ + public class DefaultHtmlDocumentBuilderTester + { + + [Fact] + public void additional_builders_can_be_added() + { + var builder = new DefaultHtmlDocumentBuilder(); + builder.Add(new ScriptTagBuilder(string.Empty)); + + builder.Get().Count().ShouldBe(5); + } + + [Fact] + public void defines_four_parts() + { + var builder = new DefaultHtmlDocumentBuilder(); + builder.Get().Count().ShouldBe(4); + } + + [Fact] + public void defines_the_title() + { + var builder = new DefaultHtmlDocumentBuilder(); + builder.Get().ShouldNotBeEmpty(); + } + + [Fact] + public void defines_the_javascript() + { + var builder = new DefaultHtmlDocumentBuilder(); + builder.Get().ShouldNotBeEmpty(); + } + + + [Fact] + public void defines_the_styles() + { + var builder = new DefaultHtmlDocumentBuilder(); + builder.Get().ShouldNotBeEmpty(); + } + + + [Fact] + public void defines_the_report_content() + { + var builder = new DefaultHtmlDocumentBuilder(); + builder.Get().ShouldNotBeEmpty(); + } + + [Fact] + public void building_the_document_results_in_html_document() + { + var response = new BatchRunResponse(); + var builder = new DefaultHtmlDocumentBuilder(); + builder.Build(response).ShouldBeOfType(); + } + + [Fact] + public void result_has_set_title() + { + var response = new BatchRunResponse() { system = "system", suite = "suite"}; + var builder = new DefaultHtmlDocumentBuilder(); + var result = builder.Build(response); + + result.Head.Children[0].TagName().ShouldBe("title"); + result.Head.Children[0].Text().ShouldBe("Storyteller Batch Results for system: suite"); + } + + [Fact] + public void result_has_set_the_style() + { + var response = new BatchRunResponse() { system = "system", suite = "suite"}; + var builder = new DefaultHtmlDocumentBuilder(); + var result = builder.Build(response); + + result.Head.Children[1].TagName().ShouldBe("style"); + result.Head.Children[1].Text().ShouldNotBeEmpty(); + } + + [Fact] + public void result_has_set_the_report() + { + var response = new BatchRunResponse() { system = "system", suite = "suite"}; + var builder = new DefaultHtmlDocumentBuilder(); + var result = builder.Build(response); + + result.Body.Children[0].TagName().ShouldBe("div"); + result.Body.Children[0].Text().ShouldNotBeEmpty(); + + result.Body.Children[1].TagName().ShouldBe("div"); + result.Body.Children[1].Text().ShouldBeEmpty(); + } + + [Fact] + public void result_has_set_the_script() + { + var response = new BatchRunResponse() { system = "system", suite = "suite"}; + var builder = new DefaultHtmlDocumentBuilder(); + var result = builder.Build(response); + + result.Body.Children[2].TagName().ShouldBe("script"); + result.Body.Children[2].Text().ShouldNotBeEmpty(); + } + + } +} diff --git a/src/StoryTeller.Testing/Results/LinkTagBuilderTester.cs b/src/StoryTeller.Testing/Results/LinkTagBuilderTester.cs new file mode 100644 index 000000000..349e4631f --- /dev/null +++ b/src/StoryTeller.Testing/Results/LinkTagBuilderTester.cs @@ -0,0 +1,53 @@ +using System; +using Shouldly; +using StoryTeller.Engine; +using StoryTeller.Results; +using StoryTeller.Util; +using Xunit; + +namespace StoryTeller.Testing.Results +{ + public class LinkTagBuilderTester + { + [Fact] + public void creates_a_link_tag() + { + var builder = new LinkTagBuilder(new Uri("/test", UriKind.Relative)); + builder.TagName().ShouldBe("link"); + } + + [Fact] + public void sets_the_tag_href_attribute_from_uri() + { + var builder = new LinkTagBuilder(new Uri("/test", UriKind.Relative)); + builder.Attr("href").ShouldBe("/test"); + } + + [Fact] + public void set_the_link_relationship_to_stylesheet_by_default() + { + var builder = new LinkTagBuilder(new Uri("/test", UriKind.Relative)); + builder.Attr("rel").ShouldBe("stylesheet"); + } + + [Fact] + public void set_the_link_relationship_to_passed_in_value() + { + var builder = new LinkTagBuilder(new Uri("/test", UriKind.Relative), "rel_test"); + builder.Attr("rel").ShouldBe("rel_test"); + } + + [Fact] + public void the_style_tag_will_attach_itself_to_the_document_head() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + var builder = new LinkTagBuilder(new Uri("/test", UriKind.Relative)); + + builder.Apply(doc, results); + + doc.Head.Children.Count.ShouldBe(2); + doc.Head.Children[1].TagName().ShouldBe("link"); + } + } +} diff --git a/src/StoryTeller.Testing/Results/LocalFileLoaderTester.cs b/src/StoryTeller.Testing/Results/LocalFileLoaderTester.cs new file mode 100644 index 000000000..c25d9d3d4 --- /dev/null +++ b/src/StoryTeller.Testing/Results/LocalFileLoaderTester.cs @@ -0,0 +1,28 @@ +using System.IO; +using Shouldly; +using StoryTeller.Results; +using Xunit; + +namespace StoryTeller.Testing.Results +{ + public class LocalFileLoaderTester + { + [Fact] + public void non_existing_file_return_emptry_string() + { + var loader = new LocalFileLoader("none.existing.file"); + loader.Read().ShouldBeNullOrEmpty(); + } + + [Fact] + public void existing_file_return_the_file_text() + { + var file = Path.GetTempFileName(); + File.WriteAllText(file, "test"); + + var loader = new LocalFileLoader(new FileInfo(file)); + loader.Read().ShouldBe("test"); + File.Delete(file); + } + } +} diff --git a/src/StoryTeller.Testing/Results/ReportPartBuilderTester.cs b/src/StoryTeller.Testing/Results/ReportPartBuilderTester.cs new file mode 100644 index 000000000..7c7569b56 --- /dev/null +++ b/src/StoryTeller.Testing/Results/ReportPartBuilderTester.cs @@ -0,0 +1,83 @@ +using Shouldly; +using StoryTeller.Engine; +using StoryTeller.Results; +using StoryTeller.Util; +using Xunit; + +namespace StoryTeller.Testing.Results +{ + public class ReportPartBuilderTester + { + [Fact] + public void adds_two_tags_to_the_document() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + new ReportPartBuilder().Apply(doc, results); + + doc.Body.Children.Count.ShouldBe(2); + } + + [Fact] + public void adds_a_div_tag_with_id_batch_data() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + new ReportPartBuilder().Apply(doc, results); + + doc.Body.Children[0].TagName().ShouldBe("div"); + doc.Body.Children[0].Id().ShouldBe("batch-data"); + } + + [Fact] + public void batch_data_tag_is_not_empty() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + new ReportPartBuilder().Apply(doc, results); + + doc.Body.Children[0].Text().ShouldNotBeNullOrEmpty(); + } + + [Fact] + public void batch_data_tag_is_hidden() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + new ReportPartBuilder().Apply(doc, results); + + doc.Body.Children[0].Style("display").ShouldBe("none"); + } + + [Fact] + public void adds_a_div_tag_with_id_main() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + new ReportPartBuilder().Apply(doc, results); + + doc.Body.Children[1].TagName().ShouldBe("div"); + doc.Body.Children[1].Id().ShouldBe("main"); + } + + [Fact] + public void main_tag_is_empty() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + new ReportPartBuilder().Apply(doc, results); + + doc.Body.Children[1].Text().ShouldBeNullOrEmpty(); + } + + [Fact] + public void main_tag_is_not_hidden() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + new ReportPartBuilder().Apply(doc, results); + + doc.Body.Children[1].HasStyle("display").ShouldBeFalse(); + } + } +} diff --git a/src/StoryTeller.Testing/Results/ResourceFileLoaderTester.cs b/src/StoryTeller.Testing/Results/ResourceFileLoaderTester.cs new file mode 100644 index 000000000..4a9da4d02 --- /dev/null +++ b/src/StoryTeller.Testing/Results/ResourceFileLoaderTester.cs @@ -0,0 +1,27 @@ +using System.IO; +using Shouldly; +using StoryTeller.Results; +using Xunit; + +namespace StoryTeller.Testing.Results +{ + public class ResourceFileLoaderTester + { + [Fact] + public void non_existing_file_return_emptry_string() + { + var loader = new StoryTellerResourceLoader("none.existing.file"); + loader.Read().ShouldBeNullOrEmpty(); + } + + [Fact] + public void existing_file_return_the_file_text() + { + var file = Path.GetTempFileName(); + File.WriteAllText(file, "test"); + + var loader = new StoryTellerResourceLoader("StoryTeller.batch-bundle.js"); + loader.Read().ShouldNotBeNullOrEmpty(); + } + } +} diff --git a/src/StoryTeller.Testing/Results/ScriptTagBuilderTester.cs b/src/StoryTeller.Testing/Results/ScriptTagBuilderTester.cs new file mode 100644 index 000000000..1fe88168b --- /dev/null +++ b/src/StoryTeller.Testing/Results/ScriptTagBuilderTester.cs @@ -0,0 +1,87 @@ +using System; +using Shouldly; +using StoryTeller.Engine; +using StoryTeller.Results; +using StoryTeller.Util; +using Xunit; + +namespace StoryTeller.Testing.Results +{ + public class ScriptTagBuilderTester + { + [Fact] + public void creates_a_script_tag() + { + var builder = new ScriptTagBuilder(string.Empty); + builder.TagName().ShouldBe("script"); + } + + [Fact] + public void loaded_script_is_wrapped_with_double_blank_lines() + { + var builder = new ScriptTagBuilder(string.Empty); + builder.Text().ShouldBe("\n\n\n\n"); + } + + [Fact] + public void sets_the_tag_content_from_a_string() + { + var builder = new ScriptTagBuilder("test"); + builder.Text().ShouldBe("\n\ntest\n\n"); + } + + [Fact] + public void sets_the_tag_content_from_a_loader() + { + var loader = new VirtualFileLoader("test"); + var builder = new ScriptTagBuilder(loader); + builder.Text().ShouldBe("\n\ntest\n\n"); + } + + [Fact] + public void sets_the_tag_src_from_a_uri() + { + var builder = new ScriptTagBuilder(new Uri("/test", UriKind.Relative)); + builder.Attr("src").ShouldBe("/test"); + } + + [Fact] + public void content_script_tags_dont_create_src_attribute() + { + var builder = new ScriptTagBuilder("test"); + builder.Attr("src").ShouldBeNullOrEmpty(); + } + + [Fact] + public void uri_script_tags_dont_create_text_content() + { + var builder = new ScriptTagBuilder(new Uri("/test", UriKind.Relative)); + builder.Text().ShouldBeNullOrEmpty(); + } + + [Fact] + public void language_is_set_to_javascript_by_default() + { + var builder = new ScriptTagBuilder(string.Empty); + builder.Attr("language").ShouldBe("javascript"); + } + + [Fact] + public void language_is_set_to_passed_in_value() + { + var builder = new ScriptTagBuilder(string.Empty, "test_language"); + builder.Attr("language").ShouldBe("test_language"); + } + + [Fact] + public void the_script_tag_will_attach_itself_to_the_document_body() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + new ScriptTagBuilder(string.Empty).Apply(doc, results); + + doc.Body.Children.Count.ShouldBe(1); + doc.Body.Children[0].TagName().ShouldBe("script"); + } + } +} diff --git a/src/StoryTeller.Testing/Results/StoryTellerTitleBuilderTester.cs b/src/StoryTeller.Testing/Results/StoryTellerTitleBuilderTester.cs new file mode 100644 index 000000000..16fd920e0 --- /dev/null +++ b/src/StoryTeller.Testing/Results/StoryTellerTitleBuilderTester.cs @@ -0,0 +1,41 @@ +using Shouldly; +using StoryTeller.Engine; +using StoryTeller.Results; +using StoryTeller.Util; +using Xunit; + +namespace StoryTeller.Testing.Results +{ + public class StoryTellerTitleBuilderTester + { + [Fact] + public void title_property_is_not_empty() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + new StoryTellerTitleBuilder().Apply(doc, results); + + doc.Title.ShouldNotBeNullOrEmpty(); + } + + [Fact] + public void title_property_contains_the_system_name() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse() { system = "test_system"}; + new StoryTellerTitleBuilder().Apply(doc, results); + + doc.Title.ShouldContain("test_system"); + } + + [Fact] + public void title_property_contains_the_suite_name() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse() { suite = "test_suite"}; + new StoryTellerTitleBuilder().Apply(doc, results); + + doc.Title.ShouldContain("test_suite"); + } + } +} diff --git a/src/StoryTeller.Testing/Results/StyleTagBuilderTester.cs b/src/StoryTeller.Testing/Results/StyleTagBuilderTester.cs new file mode 100644 index 000000000..1ae68d632 --- /dev/null +++ b/src/StoryTeller.Testing/Results/StyleTagBuilderTester.cs @@ -0,0 +1,46 @@ +using Shouldly; +using StoryTeller.Engine; +using StoryTeller.Results; +using StoryTeller.Util; +using Xunit; + +namespace StoryTeller.Testing.Results +{ + public class StyleTagBuilderTester + { + [Fact] + public void creates_a_style_tag() + { + var builder = new StyleTagBuilder(string.Empty); + builder.TagName().ShouldBe("style"); + } + + [Fact] + public void sets_the_tag_content_from_a_string() + { + var builder = new StyleTagBuilder("test"); + builder.Text().ShouldBe("test"); + } + + [Fact] + public void sets_the_tag_content_from_a_loader() + { + var loader = new VirtualFileLoader("test"); + var builder = new StyleTagBuilder(loader); + builder.Text().ShouldBe("test"); + } + + [Fact] + public void the_style_tag_will_attach_itself_to_the_document_head() + { + var doc = new HtmlDocument(); + var results = new BatchRunResponse(); + var builder = new StyleTagBuilder(string.Empty); + + builder.Apply(doc, results); + + doc.Head.Children.Count.ShouldBe(2); + doc.Head.Children[1].TagName().ShouldBe("style"); + } + } +} diff --git a/src/StoryTeller.Testing/Results/VirtualFileLoaderTester.cs b/src/StoryTeller.Testing/Results/VirtualFileLoaderTester.cs new file mode 100644 index 000000000..26902dd70 --- /dev/null +++ b/src/StoryTeller.Testing/Results/VirtualFileLoaderTester.cs @@ -0,0 +1,23 @@ +using Shouldly; +using StoryTeller.Results; +using Xunit; + +namespace StoryTeller.Testing.Results +{ + public class VirtualFileLoaderTester + { + [Fact] + public void null_return_emptry_string() + { + var loader = new VirtualFileLoader(null); + loader.Read().ShouldBeEmpty(); + } + + [Fact] + public void none_null_or_empty_return_text() + { + var loader = new VirtualFileLoader("test"); + loader.Read().ShouldBe("test"); + } + } +} diff --git a/src/StoryTeller/Results/BatchResultsWriter.cs b/src/StoryTeller/Results/BatchResultsWriter.cs index 5b224f743..3149e045f 100644 --- a/src/StoryTeller/Results/BatchResultsWriter.cs +++ b/src/StoryTeller/Results/BatchResultsWriter.cs @@ -1,69 +1,20 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using Baseline; +using System.Linq; using StoryTeller.Engine; -using StoryTeller.Remotes.Messaging; using StoryTeller.Util; namespace StoryTeller.Results { public static class BatchResultsWriter { - public static HtmlDocument BuildResults(BatchRunResponse results) - { - var document = new HtmlDocument - { - Title = "Storyteller Batch Results for {0}: {1}".ToFormat(results.system, results.suite) - }; - - WriteCSS(document); - writeJavascript(results, document); - - return document; - } - - - public static void WriteCSS(HtmlDocument document) - { - var styleTag = StyleTag(); - - document.Head.Append(styleTag); - } - - public static HtmlTag StyleTag() - { - var css = readFile("Storyteller.stylesheets.bootstrap.min.css") + "\n\n" + readFile("StoryTeller.stylesheets.storyteller.css"); - css += "\n\n" + readFile("StoryTeller.stylesheets.fixed-data-table.min.css"); + private static HtmlDocumentBuilder Builder { get; set; } = new DefaultHtmlDocumentBuilder(); - return new HtmlTag("style").Text(css).Encoded(false); - } - - - private static string readFile(string name) - { - var assembly = typeof(BatchResultsWriter).GetTypeInfo().Assembly; - var names = assembly.GetManifestResourceNames(); - var actualName = names.FirstOrDefault(x => x.EqualsIgnoreCase(name)); - - var stream = assembly.GetManifestResourceStream(actualName); - var reader = new StreamReader(stream); - return reader.ReadToEnd(); - } + public static HtmlDocument BuildResults(BatchRunResponse results) => Builder.Build(results); + public static HtmlTag StyleTag() => Builder.Get().FirstOrDefault(); - private static void writeJavascript(BatchRunResponse results, HtmlDocument document) + public static void SetBuilder(HtmlDocumentBuilder builder) { - var cleanJson = JsonSerialization.ToCleanJson(results); - - document.Body.Add("div").Hide().Id("batch-data").Text(cleanJson); - document.Body.Add("div").Id("main"); - - var js = readFile("StoryTeller.batch-bundle.js"); - - document.Body.Add("script").Attr("language", "javascript").Text("\n\n" + js + "\n\n").Encoded(false); - - + Builder = builder; } + } -} \ No newline at end of file +} diff --git a/src/StoryTeller/Results/CompoundResourceLoader.cs b/src/StoryTeller/Results/CompoundResourceLoader.cs new file mode 100644 index 000000000..9d66d6445 --- /dev/null +++ b/src/StoryTeller/Results/CompoundResourceLoader.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using Baseline; + +namespace StoryTeller.Results +{ + /// + /// Joins multiple contents into a single resource. + /// + public class CompoundResourceLoader : IDocumentPartLoader + { + private readonly string delimiter; + private List sources; + + /// + /// Creates an instance of . + /// + /// The join delimiter. Default is two new lines. + public CompoundResourceLoader(string delimiter = "\n\n") + { + this.delimiter = delimiter; + this.sources = new List(); + } + + /// + /// Add a loader to the joined content. + /// + /// + /// + public CompoundResourceLoader AddLoader(IDocumentPartLoader loader) + { + this.sources.Add(loader); + return this; + } + + /// + /// Renders the content of each child into one string. + /// + /// The text of all child loaders. + public string Read() + { + return string.Join(this.delimiter, sources.Select(n => n.Read())); + } + } +} diff --git a/src/StoryTeller/Results/DefaultHtmlDocumentBuilder.cs b/src/StoryTeller/Results/DefaultHtmlDocumentBuilder.cs new file mode 100644 index 000000000..d4eb3fea4 --- /dev/null +++ b/src/StoryTeller/Results/DefaultHtmlDocumentBuilder.cs @@ -0,0 +1,27 @@ +namespace StoryTeller.Results +{ + /// + /// Defines the document builders required to bootstrap the default script and styles + /// for storyteller. + /// + public class DefaultHtmlDocumentBuilder : HtmlDocumentBuilder + { + /// + /// Creates an instance of by registering the + /// required JS and CSS bundles along with the packaged report results. + /// + public DefaultHtmlDocumentBuilder() + { + var jsBundle = new StoryTellerResourceLoader("StoryTeller.batch-bundle.js"); + var cssBundle = new CompoundResourceLoader() + .AddLoader(new StoryTellerResourceLoader("Storyteller.stylesheets.bootstrap.min.css")) + .AddLoader(new StoryTellerResourceLoader("StoryTeller.stylesheets.storyteller.css")) + .AddLoader(new StoryTellerResourceLoader("StoryTeller.stylesheets.fixed-data-table.min.css")); + + this.Add(new StoryTellerTitleBuilder()); + this.Add(new StyleTagBuilder(cssBundle)); + this.Add(new ReportPartBuilder()); + this.Add(new ScriptTagBuilder(jsBundle)); + } + } +} diff --git a/src/StoryTeller/Results/HtmlDocumentBuilder.cs b/src/StoryTeller/Results/HtmlDocumentBuilder.cs new file mode 100644 index 000000000..aad0dcf8a --- /dev/null +++ b/src/StoryTeller/Results/HtmlDocumentBuilder.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using StoryTeller.Engine; +using StoryTeller.Util; + +namespace StoryTeller.Results +{ + /// + /// The abstract class responsible for assembling html builders against an + /// object. The default implementation + /// is automatically configured in . + /// + public abstract class HtmlDocumentBuilder + { + private List Parts { get; } = new List(); + + /// + /// Creates the HtmlDocument with by applying each individual builder against + /// the new . + /// + /// + /// + public HtmlDocument Build(BatchRunResponse results) + { + var document = new HtmlDocument(); + foreach (var builder in this.Parts) + { + builder.Apply(document, results); + } + + return document; + } + + /// + /// Exposes the internal builders to be consumed. + /// + /// + /// + public IEnumerable Get() + { + return this.Parts.OfType(); + } + + /// + /// Adds additional builder to the current builder execution chain. + /// + /// + public void Add(IDocumentPartBuilder builder) + { + this.Parts.Add(builder); + } + } +} diff --git a/src/StoryTeller/Results/HtmlTagBuilder.cs b/src/StoryTeller/Results/HtmlTagBuilder.cs new file mode 100644 index 000000000..c46dee76b --- /dev/null +++ b/src/StoryTeller/Results/HtmlTagBuilder.cs @@ -0,0 +1,38 @@ +using StoryTeller.Engine; +using StoryTeller.Util; + +namespace StoryTeller.Results +{ + /// + /// Simplifies the implementation by allowing + /// to build tags that register themselves to the report. + /// + public abstract class HtmlTagBuilder : HtmlTag, IDocumentPartBuilder + { + /// + /// Creates an instance of . + /// + /// The tag name that is being created. + protected HtmlTagBuilder(string tag) : base(tag, (HtmlTag) null) + { + } + + /// + /// EAppends this to current document. + /// + /// The current document being modified. + /// The results of the storyteller run. + public void Apply(HtmlDocument document, BatchRunResponse results) + { + this.AttachTo(document).Append(this); + } + + /// + /// Selects the element from the document to be used as the target of + /// the append function. + /// + /// + /// + protected virtual HtmlTag AttachTo(HtmlDocument document) => document.Body; + } +} diff --git a/src/StoryTeller/Results/IDocumentPartBuilder.cs b/src/StoryTeller/Results/IDocumentPartBuilder.cs new file mode 100644 index 000000000..56bc498b0 --- /dev/null +++ b/src/StoryTeller/Results/IDocumentPartBuilder.cs @@ -0,0 +1,18 @@ +using StoryTeller.Engine; +using StoryTeller.Util; + +namespace StoryTeller.Results +{ + /// + /// A component for building reports. + /// + public interface IDocumentPartBuilder + { + /// + /// Executes logic that modifies the being generated. + /// + /// The current document being modified. + /// The results of the storyteller run. + void Apply(HtmlDocument document, BatchRunResponse results); + } +} diff --git a/src/StoryTeller/Results/IDocumentPartLoader.cs b/src/StoryTeller/Results/IDocumentPartLoader.cs new file mode 100644 index 000000000..55f103b12 --- /dev/null +++ b/src/StoryTeller/Results/IDocumentPartLoader.cs @@ -0,0 +1,17 @@ +namespace StoryTeller.Results +{ + /// + /// Provides a way to load chunks of text as content for + /// implementations. Allows us to treat files being pulled from assembly resources + /// the same way as local file system files and any other text content source we want + /// to wrap in this interface. + /// + public interface IDocumentPartLoader + { + /// + /// Renders the content of the document part as text. + /// + /// The text of the document part. + string Read(); + } +} diff --git a/src/StoryTeller/Results/LinkTagBuilder.cs b/src/StoryTeller/Results/LinkTagBuilder.cs new file mode 100644 index 000000000..ba2678796 --- /dev/null +++ b/src/StoryTeller/Results/LinkTagBuilder.cs @@ -0,0 +1,25 @@ +using System; +using StoryTeller.Util; + +namespace StoryTeller.Results +{ + /// + /// A self attaching link tag used for building the . + /// + public class LinkTagBuilder : HtmlTagBuilder + { + /// + /// Creates an instance of . + /// + /// The uri of the linked content. + /// The relationship to the document. Default is "stylesheet". + public LinkTagBuilder(Uri path, string relationship = "stylesheet") + : base("link") + { + this.Attr("rel", relationship); + this.Attr("href", path); + } + + protected override HtmlTag AttachTo(HtmlDocument document) => document.Head; + } +} diff --git a/src/StoryTeller/Results/LocalFileLoader.cs b/src/StoryTeller/Results/LocalFileLoader.cs new file mode 100644 index 000000000..e8ced782c --- /dev/null +++ b/src/StoryTeller/Results/LocalFileLoader.cs @@ -0,0 +1,49 @@ +using System.IO; + +namespace StoryTeller.Results +{ + /// + /// Loads file system files to be used as content in + /// classes. + /// + public class LocalFileLoader : IDocumentPartLoader + { + private readonly FileInfo file; + + /// + /// Creates an instance of with a local file. + /// + /// The name of the file to load. + public LocalFileLoader(string name) + { + this.file = new FileInfo(name); + } + + /// + /// Creates an instance of with a local file. + /// + /// The file info of the file to load. + public LocalFileLoader(FileInfo file) + { + this.file = file; + } + + /// + /// Reads the content of the file. + /// + /// The text content of the file. + public string Read() + { + if (!file.Exists) + { + return string.Empty; + } + + using (var stream = file.OpenRead()) + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/src/StoryTeller/Results/ReportPartBuilder.cs b/src/StoryTeller/Results/ReportPartBuilder.cs new file mode 100644 index 000000000..59ff2a653 --- /dev/null +++ b/src/StoryTeller/Results/ReportPartBuilder.cs @@ -0,0 +1,26 @@ +using StoryTeller.Engine; +using StoryTeller.Remotes.Messaging; +using StoryTeller.Util; + +namespace StoryTeller.Results +{ + /// + /// Renders the report content to the + /// + public class ReportPartBuilder : IDocumentPartBuilder + { + /// + /// Appends the cleaned json of the document and + /// creates the #main div element for the report container. + /// + /// The current document being modified. + /// The results of the storyteller run. + public void Apply(HtmlDocument document, BatchRunResponse results) + { + var cleanJson = JsonSerialization.ToCleanJson(results); + + document.Body.Add("div").Id("batch-data").Text(cleanJson).Hide(); + document.Body.Add("div").Id("main"); + } + } +} diff --git a/src/StoryTeller/Results/ResourceFileLoader.cs b/src/StoryTeller/Results/ResourceFileLoader.cs new file mode 100644 index 000000000..72430bc0a --- /dev/null +++ b/src/StoryTeller/Results/ResourceFileLoader.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Baseline; + +namespace StoryTeller.Results +{ + /// + /// Loads a resource file from an assembly to render it as text content. + /// + /// A from the assembly to use as the resource source. + public class ResourceFileLoader : ResourceFileLoader + where TAssemblySource : class + { + /// + /// Creates an instance of + /// + /// Name of the resource in the assembly. + public ResourceFileLoader(string name) : base(name, typeof(TAssemblySource).Assembly) + { + } + } + + /// + /// Loads a resource file from an assembly to render it as text content. + /// + public class ResourceFileLoader : IDocumentPartLoader + { + private readonly string name; + private readonly Assembly assembly; + + /// + /// Creates an instance of + /// + /// Name of the resource in the assembly. + /// The assembly to look for the file in. + public ResourceFileLoader(string name, Assembly assembly) + { + this.name = name; + this.assembly = assembly; + } + + /// + /// Renders the content resource file from the assembly as text. + /// + /// The text of the document part. + public string Read() + { + var names = assembly.GetManifestResourceNames(); + var actualName = names.FirstOrDefault(x => x.EqualsIgnoreCase(name)); + if (actualName == null) + { + return string.Empty; + } + + using (var stream = assembly.GetManifestResourceStream(actualName)) + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/src/StoryTeller/Results/ScriptTagBuilder.cs b/src/StoryTeller/Results/ScriptTagBuilder.cs new file mode 100644 index 000000000..c54ab5696 --- /dev/null +++ b/src/StoryTeller/Results/ScriptTagBuilder.cs @@ -0,0 +1,47 @@ +using System; +using StoryTeller.Engine; +using StoryTeller.Util; + +namespace StoryTeller.Results +{ + /// + /// A self attaching script tag used for building the . + /// + public class ScriptTagBuilder : HtmlTagBuilder + { + /// + /// Creates an instance of a . + /// + /// A with script content. + /// The language being defined in the script tag. Defaults to "javascript". + public ScriptTagBuilder(IDocumentPartLoader loader, string language ="javascript") + : base("script") + { + this.Attr("language", language); + this.Text("\n\n" + loader.Read() + "\n\n"); + this.Encoded(false); + } + + /// + /// Creates an instance of a . + /// + /// A script content string. + /// The language being defined in the script tag. Defaults to "javascript". + public ScriptTagBuilder(string content, string language = "javascript") + : this(new VirtualFileLoader(content), language) + { + } + + /// + /// Creates an instance of a . + /// + /// The url to the script resource. + /// The language being defined in the script tag. Defaults to "javascript". + public ScriptTagBuilder(Uri path, string language = "javascript") + : base("script") + { + this.Attr("language", language); + this.Attr("src", path); + } + } +} diff --git a/src/StoryTeller/Results/StoryTellerResourceLoader.cs b/src/StoryTeller/Results/StoryTellerResourceLoader.cs new file mode 100644 index 000000000..9093b92e7 --- /dev/null +++ b/src/StoryTeller/Results/StoryTellerResourceLoader.cs @@ -0,0 +1,17 @@ +namespace StoryTeller.Results +{ + /// + /// A shorthand class to facilitate + /// loading files from the StoryTeller assembly. + /// + public class StoryTellerResourceLoader : ResourceFileLoader + { + /// + /// Creates an instance of . + /// + /// The name of the resource to load. + public StoryTellerResourceLoader(string name) : base(name) + { + } + } +} diff --git a/src/StoryTeller/Results/StoryTellerTitleBuilder.cs b/src/StoryTeller/Results/StoryTellerTitleBuilder.cs new file mode 100644 index 000000000..a30c8c6df --- /dev/null +++ b/src/StoryTeller/Results/StoryTellerTitleBuilder.cs @@ -0,0 +1,22 @@ +using Baseline; +using StoryTeller.Engine; +using StoryTeller.Util; + +namespace StoryTeller.Results +{ + /// + /// Builds the storyteller title using values from the . + /// + public class StoryTellerTitleBuilder : IDocumentPartBuilder + { + /// + /// Sets the Title property on the being generated. + /// + /// The current document being modified. + /// The results of the storyteller run. + public void Apply(HtmlDocument document, BatchRunResponse results) + { + document.Title = "Storyteller Batch Results for {0}: {1}".ToFormat(results.system, results.suite); + } + } +} diff --git a/src/StoryTeller/Results/StyleTagBuilder.cs b/src/StoryTeller/Results/StyleTagBuilder.cs new file mode 100644 index 000000000..46dc03e7f --- /dev/null +++ b/src/StoryTeller/Results/StyleTagBuilder.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using StoryTeller.Grammars.Paragraphs; +using StoryTeller.Util; + +namespace StoryTeller.Results +{ + /// + /// A self attaching style tag used for building the . + /// + public class StyleTagBuilder : HtmlTagBuilder, IDocumentPartBuilder + { + /// + /// Creates an instance of a . + /// + /// A with CSS content. + public StyleTagBuilder(IDocumentPartLoader loader) + : base("style") + { + this.Text(loader.Read()); + this.Encoded(false); + } + + /// + /// Creates an instance of a . + /// + /// A CSS string. + public StyleTagBuilder(string content) + : this(new VirtualFileLoader(content)) + { + } + + /// + /// Selects the Head html element as the target of this + /// + /// The being modified. + /// The to append the builder content. + protected override HtmlTag AttachTo(HtmlDocument document) => document.Head; + + } +} diff --git a/src/StoryTeller/Results/VirtualFileLoader.cs b/src/StoryTeller/Results/VirtualFileLoader.cs new file mode 100644 index 000000000..0db099a66 --- /dev/null +++ b/src/StoryTeller/Results/VirtualFileLoader.cs @@ -0,0 +1,28 @@ +namespace StoryTeller.Results +{ + /// + /// A pass through class allowing use to use strings as content of a file. + /// + public class VirtualFileLoader : IDocumentPartLoader + { + private readonly string content; + + /// + /// Creates an instance of . + /// + /// + public VirtualFileLoader(string content) + { + this.content = content; + } + + /// + /// Renders the content of the stored string. + /// + /// The stored content string. + public string Read() + { + return this.content ?? string.Empty; + } + } +}