// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.RegularExpressions;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Tests.Integration.Playwright.Infrastructure;
using Aspire.Dashboard.Resources;
using Aspire.TestUtilities;
using Aspire.Tests.Shared.DashboardModel;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Playwright;
using Xunit;
namespace Aspire.Dashboard.Tests.Integration.Playwright;
[RequiresFeature(TestFeature.Playwright)]
public class ResourcesTests : PlaywrightTestsBase<ResourcesTests.ResourcesDashboardServerFixture>
{
public ResourcesTests(ResourcesDashboardServerFixture dashboardServerFixture)
: base(dashboardServerFixture)
{
}
[Fact]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task ResourcePane_GroupOrderSupportsDragKeyboardAndReload()
{
await RunTestAsync(async page =>
{
await page.SetViewportSizeAsync(1440, 1000);
await page.GotoAsync("/resources");
await page.Locator("#resourcePaneGroupingButton").ClickAsync();
await page.GetByRole(AriaRole.Menuitemradio, new PageGetByRoleOptions { Name = Dashboard.Resources.Layout.ResourcePaneGroupingResourceType, Exact = true }).ClickAsync();
var pane = page.Locator(".resource-pane");
var groups = pane.Locator(".resource-tag-group");
await Assertions.Expect(groups).ToHaveCountAsync(2);
var originalNames = await groups.EvaluateAllAsync<string[]>("groups => groups.map(group => group.getAttribute('aria-label'))");
var sourceKey = await groups.Last.GetAttributeAsync("data-resource-group-key");
await groups.Last.Locator("[data-resource-group-drag-handle]").DragToAsync(groups.First,
new() { TargetPosition = new() { X = 8, Y = 2 } });
await Assertions.Expect(groups.Locator(".resource-tag-name")).ToHaveTextAsync(originalNames.Reverse());
await page.ReloadAsync();
await Assertions.Expect(groups.Locator(".resource-tag-name")).ToHaveTextAsync(originalNames.Reverse());
var sourceHandle = pane.Locator($"[data-resource-group-key='{sourceKey}'] [data-resource-group-drag-handle]");
await sourceHandle.FocusAsync();
await sourceHandle.PressAsync("Alt+ArrowDown");
await Assertions.Expect(groups.Locator(".resource-tag-name")).ToHaveTextAsync(originalNames);
await Assertions.Expect(sourceHandle).ToBeFocusedAsync();
var search = pane.GetByRole(AriaRole.Searchbox);
await search.FillAsync("api");
await Assertions.Expect(pane.Locator("[data-resource-group-drag-handle]")).ToHaveCountAsync(0);
await search.FillAsync(string.Empty);
await Assertions.Expect(groups.Locator(".resource-tag-name")).ToHaveTextAsync(originalNames);
await Assertions.Expect(pane.Locator("[data-resource-group-drag-handle]")).ToHaveCountAsync(2);
});
}
[Fact]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task ResourcePane_ReordersByDraggingAndKeyboardAndRemembersOrder()
{
await RunTestAsync(async page =>
{
await page.SetViewportSizeAsync(1280, 900);
await page.GotoAsync("/resources");
var pane = page.Locator(".resource-pane");
var rows = pane.Locator("[data-resource-name]:has([data-resource-drag-handle])");
await Assertions.Expect(rows).ToHaveCountAsync(3);
var originalOrder = await rows.EvaluateAllAsync<string[]>("rows => rows.map(row => row.dataset.resourceName)");
var source = rows.Last.Locator("[data-resource-drag-handle]");
await source.DragToAsync(rows.First, new LocatorDragToOptions { TargetPosition = new TargetPosition { X = 8, Y = 2 } });
var reordered = new[] { originalOrder[2], originalOrder[0], originalOrder[1] };
await Assertions.Expect(rows.Locator(".resource-row-name")).ToHaveTextAsync(reordered);
await page.ReloadAsync();
await Assertions.Expect(rows.Locator(".resource-row-name")).ToHaveTextAsync(reordered);
var movedHandle = pane.Locator($"[data-resource-name='{originalOrder[2]}'] [data-resource-drag-handle]");
await movedHandle.FocusAsync();
await movedHandle.PressAsync("Alt+ArrowDown");
await Assertions.Expect(rows.Locator(".resource-row-name")).ToHaveTextAsync([originalOrder[0], originalOrder[2], originalOrder[1]]);
await Assertions.Expect(movedHandle).ToBeFocusedAsync();
await movedHandle.PressAsync("Alt+ArrowDown");
await Assertions.Expect(rows.Locator(".resource-row-name")).ToHaveTextAsync(originalOrder);
await Assertions.Expect(movedHandle).ToBeFocusedAsync();
var search = pane.GetByRole(AriaRole.Searchbox);
await search.FillAsync("api");
await Assertions.Expect(pane.Locator("[data-resource-drag-handle]")).ToHaveCountAsync(0);
await search.FillAsync(string.Empty);
await Assertions.Expect(pane.Locator("[data-resource-drag-handle]")).ToHaveCountAsync(3);
await page.Locator("#resourcePaneGroupingButton").ClickAsync();
await page.GetByRole(AriaRole.Menuitemradio, new PageGetByRoleOptions { Name = Dashboard.Resources.Layout.ResourcePaneGroupingState, Exact = true }).ClickAsync();
await Assertions.Expect(pane.Locator("[data-resource-drag-handle]")).ToHaveCountAsync(0);
await page.Locator("#resourcePaneGroupingButton").ClickAsync();
await page.GetByRole(AriaRole.Menuitemradio, new PageGetByRoleOptions { Name = Dashboard.Resources.Layout.ResourcePaneGroupingNone, Exact = true }).ClickAsync();
await Assertions.Expect(rows.Locator(".resource-row-name")).ToHaveTextAsync(originalOrder);
});
}
[Fact]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task ViewOptionsMenu_ReportsExpandedState()
{
await RunTestAsync(async page =>
{
await GoToParametersAndWaitForDataGridLoadAsync(page).DefaultTimeout();
var viewOptionsButton = page.GetByRole(AriaRole.Button, new() { Name = Dashboard.Resources.Resources.ResourcesChangeViewOptions, Exact = true });
await Assertions.Expect(viewOptionsButton).Not.ToHaveAttributeAsync("aria-expanded", "true");
await viewOptionsButton.ClickAsync();
await Assertions.Expect(viewOptionsButton).ToHaveAttributeAsync("aria-expanded", "true");
await Assertions.Expect(page.GetByRole(AriaRole.Menuitem, new PageGetByRoleOptions { Name = Dashboard.Resources.Resources.ResourceCollapseAllChildren, Exact = true })).ToHaveCountAsync(0);
await Assertions.Expect(page.GetByRole(AriaRole.Menuitem, new PageGetByRoleOptions { Name = Dashboard.Resources.Resources.ResourceExpandAllChildren, Exact = true })).ToHaveCountAsync(0);
var showResourceTypes = page.GetByRole(AriaRole.Menuitem, new PageGetByRoleOptions { Name = Dashboard.Resources.Resources.ResourcesShowTypes, Exact = true });
await showResourceTypes.ClickAsync();
await Assertions.Expect(viewOptionsButton).Not.ToHaveAttributeAsync("aria-expanded", "true");
});
}
[Fact]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task GridActionButtons_UseCompactMinimumWidth()
{
await RunTestAsync(async page =>
{
await GoToParametersAndWaitForDataGridLoadAsync(page).DefaultTimeout();
var buttons = page.Locator(".grid-action-container fluent-button");
await Assertions.Expect(buttons.First).ToHaveCSSAsync("min-width", "32px");
var values = await buttons.EvaluateAllAsync<double[]>(
"""
buttons => buttons.map(button => Number.parseFloat(getComputedStyle(button).minWidth))
""");
Assert.NotEmpty(values);
Assert.Single(values.Distinct());
Assert.InRange(values[0], 32, 36);
});
}
[Fact]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task NameColumn_SortsParameters()
{
await RunTestAsync(async page =>
{
await GoToParametersAndWaitForDataGridLoadAsync(page).DefaultTimeout();
var resourceNames = page.Locator(".main-grid .resource-row .resource-name-text");
await Assertions.Expect(resourceNames).ToHaveCountAsync(2);
await Assertions.Expect(resourceNames.Nth(0)).ToContainTextAsync("alpha-param");
await Assertions.Expect(resourceNames.Nth(1)).ToContainTextAsync("zeta-param");
var nameHeader = page.Locator(".main-grid th[col-index='1']");
var sortItem = page.GetByRole(AriaRole.Menuitem, new PageGetByRoleOptions { Name = ControlsStrings.FluentDataGridHeaderCellSortButtonText, Exact = true });
var nameHeaderButton = nameHeader.GetByRole(AriaRole.Button, new LocatorGetByRoleOptions { Name = ControlsStrings.NameColumnHeader, Exact = true });
await nameHeaderButton.ClickAsync();
await sortItem.ClickAsync();
await Assertions.Expect(nameHeader).ToHaveAttributeAsync("aria-sort", "ascending");
await Assertions.Expect(resourceNames.Nth(0)).ToContainTextAsync("alpha-param");
await nameHeaderButton.ClickAsync();
await page.GetByRole(AriaRole.Menuitem, new PageGetByRoleOptions { Name = ControlsStrings.FluentDataGridHeaderCellSortAscendingButtonText, Exact = true }).ClickAsync();
await Assertions.Expect(nameHeader).ToHaveAttributeAsync("aria-sort", "descending");
await Assertions.Expect(resourceNames.Nth(0)).ToContainTextAsync("zeta-param");
await Assertions.Expect(resourceNames.Nth(1)).ToContainTextAsync("alpha-param");
});
}
[Fact]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task ResourcePane_MobileDrawerSelectsResource()
{
await RunTestAsync(async page =>
{
await page.SetViewportSizeAsync(360, 720);
await page.GotoAsync("/resources");
var layout = page.Locator(".resources-layout");
await Assertions.Expect(layout).Not.ToHaveClassAsync(new Regex("drawer-open"));
await page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = Dashboard.Resources.Layout.ResourceHeaderAllResources, Exact = true }).ClickAsync();
await Assertions.Expect(layout).ToHaveClassAsync(new Regex("drawer-open"));
var resourceLink = page.Locator(".resource-pane").GetByRole(AriaRole.Link, new LocatorGetByRoleOptions { Name = "TestResource" });
var linkBounds = await resourceLink.BoundingBoxAsync();
Assert.NotNull(linkBounds);
Assert.True(linkBounds.X >= 0 && linkBounds.X + linkBounds.Width <= 360, $"The resource link should fit inside the viewport, but it spanned {linkBounds.X} to {linkBounds.X + linkBounds.Width}.");
await page.Mouse.ClickAsync(
linkBounds.X + linkBounds.Width / 2,
linkBounds.Y + linkBounds.Height / 2);
await page.WaitForFunctionAsync("() => location.pathname === '/resources/TestResource'");
await Assertions.Expect(layout).Not.ToHaveClassAsync(new Regex("drawer-open"));
await Assertions.Expect(page.Locator(".resource-title")).ToHaveTextAsync("TestResource");
});
}
[Fact]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task GraphView_SwitchesWithoutReloadOrLayoutCollapse()
{
await RunTestAsync(async page =>
{
await PlaywrightFixture.GoToHomeAndWaitForDataGridLoad(page).DefaultTimeout();
var navigationCount = await page.EvaluateAsync<int>("() => performance.getEntriesByType('navigation').length");
var layoutSelector = page.GetByRole(AriaRole.Group, new PageGetByRoleOptions { Name = Dashboard.Resources.Resources.LayoutLabel, Exact = true });
var graphButton = layoutSelector.GetByRole(AriaRole.Button, new LocatorGetByRoleOptions { Name = Dashboard.Resources.Layout.NavMenuGraphTab, Exact = true });
await graphButton.ClickAsync();
await Assertions.Expect(graphButton).ToHaveAttributeAsync("aria-pressed", "true");
var graphContainer = page.GetByRole(AriaRole.Region, new() { Name = Dashboard.Resources.ControlsStrings.ChartContainerGraphAccessibleLabel });
await Assertions.Expect(graphContainer).ToBeVisibleAsync();
var graphWidth = await graphContainer.EvaluateAsync<double>("element => element.getBoundingClientRect().width");
Assert.True(graphWidth > 100, $"The resource graph should fill the page content area, but its width was {graphWidth}px.");
var navigationCountAfterSwitch = await page.EvaluateAsync<int>("() => performance.getEntriesByType('navigation').length");
Assert.Equal(navigationCount, navigationCountAfterSwitch);
await Assertions.Expect(page.Locator("#blazor-error-ui")).ToBeHiddenAsync();
});
}
[Fact]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task ResourceGraphCog_IsKeyboardAccessibleAndDoesNotDragNode()
{
await RunTestAsync(async page =>
{
await PlaywrightFixture.GoToHomeAndWaitForDataGridLoad(page).DefaultTimeout();
await page.GetByRole(AriaRole.Group, new() { Name = Dashboard.Resources.Resources.LayoutLabel, Exact = true })
.GetByRole(AriaRole.Button, new() { Name = Dashboard.Resources.Layout.NavMenuGraphTab, Exact = true })
.ClickAsync();
var node = page.Locator(".resource-group[resource-name='TestResource']");
await Assertions.Expect(node).ToBeVisibleAsync();
await node.HoverAsync();
var resourceActionsLabel = string.Format(
Dashboard.Resources.Resources.ResourcesGraphResourceActionsButton,
"TestResource");
var otherResourceActionsLabel = string.Format(
Dashboard.Resources.Resources.ResourcesGraphResourceActionsButton,
"apigateway");
var cog = node.GetByRole(
AriaRole.Button,
new LocatorGetByRoleOptions
{
Name = resourceActionsLabel,
Exact = true
});
await Assertions.Expect(cog).ToBeVisibleAsync();
await Assertions.Expect(page.GetByRole(
AriaRole.Button,
new PageGetByRoleOptions
{
Name = otherResourceActionsLabel,
Exact = true
})).ToHaveCountAsync(1);
// Attempt a large drag from the cog. D3's drag behavior is attached to the ancestor
// resource group, so this verifies the cog stops the initiating pointer event.
await WaitForPositionToStabilizeAsync(node);
var nodeBoundsBefore = await node.BoundingBoxAsync();
var cogBounds = await cog.BoundingBoxAsync();
Assert.NotNull(nodeBoundsBefore);
Assert.NotNull(cogBounds);
await page.Mouse.MoveAsync(
cogBounds.X + cogBounds.Width / 2,
cogBounds.Y + cogBounds.Height / 2);
await page.Mouse.DownAsync();
Assert.Equal(
"none",
await cog.EvaluateAsync<string>("element => getComputedStyle(element).outlineStyle"));
await page.Mouse.MoveAsync(
cogBounds.X + cogBounds.Width / 2 + 80,
cogBounds.Y + cogBounds.Height / 2 + 80,
new MouseMoveOptions { Steps = 5 });
await page.Mouse.UpAsync();
var nodeBoundsAfter = await node.BoundingBoxAsync();
Assert.NotNull(nodeBoundsAfter);
Assert.InRange(Math.Abs(nodeBoundsAfter.X - nodeBoundsBefore.X), 0, 5);
Assert.InRange(Math.Abs(nodeBoundsAfter.Y - nodeBoundsBefore.Y), 0, 5);
Assert.False((await node.GetAttributeAsync("class"))?.Split(' ').Contains("resource-group-selected"));
await Assertions.Expect(page.Locator("fluent-menu-list:visible")).ToHaveCountAsync(0);
await node.HoverAsync();
await cog.ClickAsync();
var menu = page.Locator("fluent-menu-list:visible");
await Assertions.Expect(menu).ToBeVisibleAsync();
await Assertions.Expect(cog).ToHaveAttributeAsync("aria-expanded", "true");
Assert.False((await node.GetAttributeAsync("class"))?.Split(' ').Contains("resource-group-selected"));
await menu.GetByRole(AriaRole.Menuitem).First.FocusAsync();
await page.Keyboard.PressAsync("Escape");
await Assertions.Expect(menu).ToBeHiddenAsync();
await Assertions.Expect(cog).ToHaveAttributeAsync("aria-expanded", "false");
await node.HoverAsync();
await cog.FocusAsync();
await page.Keyboard.PressAsync("Enter");
await Assertions.Expect(menu).ToBeVisibleAsync();
await Assertions.Expect(cog).ToHaveAttributeAsync("aria-haspopup", "menu");
await Assertions.Expect(cog).ToHaveAttributeAsync("aria-expanded", "true");
var header = menu.Locator(".aspire-menu-header");
await Assertions.Expect(header.Locator(".aspire-menu-header-text")).ToHaveTextAsync("TestResource");
var headerBounds = await header.BoundingBoxAsync();
Assert.NotNull(headerBounds);
Assert.InRange(headerBounds.Height, 35, 37);
await menu.GetByRole(AriaRole.Menuitem).First.FocusAsync();
await page.Keyboard.PressAsync("Escape");
await Assertions.Expect(menu).ToBeHiddenAsync();
await Assertions.Expect(cog).ToHaveAttributeAsync("aria-expanded", "false");
await Assertions.Expect(cog).ToBeFocusedAsync();
await page.Keyboard.PressAsync("Enter");
var viewDetails = page.GetByRole(
AriaRole.Menuitem,
new PageGetByRoleOptions
{
Name = ControlsStrings.ActionViewDetailsText,
Exact = true
});
await viewDetails.FocusAsync();
await page.Keyboard.PressAsync("Enter");
// Non-parameter resources open in the Resources view rather than a graph side panel.
await page.WaitForFunctionAsync("() => location.pathname === '/resources/TestResource'");
await Assertions.Expect(page.Locator(".resource-title")).ToHaveTextAsync("TestResource");
});
}
private static async Task WaitForPositionToStabilizeAsync(ILocator locator)
{
await locator.EvaluateAsync(
"""
element => new Promise(resolve => {
let previousBounds;
let stableFrames = 0;
const checkPosition = () => {
const bounds = element.getBoundingClientRect();
if (previousBounds &&
Math.abs(bounds.x - previousBounds.x) <= 0.5 &&
Math.abs(bounds.y - previousBounds.y) <= 0.5) {
stableFrames++;
} else {
stableFrames = 0;
}
if (stableFrames >= 3) {
resolve();
return;
}
previousBounds = bounds;
requestAnimationFrame(checkPosition);
};
requestAnimationFrame(checkPosition);
})
""").DefaultTimeout();
}
[Fact]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task ResourceGraphGame_IsKeyboardAccessibleAndReleasesGraphOnExit()
{
await RunTestAsync(async page =>
{
await PlaywrightFixture.GoToHomeAndWaitForDataGridLoad(page).DefaultTimeout();
await page.GetByRole(AriaRole.Group, new() { Name = Dashboard.Resources.Resources.LayoutLabel, Exact = true })
.GetByRole(AriaRole.Button, new() { Name = Dashboard.Resources.Layout.NavMenuGraphTab, Exact = true })
.ClickAsync();
var node = page.Locator(".resource-group[resource-name='TestResource']");
await Assertions.Expect(node).ToBeVisibleAsync();
var graphContainer = page.GetByRole(AriaRole.Region, new() { Name = Dashboard.Resources.ControlsStrings.ChartContainerGraphAccessibleLabel });
var game = page.GetByRole(AriaRole.Application, new() { Name = Dashboard.Resources.Resources.ResourceGraphGameInstructions, Exact = true });
var cog = node.GetByRole(AriaRole.Button, new()
{
Name = string.Format(Dashboard.Resources.Resources.ResourcesGraphResourceActionsButton, "TestResource"),
Exact = true
});
// The visible way in: the button is hidden with the rest of the graph, and comes back on exit.
var play = graphContainer.GetByRole(AriaRole.Button, new() { Name = Dashboard.Resources.Resources.ResourceGraphGamePlay, Exact = true });
await play.ClickAsync();
await Assertions.Expect(game).ToBeVisibleAsync();
await Assertions.Expect(game).ToBeFocusedAsync();
await Assertions.Expect(play).ToBeHiddenAsync();
await page.Keyboard.PressAsync("Escape");
await Assertions.Expect(game).ToHaveCountAsync(0);
await Assertions.Expect(play).ToBeVisibleAsync();
Assert.True(await page.EvaluateAsync<bool>("() => document.activeElement?.closest('.graph-game') != null"));
await graphContainer.FocusAsync();
await page.Keyboard.PressAsync("o");
await Assertions.Expect(game).ToBeVisibleAsync();
await Assertions.Expect(game).ToBeFocusedAsync();
// The graph behind the game can't be reached by keyboard or assistive technology.
await Assertions.Expect(cog).ToHaveCountAsync(0);
await page.Keyboard.PressAsync("Shift+Tab");
Assert.True(await page.EvaluateAsync<bool>("() => document.activeElement?.closest('.resource-graph') == null"));
await game.FocusAsync();
await page.WaitForTimeoutAsync(1500);
// Hard core is off until it's picked, and picking it says what it does.
var normal = game.GetByRole(AriaRole.Button, new() { Name = Dashboard.Resources.Resources.ResourceGraphGameModeNormal, Exact = true });
var hardCore = game.GetByRole(AriaRole.Button, new() { Name = Dashboard.Resources.Resources.ResourceGraphGameModeHardCore, Exact = true });
await Assertions.Expect(normal).ToHaveAttributeAsync("aria-pressed", "true");
await Assertions.Expect(game.GetByText(Dashboard.Resources.Resources.ResourceGraphGameHardCoreWarning)).ToHaveCountAsync(0);
await hardCore.ClickAsync();
await Assertions.Expect(hardCore).ToHaveAttributeAsync("aria-pressed", "true");
await Assertions.Expect(game.GetByText(Dashboard.Resources.Resources.ResourceGraphGameHardCoreWarning)).ToBeVisibleAsync();
await Assertions.Expect(game).ToBeFocusedAsync();
await page.Keyboard.PressAsync("h");
await Assertions.Expect(normal).ToHaveAttributeAsync("aria-pressed", "true");
// Enter starts the game from the title screen, and doesn't leave it. The modes can't be changed during play.
await page.Keyboard.PressAsync("Enter");
await Assertions.Expect(hardCore).ToBeHiddenAsync();
var sound = game.GetByRole(AriaRole.Button, new() { Name = Dashboard.Resources.Resources.ResourceGraphGameSoundOn, Exact = true });
await page.Keyboard.PressAsync("Tab");
await Assertions.Expect(sound).ToBeFocusedAsync();
await page.Keyboard.PressAsync("Enter");
await Assertions.Expect(game.GetByRole(AriaRole.Button, new() { Name = Dashboard.Resources.Resources.ResourceGraphGameSoundOff, Exact = true })).ToBeVisibleAsync();
// Sound gives focus back to the game, so that Space keeps firing.
await Assertions.Expect(game).ToBeFocusedAsync();
await page.Keyboard.PressAsync("Tab");
await page.Keyboard.PressAsync("Tab");
await Assertions.Expect(game.GetByRole(AriaRole.Button, new() { Name = Dashboard.Resources.Resources.ResourceGraphGameExitLabel, Exact = true })).ToBeFocusedAsync();
await page.Keyboard.PressAsync("Space");
await Assertions.Expect(game).ToHaveCountAsync(0);
await Assertions.Expect(graphContainer).ToBeFocusedAsync();
await Assertions.Expect(cog).ToHaveCountAsync(1);
await Assertions.Expect(node).ToBeVisibleAsync();
// Leaving the page while playing also closes the game, and it can be started again after coming back.
await page.Keyboard.PressAsync("o");
await Assertions.Expect(game).ToBeVisibleAsync();
await page.GetByRole(AriaRole.Tab, new() { Name = Dashboard.Resources.Layout.NavMenuConsoleLogsTab, Exact = true }).ClickAsync();
await Assertions.Expect(game).ToHaveCountAsync(0);
await Assertions.Expect(page.Locator("#resource-graph-game-style")).ToHaveCountAsync(0);
await page.GetByRole(AriaRole.Tab, new() { Name = Dashboard.Resources.Layout.ResourceTabOverview, Exact = true }).ClickAsync();
await Assertions.Expect(node).ToBeVisibleAsync();
await graphContainer.FocusAsync();
await page.Keyboard.PressAsync("o");
await Assertions.Expect(game).ToBeVisibleAsync();
await page.Keyboard.PressAsync("Escape");
await Assertions.Expect(game).ToHaveCountAsync(0);
});
}
[Fact]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task GraphNode_RightClickOpensMenuAtCursorWithoutOverlay()
{
await RunTestAsync(async page =>
{
await PlaywrightFixture.GoToHomeAndWaitForDataGridLoad(page).DefaultTimeout();
await page.GetByRole(AriaRole.Group, new() { Name = Dashboard.Resources.Resources.LayoutLabel, Exact = true })
.GetByRole(AriaRole.Button, new() { Name = Dashboard.Resources.Layout.NavMenuGraphTab, Exact = true })
.ClickAsync();
var node = page.Locator(".resource-node").First;
await Assertions.Expect(node).ToBeVisibleAsync();
var nodeBounds = await node.BoundingBoxAsync();
Assert.NotNull(nodeBounds);
var cursorX = nodeBounds.X + nodeBounds.Width / 2;
var cursorY = nodeBounds.Y + nodeBounds.Height / 2;
await node.Locator("xpath=..").EvaluateAsync(
"""
element => {
const bounds = element.querySelector('.resource-node').getBoundingClientRect();
element.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true,
button: 2,
clientX: Math.round(bounds.left + bounds.width / 2),
clientY: Math.round(bounds.top + bounds.height / 2)
}));
}
""");
var menu = page.Locator("fluent-menu-list:visible");
await Assertions.Expect(menu).ToBeVisibleAsync();
var menuBounds = await menu.BoundingBoxAsync();
Assert.NotNull(menuBounds);
Assert.InRange(Math.Abs(menuBounds.X - cursorX), 0, 2);
Assert.InRange(Math.Abs(menuBounds.Y - cursorY), 0, 2);
var visibleBlockingIndicators = await page.Locator("fluent-overlay, .aspire-progress-ring").EvaluateAllAsync<int>(
"elements => elements.filter(element => element.getBoundingClientRect().width > 0 && element.getBoundingClientRect().height > 0).length");
Assert.Equal(0, visibleBlockingIndicators);
await menu.GetByRole(AriaRole.Menuitem).First.FocusAsync();
await page.Keyboard.PressAsync("Escape");
await Assertions.Expect(menu).ToBeHiddenAsync();
await node.Locator("xpath=..").EvaluateAsync(
"""
element => {
const bounds = element.querySelector('.resource-node').getBoundingClientRect();
element.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true,
button: 2,
clientX: Math.round(bounds.left + bounds.width / 2),
clientY: Math.round(bounds.top + bounds.height / 2)
}));
}
""");
await Assertions.Expect(menu).ToBeVisibleAsync();
});
}
public sealed class ResourcesDashboardServerFixture : DashboardServerFixture
{
protected override IReadOnlyList<ResourceViewModel> Resources =>
[
ModelTestHelpers.CreateResource(
resourceName: "apigateway",
resourceType: KnownResourceTypes.Project,
state: KnownResourceState.Running),
ModelTestHelpers.CreateResource(
resourceName: "basketcache",
resourceType: KnownResourceTypes.Container,
state: KnownResourceState.Running),
ModelTestHelpers.CreateResource(
resourceName: "hidden-resource",
resourceType: KnownResourceTypes.Container,
state: KnownResourceState.Running,
hidden: true),
ModelTestHelpers.CreateResource(
resourceName: "TestResource",
resourceType: KnownResourceTypes.Project,
state: KnownResourceState.Running,
urls:
[
new UrlViewModel("http", new Uri("about:blank#resource-url"), isInternal: false, isInactive: false, UrlDisplayPropertiesViewModel.Empty)
]),
ModelTestHelpers.CreateResource(
resourceName: "zeta-param",
resourceType: KnownResourceTypes.Parameter,
state: KnownResourceState.Running),
ModelTestHelpers.CreateResource(
resourceName: "alpha-param",
resourceType: KnownResourceTypes.Parameter,
state: KnownResourceState.Running)
];
}
private async Task GoToParametersAndWaitForDataGridLoadAsync(IPage page)
{
var baseUrl = DashboardServerFixture.DashboardApp.FrontendSingleEndPointAccessor().GetResolvedAddress();
await page.Context.AddCookiesAsync([new Cookie { Name = "Aspire_Dashboard_Experience", Value = "legacy", Url = baseUrl }]);
await page.GotoAsync("/legacy/resources");
await page.GetByRole(AriaRole.Tab, new() { Name = Dashboard.Resources.Layout.NavMenuParametersTab, Exact = true }).ClickAsync();
await Assertions.Expect(page.Locator(".main-grid .resource-row").First).ToBeVisibleAsync();
}
}