blob: 19de618f1e26eb4f697a6edea808d450a3206c60 [file] [edit]
// 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 {
expandSelectedNodeRecursively,
waitForAndClickTreeElementWithPartialText,
waitForElementsStyleSection,
} from '../helpers/elements-helpers.js';
import {togglePreferenceInSettingsTab} from '../helpers/settings-helpers.js';
import type {DevToolsPage} from '../shared/frontend-helper.js';
// Return the full list of {selector: string, active: boolean}
async function getRuleActivity(devToolsPage: DevToolsPage) {
const sections = await devToolsPage.$$('.styles-section', undefined, 'pierce');
const results: Array<{selector: string, active: boolean}> = [];
for (const section of sections) {
const isInactive = await section.evaluate(s => s.classList.contains('styles-section-inactive'));
const selText = await section.evaluate(s => {
const el = s.querySelector('[aria-label="CSS selector"]');
return el ? el.textContent : null;
});
if (selText) {
results.push({selector: selText, active: !isInactive});
}
}
return results;
}
async function waitForActiveRule(devToolsPage: DevToolsPage, expectedText: string) {
await devToolsPage.waitFor(`[aria-label="${expectedText}, css selector"]:not(.styles-section-inactive)`);
}
describe('The Elements tab', function() {
it('correctly marks styles as inactive based on focus state', async ({devToolsPage, inspectedPage}) => {
// Navigate to page
await inspectedPage.goToResource('elements/inactive-styles.html');
await togglePreferenceInSettingsTab(devToolsPage, 'Show inactive CSS rules', true);
// Select the target element
await expandSelectedNodeRecursively(devToolsPage);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"target"');
await waitForElementsStyleSection(devToolsPage, 'target-element');
await waitForActiveRule(devToolsPage, '.target-element:not(:focus)');
// Unfocused state tests
let rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'element.style', active: true},
{selector: '.target-element:not(:focus)', active: true},
{selector: 'button', active: true},
{selector: '.container:not(:focus-within)', active: true},
{selector: '@font-palette-values --my-palette-2', active: true},
{selector: '--my-prop', active: true},
{selector: '--my-other-func(--a)', active: true},
]);
// Focus the target element in the inspected page
await inspectedPage.click('#target');
await waitForActiveRule(devToolsPage, '.target-element:focus');
rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'element.style', active: true},
{selector: '.target-element:not(:focus)', active: false},
{selector: '.target-element:focus', active: true},
{selector: 'button:enabled:hover', active: true},
{selector: 'button', active: true},
{selector: '.container:not(:focus-within)', active: false},
{selector: '.container:focus-within', active: true},
{selector: '@font-palette-values --my-palette-2', active: false},
{selector: '@font-palette-values --my-palette-1', active: true},
{selector: '--my-prop', active: true},
{selector: '--my-other-func(--a)', active: false},
{selector: '--my-func()', active: true},
]);
// Focus another element
await inspectedPage.click('#other');
await waitForActiveRule(devToolsPage, '.target-element:not(:focus)');
rules = await getRuleActivity(devToolsPage);
// Order is preserved, only active/inactive state changes
assert.deepEqual(rules, [
{selector: 'element.style', active: true},
{selector: '.target-element:not(:focus)', active: true},
{selector: '.target-element:focus', active: false},
{selector: 'button:enabled:hover', active: false},
{selector: 'button', active: true},
{selector: '.container:not(:focus-within)', active: true},
{selector: '.container:focus-within', active: false},
{selector: '@font-palette-values --my-palette-2', active: true},
{selector: '@font-palette-values --my-palette-1', active: false},
{selector: '--my-prop', active: true},
{selector: '--my-other-func(--a)', active: true},
{selector: '--my-func()', active: false},
]);
});
it('correctly shows only active rules when feature is disabled', async ({devToolsPage, inspectedPage}) => {
// Navigate to page
await inspectedPage.goToResource('elements/inactive-styles.html');
// Select the target element
await expandSelectedNodeRecursively(devToolsPage);
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"target"');
await waitForElementsStyleSection(devToolsPage, 'target-element');
await waitForActiveRule(devToolsPage, '.target-element:not(:focus)');
// Unfocused state tests
let rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'element.style', active: true},
{selector: '.target-element:not(:focus)', active: true},
{selector: 'button', active: true},
{selector: '.container:not(:focus-within)', active: true},
{selector: '@font-palette-values --my-palette-2', active: true},
{selector: '--my-prop', active: true},
{selector: '--my-other-func(--a)', active: true},
]);
// Focus the target element in the inspected page
await inspectedPage.click('#target');
await waitForActiveRule(devToolsPage, '.target-element:focus');
rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'element.style', active: true},
{selector: '.target-element:focus', active: true},
{selector: 'button:enabled:hover', active: true},
{selector: 'button', active: true},
{selector: '.container:focus-within', active: true},
{selector: '@font-palette-values --my-palette-1', active: true},
{selector: '--my-prop', active: true},
{selector: '--my-func()', active: true},
]);
// Focus another element
await inspectedPage.click('#other');
await waitForActiveRule(devToolsPage, '.target-element:not(:focus)');
rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'element.style', active: true},
{selector: '.target-element:not(:focus)', active: true},
{selector: 'button', active: true},
{selector: '.container:not(:focus-within)', active: true},
{selector: '@font-palette-values --my-palette-2', active: true},
{selector: '--my-prop', active: true},
{selector: '--my-other-func(--a)', active: true},
]);
});
it('correctly marks animation styles as inactive', async ({devToolsPage, inspectedPage}) => {
// Navigate to page
await inspectedPage.goToResource('elements/inactive-styles.html');
// Remember to turn off the "Show animation styles only when the Animations tab is open" feature.
await togglePreferenceInSettingsTab(devToolsPage, 'Show animation styles only when the Animations tab is open',
false);
await togglePreferenceInSettingsTab(devToolsPage, 'Show inactive CSS rules', true);
// Actually click the element with the exact text "id="animated""
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"animated"');
await waitForElementsStyleSection(devToolsPage, 'animated-element');
async function waitForInactiveRule(expectedText: string) {
await devToolsPage.waitFor(`[aria-label="${expectedText}, css selector"].styles-section-inactive`);
}
// Inspect the element while it is not animated
await waitForActiveRule(devToolsPage, 'div');
let rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'element.style', active: true},
{selector: 'div', active: true},
{selector: '--my-prop', active: true},
]);
// Hover the animated element to trigger the animation
await inspectedPage.click('#animated');
await waitForActiveRule(devToolsPage, '100%');
rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'my-animation animation', active: true},
{selector: 'element.style', active: true},
{selector: '.animated-element.animating', active: true},
{selector: 'div', active: true},
{selector: '0%', active: true},
{selector: '100%', active: true},
{selector: '--my-prop', active: true},
]);
// Un-click the animated element to stop the animation
await inspectedPage.click('#animated');
await waitForInactiveRule('100%');
await waitForActiveRule(devToolsPage, 'div');
// Keyframes section remains above the properties section, though there are
// no active rules in it.
rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'my-animation animation', active: false}, // if there's a link header/section
{selector: 'element.style', active: true},
{selector: '.animated-element.animating', active: false},
{selector: 'div', active: true},
{selector: '0%', active: false},
{selector: '100%', active: false},
{selector: '--my-prop', active: true},
]);
});
it('correctly reorders rules when simultaneously active', async ({devToolsPage, inspectedPage}) => {
// Navigate to page
await inspectedPage.goToResource('elements/inactive-styles.html');
await togglePreferenceInSettingsTab(devToolsPage, 'Show inactive CSS rules', true);
// Select the ab element
await waitForAndClickTreeElementWithPartialText(devToolsPage, 'id=\u200B"ab-target"');
await waitForElementsStyleSection(devToolsPage, 'ab-element');
// Initially State A
await waitForActiveRule(devToolsPage, '.ab-element.state-a'); // poll until DevTools refreshes the style pane
let rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'element.style', active: true},
{selector: '.ab-element.state-a', active: true},
{selector: 'div', active: true},
{selector: '--my-prop', active: true},
]);
// Click to transition to State B
await inspectedPage.click('#ab-target');
await waitForActiveRule(devToolsPage, '.ab-element.state-b');
rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'element.style', active: true},
{selector: '.ab-element.state-a', active: false},
{selector: '.ab-element.state-b', active: true},
{selector: 'div', active: true},
{selector: '--my-prop', active: true},
]);
// Click to transition to State A + B
await inspectedPage.click('#ab-target');
await waitForActiveRule(devToolsPage, '.ab-element.state-a'); // state-a becomes active again!
// Now that both state-a and state-b are active, we discover the correct order
rules = await getRuleActivity(devToolsPage);
assert.deepEqual(rules, [
{selector: 'element.style', active: true},
{selector: '.ab-element.state-b', active: true},
{selector: '.ab-element.state-a', active: true},
{selector: 'div', active: true},
{selector: '--my-prop', active: true},
]);
});
});