8000 Support persistent component state across enhanced page navigations by Copilot · Pull Request #62526 · dotnet/aspnetcore · GitHub
[go: up one dir, main page]

Skip to content

Support persistent component state across enhanced page navigations #62526

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
Implement E2E tests for scenario-based persistent component state fil…
…tering

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
  • Loading branch information
Copilot and javiercn committed Jul 2, 2025
commit 60fcdf5a6cb8c5224f398cb2badfa203961ae26d
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,53 @@ private void TriggerClientPauseAndInteract(IJavaScriptExecutor javascript)

Browser.Exists(By.Id("increment-persistent-counter-count")).Click();
}

[Fact]
public void NonPersistedStateIsNotRestoredAfterDisconnection()
{
// Verify initial state during/after SSR - NonPersistedCounter should be 5
Browser.Equal("5", () => Browser.Exists(By.Id("non-persisted-counter")).Text);

// Wait for interactivity - the value should still be 5
Browser.Exists(By.Id("render-mode-interactive"));
Browser.Equal("5", () => Browser.Exists(By.Id("non-persisted-counter")).Text);

// Increment the non-persisted counter to 6 to show it works during interactive session
Browser.Exists(By.Id("increment-non-persisted-counter")).Click();
Browser.Equal("6", () => Browser.Exists(By.Id("non-persisted-counter")).Text);

// Also increment the persistent counter to show the contrast
Browser.Exists(By.Id("increment-persistent-counter-count")).Click();
Browser.Equal("1", () => Browser.Exists(By.Id("persistent-counter-count")).Text);

// Force disconnection and reconnection
var javascript = (IJavaScriptExecutor)Browser;
javascript.ExecuteScript("window.replaceReconnectCallback()");
TriggerReconnectAndInteract(javascript);

// After reconnection:
// - Persistent counter should be 2 (was 1, incremented by TriggerReconnectAndInteract)
Browser.Equal("2", () => Browser.Exists(By.Id("persistent-counter-count")).Text);

// - Non-persisted counter should be 0 (default value) because RestoreStateOnPrerendering
// prevented it from being restored after disconnection
Browser.Equal("0", () => Browser.Exists(By.Id("non-persisted-counter")).Text);

// Verify the non-persisted counter can still be incremented in the new session
Browser.Exists(By.Id("increment-non-persisted-counter")).Click();
Browser.Equal("1", () => Browser.Exists(By.Id("non-persisted-counter")).Text);

// Test repeatability - trigger another disconnection cycle
javascript.ExecuteScript("resetReconnect()");
TriggerReconnectAndInteract(javascript);

// After second reconnection:
// - Persistent counter should be 3
Browser.Equal("3", () => Browser.Exists(By.Id("persistent-counter-count")).Text);

// - Non-persisted counter should be 0 again (reset to default)
Browser.Equal("0", () => Browser.Exists(By.Id("non-persisted-counter")).Text);
}
}

public class CustomUIServerResumeTests : ServerResumeTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,8 @@ public void CanPersistPrerenderedStateDeclaratively_Server()

Browser.Equal("restored", () => Browser.FindElement(By.Id("server")).Text);
Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-server")).Text);
Browser.Equal("restored-prerendering-enabled", () => Browser.FindElement(By.Id("prerendering-enabled-server")).Text);
Browser.Equal("restored-prerendering-disabled", () => Browser.FindElement(By.Id("prerendering-disabled-server")).Text);
}

[Fact]
Expand Down
113 changes: 113 additions & 0 deletions src/Components/test/E2ETest/Tests/StatePersistenceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,117 @@ private void AssertPageState(
Browser.Equal("Streaming: True", () => Browser.FindElement(By.Id("streaming")).Text);
}
}

[Theory]
[InlineData(typeof(InteractiveServerRenderMode), (string)null)]
[InlineData(typeof(InteractiveServerRenderMode), "ServerStreaming")]
[InlineData(typeof(InteractiveWebAssemblyRenderMode), (string)null)]
[InlineData(typeof(InteractiveWebAssemblyRenderMode), "WebAssemblyStreaming")]
[InlineData(typeof(InteractiveAutoRenderMode), (string)null)]
[InlineData(typeof(InteractiveAutoRenderMode), "AutoStreaming")]
public void ComponentWithUpdateStateOnEnhancedNavigationReceivesStateUpdates(Type renderMode, string streaming)
{
var mode = renderMode switch
{
var t when t == typeof(InteractiveServerRenderMode) => "server",
var t when t == typeof(InteractiveWebAssemblyRenderMode) => "wasm",
var t when t == typeof(InteractiveAutoRenderMode) => "auto",
_ => throw new ArgumentException($"Unknown render mode: {renderMode.Name}")
};

// Step 1: Navigate to page without components first to establish initial state
if (streaming == null)
{
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&suppress-autostart");
}
else
{
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&streaming-id={streaming}&suppress-autostart");
}

if (mode == "auto")
{
BlockWebAssemblyResourceLoad();
}

Browser.Click(By.Id("call-blazor-start"));
Browser.Click(By.Id("page-with-components-link"));

// Step 2: Validate initial state - no enhanced nav state should be found
ValidateEnhancedNavState(
mode: mode,
renderMode: renderMode.Name,
interactive: streaming == null,
enhancedNavStateFound: false,
enhancedNavStateValue: "no-enhanced-nav-state",
streamingId: streaming,
streamingCompleted: false);

if (streaming != null)
{
Browser.Click(By.Id("end-streaming"));
ValidateEnhancedNavState(
mode: mode,
renderMode: renderMode.Name,
interactive: true,
enhancedNavStateFound: false,
enhancedNavStateValue: "no-enhanced-nav-state",
streamingId: streaming,
streamingCompleted: true);
}

// Step 3: Navigate back to page without components (this persists state)
Browser.Click(By.Id("page-no-components-link"));

// Step 4: Navigate back to page with components via enhanced navigation
// This should trigger [UpdateStateOnEnhancedNavigation] and update the state
Browser.Click(By.Id("page-with-components-link"));

// Step 5: Validate that enhanced navigation state was updated
ValidateEnhancedNavState(
mode: mode,
renderMode: renderMode.Name,
interactive: streaming == null,
enhancedNavStateFound: true,
enhancedNavStateValue: "enhanced-nav-updated",
streamingId: streaming,
streamingCompleted: streaming == null);

if (streaming != null)
{
Browser.Click(By.Id("end-streaming"));
ValidateEnhancedNavState(
mode: mode,
renderMode: renderMode.Name,
interactive: true,
enhancedNavStateFound: true,
enhancedNavStateValue: "enhanced-nav-updated",
streamingId: streaming,
streamingCompleted: true);
}
}

private void ValidateEnhancedNavState(
string mode,
string renderMode,
bool interactive,
bool enhancedNavStateFound,
string enhancedNavStateValue,
string streamingId = null,
bool streamingCompleted = false)
{
Browser.Equal($"Render mode: {renderMode}", () => Browser.FindElement(By.Id("render-mode")).Text);
Browser.Equal($"Streaming id:{streamingId}", () => Browser.FindElement(By.Id("streaming-id")).Text);
Browser.Equal($"Interactive: {interactive}", () => Browser.FindElement(By.Id("interactive")).Text);

if (streamingId == null || streamingCompleted)
{
Browser.Equal($"Enhanced nav state found:{enhancedNavStateFound}", () => Browser.FindElement(By.Id("enhanced-nav-state-found")).Text);
Browser.Equal($"Enhanced nav state value:{enhancedNavStateValue}", () => Browser.FindElement(By.Id("enhanced-nav-state-value")).Text);
}
else
{
Browser.Equal("Streaming: True", () => Browser.FindElement(By.Id("streaming")).Text);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<p>Application state is <span id="@KeyName">@Value</span></p>
@using Microsoft.AspNetCore.Components.Web
<p>Application state is <span id="@KeyName">@Value</span></p>
<p>Render mode: <span id="render-mode-@KeyName">@_renderMode</span></p>
<p>Prerendering enabled state: <span id="prerendering-enabled-@KeyName">@PrerenderingEnabledValue</span></p>
<p>Prerendering disabled state: <span id="prerendering-disabled-@KeyName">@PrerenderingDisabledValue</span></p>

@code F438 {
[Parameter, EditorRequired]
Expand All @@ -11,11 +14,21 @@
[SupplyParameterFromPersistentComponentState]
public string Value { get; set; }

[SupplyParameterFromPersistentComponentState]
[RestoreStateOnPrerendering]
public string PrerenderingEnabledValue { get; set; }

[SupplyParameterFromPersistentComponentState]
[UpdateStateOnEnhancedNavigation]
public string PrerenderingDisabledValue { get; set; }

private string _renderMode = "SSR";
Copy link
Member
@javiercn javiercn Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot
Look at the #51584

In the Scenario validation criteria.

You are not implementing the test correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the test scenarios to correctly implement the three E2E tests defined in the design document. Changed DeclarativePersistStateComponent to use [RestoreStateOnPrerendering(false)] for opt-out behavior testing, and PersistentCounter to use [RestoreStateOnReconnection(false)] for reconnection opt-out testing. (e180005)


protected override void OnInitialized()
{
Value ??= !RendererInfo.IsInteractive ? InitialValue : "not restored";
PrerenderingEnabledValue ??= !RendererInfo.IsInteractive ? $"{InitialValue}-prerendering-enabled" : "not restored";
PrerenderingDisabledValue ??= !RendererInfo.IsInteractive ? $"{InitialValue}-prerendering-disabled" : "not restored";
_renderMode = OperatingSystem.IsBrowser() ? "WebAssembly" : "Server";
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
<p>Non streaming component with persistent state</p>
@using Microsoft.AspNetCore.Components.Web
<p>Non streaming component with persistent state</p>

<p>This component demonstrates state persistence in the absence of streaming rendering. When the component renders it will try to restore the state and if present display that it succeeded in doing so and the restored value. If the state is not present, it will indicate it didn't find it and display a "fresh" value.</p>

<p id="interactive">Interactive: @(!RunningOnServer)</p>
<p id="interactive-runtime">Interactive runtime: @_interactiveRuntime& 10000 lt;/p>
<p id="state-found">State found:@_stateFound</p>
<p id="state-value">State value:@_stateValue</p>
<p id="enhanced-nav-state-found">Enhanced nav state found:@_enhancedNavStateFound</p>
<p id="enhanced-nav-state-value">Enhanced nav state value:@_enhancedNavState</p>

@code {

private bool _stateFound;
private string _stateValue;
private string _interactiveRuntime;
private bool _enhancedNavStateFound;
private string _enhancedNavState;

[Inject] public PersistentComponentState PersistentComponentState { get; set; }

[CascadingParameter(Name = nameof(RunningOnServer))] public bool RunningOnServer { get; set; }
[Parameter] public string ServerState { get; set; }

[Parameter]
[SupplyParameterFromPersistentComponentState]
[UpdateStateOnEnhancedNavigation]
public string EnhancedNavState { get; set; }

protected override void OnInitialized()
{
PersistentComponentState.RegisterOnPersisting(PersistState);
Expand All @@ -39,11 +49,23 @@
{
_interactiveRuntime = OperatingSystem.IsBrowser() ? "wasm" : "server";
}

// Track enhanced navigation state updates
_enhancedNavState = EnhancedNavState ?? "no-enhanced-nav-state";
_enhancedNavStateFound = !string.IsNullOrEmpty(EnhancedNavState);
}

protected override void OnParametersSet()
{
// This will be called during enhanced navigation when [UpdateStateOnEnhancedNavigation] triggers
_enhancedNavState = EnhancedNavState ?? "no-enhanced-nav-state";
_enhancedNavStateFound = !string.IsNullOrEmpty(EnhancedNavState);
}

Task PersistState()
{
PersistentComponentState.PersistAsJson("NonStreamingComponentWithPersistentState", _stateValue);
PersistentComponentState.PersistAsJson("EnhancedNavState", "enhanced-nav-updated");
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Components.Web
@inject IJSRuntime JSRuntime

<!-- The render changes when the component is resumed because it runs in a separate circuit -->
Expand All @@ -9,13 +10,19 @@
<p>Current render GUID: <span id="persistent-counter-render">@Guid.NewGuid().ToString()</span></p>

<p>Current count: <span id="persistent-counter-count">@State.Count</span></p>
<p>Non-persisted counter: <span id="non-persisted-counter">@NonPersistedCounter</span></p>

<button id="increment-persistent-counter-count" @onclick="IncrementCount">Click me</button>
<button id="increment-non-persisted-counter" @onclick="IncrementNonPersistedCount">Increment non-persisted</button>

@code {

[SupplyParameterFromPersistentComponentState] public CounterState State { get; set; }

[SupplyParameterFromPersistentComponentState]
[RestoreStateOnPrerendering]
public int NonPersistedCounter { get; set; }

public class CounterState
{
public int Count { get; set; } = 0;
Expand All @@ -25,10 +32,21 @@
{
// State is preserved across disconnections
State ??= new CounterState();

// Initialize non-persisted counter to 5 during SSR (before interactivity)
if (!RendererInfo.IsInteractive)
{
NonPersistedCounter = 5;
}
}

private void IncrementCount()
{
State.Count = State.Count + 1;
}

private void IncrementNonPersistedCount()
{
NonPersistedCounter = NonPersistedCounter + 1;
}
}
Loading
0