Tim van der Lippe | 68efc70 | 2020-03-03 17:27:45 | [diff] [blame] | 1 | // Copyright 2020 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | import {assert} from 'chai'; |
Jack Franklin | a75ae7c | 2021-05-11 13:22:54 | [diff] [blame] | 5 | import type * as puppeteer from 'puppeteer'; |
Philip Pfaffe | a1f88f5 | 2023-04-20 08:31:07 | [diff] [blame] | 6 | import {AsyncScope} from '../../shared/async-scope.js'; |
Tim van der Lippe | 68efc70 | 2020-03-03 17:27:45 | [diff] [blame] | 7 | |
Jack Franklin | e839c0c | 2022-05-03 08:47:44 | [diff] [blame] | 8 | import { |
| 9 | $, |
| 10 | $$, |
| 11 | click, |
| 12 | getBrowserAndPages, |
PhistucK | 257ad69 | 2022-08-29 14:36:19 | [diff] [blame] | 13 | getTextContent, |
Philip Pfaffe | 7fb6a83 | 2022-11-14 13:39:47 | [diff] [blame] | 14 | goToResource, |
Jack Franklin | e839c0c | 2022-05-03 08:47:44 | [diff] [blame] | 15 | pressKey, |
| 16 | step, |
PhistucK | 257ad69 | 2022-08-29 14:36:19 | [diff] [blame] | 17 | summonSearchBox, |
Jack Franklin | e839c0c | 2022-05-03 08:47:44 | [diff] [blame] | 18 | timeout, |
| 19 | typeText, |
| 20 | waitFor, |
| 21 | waitForAria, |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 22 | clickElement, |
Jack Franklin | e839c0c | 2022-05-03 08:47:44 | [diff] [blame] | 23 | waitForFunction, |
| 24 | } from '../../shared/helper.js'; |
Tim van der Lippe | 68efc70 | 2020-03-03 17:27:45 | [diff] [blame] | 25 | |
| 26 | const SELECTED_TREE_ELEMENT_SELECTOR = '.selected[role="treeitem"]'; |
Changhao Han | d1b1b5f | 2020-03-27 12:00:53 | [diff] [blame] | 27 | const CSS_PROPERTY_NAME_SELECTOR = '.webkit-css-property'; |
Patrick Brosset | b49605b | 2020-10-12 12:35:04 | [diff] [blame] | 28 | const CSS_PROPERTY_VALUE_SELECTOR = '.value'; |
Changhao Han | 7bdd445 | 2022-08-26 11:03:48 | [diff] [blame] | 29 | const CSS_DECLARATION_SELECTOR = |
| 30 | `[role="treeitem"]:has(${CSS_PROPERTY_NAME_SELECTOR}):has(${CSS_PROPERTY_VALUE_SELECTOR})`; |
Patrick Brosset | 394c92f | 2020-10-28 17:25:14 | [diff] [blame] | 31 | const COLOR_SWATCH_SELECTOR = '.color-swatch-inner'; |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 32 | const CSS_STYLE_RULE_SELECTOR = '[aria-label*="css selector"]'; |
Changhao Han | d245432 | 2020-08-11 13:09:35 | [diff] [blame] | 33 | const COMPUTED_PROPERTY_SELECTOR = 'devtools-computed-style-property'; |
Changhao Han | 2bcf0b8 | 2020-07-17 09:32:08 | [diff] [blame] | 34 | const COMPUTED_STYLES_PANEL_SELECTOR = '[aria-label="Computed panel"]'; |
Elorm Coch | 34ad334 | 2023-06-06 20:15:59 | [diff] [blame^] | 35 | const COMPUTED_STYLES_SHOW_ALL_SELECTOR = '[title="Show all"]'; |
| 36 | const COMPUTED_STYLES_GROUP_SELECTOR = '[title="Group"]'; |
Jose Leal Chapa | dd4d881 | 2020-05-21 16:45:00 | [diff] [blame] | 37 | const ELEMENTS_PANEL_SELECTOR = '.panel[aria-label="elements"]'; |
Michael Liao | 6035f4a | 2020-12-01 19:12:45 | [diff] [blame] | 38 | const FONT_EDITOR_SELECTOR = '[aria-label="Font Editor"]'; |
| 39 | const HIDDEN_FONT_EDITOR_SELECTOR = '.font-toolbar-hidden'; |
Alex Rudenko | 52767b7 | 2020-06-22 06:26:47 | [diff] [blame] | 40 | const SECTION_SUBTITLE_SELECTOR = '.styles-section-subtitle'; |
Patrick Brosset | 78fa1f5 | 2020-08-17 14:33:48 | [diff] [blame] | 41 | const CLS_PANE_SELECTOR = '.styles-sidebar-toolbar-pane'; |
| 42 | const CLS_BUTTON_SELECTOR = '[aria-label="Element Classes"]'; |
| 43 | const CLS_INPUT_SELECTOR = '[aria-placeholder="Add new class"]'; |
Alex Rudenko | 034db88 | 2020-08-13 12:21:09 | [diff] [blame] | 44 | const LAYOUT_PANE_TAB_SELECTOR = '[aria-label="Layout"]'; |
Patrick Brosset | f398c10 | 2020-10-14 07:35:08 | [diff] [blame] | 45 | const LAYOUT_PANE_TABPANEL_SELECTOR = '[aria-label="Layout panel"]'; |
Patrick Brosset | a3fd937 | 2020-09-22 07:55:35 | [diff] [blame] | 46 | const ADORNER_SELECTOR = 'devtools-adorner'; |
Alex Rudenko | 1cb771b | 2020-08-20 07:02:31 | [diff] [blame] | 47 | export const INACTIVE_GRID_ADORNER_SELECTOR = '[aria-label="Enable grid mode"]'; |
| 48 | export const ACTIVE_GRID_ADORNER_SELECTOR = '[aria-label="Disable grid mode"]'; |
Alex Rudenko | 034db88 | 2020-08-13 12:21:09 | [diff] [blame] | 49 | const ELEMENT_CHECKBOX_IN_LAYOUT_PANE_SELECTOR = '.elements input[type=checkbox]'; |
Alex Rudenko | 651f6d3 | 2021-03-24 07:02:13 | [diff] [blame] | 50 | const ELEMENT_STYLE_SECTION_SELECTOR = '[aria-label="element.style, css selector"]'; |
Changhao Han | ab1d884 | 2021-07-02 10:09:33 | [diff] [blame] | 51 | const STYLE_QUERY_RULE_TEXT_SELECTOR = '.query-text'; |
Al Muthanna Athamina | f385255 | 2023-01-23 16:11:50 | [diff] [blame] | 52 | const STYLE_PROPERTIES_SELECTOR = '.tree-outline-disclosure [role="treeitem"]'; |
Saba Khukhunashvili | 41695dc | 2022-06-28 02:23:23 | [diff] [blame] | 53 | const CSS_AUTHORING_HINTS_ICON_SELECTOR = '.hint'; |
Philip Pfaffe | 32d9de9 | 2023-05-08 07:23:49 | [diff] [blame] | 54 | export const SEARCH_BOX_SELECTOR = '.search-bar'; |
PhistucK | 257ad69 | 2022-08-29 14:36:19 | [diff] [blame] | 55 | const SEARCH_RESULTS_MATCHES = '.search-results-matches'; |
Alex Rudenko | 034db88 | 2020-08-13 12:21:09 | [diff] [blame] | 56 | |
| 57 | export const openLayoutPane = async () => { |
| 58 | await step('Open Layout pane', async () => { |
Alex Rudenko | 034db88 | 2020-08-13 12:21:09 | [diff] [blame] | 59 | await waitFor(LAYOUT_PANE_TAB_SELECTOR); |
| 60 | await click(LAYOUT_PANE_TAB_SELECTOR); |
Patrick Brosset | f398c10 | 2020-10-14 07:35:08 | [diff] [blame] | 61 | |
| 62 | const panel = await waitFor(LAYOUT_PANE_TABPANEL_SELECTOR); |
| 63 | await waitFor('.elements', panel); |
Alex Rudenko | 034db88 | 2020-08-13 12:21:09 | [diff] [blame] | 64 | }); |
| 65 | }; |
| 66 | |
Patrick Brosset | a3fd937 | 2020-09-22 07:55:35 | [diff] [blame] | 67 | export const waitForAdorners = async (expectedAdorners: {textContent: string, isActive: boolean}[]) => { |
| 68 | await waitForFunction(async () => { |
| 69 | const actualAdorners = await $$(ADORNER_SELECTOR); |
| 70 | const actualAdornersStates = await Promise.all(actualAdorners.map(n => { |
| 71 | return n.evaluate((node, activeSelector: string) => { |
Patrick Brosset | d4e59f4 | 2020-11-02 18:18:10 | [diff] [blame] | 72 | // TODO for now only the grid adorner that can be active. When the flex (or other) adorner can be activated |
| 73 | // too we should change the selector passed here crbug.com/1144090. |
Patrick Brosset | a3fd937 | 2020-09-22 07:55:35 | [diff] [blame] | 74 | return {textContent: node.textContent, isActive: node.matches(activeSelector)}; |
| 75 | }, ACTIVE_GRID_ADORNER_SELECTOR); |
| 76 | })); |
Alex Rudenko | 034db88 | 2020-08-13 12:21:09 | [diff] [blame] | 77 | |
Patrick Brosset | a3fd937 | 2020-09-22 07:55:35 | [diff] [blame] | 78 | if (actualAdornersStates.length !== expectedAdorners.length) { |
| 79 | return false; |
| 80 | } |
| 81 | |
| 82 | for (let i = 0; i < actualAdornersStates.length; i++) { |
| 83 | const index = expectedAdorners.findIndex(expected => { |
| 84 | const actual = actualAdornersStates[i]; |
| 85 | return expected.textContent === actual.textContent && expected.isActive === actual.isActive; |
| 86 | }); |
| 87 | if (index !== -1) { |
| 88 | expectedAdorners.splice(index, 1); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | return expectedAdorners.length === 0; |
Alex Rudenko | 034db88 | 2020-08-13 12:21:09 | [diff] [blame] | 93 | }); |
| 94 | }; |
| 95 | |
Philip Pfaffe | 940b191 | 2022-09-30 08:17:51 | [diff] [blame] | 96 | export const waitForSelectedNodeToBeExpanded = async () => { |
| 97 | await waitFor(`${SELECTED_TREE_ELEMENT_SELECTOR}[aria-expanded="true"]`); |
| 98 | }; |
| 99 | |
Patrick Brosset | 24e2e67 | 2020-11-12 09:03:13 | [diff] [blame] | 100 | export const waitForAdornerOnSelectedNode = async (expectedAdornerText: string) => { |
| 101 | await waitForFunction(async () => { |
| 102 | const selectedNode = await waitFor(SELECTED_TREE_ELEMENT_SELECTOR); |
| 103 | const adorner = await waitFor(ADORNER_SELECTOR, selectedNode); |
| 104 | return expectedAdornerText === await adorner.evaluate(node => node.textContent); |
| 105 | }); |
| 106 | }; |
| 107 | |
Alex Rudenko | 034db88 | 2020-08-13 12:21:09 | [diff] [blame] | 108 | export const toggleElementCheckboxInLayoutPane = async () => { |
| 109 | await step('Click element checkbox in Layout pane', async () => { |
| 110 | await waitFor(ELEMENT_CHECKBOX_IN_LAYOUT_PANE_SELECTOR); |
| 111 | await click(ELEMENT_CHECKBOX_IN_LAYOUT_PANE_SELECTOR); |
| 112 | }); |
| 113 | }; |
Tim van der Lippe | 68efc70 | 2020-03-03 17:27:45 | [diff] [blame] | 114 | |
Patrick Brosset | f398c10 | 2020-10-14 07:35:08 | [diff] [blame] | 115 | export const getGridsInLayoutPane = async () => { |
| 116 | const panel = await waitFor(LAYOUT_PANE_TABPANEL_SELECTOR); |
| 117 | return await $$('.elements .element', panel); |
| 118 | }; |
| 119 | |
| 120 | export const waitForSomeGridsInLayoutPane = async (minimumGridCount: number) => { |
| 121 | await waitForFunction(async () => { |
| 122 | const grids = await getGridsInLayoutPane(); |
| 123 | return grids.length >= minimumGridCount; |
| 124 | }); |
| 125 | }; |
| 126 | |
Johan Bay | 00fc92a | 2020-08-14 20:06:20 | [diff] [blame] | 127 | export const waitForContentOfSelectedElementsNode = async (expectedTextContent: string) => { |
| 128 | await waitForFunction(async () => { |
Mathias Bynens | 00e1aac | 2020-12-21 14:57:12 | [diff] [blame] | 129 | const selectedTextContent = await getContentOfSelectedNode(); |
Johan Bay | 00fc92a | 2020-08-14 20:06:20 | [diff] [blame] | 130 | return selectedTextContent === expectedTextContent; |
| 131 | }); |
Tim van der Lippe | 81aeee7 | 2020-03-09 16:14:28 | [diff] [blame] | 132 | }; |
Tim van der Lippe | 68efc70 | 2020-03-03 17:27:45 | [diff] [blame] | 133 | |
Changhao Han | 59173bb | 2021-08-11 13:29:25 | [diff] [blame] | 134 | export const waitForPartialContentOfSelectedElementsNode = async (expectedPartialTextContent: string) => { |
| 135 | await waitForFunction(async () => { |
| 136 | const selectedTextContent = await getContentOfSelectedNode(); |
| 137 | return selectedTextContent.includes(expectedPartialTextContent); |
| 138 | }); |
| 139 | }; |
| 140 | |
Paul Lewis | ab0c65c | 2020-03-27 10:25:09 | [diff] [blame] | 141 | /** |
| 142 | * Gets the text content of the currently selected element. |
| 143 | */ |
| 144 | export const getContentOfSelectedNode = async () => { |
Johan Bay | 504a6f1 | 2020-08-04 13:32:53 | [diff] [blame] | 145 | const selectedNode = await waitFor(SELECTED_TREE_ELEMENT_SELECTOR); |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 146 | return await selectedNode.evaluate(node => node.textContent as string); |
Paul Lewis | ab0c65c | 2020-03-27 10:25:09 | [diff] [blame] | 147 | }; |
| 148 | |
Philip Pfaffe | a1f88f5 | 2023-04-20 08:31:07 | [diff] [blame] | 149 | export const waitForSelectedNodeChange = async(initialValue: string, asyncScope = new AsyncScope()): Promise<void> => { |
| 150 | await waitForFunction(async () => { |
Paul Lewis | ab0c65c | 2020-03-27 10:25:09 | [diff] [blame] | 151 | const currentContent = await getContentOfSelectedNode(); |
Philip Pfaffe | a1f88f5 | 2023-04-20 08:31:07 | [diff] [blame] | 152 | return currentContent !== initialValue; |
| 153 | }, asyncScope); |
Paul Lewis | ab0c65c | 2020-03-27 10:25:09 | [diff] [blame] | 154 | }; |
| 155 | |
Jack Franklin | a35ca2b | 2020-03-27 14:05:01 | [diff] [blame] | 156 | export const assertSelectedElementsNodeTextIncludes = async (expectedTextContent: string) => { |
Johan Bay | 504a6f1 | 2020-08-04 13:32:53 | [diff] [blame] | 157 | const selectedNode = await waitFor(SELECTED_TREE_ELEMENT_SELECTOR); |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 158 | const selectedTextContent = await selectedNode.evaluate(node => node.textContent as string); |
Jack Franklin | a35ca2b | 2020-03-27 14:05:01 | [diff] [blame] | 159 | assert.include(selectedTextContent, expectedTextContent); |
| 160 | }; |
| 161 | |
Tim van der Lippe | 869374b | 2020-04-20 10:12:31 | [diff] [blame] | 162 | export const waitForSelectedTreeElementSelectorWithTextcontent = async (expectedTextContent: string) => { |
| 163 | await waitForFunction(async () => { |
Johan Bay | 504a6f1 | 2020-08-04 13:32:53 | [diff] [blame] | 164 | const selectedNode = await waitFor(SELECTED_TREE_ELEMENT_SELECTOR); |
Tim van der Lippe | 869374b | 2020-04-20 10:12:31 | [diff] [blame] | 165 | const selectedTextContent = await selectedNode.evaluate(node => node.textContent); |
| 166 | return selectedTextContent === expectedTextContent; |
Philip Pfaffe | 6d143ae | 2020-07-31 11:32:22 | [diff] [blame] | 167 | }); |
Tim van der Lippe | 869374b | 2020-04-20 10:12:31 | [diff] [blame] | 168 | }; |
| 169 | |
Kateryna Prokopenko | 314d6b7 | 2020-08-19 06:33:00 | [diff] [blame] | 170 | export const waitForSelectedTreeElementSelectorWhichIncludesText = async (expectedTextContent: string) => { |
| 171 | await waitForFunction(async () => { |
| 172 | const selectedNode = await waitFor(SELECTED_TREE_ELEMENT_SELECTOR); |
| 173 | const selectedTextContent = await selectedNode.evaluate(node => node.textContent); |
| 174 | return selectedTextContent && selectedTextContent.includes(expectedTextContent); |
| 175 | }); |
| 176 | }; |
| 177 | |
Tim van der Lippe | 81aeee7 | 2020-03-09 16:14:28 | [diff] [blame] | 178 | export const waitForChildrenOfSelectedElementNode = async () => { |
Tim van der Lippe | 68efc70 | 2020-03-03 17:27:45 | [diff] [blame] | 179 | await waitFor(`${SELECTED_TREE_ELEMENT_SELECTOR} + ol > li`); |
Tim van der Lippe | 81aeee7 | 2020-03-09 16:14:28 | [diff] [blame] | 180 | }; |
| 181 | |
Johan Bay | 8b2613a | 2022-06-02 12:21:26 | [diff] [blame] | 182 | export const waitForAndClickTreeElementWithPartialText = async (text: string) => |
| 183 | waitForFunction(async () => clickTreeElementWithPartialText(text)); |
| 184 | |
Danil Somsikov | 9a49c71 | 2022-06-29 14:56:24 | [diff] [blame] | 185 | export const waitForElementWithPartialText = async (text: string) => { |
| 186 | return waitForFunction(async () => elementWithPartialText(text)); |
| 187 | }; |
| 188 | |
| 189 | const elementWithPartialText = async (text: string) => { |
Johan Bay | 8b2613a | 2022-06-02 12:21:26 | [diff] [blame] | 190 | const tree = await waitFor('Page DOM[role="tree"]', undefined, undefined, 'aria'); |
| 191 | const elements = await $$('[role="treeitem"]', tree, 'aria'); |
| 192 | for (const handle of elements) { |
| 193 | const match = await handle.evaluate((element, text) => element.textContent?.includes(text), text); |
| 194 | if (match) { |
Danil Somsikov | 9a49c71 | 2022-06-29 14:56:24 | [diff] [blame] | 195 | return handle; |
Johan Bay | 8b2613a | 2022-06-02 12:21:26 | [diff] [blame] | 196 | } |
| 197 | } |
Danil Somsikov | 9a49c71 | 2022-06-29 14:56:24 | [diff] [blame] | 198 | return null; |
| 199 | }; |
| 200 | |
| 201 | export const clickTreeElementWithPartialText = async (text: string) => { |
| 202 | const handle = await elementWithPartialText(text); |
| 203 | if (handle) { |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 204 | await clickElement(handle); |
Danil Somsikov | 9a49c71 | 2022-06-29 14:56:24 | [diff] [blame] | 205 | return true; |
| 206 | } |
| 207 | |
Johan Bay | 8b2613a | 2022-06-02 12:21:26 | [diff] [blame] | 208 | throw false; |
| 209 | }; |
| 210 | |
Alex Rudenko | 3e3197d | 2021-03-22 12:18:33 | [diff] [blame] | 211 | export const clickNthChildOfSelectedElementNode = async (childIndex: number) => { |
| 212 | assert(childIndex > 0, 'CSS :nth-child() selector indices are 1-based.'); |
| 213 | const element = await waitFor(`${SELECTED_TREE_ELEMENT_SELECTOR} + ol > li:nth-child(${childIndex})`); |
| 214 | await element.click(); |
| 215 | }; |
| 216 | |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 217 | export const focusElementsTree = async () => { |
| 218 | await click(SELECTED_TREE_ELEMENT_SELECTOR); |
| 219 | }; |
| 220 | |
| 221 | export const navigateToSidePane = async (paneName: string) => { |
| 222 | await click(`[aria-label="${paneName}"]`); |
| 223 | await waitFor(`[aria-label="${paneName} panel"]`); |
| 224 | }; |
| 225 | |
Tim van der Lippe | 81aeee7 | 2020-03-09 16:14:28 | [diff] [blame] | 226 | export const waitForElementsStyleSection = async () => { |
| 227 | // Wait for the file to be loaded and selectors to be shown |
| 228 | await waitFor('.styles-selector'); |
| 229 | }; |
| 230 | |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 231 | export const waitForElementsComputedSection = async () => { |
| 232 | await waitFor(COMPUTED_PROPERTY_SELECTOR); |
| 233 | }; |
| 234 | |
| 235 | export const getContentOfComputedPane = async () => { |
Johan Bay | 86a70e0 | 2022-05-25 08:01:46 | [diff] [blame] | 236 | const pane = await waitFor('Computed panel', undefined, undefined, 'aria'); |
| 237 | const tree = await waitFor('[role="tree"]', pane, undefined, 'aria'); |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 238 | return await tree.evaluate(node => node.textContent as string); |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 239 | }; |
| 240 | |
| 241 | export const waitForComputedPaneChange = async (initialValue: string) => { |
| 242 | await waitForFunction(async () => { |
| 243 | const value = await getContentOfComputedPane(); |
| 244 | return value !== initialValue; |
Philip Pfaffe | 6d143ae | 2020-07-31 11:32:22 | [diff] [blame] | 245 | }); |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 246 | }; |
| 247 | |
| 248 | export const getAllPropertiesFromComputedPane = async () => { |
| 249 | const properties = await $$(COMPUTED_PROPERTY_SELECTOR); |
Jack Franklin | 7fc544b | 2023-01-03 11:50:34 | [diff] [blame] | 250 | return (await Promise.all(properties.map(elem => elem.evaluate(async node => { |
| 251 | const nameSlot = node.shadowRoot?.querySelector<HTMLSlotElement>('.property-name slot'); |
| 252 | const valueSlot = node.shadowRoot?.querySelector<HTMLSlotElement>('.property-value slot'); |
| 253 | const name = nameSlot?.assignedElements().at(0); |
| 254 | const value = valueSlot?.assignedElements().at(0); |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 255 | |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 256 | return (!name || !value) ? null : { |
| 257 | name: name.textContent ? name.textContent.trim().replace(/:$/, '') : '', |
| 258 | value: value.textContent ? value.textContent.trim().replace(/;$/, '') : '', |
| 259 | }; |
| 260 | })))) |
Tim van der Lippe | ed61abd | 2020-12-16 15:11:16 | [diff] [blame] | 261 | .filter(prop => Boolean(prop)); |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 262 | }; |
| 263 | |
Patrick Brosset | 394c92f | 2020-10-28 17:25:14 | [diff] [blame] | 264 | export const getPropertyFromComputedPane = async (name: string) => { |
| 265 | const properties = await $$(COMPUTED_PROPERTY_SELECTOR); |
| 266 | for (const property of properties) { |
Johan Bay | 86a70e0 | 2022-05-25 08:01:46 | [diff] [blame] | 267 | const matchingProperty = await property.evaluate((node, name) => { |
Simon Zünd | 9e2c759 | 2023-01-17 07:59:41 | [diff] [blame] | 268 | const nameSlot = node.shadowRoot?.querySelector<HTMLSlotElement>('.property-name slot'); |
Jack Franklin | 7fc544b | 2023-01-03 11:50:34 | [diff] [blame] | 269 | const nameEl = nameSlot?.assignedElements().at(0); |
Johan Bay | 86a70e0 | 2022-05-25 08:01:46 | [diff] [blame] | 270 | return nameEl?.textContent?.trim().replace(/:$/, '') === name; |
Patrick Brosset | 394c92f | 2020-10-28 17:25:14 | [diff] [blame] | 271 | }, name); |
| 272 | // Note that evaluateHandle always returns a handle, even if it points to an undefined remote object, so we need to |
| 273 | // check it's defined here or continue iterating. |
Johan Bay | 86a70e0 | 2022-05-25 08:01:46 | [diff] [blame] | 274 | if (matchingProperty) { |
| 275 | return property; |
Patrick Brosset | 394c92f | 2020-10-28 17:25:14 | [diff] [blame] | 276 | } |
| 277 | } |
| 278 | return undefined; |
| 279 | }; |
| 280 | |
Patrick Brosset | 6e337aa | 2020-10-29 14:25:38 | [diff] [blame] | 281 | export const waitForPropertyValueInComputedPane = async (name: string, value: string) => { |
| 282 | await waitForFunction(async () => { |
| 283 | const properties = await getAllPropertiesFromComputedPane(); |
| 284 | for (const property of properties) { |
| 285 | if (property && property.name === name && property.value === value) { |
| 286 | return true; |
| 287 | } |
| 288 | } |
| 289 | return false; |
| 290 | }); |
| 291 | }; |
| 292 | |
Paul Lewis | ab0c65c | 2020-03-27 10:25:09 | [diff] [blame] | 293 | export const expandSelectedNodeRecursively = async () => { |
| 294 | const EXPAND_RECURSIVELY = '[aria-label="Expand recursively"]'; |
| 295 | |
| 296 | // Find the selected node, right click. |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 297 | await click(SELECTED_TREE_ELEMENT_SELECTOR, {clickOptions: {button: 'right'}}); |
Paul Lewis | ab0c65c | 2020-03-27 10:25:09 | [diff] [blame] | 298 | |
| 299 | // Wait for the 'expand recursively' option, and click it. |
| 300 | await waitFor(EXPAND_RECURSIVELY); |
| 301 | await click(EXPAND_RECURSIVELY); |
| 302 | }; |
| 303 | |
Tim van der Lippe | 81aeee7 | 2020-03-09 16:14:28 | [diff] [blame] | 304 | export const forcePseudoState = async (pseudoState: string) => { |
| 305 | // Open element state pane and wait for it to be loaded asynchronously |
| 306 | await click('[aria-label="Toggle Element State"]'); |
| 307 | await waitFor(`[aria-label="${pseudoState}"]`); |
Johan Bay | 00fc92a | 2020-08-14 20:06:20 | [diff] [blame] | 308 | // FIXME(crbug/1112692): Refactor test to remove the timeout. |
| 309 | await timeout(100); |
Tim van der Lippe | 81aeee7 | 2020-03-09 16:14:28 | [diff] [blame] | 310 | await click(`[aria-label="${pseudoState}"]`); |
| 311 | }; |
| 312 | |
Tim van der Lippe | 01e70f1 | 2020-03-10 18:34:50 | [diff] [blame] | 313 | export const removePseudoState = async (pseudoState: string) => { |
| 314 | await click(`[aria-label="${pseudoState}"]`); |
| 315 | }; |
Tim van der Lippe | 81aeee7 | 2020-03-09 16:14:28 | [diff] [blame] | 316 | |
Randolf | cc89254 | 2023-01-27 23:44:07 | [diff] [blame] | 317 | export const getComputedStylesForDomNode = |
| 318 | async (elementSelector: string, styleAttribute: keyof CSSStyleDeclaration) => { |
Tim van der Lippe | 01e70f1 | 2020-03-10 18:34:50 | [diff] [blame] | 319 | const {target} = getBrowserAndPages(); |
Tim van der Lippe | 81aeee7 | 2020-03-09 16:14:28 | [diff] [blame] | 320 | |
| 321 | return target.evaluate((elementSelector, styleAttribute) => { |
| 322 | const element = document.querySelector(elementSelector); |
| 323 | if (!element) { |
| 324 | throw new Error(`${elementSelector} could not be found`); |
| 325 | } |
Simon Zünd | 9e2c759 | 2023-01-17 07:59:41 | [diff] [blame] | 326 | return getComputedStyle(element)[styleAttribute]; |
Tim van der Lippe | 81aeee7 | 2020-03-09 16:14:28 | [diff] [blame] | 327 | }, elementSelector, styleAttribute); |
| 328 | }; |
| 329 | |
Johan Bay | 86a70e0 | 2022-05-25 08:01:46 | [diff] [blame] | 330 | export const waitForNumberOfComputedProperties = async (numberToWaitFor: number) => { |
| 331 | const computedPane = await getComputedPanel(); |
| 332 | return waitForFunction( |
| 333 | async () => numberToWaitFor === |
| 334 | await computedPane.$$eval('pierce/' + COMPUTED_PROPERTY_SELECTOR, properties => properties.length)); |
| 335 | }; |
| 336 | |
| 337 | export const getComputedPanel = async () => waitFor(COMPUTED_STYLES_PANEL_SELECTOR); |
| 338 | |
| 339 | export const filterComputedProperties = async (filterString: string) => { |
| 340 | const initialContent = await getContentOfComputedPane(); |
| 341 | |
| 342 | const computedPanel = await waitFor(COMPUTED_STYLES_PANEL_SELECTOR); |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 343 | await click('[aria-label="Filter Computed Styles"]', { |
| 344 | root: computedPanel, |
| 345 | }); |
Johan Bay | 86a70e0 | 2022-05-25 08:01:46 | [diff] [blame] | 346 | await typeText(filterString); |
| 347 | await waitForComputedPaneChange(initialContent); |
| 348 | }; |
| 349 | |
Changhao Han | 2bcf0b8 | 2020-07-17 09:32:08 | [diff] [blame] | 350 | export const toggleShowAllComputedProperties = async () => { |
| 351 | const initialContent = await getContentOfComputedPane(); |
| 352 | |
Johan Bay | 504a6f1 | 2020-08-04 13:32:53 | [diff] [blame] | 353 | const computedPanel = await waitFor(COMPUTED_STYLES_PANEL_SELECTOR); |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 354 | await click(COMPUTED_STYLES_SHOW_ALL_SELECTOR, {root: computedPanel}); |
Changhao Han | 2bcf0b8 | 2020-07-17 09:32:08 | [diff] [blame] | 355 | await waitForComputedPaneChange(initialContent); |
| 356 | }; |
| 357 | |
Changhao Han | ca13f09 | 2020-09-03 04:28:43 | [diff] [blame] | 358 | export const toggleGroupComputedProperties = async () => { |
| 359 | const computedPanel = await waitFor(COMPUTED_STYLES_PANEL_SELECTOR); |
| 360 | const groupCheckbox = await waitFor(COMPUTED_STYLES_GROUP_SELECTOR, computedPanel); |
| 361 | |
| 362 | const wasChecked = await groupCheckbox.evaluate(checkbox => (checkbox as HTMLInputElement).checked); |
| 363 | |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 364 | await click(COMPUTED_STYLES_GROUP_SELECTOR, { |
| 365 | root: computedPanel, |
| 366 | }); |
Changhao Han | ca13f09 | 2020-09-03 04:28:43 | [diff] [blame] | 367 | |
| 368 | if (wasChecked) { |
Changhao Han | bb4af12 | 2020-09-10 13:45:25 | [diff] [blame] | 369 | await waitFor('[role="tree"].alphabetical-list', computedPanel); |
Changhao Han | ca13f09 | 2020-09-03 04:28:43 | [diff] [blame] | 370 | } else { |
Changhao Han | bb4af12 | 2020-09-10 13:45:25 | [diff] [blame] | 371 | await waitFor('[role="tree"].grouped-list', computedPanel); |
Changhao Han | ca13f09 | 2020-09-03 04:28:43 | [diff] [blame] | 372 | } |
| 373 | }; |
| 374 | |
Mathias Bynens | 78e6594 | 2020-03-24 13:25:11 | [diff] [blame] | 375 | export const waitForDomNodeToBeVisible = async (elementSelector: string) => { |
Tim van der Lippe | 01e70f1 | 2020-03-10 18:34:50 | [diff] [blame] | 376 | const {target} = getBrowserAndPages(); |
| 377 | |
| 378 | // DevTools will force Blink to make the hover shown, so we have |
| 379 | // to wait for the element to be DOM-visible (e.g. no `display: none;`) |
| 380 | await target.waitForSelector(elementSelector, {visible: true}); |
| 381 | }; |
| 382 | |
Mathias Bynens | 78e6594 | 2020-03-24 13:25:11 | [diff] [blame] | 383 | export const waitForDomNodeToBeHidden = async (elementSelector: string) => { |
Tim van der Lippe | 01e70f1 | 2020-03-10 18:34:50 | [diff] [blame] | 384 | const {target} = getBrowserAndPages(); |
| 385 | await target.waitForSelector(elementSelector, {hidden: true}); |
| 386 | }; |
| 387 | |
Mathias Bynens | 78e6594 | 2020-03-24 13:25:11 | [diff] [blame] | 388 | export const assertGutterDecorationForDomNodeExists = async () => { |
Tim van der Lippe | 81aeee7 | 2020-03-09 16:14:28 | [diff] [blame] | 389 | await waitFor('.elements-gutter-decoration'); |
| 390 | }; |
Changhao Han | d1b1b5f | 2020-03-27 12:00:53 | [diff] [blame] | 391 | |
Patrick Brosset | d93ee76 | 2020-10-05 12:33:28 | [diff] [blame] | 392 | export const getStyleRuleSelector = (selector: string) => `[aria-label="${selector}, css selector"]`; |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 393 | |
| 394 | export const waitForStyleRule = async (expectedSelector: string) => { |
| 395 | await waitForFunction(async () => { |
| 396 | const rules = await getDisplayedStyleRules(); |
| 397 | return rules.map(rule => rule.selectorText).includes(expectedSelector); |
Philip Pfaffe | 6d143ae | 2020-07-31 11:32:22 | [diff] [blame] | 398 | }); |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 399 | }; |
| 400 | |
Johan Bay | 86a70e0 | 2022-05-25 08:01:46 | [diff] [blame] | 401 | export const getComputedStyleProperties = async () => { |
| 402 | const computedPanel = await getComputedPanel(); |
| 403 | const allProperties = await computedPanel.$$('pierce/[role="treeitem"][aria-level="1"]'); |
| 404 | const properties = []; |
| 405 | for (const prop of allProperties) { |
| 406 | const name = await prop.$eval('pierce/' + CSS_PROPERTY_NAME_SELECTOR, element => element.textContent); |
| 407 | const value = await prop.$eval('pierce/' + CSS_PROPERTY_VALUE_SELECTOR, element => element.textContent); |
| 408 | const traceElements = await prop.$$('pierce/devtools-computed-style-trace'); |
| 409 | const trace = await Promise.all(traceElements.map(async element => { |
| 410 | const value = await element.$eval('pierce/.value', element => element.textContent); |
| 411 | const selector = await element.$eval('pierce/.trace-selector', element => element.textContent); |
| 412 | const link = await element.$eval('pierce/.trace-link', element => element.textContent); |
| 413 | return {value, selector, link}; |
| 414 | })); |
| 415 | properties.push({name, value, trace}); |
| 416 | } |
| 417 | return properties; |
| 418 | }; |
| 419 | |
Changhao Han | 7bdd445 | 2022-08-26 11:03:48 | [diff] [blame] | 420 | export const getDisplayedCSSDeclarations = async () => { |
Philip Pfaffe | e558072 | 2022-09-30 14:55:38 | [diff] [blame] | 421 | const cssDeclarations = await $$(CSS_DECLARATION_SELECTOR); |
| 422 | return Promise.all(cssDeclarations.map(async node => await node.evaluate(n => n.textContent?.trim()))); |
Changhao Han | 7bdd445 | 2022-08-26 11:03:48 | [diff] [blame] | 423 | }; |
| 424 | |
Johan Bay | 86a70e0 | 2022-05-25 08:01:46 | [diff] [blame] | 425 | export const getDisplayedStyleRulesCompact = async () => { |
| 426 | const compactRules = []; |
| 427 | for (const rule of await getDisplayedStyleRules()) { |
| 428 | compactRules.push( |
| 429 | {selectorText: rule.selectorText, propertyNames: rule.propertyData.map(data => data.propertyName)}); |
| 430 | } |
| 431 | return compactRules; |
| 432 | }; |
| 433 | |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 434 | export const getDisplayedStyleRules = async () => { |
| 435 | const allRuleSelectors = await $$(CSS_STYLE_RULE_SELECTOR); |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 436 | const rules = []; |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 437 | for (const ruleSelector of allRuleSelectors) { |
Dan Clark | 621ed50 | 2022-04-21 00:17:44 | [diff] [blame] | 438 | const propertyData = await getDisplayedCSSPropertyData(ruleSelector); |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 439 | const selectorText = await ruleSelector.evaluate(node => { |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 440 | const attribute = node.getAttribute('aria-label') || ''; |
| 441 | return attribute.substring(0, attribute.lastIndexOf(', css selector')); |
| 442 | }); |
Dan Clark | 621ed50 | 2022-04-21 00:17:44 | [diff] [blame] | 443 | rules.push({selectorText, propertyData}); |
Patrick Brosset | ba95542 | 2020-05-22 07:33:29 | [diff] [blame] | 444 | } |
| 445 | |
| 446 | return rules; |
| 447 | }; |
| 448 | |
Dan Clark | 621ed50 | 2022-04-21 00:17:44 | [diff] [blame] | 449 | /** |
| 450 | * @param propertiesSection - The element containing this properties section. |
| 451 | * @returns an array with an entry for each property in the section. Each entry has: |
| 452 | * - propertyName: The name of this property. |
| 453 | * - isOverloaded: True if this is an inherited properties section, and this property is overloaded by a child node. |
| 454 | * The property will be shown as crossed out in the style pane. |
| 455 | * - isInherited: True if this is an inherited properties section, and this property is a non-inherited CSS property. |
| 456 | * The property will be shown as grayed-out in the style pane. |
| 457 | */ |
| 458 | export const getDisplayedCSSPropertyData = async (propertiesSection: puppeteer.ElementHandle<Element>) => { |
| 459 | const cssPropertyNames = await $$(CSS_PROPERTY_NAME_SELECTOR, propertiesSection); |
| 460 | const propertyNamesData = |
| 461 | (await Promise.all(cssPropertyNames.map( |
| 462 | async node => { |
| 463 | return { |
| 464 | propertyName: await node.evaluate(n => n.textContent), |
| 465 | isOverLoaded: await node.evaluate(n => n.parentElement && n.parentElement.matches('.overloaded')), |
| 466 | isInherited: await node.evaluate(n => n.parentElement && n.parentElement.matches('.inherited')), |
| 467 | }; |
| 468 | }, |
| 469 | ))) |
| 470 | .filter(c => Boolean(c.propertyName)); |
| 471 | return propertyNamesData; |
| 472 | }; |
| 473 | |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 474 | export const getDisplayedCSSPropertyNames = async (propertiesSection: puppeteer.ElementHandle<Element>) => { |
Changhao Han | d1b1b5f | 2020-03-27 12:00:53 | [diff] [blame] | 475 | const cssPropertyNames = await $$(CSS_PROPERTY_NAME_SELECTOR, propertiesSection); |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 476 | const propertyNamesText = (await Promise.all(cssPropertyNames.map( |
| 477 | node => node.evaluate(n => n.textContent), |
| 478 | ))) |
Tim van der Lippe | ed61abd | 2020-12-16 15:11:16 | [diff] [blame] | 479 | .filter(c => Boolean(c)); |
Changhao Han | d1b1b5f | 2020-03-27 12:00:53 | [diff] [blame] | 480 | return propertyNamesText; |
| 481 | }; |
Jack Franklin | a35ca2b | 2020-03-27 14:05:01 | [diff] [blame] | 482 | |
Johan Bay | 504a6f1 | 2020-08-04 13:32:53 | [diff] [blame] | 483 | export const getStyleRule = (selector: string) => { |
Patrick Brosset | d93ee76 | 2020-10-05 12:33:28 | [diff] [blame] | 484 | return waitFor(getStyleRuleSelector(selector)); |
Patrick Brosset | a340d9e | 2020-05-26 11:05:40 | [diff] [blame] | 485 | }; |
| 486 | |
Sigurd Schneider | a7d1bb1 | 2021-04-30 06:45:37 | [diff] [blame] | 487 | export const getStyleRuleWithSourcePosition = (styleSelector: string, sourcePosition?: string) => { |
| 488 | if (!sourcePosition) { |
| 489 | return getStyleRule(styleSelector); |
| 490 | } |
| 491 | const selector = getStyleRuleSelector(styleSelector); |
| 492 | return waitForFunction(async () => { |
| 493 | const candidate = await waitFor(selector); |
| 494 | if (candidate) { |
| 495 | const sourcePositionElement = await candidate.$('.styles-section-subtitle .devtools-link'); |
| 496 | const text = await sourcePositionElement?.evaluate(node => node.textContent); |
| 497 | if (text === sourcePosition) { |
| 498 | return candidate; |
| 499 | } |
| 500 | } |
| 501 | return undefined; |
| 502 | }); |
| 503 | }; |
| 504 | |
Philip Pfaffe | 7fb6a83 | 2022-11-14 13:39:47 | [diff] [blame] | 505 | export const getColorSwatch = async (parent: puppeteer.ElementHandle<Element>|undefined, index: number) => { |
Patrick Brosset | 394c92f | 2020-10-28 17:25:14 | [diff] [blame] | 506 | const swatches = await $$(COLOR_SWATCH_SELECTOR, parent); |
| 507 | return swatches[index]; |
| 508 | }; |
| 509 | |
| 510 | export const getColorSwatchColor = async (parent: puppeteer.ElementHandle<Element>, index: number) => { |
| 511 | const swatch = await getColorSwatch(parent, index); |
| 512 | return await swatch.evaluate(node => (node as HTMLElement).style.backgroundColor); |
| 513 | }; |
| 514 | |
| 515 | export const shiftClickColorSwatch = async (ruleSection: puppeteer.ElementHandle<Element>, index: number) => { |
| 516 | const swatch = await getColorSwatch(ruleSection, index); |
| 517 | const {frontend} = getBrowserAndPages(); |
| 518 | await frontend.keyboard.down('Shift'); |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 519 | await clickElement(swatch); |
Patrick Brosset | 394c92f | 2020-10-28 17:25:14 | [diff] [blame] | 520 | await frontend.keyboard.up('Shift'); |
Alex Rudenko | 0feb30f | 2020-06-09 06:43:00 | [diff] [blame] | 521 | }; |
| 522 | |
Alex Rudenko | 651f6d3 | 2021-03-24 07:02:13 | [diff] [blame] | 523 | export const getElementStyleFontEditorButton = async () => { |
| 524 | const section = await waitFor(ELEMENT_STYLE_SECTION_SELECTOR); |
| 525 | return await $(FONT_EDITOR_SELECTOR, section); |
| 526 | }; |
| 527 | |
Michael Liao | 6035f4a | 2020-12-01 19:12:45 | [diff] [blame] | 528 | export const getFontEditorButtons = async () => { |
| 529 | const buttons = await $$(FONT_EDITOR_SELECTOR); |
| 530 | return buttons; |
| 531 | }; |
| 532 | |
| 533 | export const getHiddenFontEditorButtons = async () => { |
| 534 | const buttons = await $$(HIDDEN_FONT_EDITOR_SELECTOR); |
| 535 | return buttons; |
| 536 | }; |
| 537 | |
Alex Rudenko | 52767b7 | 2020-06-22 06:26:47 | [diff] [blame] | 538 | export const getStyleSectionSubtitles = async () => { |
| 539 | const subtitles = await $$(SECTION_SUBTITLE_SELECTOR); |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 540 | return Promise.all(subtitles.map(node => node.evaluate(n => n.textContent))); |
Alex Rudenko | 52767b7 | 2020-06-22 06:26:47 | [diff] [blame] | 541 | }; |
| 542 | |
Sigurd Schneider | a7d1bb1 | 2021-04-30 06:45:37 | [diff] [blame] | 543 | export const getCSSPropertyInRule = |
| 544 | async (ruleSection: puppeteer.ElementHandle<Element>|string, name: string, sourcePosition?: string) => { |
Patrick Brosset | b49605b | 2020-10-12 12:35:04 | [diff] [blame] | 545 | if (typeof ruleSection === 'string') { |
Sigurd Schneider | a7d1bb1 | 2021-04-30 06:45:37 | [diff] [blame] | 546 | ruleSection = await getStyleRuleWithSourcePosition(ruleSection, sourcePosition); |
Patrick Brosset | b49605b | 2020-10-12 12:35:04 | [diff] [blame] | 547 | } |
| 548 | |
Patrick Brosset | a340d9e | 2020-05-26 11:05:40 | [diff] [blame] | 549 | const propertyNames = await $$(CSS_PROPERTY_NAME_SELECTOR, ruleSection); |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 550 | for (const node of propertyNames) { |
| 551 | const parent = |
Randolf | cc89254 | 2023-01-27 23:44:07 | [diff] [blame] | 552 | (await node.evaluateHandle((node, name) => (name === node.textContent) ? node.parentNode : undefined, name)) |
| 553 | .asElement(); |
| 554 | if (parent) { |
| 555 | return parent as puppeteer.ElementHandle<HTMLElement>; |
Johan Bay | e824571 | 2020-08-04 13:32:10 | [diff] [blame] | 556 | } |
| 557 | } |
| 558 | return undefined; |
Patrick Brosset | a340d9e | 2020-05-26 11:05:40 | [diff] [blame] | 559 | }; |
| 560 | |
| 561 | export const focusCSSPropertyValue = async (selector: string, propertyName: string) => { |
| 562 | await waitForStyleRule(selector); |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 563 | let property = await getCSSPropertyInRule(selector, propertyName); |
| 564 | // Clicking on the semicolon element to make sure we don't hit the swatch or other |
| 565 | // non-editable elements. |
| 566 | await click(CSS_PROPERTY_VALUE_SELECTOR + ' + .styles-semicolon', {root: property}); |
| 567 | await waitForFunction(async () => { |
| 568 | property = await getCSSPropertyInRule(selector, propertyName); |
| 569 | const value = await $(CSS_PROPERTY_VALUE_SELECTOR, property); |
| 570 | if (!value) { |
| 571 | assert.fail(`Could not find property ${propertyName} in rule ${selector}`); |
| 572 | } |
| 573 | return await value.evaluate(node => { |
| 574 | return node.classList.contains('text-prompt') && node.hasAttribute('contenteditable'); |
| 575 | }); |
| 576 | }); |
Patrick Brosset | a340d9e | 2020-05-26 11:05:40 | [diff] [blame] | 577 | }; |
| 578 | |
Patrick Brosset | b49605b | 2020-10-12 12:35:04 | [diff] [blame] | 579 | /** |
| 580 | * Edit a CSS property value in a given rule |
| 581 | * @param selector The selector of the rule to be updated. Note that because of the way the Styles populates, it is |
| 582 | * important to provide a rule selector that is unique here, to avoid editing a property in the wrong rule. |
| 583 | * @param propertyName The name of the property to be found and edited. If several properties have the same names, the |
| 584 | * first one is edited. |
| 585 | * @param newValue The new value to be used. |
| 586 | */ |
Patrick Brosset | a340d9e | 2020-05-26 11:05:40 | [diff] [blame] | 587 | export async function editCSSProperty(selector: string, propertyName: string, newValue: string) { |
| 588 | await focusCSSPropertyValue(selector, propertyName); |
| 589 | |
| 590 | const {frontend} = getBrowserAndPages(); |
Patrick Brosset | b49605b | 2020-10-12 12:35:04 | [diff] [blame] | 591 | await frontend.keyboard.type(newValue, {delay: 100}); |
Patrick Brosset | a340d9e | 2020-05-26 11:05:40 | [diff] [blame] | 592 | await frontend.keyboard.press('Enter'); |
Patrick Brosset | b49605b | 2020-10-12 12:35:04 | [diff] [blame] | 593 | |
| 594 | await waitForFunction(async () => { |
| 595 | // Wait until the value element is not a text-prompt anymore. |
| 596 | const property = await getCSSPropertyInRule(selector, propertyName); |
| 597 | const value = await $(CSS_PROPERTY_VALUE_SELECTOR, property); |
| 598 | if (!value) { |
| 599 | assert.fail(`Could not find property ${propertyName} in rule ${selector}`); |
| 600 | } |
| 601 | return await value.evaluate(node => { |
| 602 | return !node.classList.contains('text-prompt') && !node.hasAttribute('contenteditable'); |
| 603 | }); |
| 604 | }); |
Patrick Brosset | a340d9e | 2020-05-26 11:05:40 | [diff] [blame] | 605 | } |
| 606 | |
Changhao Han | 46cdd9c | 2021-06-10 19:45:59 | [diff] [blame] | 607 | // Edit a media or container query rule text for the given styles section |
| 608 | export async function editQueryRuleText(queryStylesSections: puppeteer.ElementHandle<Element>, newQueryText: string) { |
| 609 | await click(STYLE_QUERY_RULE_TEXT_SELECTOR, {root: queryStylesSections}); |
Johan Bay | 3968cbe | 2021-12-06 10:28:06 | [diff] [blame] | 610 | await waitForFunction(async () => { |
| 611 | // Wait until the value element has been marked as a text-prompt. |
| 612 | const queryText = await $(STYLE_QUERY_RULE_TEXT_SELECTOR, queryStylesSections); |
| 613 | if (!queryText) { |
| 614 | assert.fail('Could not find any query in the given styles section'); |
| 615 | } |
| 616 | const check = await queryText.evaluate(node => { |
| 617 | return node.classList.contains('being-edited') && node.hasAttribute('contenteditable'); |
| 618 | }); |
| 619 | return check; |
| 620 | }); |
| 621 | await typeText(newQueryText); |
| 622 | await pressKey('Enter'); |
Changhao Han | 46cdd9c | 2021-06-10 19:45:59 | [diff] [blame] | 623 | |
| 624 | await waitForFunction(async () => { |
| 625 | // Wait until the value element is not a text-prompt anymore. |
| 626 | const queryText = await $(STYLE_QUERY_RULE_TEXT_SELECTOR, queryStylesSections); |
| 627 | if (!queryText) { |
| 628 | assert.fail('Could not find any query in the given styles section'); |
| 629 | } |
Johan Bay | 3968cbe | 2021-12-06 10:28:06 | [diff] [blame] | 630 | const check = await queryText.evaluate(node => { |
| 631 | return !node.classList.contains('being-edited') && !node.hasAttribute('contenteditable'); |
Changhao Han | 46cdd9c | 2021-06-10 19:45:59 | [diff] [blame] | 632 | }); |
Johan Bay | 3968cbe | 2021-12-06 10:28:06 | [diff] [blame] | 633 | return check; |
Changhao Han | 46cdd9c | 2021-06-10 19:45:59 | [diff] [blame] | 634 | }); |
| 635 | } |
| 636 | |
Sigurd Schneider | a7d1bb1 | 2021-04-30 06:45:37 | [diff] [blame] | 637 | export async function waitForCSSPropertyValue(selector: string, name: string, value: string, sourcePosition?: string) { |
| 638 | return await waitForFunction(async () => { |
| 639 | const propertyHandle = await getCSSPropertyInRule(selector, name, sourcePosition); |
Patrick Brosset | 394c92f | 2020-10-28 17:25:14 | [diff] [blame] | 640 | if (!propertyHandle) { |
Sigurd Schneider | a7d1bb1 | 2021-04-30 06:45:37 | [diff] [blame] | 641 | return undefined; |
Patrick Brosset | 394c92f | 2020-10-28 17:25:14 | [diff] [blame] | 642 | } |
| 643 | |
| 644 | const valueHandle = await $(CSS_PROPERTY_VALUE_SELECTOR, propertyHandle); |
| 645 | if (!valueHandle) { |
Sigurd Schneider | a7d1bb1 | 2021-04-30 06:45:37 | [diff] [blame] | 646 | return undefined; |
Patrick Brosset | 394c92f | 2020-10-28 17:25:14 | [diff] [blame] | 647 | } |
| 648 | |
Sigurd Schneider | a7d1bb1 | 2021-04-30 06:45:37 | [diff] [blame] | 649 | const matches = await valueHandle.evaluate((node, value) => node.textContent === value, value); |
| 650 | if (matches) { |
| 651 | return valueHandle; |
| 652 | } |
| 653 | return undefined; |
Patrick Brosset | 394c92f | 2020-10-28 17:25:14 | [diff] [blame] | 654 | }); |
| 655 | } |
| 656 | |
Patrick Brosset | d93ee76 | 2020-10-05 12:33:28 | [diff] [blame] | 657 | export async function waitForPropertyToHighlight(ruleSelector: string, propertyName: string) { |
| 658 | await waitForFunction(async () => { |
Patrick Brosset | b49605b | 2020-10-12 12:35:04 | [diff] [blame] | 659 | const property = await getCSSPropertyInRule(ruleSelector, propertyName); |
Patrick Brosset | d93ee76 | 2020-10-05 12:33:28 | [diff] [blame] | 660 | if (!property) { |
| 661 | assert.fail(`Could not find property ${propertyName} in rule ${ruleSelector}`); |
| 662 | } |
| 663 | // StylePropertyHighlighter temporarily highlights the property using the Web Animations API, so the only way to |
| 664 | // know it's happening is by listing all animations. |
Randolf | cc89254 | 2023-01-27 23:44:07 | [diff] [blame] | 665 | const animationCount = await property.evaluate(node => (node as HTMLElement).getAnimations().length); |
Patrick Brosset | d93ee76 | 2020-10-05 12:33:28 | [diff] [blame] | 666 | return animationCount > 0; |
| 667 | }); |
| 668 | } |
| 669 | |
Jack Franklin | 81f6410 | 2021-05-04 12:49:12 | [diff] [blame] | 670 | export const getBreadcrumbsTextContent = async ({expectedNodeCount}: {expectedNodeCount: number}) => { |
| 671 | const crumbsSelector = 'li.crumb > a > devtools-node-text'; |
| 672 | await waitForFunction(async () => { |
| 673 | const crumbs = await $$(crumbsSelector); |
| 674 | return crumbs.length === expectedNodeCount; |
| 675 | }); |
Jack Franklin | a35ca2b | 2020-03-27 14:05:01 | [diff] [blame] | 676 | |
Jack Franklin | 81f6410 | 2021-05-04 12:49:12 | [diff] [blame] | 677 | const crumbs = await $$(crumbsSelector); |
Jack Franklin | 60cc8ba | 2021-02-10 12:14:26 | [diff] [blame] | 678 | const crumbsAsText: string[] = await Promise.all(crumbs.map(node => node.evaluate((node: Element) => { |
Jack Franklin | fc49590 | 2020-12-01 17:24:37 | [diff] [blame] | 679 | if (!node.shadowRoot) { |
| 680 | assert.fail('Found breadcrumbs node that unexpectedly has no shadowRoot.'); |
| 681 | } |
| 682 | return Array.from(node.shadowRoot.querySelectorAll('span') || []).map(span => span.textContent).join(''); |
Alex Rudenko | 550dc4d | 2020-08-20 06:05:02 | [diff] [blame] | 683 | }))); |
Jack Franklin | a35ca2b | 2020-03-27 14:05:01 | [diff] [blame] | 684 | return crumbsAsText; |
| 685 | }; |
| 686 | |
| 687 | export const getSelectedBreadcrumbTextContent = async () => { |
Alex Rudenko | 550dc4d | 2020-08-20 06:05:02 | [diff] [blame] | 688 | const selectedCrumb = await waitFor('li.crumb.selected > a > devtools-node-text'); |
Jack Franklin | 60cc8ba | 2021-02-10 12:14:26 | [diff] [blame] | 689 | const text = selectedCrumb.evaluate((node: Element) => { |
Jack Franklin | fc49590 | 2020-12-01 17:24:37 | [diff] [blame] | 690 | if (!node.shadowRoot) { |
| 691 | assert.fail('Found breadcrumbs node that unexpectedly has no shadowRoot.'); |
| 692 | } |
| 693 | return Array.from(node.shadowRoot.querySelectorAll('span') || []).map(span => span.textContent).join(''); |
Alex Rudenko | 550dc4d | 2020-08-20 06:05:02 | [diff] [blame] | 694 | }); |
Jack Franklin | a35ca2b | 2020-03-27 14:05:01 | [diff] [blame] | 695 | return text; |
| 696 | }; |
Jose Leal Chapa | dd4d881 | 2020-05-21 16:45:00 | [diff] [blame] | 697 | |
| 698 | export const navigateToElementsTab = async () => { |
| 699 | // Open Elements panel |
| 700 | await click('#tab-elements'); |
| 701 | await waitFor(ELEMENTS_PANEL_SELECTOR); |
| 702 | }; |
| 703 | |
| 704 | export const clickOnFirstLinkInStylesPanel = async () => { |
| 705 | const stylesPane = await waitFor('div.styles-pane'); |
| 706 | await click('div.styles-section-subtitle span.devtools-link', {root: stylesPane}); |
| 707 | }; |
Patrick Brosset | 78fa1f5 | 2020-08-17 14:33:48 | [diff] [blame] | 708 | |
| 709 | export const toggleClassesPane = async () => { |
| 710 | await click(CLS_BUTTON_SELECTOR); |
| 711 | }; |
| 712 | |
| 713 | export const typeInClassesPaneInput = |
Jack Franklin | 60cc8ba | 2021-02-10 12:14:26 | [diff] [blame] | 714 | async (text: string, commitWith: puppeteer.KeyInput = 'Enter', waitForNodeChange: Boolean = true) => { |
Patrick Brosset | 78fa1f5 | 2020-08-17 14:33:48 | [diff] [blame] | 715 | await step(`Typing in new class names ${text}`, async () => { |
| 716 | const clsInput = await waitFor(CLS_INPUT_SELECTOR); |
| 717 | await clsInput.type(text, {delay: 50}); |
| 718 | }); |
| 719 | |
| 720 | if (commitWith) { |
| 721 | await step(`Committing the changes with ${commitWith}`, async () => { |
| 722 | const {frontend} = getBrowserAndPages(); |
| 723 | await frontend.keyboard.press(commitWith); |
| 724 | }); |
| 725 | } |
| 726 | |
| 727 | if (waitForNodeChange) { |
| 728 | // Make sure the classes provided in text can be found in the selected element's content. This is important as the |
| 729 | // cls pane applies classes as you type, so it is not enough to wait for the selected node to change just once. |
| 730 | await step('Waiting for the selected node to change', async () => { |
| 731 | await waitForFunction(async () => { |
| 732 | const nodeContent = await getContentOfSelectedNode(); |
| 733 | return text.split(' ').every(cls => nodeContent.includes(cls)); |
| 734 | }); |
| 735 | }); |
| 736 | } |
| 737 | }; |
| 738 | |
| 739 | export const toggleClassesPaneCheckbox = async (checkboxLabel: string) => { |
| 740 | const initialValue = await getContentOfSelectedNode(); |
| 741 | |
| 742 | const classesPane = await waitFor(CLS_PANE_SELECTOR); |
Elorm Coch | 34ad334 | 2023-06-06 20:15:59 | [diff] [blame^] | 743 | await click(`[title="${checkboxLabel}"]`, {root: classesPane}); |
Patrick Brosset | 78fa1f5 | 2020-08-17 14:33:48 | [diff] [blame] | 744 | |
| 745 | await waitForSelectedNodeChange(initialValue); |
| 746 | }; |
| 747 | |
Al Muthanna Athamina | f385255 | 2023-01-23 16:11:50 | [diff] [blame] | 748 | export const uncheckStylesPaneCheckbox = async (checkboxLabel: string) => { |
| 749 | const initialValue = await getContentOfSelectedNode(); |
| 750 | await click(`.enabled-button[aria-label="${checkboxLabel}"]`); |
| 751 | await waitForSelectedNodeChange(initialValue); |
| 752 | }; |
| 753 | |
Patrick Brosset | 78fa1f5 | 2020-08-17 14:33:48 | [diff] [blame] | 754 | export const assertSelectedNodeClasses = async (expectedClasses: string[]) => { |
| 755 | const nodeText = await getContentOfSelectedNode(); |
| 756 | const match = nodeText.match(/class=\u200B"([^"]*)/); |
| 757 | const classText = match ? match[1] : ''; |
| 758 | const classes = classText.split(/[\s]/).map(className => className.trim()).filter(className => className.length); |
| 759 | |
| 760 | assert.strictEqual( |
| 761 | classes.length, expectedClasses.length, 'Did not find the expected number of classes on the element'); |
| 762 | |
| 763 | for (const expectedClass of expectedClasses) { |
| 764 | assert.include(classes, expectedClass, `Could not find class ${expectedClass} on the element`); |
| 765 | } |
| 766 | }; |
Johan Bay | 0bda6bd | 2021-07-30 07:10:02 | [diff] [blame] | 767 | |
| 768 | export const toggleAccessibilityPane = async () => { |
| 769 | let a11yPane = await $('Accessibility', undefined, 'aria'); |
| 770 | if (!a11yPane) { |
| 771 | const elementsPanel = await waitForAria('Elements panel'); |
| 772 | const moreTabs = await waitForAria('More tabs', elementsPanel); |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 773 | await clickElement(moreTabs); |
Johan Bay | 0bda6bd | 2021-07-30 07:10:02 | [diff] [blame] | 774 | a11yPane = await waitForAria('Accessibility'); |
| 775 | } |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 776 | await clickElement(a11yPane); |
Johan Bay | 0bda6bd | 2021-07-30 07:10:02 | [diff] [blame] | 777 | }; |
| 778 | |
| 779 | export const toggleAccessibilityTree = async () => { |
Alex Rudenko | e92fe9d | 2023-01-30 13:12:23 | [diff] [blame] | 780 | await click('aria/Switch to Accessibility Tree view'); |
Johan Bay | 0bda6bd | 2021-07-30 07:10:02 | [diff] [blame] | 781 | }; |
Saba Khukhunashvili | 41695dc | 2022-06-28 02:23:23 | [diff] [blame] | 782 | |
| 783 | export const getPropertiesWithHints = async () => { |
| 784 | const allRuleSelectors = await $$(CSS_STYLE_RULE_SELECTOR); |
| 785 | |
| 786 | const propertiesWithHints = []; |
| 787 | for (const propertiesSection of allRuleSelectors) { |
| 788 | const cssRuleNodes = await $$('li ', propertiesSection); |
| 789 | |
| 790 | for (const cssRuleNode of cssRuleNodes) { |
| 791 | const propertyNode = await $(CSS_PROPERTY_NAME_SELECTOR, cssRuleNode); |
| 792 | const propertyName = propertyNode !== null ? await propertyNode.evaluate(n => n.textContent) : null; |
| 793 | if (propertyName === null) { |
| 794 | continue; |
| 795 | } |
| 796 | |
| 797 | const authoringHintsIcon = await $(CSS_AUTHORING_HINTS_ICON_SELECTOR, cssRuleNode); |
| 798 | if (authoringHintsIcon) { |
| 799 | propertiesWithHints.push(propertyName); |
| 800 | } |
| 801 | } |
| 802 | } |
| 803 | |
| 804 | return propertiesWithHints; |
| 805 | }; |
PhistucK | 257ad69 | 2022-08-29 14:36:19 | [diff] [blame] | 806 | |
| 807 | export const summonAndWaitForSearchBox = async () => { |
| 808 | await summonSearchBox(); |
| 809 | await waitFor(SEARCH_BOX_SELECTOR); |
| 810 | }; |
| 811 | |
Ergün Erdoğmuş | 907a542 | 2022-09-16 18:07:43 | [diff] [blame] | 812 | export const assertSearchResultMatchesText = async (text: string) => { |
| 813 | await waitForFunction(async () => { |
| 814 | return await getTextContent(SEARCH_RESULTS_MATCHES) === text; |
| 815 | }); |
PhistucK | 257ad69 | 2022-08-29 14:36:19 | [diff] [blame] | 816 | }; |
Philip Pfaffe | 7fb6a83 | 2022-11-14 13:39:47 | [diff] [blame] | 817 | |
| 818 | export const goToResourceAndWaitForStyleSection = async (path: string) => { |
| 819 | await goToResource(path); |
| 820 | await waitForElementsStyleSection(); |
| 821 | |
| 822 | // Check to make sure we have the correct node selected after opening a file. |
| 823 | await waitForPartialContentOfSelectedElementsNode('<body>\u200B'); |
| 824 | }; |
Al Muthanna Athamina | f385255 | 2023-01-23 16:11:50 | [diff] [blame] | 825 | |
| 826 | export const checkStyleAttributes = async (expectedStyles: string[]) => { |
| 827 | const result = await $$(STYLE_PROPERTIES_SELECTOR, undefined, 'pierce'); |
| 828 | const actual = await Promise.all(result.map(e => e.evaluate(e => e.textContent?.trim()))); |
| 829 | return actual.sort().join(' ') === expectedStyles.sort().join(' '); |
| 830 | }; |