blob: 0e65c6017597ca4fd216fb7e8235caa4780ae2c3 [file] [log] [blame]
// 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 * as Common from '../../core/common/common.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as ComputedStyle from '../../models/computed_style/computed_style.js';
import {renderElementIntoDOM} from '../../testing/DOMHelpers.js';
import {stubNoopSettings} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import type * as TreeOutline from '../../ui/components/tree_outline/tree_outline.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Elements from './elements.js';
async function waitForTraceElement(treeOutline: TreeOutline.TreeOutline.TreeOutline<unknown>): Promise<HTMLElement> {
const element = treeOutline.shadowRoot?.querySelector('devtools-computed-style-trace');
if (element) {
return element;
}
return await new Promise<HTMLElement>(resolve => {
requestAnimationFrame(async () => {
const result = await waitForTraceElement(treeOutline);
resolve(result);
});
});
}
describeWithMockConnection('ComputedStyleWidget', () => {
let computedStyleWidget: Elements.ComputedStyleWidget.ComputedStyleWidget;
beforeEach(() => {
stubNoopSettings();
});
afterEach(() => {
computedStyleWidget.detach();
});
describe('trace element', () => {
function createComputedStyleWidgetForTest(
cssStyleDeclarationType: SDK.CSSStyleDeclaration.Type, cssStyleDeclarationName?: string,
parentRule?: SDK.CSSRule.CSSRule): Elements.ComputedStyleWidget.ComputedStyleWidget {
const node = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.id = 1 as Protocol.DOM.NodeId;
const stubCSSStyle = {
styleSheetId: 'STYLE_SHEET_ID' as Protocol.DOM.StyleSheetId,
cssProperties: [{
name: 'color',
value: 'red',
disabled: false,
implicit: false,
longhandProperties: [],
range: {startLine: 1, startColumn: 4, endLine: 1, endColumn: 20},
text: 'color: red;',
}],
shorthandEntries: [],
} as Protocol.CSS.CSSStyle;
const cssMatchedStyles = sinon.createStubInstance(SDK.CSSMatchedStyles.CSSMatchedStyles, {
node,
propertyState: SDK.CSSMatchedStyles.PropertyState.ACTIVE,
nodeStyles: [
new SDK.CSSStyleDeclaration.CSSStyleDeclaration(
{} as SDK.CSSModel.CSSModel, parentRule ?? null, stubCSSStyle, cssStyleDeclarationType,
cssStyleDeclarationName),
]
});
const computedStyleModel = new ComputedStyle.ComputedStyleModel.ComputedStyleModel(node);
sinon.stub(computedStyleModel, 'fetchComputedStyle').callsFake(() => {
return Promise.resolve({node, computedStyle: new Map([['color', 'red']])});
});
sinon.stub(computedStyleModel, 'cssModel').callsFake(() => {
return sinon.createStubInstance(
SDK.CSSModel.CSSModel, {cachedMatchedCascadeForNode: Promise.resolve(cssMatchedStyles)});
});
const computedStyleWidget = new Elements.ComputedStyleWidget.ComputedStyleWidget();
renderElementIntoDOM(computedStyleWidget);
computedStyleWidget.computedStyleModel = computedStyleModel;
computedStyleWidget.nodeStyle = {node, computedStyle: new Map([['color', 'red']])};
computedStyleWidget.matchedStyles = cssMatchedStyles;
return computedStyleWidget;
}
it('renders colors correctly', async () => {
computedStyleWidget =
createComputedStyleWidgetForTest(SDK.CSSStyleDeclaration.Type.Animation, '--animation-name');
computedStyleWidget.requestUpdate();
await computedStyleWidget.updateComplete;
const treeOutline = computedStyleWidget.contentElement.querySelector('devtools-tree-outline') as
TreeOutline.TreeOutline.TreeOutline<unknown>;
await treeOutline.expandRecursively(2);
const traceElement = await waitForTraceElement(treeOutline);
assert.strictEqual(traceElement?.innerText, 'red');
});
it('renders trace element with correct selector for declarations coming from animations', async () => {
computedStyleWidget =
createComputedStyleWidgetForTest(SDK.CSSStyleDeclaration.Type.Animation, '--animation-name');
computedStyleWidget.requestUpdate();
await computedStyleWidget.updateComplete;
const treeOutline = computedStyleWidget.contentElement.querySelector('devtools-tree-outline') as
TreeOutline.TreeOutline.TreeOutline<unknown>;
await treeOutline.expandRecursively(2);
const traceElement = await waitForTraceElement(treeOutline);
const traceSelector = traceElement.shadowRoot?.querySelector('.trace-selector');
assert.strictEqual(traceSelector?.textContent, '--animation-name animation');
});
it('renders trace element with correct selector for declarations coming from WAAPI animations', async () => {
computedStyleWidget = createComputedStyleWidgetForTest(SDK.CSSStyleDeclaration.Type.Animation);
computedStyleWidget.requestUpdate();
await computedStyleWidget.updateComplete;
const treeOutline = computedStyleWidget.contentElement.querySelector('devtools-tree-outline') as
TreeOutline.TreeOutline.TreeOutline<unknown>;
await treeOutline.expandRecursively(2);
const traceElement = await waitForTraceElement(treeOutline);
const traceSelector = traceElement.shadowRoot?.querySelector('.trace-selector');
assert.strictEqual(traceSelector?.textContent, 'animation style');
});
it('renders trace element with correct selector for declarations transitions', async () => {
computedStyleWidget = createComputedStyleWidgetForTest(SDK.CSSStyleDeclaration.Type.Transition);
computedStyleWidget.requestUpdate();
await computedStyleWidget.updateComplete;
const treeOutline = computedStyleWidget.contentElement.querySelector('devtools-tree-outline') as
TreeOutline.TreeOutline.TreeOutline<unknown>;
await treeOutline.expandRecursively(2);
const traceElement = await waitForTraceElement(treeOutline);
const traceSelector = traceElement.shadowRoot?.querySelector('.trace-selector');
assert.strictEqual(traceSelector?.textContent, 'transitions style');
});
it('renders trace element with correct selector for declarations coming from CSS rules', async () => {
computedStyleWidget = createComputedStyleWidgetForTest(
SDK.CSSStyleDeclaration.Type.Regular, undefined,
SDK.CSSRule.CSSStyleRule.createDummyRule({} as SDK.CSSModel.CSSModel, '.container'));
computedStyleWidget.requestUpdate();
await computedStyleWidget.updateComplete;
const treeOutline = computedStyleWidget.contentElement.querySelector('devtools-tree-outline') as
TreeOutline.TreeOutline.TreeOutline<unknown>;
await treeOutline.expandRecursively(5);
const traceElement = await waitForTraceElement(treeOutline);
const traceSelector = traceElement.shadowRoot?.querySelector('.trace-selector');
assert.strictEqual(traceSelector?.textContent, '.container');
});
it('renders trace element with correct selector for declarations coming from inline styles', async () => {
computedStyleWidget = createComputedStyleWidgetForTest(SDK.CSSStyleDeclaration.Type.Inline);
computedStyleWidget.requestUpdate();
await computedStyleWidget.updateComplete;
const treeOutline = computedStyleWidget.contentElement.querySelector('devtools-tree-outline') as
TreeOutline.TreeOutline.TreeOutline<unknown>;
await treeOutline.expandRecursively(5);
const traceElement = await waitForTraceElement(treeOutline);
const traceSelector = traceElement.shadowRoot?.querySelector('.trace-selector');
assert.strictEqual(traceSelector?.textContent, 'element.style');
});
});
describe('regex filter toggle', () => {
function createWidgetWithMultipleProperties(
properties: Map<string, string>,
): Elements.ComputedStyleWidget.ComputedStyleWidget {
Common.Settings.Settings.instance().createSetting('group-computed-styles', false).set(false);
const node = sinon.createStubInstance(SDK.DOMModel.DOMNode);
node.id = 1 as Protocol.DOM.NodeId;
const cssMatchedStyles = sinon.createStubInstance(SDK.CSSMatchedStyles.CSSMatchedStyles, {
node,
propertyState: SDK.CSSMatchedStyles.PropertyState.ACTIVE,
nodeStyles: [],
});
const computedStyleModel = new ComputedStyle.ComputedStyleModel.ComputedStyleModel(node);
sinon.stub(computedStyleModel, 'fetchComputedStyle').callsFake(() => {
return Promise.resolve({node, computedStyle: properties});
});
sinon.stub(computedStyleModel, 'cssModel').callsFake(() => {
return sinon.createStubInstance(
SDK.CSSModel.CSSModel, {cachedMatchedCascadeForNode: Promise.resolve(cssMatchedStyles)});
});
const widget = new Elements.ComputedStyleWidget.ComputedStyleWidget();
widget.computedStyleModel = computedStyleModel;
widget.nodeStyle = {node, computedStyle: properties};
widget.matchedStyles = cssMatchedStyles;
renderElementIntoDOM(widget);
return widget;
}
it('renders a regex toggle button that is off by default', async () => {
const properties = new Map([
['display', 'block'],
['height', '100px'],
]);
computedStyleWidget = createWidgetWithMultipleProperties(properties);
computedStyleWidget.requestUpdate();
await computedStyleWidget.updateComplete;
await UI.Widget.Widget.allUpdatesComplete;
const regexButton = computedStyleWidget.contentElement.querySelector('devtools-button');
assert.exists(regexButton);
});
it('filters with plain text by default (pipe is literal, not OR)', async () => {
const properties = new Map([
['display', 'block'],
['height', '100px'],
['width', '200px'],
]);
computedStyleWidget = createWidgetWithMultipleProperties(properties);
computedStyleWidget.requestUpdate();
await computedStyleWidget.updateComplete;
// In plain text mode, "display|height" is escaped and treated as a literal string.
// No property name contains the literal character "|", so nothing matches.
await computedStyleWidget.filterComputedStyles(new RegExp('display\\|height', 'i'));
const treeOutline = computedStyleWidget.contentElement.querySelector('devtools-tree-outline') as
TreeOutline.TreeOutline.TreeOutline<unknown>;
assert.lengthOf(treeOutline.data.tree, 0);
});
it('filters with regex OR when given a real regex', async () => {
const properties = new Map([
['display', 'block'],
['height', '100px'],
['width', '200px'],
]);
computedStyleWidget = createWidgetWithMultipleProperties(properties);
computedStyleWidget.requestUpdate();
await computedStyleWidget.updateComplete;
// When regex mode is on, "display|width" is a real regex OR.
await computedStyleWidget.filterComputedStyles(new RegExp('display|width', 'i'));
const treeOutline = computedStyleWidget.contentElement.querySelector('devtools-tree-outline') as
TreeOutline.TreeOutline.TreeOutline<unknown>;
type TreeNodeData = {tag: 'property', propertyName: string}|{tag: 'category'};
const matchedPropertyNames: string[] = [];
for (const node of treeOutline.data.tree) {
const data = node.treeNodeData as TreeNodeData;
if (data.tag === 'property') {
matchedPropertyNames.push(data.propertyName);
continue;
}
if (data.tag === 'category' && node.children) {
const children = await node.children();
for (const child of children) {
const childData = child.treeNodeData as TreeNodeData;
if (childData.tag === 'property') {
matchedPropertyNames.push(childData.propertyName);
}
}
}
}
assert.sameMembers(matchedPropertyNames, ['display', 'width']);
});
});
});