Skip to content

Commit 4a47d92

Browse files
committed
Stabilize browser CI races
1 parent 9c5f969 commit 4a47d92

11 files changed

Lines changed: 194 additions & 55 deletions

File tree

.github/workflows/deploy-github-pages.yml

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ env:
2323
UI_TEST_PROJECT: tests/PrompterOne.Web.UITests/PrompterOne.Web.UITests.csproj
2424

2525
jobs:
26-
validate_supporting_suites:
27-
name: Validate Supporting Suites
26+
build_supporting_suites:
27+
name: Restore + Build Supporting Suites
2828
runs-on: ubuntu-latest
2929
timeout-minutes: 15
3030

@@ -40,16 +40,37 @@ jobs:
4040
with:
4141
global-json-file: global.json
4242

43+
- name: Restore solution
44+
run: dotnet restore "$SOLUTION_FILE"
45+
4346
- name: Build solution
4447
run: dotnet build "$SOLUTION_FILE" -warnaserror
4548

49+
test_supporting_suites:
50+
name: Run Supporting Suites
51+
needs: build_supporting_suites
52+
runs-on: ubuntu-latest
53+
timeout-minutes: 15
54+
55+
permissions:
56+
contents: read
57+
58+
steps:
59+
- name: Checkout
60+
uses: actions/checkout@v6
61+
62+
- name: Setup .NET
63+
uses: actions/setup-dotnet@v5
64+
with:
65+
global-json-file: global.json
66+
4667
- name: Test supporting suites
4768
run: |
4869
dotnet test --project "$CORE_TEST_PROJECT"
4970
dotnet test --project "$APP_TEST_PROJECT"
5071
51-
validate_browser_suite:
52-
name: Validate Browser Suite
72+
build_browser_suite:
73+
name: Restore + Build Browser Suite
5374
runs-on: macos-latest
5475
timeout-minutes: 30
5576

@@ -65,9 +86,30 @@ jobs:
6586
with:
6687
global-json-file: global.json
6788

89+
- name: Restore solution
90+
run: dotnet restore "$SOLUTION_FILE"
91+
6892
- name: Build solution
6993
run: dotnet build "$SOLUTION_FILE" -warnaserror
7094

95+
test_browser_suite:
96+
name: Run Browser Suite
97+
needs: build_browser_suite
98+
runs-on: macos-latest
99+
timeout-minutes: 30
100+
101+
permissions:
102+
contents: read
103+
104+
steps:
105+
- name: Checkout
106+
uses: actions/checkout@v6
107+
108+
- name: Setup .NET
109+
uses: actions/setup-dotnet@v5
110+
with:
111+
global-json-file: global.json
112+
71113
- name: Install Playwright browser
72114
run: node "$PLAYWRIGHT_CLI" install chromium
73115

@@ -76,8 +118,8 @@ jobs:
76118
prepare_release:
77119
name: Resolve Release Version
78120
needs:
79-
- validate_supporting_suites
80-
- validate_browser_suite
121+
- test_supporting_suites
122+
- test_browser_suite
81123
runs-on: ubuntu-latest
82124
env:
83125
APP_PROJECT: src/PrompterOne.Web/PrompterOne.Web.csproj

.github/workflows/pr-validation.yml

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ env:
2121
UI_TEST_PROJECT: tests/PrompterOne.Web.UITests/PrompterOne.Web.UITests.csproj
2222

2323
jobs:
24-
build_and_test_supporting:
25-
name: Validate Supporting Suites
24+
build_supporting_suites:
25+
name: Restore + Build Supporting Suites
2626
runs-on: ubuntu-latest
2727
timeout-minutes: 15
2828

@@ -35,16 +35,34 @@ jobs:
3535
with:
3636
global-json-file: global.json
3737

38+
- name: Restore solution
39+
run: dotnet restore "$SOLUTION_FILE"
40+
3841
- name: Build solution
3942
run: dotnet build "$SOLUTION_FILE" -warnaserror
4043

44+
test_supporting_suites:
45+
name: Run Supporting Suites
46+
needs: build_supporting_suites
47+
runs-on: ubuntu-latest
48+
timeout-minutes: 15
49+
50+
steps:
51+
- name: Checkout
52+
uses: actions/checkout@v6
53+
54+
- name: Setup .NET
55+
uses: actions/setup-dotnet@v5
56+
with:
57+
global-json-file: global.json
58+
4159
- name: Test supporting suites
4260
run: |
4361
dotnet test --project "$CORE_TEST_PROJECT"
4462
dotnet test --project "$APP_TEST_PROJECT"
4563
46-
build_and_test_browser:
47-
name: Validate Browser Suite
64+
build_browser_suite:
65+
name: Restore + Build Browser Suite
4866
runs-on: macos-latest
4967
timeout-minutes: 30
5068

@@ -57,9 +75,27 @@ jobs:
5775
with:
5876
global-json-file: global.json
5977

78+
- name: Restore solution
79+
run: dotnet restore "$SOLUTION_FILE"
80+
6081
- name: Build solution
6182
run: dotnet build "$SOLUTION_FILE" -warnaserror
6283

84+
test_browser_suite:
85+
name: Run Browser Suite
86+
needs: build_browser_suite
87+
runs-on: macos-latest
88+
timeout-minutes: 30
89+
90+
steps:
91+
- name: Checkout
92+
uses: actions/checkout@v6
93+
94+
- name: Setup .NET
95+
uses: actions/setup-dotnet@v5
96+
with:
97+
global-json-file: global.json
98+
6399
- name: Install Playwright browser
64100
run: node "$PLAYWRIGHT_CLI" install chromium
65101

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ Browser test execution rules:
133133
- Inside that single process, the browser suite may run up to `4` parallel TUnit workers locally; when repeated full-suite CI runs prove resource contention, lower the CI worker cap instead of weakening browser assertions.
134134
- Do not run `PrompterOne.Web.UITests` in parallel with another `dotnet build` or `dotnet test` command.
135135
- In GitHub Actions, run the browser suite in its own dedicated macOS job and keep supporting suites in a separate job so CI can parallelize work without Linux x64 browser-runner contention stretching release validation.
136+
- GitHub Actions pipelines must expose explicit staged jobs with readable names such as restore, build, supporting tests, browser tests, release publish, and deploy; vague single-job `validate` graphs are not acceptable when the user needs to see pipeline phases clearly in the Actions UI.
136137
- Browser acceptance tests must stay on the production-shaped runtime path; do not add or keep `?wasm-debug=1` or similar debug-query scenarios in automated acceptance coverage unless the user explicitly asks for that path.
137138
- Do not add Python or ad-hoc runner scripts to bootstrap browser verification. The repo test commands must self-host the app and execute the flows end to end on their own.
138139
- Browser UI scenarios are the primary acceptance gate for this repo. Component and core tests are supporting layers, not the release bar.

src/PrompterOne.Shared/Editor/Pages/EditorPage.Loading.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ private async Task LoadScriptFromQueryAsync(string requestedScriptId)
6363
}
6464
}
6565

66-
private void PopulateEditorState(bool resetHistory = false)
66+
private void PopulateEditorState(bool resetHistory = false, bool clearSplitFeedback = true)
6767
{
6868
var state = SessionService.State;
6969
var document = _frontMatterService.Parse(state.Text);
@@ -75,7 +75,10 @@ private void PopulateEditorState(bool resetHistory = false)
7575
}
7676

7777
ResetMetadataDefaults(state);
78-
_splitFeedback = null;
78+
if (clearSplitFeedback)
79+
{
80+
_splitFeedback = null;
81+
}
7982
_sourceText = document.Body;
8083
ApplyLoadedMetadata(metadata, state);
8184
_segments = OutlineBuilder.Build(state.ScriptData, document.Body, 0);

src/PrompterOne.Shared/Editor/Pages/EditorPage.Persistence.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ private void ApplyPersistedDraftState(long revision)
100100
return;
101101
}
102102

103-
PopulateEditorState();
103+
PopulateEditorState(clearSplitFeedback: false);
104104
_ = InvokeAsync(StateHasChanged);
105105
}
106106

tests/PrompterOne.Web.Tests/Editor/EditorSplitFeedbackInteractionTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,46 @@ public void EditorPage_SplitFeedbackStaysVisibleAcrossRedundantSourceChangeEvent
5858
});
5959
}
6060

61+
[Test]
62+
public async Task EditorPage_SplitFeedbackStaysVisibleAfterAutosaveRefresh()
63+
{
64+
var navigationManager = Services.GetRequiredService<NavigationManager>();
65+
navigationManager.NavigateTo(AppTestData.Routes.EditorDemo);
66+
var cut = Render<EditorPage>();
67+
68+
cut.WaitForAssertion(() =>
69+
{
70+
var source = cut.FindByTestId(UiTestIds.Editor.SourceInput);
71+
Assert.Contains(AppTestData.Editor.BodyHeading, source.GetAttribute("value"));
72+
});
73+
74+
cut.FindByTestId(UiTestIds.Editor.SourceInput).Input(EditorSplitFeedbackInteractionTestSource.SplitSource);
75+
cut.FindByTestId(UiTestIds.Editor.SplitSegment).Click();
76+
77+
cut.WaitForAssertion(() =>
78+
{
79+
Assert.Equal(
80+
EditorSplitFeedbackInteractionTestSource.SplitActionLabel,
81+
cut.FindByTestId(UiTestIds.Editor.SplitResultOpenLibrary).TextContent.Trim());
82+
});
83+
84+
await Task.Delay(EditorSplitFeedbackInteractionTestSource.PostAutosaveObservationDelayMs);
85+
86+
cut.WaitForAssertion(() =>
87+
{
88+
Assert.Equal(
89+
EditorSplitFeedbackInteractionTestSource.SplitActionLabel,
90+
cut.FindByTestId(UiTestIds.Editor.SplitResultOpenLibrary).TextContent.Trim());
91+
});
92+
93+
cut.FindByTestId(UiTestIds.Editor.SplitResultOpenLibrary).Click();
94+
95+
cut.WaitForAssertion(() =>
96+
{
97+
Assert.EndsWith(AppRoutes.Library, navigationManager.Uri, StringComparison.Ordinal);
98+
});
99+
}
100+
61101
[Test]
62102
public void EditorPage_SplitFeedbackStaysVisibleAfterSourceEdits()
63103
{
@@ -100,6 +140,7 @@ public void EditorPage_SplitFeedbackStaysVisibleAfterSourceEdits()
100140

101141
private static class EditorSplitFeedbackInteractionTestSource
102142
{
143+
public const int PostAutosaveObservationDelayMs = 1_700;
103144
public const string EditedSplitSource =
104145
"""
105146
## [Episode 1 - How to Think About Systems|140WPM|Professional]

tests/PrompterOne.Web.UITests/Editor/EditorSelectionRenderRegressionTests.cs

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,11 @@ namespace PrompterOne.Web.UITests;
77

88
[ClassDataSource<StandaloneAppFixture>(Shared = SharedType.PerClass)]
99
[NotInParallel(UiTestParallelization.EditorAuthoringConstraintKey)]
10-
public sealed class EditorSelectionRenderRegressionTests(StandaloneAppFixture fixture)
10+
public sealed class EditorSelectionRenderRegressionTests(StandaloneAppFixture fixture) : AppUiTestBase(fixture)
1111
{
12-
private readonly StandaloneAppFixture _fixture = fixture;
13-
1412
[Test]
15-
public async Task EditorScreen_SetTextThenSelectStillRendersFloatingBar()
16-
{
17-
var page = await _fixture.NewPageAsync();
18-
19-
try
13+
public Task EditorScreen_SetTextThenSelectStillRendersFloatingBar() =>
14+
RunPageAsync(async page =>
2015
{
2116
await page.GotoAsync(BrowserTestConstants.Routes.EditorDemo);
2217
await Expect(page.GetByTestId(UiTestIds.Editor.Page)).ToBeVisibleAsync();
@@ -27,19 +22,11 @@ public async Task EditorScreen_SetTextThenSelectStillRendersFloatingBar()
2722

2823
await Expect(page.GetByTestId(UiTestIds.Editor.FloatingBar))
2924
.ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.FastVisibleTimeoutMs });
30-
}
31-
finally
32-
{
33-
await page.Context.CloseAsync();
34-
}
35-
}
25+
});
3626

3727
[Test]
38-
public async Task EditorScreen_BackwardSelection_SelectsExpectedTrailingCharactersFromWordEnd()
39-
{
40-
var page = await _fixture.NewPageAsync();
41-
42-
try
28+
public Task EditorScreen_BackwardSelection_SelectsExpectedTrailingCharactersFromWordEnd() =>
29+
RunPageAsync(async page =>
4330
{
4431
await page.GotoAsync(BrowserTestConstants.Routes.EditorDemo);
4532
await Expect(page.GetByTestId(UiTestIds.Editor.Page)).ToBeVisibleAsync();
@@ -55,19 +42,11 @@ await EditorMonacoDriver.SetBackwardSelectionFromTextEndAsync(
5542
var selectedText = ReadSelectedText(state);
5643

5744
await Assert.That(selectedText).IsEqualTo(BrowserTestConstants.Editor.ReverseSelectionExpectedText);
58-
}
59-
finally
60-
{
61-
await page.Context.CloseAsync();
62-
}
63-
}
45+
});
6446

6547
[Test]
66-
public async Task EditorScreen_BackwardSelection_CanExtendAcrossLineBreaks()
67-
{
68-
var page = await _fixture.NewPageAsync();
69-
70-
try
48+
public Task EditorScreen_BackwardSelection_CanExtendAcrossLineBreaks() =>
49+
RunPageAsync(async page =>
7150
{
7251
await page.GotoAsync(BrowserTestConstants.Routes.EditorDemo);
7352
await Expect(page.GetByTestId(UiTestIds.Editor.Page)).ToBeVisibleAsync();
@@ -84,12 +63,7 @@ await EditorMonacoDriver.SetBackwardSelectionFromTextEndAsync(
8463

8564
await Assert.That(selectedText.Length >= BrowserTestConstants.Editor.ReverseMultilineSelectionCharacterCount).IsTrue().Because($"Expected backward selection to keep growing across lines, but only selected {selectedText.Length} characters.");
8665
await Assert.That(selectedText).Contains(BrowserTestConstants.Editor.LineFeed);
87-
}
88-
finally
89-
{
90-
await page.Context.CloseAsync();
91-
}
92-
}
66+
});
9367

9468
private static string ReadSelectedText(EditorMonacoState state)
9569
{
Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Runtime.CompilerServices;
12
using Microsoft.Playwright;
23

34
namespace PrompterOne.Web.UITests;
@@ -6,24 +7,44 @@ public abstract class AppUiTestBase(StandaloneAppFixture fixture)
67
{
78
private readonly StandaloneAppFixture _fixture = fixture;
89

9-
protected Task RunPageAsync(Func<IPage, Task> scenario) =>
10+
protected Task RunPageAsync(
11+
Func<IPage, Task> scenario,
12+
[CallerMemberName] string testName = "") =>
1013
RunPageAsync(async page =>
1114
{
1215
await scenario(page);
1316
return true;
14-
});
17+
}, testName);
1518

16-
protected async Task<T> RunPageAsync<T>(Func<IPage, Task<T>> scenario)
19+
protected async Task<T> RunPageAsync<T>(
20+
Func<IPage, Task<T>> scenario,
21+
[CallerMemberName] string testName = "")
1722
{
1823
var page = await _fixture.NewPageAsync();
1924

2025
try
2126
{
2227
return await scenario(page);
2328
}
29+
catch
30+
{
31+
await TryCaptureFailurePageAsync(page, testName);
32+
throw;
33+
}
2434
finally
2535
{
2636
await page.Context.CloseAsync();
2737
}
2838
}
39+
40+
private static async Task TryCaptureFailurePageAsync(IPage page, string testName)
41+
{
42+
try
43+
{
44+
await UiScenarioArtifacts.CaptureFailurePageAsync(page, testName);
45+
}
46+
catch
47+
{
48+
}
49+
}
2950
}

0 commit comments

Comments
 (0)