diff --git a/src/Dfe.ContentSupport.Web.Node/styles/scss/vertical-navigation.scss b/src/Dfe.ContentSupport.Web.Node/styles/scss/vertical-navigation.scss new file mode 100644 index 0000000..6327975 --- /dev/null +++ b/src/Dfe.ContentSupport.Web.Node/styles/scss/vertical-navigation.scss @@ -0,0 +1,41 @@ +:root { + --govuk-link-color: #1d70b8; + --govuk-black: #0b0c0c; +} + +.dfe-vertical-nav__section-item { + list-style-type: none; + font-size: 1rem; +} + +.dfe-vertical-nav__link { + display: block; + padding: 7px 30px 8px 10px; + border-left: 4px solid #b1b4b6; + text-decoration: none; +} + +.dfe-vertical-nav__link--selected { + border-left: 4px solid var(--govuk-link-color); + background-color: #f3f2f1; + font-weight: bold; +} + +.dfe-vertical-nav__link, +.dfe-vertical-nav__link--selected { + color: var(--govuk-link-color); + + &:active, &:hover { + background-color: #fd0; + color: var(--govuk-black); + border-left: 4px solid var(--govuk-black); + font-weight: normal; + } +} + +.dfe-vertical-nav__theme { + border-top: 1px solid var(--govuk-link-color); + padding-top: 5px; + margin-top: 10px; + margin-left: 0; +} diff --git a/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs b/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs index d9c4ec0..681091a 100644 --- a/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs +++ b/src/Dfe.ContentSupport.Web/Controllers/ContentController.cs @@ -9,7 +9,7 @@ namespace Dfe.ContentSupport.Web.Controllers; [Route("/content")] [AllowAnonymous] -public class ContentController(IContentService contentService) +public class ContentController(IContentService contentService, ILayoutService layoutService) : Controller { public async Task Home() @@ -28,14 +28,17 @@ public async Task Home() return View(defaultModel); } - [HttpGet("{slug}")] - public async Task Index(string slug, bool isPreview = false) + [HttpGet("{slug}/{page?}")] + public async Task Index(string slug, string page = "", bool isPreview = false) { if (!ModelState.IsValid) return RedirectToAction("error"); if (string.IsNullOrEmpty(slug)) return RedirectToAction("error"); var resp = await contentService.GetContent(slug, isPreview); if (resp is null) return RedirectToAction("error"); + + resp = layoutService.GenerateLayout(resp, Request, page); + return View("CsIndex", resp); } @@ -48,6 +51,6 @@ public IActionResult Privacy() public IActionResult Error() { return View(new ErrorViewModel - { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } } \ No newline at end of file diff --git a/src/Dfe.ContentSupport.Web/Extensions/WebApplicationBuilderExtensions.cs b/src/Dfe.ContentSupport.Web/Extensions/WebApplicationBuilderExtensions.cs index 16a9078..71ea637 100644 --- a/src/Dfe.ContentSupport.Web/Extensions/WebApplicationBuilderExtensions.cs +++ b/src/Dfe.ContentSupport.Web/Extensions/WebApplicationBuilderExtensions.cs @@ -24,6 +24,7 @@ public static void InitCsDependencyInjection(this WebApplicationBuilder app) app.Services.AddTransient(); app.Services.AddTransient(); app.Services.AddTransient(); + app.Services.AddTransient(); app.Services.Configure(options => { diff --git a/src/Dfe.ContentSupport.Web/Models/ContentBase.cs b/src/Dfe.ContentSupport.Web/Models/ContentBase.cs index 636506b..c1b04e3 100644 --- a/src/Dfe.ContentSupport.Web/Models/ContentBase.cs +++ b/src/Dfe.ContentSupport.Web/Models/ContentBase.cs @@ -6,4 +6,8 @@ namespace Dfe.ContentSupport.Web.Models; public class ContentBase : ContentType { public string InternalName { get; set; } = null!; + + public string? Title { get; set; } = null; + + public string? Subtitle { get; set; } = null; } \ No newline at end of file diff --git a/src/Dfe.ContentSupport.Web/Models/Mapped/CsContentItem.cs b/src/Dfe.ContentSupport.Web/Models/Mapped/CsContentItem.cs index ce02e65..7255ede 100644 --- a/src/Dfe.ContentSupport.Web/Models/Mapped/CsContentItem.cs +++ b/src/Dfe.ContentSupport.Web/Models/Mapped/CsContentItem.cs @@ -6,4 +6,6 @@ namespace Dfe.ContentSupport.Web.Models.Mapped; public class CsContentItem { public string InternalName { get; set; } = null!; + public string? Title { get; set; } = null; + public string? Subtitle { get; set; } = null; } \ No newline at end of file diff --git a/src/Dfe.ContentSupport.Web/Models/Mapped/CsPage.cs b/src/Dfe.ContentSupport.Web/Models/Mapped/CsPage.cs index e933fee..91f70cb 100644 --- a/src/Dfe.ContentSupport.Web/Models/Mapped/CsPage.cs +++ b/src/Dfe.ContentSupport.Web/Models/Mapped/CsPage.cs @@ -9,9 +9,12 @@ public class CsPage public string Slug { get; set; } = null!; public bool IsSitemap { get; set; } public bool HasCitation { get; set; } + public bool ShowVerticalNavigation { get; set; } public bool HasBackToTop { get; set; } public List Content { get; set; } = null!; public DateTime? CreatedAt { get; init; } public DateTime? UpdatedAt { get; init; } public bool HasFeedbackBanner { get; set; } + public List? MenuItems { get; set; } + } \ No newline at end of file diff --git a/src/Dfe.ContentSupport.Web/Models/Mapped/PageLink.cs b/src/Dfe.ContentSupport.Web/Models/Mapped/PageLink.cs new file mode 100644 index 0000000..8e4c640 --- /dev/null +++ b/src/Dfe.ContentSupport.Web/Models/Mapped/PageLink.cs @@ -0,0 +1,10 @@ +namespace Dfe.ContentSupport.Web.Models.Mapped +{ + public class PageLink + { + public string? Title { get; set; } = null; + public string? Subtitle { get; set; } = null; + public required string Url { get; set; } + public required bool IsActive { get; set; } + } +} diff --git a/src/Dfe.ContentSupport.Web/Program.cs b/src/Dfe.ContentSupport.Web/Program.cs index 62b2e40..6199934 100644 --- a/src/Dfe.ContentSupport.Web/Program.cs +++ b/src/Dfe.ContentSupport.Web/Program.cs @@ -55,7 +55,7 @@ public static void Main(string[] args) app.MapControllerRoute( name: "slug", - pattern: "{slug}", + pattern: "{slug}/{page?}", defaults: new { controller = "Content", action = "Index" }); diff --git a/src/Dfe.ContentSupport.Web/Services/ILayoutService.cs b/src/Dfe.ContentSupport.Web/Services/ILayoutService.cs new file mode 100644 index 0000000..d028752 --- /dev/null +++ b/src/Dfe.ContentSupport.Web/Services/ILayoutService.cs @@ -0,0 +1,10 @@ +using Dfe.ContentSupport.Web.Models.Mapped; + +namespace Dfe.ContentSupport.Web.Services +{ + public interface ILayoutService + { + CsPage GenerateLayout(CsPage page, HttpRequest request, string pageName); + + } +} diff --git a/src/Dfe.ContentSupport.Web/Services/LayoutService.cs b/src/Dfe.ContentSupport.Web/Services/LayoutService.cs new file mode 100644 index 0000000..1f559c0 --- /dev/null +++ b/src/Dfe.ContentSupport.Web/Services/LayoutService.cs @@ -0,0 +1,85 @@ +using Dfe.ContentSupport.Web.Models; +using Dfe.ContentSupport.Web.Models.Mapped; + + +namespace Dfe.ContentSupport.Web.Services +{ + public class LayoutService : ILayoutService + { + public CsPage GenerateLayout(CsPage page, HttpRequest request, string pageName) + { + if (!page.ShowVerticalNavigation) return page; + + return new() + { + Heading = GetHeading(page, pageName), + MenuItems = GenerateVerticalNavigation(page, request, pageName), + Content = GetVisiblePageList(page, pageName), + UpdatedAt = page.UpdatedAt, + CreatedAt = page.CreatedAt, + HasCitation = page.HasCitation, + HasBackToTop = page.HasBackToTop, + IsSitemap = page.IsSitemap, + ShowVerticalNavigation = page.ShowVerticalNavigation, + Slug = page.Slug, + }; + } + + + public Heading GetHeading(CsPage page, string pageName) + { + var selectedPage = page.Content.Find(o => o.InternalName == pageName); + + if (selectedPage != null) + return new() + { + Title = selectedPage.Title ?? "", + Subtitle = selectedPage.Subtitle ?? "" + }; + + + return new() + { + Title = page.Content[0]?.Title ?? "", + Subtitle = page.Content[0]?.Subtitle ?? "" + }; + } + + + public List GenerateVerticalNavigation(CsPage page, HttpRequest request, string pageName) + { + var baseUrl = GetNavigationUrl(request); + + var menuItems = page.Content.Select(o => new PageLink() + { + Title = o.Title ?? "", + Subtitle = o.Subtitle ?? "", + Url = $"{baseUrl}/{o.InternalName}", + IsActive = pageName == o.InternalName + }).ToList(); + + if (string.IsNullOrEmpty(pageName) && menuItems.Count > 0) + menuItems[0].IsActive = true; + + return menuItems; + } + + + public List GetVisiblePageList(CsPage page, string pageName) + { + if (!string.IsNullOrEmpty(pageName)) + return page.Content.Where(o => o.InternalName == pageName).ToList(); + + + return page.Content.GetRange(0, 1); + } + + + public string GetNavigationUrl(HttpRequest request) + { + var splitUrl = request.Path.ToString().Split("/"); + return string.Join("/", splitUrl.Take(3)); + } + + } +} diff --git a/src/Dfe.ContentSupport.Web/Services/ModelMapper.cs b/src/Dfe.ContentSupport.Web/Services/ModelMapper.cs index c8d4bef..b5f1a9b 100644 --- a/src/Dfe.ContentSupport.Web/Services/ModelMapper.cs +++ b/src/Dfe.ContentSupport.Web/Services/ModelMapper.cs @@ -27,6 +27,7 @@ public CsPage MapToCsPage(ContentSupportPage incoming) HasBackToTop = incoming.HasBackToTop, HasFeedbackBanner = incoming.HasFeedbackBanner, Content = MapEntriesToContent(incoming.Content), + ShowVerticalNavigation = incoming.ShowVerticalNavigation, CreatedAt = incoming.Sys.CreatedAt, UpdatedAt = incoming.Sys.UpdatedAt }; @@ -41,18 +42,20 @@ private List MapEntriesToContent(List entries) public CsContentItem ConvertEntryToContentItem(Entry entry) { CsContentItem item = entry.RichText is not null - ? MapRichTextContent(entry.RichText)! - : new CsContentItem { InternalName = entry.InternalName }; + ? MapRichTextContent(entry.RichText, entry)! + : new CsContentItem { InternalName = entry.InternalName, Title = entry.Title, Subtitle = entry.Subtitle }; return item; } - public RichTextContentItem? MapRichTextContent(ContentItemBase? richText) + public RichTextContentItem? MapRichTextContent(ContentItemBase? richText, Entry entry) { if (richText is null) return null; RichTextContentItem item = new RichTextContentItem { - InternalName = richText.InternalName, + InternalName = entry.InternalName, + Title = entry.Title, + Subtitle = entry.Subtitle, NodeType = ConvertToRichTextNodeType(richText.NodeType), Content = MapRichTextNodes(richText.Content), }; @@ -62,7 +65,7 @@ public CsContentItem ConvertEntryToContentItem(Entry entry) public List MapRichTextNodes(List nodes) { return nodes.Select(node => MapContent(node) ?? new RichTextContentItem - { NodeType = RichTextNodeType.Unknown, InternalName = node.InternalName }).ToList(); + { NodeType = RichTextNodeType.Unknown, InternalName = node.InternalName }).ToList(); } public RichTextContentItem? MapContent(ContentItem contentItem) @@ -102,7 +105,7 @@ public List MapRichTextNodes(List nodes) item = new EmbeddedEntry { JumpIdentifier = target.JumpIdentifier, - RichText = MapRichTextContent(target.RichText), + RichText = MapRichTextContent(target.RichText, target), CustomComponent = GenerateCustomComponent(target) }; break; @@ -155,7 +158,7 @@ private CustomAccordion GenerateCustomAccordion(Target target) return new CustomAccordion { InternalName = target.InternalName, - Body = MapRichTextContent(target.RichText), + Body = MapRichTextContent(target.RichText, target), SummaryLine = target.SummaryLine, Title = target.Title, Accordions = target.Content.Select(GenerateCustomAccordion).ToList() diff --git a/src/Dfe.ContentSupport.Web/ViewModels/ContentSupportPage.cs b/src/Dfe.ContentSupport.Web/ViewModels/ContentSupportPage.cs index 6b6cf1d..350bcd8 100644 --- a/src/Dfe.ContentSupport.Web/ViewModels/ContentSupportPage.cs +++ b/src/Dfe.ContentSupport.Web/ViewModels/ContentSupportPage.cs @@ -18,4 +18,6 @@ public class ContentSupportPage : ContentBase public bool HasCitation { get; init; } public bool HasBackToTop { get; init; } public bool HasFeedbackBanner { get; init; } + public bool ShowVerticalNavigation { get; init; } + } \ No newline at end of file diff --git a/src/Dfe.ContentSupport.Web/Views/Content/CsIndex.cshtml b/src/Dfe.ContentSupport.Web/Views/Content/CsIndex.cshtml index 7a91cdd..29b5d31 100644 --- a/src/Dfe.ContentSupport.Web/Views/Content/CsIndex.cshtml +++ b/src/Dfe.ContentSupport.Web/Views/Content/CsIndex.cshtml @@ -5,13 +5,34 @@ }
+ + @if (Model.MenuItems is not null) + { +
+ +
+ } +
@foreach (var content in Model.Content) { } +
+
@if (Model.HasCitation) { diff --git a/src/Dfe.ContentSupport.Web/Views/Shared/Error.cshtml b/src/Dfe.ContentSupport.Web/Views/Shared/Error.cshtml index e44e743..5c54262 100644 --- a/src/Dfe.ContentSupport.Web/Views/Shared/Error.cshtml +++ b/src/Dfe.ContentSupport.Web/Views/Shared/Error.cshtml @@ -10,7 +10,7 @@ {

Request ID: @Model.RequestId -

+

}

Development Mode

diff --git a/src/Dfe.ContentSupport.Web/wwwroot/css/cands-site.css b/src/Dfe.ContentSupport.Web/wwwroot/css/cands-site.css index dd61433..6753196 100644 --- a/src/Dfe.ContentSupport.Web/wwwroot/css/cands-site.css +++ b/src/Dfe.ContentSupport.Web/wwwroot/css/cands-site.css @@ -10,62 +10,62 @@ video { position: relative; } -.attachment:after { - content: ''; - display: block; - clear: both; -} + .attachment:after { + content: ''; + display: block; + clear: both; + } -.attachment .attachment-title { - font-weight: 400; - font-size: 1.6875rem; - line-height: 1.1111111111; - margin: 0 0 15px; -} + .attachment .attachment-title { + font-weight: 400; + font-size: 1.6875rem; + line-height: 1.1111111111; + margin: 0 0 15px; + } -.attachment .attachment-title .attachment-link { - line-height: 1.29; -} + .attachment .attachment-title .attachment-link { + line-height: 1.29; + } -.attachment .attachment-details { - padding-left: 134px; -} + .attachment .attachment-details { + padding-left: 134px; + } -.attachment .attachment-thumbnail { - position: relative; - float: left; - margin-right: 25px; - margin-bottom: 15px; - padding: 5px; - display: block; - max-height: 140px; - max-width: 99px; - border: #e6e6e6; - outline: 5px solid #e6e6e6; - background: #ffffff; - box-shadow: 0 2px 2px #999999; - fill: #d8d8d8; - stroke: #d8d8d8; -} + .attachment .attachment-thumbnail { + position: relative; + float: left; + margin-right: 25px; + margin-bottom: 15px; + padding: 5px; + display: block; + max-height: 140px; + max-width: 99px; + border: #e6e6e6; + outline: 5px solid #e6e6e6; + background: #ffffff; + box-shadow: 0 2px 2px #999999; + fill: #d8d8d8; + stroke: #d8d8d8; + } -.attachment .attachment-metadata { - font-weight: 400; - font-size: 1.1875rem; - line-height: 1.32; - margin: 0 0 15px; - color: #505a5f; -} + .attachment .attachment-metadata { + font-weight: 400; + font-size: 1.1875rem; + line-height: 1.32; + margin: 0 0 15px; + color: #505a5f; + } -.attachment .attachment-metadata:last-of-type { - margin-bottom: 0; -} + .attachment .attachment-metadata:last-of-type { + margin-bottom: 0; + } -.attachment .attachment-metadata .attachment-attribute { - word-wrap: break-word; - overflow-wrap: break-word; - text-decoration: none; - cursor: help; -} + .attachment .attachment-metadata .attachment-attribute { + word-wrap: break-word; + overflow-wrap: break-word; + text-decoration: none; + cursor: help; + } .attachment-thumbnail { display: block; @@ -145,9 +145,9 @@ video { line-height: 1.25; } - .gem-c-metadata a { - font-family: sans-serif; - } + .gem-c-metadata a { + font-family: sans-serif; + } .gem-c-metadata--inverse-padded .gem-c-metadata__list { margin: 15px; @@ -161,9 +161,9 @@ video { margin-top: 0; } - .gem-c-metadata__term .gem-c-metadata__definition { - line-height: 1.4; - } + .gem-c-metadata__term .gem-c-metadata__definition { + line-height: 1.4; + } .gem-c-metadata__definition:not(:last-of-type) { margin-bottom: 5px; @@ -193,24 +193,24 @@ video { color: #ffffff; } -.gem-c-metadata-inverse a:link, -.gem-c-metadata-inverse a:hover, -.gem-c-metadata-inverse a:visited, -.gem-c-metadata-inverse a:active { - color: #ffffff; -} + .gem-c-metadata-inverse a:link, + .gem-c-metadata-inverse a:hover, + .gem-c-metadata-inverse a:visited, + .gem-c-metadata-inverse a:active { + color: #ffffff; + } -.gem-c-metadata-inverse a:focus { - color: #0b0c0c; -} + .gem-c-metadata-inverse a:focus { + color: #0b0c0c; + } .gem-c-metadata-inverse-padded { padding: 10px; } -.gem-c-metadata-inverse-padded .gem-c-metadata-inverse.gem-c-metadata__list { - margin: 10px; -} + .gem-c-metadata-inverse-padded .gem-c-metadata-inverse.gem-c-metadata__list { + margin: 10px; + } .gem-c-metadata__definition { margin: 0; @@ -267,4 +267,174 @@ dl.gem-c-metadata__list { padding: 15px 10px 15px 20px; margin-top: 60px; border-left: 5px solid #347ca9; -} \ No newline at end of file +} + + + +.dfe-vertical-nav { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 400; + font-size: 16px; + font-size: 0.875rem; + line-height: 1.14286; +} + + .dfe-vertical-nav .dfe-vertical-nav--section-header { + color: #505a5f; + font-size: 19px; + line-height: 1.25; + font-weight: 600; + color: #505a5f; + margin-bottom: 15px; + padding-top: 0; + } + +@media (min-width: 40.0625em) { + .dfe-vertical-nav { + padding-left: 15px; + } +} + +@media print { + .dfe-vertical-nav { + font-size: 14pt; + line-height: 1.2; + } +} + +@media (min-width: 40.0625em) { + .dfe-vertical-nav { + margin-left: -15px; + } +} + +.dfe-vertical-nav__section { + margin: 0 0 20px; + padding: 0; + list-style-type: none; +} + +.dfe-vertical-nav__link { + display: block; + padding: 7px 30px 8px 10px; + text-decoration: none; + margin-bottom: 5px; + color: #003a69; +} + +.dfe-vertical-nav__section-item { + border-left: 4px solid #b1b4b6; + font-size: 16px; + font-size: 1rem; + line-height: 1.25; +} + + .dfe-vertical-nav__section-item:hover { + border-left: 4px solid #347ca9; + } + +.dfe-vertical-nav__link:focus { + background: inherit; +} + +.dfe-vertical-nav__section-item--current { + border-left: 4px solid #003a69; + font-weight: 700; + background: #f3f2f1; +} + +.dfe-vertical-nav__link:active, .dfe-vertical-nav__link:hover { + color: #1d70b8; + border-left-color: #347ca9; + text-decoration: none; + box-shadow: none; + outline: 0; +} + +.dfe-vertical-nav__link:focus { + background: #fd0; + color: #0b0c0c; + text-decoration: none; + box-shadow: none; + outline: 0; +} + +.dfe-vertical-nav__section-item--current .dfe-vertical-nav__link { + border-left-color: #003a69; + font-weight: 700; + color: #003a69; +} + + .dfe-vertical-nav__section-item--current .dfe-vertical-nav__link:hover { + text-decoration: none; + } + +.dfe-vertical-nav__section--nested { + margin-bottom: 5px; +} + + .dfe-vertical-nav__section--nested .dfe-vertical-nav__link { + padding-left: 20px; + font-weight: 400; + margin-bottom: 0; + margin-top: -5px; + } + + .dfe-vertical-nav__section--nested .dfe-vertical-nav__section-item::before { + content: "—"; + margin-left: -20px; + color: #505a5f; + } + +.dfe-vertical-nav--count { + float: right; + background: #b1b4b6; + padding: 9px 5px 9px 5px; + border-radius: 0; + color: #0b0c0c; + font-weight: 700; + min-width: 25px; + text-align: center; + font-size: 12px; +} + +@media print { + .dfe-vertical-nav__theme { + font-family: sans-serif; + } +} + +@media (min-width: 40.0625em) { + .dfe-vertical-nav__theme { + font-size: 19px; + font-size: 1.1875rem; + line-height: 1.31579; + } + + .dfe-vertical-nav--count { + padding: 12px 8px 12px 8px; + } +} + +.dfe-vertical-nav__section .dfe-vertical-nav__section-item--current--child-active .dfe-vertical-nav__link { + font-weight: 400; +} + +.dfe-vertical-nav__section .dfe-vertical-nav__section { + margin-bottom: 0; +} + + .dfe-vertical-nav__section .dfe-vertical-nav__section .dfe-vertical-nav__section-item { + border-left: none; + } + + + .dfe-vertical-nav__section .dfe-vertical-nav__section.dfe-vertical-nav__section-item--current { + border-left: none; + background: #347ca9; + } + + .dfe-vertical-nav__section .dfe-vertical-nav__section.dfe-vertical-nav__section-item--current .dfe-vertical-nav__link { + font-weight: 700; + } diff --git a/tests/Dfe.ContentSupport.Web.Tests/Controllers/ContentControllerTests.cs b/tests/Dfe.ContentSupport.Web.Tests/Controllers/ContentControllerTests.cs index dd60afa..eb5f3de 100644 --- a/tests/Dfe.ContentSupport.Web.Tests/Controllers/ContentControllerTests.cs +++ b/tests/Dfe.ContentSupport.Web.Tests/Controllers/ContentControllerTests.cs @@ -11,7 +11,7 @@ public class ContentControllerTests private ContentController GetController() { - return new ContentController(_contentServiceMock.Object); + return new ContentController(_contentServiceMock.Object, new LayoutService()); } @@ -45,7 +45,7 @@ public async Task Index_Calls_Service_GetContent() const bool isPreview = true; var sut = GetController(); - await sut.Index(dummySlug, isPreview); + await sut.Index(dummySlug, "", isPreview); _contentServiceMock.Verify(o => o.GetContent(dummySlug, isPreview), Times.Once); } diff --git a/tests/Dfe.ContentSupport.Web.Tests/Controllers/HomeControllerTests.cs b/tests/Dfe.ContentSupport.Web.Tests/Controllers/HomeControllerTests.cs deleted file mode 100644 index 2b4ae39..0000000 --- a/tests/Dfe.ContentSupport.Web.Tests/Controllers/HomeControllerTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Dfe.ContentSupport.Web.Controllers; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Dfe.ContentSupport.Web.Tests.Controllers; - -public class HomeControllerTests -{ - private readonly Mock _contentServiceMock = new(); - - private HomeController GetController() - { - return new HomeController(_contentServiceMock.Object); - } - - - [Fact] - public async void Index_NoSlug_Returns_ErrorAction() - { - var sut = GetController(); - - var result = await sut.Index(string.Empty); - - result.Should().BeOfType(); - (result as RedirectToActionResult)!.ActionName.Should().BeEquivalentTo("error"); - } - - [Fact] - public async void Index_Calls_Service_GetContent() - { - const string dummySlug = "dummySlug"; - const bool isPreview = true; - var sut = GetController(); - - await sut.Index(dummySlug, isPreview); - - _contentServiceMock.Verify(o => o.GetContent(dummySlug, isPreview), Times.Once); - } - - [Fact] - public async void Index_NullResponse_ReturnsErrorAction() - { - _contentServiceMock.Setup(o => o.GetContent(It.IsAny(), It.IsAny())) - .ReturnsAsync((ContentSupportPage?)null); - - var sut = GetController(); - - var result = await sut.Index("slug"); - - result.Should().BeOfType(); - (result as RedirectToActionResult)!.ActionName.Should().BeEquivalentTo("error"); - } - - [Fact] - public async void Index_WithSlug_Returns_View() - { - _contentServiceMock.Setup(o => o.GetContent(It.IsAny(), It.IsAny())) - .ReturnsAsync(new ContentSupportPage()); - - var sut = GetController(); - var result = await sut.Index("slug1"); - - result.Should().BeOfType(); - (result as ViewResult)!.Model.Should().BeOfType(); - } - - [Fact] - public void Privacy_Returns_EmptyView() - { - var sut = GetController(); - - var result = sut.Privacy(); - - result.Should().BeOfType(); - } - - [Fact] - public void Error_Returns_ErrorView() - { - var sut = GetController(); - sut.ControllerContext.HttpContext = new DefaultHttpContext(); - var result = sut.Error(); - - result.Should().BeOfType(); - (result as ViewResult)!.Model.Should().BeOfType(); - } -} \ No newline at end of file diff --git a/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAccordionTests.cs b/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAccordionTests.cs index d34d40f..dd1b845 100644 --- a/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAccordionTests.cs +++ b/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAccordionTests.cs @@ -46,7 +46,7 @@ public class CustomAccordionTests ], RichText = new ContentItem { - InternalName = ContentInternalName, + InternalName = null, NodeType = "paragraph" } } @@ -75,7 +75,7 @@ public void MapCorrectly() var expectedBody= new RichTextContentItem { - InternalName = ContentInternalName, + InternalName = InternalName, NodeType = RichTextNodeType.Paragraph, Content = [] }; diff --git a/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAttachmentTests.cs b/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAttachmentTests.cs index 241aabd..7b2ffdf 100644 --- a/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAttachmentTests.cs +++ b/tests/Dfe.ContentSupport.Web.Tests/Models/Mapped/Custom/CustomAttachmentTests.cs @@ -50,7 +50,11 @@ public class CustomAttachmentTests Details = new FileDetails { Size = Size - } + }, + }, + SystemProperties = new SystemProperties + { + UpdatedAt = DateTime.Now } } } diff --git a/tests/Dfe.ContentSupport.Web.Tests/Services/LayoutServiceTests.cs b/tests/Dfe.ContentSupport.Web.Tests/Services/LayoutServiceTests.cs new file mode 100644 index 0000000..d1c405a --- /dev/null +++ b/tests/Dfe.ContentSupport.Web.Tests/Services/LayoutServiceTests.cs @@ -0,0 +1,231 @@ + +using Dfe.ContentSupport.Web.Models.Mapped; +using Microsoft.AspNetCore.Http; + + +namespace Dfe.ContentSupport.Web.Tests.Services +{ + public class LayoutServiceTests + { + private readonly LayoutService _layoutService = new(); + + private readonly string Home = "Home"; + private readonly string About = "About"; + private readonly string Contact = "Contact"; + private readonly string HomeTitle = "Home Title"; + private readonly string AboutTitle = "About Title"; + private readonly string HomeSubtitle = "Home Subtitle"; + private readonly string AboutSubtitle = "About Subtitle"; + + private CsPage GetPage() + { + return new CsPage + { + Content = new() + { + new () { InternalName = Home, Title = HomeTitle, Subtitle = HomeSubtitle }, + new () { InternalName = About, Title = AboutTitle, Subtitle = AboutSubtitle } + } + }; + } + + + private string GetSegmentLength(int length) + { + var segment = ""; + for (var i = 1; i <= length; i++) + { + segment += $"/segment{i}"; + } + + return segment; + } + + + [Fact] + public void GetHeading_PageExists_ReturnsCorrectHeading() + { + // Arrange + var page = GetPage(); + + // Act + var result = _layoutService.GetHeading(page, About); + + // Assert + Assert.Equal(AboutTitle, result.Title); + Assert.Equal(AboutSubtitle, result.Subtitle); + } + + + [Fact] + public void GetHeading_PageDoesNotExist_ReturnsFirstPageHeading() + { + // Arrange + var page = GetPage(); + + // Act + var result = _layoutService.GetHeading(page, Contact); + + // Assert + Assert.Equal(HomeTitle, result.Title); + Assert.Equal(HomeSubtitle, result.Subtitle); + } + + + [Fact] + public void GenerateVerticalNavigation_PageNameMatches_ReturnsCorrectMenuItems() + { + // Arrange + var page = GetPage(); + + var request = new DefaultHttpContext().Request; + + // Act + var result = _layoutService.GenerateVerticalNavigation(page, request, About); + + // Assert + Assert.Equal(page.Content.Count, result.Count); + Assert.Equal(AboutTitle, result[1].Title); + Assert.True(result[1].IsActive); + } + + + [Fact] + public void GenerateVerticalNavigation_PageNameDoesNotMatch_ReturnsMenuItemsWithFirstActive() + { + // Arrange + var page = GetPage(); + + var request = new DefaultHttpContext().Request; + + // Act + var result = _layoutService.GenerateVerticalNavigation(page, request, Contact); + + // Assert + Assert.Equal(page.Content.Count, result.Count); + Assert.Equal(HomeTitle, result[0].Title); + Assert.Equal(0, result.Count(o => o.IsActive)); + } + + + [Fact] + public void GetVisiblePageList_PageNameProvidedAndMatches_ReturnsMatchingItems() + { + // Arrange + var page = GetPage(); + + // Act + var result = _layoutService.GetVisiblePageList(page, About); + + // Assert + Assert.Single(result); + Assert.Equal(About, result[0].InternalName); + } + + + [Fact] + public void GetVisiblePageList_PageNameProvidedAndDoesNotMatch_ReturnsEmptyList() + { + // Arrange + var page = GetPage(); + + // Act + var result = _layoutService.GetVisiblePageList(page, Contact); + + // Assert + Assert.Empty(result); + } + + + [Fact] + public void GetVisiblePageList_PageNameIsNullOrEmpty_ReturnsFirstItem() + { + // Arrange + var page = GetPage(); + + // Act + var result = _layoutService.GetVisiblePageList(page, string.Empty); + + // Assert + Assert.Single(result); + Assert.Equal(Home, result[0].InternalName); + } + + + [Fact] + public void GetVisiblePageList_ContentListIsEmpty_ReturnsEmptyList() + { + // Arrange + var page = GetPage(); + page.Content = new(); + + // Act + var result = _layoutService.GetVisiblePageList(page, Home); + + // Assert + Assert.Empty(result); + } + + + [Fact] + public void GetNavigationUrl_MoreThanTwoSegments_ReturnsFirstTwoSegments() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.Path = GetSegmentLength(4); + + // Act + var result = _layoutService.GetNavigationUrl(context.Request); + + // Assert + Assert.Equal(GetSegmentLength(2), result); + } + + + [Fact] + public void GetNavigationUrl_ExactlyTwoSegments_ReturnsAllSegments() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.Path = GetSegmentLength(2); + + // Act + var result = _layoutService.GetNavigationUrl(context.Request); + + // Assert + Assert.Equal(GetSegmentLength(2), result); + } + + + [Fact] + public void GetNavigationUrl_FewerThanTwoSegments_ReturnsAllSegments() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.Path = GetSegmentLength(1); + + // Act + var result = _layoutService.GetNavigationUrl(context.Request); + + // Assert + Assert.Equal(GetSegmentLength(1), result); + } + + + [Fact] + public void GetNavigationUrl_EmptyUrl_ReturnsEmptyString() + { + // Arrange + var context = new DefaultHttpContext(); + var emptyRequestPath = string.Empty; + context.Request.Path = emptyRequestPath; + + // Act + var result = _layoutService.GetNavigationUrl(context.Request); + + // Assert + Assert.Equal(emptyRequestPath, result); + } + + } +}