blob: c750facc095d5ea11e0b5e369a85bee77885b535 [file] [edit]
// Copyright 2020 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 type * as puppeteer from 'puppeteer-core';
import {expectError} from '../../conductor/events.js';
import {
editQueryRuleText,
elementWithPartialText,
expandSelectedNodeRecursively,
focusCSSPropertyValue,
getComputedStylesForDomNode,
getCSSPropertyInRule,
getDisplayedCSSDeclarations,
getDisplayedCSSPropertyNames,
getDisplayedStyleRules,
getDisplayedStyleRulesCompact,
getGhostText,
getGhostTextInCurrentTextPrompt,
getLastAidaRequest,
getMultilineGhostElements,
getStyleRule,
getStyleRuleSelector,
getStyleSectionSubtitles,
GHOST_ROW_SELECTOR,
GHOST_VALUE_PREDICTION_SELECTOR,
goToResourceAndWaitForStyleSection,
mockAidaCodeComplete,
TEXT_PROMPT_GHOST_TEXT_SELECTOR,
waitForAndClickTreeElementWithPartialText,
waitForChildrenOfSelectedElementNode,
waitForContentOfSelectedElementsNode,
waitForCSSPropertyValue,
waitForElementsStyleSection,
waitForPartialContentOfSelectedElementsNode,
waitForPropertyToHighlight,
waitForStyleRule,
} from '../helpers/elements-helpers.js';
import {openPanelViaMoreTools} from '../helpers/settings-helpers.js';
import {expectVeEvents, veImpression, veImpressionsUnder} from '../helpers/visual-logging-helpers.js';
import type {DevToolsPage} from '../shared/frontend-helper.js';
import type {InspectedPage} from '../shared/target-helper.js';
const PROPERTIES_TO_DELETE_SELECTOR = '#properties-to-delete';
const PROPERTIES_TO_INSPECT_SELECTOR = '#properties-to-inspect';
const KEYFRAMES_100_PERCENT_RULE_SELECTOR = '100%';
const FIRST_PROPERTY_NAME_SELECTOR = '.tree-outline li:nth-of-type(1) > .webkit-css-property';
const SECOND_PROPERTY_NAME_SELECTOR = '.tree-outline li:nth-of-type(2) > .webkit-css-property';
const FIRST_PROPERTY_VALUE_SELECTOR = '.tree-outline li:nth-of-type(1) > .value';
const RULE1_SELECTOR = '.rule1';
const RULE2_SELECTOR = '.rule2';
const LAYER_SEPARATOR_SELECTOR = '.layer-separator';
const SIDEBAR_SEPARATOR_SELECTOR = '.sidebar-separator';
const prepareElementsTab = async (devToolsPage: DevToolsPage) => {
await waitForElementsStyleSection(devToolsPage, null);
await expandSelectedNodeRecursively(devToolsPage);
};
const deletePropertyByBackspace =
async (devToolsPage: DevToolsPage, selector: string, root?: puppeteer.ElementHandle<Element>) => {
await devToolsPage.click(selector, {root});
await devToolsPage.page.keyboard.press('Backspace');
await devToolsPage.page.keyboard.press('Tab');
await devToolsPage.waitFor('.tree-outline .child-editing', root);
};
describe('The Styles pane', () => {
it('can show overridden shorthands as inactive (ported layout test)', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-shorthand-override.html');
await prepareElementsTab(devToolsPage);
await waitForStyleRule(devToolsPage, 'body');
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"inspected1"');
await waitForStyleRule(devToolsPage, '#inspected1');
const inspected1Rules = await getDisplayedStyleRules(devToolsPage);
const expectedInspected1Rules = [
{selectorText: 'element.style', propertyData: []},
{
selectorText: '#inspected1',
propertyData: [
{propertyName: 'margin-top', isOverLoaded: true, isInherited: false},
{propertyName: 'margin', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-top', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-right', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-bottom', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-left', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: 'div',
propertyData: [
{propertyName: 'display', isOverLoaded: false, isInherited: false},
{propertyName: 'unicode-bidi', isOverLoaded: false, isInherited: false},
],
},
];
assert.deepEqual(inspected1Rules, expectedInspected1Rules);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"inspected2"');
await waitForStyleRule(devToolsPage, '#inspected2');
const inspected2Rules = await getDisplayedStyleRules(devToolsPage);
const expectedInspected2Rules = [
{selectorText: 'element.style', propertyData: []},
{
selectorText: '#inspected2',
propertyData: [
{propertyName: 'padding', isOverLoaded: true, isInherited: false},
{propertyName: 'padding-top', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-right', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-bottom', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-left', isOverLoaded: false, isInherited: false},
{propertyName: 'padding', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-top', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-right', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-bottom', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-left', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: 'div',
propertyData: [
{propertyName: 'display', isOverLoaded: false, isInherited: false},
{propertyName: 'unicode-bidi', isOverLoaded: false, isInherited: false},
],
},
];
assert.deepEqual(inspected2Rules, expectedInspected2Rules);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"inspected3"');
await waitForStyleRule(devToolsPage, '#inspected3');
const inspected3Rules = await getDisplayedStyleRules(devToolsPage);
const expectedInspected3Rules = [
{
selectorText: 'element.style',
propertyData: [],
},
{
selectorText: '#inspected3',
propertyData: [
{
propertyName: 'border-width',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-top-width',
isOverLoaded: true,
isInherited: false,
},
{
propertyName: 'border-right-width',
isOverLoaded: true,
isInherited: false,
},
{
propertyName: 'border-bottom-width',
isOverLoaded: true,
isInherited: false,
},
{
propertyName: 'border-left-width',
isOverLoaded: true,
isInherited: false,
},
{
propertyName: 'border',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-top-width',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-right-width',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-bottom-width',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-left-width',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-top-style',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-right-style',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-bottom-style',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-left-style',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-top-color',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-right-color',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-bottom-color',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-left-color',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-image-source',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-image-slice',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-image-width',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-image-outset',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'border-image-repeat',
isOverLoaded: false,
isInherited: false,
},
],
},
{
selectorText: 'div',
propertyData: [
{
propertyName: 'display',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'unicode-bidi',
isOverLoaded: false,
isInherited: false,
},
],
},
];
assert.deepEqual(inspected3Rules, expectedInspected3Rules);
});
it('can display the CSS properties of the selected element', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/simple-styled-page.html');
await prepareElementsTab(devToolsPage);
const onH1RuleAppeared = waitForStyleRule(devToolsPage, 'h1');
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<h1>');
await onH1RuleAppeared;
await devToolsPage.waitForFunction(async () => (await getDisplayedStyleRules(devToolsPage)).length === 4);
const h1Rules = await getDisplayedStyleRules(devToolsPage);
// Waiting for the first h1 rule, that's the authored rule, right after the element style.
assert.deepEqual(h1Rules[1], {
selectorText: 'body h1',
propertyData: [{propertyName: 'color', isOverLoaded: false, isInherited: false}],
});
const onH2RuleAppeared = waitForStyleRule(devToolsPage, 'h2');
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<h2>');
await onH2RuleAppeared;
await devToolsPage.waitForFunction(async () => (await getDisplayedStyleRules(devToolsPage)).length === 3);
// Waiting for the first h2 rule, that's the authored rule, right after the element style.
const h2Rules = await getDisplayedStyleRules(devToolsPage);
assert.deepEqual(h2Rules[1], {
selectorText: 'h2',
propertyData: [
{propertyName: 'background-color', isOverLoaded: false, isInherited: false},
{propertyName: 'color', isOverLoaded: false, isInherited: false},
],
});
});
it('can jump to a CSS variable definition', async ({devToolsPage, inspectedPage}) => {
await devToolsPage.enableAnimations();
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-variables.html');
// Select div that we will inspect the CSS variables for
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'properties-to-inspect');
await waitForContentOfSelectedElementsNode(devToolsPage,
'<div id=\u200B"properties-to-inspect">\u200B</div>\u200B');
const testElementRule = await getStyleRule(devToolsPage, PROPERTIES_TO_INSPECT_SELECTOR);
await devToolsPage.click('.link-swatch-link', {root: testElementRule});
await waitForPropertyToHighlight(devToolsPage, 'html', '--title-color');
});
it('can jump to an unexpanded CSS variable definition', async ({devToolsPage, inspectedPage}) => {
await devToolsPage.enableAnimations();
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-variables-many.html');
// Select div that we will inspect the CSS variables for
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'properties-to-inspect');
await waitForContentOfSelectedElementsNode(devToolsPage,
'<div id=\u200B"properties-to-inspect">\u200B</div>\u200B');
const testElementRule = await getStyleRule(devToolsPage, PROPERTIES_TO_INSPECT_SELECTOR);
await devToolsPage.click('.link-swatch-link', {root: testElementRule});
await waitForPropertyToHighlight(devToolsPage, 'html', '--color56');
});
it('displays the correct value when editing CSS var() functions', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-variables.html');
// Select div that we will inspect the CSS variables for
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'properties-to-inspect');
await waitForContentOfSelectedElementsNode(devToolsPage,
'<div id=\u200B"properties-to-inspect">\u200B</div>\u200B');
const propertiesSection = await getStyleRule(devToolsPage, PROPERTIES_TO_INSPECT_SELECTOR);
const propertyValue = await devToolsPage.waitFor(FIRST_PROPERTY_VALUE_SELECTOR, propertiesSection);
// Specifying 10px from the left of the value to click on the word var rather than in the middle which would jump to
// the property definition.
await propertyValue.click();
const editedValueText = await propertyValue.evaluate(node => (node as HTMLElement).innerText);
assert.strictEqual(editedValueText, 'var(--title-color)', 'The value is incorrect when being edited');
});
it('generates links inside var() functions for defined properties', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-variables.html');
// Select div that we will inspect the CSS variables for
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'properties-to-inspect');
await waitForContentOfSelectedElementsNode(devToolsPage,
'<div id=\u200B"properties-to-inspect">\u200B</div>\u200B');
const propertiesSection = await getStyleRule(devToolsPage, PROPERTIES_TO_INSPECT_SELECTOR);
const propertyValue = await devToolsPage.waitFor(FIRST_PROPERTY_VALUE_SELECTOR, propertiesSection);
const link = await devToolsPage.$$('.link-swatch-link', propertyValue);
assert.lengthOf(link, 1, 'The expected var link was not created');
});
it('renders computed CSS variables in @keyframes rules', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-variables.html');
// Select div that we will inspect the CSS variables for
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'keyframes-rule');
await waitForContentOfSelectedElementsNode(devToolsPage, '<div id=\u200B"keyframes-rule">\u200B</div>\u200B');
const propertiesSection = await getStyleRule(devToolsPage, KEYFRAMES_100_PERCENT_RULE_SELECTOR);
const propertyValue = await devToolsPage.waitFor(FIRST_PROPERTY_VALUE_SELECTOR, propertiesSection);
const propertyValueText = await propertyValue.evaluate(node => (node as HTMLElement).innerText);
assert.strictEqual(
propertyValueText, 'var(--move-final-width)', 'CSS variable in @keyframes rule is not correctly rendered');
});
it('Shows a CSS hint popover', async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
body {
grid-column-gap: 4px;
}
</style>`);
await waitForElementsStyleSection(devToolsPage, undefined);
await devToolsPage.hover('.hint-wrapper');
const infobox = await devToolsPage.waitFor(':popover-open');
const textContent: string = await infobox.evaluate(e => e.deepInnerText());
assert.strictEqual(
textContent,
'The display: block property prevents grid-column-gap from having an effect.\nTry setting display to something other than block.');
await expectVeEvents(
devToolsPage,
[veImpressionsUnder(
'Panel: elements > Pane: styles > Section: style-properties > Tree > TreeItem: grid-column-gap',
[veImpression('Popover', 'elements.css-hint')])],
undefined);
});
it('Shows a syntax error popover for registered property', async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
body {
--color: 2px;
}
@property --color {
syntax: "<color>";
inherits: false;
initial-value: green;
}
</style>`);
await waitForElementsStyleSection(devToolsPage, undefined);
await waitForStyleRule(devToolsPage, '--color');
const bodySection = await getStyleRule(devToolsPage, 'body');
const isCollapsed = await bodySection.evaluate(section => section.classList.contains('collapsed'));
if (isCollapsed) {
await devToolsPage.click('.section-collapse-icon', {root: bodySection});
}
await devToolsPage.hover('.exclamation-mark', {root: bodySection});
const infobox = await devToolsPage.waitFor(':popover-open');
const textContent: string = await infobox.evaluate(e => e.deepInnerText());
assert.strictEqual(
textContent.replaceAll(/\s+/g, ' ').trim(),
'Invalid property value, expected type "<color>" View registered property');
await expectVeEvents(
devToolsPage,
[veImpressionsUnder(
'Panel: elements > Pane: styles > Section: style-properties > Tree > TreeItem: custom-property',
[veImpression('Popover', 'elements.invalid-property-decl-popover')])],
undefined);
});
it('shows variable values in a popover for property values', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-variables.html');
// Select div that we will inspect the CSS variables for
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'properties-to-inspect');
await waitForContentOfSelectedElementsNode(devToolsPage,
'<div id=\u200B"properties-to-inspect">\u200B</div>\u200B');
const testElementRule = await getStyleRule(devToolsPage, PROPERTIES_TO_INSPECT_SELECTOR);
await devToolsPage.hover('.link-swatch-link', {root: testElementRule});
const infobox = await devToolsPage.waitFor('[aria-label="CSS property value: var(--title-color)"] :popover-open');
const textContent = await infobox.evaluate(e => e.deepInnerText());
assert.strictEqual(textContent.trim(), 'black');
await expectVeEvents(
devToolsPage,
[veImpressionsUnder(
'Panel: elements > Pane: styles > Section: style-properties > Tree > TreeItem: color > Value > Link: css-variable',
[veImpression('Popover', 'elements.css-var')])],
undefined);
});
it('shows variable values in a popover for property names', async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
body {
--color: red;
}
</style>`);
await waitForElementsStyleSection(devToolsPage, undefined);
await devToolsPage.hover('aria/CSS property name: --color');
const infobox = await devToolsPage.waitFor('.tree-outline :popover-open');
const textContent = await infobox.evaluate(e => e.deepInnerText());
assert.strictEqual(textContent.trim(), 'red');
await expectVeEvents(
devToolsPage,
[veImpressionsUnder(
'Panel: elements > Pane: styles > Section: style-properties > Tree > TreeItem: custom-property > Key',
[veImpression('Popover', 'elements.css-var')])],
undefined);
});
it('shows mixed colors in a popover', async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
body {
color: color-mix(in srgb, red, blue);
}
</style>`);
await waitForElementsStyleSection(devToolsPage, undefined);
await devToolsPage.hover('devtools-color-mix-swatch');
const infobox =
await devToolsPage.waitFor('[aria-label="CSS property value: color-mix(in srgb, red, blue)"] :popover-open');
const textContent = await infobox.evaluate(e => e.deepInnerText());
assert.strictEqual(textContent.trim(), '#800080');
await expectVeEvents(
devToolsPage, [veImpressionsUnder(
'Panel: elements > Pane: styles > Section: style-properties > Tree > TreeItem: color > Value',
[veImpression('Popover', 'elements.css-color-mix')])],
undefined);
});
it('shows absolute length units in a popover', async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
body {
width: 1em;
}
</style>`);
await waitForElementsStyleSection(devToolsPage, undefined);
await devToolsPage.hover('text/1em', {root: await devToolsPage.waitForAria('CSS property value: 1em')});
const infobox = await devToolsPage.waitFor('[aria-label="CSS property value: 1em"] :popover-open');
const textContent = await infobox.evaluate(e => e.deepInnerText());
assert.strictEqual(textContent.trim(), '16px');
await expectVeEvents(
devToolsPage, [veImpressionsUnder(
'Panel: elements > Pane: styles > Section: style-properties > Tree > TreeItem: width > Value',
[veImpression('Popover', 'length-popover')])],
undefined);
});
it('can remove a CSS property when its name or value is deleted', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/style-pane-properties.html');
// Select div that we will remove the CSS properties from
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'properties-to-delete');
await waitForContentOfSelectedElementsNode(devToolsPage, '<div id=\u200B"properties-to-delete">\u200B</div>\u200B');
const propertiesSection = await getStyleRule(devToolsPage, PROPERTIES_TO_DELETE_SELECTOR);
{
const displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, propertiesSection);
assert.deepEqual(
displayedNames,
[
'height',
'width',
],
'incorrectly displayed style after initialization');
}
// select second property's name and delete
await deletePropertyByBackspace(devToolsPage, SECOND_PROPERTY_NAME_SELECTOR, propertiesSection);
// verify the second CSS property entry has been removed
{
const displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, propertiesSection);
assert.deepEqual(
displayedNames,
[
'height',
],
'incorrectly displayed style after removing second property\'s value');
}
// select first property's name and delete
await deletePropertyByBackspace(devToolsPage, FIRST_PROPERTY_NAME_SELECTOR, propertiesSection);
// verify the first CSS property entry has been removed
{
const displayedValues = await getDisplayedCSSPropertyNames(devToolsPage, propertiesSection);
assert.deepEqual(displayedValues, [], 'incorrectly displayed style after removing first property\'s name');
}
});
it('can display the source names for stylesheets', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage,
'elements/stylesheets-with-various-sources.html');
// Select the div element by pressing down, since <body> is the default selected element.
const onDivRuleAppeared = waitForStyleRule(devToolsPage, 'div');
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<div');
await onDivRuleAppeared;
const subtitles = await getStyleSectionSubtitles(devToolsPage);
assert.sameDeepMembers(
subtitles,
[
'',
'css-module.css:7',
'constructed stylesheet',
'stylesheets…ces.html:10',
'stylesheets…rces.html:7',
'user agent stylesheet',
],
'incorrectly displayed style sources');
const divRules = await getDisplayedStyleRules(devToolsPage);
assert.deepEqual(
divRules,
[
{selectorText: 'element.style', propertyData: []},
{
selectorText: '#properties-to-inspect',
propertyData: [{propertyName: 'height', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '#properties-to-inspect',
propertyData: [{propertyName: 'color', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '#properties-to-inspect',
propertyData: [{propertyName: 'text-align', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '#properties-to-inspect',
propertyData: [{propertyName: 'width', isOverLoaded: false, isInherited: false}],
},
{
selectorText: 'div',
propertyData: [
{propertyName: 'display', isOverLoaded: false, isInherited: false},
{propertyName: 'unicode-bidi', isOverLoaded: false, isInherited: false},
],
},
],
'The correct rule is displayed');
});
it('can edit multiple constructed stylesheets', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage,
'elements/multiple-constructed-stylesheets.html');
// Select div that we will remove a CSS property from.
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<div class=\u200B"rule1 rule2">\u200B</div>\u200B');
await waitForContentOfSelectedElementsNode(devToolsPage, '<div class=\u200B"rule1 rule2">\u200B</div>\u200B');
// Verify that initial CSS properties correspond to the ones in the test file.
const rule1PropertiesSection = await getStyleRule(devToolsPage, RULE1_SELECTOR);
const rule2PropertiesSection = await getStyleRule(devToolsPage, RULE2_SELECTOR);
{
const displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, rule1PropertiesSection);
assert.deepEqual(
displayedNames,
[
'background-color',
],
'incorrectly displayed style after initialization');
}
{
const displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, rule2PropertiesSection);
assert.deepEqual(
displayedNames,
[
'background-color',
'color',
],
'incorrectly displayed style after initialization');
}
// Select the first property's name of .rule2 (background-color) and delete.
await deletePropertyByBackspace(devToolsPage, FIRST_PROPERTY_NAME_SELECTOR, rule2PropertiesSection);
// Verify that .rule1 has background-color.
{
const displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, rule1PropertiesSection);
assert.deepEqual(
displayedNames,
[
'background-color',
],
'incorrectly displayed style after property removal');
}
// Verify that .rule2 has background-color removed and only color remains.
{
const displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, rule2PropertiesSection);
assert.deepEqual(
displayedNames,
[
'color',
],
'incorrectly displayed style after property removal');
}
// Verify that computed styles correspond to the changes made.
const computedStyles = [
await getComputedStylesForDomNode(inspectedPage, RULE1_SELECTOR, 'color'),
await getComputedStylesForDomNode(inspectedPage, RULE1_SELECTOR, 'backgroundColor'),
];
assert.deepEqual(computedStyles, ['rgb(255, 0, 0)', 'rgb(255, 0, 0)'], 'Styles are not correct after the update');
});
it('can display and edit container queries', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-container-queries.html');
// Select the child that has container queries.
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<div class=\u200B"rule1 rule2">\u200B</div>\u200B');
await waitForContentOfSelectedElementsNode(devToolsPage, '<div class=\u200B"rule1 rule2">\u200B</div>\u200B');
// Verify that initial CSS properties correspond to the ones in the test file.
const rule1PropertiesSection = await getStyleRule(devToolsPage, RULE1_SELECTOR);
let rule2PropertiesSection = await getStyleRule(devToolsPage, RULE2_SELECTOR);
{
const displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, rule1PropertiesSection);
assert.deepEqual(
displayedNames,
[
'width',
],
'incorrectly displayed style after initialization');
}
{
const displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, rule2PropertiesSection);
assert.deepEqual(
displayedNames,
[
'height',
],
'incorrectly displayed style after initialization');
}
await editQueryRuleText(devToolsPage, rule1PropertiesSection, '(min-width: 300px)', true);
// This can cause the styles pane to be regenerated, so we need to get the
// style rule handle again.
rule2PropertiesSection = await getStyleRule(devToolsPage, RULE2_SELECTOR);
await editQueryRuleText(devToolsPage, rule2PropertiesSection, '(max-width: 300px)');
// Verify that computed styles correspond to the changes made.
const computedStyles = [
await getComputedStylesForDomNode(inspectedPage, RULE1_SELECTOR, 'width'),
await getComputedStylesForDomNode(inspectedPage, RULE2_SELECTOR, 'height'),
];
assert.deepEqual(computedStyles, ['0px', '10px'], 'Styles are not correct after the update');
});
it('can display container link', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-container-queries.html');
// Select the child that has container queries.
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<div class=\u200B"rule1 rule2">\u200B</div>\u200B');
await waitForContentOfSelectedElementsNode(devToolsPage, '<div class=\u200B"rule1 rule2">\u200B</div>\u200B');
const rule1PropertiesSection = await getStyleRule(devToolsPage, RULE1_SELECTOR);
const containerLink = await devToolsPage.waitFor('.container-link', rule1PropertiesSection);
const nodeLabelName = await devToolsPage.waitFor('.node-label-name', containerLink);
const nodeLabelNameContent = await nodeLabelName.evaluate(node => node.textContent as string);
assert.strictEqual(nodeLabelNameContent, 'body', 'container link name does not match');
await containerLink.hover();
const queriedSizeDetails = await devToolsPage.waitFor('.queried-size-details');
const queriedSizeDetailsContent = await queriedSizeDetails.evaluate(node => (node as HTMLElement).innerText);
assert.strictEqual(
queriedSizeDetailsContent, '(size) width: 200px height: 0px', 'container queried details does not match');
});
it('can display @supports at-rules', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-supports.html');
// Select the child that has @supports rules.
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<div class=\u200B"rule1">\u200B</div>\u200B');
await waitForContentOfSelectedElementsNode(devToolsPage, '<div class=\u200B"rule1">\u200B</div>\u200B');
const rule1PropertiesSection = await getStyleRule(devToolsPage, RULE1_SELECTOR);
const supportsQuery = await devToolsPage.waitFor('.query.editable', rule1PropertiesSection);
const supportsQueryText = await supportsQuery.evaluate(node => (node as HTMLElement).innerText);
assert.deepEqual(supportsQueryText, '@supports (width: 10px) {', 'incorrectly displayed @supports rule');
});
it('can display @layer separators', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-layers.html');
// Select the child that has @layer rules.
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<div class=\u200B"rule1">\u200B</div>\u200B');
await waitForContentOfSelectedElementsNode(devToolsPage, '<div class=\u200B"rule1">\u200B</div>\u200B');
const layerSeparators = await devToolsPage.waitForFunction(async () => {
const layers = await devToolsPage.$$(LAYER_SEPARATOR_SELECTOR);
return layers.length === 6 ? layers : null;
});
const layerText = await Promise.all(layerSeparators.map(element => element.evaluate(node => node.textContent)));
assert.deepEqual(layerText, [
'Layer<anonymous>',
'Layerimportant',
'Layeroverrule',
'Layeroverrule.<anonymous>',
'Layerbase',
'Layer\xa0user\xa0agent\xa0stylesheet',
]);
});
it('can click @layer separators to open layer tree', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-layers.html');
// Select the child that has @layer rules.
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<div class=\u200B"rule1">\u200B</div>\u200B');
await waitForContentOfSelectedElementsNode(devToolsPage, '<div class=\u200B"rule1">\u200B</div>\u200B');
await devToolsPage.click('aria/overrule[role="button"]');
const treeElement = await devToolsPage.waitFor('[data-node-key="2: overrule"]');
assert.isOk(treeElement);
});
it('can display inherited CSS highlight pseudo styles', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/highlight-pseudo-inheritance.html');
const onH1RuleAppeared = waitForStyleRule(devToolsPage, 'h1');
// Select the h1 for which we will inspect the pseudo styles
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<h1');
await onH1RuleAppeared;
const h1Rules = await getDisplayedStyleRules(devToolsPage);
// The 12 rule blocks for the h1 are:
// 1. Inline styles from the style attribute
// 2. The h1's user agent styles
// 3. Styles that the h1 inherits from the body
// 4. The h1's own ::before pseudo
// 5. The h1's own ::after pseudo
// 6-7. The h1's own highlight(bar) pseudo
// 8. The h1's inherited highlight(bar) pseudo
// 9-10. The h1's own highlight(foo) pseudo
// 11. The h1's own selection pseudo
// 12. The h1's inherited selection pseudo
// And there is no 13th block for the ::first-letter style, since only
// highlight pseudos are inherited.
assert.lengthOf(h1Rules, 12, 'The h1 should have 12 style rule blocks');
assert.deepEqual(
h1Rules[2], {
selectorText: 'body',
propertyData: [
{propertyName: 'color', isOverLoaded: false, isInherited: false},
{propertyName: 'background-color', isOverLoaded: false, isInherited: true},
],
},
'The inherited styles from the body are displayed');
assert.deepEqual(
h1Rules[3], {
selectorText: 'h1::before',
propertyData: [{propertyName: 'content', isOverLoaded: false, isInherited: false}],
},
'The h1\'s own ::before pseudo is displayed');
assert.deepEqual(
h1Rules[4], {
selectorText: 'h1::after',
propertyData: [{propertyName: 'content', isOverLoaded: false, isInherited: false}],
},
'The h1\'s own ::after pseudo is displayed');
assert.deepEqual(
h1Rules[5], {
selectorText: 'h1::highlight(bar)',
propertyData: [{propertyName: 'background-color', isOverLoaded: false, isInherited: false}],
},
'The h1\'s own highlight(bar) pseudo is displayed (1)');
assert.deepEqual(
h1Rules[6], {
selectorText: 'h1::highlight(foo), h1::highlight(bar)',
propertyData: [{propertyName: 'color', isOverLoaded: false, isInherited: false}],
},
'The h1\'s own highlight(bar) pseudo is displayed (2)');
assert.deepEqual(
h1Rules[7], {
selectorText: 'body::highlight(bar)',
propertyData: [
{propertyName: 'color', isOverLoaded: true, isInherited: false},
{propertyName: 'background-color', isOverLoaded: true, isInherited: false},
],
},
'The h1\'s inherited highlight(bar) pseudo is displayed');
assert.deepEqual(
h1Rules[8], {
selectorText: 'h1::highlight(foo)',
propertyData: [{propertyName: 'background-color', isOverLoaded: false, isInherited: false}],
},
'The h1\'s own highlight(foo) pseudo is displayed (1)');
assert.deepEqual(
h1Rules[9], {
selectorText: 'h1::highlight(foo), h1::highlight(bar)',
propertyData: [{propertyName: 'color', isOverLoaded: false, isInherited: false}],
},
'The h1\'s own highlight(foo) pseudo is displayed (2)');
assert.deepEqual(
h1Rules[10], {
selectorText: 'h1::selection',
propertyData: [{propertyName: 'background-color', isOverLoaded: false, isInherited: false}],
},
'The h1\'s own selection pseudo is displayed');
assert.deepEqual(
h1Rules[11], {
selectorText: 'body::selection',
propertyData: [
{propertyName: 'text-shadow', isOverLoaded: false, isInherited: false},
{propertyName: 'background-color', isOverLoaded: true, isInherited: false},
],
},
'The h1\'s inherited selection pseudo is displayed');
const sidebarSeparators = await devToolsPage.waitForFunction(async () => {
const separators = await devToolsPage.$$(SIDEBAR_SEPARATOR_SELECTOR);
return separators.length === 8 ? separators : null;
});
const layerText = await Promise.all(sidebarSeparators.map(element => element.evaluate(node => node.textContent)));
assert.deepEqual(layerText, [
'Inherited from ',
'Pseudo ::before element',
'Pseudo ::after element',
'Pseudo ::highlight(bar) element',
'Inherited from ::highlight(bar) pseudo of ',
'Pseudo ::highlight(foo) element',
'Pseudo ::selection element',
'Inherited from ::selection pseudo of ',
]);
});
it('can show styles properly (ported layout test)', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/elements-panel-styles.html');
await prepareElementsTab(devToolsPage);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"container"');
await waitForStyleRule(devToolsPage, '#container');
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"foo"');
await waitForStyleRule(devToolsPage, '.foo');
const fooRules = await getDisplayedStyleRules(devToolsPage);
const expected = [
{
selectorText: 'element.style',
propertyData: [
{propertyName: 'display', isOverLoaded: true, isInherited: false},
{propertyName: '-webkit-font-smoothing', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '#container .foo',
propertyData: [{propertyName: 'font-style', isOverLoaded: false, isInherited: false}],
},
{
selectorText: 'body .foo',
propertyData: [{propertyName: 'text-indent', isOverLoaded: true, isInherited: false}],
},
{selectorText: '.foo', propertyData: []},
{
selectorText: '.foo, .foo::before',
propertyData: [
{propertyName: 'content', isOverLoaded: false, isInherited: false},
{propertyName: 'color', isOverLoaded: false, isInherited: false},
{propertyName: 'display', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '.foo',
propertyData: [
{propertyName: 'display', isOverLoaded: true, isInherited: false},
{propertyName: 'color', isOverLoaded: true, isInherited: false},
{propertyName: 'margin-left', isOverLoaded: true, isInherited: false},
{propertyName: 'margin', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-top', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-right', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-bottom', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-left', isOverLoaded: false, isInherited: false},
{propertyName: 'border-radius', isOverLoaded: false, isInherited: false},
{propertyName: 'border-top-left-radius', isOverLoaded: false, isInherited: false},
{propertyName: 'border-top-right-radius', isOverLoaded: false, isInherited: false},
{propertyName: 'border-bottom-right-radius', isOverLoaded: false, isInherited: false},
{propertyName: 'border-bottom-left-radius', isOverLoaded: false, isInherited: false},
{propertyName: 'font-style', isOverLoaded: true, isInherited: false},
{propertyName: 'font-weight', isOverLoaded: false, isInherited: false},
{propertyName: 'font-weight', isOverLoaded: true, isInherited: false},
{propertyName: 'padding', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-top', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-right', isOverLoaded: true, isInherited: false},
{propertyName: 'padding-bottom', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-left', isOverLoaded: false, isInherited: false},
{propertyName: 'padding-right', isOverLoaded: false, isInherited: false},
{propertyName: 'text-indent', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: 'div[Attributes Style]',
propertyData: [{propertyName: 'text-align', isOverLoaded: false, isInherited: false}],
},
{
selectorText: 'div',
propertyData: [
{propertyName: 'display', isOverLoaded: true, isInherited: false},
{propertyName: 'unicode-bidi', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '#container',
propertyData: [
{propertyName: 'font-family', isOverLoaded: false, isInherited: false},
{propertyName: 'font-size', isOverLoaded: false, isInherited: false},
{propertyName: 'color', isOverLoaded: true, isInherited: false},
{propertyName: 'padding', isOverLoaded: false, isInherited: true},
{propertyName: 'padding-top', isOverLoaded: false, isInherited: true},
{propertyName: 'padding-right', isOverLoaded: false, isInherited: true},
{propertyName: 'padding-bottom', isOverLoaded: false, isInherited: true},
{propertyName: 'padding-left', isOverLoaded: false, isInherited: true},
],
},
{
selectorText: 'body',
propertyData: [
{propertyName: 'font-size', isOverLoaded: true, isInherited: false},
{propertyName: 'text-indent', isOverLoaded: true, isInherited: false},
],
},
{selectorText: 'html', propertyData: [{propertyName: 'color', isOverLoaded: true, isInherited: false}]},
{selectorText: '.foo::before', propertyData: []},
{
selectorText: '.foo::before',
propertyData: [{propertyName: 'color', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '.foo, .foo::before',
propertyData: [
{propertyName: 'content', isOverLoaded: false, isInherited: false},
{propertyName: 'color', isOverLoaded: true, isInherited: false},
{propertyName: 'display', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '.foo::after',
propertyData: [
{propertyName: 'font-family', isOverLoaded: false, isInherited: false},
{propertyName: 'content', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '.foo::after',
propertyData: [
{propertyName: 'content', isOverLoaded: true, isInherited: false},
{propertyName: 'color', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '.foo::marker',
propertyData: [
{propertyName: 'content', isOverLoaded: false, isInherited: false},
{propertyName: 'color', isOverLoaded: false, isInherited: false},
],
},
];
assert.deepEqual(fooRules, expected);
});
it('shows longhands overridden by shorthands with var() as inactive (ported layout test)',
async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-longhand-override.html');
await prepareElementsTab(devToolsPage);
await waitForStyleRule(devToolsPage, 'body');
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"inspected"');
await waitForStyleRule(devToolsPage, '#inspected');
const inspectedRules = await getDisplayedStyleRules(devToolsPage);
const expectedInspected1Rules = [
{selectorText: 'element.style', propertyData: []},
{
selectorText: '#inspected',
propertyData: [
{propertyName: 'margin', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-top', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-right', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-bottom', isOverLoaded: false, isInherited: false},
{propertyName: 'margin-left', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: 'div',
propertyData: [{propertyName: 'margin-top', isOverLoaded: true, isInherited: false}],
},
{
selectorText: 'div',
propertyData: [
{propertyName: 'display', isOverLoaded: false, isInherited: false},
{propertyName: 'unicode-bidi', isOverLoaded: false, isInherited: false},
],
},
];
assert.deepEqual(inspectedRules, expectedInspected1Rules);
});
it('shows longhands with parsed values under a shorthand', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-shorthand-override.html');
await waitForStyleRule(devToolsPage, 'body');
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected4');
await waitForStyleRule(devToolsPage, '#inspected4');
const inspectedRules = await getDisplayedCSSDeclarations(devToolsPage);
assert.deepEqual(inspectedRules, [
'margin: 10px;',
'margin-top: 10px;',
'margin-right: 10px;',
'margin-bottom: 10px;',
'margin-left: 10px;',
'margin-left: 20px;',
'display: block;',
'unicode-bidi: isolate;',
]);
});
it('shows overridden properties as inactive (ported layout test)', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-override.html');
await prepareElementsTab(devToolsPage);
await waitForStyleRule(devToolsPage, 'body');
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<div');
await waitForStyleRule(devToolsPage, 'div');
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"inspected"');
await waitForStyleRule(devToolsPage, '#inspected');
const inspectedRules = await getDisplayedStyleRules(devToolsPage);
const expectedInspected1Rules = [
{
selectorText: 'element.style',
propertyData: [],
},
{
selectorText: '#inspected',
propertyData: [
{
propertyName: 'text-align',
isOverLoaded: true,
isInherited: false,
},
{
propertyName: 'text-align',
isOverLoaded: true,
isInherited: false,
},
{
propertyName: 'text-align',
isOverLoaded: false,
isInherited: false,
},
],
},
{
selectorText: 'div[Attributes Style]',
propertyData: [{
propertyName: 'text-align',
isOverLoaded: true,
isInherited: false,
}],
},
{
selectorText: 'div',
propertyData: [
{
propertyName: 'display',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'unicode-bidi',
isOverLoaded: false,
isInherited: false,
},
],
},
];
assert.deepEqual(inspectedRules, expectedInspected1Rules);
});
it('shows non-standard mixed-cased properties correctly (ported layout test)',
async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-mixed-case.html');
await prepareElementsTab(devToolsPage);
await waitForStyleRule(devToolsPage, 'body');
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"container"');
await waitForStyleRule(devToolsPage, '#container');
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"nested"');
await waitForStyleRule(devToolsPage, '#nested');
const inspectedRules = await getDisplayedStyleRules(devToolsPage);
const expectedInspected1Rules = [
{
selectorText: 'element.style',
propertyData: [],
},
{
selectorText: '#nested',
propertyData: [{
propertyName: 'color',
isOverLoaded: false,
isInherited: false,
}],
},
{
selectorText: 'div',
propertyData: [
{
propertyName: 'display',
isOverLoaded: false,
isInherited: false,
},
{
propertyName: 'unicode-bidi',
isOverLoaded: false,
isInherited: false,
},
],
},
{
selectorText: 'style attribute',
propertyData: [{
propertyName: 'CoLoR',
isOverLoaded: true,
isInherited: false,
}],
},
{
selectorText: '#container',
propertyData: [{
propertyName: '-webkit-FONT-smoothing',
isOverLoaded: false,
isInherited: false,
}],
},
];
assert.deepEqual(inspectedRules, expectedInspected1Rules);
});
it('shows styles from injected user stylesheets (ported layout test)', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-inject-stylesheet.html');
await prepareElementsTab(devToolsPage);
await inspectedPage.evaluate(async () => {
const style = document.createElement('style');
style.textContent =
'#main { color: red; border-style: solid; -webkit-border-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==) }';
document.head.append(style);
});
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"main"');
await waitForStyleRule(devToolsPage, '#main');
const inspectedRulesBefore = await getDisplayedStyleRulesCompact(devToolsPage);
const expectedInspectedRulesBefore = [
{
selectorText: 'element.style',
propertyNames: [],
},
{
selectorText: '#main',
propertyNames: [
'color',
'border-style',
'border-top-style',
'border-right-style',
'border-bottom-style',
'border-left-style',
'-webkit-border-image',
],
},
{
selectorText: '#main',
propertyNames: [
'background',
'background-image',
'background-position-x',
'background-position-y',
'background-size',
'background-repeat',
'background-attachment',
'background-origin',
'background-clip',
'background-color',
],
},
{
selectorText: 'div',
propertyNames: ['display', 'unicode-bidi'],
},
];
assert.deepEqual(inspectedRulesBefore, expectedInspectedRulesBefore);
});
it('shows styles from injected user stylesheets for a injected iframe (ported layout test)',
async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-inject-stylesheet.html');
await prepareElementsTab(devToolsPage);
await inspectedPage.evaluate(() => {
const iframe = document.createElement('iframe');
document.getElementById('main')?.appendChild(iframe);
const {promise, resolve} = Promise.withResolvers<void>();
iframe.addEventListener('load', () => {
if (!iframe.contentDocument) {
return;
}
const style = iframe.contentDocument.createElement('style');
style.textContent = '#iframeBody { background: red }';
iframe.contentDocument.head.append(style);
resolve();
});
iframe.src = 'css-inject-stylesheet-iframe-data.html';
return promise;
});
await devToolsPage.waitForFunction(async () => {
await expandSelectedNodeRecursively(devToolsPage);
return await elementWithPartialText(devToolsPage, 'id=\u200B"iframeBody"');
});
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"iframeBody"');
await waitForStyleRule(devToolsPage, '#iframeBody');
const inspectedRulesAfter = await getDisplayedStyleRulesCompact(devToolsPage);
const expectedInspectedRulesAfter = [
{
selectorText: 'element.style',
propertyNames: [],
},
{
selectorText: '#iframeBody',
propertyNames: [
'background',
'background-image',
'background-position-x',
'background-position-y',
'background-size',
'background-repeat',
'background-attachment',
'background-origin',
'background-clip',
'background-color',
],
},
{
selectorText: 'body',
propertyNames: [
'background',
'background-image',
'background-position-x',
'background-position-y',
'background-size',
'background-repeat',
'background-attachment',
'background-origin',
'background-clip',
'background-color',
],
},
{
selectorText: 'body',
propertyNames: ['display', 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left'],
},
];
assert.deepEqual(inspectedRulesAfter, expectedInspectedRulesAfter);
});
it('can parse webkit css region styling (ported layout test)', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-webkit-region.html');
await prepareElementsTab(devToolsPage);
await waitForStyleRule(devToolsPage, 'body');
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"article1"');
await waitForStyleRule(devToolsPage, '#article1');
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"p1"');
await waitForStyleRule(devToolsPage, '#p1');
const inspectedRules = await getDisplayedStyleRulesCompact(devToolsPage);
const expectedInspectedRules = [
{
selectorText: 'element.style',
propertyNames: [],
},
{
selectorText: '#p1',
propertyNames: ['color'],
},
{
selectorText: 'p',
propertyNames: [
'display',
'margin-block-start',
'margin-block-end',
'margin-inline-start',
'margin-inline-end',
'unicode-bidi',
],
},
];
assert.deepEqual(inspectedRules, expectedInspectedRules);
});
it('can display @scope at-rules', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-scopes.html');
// Select the child that has @scope rules.
await waitForAndClickTreeElementWithPartialText(devToolsPage, '<div class=\u200B"rule1">\u200B</div>\u200B');
await waitForContentOfSelectedElementsNode(devToolsPage, '<div class=\u200B"rule1">\u200B</div>\u200B');
const rule1PropertiesSection = await getStyleRule(devToolsPage, RULE1_SELECTOR);
const scopeQuery = await devToolsPage.waitFor('.query.editable', rule1PropertiesSection);
const scopeQueryText = await scopeQuery.evaluate(node => (node as HTMLElement).innerText);
assert.deepEqual(scopeQueryText, '@scope (body) {', 'incorrectly displayed @supports rule');
});
it('shows an infobox with specificity information when hovering a selector',
async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/css-specificity.html');
// Select the child that has a style rule attached
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'properties-to-inspect');
await waitForContentOfSelectedElementsNode(devToolsPage,
'<div id=\u200B"properties-to-inspect">\u200B</div>\u200B');
// Hover the selector in the Styles pane
const testElementRule = await getStyleRule(devToolsPage, PROPERTIES_TO_INSPECT_SELECTOR);
await devToolsPage.hover('.selector-matches', {root: testElementRule});
// Check if an infobox is shown or not. If not, this will throw
const infobox = await devToolsPage.waitFor('.styles-selector :popover-open');
await expectVeEvents(
devToolsPage,
[veImpressionsUnder('Panel: elements > Pane: styles > Section: style-properties > CSSRuleHeader: selector',
[veImpression('Popover', 'elements.css-selector-specificity')])],
undefined);
// Make sure it’s the specificity infobox
const innerText = await infobox.evaluate(node => (node as HTMLElement).innerText);
assert.isTrue(innerText?.toLowerCase().startsWith('specificity'));
});
it('can display pseudo elements and their styles', async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
#inspected::target-text { color: green; }
#inspected::spelling-error { color: orange; }
#inspected::grammar-error { color: teal; }
#inspected::highlight(foo) { color: fuchsia; }
#inspected::highlight(bar) { color: cyan; }
#inspected { display: list-item; }
#inspected::marker { content: "MARKER"; }
#inspected:before, .some-other-selector { content: "BEFORE"; }
#inspected:after { content: "AFTER"; }
</style>
<div id="container">
<div id="inspected">Text</div>
</div>
`);
await waitForElementsStyleSection(devToolsPage, undefined);
// Select the node and expand it to show pseudos.
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'container');
await expandSelectedNodeRecursively(devToolsPage);
// --- Assert styles ---
// #inspected
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await waitForStyleRule(devToolsPage, '#inspected');
await devToolsPage.waitForFunction(async () => {
const inspectedStyles = await getDisplayedStyleRules(devToolsPage);
if (inspectedStyles.length !== 12) {
return false;
}
assert.sameDeepMembers(inspectedStyles, [
{selectorText: 'element.style', propertyData: []}, //
{
selectorText: '#inspected',
propertyData: [{propertyName: 'display', isOverLoaded: false, isInherited: false}],
},
{
selectorText: 'div',
propertyData: [
{propertyName: 'display', isOverLoaded: true, isInherited: false},
{propertyName: 'unicode-bidi', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '#inspected:before, .some-other-selector',
propertyData: [{propertyName: 'content', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '#inspected:after',
propertyData: [{propertyName: 'content', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '#inspected::grammar-error',
propertyData: [{propertyName: 'color', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '#inspected::highlight(bar)',
propertyData: [{propertyName: 'color', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '#inspected::highlight(foo)',
propertyData: [{propertyName: 'color', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '#inspected::marker',
propertyData: [{propertyName: 'content', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '::marker, ::before::marker, ::after::marker',
propertyData: [
{propertyName: 'unicode-bidi', isOverLoaded: false, isInherited: false},
{propertyName: 'font-variant-numeric', isOverLoaded: false, isInherited: false},
{propertyName: 'text-transform', isOverLoaded: false, isInherited: false},
{propertyName: 'text-indent', isOverLoaded: false, isInherited: false},
{propertyName: 'text-align', isOverLoaded: false, isInherited: false},
{propertyName: 'text-align-last', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '#inspected::spelling-error',
propertyData: [{propertyName: 'color', isOverLoaded: false, isInherited: false}],
},
{
selectorText: '#inspected::target-text',
propertyData: [{propertyName: 'color', isOverLoaded: false, isInherited: false}],
},
]);
return true;
});
// --- Dynamically modify styles ---
const removeLastRule = () => inspectedPage.evaluate(() => {
const sheet = document.styleSheets[0] as CSSStyleSheet;
sheet.deleteRule(sheet.cssRules.length - 1);
});
// Removing 'content' from ::after should remove the pseudo element.
await removeLastRule();
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await expandSelectedNodeRecursively(devToolsPage);
await waitForChildrenOfSelectedElementNode(devToolsPage, ['::marker', '::before', 'Text']);
// Removing 'content' from ::before should remove the pseudo element.
await removeLastRule();
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await expandSelectedNodeRecursively(devToolsPage);
await waitForChildrenOfSelectedElementNode(devToolsPage, ['::marker', 'Text']);
// Removing 'content' from ::marker should remove the pseudo element.
await removeLastRule();
await removeLastRule();
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await waitForPartialContentOfSelectedElementsNode(devToolsPage, '<div id=\u200B"inspected">\u200BText\u200B</div>');
// Add back the rules.
await inspectedPage.evaluate(() => {
const sheet = document.styleSheets[0] as CSSStyleSheet;
sheet.addRule('#inspected', 'display: list-item');
sheet.addRule('#inspected::marker', 'content: "MARKER"');
sheet.addRule('#inspected:before', 'content: "BEFORE"');
sheet.addRule('#inspected:after', 'content: "AFTER"');
});
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await expandSelectedNodeRecursively(devToolsPage);
await waitForChildrenOfSelectedElementNode(devToolsPage, ['::marker', '::before', 'Text', '::after']);
// --- Modify text content ---
await inspectedPage.evaluate(() => {
(document.getElementById('inspected') as HTMLElement).textContent = 'bar';
});
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await expandSelectedNodeRecursively(devToolsPage);
await waitForChildrenOfSelectedElementNode(devToolsPage, ['::marker', '::before', 'bar', '::after']);
// --- Remove text content ---
await inspectedPage.evaluate(() => {
(document.getElementById('inspected') as HTMLElement).textContent = '';
});
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await expandSelectedNodeRecursively(devToolsPage);
await waitForChildrenOfSelectedElementNode(devToolsPage, ['::marker', '::before', '::after']);
// --- Remove node ---
await inspectedPage.evaluate(() => document.getElementById('inspected')?.remove());
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'container');
await expandSelectedNodeRecursively(devToolsPage);
await waitForChildrenOfSelectedElementNode(devToolsPage, []);
});
it('can display nested pseudo elements and their styles', async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
#inspected::before {
content: "BEFORE";
}
#inspected::after {
content: "AFTER";
}
#inspected::before::marker {
content: "before marker";
}
#inspected::after::marker {
content: "after marker";
}
#inspected::before {
display: list-item;
}
#inspected::after {
display: list-item;
}
</style>
<div id="container">
<div id="inspected">Text</div>
</div>
`);
await waitForElementsStyleSection(devToolsPage, undefined);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'container');
await devToolsPage.pressKey('ArrowRight');
// Select the node and expand it to show pseudos.
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await expandSelectedNodeRecursively(devToolsPage);
await waitForChildrenOfSelectedElementNode(devToolsPage, ['::before', '::marker', 'Text', '::after', '::marker']);
// --- Assert styles ---
// ::before
await waitForAndClickTreeElementWithPartialText(devToolsPage, '::before');
await waitForStyleRule(devToolsPage, '#inspected::before');
await devToolsPage.waitForFunction(async () => {
const styleRules = await getDisplayedStyleRules(devToolsPage);
if (styleRules.length !== 3) {
return false;
}
assert.sameDeepMembers(styleRules, [
{
selectorText: '#inspected::before',
propertyData: [
{propertyName: 'display', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '#inspected::before',
propertyData: [
{propertyName: 'content', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '#inspected::before::marker',
propertyData: [
{propertyName: 'content', isOverLoaded: false, isInherited: false},
],
},
]);
return true;
});
await devToolsPage.pressKey('ArrowRight');
await waitForPartialContentOfSelectedElementsNode(devToolsPage, '::marker');
// ::after
await waitForAndClickTreeElementWithPartialText(devToolsPage, '::after');
await waitForStyleRule(devToolsPage, '#inspected::after');
await devToolsPage.waitForFunction(async () => {
const styleRules = await getDisplayedStyleRules(devToolsPage);
if (styleRules.length !== 3) {
return false;
}
assert.sameDeepMembers(styleRules, [
{
selectorText: '#inspected::after',
propertyData: [
{propertyName: 'display', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '#inspected::after',
propertyData: [
{propertyName: 'content', isOverLoaded: false, isInherited: false},
],
},
{
selectorText: '#inspected::after::marker',
propertyData: [
{propertyName: 'content', isOverLoaded: false, isInherited: false},
],
},
]);
return true;
});
await devToolsPage.pressKey('ArrowRight');
await waitForPartialContentOfSelectedElementsNode(devToolsPage, '::marker');
// --- Dynamically modify styles ---
const removeLastRule = () => inspectedPage.evaluate(() => {
const sheet = document.styleSheets[0] as CSSStyleSheet;
sheet.deleteRule(sheet.cssRules.length - 1);
});
// Removing 'display: list-item' from ::after should remove its marker.
await removeLastRule(); // Removes #inspected::after { display: list-item; }
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await expandSelectedNodeRecursively(devToolsPage);
await waitForChildrenOfSelectedElementNode(devToolsPage, ['::before', '::marker', 'Text', '::after']);
// Removing 'display: list-item' from ::before should remove its marker.
await removeLastRule(); // Removes #inspected::before { display: list-item; }
await waitForChildrenOfSelectedElementNode(devToolsPage, ['::before', 'Text', '::after']);
// Add back the rules.
await inspectedPage.evaluate(() => {
const sheet = document.styleSheets[0] as CSSStyleSheet;
sheet.addRule('#inspected::before', 'display: list-item');
sheet.addRule('#inspected::after', 'display: list-item');
});
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await expandSelectedNodeRecursively(devToolsPage);
await waitForChildrenOfSelectedElementNode(devToolsPage, ['::before', '::marker', 'Text', '::after', '::marker']);
// --- Remove node ---
await inspectedPage.evaluate(() => {
document.getElementById('inspected')?.remove();
});
await waitForChildrenOfSelectedElementNode(devToolsPage, []);
});
describe('Editing', () => {
async function assertBodyColor(expected: string, inspectedPage: InspectedPage) {
assert.strictEqual(
await inspectedPage.evaluate(() => {
return getComputedStyle(document.body).color;
}),
expected);
}
async function assertIsEditing(isEditing: boolean, devToolsPage: DevToolsPage) {
// .child-editing class is added by StylePropertyTreeElement when user edits a value.
assert.lengthOf(await devToolsPage.$$('.child-editing'), isEditing ? 1 : 0);
}
const green = 'rgb(0, 255, 0)';
const blue = 'rgb(0, 0, 255)';
it('cancels editing if the page is reloaded', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/simple-body-color.html');
await assertBodyColor(green, inspectedPage);
// Start editing.
await focusCSSPropertyValue(devToolsPage, 'body', 'color');
await devToolsPage.page.keyboard.type(blue, {delay: 100});
await assertBodyColor(blue, inspectedPage);
await assertIsEditing(true, devToolsPage);
// Reload and wait for styles.
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/simple-body-color.html');
// Expect the editing to be discarded and the editing mode turned off.
await waitForCSSPropertyValue(devToolsPage, 'body', 'color', green, undefined);
await assertBodyColor(green, inspectedPage);
await assertIsEditing(false, devToolsPage);
});
it('cancels editing on Esc', async ({devToolsPage, inspectedPage}) => {
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/simple-body-color.html');
await assertBodyColor(green, inspectedPage);
// Start editing.
await focusCSSPropertyValue(devToolsPage, 'body', 'color');
await devToolsPage.page.keyboard.type(blue, {delay: 100});
await assertBodyColor(blue, inspectedPage);
await assertIsEditing(true, devToolsPage);
await devToolsPage.page.keyboard.press('Escape');
// Expect the editing to be discarded and the editing mode turned off.
await waitForCSSPropertyValue(devToolsPage, 'body', 'color', green, undefined);
await assertBodyColor(green, inspectedPage);
await assertIsEditing(false, devToolsPage);
});
});
it('correctly renders and updates light-dark properties using UA color scheme',
async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
body {
color-scheme: light dark;
color: light-dark(red, blue);
}
</style>`);
await waitForElementsStyleSection(devToolsPage, undefined);
const color = await inspectedPage.evaluate(() => {
return getComputedStyle(document.body).color;
});
const red = 'rgb(255, 0, 0)';
const blue = 'rgb(0, 0, 255)';
assert.isTrue(color === red || color === blue, 'light-dark color is neither red nor blue');
await openPanelViaMoreTools(devToolsPage, 'Rendering');
let isLight = color === red;
await waitForLightDark(isLight);
isLight = await toggleColorScheme(!isLight);
await waitForLightDark(isLight);
isLight = await toggleColorScheme(!isLight);
await waitForLightDark(isLight);
isLight = await toggleColorScheme(!isLight);
await waitForLightDark(isLight);
async function toggleColorScheme(isLight: boolean): Promise<boolean> {
const select = await devToolsPage.waitFor('select:has(> option[value="light"])');
return await select.evaluate((select, isLight) => {
(select as HTMLSelectElement).selectedIndex = isLight ? 2 : 1;
return !isLight;
}, isLight);
}
async function waitForLightDark(isLight: boolean): Promise<void> {
await devToolsPage.waitForFunction(async () => {
const property = await getCSSPropertyInRule(devToolsPage, 'body', 'color', undefined);
if (!property) {
return undefined;
}
const swatches = await devToolsPage.waitForMany('.color-swatch-inner', 3);
const swatchColors = await Promise.all(
swatches.map(swatch => swatch.evaluate(swatch => getComputedStyle(swatch).backgroundColor)));
if (swatchColors[1] !== red || swatchColors[2] !== blue) {
return false;
}
return isLight ? swatchColors[0] === red : swatchColors[0] === blue;
});
}
});
it('changing selected node while editing style does update styles sidebar', async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
#inspected {
color: red;
}
#other {
color: blue;
}
</style>
<div id="inspected">Text</div>
<div id="other"></div>`);
await waitForElementsStyleSection(devToolsPage, undefined);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
let propertiesSection = await getStyleRule(devToolsPage, '#inspected');
let inspectedRules = await getDisplayedCSSDeclarations(devToolsPage);
assert.sameDeepMembers(inspectedRules, ['color: red;', 'display: block;', 'unicode-bidi: isolate;']);
// Start editing style rule for #inspected
await devToolsPage.click('.webkit-css-property[aria-label="CSS property name: color"]', {root: propertiesSection});
await devToolsPage.typeText('background-color');
// Select another node (#other)
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'other');
propertiesSection = await getStyleRule(devToolsPage, '#other');
inspectedRules = await getDisplayedCSSDeclarations(devToolsPage);
assert.sameDeepMembers(inspectedRules, ['color: blue;', 'display: block;', 'unicode-bidi: isolate;']);
// Selected #inspected again
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
propertiesSection = await getStyleRule(devToolsPage, '#inspected');
inspectedRules = await getDisplayedCSSDeclarations(devToolsPage);
assert.includeDeepMembers(inspectedRules, ['background-color: red;', 'display: block;', 'unicode-bidi: isolate;']);
});
it('correctly removes property after it is disabled', async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
#container {
font-weight:bold;
}
</style>
<div id="container">
</div>
<div id="other">
</div>`);
await waitForElementsStyleSection(devToolsPage, undefined);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'container');
let propertiesSection = await getStyleRule(devToolsPage, '#container');
let displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, propertiesSection);
assert.deepEqual(
displayedNames,
[
'font-weight',
],
'incorrectly displayed style after initialization');
let inspectedRules = await getDisplayedStyleRules(devToolsPage);
let inspectedRulesContainer = inspectedRules.filter(rule => rule.selectorText === '#container');
assert.deepEqual(inspectedRulesContainer, [{
selectorText: '#container',
propertyData: [{propertyName: 'font-weight', isOverLoaded: false, isInherited: false}],
}]);
// Disable the style rule
await devToolsPage.click('.tree-outline li:nth-of-type(1) input', {root: propertiesSection});
await devToolsPage.waitFor('.tree-outline li:nth-of-type(1).overloaded.disabled.inactive', propertiesSection);
displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, propertiesSection);
inspectedRules = await getDisplayedStyleRules(devToolsPage);
inspectedRulesContainer = inspectedRules.filter(rule => rule.selectorText === '#container');
assert.deepEqual(inspectedRulesContainer, [{
selectorText: '#container',
propertyData: [{propertyName: 'font-weight', isOverLoaded: true, isInherited: false}],
}]);
// Delete the style rule
await deletePropertyByBackspace(devToolsPage, '.webkit-css-property[aria-label="CSS property name: font-weight"]');
// Select another node (#other)
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'other');
// Selected #inspected again
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'container');
propertiesSection = await getStyleRule(devToolsPage, '#container');
displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, propertiesSection);
assert.isEmpty(displayedNames);
});
it('splits properties when pasting', async ({devToolsPage, inspectedPage}) => {
expectError('Unknown VE context: \'moo\'');
await inspectedPage.goToHtml(`
<style>
#inspected {
font-size: 12px;
}
</style>
<div id="inspected">Text</div>
<div id="other"></div>`);
await waitForElementsStyleSection(devToolsPage, undefined);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
const propertiesSectionSelector = getStyleRuleSelector('#inspected');
let propertiesSection = await devToolsPage.waitFor(propertiesSectionSelector);
let inspectedRules = await getDisplayedCSSDeclarations(devToolsPage);
assert.sameDeepMembers(inspectedRules, ['font-size: 12px;', 'display: block;', 'unicode-bidi: isolate;']);
let displayedNames = await getDisplayedCSSPropertyNames(devToolsPage, propertiesSection);
assert.deepEqual(
displayedNames,
[
'font-size',
],
'incorrectly displayed style after initialization');
await devToolsPage.click(propertiesSectionSelector);
await devToolsPage.pasteText('margin-left: 1px');
propertiesSection = await devToolsPage.waitFor(propertiesSectionSelector);
await devToolsPage.waitForFunction(async () => {
const names = await getDisplayedCSSPropertyNames(devToolsPage, propertiesSection);
return names.includes('margin-left');
});
await devToolsPage.click(propertiesSectionSelector);
displayedNames =
await getDisplayedCSSPropertyNames(devToolsPage, await devToolsPage.waitFor(propertiesSectionSelector));
assert.sameDeepMembers(
displayedNames,
[
'font-size',
'margin-left',
],
'incorrectly displayed style after pasting');
await devToolsPage.click(propertiesSectionSelector);
propertiesSection = await devToolsPage.waitFor(propertiesSectionSelector);
await devToolsPage.pasteText('margin-top: 1px; color: red;');
await devToolsPage.waitForFunction(async () => {
const names = await getDisplayedCSSPropertyNames(devToolsPage, propertiesSection);
return names.includes('margin-top') && names.includes('color');
});
await devToolsPage.click(propertiesSectionSelector);
displayedNames =
await getDisplayedCSSPropertyNames(devToolsPage, await devToolsPage.waitFor(propertiesSectionSelector));
assert.sameDeepMembers(
displayedNames,
[
'font-size',
'margin-top',
'color',
'margin-left',
],
'incorrectly displayed style after pasting');
await devToolsPage.click(
'.webkit-css-property[aria-label="CSS property name: margin-top"]',
{root: await devToolsPage.waitFor(propertiesSectionSelector)});
await devToolsPage.pasteText('foo: bar; moo: zoo;');
await devToolsPage.waitForFunction(async () => {
const names =
await getDisplayedCSSPropertyNames(devToolsPage, await devToolsPage.waitFor(propertiesSectionSelector));
return names.includes('foo') && names.includes('moo');
});
await devToolsPage.click(propertiesSectionSelector);
displayedNames =
await getDisplayedCSSPropertyNames(devToolsPage, await devToolsPage.waitFor(propertiesSectionSelector));
assert.sameDeepMembers(
displayedNames,
[
'font-size',
'foo',
'moo',
'color',
'margin-left',
],
'incorrectly displayed style after pasting new styles over \'margin-top\'');
inspectedRules = await getDisplayedCSSDeclarations(devToolsPage);
assert.sameDeepMembers(inspectedRules, [
'font-size: 12px;',
'foo: bar;',
'moo: zoo;',
'color: red;',
'margin-left: 1px;',
'display: block;',
'unicode-bidi: isolate;',
]);
});
it('adding an invalid property retains syntax', async ({devToolsPage, inspectedPage}) => {
await inspectedPage.goToHtml(`
<style>
#inspected {
font-size: 12px;
}
</style>
<div id="inspected">Text</div>
<div id="other"></div>`);
await waitForElementsStyleSection(devToolsPage, undefined);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
let propertiesSection = await getStyleRule(devToolsPage, '#inspected');
let inspectedRules = await getDisplayedCSSDeclarations(devToolsPage);
assert.sameDeepMembers(inspectedRules, ['font-size: 12px;', 'display: block;', 'unicode-bidi: isolate;']);
await propertiesSection.focus();
await devToolsPage.typeText('color');
await devToolsPage.pressKey('Enter');
await devToolsPage.pasteText('rgb(1');
await devToolsPage.pressKey('Enter');
// Select another node (#other)
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'other');
// Selected #inspected again
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
propertiesSection = await getStyleRule(devToolsPage, '#inspected');
inspectedRules = await getDisplayedCSSDeclarations(devToolsPage);
assert.sameDeepMembers(
inspectedRules, ['font-size: 12px;', 'color: rgb(1);', 'display: block;', 'unicode-bidi: isolate;']);
});
describe('AI code completion', () => {
async function setupAiCompletion(devToolsPage: DevToolsPage) {
const hostConfig = {
aidaAvailability: {
enabled: true,
disallowLogging: true,
enterprisePolicyValue: 0,
},
devToolsFreestyler: {
enabled: true,
},
isOffTheRecord: false,
devToolsAiCodeCompletion: {
enabled: true,
},
devToolsAiCodeCompletionStyles: {
enabled: true,
},
};
await devToolsPage.setupMockHostConfigAndReload(hostConfig);
await devToolsPage.evaluate(`
(async () => {
const Common = await import('./core/common/common.js');
Common.Settings.Settings.instance().createLocalSetting('ai-code-completion-enabled', false).set(true);
})()
`);
await devToolsPage.useSoftMenu();
}
async function setupAndWaitForFooStylePropertiesSection(devToolsPage: DevToolsPage, inspectedPage: InspectedPage) {
await setupAiCompletion(devToolsPage);
await goToResourceAndWaitForStyleSection(devToolsPage, inspectedPage, 'elements/elements-panel-styles.html');
await prepareElementsTab(devToolsPage);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"container"');
await waitForStyleRule(devToolsPage, '#container');
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"foo"');
await waitForStyleRule(devToolsPage, '.foo');
const propertiesSection = await getStyleRule(devToolsPage, '#container .foo');
return propertiesSection;
}
it('triggers a suggestion when typing in a property name', async ({devToolsPage, inspectedPage}) => {
const propertiesSection = await setupAndWaitForFooStylePropertiesSection(devToolsPage, inspectedPage);
await propertiesSection.focus();
// Mock Aida response
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{
generationString: 'or: red;',
sampleId: 1,
score: 1.0,
}],
metadata: {rpcGlobalId: '123'},
});
await devToolsPage.typeText('col');
// Wait for ghost text "red"
await devToolsPage.waitForFunction(async () => {
const ghostText = await getGhostText(devToolsPage);
return ghostText === 'red';
});
// Also check the name part is updated in the editor
const nameText = await devToolsPage.activeElementTextContent();
assert.strictEqual(nameText, 'color');
});
it('shows multiline ghost rows for multiline suggestions', async ({devToolsPage, inspectedPage}) => {
const propertiesSection = await setupAndWaitForFooStylePropertiesSection(devToolsPage, inspectedPage);
await propertiesSection.focus();
// Mock multiline Aida response
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{
generationString: 'or: pink;\nbackground: red;',
sampleId: 1,
score: 1.0,
}],
metadata: {rpcGlobalId: '123'},
});
await devToolsPage.typeText('col');
// Wait for ghost text in current value
await devToolsPage.waitForFunction(async () => {
const ghostText = await getGhostText(devToolsPage);
return ghostText === 'pink';
});
// Also check the name part is updated in the editor
const nameText = await devToolsPage.activeElementTextContent();
assert.strictEqual(nameText, 'color');
// Check multiline ghost rows
const multilineGhosts = await getMultilineGhostElements(devToolsPage);
assert.lengthOf(multilineGhosts, 1);
assert.include(multilineGhosts[0], 'background: red');
});
it('handles suggestions with complex values (data URLs with semicolons)', async ({devToolsPage, inspectedPage}) => {
const propertiesSection = await setupAndWaitForFooStylePropertiesSection(devToolsPage, inspectedPage);
await propertiesSection.focus();
const complexValue = 'url("data:image/png;base64,ABC;123")';
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{
generationString: `ground: ${complexValue};`,
sampleId: 1,
score: 1.0,
}],
metadata: {rpcGlobalId: '123'},
});
await devToolsPage.typeText('back');
await devToolsPage.waitForFunction(async () => {
const ghostText = await getGhostText(devToolsPage);
return ghostText === complexValue;
});
});
it('accepts a single-line suggestion on Tab', async ({devToolsPage, inspectedPage}) => {
const propertiesSection = await setupAndWaitForFooStylePropertiesSection(devToolsPage, inspectedPage);
await propertiesSection.focus();
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{
generationString: 'or: red;',
sampleId: 1,
score: 1.0,
}],
metadata: {rpcGlobalId: '123'},
});
await devToolsPage.typeText('col');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'red';
});
await devToolsPage.pressKey('Tab');
// The first tab will accept suggestion from autocomplete menu
await devToolsPage.waitForNone(TEXT_PROMPT_GHOST_TEXT_SELECTOR);
await devToolsPage.waitForFunction(async () => {
const ghostText = await getGhostText(devToolsPage);
return ghostText === 'red';
});
await devToolsPage.pressKey('Tab');
// The second tab will accept rest of the AI suggestion
await devToolsPage.waitForNone(GHOST_VALUE_PREDICTION_SELECTOR);
await devToolsPage.waitForFunction(async () => {
const rules = await getDisplayedCSSDeclarations(devToolsPage);
return rules.includes('color: red;');
});
});
it('accepts multiline suggestions on Tab', async ({devToolsPage, inspectedPage}) => {
const propertiesSection = await setupAndWaitForFooStylePropertiesSection(devToolsPage, inspectedPage);
await propertiesSection.focus();
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{
generationString: 'or: pink;\nbackground: red;',
sampleId: 1,
score: 1.0,
}],
metadata: {rpcGlobalId: '123'},
});
await devToolsPage.typeText('col');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'pink';
});
let multilineGhosts = await getMultilineGhostElements(devToolsPage);
assert.deepEqual(multilineGhosts, ['background: red;']);
// Press Tab to accept
await devToolsPage.pressKey('Tab');
// The first tab will accept suggestion from autocomplete menu
await devToolsPage.waitForNone(TEXT_PROMPT_GHOST_TEXT_SELECTOR);
await devToolsPage.waitForFunction(async () => {
const ghostText = await getGhostText(devToolsPage);
return ghostText === 'pink';
});
multilineGhosts = await getMultilineGhostElements(devToolsPage);
assert.deepEqual(multilineGhosts, ['background: red;']);
// Press Tab to accept
await devToolsPage.pressKey('Tab');
// The second tab will accept rest of the AI suggestion
await devToolsPage.waitForNone(GHOST_VALUE_PREDICTION_SELECTOR);
await devToolsPage.waitForNone(GHOST_ROW_SELECTOR);
await devToolsPage.waitForFunction(async () => {
const rules = await getDisplayedCSSDeclarations(devToolsPage);
return rules.includes('color: pink;') && rules.includes('background: red;');
});
});
it('rejects suggestions via Escape key', async ({devToolsPage, inspectedPage}) => {
const propertiesSection = await setupAndWaitForFooStylePropertiesSection(devToolsPage, inspectedPage);
await propertiesSection.focus();
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{
generationString: 'or: pink;\nbackground: red;',
sampleId: 1,
score: 1.0,
}],
metadata: {rpcGlobalId: '123'},
});
await devToolsPage.typeText('col');
await devToolsPage.waitFor('.suggest-box');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'pink';
});
let multilineGhosts = await getMultilineGhostElements(devToolsPage);
assert.deepEqual(multilineGhosts, ['background: red;']);
// Press Escape
await devToolsPage.pressKey('Escape');
// The first Escape press will get rid of the autocomplete menu
await devToolsPage.waitForNone('.suggest-box');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'pink';
});
multilineGhosts = await getMultilineGhostElements(devToolsPage);
assert.deepEqual(multilineGhosts, ['background: red;']);
// Press Escape
await devToolsPage.pressKey('Escape');
// The second Escape press will get rid of the AI suggestion
await devToolsPage.waitForNone(TEXT_PROMPT_GHOST_TEXT_SELECTOR);
await devToolsPage.waitForNone(GHOST_VALUE_PREDICTION_SELECTOR);
await devToolsPage.waitForNone(GHOST_ROW_SELECTOR);
// Verify input remains
const nameText = await devToolsPage.activeElementTextContent();
assert.strictEqual(nameText, 'col');
});
it('rejects suggestions via ArrowDown key', async ({devToolsPage, inspectedPage}) => {
const propertiesSection = await setupAndWaitForFooStylePropertiesSection(devToolsPage, inspectedPage);
await propertiesSection.focus();
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{
generationString: 'or: pink;\nbackground: red;',
sampleId: 1,
score: 1.0,
}],
metadata: {rpcGlobalId: '123'},
});
await devToolsPage.typeText('col');
await devToolsPage.waitFor('.suggest-box');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'pink';
});
const multilineGhosts = await getMultilineGhostElements(devToolsPage);
assert.deepEqual(multilineGhosts, ['background: red;']);
// Press ArrowDown
await devToolsPage.pressKey('ArrowDown');
// Verify all of the AI suggestion is gone
await devToolsPage.waitForNone(GHOST_VALUE_PREDICTION_SELECTOR);
await devToolsPage.waitForNone(GHOST_ROW_SELECTOR);
});
it('rejects suggestions when typing a non-matching character', async ({devToolsPage, inspectedPage}) => {
const propertiesSection = await setupAndWaitForFooStylePropertiesSection(devToolsPage, inspectedPage);
await propertiesSection.focus();
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{
generationString: 'or: pink;\nbackground: red;',
sampleId: 1,
score: 1.0,
}],
metadata: {rpcGlobalId: '123'},
});
await devToolsPage.typeText('col');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'pink';
});
const multilineGhosts = await getMultilineGhostElements(devToolsPage);
assert.deepEqual(multilineGhosts, ['background: red;']);
await devToolsPage.typeText('x');
// Verify all of the AI suggestion is gone
await devToolsPage.waitForNone(GHOST_VALUE_PREDICTION_SELECTOR);
await devToolsPage.waitForNone(GHOST_ROW_SELECTOR);
});
it('rejects suggestions when character is typed in the middle of text', async ({devToolsPage, inspectedPage}) => {
const propertiesSection = await setupAndWaitForFooStylePropertiesSection(devToolsPage, inspectedPage);
await propertiesSection.focus();
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{
generationString: 'or: pink;\nbackground: red;',
sampleId: 1,
score: 1.0,
}],
metadata: {rpcGlobalId: '123'},
});
await devToolsPage.typeText('col');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'pink';
});
const multilineGhosts = await getMultilineGhostElements(devToolsPage);
assert.deepEqual(multilineGhosts, ['background: red;']);
// Move caret to the middle of line
await devToolsPage.page.keyboard.press('ArrowLeft');
await devToolsPage.page.keyboard.press('ArrowLeft');
// Type a character
await devToolsPage.typeText('x');
// Verify all of the AI suggestion is gone
await devToolsPage.waitForNone(GHOST_VALUE_PREDICTION_SELECTOR);
await devToolsPage.waitForNone(GHOST_ROW_SELECTOR);
});
it('clears suggestions when editing a different property', async ({devToolsPage, inspectedPage}) => {
const propertiesSection = await setupAndWaitForFooStylePropertiesSection(devToolsPage, inspectedPage);
await propertiesSection.focus();
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{generationString: 'or: red;', sampleId: 1, score: 1.0}],
metadata: {rpcGlobalId: '123'},
});
await devToolsPage.typeText('col');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'red';
});
const displayProperty = await devToolsPage.waitFor(
'.webkit-css-property[aria-label="CSS property name: font-style"]', propertiesSection);
await displayProperty.click();
await devToolsPage.waitForNone(TEXT_PROMPT_GHOST_TEXT_SELECTOR);
await devToolsPage.waitForNone(GHOST_VALUE_PREDICTION_SELECTOR);
});
it('updates context when the stylesheet is modified via CSSOM (addRule)', async ({devToolsPage, inspectedPage}) => {
await setupAiCompletion(devToolsPage);
await inspectedPage.goToHtml(`
<style>
#container { background-color: green; }
#inspected { background-color: orange; }
</style>
<div id="container">
<div id="inspected">Text</div>
</div>
`);
await waitForElementsStyleSection(devToolsPage, undefined);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'container');
await expandSelectedNodeRecursively(devToolsPage);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await waitForStyleRule(devToolsPage, '#inspected');
// dynamically update the stylesheet
await inspectedPage.evaluate(() => {
const sheet = document.styleSheets[0] as CSSStyleSheet;
sheet.addRule('div#inspected', 'display: block');
});
// re-select the element to update devtools
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await expandSelectedNodeRecursively(devToolsPage);
const propertiesSection = await getStyleRule(devToolsPage, '#inspected');
await propertiesSection.focus();
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{
generationString: 'or: red;',
sampleId: 1,
score: 1.0,
}],
metadata: {rpcGlobalId: '123'},
});
// trigger code completion
await devToolsPage.typeText('col');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'red';
});
// check if the request sent to AIDA contains the new rule
const lastRequest = await getLastAidaRequest(devToolsPage);
assert.exists(lastRequest);
assert.include(lastRequest, 'div#inspected { display: block; }');
});
it('updates context when pasting a large block of properties', async ({devToolsPage, inspectedPage}) => {
await setupAiCompletion(devToolsPage);
await inspectedPage.goToHtml(`
<style>
#inspected { background-color: orange; }
</style>
<div id="inspected">Text</div>
`);
await waitForElementsStyleSection(devToolsPage, undefined);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await waitForStyleRule(devToolsPage, '#inspected');
const propertiesSection = await getStyleRule(devToolsPage, '#inspected');
await propertiesSection.focus();
// Paste a large block of css properties.
await devToolsPage.typeText('m');
await devToolsPage.pasteText(`argin: 10px; padding: 20px;
font-weight: bold; font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
background-image: url('https://example.com/image;v=1?query:part=true');`);
await devToolsPage.page.keyboard.press('Enter');
await devToolsPage.page.keyboard.press('Escape');
await devToolsPage.waitForFunction(async () => {
const names = await getDisplayedCSSPropertyNames(devToolsPage, propertiesSection);
return names.includes('margin') && names.includes('background-image');
});
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{generationString: 'or: red;', sampleId: 1, score: 1.0}],
metadata: {rpcGlobalId: '123'},
});
// Trigger code completion.
await propertiesSection.focus();
await devToolsPage.typeText('col');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'red';
});
const lastRequest = await getLastAidaRequest(devToolsPage);
assert.exists(lastRequest);
assert.include(lastRequest, 'margin: 10px;');
assert.include(lastRequest, 'padding: 20px;');
assert.include(lastRequest, 'font-weight: bold;');
assert.include(lastRequest, 'font-family: \'Franklin Gothic Medium\', \'Arial Narrow\', Arial, sans-serif;');
assert.include(lastRequest, 'background-image: url(\'https://example.com/image;v=1?query:part=true\');');
});
it('updates context when deleting a large block of properties', async ({devToolsPage, inspectedPage}) => {
await setupAiCompletion(devToolsPage);
await inspectedPage.goToHtml(`
<style>
#inspected {
margin: 10px;
padding: 20px;
font-weight: bold;
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
background-image: url('https://example.com/image;v=1?query:part=true');
}
</style>
<div id="inspected">Text</div>
`);
await waitForElementsStyleSection(devToolsPage, undefined);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'inspected');
await waitForStyleRule(devToolsPage, '#inspected');
const propertiesSection = await getStyleRule(devToolsPage, '#inspected');
await propertiesSection.focus();
// Delete multiple properties
await deletePropertyByBackspace(
devToolsPage, '.webkit-css-property[aria-label="CSS property name: font-family"]', propertiesSection);
await deletePropertyByBackspace(
devToolsPage, '.webkit-css-property[aria-label="CSS property name: background-image"]', propertiesSection);
await deletePropertyByBackspace(
devToolsPage, '.webkit-css-property[aria-label="CSS property name: font-weight"]', propertiesSection);
await deletePropertyByBackspace(
devToolsPage, '.webkit-css-property[aria-label="CSS property name: padding"]', propertiesSection);
await mockAidaCodeComplete(devToolsPage, {
generatedSamples: [{generationString: 'or: red;', sampleId: 1, score: 1.0}],
metadata: {rpcGlobalId: '123'},
});
// trigger code completion
await propertiesSection.focus();
await devToolsPage.typeText('col');
await devToolsPage.waitForFunction(async () => {
const ghostTextInCurrentTextPrompt = await getGhostTextInCurrentTextPrompt(devToolsPage);
const ghostText = await getGhostText(devToolsPage);
return ghostTextInCurrentTextPrompt === 'or' && ghostText === 'red';
});
const lastRequest = await getLastAidaRequest(devToolsPage);
assert.exists(lastRequest);
assert.include(lastRequest, 'margin: 10px;');
assert.notInclude(lastRequest, 'padding: 20px;');
assert.notInclude(lastRequest, 'font-weight: bold;');
assert.notInclude(lastRequest, 'font-family');
assert.notInclude(lastRequest, 'background-image');
});
});
});