chrome-devtools-frontend
Version:
Chrome DevTools UI
304 lines (277 loc) • 12.6 kB
text/typescript
// Copyright 2023 The Chromium Authors. All rights reserved.
// 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,
renderElementIntoDOM,
} from '../../../testing/DOMHelpers.js';
import {createTarget} from '../../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../../testing/MockConnection.js';
import {getMainFrame, navigate} from '../../../testing/ResourceTreeHelpers.js';
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
import * as TreeOutline from '../../../ui/components/tree_outline/tree_outline.js';
import * as ApplicationComponents from './components.js';
interface NodeData {
text: string;
iconName?: string;
}
interface Node {
treeNodeData: NodeData;
children?: Node[];
}
async function renderBackForwardCacheView(): Promise<ApplicationComponents.BackForwardCacheView.BackForwardCacheView> {
const component = new ApplicationComponents.BackForwardCacheView.BackForwardCacheView();
renderElementIntoDOM(component);
await component.render();
assert.isNotNull(component.shadowRoot);
await RenderCoordinator.done();
return component;
}
async function unpromisify(node: TreeOutline.TreeOutlineUtils.TreeNode<NodeData>): Promise<Node> {
const result: Node = {treeNodeData: node.treeNodeData};
if (node.children) {
const children = await node.children();
result.children = await Promise.all(children.map(child => unpromisify(child)));
}
return result;
}
describeWithMockConnection('BackForwardCacheView', () => {
let target: SDK.Target.Target;
let resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel;
beforeEach(async () => {
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);
await RenderCoordinator.done({waitForWork: true});
});
it('updates BFCacheView on BFCache detail update', async () => {
await renderBackForwardCacheView();
resourceTreeModel.dispatchEventToListeners(
SDK.ResourceTreeModel.Events.BackForwardCacheDetailsUpdated, getMainFrame(target));
await RenderCoordinator.done({waitForWork: true});
});
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.shadowRoot!.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.shadowRoot!.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.shadowRoot!.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 component = await renderBackForwardCacheView();
const treeOutline = component.shadowRoot!.querySelector('devtools-tree-outline');
assert.instanceOf(treeOutline, TreeOutline.TreeOutline.TreeOutline);
assert.isNotNull(treeOutline.shadowRoot);
const treeData = await Promise.all(
treeOutline.data.tree.map(node => unpromisify(node as TreeOutline.TreeOutlineUtils.TreeNode<NodeData>)));
const expected = [
{
treeNodeData: {
text: '2 issues found in 2 frames.',
},
children: [
{
treeNodeData: {
text: '(2) https://www.example.com',
iconName: 'frame',
},
children: [
{
treeNodeData: {
text: 'WebLocks',
},
},
{
treeNodeData: {
text: '(1) https://www.example.com/frame.html',
iconName: 'iframe',
},
children: [
{
treeNodeData: {
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.shadowRoot!.querySelectorAll('devtools-report-section-header');
const sectionHeadersText = Array.from(sectionHeaders).map(sectionHeader => sectionHeader.textContent?.trim());
assert.deepEqual(sectionHeadersText, ['Pending Support']);
const sections = component.shadowRoot!.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.shadowRoot!.querySelector('.details-list devtools-expandable-list');
details!.shadowRoot!.querySelector('button')!.click();
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.shadowRoot!.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]);
});
});