diff --git a/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj b/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj index e104217b..f5ea8c04 100644 --- a/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj +++ b/src/AdaptiveRemote.App/AdaptiveRemote.App.csproj @@ -31,6 +31,10 @@ + + + + PreserveNewest diff --git a/src/AdaptiveRemote.App/Components/Remote.razor b/src/AdaptiveRemote.App/Components/Remote.razor index df6c23c0..3b123202 100644 --- a/src/AdaptiveRemote.App/Components/Remote.razor +++ b/src/AdaptiveRemote.App/Components/Remote.razor @@ -1,5 +1,10 @@ @inject Services.IRemoteDefinitionService RemoteDefinitions +@inject Services.Layout.IDynamicStylesheetProvider Stylesheet +@if (Stylesheet.GetCss() is { } css) +{ + +} diff --git a/src/AdaptiveRemote.App/Configuration/HostBuilderExtensions.cs b/src/AdaptiveRemote.App/Configuration/HostBuilderExtensions.cs index f5b7f17b..69b742e3 100644 --- a/src/AdaptiveRemote.App/Configuration/HostBuilderExtensions.cs +++ b/src/AdaptiveRemote.App/Configuration/HostBuilderExtensions.cs @@ -30,6 +30,7 @@ internal static IServiceCollection AddRemoteServices(this IServiceCollection ser .AddCloudAssetServices() .AddScopedLifecycleService() .AddScoped() + .AddScoped() .AddSingleton() .Configure(configuration.GetSection(SettingsKeys.ProgrammaticSettings)) .Configure(configuration.GetSection("CloudSettings")); diff --git a/src/AdaptiveRemote.App/Services/Layout/IDynamicStylesheetProvider.cs b/src/AdaptiveRemote.App/Services/Layout/IDynamicStylesheetProvider.cs new file mode 100644 index 00000000..e644f696 --- /dev/null +++ b/src/AdaptiveRemote.App/Services/Layout/IDynamicStylesheetProvider.cs @@ -0,0 +1,9 @@ +namespace AdaptiveRemote.Services.Layout; + +/// +/// Scoped. Returns the CSS for the active layout in this scope. +/// +public interface IDynamicStylesheetProvider +{ + string? GetCss(); +} diff --git a/src/AdaptiveRemote.App/Services/Layout/LayoutStylesheetProvider.cs b/src/AdaptiveRemote.App/Services/Layout/LayoutStylesheetProvider.cs new file mode 100644 index 00000000..6571394b --- /dev/null +++ b/src/AdaptiveRemote.App/Services/Layout/LayoutStylesheetProvider.cs @@ -0,0 +1,19 @@ +using System.Reflection; + +namespace AdaptiveRemote.Services.Layout; + +internal sealed class LayoutStylesheetProvider : IDynamicStylesheetProvider +{ + private static readonly string _css = LoadCss(); + + public string? GetCss() => _css; + + private static string LoadCss() + { + Assembly assembly = typeof(LayoutStylesheetProvider).Assembly; + using Stream stream = assembly.GetManifestResourceStream( + "AdaptiveRemote.Services.Layout.layout-grid.css")!; + using StreamReader reader = new(stream); + return reader.ReadToEnd(); + } +} diff --git a/src/AdaptiveRemote.App/Services/Layout/layout-grid.css b/src/AdaptiveRemote.App/Services/Layout/layout-grid.css new file mode 100644 index 00000000..707174d2 --- /dev/null +++ b/src/AdaptiveRemote.App/Services/Layout/layout-grid.css @@ -0,0 +1,190 @@ +#ROOT { + display: grid; + grid-template-rows: 6fr 3fr 1fr; + grid-template-columns: 3fr 2fr; + grid-gap: 20px; + width: 98vw; + height: 96vh; + padding: 2vh 1vw; +} +#ROOT #DPAD { + grid-row: 1; + grid-column: 1; + display: grid; + grid-template-rows: repeat(3, 1fr); + grid-template-columns: repeat(3, 1fr); + grid-gap: 10px; +} +#ROOT #DPAD #UP { + grid-row: 1; + grid-column: 2; +} +#ROOT #DPAD #DOWN { + grid-row: 3; + grid-column: 2; +} +#ROOT #DPAD #LEFT { + grid-row: 2; + grid-column: 1; +} +#ROOT #DPAD #RIGHT { + grid-row: 2; + grid-column: 3; +} +#ROOT #DPAD #SELECT { + grid-row: 2; + grid-column: 2; +} +#ROOT #DPAD #POWER { + grid-row: 1; + grid-column: 1; + margin: 20px; + background-color: #aa2525; + border-color: #561313; + border-width: 2px; + color: black; +} +#ROOT #DPAD #POWER:hover { + background-color: #bf2a2a; +} +#ROOT #DPAD #POWER:active, +#ROOT #DPAD #POWER.btn-active { + background-color: #d74545; +} +#ROOT #DPAD #POWER.btn-not-programmed { + color: #aa2525; + border-color: #aa2525; + background-color: #222; + border-width: 5px; +} +#ROOT #DPAD #POWER.btn-not-programmed:hover { + background-color: #2f2f2f; +} +#ROOT #DPAD #POWER.btn-not-programmed:active, +#ROOT #DPAD #POWER.btn-not-programmed.btn-active { + background-color: #484848; +} +#ROOT #DPAD #POWER.btn-disabled { + background-color: #888888; + color: #3c3c3c; + border-color: #555555; +} +#ROOT #DPAD #POWERON { + display: none; +} +#ROOT #DPAD #POWEROFF { + display: none; +} +#ROOT #DPAD #BACK { + grid-row: 3; + grid-column: 1; + margin: 20px; +} +#ROOT #WELL { + grid-row: 1; + grid-column: 2; + margin: -0.5%; +} +#ROOT #WELL > button { + width: 49%; + height: 19%; + margin: 0.5%; +} +#ROOT #PLAYBACK { + grid-row: 2; + grid-column: 1; + display: grid; + grid-template-rows: repeat(3, 1fr); + grid-template-columns: repeat(5, 1fr); + grid-gap: 10px; +} +#ROOT #PLAYBACK #REPLAY { + grid-row: 1; + grid-column: 1; +} +#ROOT #PLAYBACK #PLAY { + grid-row: 1; + grid-column: 2; +} +#ROOT #PLAYBACK #PAUSE { + grid-row: 1; + grid-column: 3; +} +#ROOT #PLAYBACK #RECORD { + grid-row: 1; + grid-column: 4; +} +#ROOT #PLAYBACK #SKIP { + grid-row: 1; + grid-column: 5; +} +#ROOT #CHANNELANDVOLUME { + grid-row: 2; + grid-column: 2; + display: grid; + grid-template-rows: 36pt 1fr 1fr; + grid-template-columns: 2fr 1fr 2fr; + grid-gap: 10px; +} +#ROOT #CHANNELANDVOLUME:before { + grid-row: 1; + grid-column: 1; + font-size: 36pt; + font-weight: bold; + color: #62b0ff; + content: "Channel"; + text-align: center; +} +#ROOT #CHANNELANDVOLUME:after { + grid-row: 1; + grid-column: 3; + font-size: 36pt; + font-weight: bold; + color: #62b0ff; + content: "Volume"; + text-align: center; +} +#ROOT #CHANNELANDVOLUME #CHANNELUP { + grid-row: 2; + grid-column: 1; +} +#ROOT #CHANNELANDVOLUME #CHANNELDOWN { + grid-row: 3; + grid-column: 1; +} +#ROOT #CHANNELANDVOLUME #VOLUMEUP { + grid-row: 2; + grid-column: 3; +} +#ROOT #CHANNELANDVOLUME #VOLUMEDOWN { + grid-row: 3; + grid-column: 3; +} +#ROOT #CHANNELANDVOLUME #MUTE { + grid-row: 2; + grid-column: 2; + grid-row-end: span 2; +} +#ROOT #GUTTER { + grid-row: 3; + grid-column-start: 1; + grid-column-end: span 2; + grid-gap: 20px; + display: grid; + grid-template-rows: 1fr; + grid-template-columns: 6fr 1fr 1fr; + grid-gap: 10px; +} +#ROOT #GUTTER #EXIT { + grid-row: 1; + grid-column: 3; +} +#ROOT #GUTTER #LEARN { + grid-row: 1; + grid-column: 2; + margin: 10px; +} +#ROOT #GUTTER #LISTENING { + grid-row: 1; + grid-column: 1; +} diff --git a/src/AdaptiveRemote.App/Services/Testing/IUITestService.cs b/src/AdaptiveRemote.App/Services/Testing/IUITestService.cs index 855f6163..107c805e 100644 --- a/src/AdaptiveRemote.App/Services/Testing/IUITestService.cs +++ b/src/AdaptiveRemote.App/Services/Testing/IUITestService.cs @@ -53,4 +53,13 @@ public partial interface IUITestService : IDisposable /// Cancellation token for the operation. /// The inner HTML of the first matching element, or null if not found or not visible. Task GetInnerHtmlFromElementWithCssClassAsync(string cssClass, CancellationToken cancellationToken); + + /// + /// Gets a CSS property value from the first stylesheet rule that matches the provided selector. + /// + /// The exact CSS selector text to match. + /// The CSS property name to read (for example, display). + /// Cancellation token for the operation. + /// The matching property value, or null if no matching rule/property exists. + Task GetStylesheetRulePropertyValueAsync(string selector, string propertyName, CancellationToken cancellationToken); } diff --git a/src/AdaptiveRemote.App/wwwroot/css/app.css b/src/AdaptiveRemote.App/wwwroot/css/app.css index 83ee5b09..6f45616e 100644 --- a/src/AdaptiveRemote.App/wwwroot/css/app.css +++ b/src/AdaptiveRemote.App/wwwroot/css/app.css @@ -84,197 +84,6 @@ div.conversation-speaking-message { left: 50%; transform: translate(-50%, -50%); } -#ROOT { - display: grid; - grid-template-rows: 6fr 3fr 1fr; - grid-template-columns: 3fr 2fr; - grid-gap: 10px; - grid-gap: 20px; - width: 98vw; - height: 96vh; - padding: 2vh 1vw; -} -#ROOT #DPAD { - grid-row: 1; - grid-column: 1; - display: grid; - grid-template-rows: repeat(3, 1fr); - grid-template-columns: repeat(3, 1fr); - grid-gap: 10px; -} -#ROOT #DPAD #UP { - grid-row: 1; - grid-column: 2; -} -#ROOT #DPAD #DOWN { - grid-row: 3; - grid-column: 2; -} -#ROOT #DPAD #LEFT { - grid-row: 2; - grid-column: 1; -} -#ROOT #DPAD #RIGHT { - grid-row: 2; - grid-column: 3; -} -#ROOT #DPAD #SELECT { - grid-row: 2; - grid-column: 2; -} -#ROOT #DPAD #POWER { - grid-row: 1; - grid-column: 1; - margin: 20px; - background-color: #aa2525; - border-color: #561313; - border-width: 2px; - color: black; -} -#ROOT #DPAD #POWER:hover { - background-color: #bf2a2a; -} -#ROOT #DPAD #POWER:active, -#ROOT #DPAD #POWER.btn-active { - background-color: #d74545; -} -#ROOT #DPAD #POWER.btn-not-programmed { - color: #aa2525; - border-color: #aa2525; - background-color: #222; - border-width: 5px; -} -#ROOT #DPAD #POWER.btn-not-programmed:hover { - background-color: #2f2f2f; -} -#ROOT #DPAD #POWER.btn-not-programmed:active, -#ROOT #DPAD #POWER.btn-not-programmed.btn-active { - background-color: #484848; -} -#ROOT #DPAD #POWER.btn-disabled { - background-color: #888888; - color: #3c3c3c; - border-color: #555555; -} -#ROOT #DPAD #POWERON { - display: none; -} -#ROOT #DPAD #POWEROFF { - display: none; -} -#ROOT #DPAD #BACK { - grid-row: 3; - grid-column: 1; - margin: 20px; -} -#ROOT #WELL { - grid-row: 1; - grid-column: 2; - margin: -0.5%; -} -#ROOT #WELL > button { - width: 49%; - height: 19%; - margin: 0.5%; -} -#ROOT #PLAYBACK { - grid-row: 2; - grid-column: 1; - display: grid; - grid-template-rows: repeat(3, 1fr); - grid-template-columns: repeat(5, 1fr); - grid-gap: 10px; -} -#ROOT #PLAYBACK #REPLAY { - grid-row: 1; - grid-column: 1; -} -#ROOT #PLAYBACK #PLAY { - grid-row: 1; - grid-column: 2; -} -#ROOT #PLAYBACK #PAUSE { - grid-row: 1; - grid-column: 3; -} -#ROOT #PLAYBACK #RECORD { - grid-row: 1; - grid-column: 4; -} -#ROOT #PLAYBACK #SKIP { - grid-row: 1; - grid-column: 5; -} -#ROOT #CHANNELANDVOLUME { - grid-row: 2; - grid-column: 2; - display: grid; - grid-template-rows: 36pt 1fr 1fr; - grid-template-columns: 2fr 1fr 2fr; - grid-gap: 10px; -} -#ROOT #CHANNELANDVOLUME:before { - grid-row: 1; - grid-column: 1; - font-size: 36pt; - font-weight: bold; - color: #62b0ff; - content: "Channel"; - text-align: center; -} -#ROOT #CHANNELANDVOLUME:after { - grid-row: 1; - grid-column: 3; - font-size: 36pt; - font-weight: bold; - color: #62b0ff; - content: "Volume"; - text-align: center; -} -#ROOT #CHANNELANDVOLUME #CHANNELUP { - grid-row: 2; - grid-column: 1; -} -#ROOT #CHANNELANDVOLUME #CHANNELDOWN { - grid-row: 3; - grid-column: 1; -} -#ROOT #CHANNELANDVOLUME #VOLUMEUP { - grid-row: 2; - grid-column: 3; -} -#ROOT #CHANNELANDVOLUME #VOLUMEDOWN { - grid-row: 3; - grid-column: 3; -} -#ROOT #CHANNELANDVOLUME #MUTE { - grid-row: 2; - grid-column: 2; - grid-row-end: span 2; -} -#ROOT #GUTTER { - grid-row: 3; - grid-column-start: 1; - grid-column-end: span 2; - grid-gap: 20px; - display: grid; - grid-template-rows: 1fr; - grid-template-columns: 6fr 1fr 1fr; - grid-gap: 10px; -} -#ROOT #GUTTER #EXIT { - grid-row: 1; - grid-column: 3; -} -#ROOT #GUTTER #LEARN { - grid-row: 1; - grid-column: 2; - margin: 10px; -} -#ROOT #GUTTER #LISTENING { - grid-row: 1; - grid-column: 1; -} #LifecycleTitle { height: 50vh; width: 100%; diff --git a/src/AdaptiveRemote.App/wwwroot/css/app.less b/src/AdaptiveRemote.App/wwwroot/css/app.less index fbd3bbec..f69f7b7b 100644 --- a/src/AdaptiveRemote.App/wwwroot/css/app.less +++ b/src/AdaptiveRemote.App/wwwroot/css/app.less @@ -6,7 +6,6 @@ html, body { @import "button_ui.less"; @import "conversation_ui.less"; -@import "layout.less"; @import "loading_screen.less"; #blazor-error-ui { diff --git a/src/AdaptiveRemote.App/wwwroot/css/app.min.css b/src/AdaptiveRemote.App/wwwroot/css/app.min.css index 97e45add..ce39b9e6 100644 --- a/src/AdaptiveRemote.App/wwwroot/css/app.min.css +++ b/src/AdaptiveRemote.App/wwwroot/css/app.min.css @@ -1 +1 @@ -html,body{font-family:'Segoe UI',Helvetica,Arial,sans-serif;font-size:48px;font-weight:bold;background-color:#222;}html,body{margin:0;}.btn-primary{color:#222;border-radius:15px;font-size:36pt;font-weight:bold;cursor:pointer;background-color:#62b0ff;border-color:#007dfb;border-width:2px;}.btn-primary:hover{background-color:#7cbdff;}.btn-primary:active,.btn-primary.btn-active{background-color:#afd6ff;}.btn-primary.btn-not-programmed{color:#62b0ff;border-color:#62b0ff;background-color:#222;border-width:5px;}.btn-primary.btn-not-programmed:hover{background-color:#2f2f2f;}.btn-primary.btn-not-programmed:active,.btn-primary.btn-not-programmed.btn-active{background-color:#484848;}.btn-primary.btn-disabled{background-color:#888;color:#3c3c3c;border-color:#555;}div.conversation-border{pointer-events:none;position:fixed;top:-20px;left:-20px;border-color:#ffea00;border-style:solid;border-width:30px;border-radius:45px;}div.conversation-border div{width:100vw;height:100vh;margin:-10px;}div.conversation-status-message{color:#ffea00;text-align:center;padding:20px;border-radius:15px;}div.conversation-status-message.clickable{cursor:pointer;}div.conversation-status-message.clickable:hover{background-color:#484848;}div.conversation-speaking-message{font-size:24px;background-color:#222;border-radius:15px;border:5px solid #ffea00;margin:-5px;color:#fff;padding:0 48px;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);}#ROOT{display:grid;grid-template-rows:6fr 3fr 1fr;grid-template-columns:3fr 2fr;grid-gap:10px;grid-gap:20px;width:98vw;height:96vh;padding:2vh 1vw;}#ROOT #DPAD{grid-row:1;grid-column:1;display:grid;grid-template-rows:repeat(3,1fr);grid-template-columns:repeat(3,1fr);grid-gap:10px;}#ROOT #DPAD #UP{grid-row:1;grid-column:2;}#ROOT #DPAD #DOWN{grid-row:3;grid-column:2;}#ROOT #DPAD #LEFT{grid-row:2;grid-column:1;}#ROOT #DPAD #RIGHT{grid-row:2;grid-column:3;}#ROOT #DPAD #SELECT{grid-row:2;grid-column:2;}#ROOT #DPAD #POWER{grid-row:1;grid-column:1;margin:20px;background-color:#aa2525;border-color:#561313;border-width:2px;color:#000;}#ROOT #DPAD #POWER:hover{background-color:#bf2a2a;}#ROOT #DPAD #POWER:active,#ROOT #DPAD #POWER.btn-active{background-color:#d74545;}#ROOT #DPAD #POWER.btn-not-programmed{color:#aa2525;border-color:#aa2525;background-color:#222;border-width:5px;}#ROOT #DPAD #POWER.btn-not-programmed:hover{background-color:#2f2f2f;}#ROOT #DPAD #POWER.btn-not-programmed:active,#ROOT #DPAD #POWER.btn-not-programmed.btn-active{background-color:#484848;}#ROOT #DPAD #POWER.btn-disabled{background-color:#888;color:#3c3c3c;border-color:#555;}#ROOT #DPAD #POWERON{display:none;}#ROOT #DPAD #POWEROFF{display:none;}#ROOT #DPAD #BACK{grid-row:3;grid-column:1;margin:20px;}#ROOT #WELL{grid-row:1;grid-column:2;margin:-.5%;}#ROOT #WELL>button{width:49%;height:19%;margin:.5%;}#ROOT #PLAYBACK{grid-row:2;grid-column:1;display:grid;grid-template-rows:repeat(3,1fr);grid-template-columns:repeat(5,1fr);grid-gap:10px;}#ROOT #PLAYBACK #REPLAY{grid-row:1;grid-column:1;}#ROOT #PLAYBACK #PLAY{grid-row:1;grid-column:2;}#ROOT #PLAYBACK #PAUSE{grid-row:1;grid-column:3;}#ROOT #PLAYBACK #RECORD{grid-row:1;grid-column:4;}#ROOT #PLAYBACK #SKIP{grid-row:1;grid-column:5;}#ROOT #CHANNELANDVOLUME{grid-row:2;grid-column:2;display:grid;grid-template-rows:36pt 1fr 1fr;grid-template-columns:2fr 1fr 2fr;grid-gap:10px;}#ROOT #CHANNELANDVOLUME:before{grid-row:1;grid-column:1;font-size:36pt;font-weight:bold;color:#62b0ff;content:"Channel";text-align:center;}#ROOT #CHANNELANDVOLUME:after{grid-row:1;grid-column:3;font-size:36pt;font-weight:bold;color:#62b0ff;content:"Volume";text-align:center;}#ROOT #CHANNELANDVOLUME #CHANNELUP{grid-row:2;grid-column:1;}#ROOT #CHANNELANDVOLUME #CHANNELDOWN{grid-row:3;grid-column:1;}#ROOT #CHANNELANDVOLUME #VOLUMEUP{grid-row:2;grid-column:3;}#ROOT #CHANNELANDVOLUME #VOLUMEDOWN{grid-row:3;grid-column:3;}#ROOT #CHANNELANDVOLUME #MUTE{grid-row:2;grid-column:2;grid-row-end:span 2;}#ROOT #GUTTER{grid-row:3;grid-column-start:1;grid-column-end:span 2;grid-gap:20px;display:grid;grid-template-rows:1fr;grid-template-columns:6fr 1fr 1fr;grid-gap:10px;}#ROOT #GUTTER #EXIT{grid-row:1;grid-column:3;}#ROOT #GUTTER #LEARN{grid-row:1;grid-column:2;margin:10px;}#ROOT #GUTTER #LISTENING{grid-row:1;grid-column:1;}#LifecycleTitle{height:50vh;width:100%;color:#fff;position:relative;}#LifecycleTitle>div{text-align:center;position:absolute;width:100%;bottom:0;}#LifecycleTaskDescription{text-align:center;color:#fff;font-size:36px;}#blazor-error-ui{background:#ffffe0;bottom:0;box-shadow:0 -1px 2px rgba(0,0,0,.2);display:none;left:0;padding:.6rem 1.25rem .7rem 1.25rem;position:fixed;width:100%;z-index:1000;}#blazor-error-ui .dismiss{cursor:pointer;position:absolute;right:.75rem;top:.5rem;} \ No newline at end of file +html,body{font-family:'Segoe UI',Helvetica,Arial,sans-serif;font-size:48px;font-weight:bold;background-color:#222;}html,body{margin:0;}.btn-primary{color:#222;border-radius:15px;font-size:36pt;font-weight:bold;cursor:pointer;background-color:#62b0ff;border-color:#007dfb;border-width:2px;}.btn-primary:hover{background-color:#7cbdff;}.btn-primary:active,.btn-primary.btn-active{background-color:#afd6ff;}.btn-primary.btn-not-programmed{color:#62b0ff;border-color:#62b0ff;background-color:#222;border-width:5px;}.btn-primary.btn-not-programmed:hover{background-color:#2f2f2f;}.btn-primary.btn-not-programmed:active,.btn-primary.btn-not-programmed.btn-active{background-color:#484848;}.btn-primary.btn-disabled{background-color:#888;color:#3c3c3c;border-color:#555;}div.conversation-border{pointer-events:none;position:fixed;top:-20px;left:-20px;border-color:#ffea00;border-style:solid;border-width:30px;border-radius:45px;}div.conversation-border div{width:100vw;height:100vh;margin:-10px;}div.conversation-status-message{color:#ffea00;text-align:center;padding:20px;border-radius:15px;}div.conversation-status-message.clickable{cursor:pointer;}div.conversation-status-message.clickable:hover{background-color:#484848;}div.conversation-speaking-message{font-size:24px;background-color:#222;border-radius:15px;border:5px solid #ffea00;margin:-5px;color:#fff;padding:0 48px;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);}#LifecycleTitle{height:50vh;width:100%;color:#fff;position:relative;}#LifecycleTitle>div{text-align:center;position:absolute;width:100%;bottom:0;}#LifecycleTaskDescription{text-align:center;color:#fff;font-size:36px;}#blazor-error-ui{background:#ffffe0;bottom:0;box-shadow:0 -1px 2px rgba(0,0,0,.2);display:none;left:0;padding:.6rem 1.25rem .7rem 1.25rem;position:fixed;width:100%;z-index:1000;}#blazor-error-ui .dismiss{cursor:pointer;position:absolute;right:.75rem;top:.5rem;} \ No newline at end of file diff --git a/test/AdaptiveRemote.App.Tests/Services/Layout/LayoutStylesheetProviderTests.cs b/test/AdaptiveRemote.App.Tests/Services/Layout/LayoutStylesheetProviderTests.cs new file mode 100644 index 00000000..490179af --- /dev/null +++ b/test/AdaptiveRemote.App.Tests/Services/Layout/LayoutStylesheetProviderTests.cs @@ -0,0 +1,20 @@ +using FluentAssertions; + +namespace AdaptiveRemote.Services.Layout; + +[TestClass] +public class LayoutStylesheetProviderTests +{ + [TestMethod] + public void LayoutStylesheetProvider_GetCss_ReturnsNonNullContent() + { + // Arrange + LayoutStylesheetProvider sut = new(); + + // Act + string? css = sut.GetCss(); + + // Assert + css.Should().NotBeNullOrEmpty(); + } +} diff --git a/test/AdaptiveRemote.EndToEndTests.Host.Wpf/Features/Shared/LayoutButtons.feature b/test/AdaptiveRemote.EndToEndTests.Host.Wpf/Features/Shared/LayoutButtons.feature index aa4488e6..4b35f786 100644 --- a/test/AdaptiveRemote.EndToEndTests.Host.Wpf/Features/Shared/LayoutButtons.feature +++ b/test/AdaptiveRemote.EndToEndTests.Host.Wpf/Features/Shared/LayoutButtons.feature @@ -3,6 +3,16 @@ Feature: Layout button verification I want all expected buttons from the layout to be present and accessible So that I can control my TV and AV equipment +Scenario: Layout CSS rules are present + Given the application is in the Ready phase + Then the stylesheet selector '#ROOT' should define 'display' as 'grid' + And the stylesheet selector '#ROOT' should define 'grid-template-rows' as '6fr 3fr 1fr' + And the stylesheet selector '#ROOT' should define 'grid-template-columns' as '3fr 2fr' + And the stylesheet selector '#ROOT' should define 'grid-gap' as '20px' + When I click on the 'Exit' button + And I wait for the application to shut down + Then I should not see any error messages in the logs + Scenario: All expected buttons from layout are present Given the application is not running When I start the application diff --git a/test/AdaptiveRemote.EndToEndTests.Steps/UISteps.cs b/test/AdaptiveRemote.EndToEndTests.Steps/UISteps.cs index f94acc3b..37ad1222 100644 --- a/test/AdaptiveRemote.EndToEndTests.Steps/UISteps.cs +++ b/test/AdaptiveRemote.EndToEndTests.Steps/UISteps.cs @@ -60,4 +60,18 @@ public void ThenIShouldNotSeeAModalMessage() { Host.UI.WaitForNoModalMessage(); } + + [Then(@"the stylesheet selector {string} should define {string} as {string}")] + public void ThenTheStylesheetSelectorShouldDefineAs(string selector, string propertyName, string expectedValue) + { + string? actualValue = Host.UI.GetStylesheetRulePropertyValue(selector, propertyName); + Assert.AreEqual( + expectedValue, + actualValue, + "Expected selector '{0}' to define '{1}' as '{2}', but was '{3}'.", + selector, + propertyName, + expectedValue, + actualValue ?? ""); + } } diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/IUITestServiceExtensions.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/IUITestServiceExtensions.cs index e3565d4d..502ddba8 100644 --- a/test/AdaptiveRemote.EndtoEndTests.TestServices/IUITestServiceExtensions.cs +++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/IUITestServiceExtensions.cs @@ -270,6 +270,28 @@ public static bool WaitForButtonProgrammed(this IUITestService service, string l return WaitHelpers.WaitForState(button.IsProgrammedAsync, programmed, timeout); } + /// + /// Gets a stylesheet rule property value using default timeout (synchronous wrapper; blocks the calling thread). + /// + /// The UI test service. + /// The exact CSS selector text to match. + /// The CSS property name to read. + /// Optional timeout for the operation. + /// The property value if found; otherwise null. + public static string? GetStylesheetRulePropertyValue(this IUITestService service, string selector, string propertyName, int timeoutInSeconds = DefaultUITimeoutInSeconds) + => service.GetStylesheetRulePropertyValue(selector, propertyName, TimeSpan.FromSeconds(timeoutInSeconds)); + + /// + /// Gets a stylesheet rule property value using explicit timeout (synchronous wrapper; blocks the calling thread). + /// + /// The UI test service. + /// The exact CSS selector text to match. + /// The CSS property name to read. + /// Timeout for the operation. + /// The property value if found; otherwise null. + public static string? GetStylesheetRulePropertyValue(this IUITestService service, string selector, string propertyName, TimeSpan timeout) + => WaitHelpers.WaitForAsyncTask(ct => service.GetStylesheetRulePropertyValueAsync(selector, propertyName, ct), timeout); + private static IUIButtonTestObject GetButtonByLabel(this IUITestService service, string label, TimeSpan timeout) { IUIButtonTestObject? button = null; diff --git a/test/AdaptiveRemote.EndtoEndTests.TestServices/PlaywrightUITestService.cs b/test/AdaptiveRemote.EndtoEndTests.TestServices/PlaywrightUITestService.cs index 2283ae6e..ce80d0c5 100644 --- a/test/AdaptiveRemote.EndtoEndTests.TestServices/PlaywrightUITestService.cs +++ b/test/AdaptiveRemote.EndtoEndTests.TestServices/PlaywrightUITestService.cs @@ -171,6 +171,38 @@ private ILocator GetTextLocator(string text) } } + public async Task GetStylesheetRulePropertyValueAsync( + string selector, + string propertyName, + CancellationToken cancellationToken = default) + { + return await CurrentPage.EvaluateAsync( + @"({ selector, propertyName }) => { + for (const stylesheet of Array.from(document.styleSheets)) { + let rules; + try { + rules = stylesheet.cssRules; + } catch { + continue; + } + + for (const rule of Array.from(rules)) { + if (rule.type !== CSSRule.STYLE_RULE) { + continue; + } + + if (rule.selectorText === selector) { + const value = rule.style.getPropertyValue(propertyName); + return value ? value.trim() : null; + } + } + } + + return null; + }", + new { selector, propertyName }); + } + public void Dispose() { GC.SuppressFinalize(this);