Skip to content

Commit 2856dca

Browse files
committed
[Blazor][Templates] Replaces selenium with playwright for our E2E Tests
* Supports multiple browsers. * Supports multiple OS. * Individual configuration per browser via configuration files. * Separate configuration for CI environments. * Separate configuration per Operating System. * Separate configuration for debugging. * Removes the requirement to have a browser installed. * Removes the requirement to have JAVA installed. * Removes the requirement to have nodejs installed. * Records detailed information on each test run * Network traces from the browser. * Video of the test execution in the browser. * Browser logs. * Network errors on the browser. * Page errors on the browser.
1 parent 5638880 commit 2856dca

37 files changed

+1335
-284
lines changed

.config/dotnet-tools.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
"commands": [
1414
"dotnet-format"
1515
]
16+
},
17+
"playwright-sharp-tool": {
18+
"version": "0.170.2",
19+
"commands": [
20+
"playwright-sharp"
21+
]
1622
}
1723
}
1824
}

AspNetCore.sln

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,6 +1568,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IIS.Common.TestLib", "src\S
15681568
EndProject
15691569
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InProcessNewShimWebSite", "src\Servers\IIS\IIS\test\testassets\InProcessNewShimWebSite\InProcessNewShimWebSite.csproj", "{22EA0993-8DFC-40C2-8481-8E85E21EFB56}"
15701570
EndProject
1571+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BrowserTesting", "BrowserTesting", "{8F33439F-5532-45D6-8A44-20EF9104AA9D}"
1572+
EndProject
1573+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.BrowserTesting", "src\Shared\BrowserTesting\src\Microsoft.AspNetCore.BrowserTesting.csproj", "{B739074E-6652-4F5B-B37E-775DC2245FEC}"
1574+
EndProject
15711575
Global
15721576
GlobalSection(SolutionConfigurationPlatforms) = preSolution
15731577
Debug|Any CPU = Debug|Any CPU
@@ -7435,6 +7439,18 @@ Global
74357439
{22EA0993-8DFC-40C2-8481-8E85E21EFB56}.Release|x64.Build.0 = Release|x64
74367440
{22EA0993-8DFC-40C2-8481-8E85E21EFB56}.Release|x86.ActiveCfg = Release|x86
74377441
{22EA0993-8DFC-40C2-8481-8E85E21EFB56}.Release|x86.Build.0 = Release|x86
7442+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
7443+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
7444+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Debug|x64.ActiveCfg = Debug|Any CPU
7445+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Debug|x64.Build.0 = Debug|Any CPU
7446+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Debug|x86.ActiveCfg = Debug|Any CPU
7447+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Debug|x86.Build.0 = Debug|Any CPU
7448+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
7449+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|Any CPU.Build.0 = Release|Any CPU
7450+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x64.ActiveCfg = Release|Any CPU
7451+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x64.Build.0 = Release|Any CPU
7452+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x86.ActiveCfg = Release|Any CPU
7453+
{B739074E-6652-4F5B-B37E-775DC2245FEC}.Release|x86.Build.0 = Release|Any CPU
74387454
EndGlobalSection
74397455
GlobalSection(SolutionProperties) = preSolution
74407456
HideSolutionNode = FALSE
@@ -8209,6 +8225,8 @@ Global
82098225
{9B8F871E-ED33-4D2F-AA49-E39D9299EC85} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C}
82108226
{7F295396-DBBD-40A5-A645-10004D1324DA} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C}
82118227
{22EA0993-8DFC-40C2-8481-8E85E21EFB56} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C}
8228+
{8F33439F-5532-45D6-8A44-20EF9104AA9D} = {5F0044F2-4C66-46A8-BD79-075F001AA034}
8229+
{B739074E-6652-4F5B-B37E-775DC2245FEC} = {8F33439F-5532-45D6-8A44-20EF9104AA9D}
82128230
EndGlobalSection
82138231
GlobalSection(ExtensibilityGlobals) = postSolution
82148232
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

eng/Dependencies.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ and are generated based on the last package release.
175175
<LatestPackageReference Include="Newtonsoft.Json.Bson" />
176176
<LatestPackageReference Include="NSwag.ApiDescription.Client" />
177177
<LatestPackageReference Include="NuGet.Versioning" />
178+
<LatestPackageReference Include="PlaywrightSharp" />
178179
<LatestPackageReference Include="Polly" />
179180
<LatestPackageReference Include="Polly.Extensions.Http" />
180181
<LatestPackageReference Include="Selenium.Support" />

eng/Versions.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@
273273
<NewtonsoftJsonBsonVersion>1.0.2</NewtonsoftJsonBsonVersion>
274274
<NewtonsoftJsonVersion>12.0.2</NewtonsoftJsonVersion>
275275
<NSwagApiDescriptionClientVersion>13.0.4</NSwagApiDescriptionClientVersion>
276+
<PlaywrightSharpVersion>0.180.0</PlaywrightSharpVersion>
276277
<PollyExtensionsHttpVersion>3.0.0</PollyExtensionsHttpVersion>
277278
<PollyVersion>7.1.0</PollyVersion>
278279
<SeleniumSupportVersion>4.0.0-alpha07</SeleniumSupportVersion>
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using Microsoft.AspNetCore.E2ETesting;
54
using Microsoft.AspNetCore.Testing;
5+
using ProjectTemplates.Tests.Infrastructure;
6+
using Templates.Test;
67
using Templates.Test.Helpers;
78

89
[assembly: AssemblyFixture(typeof(ProjectFactoryFixture))]
9-
[assembly: AssemblyFixture(typeof(SeleniumStandaloneServer))]
10+
[assembly: AssemblyFixture(typeof(PlaywrightFixture<BlazorServerTemplateTest>))]
1011

src/ProjectTemplates/BlazorTemplates.Tests/BlazorServerTemplateTest.cs

Lines changed: 81 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,44 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Linq;
67
using System.Net;
7-
using System.Threading;
8+
using System.Runtime.InteropServices;
89
using System.Threading.Tasks;
9-
using Microsoft.AspNetCore.E2ETesting;
10-
using Microsoft.AspNetCore.Testing;
11-
using OpenQA.Selenium;
10+
using Microsoft.AspNetCore.BrowserTesting;
11+
using PlaywrightSharp;
12+
using ProjectTemplates.Tests.Infrastructure;
1213
using Templates.Test.Helpers;
1314
using Xunit;
1415
using Xunit.Abstractions;
1516

1617
namespace Templates.Test
1718
{
18-
public class BlazorServerTemplateTest : BrowserTestBase
19+
public class BlazorServerTemplateTest : BlazorTemplateTest
1920
{
20-
public BlazorServerTemplateTest(ProjectFactoryFixture projectFactory, BrowserFixture browserFixture, ITestOutputHelper output) : base(browserFixture, output)
21+
public BlazorServerTemplateTest(ProjectFactoryFixture projectFactory, PlaywrightFixture<BlazorServerTemplateTest> fixture, ITestOutputHelper output)
22+
: base(fixture)
2123
{
22-
ProjectFactory = projectFactory;
24+
ProjectFactory = projectFactory; ;
25+
Output = output;
26+
BrowserContextInfo = new ContextInformation(CreateFactory(output));
2327
}
2428

2529
public ProjectFactoryFixture ProjectFactory { get; set; }
26-
30+
public ITestOutputHelper Output { get; }
31+
public ContextInformation BrowserContextInfo { get; }
2732
public Project Project { get; private set; }
2833

29-
[Fact]
30-
public async Task BlazorServerTemplateWorks_NoAuth()
34+
35+
[Theory]
36+
[InlineData(BrowserKind.Chromium)]
37+
public async Task BlazorServerTemplateWorks_NoAuth(BrowserKind browserKind)
3138
{
3239
// Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278
3340
Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true");
3441

35-
Project = await ProjectFactory.GetOrCreateProject("blazorservernoauth", Output);
42+
Project = await ProjectFactory.GetOrCreateProject("blazorservernoauth" + browserKind.ToString(), Output);
3643

3744
var createResult = await Project.RunDotNetNewAsync("blazorserver");
3845
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
@@ -47,21 +54,28 @@ public async Task BlazorServerTemplateWorks_NoAuth()
4754
var buildResult = await Project.RunDotNetBuildAsync();
4855
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult));
4956

57+
await using var browser = Fixture.BrowserManager.IsAvailable(browserKind) ?
58+
await Fixture.BrowserManager.GetBrowserInstance(browserKind, BrowserContextInfo) :
59+
null;
60+
5061
using (var aspNetProcess = Project.StartBuiltProjectAsync())
5162
{
5263
Assert.False(
5364
aspNetProcess.Process.HasExited,
5465
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", Project, aspNetProcess.Process));
5566

5667
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
57-
if (BrowserFixture.IsHostAutomationSupported())
68+
69+
if (Fixture.BrowserManager.IsAvailable(browserKind))
5870
{
59-
aspNetProcess.VisitInBrowser(Browser);
60-
TestBasicNavigation();
71+
var page = await browser.NewPageAsync();
72+
await aspNetProcess.VisitInBrowserAsync(page);
73+
await TestBasicNavigation(page);
74+
await page.CloseAsync();
6175
}
6276
else
6377
{
64-
BrowserFixture.EnforceSupportedConfigurations();
78+
EnsureBrowserAvailable(browserKind);
6579
}
6680
}
6781

@@ -72,27 +86,31 @@ public async Task BlazorServerTemplateWorks_NoAuth()
7286
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process));
7387

7488
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
75-
if (BrowserFixture.IsHostAutomationSupported())
89+
if (Fixture.BrowserManager.IsAvailable(browserKind))
7690
{
77-
aspNetProcess.VisitInBrowser(Browser);
78-
TestBasicNavigation();
91+
var page = await browser.NewPageAsync();
92+
await aspNetProcess.VisitInBrowserAsync(page);
93+
await TestBasicNavigation(page);
94+
await page.CloseAsync();
7995
}
8096
else
8197
{
82-
BrowserFixture.EnforceSupportedConfigurations();
98+
EnsureBrowserAvailable(browserKind);
8399
}
84100
}
85101
}
86102

103+
public static IEnumerable<object[]> BlazorServerTemplateWorks_IndividualAuthData =>
104+
BrowserManager.WithBrowsers(new[] { BrowserKind.Chromium }, true, false);
105+
87106
[Theory]
88-
[InlineData(true)]
89-
[InlineData(false)]
90-
public async Task BlazorServerTemplateWorks_IndividualAuth(bool useLocalDB)
107+
[MemberData(nameof(BlazorServerTemplateWorks_IndividualAuthData))]
108+
public async Task BlazorServerTemplateWorks_IndividualAuth(BrowserKind browserKind, bool useLocalDB)
91109
{
92110
// Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278
93111
Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true");
94112

95-
Project = await ProjectFactory.GetOrCreateProject("blazorserverindividual" + (useLocalDB ? "uld" : ""), Output);
113+
Project = await ProjectFactory.GetOrCreateProject("blazorserverindividual" + browserKind + (useLocalDB ? "uld" : ""), Output);
96114

97115
var createResult = await Project.RunDotNetNewAsync("blazorserver", auth: "Individual", useLocalDB: useLocalDB);
98116
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
@@ -107,84 +125,86 @@ public async Task BlazorServerTemplateWorks_IndividualAuth(bool useLocalDB)
107125
var buildResult = await Project.RunDotNetBuildAsync();
108126
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult));
109127

128+
var browser = !Fixture.BrowserManager.IsAvailable(browserKind) ?
129+
null :
130+
await Fixture.BrowserManager.GetBrowserInstance(browserKind, BrowserContextInfo);
131+
110132
using (var aspNetProcess = Project.StartBuiltProjectAsync())
111133
{
112134
Assert.False(
113135
aspNetProcess.Process.HasExited,
114136
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", Project, aspNetProcess.Process));
115137

116138
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
117-
if (BrowserFixture.IsHostAutomationSupported())
139+
if (Fixture.BrowserManager.IsAvailable(browserKind))
118140
{
119-
aspNetProcess.VisitInBrowser(Browser);
120-
TestBasicNavigation();
141+
var page = await browser.NewPageAsync();
142+
await aspNetProcess.VisitInBrowserAsync(page);
143+
await TestBasicNavigation(page);
144+
await page.CloseAsync();
121145
}
122146
else
123147
{
124-
BrowserFixture.EnforceSupportedConfigurations();
148+
EnsureBrowserAvailable(browserKind);
125149
}
126150
}
127151

128-
129152
using (var aspNetProcess = Project.StartPublishedProjectAsync())
130153
{
131154
Assert.False(
132155
aspNetProcess.Process.HasExited,
133156
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process));
134157

135158
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
136-
if (BrowserFixture.IsHostAutomationSupported())
159+
if (Fixture.BrowserManager.IsAvailable(browserKind))
137160
{
138-
aspNetProcess.VisitInBrowser(Browser);
139-
TestBasicNavigation();
161+
var page = await browser.NewPageAsync();
162+
await aspNetProcess.VisitInBrowserAsync(page);
163+
await TestBasicNavigation(page);
164+
await page.CloseAsync();
165+
}
166+
else
167+
{
168+
EnsureBrowserAvailable(browserKind);
140169
}
141170
}
142171
}
143172

144-
private void TestBasicNavigation()
173+
private async Task TestBasicNavigation(IPage page)
145174
{
146-
var retries = 3;
147-
var connected = false;
148-
do
149-
{
150-
try
151-
{
152-
Browser.Contains("Information: WebSocket connected to",
153-
() => string.Join(Environment.NewLine, Browser.GetBrowserLogs(LogLevel.Info).Select(b => b.Message)));
154-
connected = true;
155-
}
156-
catch (TimeoutException) when(retries-- > 0)
157-
{
158-
Browser.Navigate().Refresh();
159-
}
160-
} while (!connected && retries > 0);
175+
var socket = BrowserContextInfo.Pages[page].WebSockets.SingleOrDefault() ??
176+
(await page.WaitForEventAsync(PageEvent.WebSocket)).WebSocket;
177+
178+
// Receive render batch
179+
await socket.WaitForEventAsync(WebSocketEvent.FrameReceived);
180+
await socket.WaitForEventAsync(WebSocketEvent.FrameSent);
161181

182+
// JS interop call to intercept navigation
183+
await socket.WaitForEventAsync(WebSocketEvent.FrameReceived);
184+
await socket.WaitForEventAsync(WebSocketEvent.FrameSent);
162185

163-
Browser.Exists(By.TagName("ul"));
186+
await page.WaitForSelectorAsync("ul");
164187
// <title> element gets project ID injected into it during template execution
165-
Browser.Equal(Project.ProjectName.Trim(), () => Browser.Title.Trim());
188+
Assert.Equal(Project.ProjectName.Trim(), (await page.GetTitleAsync()).Trim());
166189

167190
// Initially displays the home page
168-
Browser.Equal("Hello, world!", () => Browser.FindElement(By.TagName("h1")).Text);
191+
await page.WaitForSelectorAsync("h1 >> text=Hello, world!");
169192

170193
// Can navigate to the counter page
171-
Browser.Click(By.PartialLinkText("Counter"));
172-
Browser.Contains("counter", () => Browser.Url);
173-
Browser.Equal("Counter", () => Browser.FindElement(By.TagName("h1")).Text);
194+
await page.ClickAsync("a[href=counter] >> text=Counter");
195+
await page.WaitForSelectorAsync("h1+p >> text=Current count: 0");
174196

175197
// Clicking the counter button works
176-
Browser.Equal("Current count: 0", () => Browser.FindElement(By.CssSelector("h1 + p")).Text);
177-
Browser.Click(By.CssSelector("p+button"));
178-
Browser.Equal("Current count: 1", () => Browser.FindElement(By.CssSelector("h1 + p")).Text);
198+
await page.ClickAsync("p+button >> text=Click me");
199+
await page.WaitForSelectorAsync("h1+p >> text=Current count: 1");
179200

180201
// Can navigate to the 'fetch data' page
181-
Browser.Click(By.PartialLinkText("Fetch data"));
182-
Browser.Contains("fetchdata", () => Browser.Url);
183-
Browser.Equal("Weather forecast", () => Browser.FindElement(By.TagName("h1")).Text);
202+
await page.ClickAsync("a[href=fetchdata] >> text=Fetch data");
203+
await page.WaitForSelectorAsync("h1 >> text=Weather forecast");
184204

185205
// Asynchronously loads and displays the table of weather forecasts
186-
Browser.Exists(By.CssSelector("table>tbody>tr"));
187-
Browser.Equal(5, () => Browser.FindElements(By.CssSelector("p+table>tbody>tr")).Count);
206+
await page.WaitForSelectorAsync("table>tbody>tr");
207+
Assert.Equal(5, (await page.QuerySelectorAllAsync("p+table>tbody>tr")).Count());
188208
}
189209

190210
[Theory]

0 commit comments

Comments
 (0)