[go: nahoru, domu]

blob: 46b00a0581766ca086475527ccbc98326da4bc6e [file] [log] [blame]
// Copyright 2022 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 {assertNotNullOrUndefined} from '../../../../../../front_end/core/platform/platform.js';
import * as Root from '../../../../../../front_end/core/root/root.js';
import * as SDK from '../../../../../../front_end/core/sdk/sdk.js';
import * as Protocol from '../../../../../../front_end/generated/protocol.js';
import * as ApplicationComponents from '../../../../../../front_end/panels/application/components/components.js';
import * as Coordinator from '../../../../../../front_end/ui/components/render_coordinator/render_coordinator.js';
import * as TreeOutline from '../../../../../../front_end/ui/components/tree_outline/tree_outline.js';
import {
assertElement,
assertShadowRoot,
dispatchClickEvent,
renderElementIntoDOM,
} from '../../../helpers/DOMHelpers.js';
import {createTarget, describeWithEnvironment} from '../../../helpers/EnvironmentHelpers.js';
import {describeWithMockConnection, dispatchEvent} from '../../../helpers/MockConnection.js';
import type * as Platform from '../../../../../../front_end/core/platform/platform.js';
const coordinator = Coordinator.RenderCoordinator.RenderCoordinator.instance();
const {assert} = chai;
interface NodeData {
text: string;
iconName?: string;
}
interface Node {
treeNodeData: NodeData;
children?: Node[];
}
async function renderBackForwardCacheView(frame: SDK.ResourceTreeModel.ResourceTreeFrame):
Promise<ApplicationComponents.BackForwardCacheView.BackForwardCacheView> {
const component = new ApplicationComponents.BackForwardCacheView.BackForwardCacheView();
renderElementIntoDOM(component);
component.data = {frame};
assertShadowRoot(component.shadowRoot);
await coordinator.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('BackForwardCacheViewWrapper', () => {
const updatesBFCacheView = (targetFactory: () => SDK.Target.Target) => {
let target: SDK.Target.Target;
let resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel|null;
let wrapper: ApplicationComponents.BackForwardCacheView.BackForwardCacheViewWrapper;
let view: ApplicationComponents.BackForwardCacheView.BackForwardCacheView;
const FRAME = {
id: 'main',
loaderId: 'test',
url: 'http://example.com',
securityOrigin: 'http://example.com',
mimeType: 'text/html',
};
beforeEach(() => {
target = targetFactory();
resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assertNotNullOrUndefined(resourceTreeModel);
dispatchEvent(target, 'Page.frameNavigated', {frame: FRAME});
view = new ApplicationComponents.BackForwardCacheView.BackForwardCacheView();
wrapper = new ApplicationComponents.BackForwardCacheView.BackForwardCacheViewWrapper(view);
wrapper.markAsRoot();
wrapper.show(document.body);
});
afterEach(() => {
wrapper.detach();
});
it('updates BFCacheView on main frame navigation', async () => {
assertNotNullOrUndefined(resourceTreeModel);
assertNotNullOrUndefined(resourceTreeModel.mainFrame);
resourceTreeModel.dispatchEventToListeners(
SDK.ResourceTreeModel.Events.PrimaryPageChanged,
{frame: resourceTreeModel.mainFrame, type: SDK.ResourceTreeModel.PrimaryPageChangeType.Navigation});
const data = await new Promise(resolve => sinon.stub(view, 'data').set(resolve));
assert.deepStrictEqual(data, {frame: resourceTreeModel.mainFrame});
});
it('updates BFCacheView on BFCache detail update', async () => {
assertNotNullOrUndefined(resourceTreeModel);
assertNotNullOrUndefined(resourceTreeModel.mainFrame);
resourceTreeModel.dispatchEventToListeners(
SDK.ResourceTreeModel.Events.BackForwardCacheDetailsUpdated, resourceTreeModel.mainFrame);
const data = await new Promise(resolve => sinon.stub(view, 'data').set(resolve));
assert.deepStrictEqual(data, {frame: resourceTreeModel.mainFrame});
});
};
describe('without tab target', () => updatesBFCacheView(() => createTarget()));
describe('with tab target', () => updatesBFCacheView(() => {
const tabTarget = createTarget({type: SDK.Target.Type.Tab});
createTarget({parentTarget: tabTarget, subtype: 'prerender'});
return createTarget({parentTarget: tabTarget});
}));
});
describeWithEnvironment('BackForwardCacheView', () => {
it('renders status if restored from BFCache', async () => {
const frame = {
url: 'https://www.example.com/',
backForwardCacheDetails: {
restoredFromCache: true,
explanations: [],
},
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame;
const component = await renderBackForwardCacheView(frame);
assertShadowRoot(component.shadowRoot);
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 () => {
const frame = {
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(frame);
assertShadowRoot(component.shadowRoot);
const sectionHeaders = component.shadowRoot.querySelectorAll('devtools-report-section-header');
const sectionHeadersText = Array.from(sectionHeaders).map(sectionHeader => sectionHeader.textContent?.trim());
assert.deepStrictEqual(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.deepStrictEqual(sectionsText, expected);
});
it('renders explanation tree', async () => {
Root.Runtime.experiments.enableForTest('bfcacheDisplayTree');
const frame = {
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(frame);
assertShadowRoot(component.shadowRoot);
const treeOutline = component.shadowRoot.querySelector('devtools-tree-outline');
assertElement(treeOutline, TreeOutline.TreeOutline.TreeOutline);
assertShadowRoot(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.deepStrictEqual(treeData, expected);
});
});
const testBFCacheWorkflow = (targetFactory: () => SDK.Target.Target) => {
it('can handle delayed navigation history when testing for BFcache availability', async () => {
const mainTarget = targetFactory();
const resourceTreeModel = mainTarget?.model(SDK.ResourceTreeModel.ResourceTreeModel);
assertNotNullOrUndefined(resourceTreeModel);
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): Promise<void> => {
resourceTreeModel.frameNavigated({url} as unknown as Protocol.Page.Frame, undefined);
return Promise.resolve();
};
resourceTreeModel.navigateToHistoryEntry = (entry: Protocol.Page.NavigationEntry): void => {
resourceTreeModel.frameNavigated({url: entry.url} as unknown as Protocol.Page.Frame, undefined);
};
const navigateToHistoryEntrySpy = sinon.spy(resourceTreeModel, 'navigateToHistoryEntry');
resourceTreeModel.storageKeyForFrame = () => Promise.resolve(null);
const frame = {
url: 'about:blank',
backForwardCacheDetails: {
restoredFromCache: true,
explanations: [],
},
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame;
const component = await renderBackForwardCacheView(frame);
assertShadowRoot(component.shadowRoot);
const button = component.shadowRoot.querySelector('[aria-label="Test back/forward cache"]');
assertElement(button, HTMLElement);
dispatchClickEvent(button);
await new Promise<void>(resolve => {
let eventCounter = 0;
resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.FrameNavigated, () => {
if (++eventCounter === 2) {
resolve();
}
});
});
assert.isTrue(navigateToHistoryEntrySpy.calledOnceWithExactly(entries[0]));
});
};
describeWithMockConnection('BackForwardCacheView', () => {
describe('test BFCache workflow without tab taget', () => testBFCacheWorkflow(() => createTarget()));
describe('test BFCache workflow with tab taget', () => testBFCacheWorkflow(() => {
const tabTarget = createTarget({type: SDK.Target.Type.Tab});
createTarget({parentTarget: tabTarget, subtype: 'prerender'});
return createTarget({parentTarget: tabTarget});
}));
});