blob: c762e8822a23ca1f4ddd5fc535a588ee333e27e0 [file]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import sinon from 'sinon';
import * as ProtocolModule from '../../generated/protocol.js';
import type * as Protocol from '../../generated/protocol.js';
import {setupLocaleHooks} from '../../testing/LocaleHelpers.js';
import {setupRuntimeHooks} from '../../testing/RuntimeHelpers.js';
import {setupSettingsHooks} from '../../testing/SettingsHelpers.js';
import {TestUniverse} from '../../testing/TestUniverse.js';
import * as Platform from '../platform/platform.js';
import * as SDK from './sdk.js';
const {urlString} = Platform.DevToolsPath;
const {NodeType, cssEscape} = SDK.DOMModel;
describe('DOMModel', () => {
setupLocaleHooks();
setupSettingsHooks();
setupRuntimeHooks();
let universe: TestUniverse;
beforeEach(() => {
universe = new TestUniverse();
});
it('updates the document on an documentUpdate event if there already is a previous document', async () => {
const parentTarget = universe.createTarget();
const target = universe.createTarget({parentTarget});
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
assert.exists(domModel.agent);
domModel.setDocumentForTest({nodeId: 0} as Protocol.DOM.Node);
const spy = sinon.spy(domModel.agent, 'invoke_getDocument');
sinon.assert.notCalled(spy);
assert.isNotNull(domModel.existingDocument());
domModel.documentUpdated();
sinon.assert.calledOnce(spy);
});
it('does not request document if there is not a previous document', async () => {
const parentTarget = universe.createTarget();
const target = universe.createTarget({parentTarget});
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
assert.exists(domModel.agent);
domModel.setDocumentForTest(null);
const spy = sinon.spy(domModel.agent, 'invoke_getDocument');
sinon.assert.notCalled(spy);
assert.isNull(domModel.existingDocument());
domModel.documentUpdated();
sinon.assert.notCalled(spy);
});
describe('securityOrigin', () => {
it('returns a SecurityOrigin matching the document URL', () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const documentNode = new SDK.DOMModel.DOMDocument(domModel, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'https://example.com/page.html',
});
const origin = documentNode.securityOrigin();
assert.isTrue(origin.isSameOriginWith(SDK.SecurityOrigin.SecurityOrigin.create('https://example.com')));
assert.isFalse(origin.isSameOriginWith(SDK.SecurityOrigin.SecurityOrigin.create('https://other.com')));
});
it('returns the owner document securityOrigin for child nodes', () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const documentNode = new SDK.DOMModel.DOMDocument(domModel, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'https://example.com/page.html',
});
const childNode = SDK.DOMModel.DOMNode.create(domModel, documentNode, false, {
nodeId: 2 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'DIV',
localName: 'div',
nodeValue: '',
});
assert.isTrue(childNode.securityOrigin()?.isSameOriginWith(documentNode.securityOrigin()));
});
it('returns null for detached nodes without ownerDocument', () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const detachedNode = SDK.DOMModel.DOMNode.create(domModel, null, false, {
nodeId: 2 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'DIV',
localName: 'div',
nodeValue: '',
});
assert.isNull(detachedNode.securityOrigin());
});
it('updates securityOrigin when setDocumentURL is called on DOMDocument', () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const documentNode = new SDK.DOMModel.DOMDocument(domModel, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'https://example.com/page.html',
});
assert.isTrue(documentNode.securityOrigin().isSameOriginWith(
SDK.SecurityOrigin.SecurityOrigin.create('https://example.com')));
documentNode.setDocumentURL(urlString`https://other.com/page.html`);
assert.isTrue(documentNode.securityOrigin().isSameOriginWith(
SDK.SecurityOrigin.SecurityOrigin.create('https://other.com')));
assert.isFalse(documentNode.securityOrigin().isSameOriginWith(
SDK.SecurityOrigin.SecurityOrigin.create('https://example.com')));
});
it('preserves securityOrigin in DOMDocumentSnapshot and DOMNodeSnapshot', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const documentNode = new SDK.DOMModel.DOMDocument(domModel, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'https://example.com/page.html',
});
const docSnapshot = await documentNode.takeSnapshot();
assert.isTrue(docSnapshot.securityOrigin()?.isSameOriginWith(documentNode.securityOrigin()));
const childNode = SDK.DOMModel.DOMNode.create(domModel, documentNode, false, {
nodeId: 2 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'DIV',
localName: 'div',
nodeValue: '',
});
const nodeSnapshot = await childNode.takeSnapshot(docSnapshot as SDK.DOMModel.DOMDocument);
assert.isTrue(nodeSnapshot.securityOrigin()?.isSameOriginWith(documentNode.securityOrigin()));
});
it('returns the frameId for DOMDocument when provided, and null when omitted', () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const docWithFrame = new SDK.DOMModel.DOMDocument(
domModel,
{
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'https://example.com/page.html',
},
'frame-1' as Protocol.Page.FrameId,
);
assert.strictEqual(docWithFrame.frameId(), 'frame-1');
const docWithoutFrame = new SDK.DOMModel.DOMDocument(
domModel,
{
nodeId: 2 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'https://example.com/page.html',
},
);
assert.isNull(docWithoutFrame.frameId());
});
it('does not assign mainFrameId to detached root documents', () => {
const dataUrl = Platform.DevToolsPath.urlString`data:text/html,<h1>Hello</h1>`;
const target = universe.createTarget({url: dataUrl});
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(resourceTreeModel);
const mainFrame = resourceTreeModel.frameAttached('main' as Protocol.Page.FrameId, null);
assert.exists(mainFrame);
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
// Trigger setDetachedRoot by passing a parentId of 0.
domModel.setChildNodes(0 as Protocol.DOM.NodeId, [{
nodeId: 10 as Protocol.DOM.NodeId,
backendNodeId: 10 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'about:blank',
}]);
const detachedDoc = domModel.nodeForId(10 as Protocol.DOM.NodeId);
assert.exists(detachedDoc);
assert.instanceOf(detachedDoc, SDK.DOMModel.DOMDocument);
assert.isNull((detachedDoc as SDK.DOMModel.DOMDocument).frameId());
});
it('detached root documents receive an opaque security origin and do not use documentURL or mainFrame origin',
() => {
const mainFrameUrl = Platform.DevToolsPath.urlString`https://example.com`;
const target = universe.createTarget({url: mainFrameUrl});
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(resourceTreeModel);
const mainFrame = resourceTreeModel.frameAttached('main' as Protocol.Page.FrameId, null);
assert.exists(mainFrame);
mainFrame.navigate({
id: 'main' as Protocol.Page.FrameId,
loaderId: 'loaderId' as Protocol.Network.LoaderId,
url: mainFrameUrl,
domainAndRegistry: 'example.com',
securityOrigin: 'https://example.com',
mimeType: 'text/html',
secureContextType: ProtocolModule.Page.SecureContextType.Secure,
crossOriginIsolatedContextType: ProtocolModule.Page.CrossOriginIsolatedContextType.NotIsolated,
gatedAPIFeatures: [],
});
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
// Trigger setDetachedRoot by passing a parentId of 0 with a cross-origin documentURL.
domModel.setChildNodes(0 as Protocol.DOM.NodeId, [{
nodeId: 10 as Protocol.DOM.NodeId,
backendNodeId: 10 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'https://victim.example/page.html',
}]);
const detachedDoc = domModel.nodeForId(10 as Protocol.DOM.NodeId);
assert.instanceOf(detachedDoc, SDK.DOMModel.DOMDocument);
const mainOrigin = mainFrame.securityOrigin();
const detachedOrigin = detachedDoc.securityOrigin();
assert.isFalse(detachedOrigin.isSameOriginWith(mainOrigin));
assert.isFalse(
detachedOrigin.isSameOriginWith(SDK.SecurityOrigin.SecurityOrigin.create('https://victim.example')));
assert.isTrue(detachedOrigin.isOpaque());
});
});
it('updates top layer elements correctly', async () => {
const parentTarget = universe.createTarget();
const target = universe.createTarget({parentTarget});
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const TOP_LAYER_NODE_ID = 2 as Protocol.DOM.NodeId;
const NOT_TOP_LAYER_NODE_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 2,
children: [
{
nodeId: TOP_LAYER_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
},
{
nodeId: NOT_TOP_LAYER_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
},
],
} as Protocol.DOM.Node);
const topLayerNode = domModel.nodeForId(TOP_LAYER_NODE_ID);
const notTopLayerNode = domModel.nodeForId(NOT_TOP_LAYER_NODE_ID);
assert.exists(topLayerNode);
assert.exists(notTopLayerNode);
sinon.stub(domModel.agent, 'invoke_getTopLayerElements').resolves({
nodeIds: [TOP_LAYER_NODE_ID],
getError: () => undefined,
});
const topLayerChangePromise = domModel.once(SDK.DOMModel.Events.TopLayerElementsChanged);
domModel.topLayerElementsUpdated();
await topLayerChangePromise;
assert.notStrictEqual(topLayerNode?.topLayerIndex(), -1);
assert.strictEqual(notTopLayerNode?.topLayerIndex(), -1);
});
it('updates top layer elements correctly with backdrop', async () => {
const parentTarget = universe.createTarget();
const target = universe.createTarget({parentTarget});
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const BACKDROP_NODE_ID = 2 as Protocol.DOM.NodeId;
const TOP_LAYER_NODE_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 2,
children: [
{
nodeId: BACKDROP_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::backdrop',
localName: '::backdrop',
nodeValue: '',
},
{
nodeId: TOP_LAYER_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
},
],
} as Protocol.DOM.Node);
const topLayerNode = domModel.nodeForId(TOP_LAYER_NODE_ID);
assert.exists(topLayerNode);
sinon.stub(domModel.agent, 'invoke_getTopLayerElements').resolves({
nodeIds: [BACKDROP_NODE_ID, TOP_LAYER_NODE_ID],
getError: () => undefined,
});
const topLayerChangePromise = domModel.once(SDK.DOMModel.Events.TopLayerElementsChanged);
domModel.topLayerElementsUpdated();
const data = await topLayerChangePromise;
const topLayerShortcuts = data.documentShortcuts;
assert.lengthOf(topLayerShortcuts, 1);
assert.strictEqual(topLayerShortcuts[0].deferredNode.backendNodeId(), 3 as Protocol.DOM.BackendNodeId);
assert.lengthOf(topLayerShortcuts[0].childShortcuts, 1);
assert.strictEqual(topLayerShortcuts[0].childShortcuts[0].deferredNode.backendNodeId(),
2 as Protocol.DOM.BackendNodeId);
});
it('updates top layer elements correctly with multiple documents', async () => {
const parentTarget = universe.createTarget();
const target = universe.createTarget({parentTarget});
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const IFRAME_NODE_ID = 2 as Protocol.DOM.NodeId;
const CONTENT_DOCUMENT_NODE_ID = 3 as Protocol.DOM.NodeId;
const TOP_LAYER_NODE_1_ID = 4 as Protocol.DOM.NodeId;
const TOP_LAYER_NODE_2_ID = 5 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 2,
children: [
{
nodeId: TOP_LAYER_NODE_1_ID,
backendNodeId: 4 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
},
{
nodeId: IFRAME_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'iframe',
localName: 'iframe',
nodeValue: '',
contentDocument: {
nodeId: CONTENT_DOCUMENT_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: TOP_LAYER_NODE_2_ID,
backendNodeId: 5 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
},
],
},
},
],
} as Protocol.DOM.Node);
const topLayerNode1 = domModel.nodeForId(TOP_LAYER_NODE_1_ID);
const topLayerNode2 = domModel.nodeForId(TOP_LAYER_NODE_2_ID);
assert.exists(topLayerNode1);
assert.exists(topLayerNode2);
sinon.stub(domModel.agent, 'invoke_getTopLayerElements').resolves({
nodeIds: [TOP_LAYER_NODE_1_ID, TOP_LAYER_NODE_2_ID],
getError: () => undefined,
});
const events: Array<{document: SDK.DOMModel.DOMDocument, documentShortcuts: SDK.DOMModel.DOMNodeShortcut[]}> = [];
domModel.addEventListener(SDK.DOMModel.Events.TopLayerElementsChanged, event => {
events.push(event.data);
});
const topLayerChangePromise = domModel.once(SDK.DOMModel.Events.TopLayerElementsChanged);
domModel.topLayerElementsUpdated();
await topLayerChangePromise;
// Wait for the second event if it hasn't arrived yet.
if (events.length < 2) {
await domModel.once(SDK.DOMModel.Events.TopLayerElementsChanged);
}
assert.lengthOf(events, 2);
// Sort events by document ID to ensure deterministic order for assertions
events.sort((a, b) => a.document.id - b.document.id);
assert.strictEqual(events[0].document.id, DOCUMENT_NODE_ID);
assert.lengthOf(events[0].documentShortcuts, 1);
assert.strictEqual(events[0].documentShortcuts[0].deferredNode.backendNodeId(), 4 as Protocol.DOM.BackendNodeId);
assert.strictEqual(events[1].document.id, CONTENT_DOCUMENT_NODE_ID);
assert.lengthOf(events[1].documentShortcuts, 1);
assert.strictEqual(events[1].documentShortcuts[0].deferredNode.backendNodeId(), 5 as Protocol.DOM.BackendNodeId);
});
describe('cssEscape', () => {
it('escapes empty string', () => {
assert.strictEqual(cssEscape(''), '');
});
it('does not escape standard identifiers', () => {
assert.strictEqual(cssEscape('foo'), 'foo');
assert.strictEqual(cssEscape('foo-bar'), 'foo-bar');
assert.strictEqual(cssEscape('foo_bar'), 'foo_bar');
assert.strictEqual(cssEscape('fooBar123'), 'fooBar123');
});
it('escapes leading digit', () => {
assert.strictEqual(cssEscape('123foo'), '\\31 23foo');
assert.strictEqual(cssEscape('0'), '\\30 ');
});
it('escapes single hyphen and hyphen followed by digit', () => {
assert.strictEqual(cssEscape('-'), '\\-');
assert.strictEqual(cssEscape('-123'), '-\\31 23');
assert.strictEqual(cssEscape('--custom-property'), '--custom-property');
assert.strictEqual(cssEscape('-a'), '-a');
});
it('escapes special characters and punctuation', () => {
assert.strictEqual(cssEscape('#id'), '\\#id');
assert.strictEqual(cssEscape('.class'), '\\.class');
assert.strictEqual(cssEscape('hello world'), 'hello\\ world');
assert.strictEqual(cssEscape('foo:bar'), 'foo\\:bar');
assert.strictEqual(cssEscape('[type="text"]'), '\\[type\\=\\"text\\"\\]');
assert.strictEqual(cssEscape('(pseudo)'), '\\(pseudo\\)');
});
it('escapes null and control characters', () => {
assert.strictEqual(cssEscape('\0'), '\uFFFD');
assert.strictEqual(cssEscape('\x01'), '\\1 ');
assert.strictEqual(cssEscape('\x1f'), '\\1f ');
assert.strictEqual(cssEscape('\x7f'), '\\7f ');
});
it('preserves non-ASCII characters', () => {
assert.strictEqual(cssEscape('café'), 'café');
assert.strictEqual(cssEscape('🚀'), '🚀');
});
});
describe('DOMNode', () => {
describe('simpleSelector', () => {
let target: SDK.Target.Target;
let model: SDK.DOMModel.DOMModel;
beforeEach(() => {
target = universe.createTarget();
const modelBeforeAssertion = target.model(SDK.DOMModel.DOMModel);
assert.exists(modelBeforeAssertion);
model = modelBeforeAssertion;
});
afterEach(() => {
target.dispose('NO_REASON');
});
it('should return localName when it\'s not an input, it does not have an idea and does not contain any classes',
() => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
});
assert.strictEqual(domNode.simpleSelector(), 'div');
});
it('should return localName with input type if it is an input and does not contain any idea or classes', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'input',
attributes: [
'type',
'text',
],
localName: 'input',
nodeValue: '',
});
assert.strictEqual(domNode.simpleSelector(), 'input[type="text"]');
});
it('should return localName with id if it has an id', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'input',
attributes: [
'type',
'text',
'id',
'input-with-id',
],
localName: 'input',
nodeValue: '',
});
assert.strictEqual(domNode.simpleSelector(), 'input#input-with-id');
});
it('should return localName with classes appended for an input', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'input',
attributes: [
'type',
'text',
'class',
'first-class second-class',
],
localName: 'input',
nodeValue: '',
});
assert.strictEqual(domNode.simpleSelector(), 'input.first-class.second-class');
});
it('should return localName with classes appended for a div without mentioning div', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
attributes: [
'type',
'text',
'class',
'first-class second-class',
],
localName: 'div',
nodeValue: '',
});
assert.strictEqual(domNode.simpleSelector(), '.first-class.second-class');
});
it('should return localName for a pseudo class without pseudo identifier', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::before',
localName: '::before',
nodeValue: '',
});
assert.strictEqual(domNode.simpleSelector(), '::before');
});
it('should return localName for a pseudo class with a pseudo identifier', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
pseudoIdentifier: 'root',
nodeName: '::view-transition-new',
localName: '::view-transition-new',
nodeValue: '',
});
assert.strictEqual(domNode.simpleSelector(), '::view-transition-new(root)');
});
it('should escape pseudo identifier in simpleSelector', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
pseudoIdentifier: '123.foo',
nodeName: '::view-transition-new',
localName: '::view-transition-new',
nodeValue: '',
});
assert.strictEqual(domNode.simpleSelector(), '::view-transition-new(\\31 23\\.foo)');
});
});
describe('isCustomElement', () => {
let target: SDK.Target.Target;
let model: SDK.DOMModel.DOMModel;
beforeEach(() => {
target = universe.createTarget();
const modelBeforeAssertion = target.model(SDK.DOMModel.DOMModel);
assert.exists(modelBeforeAssertion);
model = modelBeforeAssertion;
});
afterEach(() => {
target.dispose('NO_REASON');
});
it('should return true for a custom element with a hyphen in localName', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'my-widget',
localName: 'my-widget',
nodeValue: '',
});
assert.isTrue(domNode.isCustomElement());
});
it('should return true for an element with an is attribute', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'button',
localName: 'button',
attributes: ['is', 'my-button'],
nodeValue: '',
});
assert.isTrue(domNode.isCustomElement());
});
it('should return false for excluded built-in elements with hyphens like font-face-src', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'font-face-src',
localName: 'font-face-src',
nodeValue: '',
});
assert.isFalse(domNode.isCustomElement());
});
it('should return false for excluded built-in elements with hyphens like annotation-xml', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 3 as Protocol.DOM.NodeId,
backendNodeId: 4 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'annotation-xml',
localName: 'annotation-xml',
nodeValue: '',
});
assert.isFalse(domNode.isCustomElement());
});
it('should return false for XML elements with hyphens', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'custom-xml-tag',
localName: 'custom-xml-tag',
xmlVersion: '1.0',
nodeValue: '',
});
assert.isFalse(domNode.isCustomElement());
});
it('should return false for standard HTML tags without hyphens or is attribute', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
});
assert.isFalse(domNode.isCustomElement());
});
it('should return false for non-element nodes', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.TEXT_NODE,
nodeName: '#text',
localName: '',
nodeValue: 'some text',
});
assert.isFalse(domNode.isCustomElement());
});
it('should return false for pseudo-elements with hyphens', () => {
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::interest-button',
localName: '::interest-button',
pseudoType: ProtocolModule.DOM.PseudoType.InterestButton,
nodeValue: '',
});
assert.isFalse(domNode.isCustomElement());
});
});
describe('duplicate', () => {
it('calls copyTo on parent node with nextSibling as anchor', () => {
const target = universe.createTarget();
const model = target.model(SDK.DOMModel.DOMModel) as SDK.DOMModel.DOMModel;
const parentNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
});
const childNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 2 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'span',
localName: 'span',
nodeValue: '',
});
childNode.parentNode = parentNode;
const copyToSpy = sinon.spy(childNode, 'copyTo');
void childNode.duplicate();
sinon.assert.calledOnceWithMatch(copyToSpy, parentNode, null, sinon.match.func);
});
});
describe('toggleHideElement and isToggledToHidden', () => {
it('toggles hidden-marker on node', async () => {
const target = universe.createTarget();
const model = target.model(SDK.DOMModel.DOMModel) as SDK.DOMModel.DOMModel;
const domNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
});
assert.isFalse(domNode.isToggledToHidden());
const mockResolveToObject = sinon.mock().twice().returns({callFunction: () => {}, release: () => {}});
domNode.resolveToObject = mockResolveToObject;
await domNode.toggleHideElement();
assert.isTrue(domNode.isToggledToHidden());
await domNode.toggleHideElement();
assert.isFalse(domNode.isToggledToHidden());
});
for (const [pseudoType, pseudoName] of [[ProtocolModule.DOM.PseudoType.Before, '::before'],
[ProtocolModule.DOM.PseudoType.After, '::after'],
] as const) {
it(`hides the ${pseudoName} pseudo element through its parent element`, async () => {
// Mirrors legacy elements/hide-shortcut (testToggleHide{Before,After}PseudoShortcut{On,Off}).
const target = universe.createTarget();
const model = target.model(SDK.DOMModel.DOMModel) as SDK.DOMModel.DOMModel;
const parentNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'DIV',
localName: 'div',
nodeValue: '',
attributes: ['id', 'parent-node'],
pseudoElements: [{
nodeId: 2 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: pseudoName,
localName: '',
nodeValue: '',
pseudoType,
}],
});
const pseudoNode = pseudoType === ProtocolModule.DOM.PseudoType.Before ? parentNode.beforePseudoElement() :
parentNode.afterPseudoElement();
assert.exists(pseudoNode);
assert.strictEqual(pseudoNode.parentNode, parentNode);
const callFunction = sinon.stub().resolves({});
const release = sinon.stub();
const parentResolve =
sinon.stub(parentNode, 'resolveToObject').resolves({callFunction,
release} as unknown as SDK.RemoteObject.RemoteObject);
const pseudoResolve = sinon.stub(pseudoNode, 'resolveToObject');
await pseudoNode.toggleHideElement();
// The pseudo element cannot be resolved to a JS object, so its parent element is used instead.
sinon.assert.calledOnce(parentResolve);
sinon.assert.notCalled(pseudoResolve);
sinon.assert.calledOnce(callFunction);
assert.deepEqual(callFunction.firstCall.args[1], [{value: pseudoName}, {value: true}]);
sinon.assert.calledOnce(release);
assert.isTrue(pseudoNode.isToggledToHidden());
assert.isFalse(parentNode.isToggledToHidden());
await pseudoNode.toggleHideElement();
sinon.assert.calledTwice(callFunction);
assert.deepEqual(callFunction.secondCall.args[1], [{value: pseudoName}, {value: false}]);
assert.isFalse(pseudoNode.isToggledToHidden());
});
}
it('escapes pseudo-element identifiers when hiding a pseudo element', async () => {
const target = universe.createTarget();
const model = target.model(SDK.DOMModel.DOMModel) as SDK.DOMModel.DOMModel;
const parentNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'DIV',
localName: 'div',
nodeValue: '',
pseudoElements: [{
nodeId: 2 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::view-transition-group',
localName: '::view-transition-group',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup,
pseudoIdentifier: '123.foo',
}],
});
const vtNodes = parentNode.pseudoElements().get(ProtocolModule.DOM.PseudoType.ViewTransitionGroup);
assert.exists(vtNodes);
assert.lengthOf(vtNodes, 1);
const pseudoNode = vtNodes[0];
const callFunction = sinon.stub().resolves({});
const release = sinon.stub();
sinon.stub(parentNode, 'resolveToObject').resolves({callFunction,
release} as unknown as SDK.RemoteObject.RemoteObject);
await pseudoNode.toggleHideElement();
sinon.assert.calledOnce(callFunction);
assert.deepEqual(callFunction.firstCall.args[1],
[{value: '::view-transition-group(\\31 23\\.foo)'}, {value: true}]);
});
it('generates distinct classes and style rules when hiding multiple pseudo elements', async function() {
const target = universe.createTarget();
const model = target.model(SDK.DOMModel.DOMModel) as SDK.DOMModel.DOMModel;
const parentNode = SDK.DOMModel.DOMNode.create(model, null, false, {
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'DIV',
localName: 'div',
nodeValue: '',
pseudoElements: [
{
nodeId: 2 as Protocol.DOM.NodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::view-transition-group',
localName: '::view-transition-group',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup,
pseudoIdentifier: 'ヘッダー',
},
{
nodeId: 3 as Protocol.DOM.NodeId,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::view-transition-group',
localName: '::view-transition-group',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup,
pseudoIdentifier: 'フッター',
},
{
nodeId: 4 as Protocol.DOM.NodeId,
backendNodeId: 4 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::view-transition-group',
localName: '::view-transition-group',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup,
pseudoIdentifier: 'a.b',
},
{
nodeId: 5 as Protocol.DOM.NodeId,
backendNodeId: 5 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::view-transition-group',
localName: '::view-transition-group',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup,
pseudoIdentifier: 'a b',
},
{
nodeId: 6 as Protocol.DOM.NodeId,
backendNodeId: 6 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::view-transition-group',
localName: '::view-transition-group',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup,
pseudoIdentifier: 'a__b',
},
],
});
const vtNodes = parentNode.pseudoElements().get(ProtocolModule.DOM.PseudoType.ViewTransitionGroup);
assert.exists(vtNodes);
assert.lengthOf(vtNodes, 5);
const classes = new Set<string>();
const styleClasses = new Set<string>();
const style = {
textContent: '',
classList: {
contains: function(cls: string) {
return styleClasses.has(cls);
},
add: function(cls: string) {
styleClasses.add(cls);
},
},
};
const element = {
nodeType: 1,
parentNode: null,
classList: {
toggle: function(cls: string, force: boolean) {
if (force) {
classes.add(cls);
} else {
classes.delete(cls);
}
},
},
querySelector: function() {
return style;
},
};
const callFunction = sinon.stub().callsFake(function(fn: (this: unknown, ...args: unknown[]) => void,
args: Array<{value: unknown}>) {
fn.apply(element, args.map(function(a) {
return a.value;
}));
return Promise.resolve({});
});
const release = sinon.stub();
sinon.stub(parentNode, 'resolveToObject').resolves({callFunction,
release} as unknown as SDK.RemoteObject.RemoteObject);
for (const node of vtNodes) {
await node.toggleHideElement();
}
assert.strictEqual(classes.size, 5);
assert.include(style.textContent, '::view-transition-group(ヘッダー)');
assert.include(style.textContent, '::view-transition-group(フッター)');
assert.include(style.textContent, '::view-transition-group(a\\.b)');
assert.include(style.textContent, '::view-transition-group(a\\ b)');
assert.include(style.textContent, '::view-transition-group(a__b)');
await vtNodes[0].toggleHideElement();
assert.strictEqual(classes.size, 4);
});
});
});
describe('document.open() URL update (crbug.com/370690261)', () => {
it('updates iframe contentDocument URL and dispatches DocumentURLChanged event', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const IFRAME_NODE_ID = 2 as Protocol.DOM.NodeId;
const CONTENT_DOCUMENT_NODE_ID = 3 as Protocol.DOM.NodeId;
const IFRAME_FRAME_ID = 'iframe-frame-id' as Protocol.Page.FrameId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'https://example.com/',
baseURL: 'https://example.com/',
childNodeCount: 1,
children: [
{
nodeId: IFRAME_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'IFRAME',
localName: 'iframe',
nodeValue: '',
frameId: IFRAME_FRAME_ID,
contentDocument: {
nodeId: CONTENT_DOCUMENT_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'about:blank',
baseURL: 'about:blank',
childNodeCount: 0,
children: [],
},
},
],
} as Protocol.DOM.Node);
const iframeNode = domModel.nodeForId(IFRAME_NODE_ID);
assert.exists(iframeNode);
const contentDocument = iframeNode.contentDocument();
assert.exists(contentDocument);
assert.strictEqual(contentDocument.documentURL, 'about:blank');
const documentURLChangedPromise = domModel.once(SDK.DOMModel.Events.DocumentURLChanged);
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(resourceTreeModel);
resourceTreeModel.documentOpened({
id: IFRAME_FRAME_ID,
loaderId: 'loader-1' as Protocol.Network.LoaderId,
url: 'https://example.com/',
domainAndRegistry: 'example.com',
securityOrigin: 'https://example.com',
mimeType: 'text/html',
secureContextType: ProtocolModule.Page.SecureContextType.Secure,
crossOriginIsolatedContextType: ProtocolModule.Page.CrossOriginIsolatedContextType.Isolated,
gatedAPIFeatures: [],
});
const changedDocument = await documentURLChangedPromise;
assert.strictEqual(changedDocument, contentDocument);
assert.strictEqual(contentDocument.documentURL, 'https://example.com/');
});
it('does not dispatch event when URL has not changed', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const IFRAME_NODE_ID = 2 as Protocol.DOM.NodeId;
const CONTENT_DOCUMENT_NODE_ID = 3 as Protocol.DOM.NodeId;
const IFRAME_FRAME_ID = 'iframe-frame-id' as Protocol.Page.FrameId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'https://example.com/',
baseURL: 'https://example.com/',
childNodeCount: 1,
children: [
{
nodeId: IFRAME_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'IFRAME',
localName: 'iframe',
nodeValue: '',
frameId: IFRAME_FRAME_ID,
contentDocument: {
nodeId: CONTENT_DOCUMENT_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
documentURL: 'https://example.com/',
baseURL: 'https://example.com/',
childNodeCount: 0,
children: [],
},
},
],
} as Protocol.DOM.Node);
let eventDispatched = false;
domModel.addEventListener(SDK.DOMModel.Events.DocumentURLChanged, () => {
eventDispatched = true;
});
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(resourceTreeModel);
resourceTreeModel.documentOpened({
id: IFRAME_FRAME_ID,
loaderId: 'loader-1' as Protocol.Network.LoaderId,
url: 'https://example.com/',
domainAndRegistry: 'example.com',
securityOrigin: 'https://example.com',
mimeType: 'text/html',
secureContextType: ProtocolModule.Page.SecureContextType.Secure,
crossOriginIsolatedContextType: ProtocolModule.Page.CrossOriginIsolatedContextType.Isolated,
gatedAPIFeatures: [],
});
assert.isFalse(eventDispatched);
});
});
describe('DOMNodeSnapshot', () => {
it('snapshots a clean DOMNode with children and attributes', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const PARENT_NODE_ID = 2 as Protocol.DOM.NodeId;
const CHILD_NODE_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: PARENT_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
attributes: ['class', 'container', 'id', 'parent'],
childNodeCount: 1,
children: [
{
nodeId: CHILD_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'span',
localName: 'span',
nodeValue: '',
attributes: ['class', 'child'],
},
],
},
],
} as Protocol.DOM.Node);
const parentNode = domModel.nodeForId(PARENT_NODE_ID);
assert.exists(parentNode);
const snapshot = await parentNode.takeSnapshot();
assert.instanceOf(snapshot, SDK.DOMModel.DOMNodeSnapshot);
assert.strictEqual(snapshot.id, PARENT_NODE_ID);
assert.strictEqual(snapshot.nodeName(), 'div');
assert.strictEqual(snapshot.getAttribute('class'), 'container');
assert.strictEqual(snapshot.getAttribute('id'), 'parent');
const children = snapshot.children();
assert.isNotNull(children);
assert.lengthOf(children, 1);
assert.strictEqual(children[0].id, CHILD_NODE_ID);
assert.strictEqual(children[0].nodeName(), 'span');
assert.strictEqual(children[0].getAttribute('class'), 'child');
assert.strictEqual(children[0].parentNode, snapshot);
});
it('snapshots shadow roots', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const HOST_NODE_ID = 2 as Protocol.DOM.NodeId;
const SHADOW_ROOT_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: HOST_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
shadowRoots: [
{
nodeId: SHADOW_ROOT_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_FRAGMENT_NODE,
nodeName: '#shadow-root',
localName: '',
nodeValue: '',
shadowRootType: ProtocolModule.DOM.ShadowRootType.Open,
},
],
},
],
} as Protocol.DOM.Node);
const hostNode = domModel.nodeForId(HOST_NODE_ID);
assert.exists(hostNode);
const snapshot = await hostNode.takeSnapshot();
assert.strictEqual(snapshot.id, HOST_NODE_ID);
const shadowRoots = snapshot.shadowRoots();
assert.lengthOf(shadowRoots, 1);
assert.strictEqual(shadowRoots[0].id, SHADOW_ROOT_ID);
assert.strictEqual(shadowRoots[0].shadowRootType(), ProtocolModule.DOM.ShadowRootType.Open);
assert.strictEqual(shadowRoots[0].parentNode, snapshot);
});
it('takes snapshot with adoptedStyleSheets', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const ELEMENT_NODE_ID = 2 as Protocol.DOM.NodeId;
const STYLESHEET_ID = 'stylesheet-id' as Protocol.DOM.StyleSheetId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: ELEMENT_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
adoptedStyleSheets: [STYLESHEET_ID],
},
],
} as Protocol.DOM.Node);
const elementNode = domModel.nodeForId(ELEMENT_NODE_ID);
assert.exists(elementNode);
assert.lengthOf(elementNode.adoptedStyleSheetsForNode, 1);
const snapshot = await elementNode.takeSnapshot();
assert.lengthOf(snapshot.adoptedStyleSheetsForNode, 1);
assert.strictEqual(snapshot.adoptedStyleSheetsForNode[0].id, STYLESHEET_ID);
});
it('snapshots pseudo elements', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const ELEMENT_NODE_ID = 2 as Protocol.DOM.NodeId;
const PSEUDO_NODE_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: ELEMENT_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
pseudoElements: [
{
nodeId: PSEUDO_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::before',
localName: '::before',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.Before,
},
],
},
],
} as Protocol.DOM.Node);
const elementNode = domModel.nodeForId(ELEMENT_NODE_ID);
assert.exists(elementNode);
const snapshot = await elementNode.takeSnapshot();
assert.strictEqual(snapshot.id, ELEMENT_NODE_ID);
const beforePseudo = snapshot.beforePseudoElement();
assert.exists(beforePseudo);
assert.strictEqual(beforePseudo?.id, PSEUDO_NODE_ID);
assert.strictEqual(beforePseudo?.parentNode, snapshot);
});
it('snapshots template content', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const TEMPLATE_NODE_ID = 2 as Protocol.DOM.NodeId;
const CONTENT_NODE_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: TEMPLATE_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'template',
localName: 'template',
nodeValue: '',
templateContent: {
nodeId: CONTENT_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_FRAGMENT_NODE,
nodeName: '#document-fragment',
localName: '',
nodeValue: '',
},
},
],
} as Protocol.DOM.Node);
const templateNode = domModel.nodeForId(TEMPLATE_NODE_ID);
assert.exists(templateNode);
const snapshot = await templateNode.takeSnapshot();
assert.strictEqual(snapshot.id, TEMPLATE_NODE_ID);
const content = snapshot.templateContent();
assert.exists(content);
assert.strictEqual(content?.id, CONTENT_NODE_ID);
assert.strictEqual(content?.parentNode, snapshot);
});
it('snapshots iframe content document', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const IFRAME_NODE_ID = 2 as Protocol.DOM.NodeId;
const CONTENT_DOCUMENT_NODE_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: IFRAME_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'iframe',
localName: 'iframe',
nodeValue: '',
contentDocument: {
nodeId: CONTENT_DOCUMENT_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
},
},
],
} as Protocol.DOM.Node);
const iframeNode = domModel.nodeForId(IFRAME_NODE_ID);
assert.exists(iframeNode);
const snapshot = await iframeNode.takeSnapshot();
assert.strictEqual(snapshot.id, IFRAME_NODE_ID);
const contentDocument = snapshot.contentDocument();
assert.exists(contentDocument);
assert.strictEqual(contentDocument?.id, CONTENT_DOCUMENT_NODE_ID);
assert.strictEqual(contentDocument?.parentNode, snapshot);
});
it('snapshots DOMDocument properties', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const HTML_NODE_ID = 2 as Protocol.DOM.NodeId;
const BODY_NODE_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
documentURL: 'https://example.com/',
baseURL: 'https://example.com/',
childNodeCount: 1,
children: [
{
nodeId: HTML_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'HTML',
localName: 'html',
nodeValue: '',
childNodeCount: 1,
children: [
{
nodeId: BODY_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'BODY',
localName: 'body',
nodeValue: '',
},
],
},
],
} as Protocol.DOM.Node);
const documentNode = domModel.existingDocument();
assert.exists(documentNode);
const snapshot = await documentNode.takeSnapshot();
assert.instanceOf(snapshot, SDK.DOMModel.DOMDocumentSnapshot);
const documentSnapshot = snapshot as SDK.DOMModel.DOMDocument;
assert.strictEqual(documentSnapshot.documentURL, 'https://example.com/');
assert.strictEqual(documentSnapshot.baseURL, 'https://example.com/');
assert.exists(documentSnapshot.documentElement);
assert.strictEqual(documentSnapshot.documentElement?.id, HTML_NODE_ID);
assert.exists(documentSnapshot.body);
assert.strictEqual(documentSnapshot.body?.id, BODY_NODE_ID);
assert.strictEqual(documentSnapshot.documentElement?.ownerDocument, documentSnapshot);
assert.strictEqual(documentSnapshot.body?.ownerDocument, documentSnapshot);
});
it('snapshots assigned slot', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const ELEMENT_NODE_ID = 2 as Protocol.DOM.NodeId;
const SLOT_ID = 3 as Protocol.DOM.BackendNodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: ELEMENT_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
assignedSlot: {
backendNodeId: SLOT_ID,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'slot',
},
},
],
} as Protocol.DOM.Node);
const elementNode = domModel.nodeForId(ELEMENT_NODE_ID);
assert.exists(elementNode);
const snapshot = await elementNode.takeSnapshot();
assert.strictEqual(snapshot.id, ELEMENT_NODE_ID);
assert.exists(snapshot.assignedSlot);
assert.strictEqual(snapshot.assignedSlot.deferredNode.backendNodeId(), SLOT_ID);
});
it('is immutable', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const ELEMENT_NODE_ID = 2 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: ELEMENT_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
attributes: ['id', 'test'],
},
],
} as Protocol.DOM.Node);
const elementNode = domModel.nodeForId(ELEMENT_NODE_ID);
assert.exists(elementNode);
const snapshot = await elementNode.takeSnapshot();
assert.strictEqual(snapshot.getAttribute('id'), 'test');
snapshot.setAttribute('id', 'new-id');
assert.strictEqual(snapshot.getAttribute('id'), 'test');
await snapshot.removeAttribute('id');
assert.strictEqual(snapshot.getAttribute('id'), 'test');
snapshot.setNodeName('span');
assert.strictEqual(snapshot.nodeName(), 'div');
snapshot.setNodeValue('new value');
assert.strictEqual(snapshot.nodeValue(), '');
});
it('does not reflect live DOM updates', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const ELEMENT_NODE_ID = 2 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: ELEMENT_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
attributes: ['id', 'test'],
},
],
} as Protocol.DOM.Node);
const elementNode = domModel.nodeForId(ELEMENT_NODE_ID);
assert.exists(elementNode);
const snapshot = await elementNode.takeSnapshot();
assert.strictEqual(snapshot.getAttribute('id'), 'test');
domModel.attributeModified(ELEMENT_NODE_ID, 'id', 'updated-id');
assert.strictEqual(elementNode.getAttribute('id'), 'updated-id');
assert.strictEqual(snapshot.getAttribute('id'), 'test');
});
it('does not reflect child insertion in live DOM', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const PARENT_NODE_ID = 2 as Protocol.DOM.NodeId;
const CHILD_NODE_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: PARENT_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
childNodeCount: 0,
children: [] as Protocol.DOM.Node[],
},
],
} as Protocol.DOM.Node);
const parentNode = domModel.nodeForId(PARENT_NODE_ID);
assert.exists(parentNode);
const snapshot = await parentNode.takeSnapshot();
assert.lengthOf(snapshot.children() || [], 0);
domModel.childNodeInserted(PARENT_NODE_ID, 0 as Protocol.DOM.NodeId, {
nodeId: CHILD_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'span',
localName: 'span',
nodeValue: '',
});
assert.lengthOf(parentNode.children() || [], 1);
assert.lengthOf(snapshot.children() || [], 0);
});
});
describe('setAsInspectedNode', () => {
it('does not send setInspectedNode command for non-inspectable pseudo elements', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
assert.exists(domModel.agent);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const ELEMENT_NODE_ID = 2 as Protocol.DOM.NodeId;
const PSEUDO_NODE_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: ELEMENT_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
pseudoElements: [
{
nodeId: PSEUDO_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::first-line',
localName: '::first-line',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.FirstLine,
},
],
},
],
} as Protocol.DOM.Node);
const spy = sinon.spy(domModel.agent, 'invoke_setInspectedNode');
const pseudoNode = domModel.nodeForId(PSEUDO_NODE_ID);
assert.exists(pseudoNode);
await pseudoNode.setAsInspectedNode();
sinon.assert.notCalled(spy);
});
it('sends setInspectedNode command for inspectable pseudo elements', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
assert.exists(domModel.agent);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const ELEMENT_NODE_ID = 2 as Protocol.DOM.NodeId;
const PSEUDO_NODE_ID = 3 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: ELEMENT_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
pseudoElements: [
{
nodeId: PSEUDO_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::before',
localName: '::before',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.Before,
},
],
},
],
} as Protocol.DOM.Node);
const spy = sinon.spy(domModel.agent, 'invoke_setInspectedNode');
const pseudoNode = domModel.nodeForId(PSEUDO_NODE_ID);
assert.exists(pseudoNode);
await pseudoNode.setAsInspectedNode();
sinon.assert.calledOnceWithExactly(spy, {nodeId: PSEUDO_NODE_ID});
});
it('does not send setInspectedNode command for UA shadow roots and their children', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
assert.exists(domModel.agent);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const HOST_NODE_ID = 2 as Protocol.DOM.NodeId;
const UA_SHADOW_ROOT_ID = 3 as Protocol.DOM.NodeId;
const UA_SHADOW_CHILD_ID = 4 as Protocol.DOM.NodeId;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: HOST_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
shadowRoots: [
{
nodeId: UA_SHADOW_ROOT_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_FRAGMENT_NODE,
nodeName: '#shadow-root',
localName: '',
nodeValue: '',
shadowRootType: ProtocolModule.DOM.ShadowRootType.UserAgent,
children: [{
nodeId: UA_SHADOW_CHILD_ID,
backendNodeId: 4 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'span',
localName: 'span',
nodeValue: '',
}],
},
],
},
],
} as Protocol.DOM.Node);
const spy = sinon.spy(domModel.agent, 'invoke_setInspectedNode');
const uaShadowRoot = domModel.nodeForId(UA_SHADOW_ROOT_ID);
assert.exists(uaShadowRoot);
await uaShadowRoot.setAsInspectedNode();
sinon.assert.notCalled(spy);
const uaShadowChild = domModel.nodeForId(UA_SHADOW_CHILD_ID);
assert.exists(uaShadowChild);
await uaShadowChild.setAsInspectedNode();
sinon.assert.notCalled(spy);
});
});
describe('canInspectNode', () => {
let domModel: SDK.DOMModel.DOMModel;
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const ELEMENT_NODE_ID = 2 as Protocol.DOM.NodeId;
const PSEUDO_NODE_ID = 3 as Protocol.DOM.NodeId;
const NON_INSPECTABLE_PSEUDO_NODE_ID = 6 as Protocol.DOM.NodeId;
const UA_SHADOW_ROOT_ID = 4 as Protocol.DOM.NodeId;
const UA_SHADOW_CHILD_ID = 5 as Protocol.DOM.NodeId;
beforeEach(() => {
const target = universe.createTarget();
domModel = target.model(SDK.DOMModel.DOMModel)!;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
childNodeCount: 1,
children: [
{
nodeId: ELEMENT_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
pseudoElements: [
{
nodeId: PSEUDO_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::before',
localName: '::before',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.Before,
},
{
nodeId: NON_INSPECTABLE_PSEUDO_NODE_ID,
backendNodeId: 6 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: '::first-line',
localName: '::first-line',
nodeValue: '',
pseudoType: ProtocolModule.DOM.PseudoType.FirstLine,
},
],
shadowRoots: [
{
nodeId: UA_SHADOW_ROOT_ID,
backendNodeId: 4 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_FRAGMENT_NODE,
nodeName: '#shadow-root',
localName: '',
nodeValue: '',
shadowRootType: ProtocolModule.DOM.ShadowRootType.UserAgent,
children: [{
nodeId: UA_SHADOW_CHILD_ID,
backendNodeId: 5 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'span',
localName: 'span',
nodeValue: '',
}],
},
],
},
],
} as Protocol.DOM.Node);
});
it('returns true for normal elements', () => {
const elementNode = domModel.nodeForId(ELEMENT_NODE_ID);
assert.exists(elementNode);
assert.isTrue(elementNode.canInspectNode());
});
it('returns true for inspectable pseudo elements', () => {
const pseudoNode = domModel.nodeForId(PSEUDO_NODE_ID);
assert.exists(pseudoNode);
assert.isTrue(pseudoNode.canInspectNode());
});
it('returns false for non-inspectable pseudo elements', () => {
const pseudoNode = domModel.nodeForId(NON_INSPECTABLE_PSEUDO_NODE_ID);
assert.exists(pseudoNode);
assert.isFalse(pseudoNode.canInspectNode());
});
it('returns false for user agent shadow roots', () => {
const uaShadowRoot = domModel.nodeForId(UA_SHADOW_ROOT_ID);
assert.exists(uaShadowRoot);
assert.isFalse(uaShadowRoot.canInspectNode());
});
it('returns false for nodes inside user agent shadow roots', () => {
const uaShadowChild = domModel.nodeForId(UA_SHADOW_CHILD_ID);
assert.exists(uaShadowChild);
assert.isFalse(uaShadowChild.canInspectNode());
});
it('returns false for snapshots', async () => {
const elementNode = domModel.nodeForId(ELEMENT_NODE_ID);
assert.exists(elementNode);
const snapshot = await elementNode.takeSnapshot();
assert.isFalse(snapshot.canInspectNode());
});
});
it('correctly parses baseURL and documentURL for main document and iframes', () => {
const parentTarget = universe.createTarget();
const target = universe.createTarget({parentTarget});
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const DOCUMENT_NODE_ID = 1 as Protocol.DOM.NodeId;
const IFRAME_NODE_ID = 2 as Protocol.DOM.NodeId;
const CONTENT_DOCUMENT_NODE_ID = 3 as Protocol.DOM.NodeId;
const mainBaseURL = urlString`http://127.0.0.1:8000/devtools/elements/`;
const mainDocumentURL = urlString`http://127.0.0.1:8000/devtools/resources/inspected-page.html`;
const iframeBaseURL = urlString`http://127.0.0.1:8000/devtools/elements/resources/elements-empty-iframe.html`;
const iframeDocumentURL = urlString`http://127.0.0.1:8000/devtools/elements/resources/elements-empty-iframe.html`;
domModel.setDocumentForTest({
nodeId: DOCUMENT_NODE_ID,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
baseURL: mainBaseURL,
documentURL: mainDocumentURL,
childNodeCount: 1,
children: [
{
nodeId: IFRAME_NODE_ID,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.ELEMENT_NODE,
nodeName: 'iframe',
localName: 'iframe',
nodeValue: '',
contentDocument: {
nodeId: CONTENT_DOCUMENT_NODE_ID,
backendNodeId: 3 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
baseURL: iframeBaseURL,
documentURL: iframeDocumentURL,
childNodeCount: 0,
children: [],
},
},
],
} as Protocol.DOM.Node);
const mainDocument = domModel.existingDocument();
assert.exists(mainDocument);
assert.strictEqual(mainDocument.baseURL, mainBaseURL);
assert.strictEqual(mainDocument.documentURL, mainDocumentURL);
assert.isNull(mainDocument.parentNode);
const iframeNode = domModel.nodeForId(IFRAME_NODE_ID);
assert.exists(iframeNode);
const iframeDocument = iframeNode.contentDocument();
assert.exists(iframeDocument);
assert.strictEqual(iframeDocument.baseURL, iframeBaseURL);
assert.strictEqual(iframeDocument.documentURL, iframeDocumentURL);
assert.strictEqual(iframeDocument.parentNode, iframeNode);
});
it('resolves canonical SecurityOrigin from ResourceTreeModel mainFrame for opaque documents', () => {
const parentTarget = universe.createTarget();
const dataUrl = urlString`data:text/html,<h1>Hello</h1>`;
const target = universe.createTarget({parentTarget, url: dataUrl});
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(resourceTreeModel);
const mainFrame = resourceTreeModel.frameAttached('main' as Protocol.Page.FrameId, null);
assert.exists(mainFrame);
mainFrame.navigate({
id: 'main' as Protocol.Page.FrameId,
loaderId: 'loaderId' as Protocol.Network.LoaderId,
url: dataUrl,
domainAndRegistry: '',
securityOrigin: 'null',
mimeType: 'text/html',
secureContextType: ProtocolModule.Page.SecureContextType.Secure,
crossOriginIsolatedContextType: ProtocolModule.Page.CrossOriginIsolatedContextType.NotIsolated,
gatedAPIFeatures: [],
});
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
domModel.setDocumentForTest({
nodeId: 1 as Protocol.DOM.NodeId,
backendNodeId: 1 as Protocol.DOM.BackendNodeId,
nodeType: NodeType.DOCUMENT_NODE,
nodeName: '#document',
localName: '',
nodeValue: '',
baseURL: dataUrl,
documentURL: dataUrl,
childNodeCount: 0,
children: [],
} as Protocol.DOM.Node);
const document = domModel.existingDocument();
assert.exists(document);
assert.strictEqual(document.securityOrigin(), mainFrame.securityOrigin());
});
describe('DOMModelUndoStack', () => {
it('allows calling undo multiple times with non-empty history', async () => {
const parentTarget = universe.createTarget();
const target = universe.createTarget({parentTarget});
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const markUndoableSpy = sinon.stub(domModel.agent, 'invoke_markUndoableState').resolves({
getError: () => undefined,
});
const undoSpy = sinon.stub(domModel.agent, 'invoke_undo').resolves({
getError: () => undefined,
});
const undoStack = new SDK.DOMModel.DOMModelUndoStack();
await undoStack.markUndoableState(domModel, false);
sinon.assert.calledOnce(markUndoableSpy);
await undoStack.undo();
sinon.assert.calledOnce(undoSpy);
// Perform second undo when history stack is empty.
await undoStack.undo();
// Should not call invoke_undo again because stack index is 0.
sinon.assert.calledOnce(undoSpy);
});
it('supports undo and subsequent perform/redo of mergeable actions', async () => {
const target = universe.createTarget();
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const undoSpy = sinon.stub(domModel.agent, 'invoke_undo').resolves({getError: () => undefined});
const redoSpy = sinon.stub(domModel.agent, 'invoke_redo').resolves({getError: () => undefined});
const undoStack = new SDK.DOMModel.DOMModelUndoStack();
// Coalesce two minor (mergeable) actions into a single undoable entry.
await undoStack.markUndoableState(domModel, true);
await undoStack.markUndoableState(domModel, true);
await undoStack.undo();
sinon.assert.calledOnce(undoSpy);
await undoStack.redo();
sinon.assert.calledOnce(redoSpy);
// Undo again and perform a new mergeable action; it should not coalesce with the undone entry.
await undoStack.undo();
sinon.assert.calledTwice(undoSpy);
await undoStack.markUndoableState(domModel, true);
await undoStack.undo();
sinon.assert.calledThrice(undoSpy);
});
});
});