PWA template (#18878)

* Add service worker

* Add manifest

* Bring back BaselineTest.cs

* Add baselines for blazorwasm templates

* Add publishing test for PWA template

* Baseline fixes

* Fix baseline test logic to allow for multi-project outputs

* Remove non-blazorwasm baselines, since this branch now only covers blazorwasm

* Add test for PWA publish output

* Beginning generation of assets manifest

* Generate assets manifest including blazor outputs

* Tweaks

* Write assets manifest in JSON form

* Publish service worker

* Better API

* More resilience

* Better API again

* Make ComputeBlazorAssetsManifestItems public as people will need to customize the list

* Exclude service worker files from assets manifest

* Use web standard format for hash

* Update project template

* In assets manifest, only include items being published

* Renames

* Compute default assets manifest version by combining hashes

* Emit sw manifest in .js form

* Update service worker in project

* Actually isolate browser instances when requested during E2E tests

* E2E test for published PWA operating offline

* Fix SWAM path in template

* Clarify targets
This commit is contained in:
Steve Sanderson 2020-02-14 15:52:23 +00:00 committed by GitHub
parent 52f46367b0
commit 04b4602c2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 595 additions and 1248 deletions

View File

@ -0,0 +1,82 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Blazor.Build
{
public class GenerateServiceWorkerAssetsManifest : Task
{
[Required]
public string Version { get; set; }
[Required]
public ITaskItem[] AssetsWithHashes { get; set; }
[Required]
public string OutputPath { get; set; }
public override bool Execute()
{
using var fileStream = File.Create(OutputPath);
WriteFile(fileStream);
return true;
}
internal void WriteFile(Stream stream)
{
var data = new AssetsManifestFile
{
version = Version,
assets = AssetsWithHashes.Select(item => new AssetsManifestFileEntry
{
url = item.GetMetadata("AssetUrl"),
hash = $"sha256-{item.GetMetadata("FileHash")}",
}).ToArray()
};
using var streamWriter = new StreamWriter(stream, Encoding.UTF8, bufferSize: 50, leaveOpen: true);
streamWriter.Write("self.assetsManifest = ");
streamWriter.Flush();
using var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding.UTF8, ownsStream: false, indent: true);
new DataContractJsonSerializer(typeof(AssetsManifestFile)).WriteObject(jsonWriter, data);
jsonWriter.Flush();
streamWriter.WriteLine(";");
}
#pragma warning disable IDE1006 // Naming Styles
public class AssetsManifestFile
{
/// <summary>
/// Gets or sets a version string.
/// </summary>
public string version { get; set; }
/// <summary>
/// Gets or sets the assets. Keys are URLs; values are base-64-formatted SHA256 content hashes.
/// </summary>
public AssetsManifestFileEntry[] assets { get; set; }
}
public class AssetsManifestFileEntry
{
/// <summary>
/// Gets or sets the asset URL. Normally this will be relative to the application's base href.
/// </summary>
public string url { get; set; }
/// <summary>
/// Gets or sets the file content hash. This should be the base-64-formatted SHA256 value.
/// </summary>
public string hash { get; set; }
}
#pragma warning restore IDE1006 // Naming Styles
}
}

View File

@ -21,6 +21,7 @@
<Import Project="Blazor.MonoRuntime.targets" />
<Import Project="Publish.targets" />
<Import Project="StaticWebAssets.targets" />
<Import Project="ServiceWorkerAssetsManifest.targets" />
<Target Name="GenerateBlazorMetadataFile"
BeforeTargets="GetCopyToOutputDirectoryItems">

View File

@ -21,7 +21,7 @@
<Target
Name="_BlazorCopyFilesToOutputDirectory"
DependsOnTargets="PrepareBlazorOutputs"
DependsOnTargets="PrepareBlazorOutputs;$(_BlazorCopyFilesToOutputDirectoryDependsOn)"
AfterTargets="CopyFilesToOutputDirectory"
Condition="'$(OutputType.ToLowerInvariant())'=='exe'">
@ -133,7 +133,7 @@
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
-->
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" />
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" />
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" />
<BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">

View File

@ -0,0 +1,98 @@
<Project>
<PropertyGroup>
<_BlazorCopyFilesToOutputDirectoryDependsOn>
$(_BlazorCopyFilesToOutputDirectoryDependsOn);
_ComputeServiceWorkerAssetsManifestInputs;
_WriteServiceWorkerAssetsManifest;
</_BlazorCopyFilesToOutputDirectoryDependsOn>
</PropertyGroup>
<Target Name="_ComputeServiceWorkerAssetsManifestInputs"
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
DependsOnTargets="PrepareBlazorOutputs">
<PropertyGroup>
<_ServiceWorkerAssetsManifestIntermediateOutputPath>$(BlazorIntermediateOutputPath)serviceworkerassets.js</_ServiceWorkerAssetsManifestIntermediateOutputPath>
</PropertyGroup>
<ItemGroup>
<!-- Include _framework/* content -->
<ServiceWorkerAssetsManifestItem
Include="@(BlazorOutputWithTargetPath)"
Condition="$([System.String]::Copy('%(BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/').StartsWith('dist/'))">
<AssetUrl>$([System.String]::Copy('%(BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/').Substring(5))</AssetUrl>
</ServiceWorkerAssetsManifestItem>
<!-- Include content from wwwroot -->
<ServiceWorkerAssetsManifestItem
Include="@(ContentWithTargetPath)"
Condition="
('%(ContentWithTargetPath.CopyToPublishDirectory)' == 'Always' OR '%(ContentWithTargetPath.CopyToPublishDirectory)' == 'PreserveNewest')
AND $([System.String]::Copy('%(ContentWithTargetPath.TargetPath)').Replace('\','/').StartsWith('wwwroot/'))">
<AssetUrl>$([System.String]::Copy('%(ContentWithTargetPath.TargetPath)').Replace('\','/').Substring(8))</AssetUrl>
</ServiceWorkerAssetsManifestItem>
<!-- Include SWA from references -->
<ServiceWorkerAssetsManifestItem
Include="@(StaticWebAsset)"
Condition="'%(StaticWebAsset.SourceType)' != ''">
<AssetUrl>%(StaticWebAsset.BasePath)/%(StaticWebAsset.RelativePath)</AssetUrl>
</ServiceWorkerAssetsManifestItem>
</ItemGroup>
</Target>
<UsingTask TaskName="GenerateServiceWorkerAssetsManifest" AssemblyFile="$(BlazorTasksPath)" />
<Target Name="_WriteServiceWorkerAssetsManifest"
Inputs="@(ServiceWorkerAssetsManifestItem)"
Outputs="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
DependsOnTargets="_ComputeServiceWorkerAssetsManifestFileHashes; _ComputeDefaultServiceWorkerAssetsManifestVersion">
<GenerateServiceWorkerAssetsManifest
Version="$(ServiceWorkerAssetsManifestVersion)"
AssetsWithHashes="@(_ServiceWorkerAssetsManifestItemWithHash)"
OutputPath="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" />
<ItemGroup>
<BlazorOutputWithTargetPath
Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
TargetOutputPath="$(BaseBlazorDistPath)$(ServiceWorkerAssetsManifest)" />
<FileWrites Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" />
</ItemGroup>
</Target>
<Target Name="_ComputeServiceWorkerAssetsManifestFileHashes">
<GetFileHash Files="@(ServiceWorkerAssetsManifestItem)" Algorithm="SHA256" HashEncoding="base64">
<Output TaskParameter="Items" ItemName="_ServiceWorkerAssetsManifestItemWithHash" />
</GetFileHash>
</Target>
<!--
If no ServiceWorkerAssetsManifestVersion was specified, we compute a default value by combining all the asset hashes.
This is useful because then clients will only have to repopulate caches if the contents have changed.
-->
<Target Name="_ComputeDefaultServiceWorkerAssetsManifestVersion"
Condition="'$(ServiceWorkerAssetsManifestVersion)' == ''">
<PropertyGroup>
<_CombinedHashIntermediatePath>$(BlazorIntermediateOutputPath)serviceworkerhashes.txt</_CombinedHashIntermediatePath>
</PropertyGroup>
<WriteLinesToFile
File="$(_CombinedHashIntermediatePath)"
Lines="@(_ServiceWorkerAssetsManifestItemWithHash->'%(FileHash)')"
Overwrite="true" />
<GetFileHash Files="$(_CombinedHashIntermediatePath)" Algorithm="SHA256" HashEncoding="base64">
<Output TaskParameter="Items" ItemName="_ServiceWorkerAssetsManifestCombinedHash" />
</GetFileHash>
<PropertyGroup>
<ServiceWorkerAssetsManifestVersion>$([System.String]::Copy('%(_ServiceWorkerAssetsManifestCombinedHash.FileHash)').Substring(0, 8))</ServiceWorkerAssetsManifestVersion>
</PropertyGroup>
</Target>
</Project>

View File

@ -1,3 +1,3 @@
@ECHO OFF
%~dp0..\..\startvs.cmd %~dp0Components.sln
%~dp0..\..\startvs.cmd %~dp0Blazor.sln

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>
<ItemGroup>
@ -11,10 +12,22 @@
<PackageReference Include="Microsoft.AspNetCore.Blazor.DevServer" Version="${MicrosoftAspNetCoreBlazorDevServerPackageVersion}" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.HttpClient" Version="${MicrosoftAspNetCoreBlazorHttpClientPackageVersion}" />
</ItemGroup>
<!--#if Hosted -->
<ItemGroup>
<ProjectReference Include="..\Shared\BlazorWasm-CSharp.Shared.csproj" />
</ItemGroup>
<!--#endif -->
<!--#endif -->
<!--#if PWA -->
<ItemGroup>
<!-- When publishing, swap service-worker.published.js in place of service-worker.js -->
<Content Update="wwwroot\service-worker*.js" CopyToPublishDirectory="false" />
<ContentWithTargetPath Include="wwwroot\service-worker.published.js">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<TargetPath>wwwroot\service-worker.js</TargetPath>
</ContentWithTargetPath>
</ItemGroup>
<!--#endif -->
</Project>

View File

@ -5,9 +5,12 @@
"longName": "no-restore",
"shortName": ""
},
"Hosted": {
"longName": "hosted"
},
"Hosted": {
"longName": "hosted"
},
"PWA": {
"longName": "pwa"
},
"Framework": {
"longName": "framework"
}

View File

@ -74,6 +74,14 @@
"exclude": [
"*.sln"
]
},
{
"condition": "(!PWA)",
"exclude": [
"Client/wwwroot/service-worker*.js",
"Client/wwwroot/manifest.json",
"Client/wwwroot/icon-512.png"
]
}
]
}
@ -147,6 +155,12 @@
"fallbackVariableName": "HttpsPortGenerated"
},
"replaces": "44300"
},
"PWA": {
"type": "parameter",
"datatype": "bool",
"defaultValue": "false",
"description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use."
}
},
"tags": {

View File

@ -25,6 +25,13 @@
"text": "ASP.NET Core _hosted"
},
"isVisible": "true"
},
{
"id": "PWA",
"name": {
"text": "_Progressive Web Application"
},
"isVisible": "true"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -8,6 +8,9 @@
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/site.css" rel="stylesheet" />
<!--#if PWA -->
<link href="manifest.json" rel="manifest" />
<!--#endif -->
</head>
<body>
@ -19,6 +22,9 @@
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<!--#if PWA -->
<script>navigator.serviceWorker.register('service-worker.js');</script>
<!--#endif -->
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"name": "BlazorWasm-CSharp",
"short_name": "BlazorWasm-CSharp",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#03173d",
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}

View File

@ -0,0 +1,4 @@
// In development, always fetch from the network and do not enable offline support.
// This is because caching would make development more difficult (changes would not
// be reflected on the first load after each change).
self.addEventListener('fetch', () => { });

View File

@ -0,0 +1,48 @@
// Caution! Be sure you understand the caveats before publishing an application with
// offline support. See https://aka.ms/blazor-offline-considerations
self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
async function onInstall(event) {
console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
async function onActivate(event) {
console.info('Service worker: Activate');
// Delete unused caches
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
.map(key => caches.delete(key)));
}
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
const shouldServeIndexHtml = event.request.mode === 'navigate';
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
return cachedResponse || fetch(event.request);
}

View File

@ -0,0 +1,150 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
namespace Templates.Test
{
public class BaselineTest
{
private static readonly Regex TemplateNameRegex = new Regex(
"new (?<template>[a-zA-Z]+)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline,
TimeSpan.FromSeconds(1));
private static readonly Regex AuthenticationOptionRegex = new Regex(
"-au (?<auth>[a-zA-Z]+)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline,
TimeSpan.FromSeconds(1));
private static readonly Regex LanguageRegex = new Regex(
"--language (?<language>\\w+)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline,
TimeSpan.FromSeconds(1));
public BaselineTest(ProjectFactoryFixture projectFactory, ITestOutputHelper output)
{
ProjectFactory = projectFactory;
Output = output;
}
public Project Project { get; set; }
public static TheoryData<string, string[]> TemplateBaselines
{
get
{
using (var stream = typeof(BaselineTest).Assembly.GetManifestResourceStream("ProjectTemplates.Tests.template-baselines.json"))
{
using (var jsonReader = new JsonTextReader(new StreamReader(stream)))
{
var baseline = JObject.Load(jsonReader);
var data = new TheoryData<string, string[]>();
foreach (var template in baseline)
{
foreach (var authOption in (JObject)template.Value)
{
data.Add(
(string)authOption.Value["Arguments"],
((JArray)authOption.Value["Files"]).Select(s => (string)s).ToArray());
}
}
return data;
}
}
}
}
public ProjectFactoryFixture ProjectFactory { get; }
public ITestOutputHelper Output { get; }
[Theory]
[MemberData(nameof(TemplateBaselines))]
public async Task Template_Produces_The_Right_Set_Of_FilesAsync(string arguments, string[] expectedFiles)
{
Project = await ProjectFactory.GetOrCreateProject("baseline" + SanitizeArgs(arguments), Output);
var createResult = await Project.RunDotNetNewRawAsync(arguments);
Assert.True(createResult.ExitCode == 0, createResult.GetFormattedOutput());
foreach (var file in expectedFiles)
{
AssertFileExists(Project.TemplateOutputDir, file, shouldExist: true);
}
var filesInFolder = Directory.EnumerateFiles(Project.TemplateOutputDir, "*", SearchOption.AllDirectories);
foreach (var file in filesInFolder)
{
var relativePath = file.Replace(Project.TemplateOutputDir, "").Replace("\\", "/").Trim('/');
if (relativePath.EndsWith(".csproj", StringComparison.Ordinal) ||
relativePath.EndsWith(".fsproj", StringComparison.Ordinal) ||
relativePath.EndsWith(".props", StringComparison.Ordinal) ||
relativePath.EndsWith(".sln", StringComparison.Ordinal) ||
relativePath.EndsWith(".targets", StringComparison.Ordinal) ||
relativePath.StartsWith("bin/", StringComparison.Ordinal) ||
relativePath.StartsWith("obj/", StringComparison.Ordinal) ||
relativePath.Contains("/bin/", StringComparison.Ordinal) ||
relativePath.Contains("/obj/", StringComparison.Ordinal))
{
continue;
}
Assert.Contains(relativePath, expectedFiles);
}
}
private string SanitizeArgs(string arguments)
{
var text = TemplateNameRegex.Match(arguments)
.Groups.TryGetValue("template", out var template) ? template.Value : "";
text += AuthenticationOptionRegex.Match(arguments)
.Groups.TryGetValue("auth", out var auth) ? auth.Value : "";
text += arguments.Contains("--uld") ? "uld" : "";
text += LanguageRegex.Match(arguments)
.Groups.TryGetValue("language", out var language) ? language.Value.Replace("#", "Sharp") : "";
if (arguments.Contains("--support-pages-and-views true"))
{
text += "supportpagesandviewstrue";
}
if (arguments.Contains("-ho"))
{
text += "hosted";
}
if (arguments.Contains("--pwa"))
{
text += "pwa";
}
return text;
}
private void AssertFileExists(string basePath, string path, bool shouldExist)
{
var fullPath = Path.Combine(basePath, path);
var doesExist = File.Exists(fullPath);
if (shouldExist)
{
Assert.True(doesExist, "Expected file to exist, but it doesn't: " + path);
}
else
{
Assert.False(doesExist, "Expected file not to exist, but it does: " + path);
}
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net;
using System.Threading;
@ -24,6 +25,11 @@ namespace Templates.Test
public ProjectFactoryFixture ProjectFactory { get; set; }
public override Task InitializeAsync()
{
return InitializeAsync(isolationContext: Guid.NewGuid().ToString());
}
[Fact]
public async Task BlazorWasmStandaloneTemplate_Works()
{
@ -90,6 +96,51 @@ namespace Templates.Test
}
}
[Fact]
public async Task BlazorWasmPwaTemplate_Works()
{
var project = await ProjectFactory.GetOrCreateProject("blazorpwa", Output);
project.TargetFramework = "netstandard2.1";
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--pwa" });
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult));
var publishResult = await project.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", project, publishResult));
var buildResult = await project.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", project, buildResult));
await BuildAndRunTest(project.ProjectName, project);
var publishDir = Path.Combine(project.TemplatePublishDir, project.ProjectName, "dist");
AspNetProcess.EnsureDevelopmentCertificates();
// When publishing the PWA template, we generate an assets manifest
// and move service-worker.published.js to overwrite service-worker.js
Assert.False(File.Exists(Path.Combine(publishDir, "service-worker.published.js")), "service-worker.published.js should not be published");
Assert.True(File.Exists(Path.Combine(publishDir, "service-worker.js")), "service-worker.js should be published");
Assert.True(File.Exists(Path.Combine(publishDir, "service-worker-assets.js")), "service-worker-assets.js should be published");
// Todo: Use dynamic port assignment: https://github.com/natemcmaster/dotnet-serve/pull/40/files
var listeningUri = "https://localhost:8080";
Output.WriteLine("Running dotnet serve on published output...");
using (var serveProcess = ProcessEx.Run(Output, publishDir, DotNetMuxer.MuxerPathOrDefault(), "serve -S"))
{
Output.WriteLine($"Opening browser at {listeningUri}...");
Browser.Navigate().GoToUrl(listeningUri);
TestBasicNavigation(project.ProjectName);
}
// The PWA template supports offline use. By now, the browser should have cached everything it needs,
// so we can continue working even without the server.
Browser.Navigate().GoToUrl("about:blank"); // Be sure we're really reloading
Output.WriteLine($"Opening browser without corresponding server at {listeningUri}...");
Browser.Navigate().GoToUrl(listeningUri);
TestBasicNavigation(project.ProjectName);
}
protected async Task BuildAndRunTest(string appName, Project project)
{
using var aspNetProcess = project.StartBuiltProjectAsync();

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
@ -68,6 +69,15 @@ namespace Microsoft.AspNetCore.E2ETesting
{
browser.Dispose();
}
foreach (var context in _browsers.Keys)
{
var userProfileDirectory = UserProfileDirectory(context);
if (!string.IsNullOrEmpty(userProfileDirectory) && Directory.Exists(userProfileDirectory))
{
Directory.Delete(userProfileDirectory, recursive: true);
}
}
}
public Task<(IWebDriver, ILogs)> GetOrCreateBrowserAsync(ITestOutputHelper output, string isolationContext = "")
@ -116,6 +126,13 @@ namespace Microsoft.AspNetCore.E2ETesting
output.WriteLine($"Set {nameof(ChromeOptions)}.{nameof(opts.BinaryLocation)} to {binaryLocation}");
}
var userProfileDirectory = UserProfileDirectory(context);
if (!string.IsNullOrEmpty(userProfileDirectory))
{
Directory.CreateDirectory(userProfileDirectory);
opts.AddArgument($"--user-data-dir={userProfileDirectory}");
}
var instance = await SeleniumStandaloneServer.GetInstanceAsync(output);
var attempt = 0;
@ -155,6 +172,16 @@ namespace Microsoft.AspNetCore.E2ETesting
throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive");
}
private string UserProfileDirectory(string context)
{
if (string.IsNullOrEmpty(context))
{
return null;
}
return Path.Combine(Path.GetTempPath(), "BrowserFixtureUserProfiles", context);
}
private async Task<(IWebDriver browser, ILogs log)> CreateSauceBrowserAsync(string context, ITestOutputHelper output)
{
var sauce = E2ETestOptions.Instance.Sauce;