| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import {assert} from 'chai'; | |
| import {describe, it} from '../conductor/mocha-interface.js'; | |
| import { | |
| clickTreeElementWithPartialText, | |
| expandSelectedNodeRecursively, | |
| waitForPartialContentOfSelectedElementsNode, | |
| } from '../helpers/elements-helpers.js'; | |
| describe('The Styles Pane', () => { | |
| it('does not double-redraw when toggling the last active style property', async ({devToolsPage, inspectedPage}) => { | |
| // 1. Navigate directly to a fully-formed document. | |
| // By providing the target styles and element at navigation time, we completely bypass | |
| // the dynamic DOMMutated -> ComputedStyleUpdated 500ms race condition throttle in DevTools. | |
| const html = ` | |
| <!DOCTYPE html> | |
| <style> | |
| .other-rule { color: blue; } | |
| .toggled-rule { background-color: red; } | |
| </style> | |
| <div id="test-target" class="other-rule toggled-rule"></div> | |
| `; | |
| await inspectedPage.goTo(`data:text/html,${encodeURIComponent(html)}`); | |
| // 2. Wait for the styles pane to populate initially | |
| await devToolsPage.waitFor('.styles-selector'); | |
| await expandSelectedNodeRecursively(devToolsPage); | |
| await clickTreeElementWithPartialText(devToolsPage, 'test-target'); | |
| await waitForPartialContentOfSelectedElementsNode(devToolsPage, 'test-target'); | |
| // 3. Retrieve the correct property checkboxes by their explicit DevTools ARIA labels | |
| const getCheckboxByAria = async (ariaLabel: string) => { | |
| await devToolsPage.waitForFunction(async () => { | |
| const boxes = await devToolsPage.$$(`input.enabled-button[aria-label="${ariaLabel}"]`, undefined, 'pierce'); | |
| return boxes.length >= 1; | |
| }); | |
| const matches = await devToolsPage.$$(`input.enabled-button[aria-label="${ariaLabel}"]`, undefined, 'pierce'); | |
| return matches[0]; | |
| }; | |
| const toggledCheckbox = await getCheckboxByAria('background-color red'); | |
| const controlCheckbox = await getCheckboxByAria('color blue'); | |
| // 4. Toggle the correct .toggled-rule checkbox | |
| // This immediately modifies the computed style for 'background-color', firing ComputedStyleChanged. | |
| // We use .evaluate to bypass Puppeteer's visibility check, since the checkbox is opacity: 0 until hovered. | |
| await toggledCheckbox.evaluate(el => (el as HTMLElement).click()); | |
| // Wait for the inspected page to actually reflect the disabled property. | |
| await inspectedPage.waitForFunction(async () => { | |
| return await inspectedPage.evaluate(() => { | |
| const el = document.getElementById('test-target'); | |
| return el ? window.getComputedStyle(el).backgroundColor !== 'rgb(255, 0, 0)' : false; | |
| }); | |
| }); | |
| // Quick yield to give the DevTools frontend time to process the CSSModelChanged/ComputedStyleChanged events | |
| // and potentially trigger the double-flash if the bug was present. (Throttler uses 0ms timeout if idle) | |
| await new Promise(r => setTimeout(r, 50)); | |
| // 5. Assert that toggling the property didn't cause the ENTIRE Styles panel to completely rebuild (double-flash) | |
| let isConnected = false; | |
| try { | |
| // We check controlCheckbox (which belongs to a different rule, .other-rule). | |
| // Optimal updates only rebuild the precisely edited section (where toggledCheckbox lives). | |
| // A double-flash bug would cause a full pane ResetCache & update, wiping controlCheckbox as well! | |
| isConnected = await controlCheckbox.evaluate(el => el.isConnected); | |
| } catch { | |
| // ElementHandle throws if the node is already fully detached from the DOM footprint | |
| isConnected = false; | |
| } | |
| assert.isTrue( | |
| isConnected, | |
| 'Toggling the last active style property caused an additional unexpected redraw (double flash)', | |
| ); | |
| }); | |
| }); |