| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type * as Platform from '../../../core/platform/platform.js'; |
| import * as SDK from '../../../core/sdk/sdk.js'; |
| import * as Protocol from '../../../generated/protocol.js'; |
| import { |
| dispatchClickEvent, |
| raf, |
| renderElementIntoDOM, |
| } from '../../../testing/DOMHelpers.js'; |
| import {createTarget} from '../../../testing/EnvironmentHelpers.js'; |
| import {describeWithMockConnection} from '../../../testing/MockConnection.js'; |
| import {getMainFrame, navigate, setMockResourceTree} from '../../../testing/ResourceTreeHelpers.js'; |
| import {createViewFunctionStub} from '../../../testing/ViewFunctionHelpers.js'; |
| |
| import * as ApplicationComponents from './components.js'; |
| |
| async function renderBackForwardCacheView(): Promise<ApplicationComponents.BackForwardCacheView.BackForwardCacheView> { |
| const component = new ApplicationComponents.BackForwardCacheView.BackForwardCacheView(); |
| renderElementIntoDOM(component); |
| component.requestUpdate(); |
| await component.updateComplete; |
| return component; |
| } |
| |
| describeWithMockConnection('BackForwardCacheView', () => { |
| let target: SDK.Target.Target; |
| let resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel; |
| |
| beforeEach(async () => { |
| setMockResourceTree(false); |
| const tabTarget = createTarget({type: SDK.Target.Type.TAB}); |
| createTarget({parentTarget: tabTarget, subtype: 'prerender'}); |
| target = createTarget({parentTarget: tabTarget}); |
| resourceTreeModel = |
| target.model(SDK.ResourceTreeModel.ResourceTreeModel) as SDK.ResourceTreeModel.ResourceTreeModel; |
| }); |
| |
| it('updates BFCacheView on main frame navigation', async () => { |
| await renderBackForwardCacheView(); |
| navigate(getMainFrame(target), {}, Protocol.Page.NavigationType.BackForwardCacheRestore); |
| }); |
| |
| it('updates BFCacheView on BFCache detail update', async () => { |
| await renderBackForwardCacheView(); |
| resourceTreeModel.dispatchEventToListeners( |
| SDK.ResourceTreeModel.Events.BackForwardCacheDetailsUpdated, getMainFrame(target)); |
| }); |
| |
| it('renders status if restored from BFCache', async () => { |
| resourceTreeModel.mainFrame = { |
| url: 'https://www.example.com/', |
| backForwardCacheDetails: { |
| restoredFromCache: true, |
| explanations: [], |
| }, |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame; |
| const component = await renderBackForwardCacheView(); |
| const renderedStatus = component.contentElement.querySelector('devtools-report-section'); |
| assert.strictEqual(renderedStatus?.textContent?.trim(), 'Successfully served from back/forward cache.'); |
| }); |
| |
| it('renders explanations if not restorable from BFCache', async () => { |
| resourceTreeModel.mainFrame = { |
| url: 'https://www.example.com/', |
| backForwardCacheDetails: { |
| restoredFromCache: false, |
| explanations: [ |
| { |
| type: Protocol.Page.BackForwardCacheNotRestoredReasonType.SupportPending, |
| reason: Protocol.Page.BackForwardCacheNotRestoredReason.WebLocks, |
| }, |
| { |
| type: Protocol.Page.BackForwardCacheNotRestoredReasonType.PageSupportNeeded, |
| reason: Protocol.Page.BackForwardCacheNotRestoredReason.ServiceWorkerUnregistration, |
| }, |
| { |
| type: Protocol.Page.BackForwardCacheNotRestoredReasonType.Circumstantial, |
| reason: Protocol.Page.BackForwardCacheNotRestoredReason.MainResourceHasCacheControlNoStore, |
| }, |
| ], |
| }, |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame; |
| const component = await renderBackForwardCacheView(); |
| const sectionHeaders = component.contentElement.querySelectorAll('devtools-report-section-header'); |
| const sectionHeadersText = Array.from(sectionHeaders).map(sectionHeader => sectionHeader.textContent?.trim()); |
| assert.deepEqual(sectionHeadersText, ['Actionable', 'Pending Support', 'Not Actionable']); |
| |
| const sections = component.contentElement.querySelectorAll('devtools-report-section'); |
| const sectionsText = Array.from(sections).map(section => section.textContent?.trim()); |
| const expected = [ |
| 'Not served from back/forward cache: to trigger back/forward cache, use Chrome\'s back/forward buttons, or use the test button below to automatically navigate away and back.', |
| 'Test back/forward cache', |
| 'ServiceWorker was unregistered while a page was in back/forward cache.', |
| 'Pages that use WebLocks are not currently eligible for back/forward cache.', |
| 'Pages whose main resource has cache-control:no-store cannot enter back/forward cache.', |
| 'Learn more: back/forward cache eligibility', |
| ]; |
| assert.deepEqual(sectionsText, expected); |
| }); |
| |
| it('renders explanation tree', async () => { |
| resourceTreeModel.mainFrame = { |
| url: 'https://www.example.com/', |
| backForwardCacheDetails: { |
| restoredFromCache: false, |
| explanationsTree: { |
| url: 'https://www.example.com', |
| explanations: [{ |
| type: Protocol.Page.BackForwardCacheNotRestoredReasonType.SupportPending, |
| reason: Protocol.Page.BackForwardCacheNotRestoredReason.WebLocks, |
| }], |
| children: [{ |
| url: 'https://www.example.com/frame.html', |
| explanations: [{ |
| type: Protocol.Page.BackForwardCacheNotRestoredReasonType.Circumstantial, |
| reason: Protocol.Page.BackForwardCacheNotRestoredReason.MainResourceHasCacheControlNoStore, |
| }], |
| children: [], |
| }], |
| }, |
| explanations: [ |
| { |
| type: Protocol.Page.BackForwardCacheNotRestoredReasonType.SupportPending, |
| reason: Protocol.Page.BackForwardCacheNotRestoredReason.WebLocks, |
| }, |
| { |
| type: Protocol.Page.BackForwardCacheNotRestoredReasonType.Circumstantial, |
| reason: Protocol.Page.BackForwardCacheNotRestoredReason.MainResourceHasCacheControlNoStore, |
| }, |
| ], |
| }, |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame; |
| const view = createViewFunctionStub(ApplicationComponents.BackForwardCacheView.BackForwardCacheView); |
| new ApplicationComponents.BackForwardCacheView.BackForwardCacheView(view); |
| |
| const treeData = (await view.nextInput).frameTreeData; |
| |
| const expected = { |
| frameCount: 2, |
| issueCount: 2, |
| node: { |
| text: '(2) https://www.example.com', |
| iconName: 'frame', |
| children: [ |
| { |
| text: 'WebLocks', |
| }, |
| { |
| text: '(1) https://www.example.com/frame.html', |
| iconName: 'iframe', |
| children: [ |
| { |
| text: 'MainResourceHasCacheControlNoStore', |
| }, |
| ], |
| }, |
| ], |
| }, |
| }; |
| |
| assert.deepEqual(treeData, expected); |
| }); |
| |
| it('renders blocking details if available', async () => { |
| resourceTreeModel.mainFrame = { |
| resourceForURL: () => null, |
| url: 'https://www.example.com/', |
| backForwardCacheDetails: { |
| restoredFromCache: false, |
| explanations: [ |
| { |
| type: Protocol.Page.BackForwardCacheNotRestoredReasonType.SupportPending, |
| reason: Protocol.Page.BackForwardCacheNotRestoredReason.WebLocks, |
| details: [ |
| {url: 'https://www.example.com/index.html', lineNumber: 10, columnNumber: 5}, |
| {url: 'https://www.example.com/script.js', lineNumber: 15, columnNumber: 20}, |
| ], |
| }, |
| ], |
| }, |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame; |
| |
| const component = await renderBackForwardCacheView(); |
| const sectionHeaders = component.contentElement.querySelectorAll('devtools-report-section-header'); |
| const sectionHeadersText = Array.from(sectionHeaders).map(sectionHeader => sectionHeader.textContent?.trim()); |
| assert.deepEqual(sectionHeadersText, ['Pending Support']); |
| |
| const sections = component.contentElement.querySelectorAll('devtools-report-section'); |
| const sectionsText = Array.from(sections).map(section => section.textContent?.trim()); |
| const expected = [ |
| 'Not served from back/forward cache: to trigger back/forward cache, use Chrome\'s back/forward buttons, or use the test button below to automatically navigate away and back.', |
| 'Test back/forward cache', |
| 'Pages that use WebLocks are not currently eligible for back/forward cache.', |
| 'Learn more: back/forward cache eligibility', |
| ]; |
| assert.deepEqual(sectionsText, expected); |
| |
| const details = component.contentElement.querySelector('.details-list devtools-expandable-list'); |
| details!.shadowRoot!.querySelector('button')!.click(); |
| await raf(); |
| const items = details!.shadowRoot!.querySelectorAll('.expandable-list-items .devtools-link'); |
| const detailsText = Array.from(items).map(detail => detail.textContent?.trim()); |
| assert.deepEqual(detailsText, ['www.example.com/index.html:11:6', 'www.example.com/script.js:16:21']); |
| }); |
| |
| it('can handle delayed navigation history when testing for BFcache availability', async () => { |
| const entries = [ |
| { |
| id: 5, |
| url: 'about:blank', |
| userTypedURL: 'about:blank', |
| title: '', |
| transitionType: Protocol.Page.TransitionType.Typed, |
| }, |
| { |
| id: 8, |
| url: 'chrome://terms/', |
| userTypedURL: '', |
| title: '', |
| transitionType: Protocol.Page.TransitionType.Typed, |
| }, |
| ]; |
| const stub = sinon.stub(); |
| stub.onCall(0).returns({entries, currentIndex: 0}); |
| stub.onCall(1).returns({entries, currentIndex: 0}); |
| stub.onCall(2).returns({entries, currentIndex: 0}); |
| stub.onCall(3).returns({entries, currentIndex: 0}); |
| stub.onCall(4).returns({entries, currentIndex: 1}); |
| resourceTreeModel.navigationHistory = stub; |
| |
| resourceTreeModel.navigate = (url: Platform.DevToolsPath.UrlString) => { |
| resourceTreeModel.frameNavigated({url} as unknown as Protocol.Page.Frame, undefined); |
| return Promise.resolve({frameId: '' as Protocol.Page.FrameId, getError(): undefined {}}); |
| }; |
| resourceTreeModel.navigateToHistoryEntry = (entry: Protocol.Page.NavigationEntry) => { |
| resourceTreeModel.frameNavigated({url: entry.url} as unknown as Protocol.Page.Frame, undefined); |
| }; |
| const navigateToHistoryEntrySpy = sinon.spy(resourceTreeModel, 'navigateToHistoryEntry'); |
| resourceTreeModel.storageKeyForFrame = () => Promise.resolve(null); |
| |
| resourceTreeModel.mainFrame = { |
| url: 'about:blank', |
| backForwardCacheDetails: { |
| restoredFromCache: true, |
| explanations: [], |
| }, |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame; |
| const component = await renderBackForwardCacheView(); |
| const button = component.contentElement.querySelector('[aria-label="Test back/forward cache"]'); |
| assert.instanceOf(button, HTMLElement); |
| dispatchClickEvent(button); |
| |
| await new Promise<void>(resolve => { |
| let eventCounter = 0; |
| resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.FrameNavigated, () => { |
| if (++eventCounter === 2) { |
| resolve(); |
| } |
| }); |
| }); |
| sinon.assert.calledOnceWithExactly(navigateToHistoryEntrySpy, entries[0]); |
| }); |
| }); |