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:
parent
52f46367b0
commit
04b4602c2b
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
@ECHO OFF
|
||||
|
||||
%~dp0..\..\startvs.cmd %~dp0Components.sln
|
||||
%~dp0..\..\startvs.cmd %~dp0Blazor.sln
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@
|
|||
"longName": "no-restore",
|
||||
"shortName": ""
|
||||
},
|
||||
"Hosted": {
|
||||
"longName": "hosted"
|
||||
},
|
||||
"Hosted": {
|
||||
"longName": "hosted"
|
||||
},
|
||||
"PWA": {
|
||||
"longName": "pwa"
|
||||
},
|
||||
"Framework": {
|
||||
"longName": "framework"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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', () => { });
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue