| // 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'); |
| }); |
| }); |
| }); |