// 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.Collections.Generic; using System.Linq; using System.Threading.Tasks; using HostedInAspNet.Server; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.Tests { public class BootResourceCachingTest : ServerTestBase { // The cache name is derived from the application's base href value (in this case, '/') private const string CacheName = "blazor-resources-/"; public BootResourceCachingTest( BrowserFixture browserFixture, AspNetSiteServerFixture serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { serverFixture.BuildWebHostMethod = Program.BuildWebHost; } public override Task InitializeAsync() { return base.InitializeAsync(Guid.NewGuid().ToString()); } [Fact] [QuarantinedTest] public void CachesResourcesAfterFirstLoad() { // On the first load, we have to fetch everything Navigate("/"); WaitUntilLoaded(); var initialResourcesRequested = GetAndClearRequestedPaths(); Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith("/blazor.boot.json"))); Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith("/dotnet.wasm"))); Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith(".js"))); Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith(".dll"))); // On subsequent loads, we skip the items referenced from blazor.boot.json // which includes .dll files and dotnet.wasm Navigate("about:blank"); Navigate("/"); WaitUntilLoaded(); var subsequentResourcesRequested = GetAndClearRequestedPaths(); Assert.NotEmpty(initialResourcesRequested.Where(path => path.EndsWith("/blazor.boot.json"))); Assert.Empty(subsequentResourcesRequested.Where(path => path.EndsWith("/dotnet.wasm"))); Assert.NotEmpty(subsequentResourcesRequested.Where(path => path.EndsWith(".js"))); Assert.Empty(subsequentResourcesRequested.Where(path => path.EndsWith(".dll"))); } [Fact] [QuarantinedTest] public void IncrementallyUpdatesCache() { // Perform a first load to populate the cache Navigate("/"); WaitUntilLoaded(); var cacheEntryUrls1 = GetCacheEntryUrls(); var cacheEntryForComponentsDll = cacheEntryUrls1.Single(url => url.Contains("/Microsoft.AspNetCore.Components.dll")); var cacheEntryForDotNetWasm = cacheEntryUrls1.Single(url => url.Contains("/dotnet.wasm")); var cacheEntryForDotNetWasmWithChangedHash = cacheEntryForDotNetWasm.Replace(".sha256-", ".sha256-different"); // Remove some items we do need, and add an item we don't need RemoveCacheEntry(cacheEntryForComponentsDll); RemoveCacheEntry(cacheEntryForDotNetWasm); AddCacheEntry(cacheEntryForDotNetWasmWithChangedHash, "ignored content"); var cacheEntryUrls2 = GetCacheEntryUrls(); Assert.DoesNotContain(cacheEntryForComponentsDll, cacheEntryUrls2); Assert.DoesNotContain(cacheEntryForDotNetWasm, cacheEntryUrls2); Assert.Contains(cacheEntryForDotNetWasmWithChangedHash, cacheEntryUrls2); // On the next load, we'll fetch only the items we need (not things already cached) GetAndClearRequestedPaths(); Navigate("about:blank"); Navigate("/"); WaitUntilLoaded(); var subsequentResourcesRequested = GetAndClearRequestedPaths(); Assert.Collection(subsequentResourcesRequested.Where(url => url.Contains(".dll")), requestedDll => Assert.Contains("/Microsoft.AspNetCore.Components.dll", requestedDll)); Assert.Collection(subsequentResourcesRequested.Where(url => url.Contains(".wasm")), requestedDll => Assert.Contains("/dotnet.wasm", requestedDll)); // We also update the cache (add new items, remove unnecessary items) var cacheEntryUrls3 = GetCacheEntryUrls(); Assert.Contains(cacheEntryForComponentsDll, cacheEntryUrls3); Assert.Contains(cacheEntryForDotNetWasm, cacheEntryUrls3); Assert.DoesNotContain(cacheEntryForDotNetWasmWithChangedHash, cacheEntryUrls3); } private IReadOnlyCollection GetCacheEntryUrls() { var js = @" (async function(cacheName, completedCallback) { const cache = await caches.open(cacheName); const keys = await cache.keys(); const urls = keys.map(r => r.url); completedCallback(urls); }).apply(null, arguments)"; var jsExecutor = (IJavaScriptExecutor)Browser; var result = (IEnumerable)jsExecutor.ExecuteAsyncScript(js, CacheName); return result.Cast().ToList(); } private void RemoveCacheEntry(string url) { var js = @" (async function(cacheName, urlToRemove, completedCallback) { const cache = await caches.open(cacheName); await cache.delete(urlToRemove); completedCallback(); }).apply(null, arguments)"; ((IJavaScriptExecutor)Browser).ExecuteAsyncScript(js, CacheName, url); } private void AddCacheEntry(string url, string content) { var js = @" (async function(cacheName, urlToAdd, contentToAdd, completedCallback) { const cache = await caches.open(cacheName); await cache.put(urlToAdd, new Response(contentToAdd)); completedCallback(); }).apply(null, arguments)"; ((IJavaScriptExecutor)Browser).ExecuteAsyncScript(js, CacheName, url, content); } private IReadOnlyCollection GetAndClearRequestedPaths() { var requestLog = _serverFixture.Host.Services.GetRequiredService(); var result = requestLog.RequestPaths.ToList(); requestLog.Clear(); return result; } private void WaitUntilLoaded() { new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until( driver => driver.FindElement(By.TagName("h1")).Text == "Hello, world!"); } } }