blob: 577a1a6b824b47e56d2646b5a53a156df0cfd8e6 [file] [log] [blame]
Tim van der Lippe68efc702020-03-03 17:27:451// 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.
4import {assert} from 'chai';
Jack Franklina75ae7c2021-05-11 13:22:545import type * as puppeteer from 'puppeteer';
Philip Pfaffea1f88f52023-04-20 08:31:076import {AsyncScope} from '../../shared/async-scope.js';
Tim van der Lippe68efc702020-03-03 17:27:457
Jack Frankline839c0c2022-05-03 08:47:448import {
9 $,
10 $$,
11 click,
12 getBrowserAndPages,
PhistucK257ad692022-08-29 14:36:1913 getTextContent,
Philip Pfaffe7fb6a832022-11-14 13:39:4714 goToResource,
Jack Frankline839c0c2022-05-03 08:47:4415 pressKey,
16 step,
PhistucK257ad692022-08-29 14:36:1917 summonSearchBox,
Jack Frankline839c0c2022-05-03 08:47:4418 timeout,
19 typeText,
20 waitFor,
21 waitForAria,
Alex Rudenkoe92fe9d2023-01-30 13:12:2322 clickElement,
Jack Frankline839c0c2022-05-03 08:47:4423 waitForFunction,
24} from '../../shared/helper.js';
Tim van der Lippe68efc702020-03-03 17:27:4525
26const SELECTED_TREE_ELEMENT_SELECTOR = '.selected[role="treeitem"]';
Changhao Hand1b1b5f2020-03-27 12:00:5327const CSS_PROPERTY_NAME_SELECTOR = '.webkit-css-property';
Patrick Brossetb49605b2020-10-12 12:35:0428const CSS_PROPERTY_VALUE_SELECTOR = '.value';
Changhao Han7bdd4452022-08-26 11:03:4829const CSS_DECLARATION_SELECTOR =
30 `[role="treeitem"]:has(${CSS_PROPERTY_NAME_SELECTOR}):has(${CSS_PROPERTY_VALUE_SELECTOR})`;
Patrick Brosset394c92f2020-10-28 17:25:1431const COLOR_SWATCH_SELECTOR = '.color-swatch-inner';
Patrick Brossetba955422020-05-22 07:33:2932const CSS_STYLE_RULE_SELECTOR = '[aria-label*="css selector"]';
Changhao Hand2454322020-08-11 13:09:3533const COMPUTED_PROPERTY_SELECTOR = 'devtools-computed-style-property';
Changhao Han2bcf0b82020-07-17 09:32:0834const COMPUTED_STYLES_PANEL_SELECTOR = '[aria-label="Computed panel"]';
Elorm Coch34ad3342023-06-06 20:15:5935const COMPUTED_STYLES_SHOW_ALL_SELECTOR = '[title="Show all"]';
36const COMPUTED_STYLES_GROUP_SELECTOR = '[title="Group"]';
Jose Leal Chapadd4d8812020-05-21 16:45:0037const ELEMENTS_PANEL_SELECTOR = '.panel[aria-label="elements"]';
Michael Liao6035f4a2020-12-01 19:12:4538const FONT_EDITOR_SELECTOR = '[aria-label="Font Editor"]';
39const HIDDEN_FONT_EDITOR_SELECTOR = '.font-toolbar-hidden';
Alex Rudenko52767b72020-06-22 06:26:4740const SECTION_SUBTITLE_SELECTOR = '.styles-section-subtitle';
Patrick Brosset78fa1f52020-08-17 14:33:4841const CLS_PANE_SELECTOR = '.styles-sidebar-toolbar-pane';
42const CLS_BUTTON_SELECTOR = '[aria-label="Element Classes"]';
43const CLS_INPUT_SELECTOR = '[aria-placeholder="Add new class"]';
Alex Rudenko034db882020-08-13 12:21:0944const LAYOUT_PANE_TAB_SELECTOR = '[aria-label="Layout"]';
Patrick Brossetf398c102020-10-14 07:35:0845const LAYOUT_PANE_TABPANEL_SELECTOR = '[aria-label="Layout panel"]';
Patrick Brosseta3fd9372020-09-22 07:55:3546const ADORNER_SELECTOR = 'devtools-adorner';
Alex Rudenko1cb771b2020-08-20 07:02:3147export const INACTIVE_GRID_ADORNER_SELECTOR = '[aria-label="Enable grid mode"]';
48export const ACTIVE_GRID_ADORNER_SELECTOR = '[aria-label="Disable grid mode"]';
Alex Rudenko034db882020-08-13 12:21:0949const ELEMENT_CHECKBOX_IN_LAYOUT_PANE_SELECTOR = '.elements input[type=checkbox]';
Alex Rudenko651f6d32021-03-24 07:02:1350const ELEMENT_STYLE_SECTION_SELECTOR = '[aria-label="element.style, css selector"]';
Changhao Hanab1d8842021-07-02 10:09:3351const STYLE_QUERY_RULE_TEXT_SELECTOR = '.query-text';
Al Muthanna Athaminaf3852552023-01-23 16:11:5052const STYLE_PROPERTIES_SELECTOR = '.tree-outline-disclosure [role="treeitem"]';
Saba Khukhunashvili41695dc2022-06-28 02:23:2353const CSS_AUTHORING_HINTS_ICON_SELECTOR = '.hint';
Philip Pfaffe32d9de92023-05-08 07:23:4954export const SEARCH_BOX_SELECTOR = '.search-bar';
PhistucK257ad692022-08-29 14:36:1955const SEARCH_RESULTS_MATCHES = '.search-results-matches';
Alex Rudenko034db882020-08-13 12:21:0956
57export const openLayoutPane = async () => {
58 await step('Open Layout pane', async () => {
Alex Rudenko034db882020-08-13 12:21:0959 await waitFor(LAYOUT_PANE_TAB_SELECTOR);
60 await click(LAYOUT_PANE_TAB_SELECTOR);
Patrick Brossetf398c102020-10-14 07:35:0861
62 const panel = await waitFor(LAYOUT_PANE_TABPANEL_SELECTOR);
63 await waitFor('.elements', panel);
Alex Rudenko034db882020-08-13 12:21:0964 });
65};
66
Patrick Brosseta3fd9372020-09-22 07:55:3567export 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 Brossetd4e59f42020-11-02 18:18:1072 // 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 Brosseta3fd9372020-09-22 07:55:3574 return {textContent: node.textContent, isActive: node.matches(activeSelector)};
75 }, ACTIVE_GRID_ADORNER_SELECTOR);
76 }));
Alex Rudenko034db882020-08-13 12:21:0977
Patrick Brosseta3fd9372020-09-22 07:55:3578 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 Rudenko034db882020-08-13 12:21:0993 });
94};
95
Philip Pfaffe940b1912022-09-30 08:17:5196export const waitForSelectedNodeToBeExpanded = async () => {
97 await waitFor(`${SELECTED_TREE_ELEMENT_SELECTOR}[aria-expanded="true"]`);
98};
99
Patrick Brosset24e2e672020-11-12 09:03:13100export 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 Rudenko034db882020-08-13 12:21:09108export 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 Lippe68efc702020-03-03 17:27:45114
Patrick Brossetf398c102020-10-14 07:35:08115export const getGridsInLayoutPane = async () => {
116 const panel = await waitFor(LAYOUT_PANE_TABPANEL_SELECTOR);
117 return await $$('.elements .element', panel);
118};
119
120export const waitForSomeGridsInLayoutPane = async (minimumGridCount: number) => {
121 await waitForFunction(async () => {
122 const grids = await getGridsInLayoutPane();
123 return grids.length >= minimumGridCount;
124 });
125};
126
Johan Bay00fc92a2020-08-14 20:06:20127export const waitForContentOfSelectedElementsNode = async (expectedTextContent: string) => {
128 await waitForFunction(async () => {
Mathias Bynens00e1aac2020-12-21 14:57:12129 const selectedTextContent = await getContentOfSelectedNode();
Johan Bay00fc92a2020-08-14 20:06:20130 return selectedTextContent === expectedTextContent;
131 });
Tim van der Lippe81aeee72020-03-09 16:14:28132};
Tim van der Lippe68efc702020-03-03 17:27:45133
Changhao Han59173bb2021-08-11 13:29:25134export const waitForPartialContentOfSelectedElementsNode = async (expectedPartialTextContent: string) => {
135 await waitForFunction(async () => {
136 const selectedTextContent = await getContentOfSelectedNode();
137 return selectedTextContent.includes(expectedPartialTextContent);
138 });
139};
140
Paul Lewisab0c65c2020-03-27 10:25:09141/**
142 * Gets the text content of the currently selected element.
143 */
144export const getContentOfSelectedNode = async () => {
Johan Bay504a6f12020-08-04 13:32:53145 const selectedNode = await waitFor(SELECTED_TREE_ELEMENT_SELECTOR);
Johan Baye8245712020-08-04 13:32:10146 return await selectedNode.evaluate(node => node.textContent as string);
Paul Lewisab0c65c2020-03-27 10:25:09147};
148
Philip Pfaffea1f88f52023-04-20 08:31:07149export const waitForSelectedNodeChange = async(initialValue: string, asyncScope = new AsyncScope()): Promise<void> => {
150 await waitForFunction(async () => {
Paul Lewisab0c65c2020-03-27 10:25:09151 const currentContent = await getContentOfSelectedNode();
Philip Pfaffea1f88f52023-04-20 08:31:07152 return currentContent !== initialValue;
153 }, asyncScope);
Paul Lewisab0c65c2020-03-27 10:25:09154};
155
Jack Franklina35ca2b2020-03-27 14:05:01156export const assertSelectedElementsNodeTextIncludes = async (expectedTextContent: string) => {
Johan Bay504a6f12020-08-04 13:32:53157 const selectedNode = await waitFor(SELECTED_TREE_ELEMENT_SELECTOR);
Johan Baye8245712020-08-04 13:32:10158 const selectedTextContent = await selectedNode.evaluate(node => node.textContent as string);
Jack Franklina35ca2b2020-03-27 14:05:01159 assert.include(selectedTextContent, expectedTextContent);
160};
161
Tim van der Lippe869374b2020-04-20 10:12:31162export const waitForSelectedTreeElementSelectorWithTextcontent = async (expectedTextContent: string) => {
163 await waitForFunction(async () => {
Johan Bay504a6f12020-08-04 13:32:53164 const selectedNode = await waitFor(SELECTED_TREE_ELEMENT_SELECTOR);
Tim van der Lippe869374b2020-04-20 10:12:31165 const selectedTextContent = await selectedNode.evaluate(node => node.textContent);
166 return selectedTextContent === expectedTextContent;
Philip Pfaffe6d143ae2020-07-31 11:32:22167 });
Tim van der Lippe869374b2020-04-20 10:12:31168};
169
Kateryna Prokopenko314d6b72020-08-19 06:33:00170export 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 Lippe81aeee72020-03-09 16:14:28178export const waitForChildrenOfSelectedElementNode = async () => {
Tim van der Lippe68efc702020-03-03 17:27:45179 await waitFor(`${SELECTED_TREE_ELEMENT_SELECTOR} + ol > li`);
Tim van der Lippe81aeee72020-03-09 16:14:28180};
181
Johan Bay8b2613a2022-06-02 12:21:26182export const waitForAndClickTreeElementWithPartialText = async (text: string) =>
183 waitForFunction(async () => clickTreeElementWithPartialText(text));
184
Danil Somsikov9a49c712022-06-29 14:56:24185export const waitForElementWithPartialText = async (text: string) => {
186 return waitForFunction(async () => elementWithPartialText(text));
187};
188
189const elementWithPartialText = async (text: string) => {
Johan Bay8b2613a2022-06-02 12:21:26190 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 Somsikov9a49c712022-06-29 14:56:24195 return handle;
Johan Bay8b2613a2022-06-02 12:21:26196 }
197 }
Danil Somsikov9a49c712022-06-29 14:56:24198 return null;
199};
200
201export const clickTreeElementWithPartialText = async (text: string) => {
202 const handle = await elementWithPartialText(text);
203 if (handle) {
Alex Rudenkoe92fe9d2023-01-30 13:12:23204 await clickElement(handle);
Danil Somsikov9a49c712022-06-29 14:56:24205 return true;
206 }
207
Johan Bay8b2613a2022-06-02 12:21:26208 throw false;
209};
210
Alex Rudenko3e3197d2021-03-22 12:18:33211export 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 Brossetba955422020-05-22 07:33:29217export const focusElementsTree = async () => {
218 await click(SELECTED_TREE_ELEMENT_SELECTOR);
219};
220
221export const navigateToSidePane = async (paneName: string) => {
222 await click(`[aria-label="${paneName}"]`);
223 await waitFor(`[aria-label="${paneName} panel"]`);
224};
225
Tim van der Lippe81aeee72020-03-09 16:14:28226export const waitForElementsStyleSection = async () => {
227 // Wait for the file to be loaded and selectors to be shown
228 await waitFor('.styles-selector');
229};
230
Patrick Brossetba955422020-05-22 07:33:29231export const waitForElementsComputedSection = async () => {
232 await waitFor(COMPUTED_PROPERTY_SELECTOR);
233};
234
235export const getContentOfComputedPane = async () => {
Johan Bay86a70e02022-05-25 08:01:46236 const pane = await waitFor('Computed panel', undefined, undefined, 'aria');
237 const tree = await waitFor('[role="tree"]', pane, undefined, 'aria');
Johan Baye8245712020-08-04 13:32:10238 return await tree.evaluate(node => node.textContent as string);
Patrick Brossetba955422020-05-22 07:33:29239};
240
241export const waitForComputedPaneChange = async (initialValue: string) => {
242 await waitForFunction(async () => {
243 const value = await getContentOfComputedPane();
244 return value !== initialValue;
Philip Pfaffe6d143ae2020-07-31 11:32:22245 });
Patrick Brossetba955422020-05-22 07:33:29246};
247
248export const getAllPropertiesFromComputedPane = async () => {
249 const properties = await $$(COMPUTED_PROPERTY_SELECTOR);
Jack Franklin7fc544b2023-01-03 11:50:34250 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 Brossetba955422020-05-22 07:33:29255
Johan Baye8245712020-08-04 13:32:10256 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 Lippeed61abd2020-12-16 15:11:16261 .filter(prop => Boolean(prop));
Patrick Brossetba955422020-05-22 07:33:29262};
263
Patrick Brosset394c92f2020-10-28 17:25:14264export const getPropertyFromComputedPane = async (name: string) => {
265 const properties = await $$(COMPUTED_PROPERTY_SELECTOR);
266 for (const property of properties) {
Johan Bay86a70e02022-05-25 08:01:46267 const matchingProperty = await property.evaluate((node, name) => {
Simon Zünd9e2c7592023-01-17 07:59:41268 const nameSlot = node.shadowRoot?.querySelector<HTMLSlotElement>('.property-name slot');
Jack Franklin7fc544b2023-01-03 11:50:34269 const nameEl = nameSlot?.assignedElements().at(0);
Johan Bay86a70e02022-05-25 08:01:46270 return nameEl?.textContent?.trim().replace(/:$/, '') === name;
Patrick Brosset394c92f2020-10-28 17:25:14271 }, 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 Bay86a70e02022-05-25 08:01:46274 if (matchingProperty) {
275 return property;
Patrick Brosset394c92f2020-10-28 17:25:14276 }
277 }
278 return undefined;
279};
280
Patrick Brosset6e337aa2020-10-29 14:25:38281export 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 Lewisab0c65c2020-03-27 10:25:09293export const expandSelectedNodeRecursively = async () => {
294 const EXPAND_RECURSIVELY = '[aria-label="Expand recursively"]';
295
296 // Find the selected node, right click.
Alex Rudenkoe92fe9d2023-01-30 13:12:23297 await click(SELECTED_TREE_ELEMENT_SELECTOR, {clickOptions: {button: 'right'}});
Paul Lewisab0c65c2020-03-27 10:25:09298
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 Lippe81aeee72020-03-09 16:14:28304export 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 Bay00fc92a2020-08-14 20:06:20308 // FIXME(crbug/1112692): Refactor test to remove the timeout.
309 await timeout(100);
Tim van der Lippe81aeee72020-03-09 16:14:28310 await click(`[aria-label="${pseudoState}"]`);
311};
312
Tim van der Lippe01e70f12020-03-10 18:34:50313export const removePseudoState = async (pseudoState: string) => {
314 await click(`[aria-label="${pseudoState}"]`);
315};
Tim van der Lippe81aeee72020-03-09 16:14:28316
Randolfcc892542023-01-27 23:44:07317export const getComputedStylesForDomNode =
318 async (elementSelector: string, styleAttribute: keyof CSSStyleDeclaration) => {
Tim van der Lippe01e70f12020-03-10 18:34:50319 const {target} = getBrowserAndPages();
Tim van der Lippe81aeee72020-03-09 16:14:28320
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ünd9e2c7592023-01-17 07:59:41326 return getComputedStyle(element)[styleAttribute];
Tim van der Lippe81aeee72020-03-09 16:14:28327 }, elementSelector, styleAttribute);
328};
329
Johan Bay86a70e02022-05-25 08:01:46330export 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
337export const getComputedPanel = async () => waitFor(COMPUTED_STYLES_PANEL_SELECTOR);
338
339export const filterComputedProperties = async (filterString: string) => {
340 const initialContent = await getContentOfComputedPane();
341
342 const computedPanel = await waitFor(COMPUTED_STYLES_PANEL_SELECTOR);
Alex Rudenkoe92fe9d2023-01-30 13:12:23343 await click('[aria-label="Filter Computed Styles"]', {
344 root: computedPanel,
345 });
Johan Bay86a70e02022-05-25 08:01:46346 await typeText(filterString);
347 await waitForComputedPaneChange(initialContent);
348};
349
Changhao Han2bcf0b82020-07-17 09:32:08350export const toggleShowAllComputedProperties = async () => {
351 const initialContent = await getContentOfComputedPane();
352
Johan Bay504a6f12020-08-04 13:32:53353 const computedPanel = await waitFor(COMPUTED_STYLES_PANEL_SELECTOR);
Alex Rudenkoe92fe9d2023-01-30 13:12:23354 await click(COMPUTED_STYLES_SHOW_ALL_SELECTOR, {root: computedPanel});
Changhao Han2bcf0b82020-07-17 09:32:08355 await waitForComputedPaneChange(initialContent);
356};
357
Changhao Hanca13f092020-09-03 04:28:43358export 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 Rudenkoe92fe9d2023-01-30 13:12:23364 await click(COMPUTED_STYLES_GROUP_SELECTOR, {
365 root: computedPanel,
366 });
Changhao Hanca13f092020-09-03 04:28:43367
368 if (wasChecked) {
Changhao Hanbb4af122020-09-10 13:45:25369 await waitFor('[role="tree"].alphabetical-list', computedPanel);
Changhao Hanca13f092020-09-03 04:28:43370 } else {
Changhao Hanbb4af122020-09-10 13:45:25371 await waitFor('[role="tree"].grouped-list', computedPanel);
Changhao Hanca13f092020-09-03 04:28:43372 }
373};
374
Mathias Bynens78e65942020-03-24 13:25:11375export const waitForDomNodeToBeVisible = async (elementSelector: string) => {
Tim van der Lippe01e70f12020-03-10 18:34:50376 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 Bynens78e65942020-03-24 13:25:11383export const waitForDomNodeToBeHidden = async (elementSelector: string) => {
Tim van der Lippe01e70f12020-03-10 18:34:50384 const {target} = getBrowserAndPages();
385 await target.waitForSelector(elementSelector, {hidden: true});
386};
387
Mathias Bynens78e65942020-03-24 13:25:11388export const assertGutterDecorationForDomNodeExists = async () => {
Tim van der Lippe81aeee72020-03-09 16:14:28389 await waitFor('.elements-gutter-decoration');
390};
Changhao Hand1b1b5f2020-03-27 12:00:53391
Patrick Brossetd93ee762020-10-05 12:33:28392export const getStyleRuleSelector = (selector: string) => `[aria-label="${selector}, css selector"]`;
Patrick Brossetba955422020-05-22 07:33:29393
394export const waitForStyleRule = async (expectedSelector: string) => {
395 await waitForFunction(async () => {
396 const rules = await getDisplayedStyleRules();
397 return rules.map(rule => rule.selectorText).includes(expectedSelector);
Philip Pfaffe6d143ae2020-07-31 11:32:22398 });
Patrick Brossetba955422020-05-22 07:33:29399};
400
Johan Bay86a70e02022-05-25 08:01:46401export 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 Han7bdd4452022-08-26 11:03:48420export const getDisplayedCSSDeclarations = async () => {
Philip Pfaffee5580722022-09-30 14:55:38421 const cssDeclarations = await $$(CSS_DECLARATION_SELECTOR);
422 return Promise.all(cssDeclarations.map(async node => await node.evaluate(n => n.textContent?.trim())));
Changhao Han7bdd4452022-08-26 11:03:48423};
424
Johan Bay86a70e02022-05-25 08:01:46425export 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 Brossetba955422020-05-22 07:33:29434export const getDisplayedStyleRules = async () => {
435 const allRuleSelectors = await $$(CSS_STYLE_RULE_SELECTOR);
Patrick Brossetba955422020-05-22 07:33:29436 const rules = [];
Johan Baye8245712020-08-04 13:32:10437 for (const ruleSelector of allRuleSelectors) {
Dan Clark621ed502022-04-21 00:17:44438 const propertyData = await getDisplayedCSSPropertyData(ruleSelector);
Johan Baye8245712020-08-04 13:32:10439 const selectorText = await ruleSelector.evaluate(node => {
Patrick Brossetba955422020-05-22 07:33:29440 const attribute = node.getAttribute('aria-label') || '';
441 return attribute.substring(0, attribute.lastIndexOf(', css selector'));
442 });
Dan Clark621ed502022-04-21 00:17:44443 rules.push({selectorText, propertyData});
Patrick Brossetba955422020-05-22 07:33:29444 }
445
446 return rules;
447};
448
Dan Clark621ed502022-04-21 00:17:44449/**
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 */
458export 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 Baye8245712020-08-04 13:32:10474export const getDisplayedCSSPropertyNames = async (propertiesSection: puppeteer.ElementHandle<Element>) => {
Changhao Hand1b1b5f2020-03-27 12:00:53475 const cssPropertyNames = await $$(CSS_PROPERTY_NAME_SELECTOR, propertiesSection);
Johan Baye8245712020-08-04 13:32:10476 const propertyNamesText = (await Promise.all(cssPropertyNames.map(
477 node => node.evaluate(n => n.textContent),
478 )))
Tim van der Lippeed61abd2020-12-16 15:11:16479 .filter(c => Boolean(c));
Changhao Hand1b1b5f2020-03-27 12:00:53480 return propertyNamesText;
481};
Jack Franklina35ca2b2020-03-27 14:05:01482
Johan Bay504a6f12020-08-04 13:32:53483export const getStyleRule = (selector: string) => {
Patrick Brossetd93ee762020-10-05 12:33:28484 return waitFor(getStyleRuleSelector(selector));
Patrick Brosseta340d9e2020-05-26 11:05:40485};
486
Sigurd Schneidera7d1bb12021-04-30 06:45:37487export 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 Pfaffe7fb6a832022-11-14 13:39:47505export const getColorSwatch = async (parent: puppeteer.ElementHandle<Element>|undefined, index: number) => {
Patrick Brosset394c92f2020-10-28 17:25:14506 const swatches = await $$(COLOR_SWATCH_SELECTOR, parent);
507 return swatches[index];
508};
509
510export 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
515export 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 Rudenkoe92fe9d2023-01-30 13:12:23519 await clickElement(swatch);
Patrick Brosset394c92f2020-10-28 17:25:14520 await frontend.keyboard.up('Shift');
Alex Rudenko0feb30f2020-06-09 06:43:00521};
522
Alex Rudenko651f6d32021-03-24 07:02:13523export const getElementStyleFontEditorButton = async () => {
524 const section = await waitFor(ELEMENT_STYLE_SECTION_SELECTOR);
525 return await $(FONT_EDITOR_SELECTOR, section);
526};
527
Michael Liao6035f4a2020-12-01 19:12:45528export const getFontEditorButtons = async () => {
529 const buttons = await $$(FONT_EDITOR_SELECTOR);
530 return buttons;
531};
532
533export const getHiddenFontEditorButtons = async () => {
534 const buttons = await $$(HIDDEN_FONT_EDITOR_SELECTOR);
535 return buttons;
536};
537
Alex Rudenko52767b72020-06-22 06:26:47538export const getStyleSectionSubtitles = async () => {
539 const subtitles = await $$(SECTION_SUBTITLE_SELECTOR);
Johan Baye8245712020-08-04 13:32:10540 return Promise.all(subtitles.map(node => node.evaluate(n => n.textContent)));
Alex Rudenko52767b72020-06-22 06:26:47541};
542
Sigurd Schneidera7d1bb12021-04-30 06:45:37543export const getCSSPropertyInRule =
544 async (ruleSection: puppeteer.ElementHandle<Element>|string, name: string, sourcePosition?: string) => {
Patrick Brossetb49605b2020-10-12 12:35:04545 if (typeof ruleSection === 'string') {
Sigurd Schneidera7d1bb12021-04-30 06:45:37546 ruleSection = await getStyleRuleWithSourcePosition(ruleSection, sourcePosition);
Patrick Brossetb49605b2020-10-12 12:35:04547 }
548
Patrick Brosseta340d9e2020-05-26 11:05:40549 const propertyNames = await $$(CSS_PROPERTY_NAME_SELECTOR, ruleSection);
Johan Baye8245712020-08-04 13:32:10550 for (const node of propertyNames) {
551 const parent =
Randolfcc892542023-01-27 23:44:07552 (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 Baye8245712020-08-04 13:32:10556 }
557 }
558 return undefined;
Patrick Brosseta340d9e2020-05-26 11:05:40559};
560
561export const focusCSSPropertyValue = async (selector: string, propertyName: string) => {
562 await waitForStyleRule(selector);
Alex Rudenkoe92fe9d2023-01-30 13:12:23563 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 Brosseta340d9e2020-05-26 11:05:40577};
578
Patrick Brossetb49605b2020-10-12 12:35:04579/**
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 Brosseta340d9e2020-05-26 11:05:40587export async function editCSSProperty(selector: string, propertyName: string, newValue: string) {
588 await focusCSSPropertyValue(selector, propertyName);
589
590 const {frontend} = getBrowserAndPages();
Patrick Brossetb49605b2020-10-12 12:35:04591 await frontend.keyboard.type(newValue, {delay: 100});
Patrick Brosseta340d9e2020-05-26 11:05:40592 await frontend.keyboard.press('Enter');
Patrick Brossetb49605b2020-10-12 12:35:04593
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 Brosseta340d9e2020-05-26 11:05:40605}
606
Changhao Han46cdd9c2021-06-10 19:45:59607// Edit a media or container query rule text for the given styles section
608export async function editQueryRuleText(queryStylesSections: puppeteer.ElementHandle<Element>, newQueryText: string) {
609 await click(STYLE_QUERY_RULE_TEXT_SELECTOR, {root: queryStylesSections});
Johan Bay3968cbe2021-12-06 10:28:06610 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 Han46cdd9c2021-06-10 19:45:59623
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 Bay3968cbe2021-12-06 10:28:06630 const check = await queryText.evaluate(node => {
631 return !node.classList.contains('being-edited') && !node.hasAttribute('contenteditable');
Changhao Han46cdd9c2021-06-10 19:45:59632 });
Johan Bay3968cbe2021-12-06 10:28:06633 return check;
Changhao Han46cdd9c2021-06-10 19:45:59634 });
635}
636
Sigurd Schneidera7d1bb12021-04-30 06:45:37637export 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 Brosset394c92f2020-10-28 17:25:14640 if (!propertyHandle) {
Sigurd Schneidera7d1bb12021-04-30 06:45:37641 return undefined;
Patrick Brosset394c92f2020-10-28 17:25:14642 }
643
644 const valueHandle = await $(CSS_PROPERTY_VALUE_SELECTOR, propertyHandle);
645 if (!valueHandle) {
Sigurd Schneidera7d1bb12021-04-30 06:45:37646 return undefined;
Patrick Brosset394c92f2020-10-28 17:25:14647 }
648
Sigurd Schneidera7d1bb12021-04-30 06:45:37649 const matches = await valueHandle.evaluate((node, value) => node.textContent === value, value);
650 if (matches) {
651 return valueHandle;
652 }
653 return undefined;
Patrick Brosset394c92f2020-10-28 17:25:14654 });
655}
656
Patrick Brossetd93ee762020-10-05 12:33:28657export async function waitForPropertyToHighlight(ruleSelector: string, propertyName: string) {
658 await waitForFunction(async () => {
Patrick Brossetb49605b2020-10-12 12:35:04659 const property = await getCSSPropertyInRule(ruleSelector, propertyName);
Patrick Brossetd93ee762020-10-05 12:33:28660 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.
Randolfcc892542023-01-27 23:44:07665 const animationCount = await property.evaluate(node => (node as HTMLElement).getAnimations().length);
Patrick Brossetd93ee762020-10-05 12:33:28666 return animationCount > 0;
667 });
668}
669
Jack Franklin81f64102021-05-04 12:49:12670export 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 Franklina35ca2b2020-03-27 14:05:01676
Jack Franklin81f64102021-05-04 12:49:12677 const crumbs = await $$(crumbsSelector);
Jack Franklin60cc8ba2021-02-10 12:14:26678 const crumbsAsText: string[] = await Promise.all(crumbs.map(node => node.evaluate((node: Element) => {
Jack Franklinfc495902020-12-01 17:24:37679 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 Rudenko550dc4d2020-08-20 06:05:02683 })));
Jack Franklina35ca2b2020-03-27 14:05:01684 return crumbsAsText;
685};
686
687export const getSelectedBreadcrumbTextContent = async () => {
Alex Rudenko550dc4d2020-08-20 06:05:02688 const selectedCrumb = await waitFor('li.crumb.selected > a > devtools-node-text');
Jack Franklin60cc8ba2021-02-10 12:14:26689 const text = selectedCrumb.evaluate((node: Element) => {
Jack Franklinfc495902020-12-01 17:24:37690 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 Rudenko550dc4d2020-08-20 06:05:02694 });
Jack Franklina35ca2b2020-03-27 14:05:01695 return text;
696};
Jose Leal Chapadd4d8812020-05-21 16:45:00697
698export const navigateToElementsTab = async () => {
699 // Open Elements panel
700 await click('#tab-elements');
701 await waitFor(ELEMENTS_PANEL_SELECTOR);
702};
703
704export 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 Brosset78fa1f52020-08-17 14:33:48708
709export const toggleClassesPane = async () => {
710 await click(CLS_BUTTON_SELECTOR);
711};
712
713export const typeInClassesPaneInput =
Jack Franklin60cc8ba2021-02-10 12:14:26714 async (text: string, commitWith: puppeteer.KeyInput = 'Enter', waitForNodeChange: Boolean = true) => {
Patrick Brosset78fa1f52020-08-17 14:33:48715 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
739export const toggleClassesPaneCheckbox = async (checkboxLabel: string) => {
740 const initialValue = await getContentOfSelectedNode();
741
742 const classesPane = await waitFor(CLS_PANE_SELECTOR);
Elorm Coch34ad3342023-06-06 20:15:59743 await click(`[title="${checkboxLabel}"]`, {root: classesPane});
Patrick Brosset78fa1f52020-08-17 14:33:48744
745 await waitForSelectedNodeChange(initialValue);
746};
747
Al Muthanna Athaminaf3852552023-01-23 16:11:50748export 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 Brosset78fa1f52020-08-17 14:33:48754export 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 Bay0bda6bd2021-07-30 07:10:02767
768export 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 Rudenkoe92fe9d2023-01-30 13:12:23773 await clickElement(moreTabs);
Johan Bay0bda6bd2021-07-30 07:10:02774 a11yPane = await waitForAria('Accessibility');
775 }
Alex Rudenkoe92fe9d2023-01-30 13:12:23776 await clickElement(a11yPane);
Johan Bay0bda6bd2021-07-30 07:10:02777};
778
779export const toggleAccessibilityTree = async () => {
Alex Rudenkoe92fe9d2023-01-30 13:12:23780 await click('aria/Switch to Accessibility Tree view');
Johan Bay0bda6bd2021-07-30 07:10:02781};
Saba Khukhunashvili41695dc2022-06-28 02:23:23782
783export 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};
PhistucK257ad692022-08-29 14:36:19806
807export const summonAndWaitForSearchBox = async () => {
808 await summonSearchBox();
809 await waitFor(SEARCH_BOX_SELECTOR);
810};
811
Ergün Erdoğmuş907a5422022-09-16 18:07:43812export const assertSearchResultMatchesText = async (text: string) => {
813 await waitForFunction(async () => {
814 return await getTextContent(SEARCH_RESULTS_MATCHES) === text;
815 });
PhistucK257ad692022-08-29 14:36:19816};
Philip Pfaffe7fb6a832022-11-14 13:39:47817
818export 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 Athaminaf3852552023-01-23 16:11:50825
826export 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};