Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public static IServiceCollection AddApplicationDependencies(this IServiceCollect
services.AddScoped<IPageNodeContentEditor, PageNodeContentEditor>();
services.AddScoped<DefaultPageNodeSeeder>();
services.AddScoped<SamplePageNodeSeeder>();
services.AddScoped<TestFixturePageNodeSeeder>();
services.AddScoped<Analytics.SampleSearchDataSeeder>();
services.AddScoped<ContentStaging.IContentStagingService, ContentStaging.ContentStagingService>();
services.AddScoped<ContentStaging.ContentBundleSanitiser>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@
importer by SamplePageNodeSeeder. Embedded so it travels with the assembly and
needs no deployment-relative file path. -->
<EmbeddedResource Include="PageTree/SeedContent/sample-content.json" />

<!-- Content the automated browser tests navigate to, seeded under its own root by
TestFixturePageNodeSeeder. Separate from the sample content because it is re-imported
over the top on every seed, which the sample content must never be. -->
<EmbeddedResource Include="PageTree/SeedContent/test-fixture-content.json" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ public static class DefaultPageNodeRoots
/// <summary>Stable Guid for the auto-seeded /help/not-found content page.</summary>
public static readonly Guid HelpNotFoundId = new("00000000-cd94-4a01-8f01-00000000000f");

/// <summary>
/// The root the automated browser tests' fixture content hangs off. Deliberately NOT in
/// <see cref="All"/>: the four roots above are part of the product and exist in every
/// environment, whereas this one has no business existing anywhere nobody runs the browser
/// suite. It is created by the fixture seeder instead, so it appears only where somebody has
/// asked for it — and, being a folder that is hidden from the menu, an ordinary visitor has
/// no route to it even there.
///
/// Pinned for the same reason as the roots, and one more besides: the fixture seed re-imports
/// over the top of whatever is already present, and it can only find the rows to repair while
/// their identity is stable across environments.
/// </summary>
public static readonly Guid DevelopmentTestingRootId = new("00000000-cd94-4a01-8f01-00000000000e");

/// <summary>URL segment of the fixture root — <c>/development-testing</c>.</summary>
public const string DevelopmentTestingSegment = "development-testing";

public static readonly IReadOnlySet<string> Segments =
new HashSet<string>(All.Select(r => r.Segment), StringComparer.OrdinalIgnoreCase);
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,24 +110,6 @@
}
]
},
{
"id": "4cfa30c7-ee17-4c28-a540-67f4c6402b21",
"parentId": "00000000-cd94-4a01-8f01-000000000002",
"segment": "wiki-sandbox",
"title": "Wiki sandbox page",
"pageType": "wiki",
"sortOrder": 3,
"appearInSearch": true,
"versions": [
{
"versionId": 1,
"minorVersion": 0,
"publishFrom": "2026-08-10T18:56:35.812918Z",
"content": "<p>This page is seeded specifically so the wiki render path has real content to exercise. It stores raw HTML in the version body, unlike the widget-tree JSON that content-type pages use.</p><h2>Why a dedicated wiki sample?</h2><p>Most CMS pages are content-typed and rendered through <code>Content.cshtml</code>. Wiki pages take a separate code path (<code>Wiki.cshtml</code>, <code>IHtmlRenderingService</code>), and without a live wiki page nothing exercises it — regressions there wouldn't surface until someone manually created one.</p><h2>Layout</h2><p>Wiki pages use a 1/3–2/3 grid with a sibling-nav in the left column and the article body in the right.</p><ul><li>Left column: sibling navigation</li><li>Right column: heading, subtitle, body HTML</li></ul><h2>Enough words for a good scroll</h2><p>The body deliberately runs long enough that a typical laptop viewport can scroll comfortably. If you want to add more test content just extend this HTML string.</p><p>Paragraph two — filler so the page has scroll depth for exercising the back-to-top link and any future long-form nav behaviour.</p><p>Paragraph three — additional filler so the section headings are far enough apart that the sibling nav in the sidebar has room to render the whole tree without collapsing.</p><p>Paragraph four — the final paragraph before the closing heading. When you click the back-to-top link from anywhere on the page, the browser should jump straight back to the H1 at the top of this article.</p><h2>End</h2><p>You have reached the end. Try the back-to-top link.</p>",
"bodyPlainText": "Wiki sandbox page This page is seeded specifically so the wiki render path has real content to exercise. It stores raw HTML in the version body, unlike the widget-tree JSON that content-type pages use.Why a dedicated wiki sample?Most CMS pages are content-typed and rendered through Content.cshtml. Wiki pages take a separate code path (Wiki.cshtml, IHtmlRenderingService), and without a live wiki page nothing exercises it — regressions there wouldn't surface until someone manually created one.LayoutWiki pages use a 1/3–2/3 grid with a sibling-nav in the left column and the article body in the right.Left column: sibling navigationRight column: heading, subtitle, body HTMLEnough words for a good scrollThe body deliberately runs long enough that a typical laptop viewport can scroll comfortably. If you want to add more test content just extend this HTML string.Paragraph two — filler so the page has scroll depth for exercising the back-to-top link and any future long-form nav behaviour.Paragraph three — additional filler so the section headings are far enough apart that the sibling nav in the sidebar has room to render the whole tree without collapsing.Paragraph four — the final paragraph before the closing heading. When you click the back-to-top link from anywhere on the page, the browser should jump straight back to the H1 at the top of this article.EndYou have reached the end. Try the back-to-top link."
}
]
},
{
"id": "12f200c0-576e-4196-9116-f201203314bb",
"parentId": "00000000-cd94-4a01-8f01-000000000003",
Expand Down Expand Up @@ -235,24 +217,6 @@
"bodyPlainText": "Post-16 performance data Performance results for sixth-form colleges, FE colleges and school sixth forms. Data is provisional until the checking window closes."
}
]
},
{
"id": "ca9e1bc3-ed66-40d0-8313-cb6a28971d3a",
"parentId": "00000000-cd94-4a01-8f01-000000000004",
"segment": "short-page",
"title": "Short page",
"pageType": "content",
"sortOrder": 3,
"appearInSearch": true,
"versions": [
{
"versionId": 1,
"minorVersion": 0,
"publishFrom": "2026-08-10T19:53:42.999299Z",
"content": "[\n {\n \"kind\": \"region\",\n \"layout\": \"single\",\n \"columns\": [\n [\n {\n \"kind\": \"widget\",\n \"type\": \"heading\",\n \"props\": {\n \"level\": 2,\n \"text\": \"Short page\"\n }\n },\n {\n \"kind\": \"widget\",\n \"type\": \"richtext\",\n \"props\": {\n \"html\": \"\\u003Cp\\u003EA deliberately short page. There is not enough content here to scroll, so no back-to-top link should appear.\\u003C/p\\u003E\"\n }\n }\n ]\n ]\n }\n]",
"bodyPlainText": "Short page A deliberately short page. There is not enough content here to scroll, so no back-to-top link should appear."
}
]
}
],
"contentBlocks": []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"$schema": "cpd-content-v2",
"schemaVersion": 2,
"pageNodes": [
{
"id": "00000000-cd94-4a01-8f01-0000000e0001",
"parentId": "00000000-cd94-4a01-8f01-00000000000e",
"segment": "long-page",
"title": "Long fixture page",
"pageType": "wiki",
"sortOrder": 0,
"appearInSearch": false,
"versions": [
{
"versionId": 1,
"minorVersion": 0,
"publishFrom": "2026-09-15T09:00:00.000000Z",
"content": "<p>This page belongs to the automated browser tests. It is seeded under the development testing root so it never sits alongside content an editor is working on, and it is re-imported every time the seed runs, so editing it here is pointless — the next seed puts it back.</p><h2>Why a wiki-typed fixture?</h2><p>Most CMS pages are content-typed and rendered through <code>Content.cshtml</code>. Wiki pages take a separate code path (<code>Wiki.cshtml</code>, <code>IHtmlRenderingService</code>), and without a live wiki page nothing exercises it — regressions there wouldn't surface until someone manually created one.</p><h2>Layout</h2><p>Wiki pages use a 1/3–2/3 grid with a sibling-nav in the left column and the article body in the right.</p><ul><li>Left column: sibling navigation</li><li>Right column: heading, subtitle, body HTML</li></ul><h2>Enough words for a good scroll</h2><p>The body deliberately runs long enough that a typical laptop viewport can scroll comfortably. The back-to-top tests assert on how far the reader has travelled before the link appears, so shortening this page would quietly make those assertions vacuous.</p><p>Paragraph two — filler so the page has scroll depth for exercising the back-to-top link and any future long-form nav behaviour.</p><p>Paragraph three — additional filler so the section headings are far enough apart that the sibling nav in the sidebar has room to render the whole tree without collapsing.</p><p>Paragraph four — the final paragraph before the closing heading. When you click the back-to-top link from anywhere on the page, the browser should jump straight back to the H1 at the top of this article.</p><h2>End</h2><p>You have reached the end. Try the back-to-top link.</p>",
"bodyPlainText": "Long fixture page This page belongs to the automated browser tests. It is seeded under the development testing root so it never sits alongside content an editor is working on, and it is re-imported every time the seed runs, so editing it here is pointless — the next seed puts it back.Why a wiki-typed fixture?Most CMS pages are content-typed and rendered through Content.cshtml. Wiki pages take a separate code path (Wiki.cshtml, IHtmlRenderingService), and without a live wiki page nothing exercises it — regressions there wouldn't surface until someone manually created one.LayoutWiki pages use a 1/3–2/3 grid with a sibling-nav in the left column and the article body in the right.Left column: sibling navigationRight column: heading, subtitle, body HTMLEnough words for a good scrollThe body deliberately runs long enough that a typical laptop viewport can scroll comfortably. The back-to-top tests assert on how far the reader has travelled before the link appears, so shortening this page would quietly make those assertions vacuous.Paragraph two — filler so the page has scroll depth for exercising the back-to-top link and any future long-form nav behaviour.Paragraph three — additional filler so the section headings are far enough apart that the sibling nav in the sidebar has room to render the whole tree without collapsing.Paragraph four — the final paragraph before the closing heading. When you click the back-to-top link from anywhere on the page, the browser should jump straight back to the H1 at the top of this article.EndYou have reached the end. Try the back-to-top link."
}
]
},
{
"id": "00000000-cd94-4a01-8f01-0000000e0002",
"parentId": "00000000-cd94-4a01-8f01-00000000000e",
"segment": "short-page",
"title": "Short fixture page",
"pageType": "content",
"sortOrder": 1,
"appearInSearch": false,
"versions": [
{
"versionId": 1,
"minorVersion": 0,
"publishFrom": "2026-09-15T09:00:00.000000Z",
"content": "[\n {\n \"kind\": \"region\",\n \"layout\": \"single\",\n \"columns\": [\n [\n {\n \"kind\": \"widget\",\n \"type\": \"heading\",\n \"props\": {\n \"level\": 2,\n \"text\": \"Short fixture page\"\n }\n },\n {\n \"kind\": \"widget\",\n \"type\": \"richtext\",\n \"props\": {\n \"html\": \"<p>A deliberately short fixture. There is not enough content here to scroll, so no back-to-top link should appear.</p>\"\n }\n }\n ]\n ]\n }\n]",
"bodyPlainText": "Short fixture page A deliberately short fixture. There is not enough content here to scroll, so no back-to-top link should appear."
}
]
},
{
"id": "00000000-cd94-4a01-8f01-0000000e0003",
"parentId": "00000000-cd94-4a01-8f01-00000000000e",
"segment": "search-fixture",
"title": "Search fixture page",
"pageType": "content",
"sortOrder": 2,
"appearInSearch": true,
"keywords": "testfixture",
"versions": [
{
"versionId": 1,
"minorVersion": 0,
"publishFrom": "2026-09-15T09:00:00.000000Z",
"content": "[\n {\n \"kind\": \"region\",\n \"layout\": \"single\",\n \"columns\": [\n [\n {\n \"kind\": \"widget\",\n \"type\": \"heading\",\n \"props\": {\n \"level\": 2,\n \"text\": \"Search fixture page\"\n }\n },\n {\n \"kind\": \"widget\",\n \"type\": \"richtext\",\n \"props\": {\n \"html\": \"<p>This fixture exists so a site search can be relied on to return at least one hit. It carries the keyword <code>testfixture</code>, which nothing else in the corpus uses, so a search for that term matches this page and nothing an editor wrote.</p><p>The admin search-analytics screens record a visitor's search and then show the hits it produced. Those tests used to search for a word that merely happened to be somewhere in the content, which made them fail the moment an editor changed it.</p>\"\n }\n }\n ]\n ]\n }\n]",
"bodyPlainText": "Search fixture page This fixture exists so a site search can be relied on to return at least one hit. It carries the keyword testfixture, which nothing else in the corpus uses, so a search for that term matches this page and nothing an editor wrote. The admin search-analytics screens record a visitor's search and then show the hits it produced. Those tests used to search for a word that merely happened to be somewhere in the content, which made them fail the moment an editor changed it."
}
]
}
],
"contentBlocks": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using DfE.CheckPerformanceData.Application.ContentStaging;

namespace DfE.CheckPerformanceData.Application.PageTree;

// Seeds the content the automated browser tests navigate to, under a root of its own.
//
// Two things separate this from SamplePageNodeSeeder, and both are the point of it existing:
//
// * It owns its root. Nothing creates /development-testing at start-up, because a container for
// test scaffolding has no business appearing in an environment nobody runs the browser suite
// against. Created here, the root shows up only where somebody has actually seeded fixtures.
//
// * It imports in Replace, not Skip. The suite was failing because sample pages it navigated to
// had been emptied — an editor deleted the version, the page row survived, and the route
// started returning 404. Skip-on-collision cannot repair that: the collision test is the page
// identity, not whether the page has anything to render, so the seed walks past the broken row
// every time. Replacing the versions is what brings the page back.
//
// Replace is safe here in a way it would not be for the sample content: nothing under this root is
// anyone's work. It is re-imported wholesale on every seed by design, so a fixture is always in the
// state the tests expect rather than whatever the last person to open it left behind.
public sealed class TestFixturePageNodeSeeder(
IPageNodeService pageNodes,
IPageNodeRepository pageNodeRepository,
IContentStagingService staging)
{
private const string RootTitle = "Development testing";

private const string RootSubtitle =
"Content the automated tests navigate to. Re-created on every seed — edits here are lost.";

/// <summary>
/// Creates the fixture root if it is missing, then imports the fixture bundle over the top of
/// whatever is there. Returns the number of fixture pages created or repaired, which is what
/// the admin screen reports back.
/// </summary>
public async Task<int> SeedAsync()
{
await EnsureRootAsync();

var result = await staging.ImportAsync(
TestFixtureSeedBundle.Load(),
mode: ContentImportMode.Replace,
decisions: null,
newItemMode: ContentImportMode.Replace);

// Both counts, unlike the sample seed. There, "created" is the whole story because existing
// pages are deliberately left alone. Here a run that repaired three emptied fixtures created
// nothing at all and did all of the work, so reporting creations alone would tell the
// operator that nothing happened.
return result.PageNodesCreated + result.PageNodesUpdated;
}

// Folder-typed and out of the menu. Folder is what keeps the root out of search results — the
// search query filters folders structurally, so it needs no flag of its own — and clearing
// ShowInMenu keeps it out of the site navigation. Between them there is no route by which an
// ordinary visitor arrives at the fixture tree, while an editor browsing the page tree sees one
// plainly-labelled container rather than test pages interleaved with their own.
//
// The re-hide on an existing root is not belt-and-braces: ShowInMenu does not round-trip
// through a content-staging bundle, so a root that travelled between environments inside an
// export arrives with the column at its default of true.
private async Task EnsureRootAsync()
{
var existing = await pageNodes.GetNodeByPathAsync(DefaultPageNodeRoots.DevelopmentTestingSegment);

if (existing is null)
{
// Explicit Id through the staging-create path so the row carries the pinned Guid the
// bundle's pages name as their parent. Created with a fresh one the root would still
// sit at the right path, but every fixture would be an orphan and the import would
// quietly seed nothing.
var created = await pageNodeRepository.CreateNodeForStagingAsync(
DefaultPageNodeRoots.DevelopmentTestingRootId,
parentId: null,
DefaultPageNodeRoots.DevelopmentTestingSegment,
path: DefaultPageNodeRoots.DevelopmentTestingSegment,
RootTitle, RootSubtitle, pageName: null,
pageType: "folder", sortOrder: 100,
appearInSearch: false, keywords: null, userId: "system");

await pageNodeRepository.SetShowInMenuAsync(created.Id, false, "system");
return;
}

if (existing.ShowInMenu)
await pageNodeRepository.SetShowInMenuAsync(existing.Id, false, "system");
}
}
Loading
Loading