diff --git a/front_end/core/sdk/DOMModel.ts b/front_end/core/sdk/DOMModel.ts index 0a656aafdb..1d47f4cd6d 100644 --- a/front_end/core/sdk/DOMModel.ts +++ b/front_end/core/sdk/DOMModel.ts @@ -238,6 +238,20 @@ export class DOMNode { return this.#nodeNameInternal === 'AUDIO' || this.#nodeNameInternal === 'VIDEO'; } + isViewTransitionPseudoNode(): boolean { + if (!this.#pseudoTypeInternal) { + return false; + } + + return [ + Protocol.DOM.PseudoType.ViewTransition, + Protocol.DOM.PseudoType.ViewTransitionGroup, + Protocol.DOM.PseudoType.ViewTransitionImagePair, + Protocol.DOM.PseudoType.ViewTransitionOld, + Protocol.DOM.PseudoType.ViewTransitionNew, + ].includes(this.#pseudoTypeInternal); + } + creationStackTrace(): Promise { if (this.#creationStackTraceInternal) { return this.#creationStackTraceInternal; diff --git a/front_end/panels/elements/ElementsPanel.ts b/front_end/panels/elements/ElementsPanel.ts index 432bcd1403..eaa1a4142c 100644 --- a/front_end/panels/elements/ElementsPanel.ts +++ b/front_end/panels/elements/ElementsPanel.ts @@ -211,6 +211,13 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S private notFirstInspectElement?: boolean; sidebarPaneView?: UI.View.TabbedViewLocation; private stylesViewToReveal?: UI.View.SimpleView; + private nodeInsertedTaskRunner = { + queue: Promise.resolve(), + run(task: () => Promise): + void { + this.queue = this.queue.then(task); + }, + }; private cssStyleTrackerByCSSModel: Map; @@ -378,10 +385,39 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S treeOutline.focus(); } domModel.addEventListener(SDK.DOMModel.Events.DocumentUpdated, this.documentUpdatedEvent, this); + domModel.addEventListener(SDK.DOMModel.Events.NodeInserted, this.handleNodeInserted, this); + } + + private handleNodeInserted(event: Common.EventTarget.EventTargetEvent): void { + // Queue the task for the case when all the view transitions are added + // around the same time. Otherwise there is a race condition on + // accessing `cssText` of inspector stylesheet causing some rules + // to be not added. + this.nodeInsertedTaskRunner.run(async () => { + const node = event.data; + if (!node.isViewTransitionPseudoNode()) { + return; + } + + const cssModel = node.domModel().cssModel(); + const styleSheetHeader = await cssModel.requestViaInspectorStylesheet(node); + if (!styleSheetHeader) { + return; + } + + const cssText = await cssModel.getStyleSheetText(styleSheetHeader.id); + // Do not add a rule for the view transition pseudo if there already is a rule for it. + if (cssText?.includes(`${node.simpleSelector()} {`)) { + return; + } + + await cssModel.setStyleSheetText(styleSheetHeader.id, `${cssText}\n${node.simpleSelector()} {}`, false); + }); } modelRemoved(domModel: SDK.DOMModel.DOMModel): void { domModel.removeEventListener(SDK.DOMModel.Events.DocumentUpdated, this.documentUpdatedEvent, this); + domModel.removeEventListener(SDK.DOMModel.Events.NodeInserted, this.handleNodeInserted, this); const treeOutline = ElementsTreeOutline.forDOMModel(domModel); if (!treeOutline) { return; diff --git a/front_end/panels/elements/ElementsTreeElement.ts b/front_end/panels/elements/ElementsTreeElement.ts index e4754143f2..62cf899486 100644 --- a/front_end/panels/elements/ElementsTreeElement.ts +++ b/front_end/panels/elements/ElementsTreeElement.ts @@ -708,7 +708,21 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement { }, {jslogContext: 'focus'}); } - populateScrollIntoView(contextMenu: UI.ContextMenu.ContextMenu): void { + populatePseudoElementContextMenu(contextMenu: UI.ContextMenu.ContextMenu): void { + if (this.childCount() !== 0) { + this.populateExpandRecursively(contextMenu); + } + + this.populateScrollIntoView(contextMenu); + } + + private populateExpandRecursively(contextMenu: UI.ContextMenu.ContextMenu): void { + contextMenu.viewSection().appendItem( + i18nString(UIStrings.expandRecursively), this.expandRecursively.bind(this), + {jslogContext: 'expandRecursively'}); + } + + private populateScrollIntoView(contextMenu: UI.ContextMenu.ContextMenu): void { contextMenu.viewSection().appendItem( i18nString(UIStrings.scrollIntoView), () => this.nodeInternal.scrollIntoView(), {jslogContext: 'scrollIntoView'}); @@ -797,9 +811,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement { i18nString(UIStrings.deleteElement), this.remove.bind(this), {jslogContext: 'deleteElement'}); } - contextMenu.viewSection().appendItem( - i18nString(UIStrings.expandRecursively), this.expandRecursively.bind(this), - {jslogContext: 'expandRecursively'}); + this.populateExpandRecursively(contextMenu); contextMenu.viewSection().appendItem( i18nString(UIStrings.collapseChildren), this.collapseChildren.bind(this), {jslogContext: 'collapseChildren'}); const deviceModeWrapperAction = new Emulation.DeviceModeWrapper.ActionDelegate(); diff --git a/front_end/panels/elements/ElementsTreeOutline.ts b/front_end/panels/elements/ElementsTreeOutline.ts index 61bade227f..0c5207da38 100644 --- a/front_end/panels/elements/ElementsTreeOutline.ts +++ b/front_end/panels/elements/ElementsTreeOutline.ts @@ -938,7 +938,7 @@ export class ElementsTreeOutline extends } else if (commentNode) { treeElement.populateNodeContextMenu(contextMenu); } else if (isPseudoElement) { - treeElement.populateScrollIntoView(contextMenu); + treeElement.populatePseudoElementContextMenu(contextMenu); } contextMenu.viewSection().appendItem(i18nString(UIStrings.adornerSettings), () => { diff --git a/test/e2e/elements/BUILD.gn b/test/e2e/elements/BUILD.gn index 8a3008e3f3..f0b9639346 100644 --- a/test/e2e/elements/BUILD.gn +++ b/test/e2e/elements/BUILD.gn @@ -8,6 +8,7 @@ node_ts_library("elements") { sources = [ "accessibility-pane_test.ts", "accessibility-tree_test.ts", + "add-view-transition-pseudo-styles_test.ts", "adornment_test.ts", "at-property-sections_test.ts", "classes-pane_test.ts", diff --git a/test/e2e/elements/add-view-transition-pseudo-styles_test.ts b/test/e2e/elements/add-view-transition-pseudo-styles_test.ts new file mode 100644 index 0000000000..7518b5c50a --- /dev/null +++ b/test/e2e/elements/add-view-transition-pseudo-styles_test.ts @@ -0,0 +1,57 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {assert} from 'chai'; + +import {getBrowserAndPages} from '../../shared/helper.js'; +import {describe, it} from '../../shared/mocha-extensions.js'; +import { + expandSelectedNodeRecursively, + getDisplayedStyleRules, + goToResourceAndWaitForStyleSection, + waitForAndClickTreeElementWithPartialText, + waitForExactStyleRule, +} from '../helpers/elements-helpers.js'; + +describe('View transition pseudo styles on inspector stylesheet', async () => { + it('should add view transition pseudo styles on inspector stylesheet when a view transition pseudo is added', + async () => { + const {frontend, target} = getBrowserAndPages(); + await goToResourceAndWaitForStyleSection('elements/view-transition.html'); + + await target.bringToFront(); + await target.evaluate('startFirstViewTransition()'); + + await frontend.bringToFront(); + await waitForAndClickTreeElementWithPartialText('::view-transition'); + await waitForExactStyleRule('::view-transition'); + + await expandSelectedNodeRecursively(); + await waitForAndClickTreeElementWithPartialText('::view-transition-old(root)'); + await waitForExactStyleRule('::view-transition-old(root)'); + }); + + it('should not add view transition pseudo styles if inspector stylesheet already has view transition pseudo styles', + async () => { + const {frontend, target} = getBrowserAndPages(); + await goToResourceAndWaitForStyleSection('elements/view-transition.html'); + + await target.bringToFront(); + await target.evaluate('startFirstViewTransition()'); + + await frontend.bringToFront(); + await waitForAndClickTreeElementWithPartialText('::view-transition'); + await waitForExactStyleRule('::view-transition'); + + await target.bringToFront(); + await target.evaluate('startNextViewTransition()'); + + await frontend.bringToFront(); + await waitForAndClickTreeElementWithPartialText('::view-transition'); + await waitForExactStyleRule('::view-transition'); + + const displayedRules = await getDisplayedStyleRules(); + assert.strictEqual(displayedRules.filter(rule => rule.selectorText === '::view-transition').length, 1); + }); +}); diff --git a/test/e2e/helpers/elements-helpers.ts b/test/e2e/helpers/elements-helpers.ts index 72ea6365e6..55cf921104 100644 --- a/test/e2e/helpers/elements-helpers.ts +++ b/test/e2e/helpers/elements-helpers.ts @@ -391,6 +391,13 @@ export const assertGutterDecorationForDomNodeExists = async () => { export const getStyleRuleSelector = (selector: string) => `[aria-label="${selector}, css selector"]`; +export const waitForExactStyleRule = async (expectedSelector: string) => { + await waitForFunction(async () => { + const rules = await getDisplayedStyleRules(); + return rules.find(rule => rule.selectorText === expectedSelector); + }); +}; + export const waitForStyleRule = async (expectedSelector: string) => { await waitForFunction(async () => { const rules = await getDisplayedStyleRules(); diff --git a/test/e2e/resources/elements/BUILD.gn b/test/e2e/resources/elements/BUILD.gn index 7bf6b20255..153a5beb7f 100644 --- a/test/e2e/resources/elements/BUILD.gn +++ b/test/e2e/resources/elements/BUILD.gn @@ -64,5 +64,6 @@ copy_to_gen("elements") { "stylesheets-with-various-sources.html", "switch-panels-while-editing-as-html.html", "target.html", + "view-transition.html", ] } diff --git a/test/e2e/resources/elements/view-transition.html b/test/e2e/resources/elements/view-transition.html new file mode 100644 index 0000000000..05cbfd7661 --- /dev/null +++ b/test/e2e/resources/elements/view-transition.html @@ -0,0 +1,41 @@ + + + + +
Initial state
+ + \ No newline at end of file