| // 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 {beforeEach, describe, it} from 'mocha'; | |
| import type * as SDK from '../../core/sdk/sdk.js'; | |
| import {updateHostConfig} from '../../testing/EnvironmentHelpers.js'; | |
| import * as ChangeTracker from '../change_tracker/change_tracker.js'; | |
| import * as CommentManager from '../comment_manager/comment_manager.js'; | |
| import * as Elements from './elements.js'; | |
| describe('StyleChanges', () => { | |
| let commentManager: CommentManager.CommentManager.CommentManager; | |
| let tracker: ChangeTracker.ChangeTracker.ChangeTracker; | |
| beforeEach(() => { | |
| updateHostConfig({ | |
| devToolsComments: { | |
| enabled: true, | |
| }, | |
| }); | |
| commentManager = new CommentManager.CommentManager.CommentManager(); | |
| commentManager.setAgentAttached(true); | |
| tracker = new ChangeTracker.ChangeTracker.ChangeTracker(commentManager); | |
| }); | |
| function createNode(backendNodeId = 1, nodeName = 'DIV'): SDK.DOMModel.DOMNode { | |
| return { | |
| backendNodeId: () => backendNodeId, | |
| nodeName: () => nodeName, | |
| domModel: () => ({target: () => ({id: () => 'mock-target-id'})}), | |
| } as unknown as SDK.DOMModel.DOMNode; | |
| } | |
| function lastAnchor(): CommentManager.CommentManager.CommentAnchorSignature|undefined { | |
| return commentManager.getCommentThreads().at(-1)?.anchor; | |
| } | |
| function lastDescription(): string|undefined { | |
| return commentManager.getCommentThreads().at(-1)?.comments[0]?.text; | |
| } | |
| function trackStylePropertyEdit(edit: Elements.StyleChanges.StylePropertyEdit): void { | |
| Elements.StyleChanges.trackStylePropertyEdit(tracker, createNode(), edit); | |
| } | |
| it('records an added style property together with an anchor for the node', () => { | |
| Elements.StyleChanges.trackStylePropertyEdit(tracker, createNode(42), { | |
| ruleHeader: '.card:hover', | |
| oldName: '', | |
| oldValue: '', | |
| newName: 'color', | |
| newValue: 'red', | |
| }); | |
| assert.strictEqual(lastDescription(), 'Added style property "color: red" to rule ".card:hover"'); | |
| assert.deepEqual(lastAnchor(), { | |
| vePath: 'Panel: elements > Pane: styles', | |
| node: {backendNodeId: 42, targetId: 'mock-target-id'}, | |
| }); | |
| }); | |
| it('records a removed style property', () => { | |
| trackStylePropertyEdit({ | |
| ruleHeader: 'element.style', | |
| oldName: 'display', | |
| oldValue: 'flex', | |
| newName: '', | |
| newValue: '', | |
| }); | |
| assert.strictEqual(lastDescription(), 'Removed style property "display: flex" from rule "element.style"'); | |
| }); | |
| it('records a changed style property value', () => { | |
| trackStylePropertyEdit({ | |
| ruleHeader: '.btn', | |
| oldName: 'color', | |
| oldValue: 'blue', | |
| newName: 'color', | |
| newValue: 'green', | |
| }); | |
| assert.strictEqual(lastDescription(), 'Changed style property "color" from "blue" to "green" in rule ".btn"'); | |
| }); | |
| it('records a renamed style property', () => { | |
| trackStylePropertyEdit({ | |
| ruleHeader: 'element.style', | |
| oldName: 'margin-left', | |
| oldValue: '10px', | |
| newName: 'margin-right', | |
| newValue: '10px', | |
| }); | |
| assert.strictEqual( | |
| lastDescription(), | |
| 'Changed style property from "margin-left: 10px" to "margin-right: 10px" in rule "element.style"', | |
| ); | |
| }); | |
| it('records a style property that is renamed and given a new value', () => { | |
| trackStylePropertyEdit({ | |
| ruleHeader: 'element.style', | |
| oldName: 'color', | |
| oldValue: 'red', | |
| newName: 'background-color', | |
| newValue: 'blue', | |
| }); | |
| assert.strictEqual( | |
| lastDescription(), | |
| 'Changed style property from "color: red" to "background-color: blue" in rule "element.style"', | |
| ); | |
| }); | |
| it('records disabling and enabling a style property', () => { | |
| Elements.StyleChanges.trackStylePropertyToggle(tracker, createNode(42), { | |
| ruleHeader: 'element.style', | |
| name: 'font-weight', | |
| value: 'bold', | |
| disabled: true, | |
| }); | |
| assert.strictEqual(lastDescription(), 'Disabled style property "font-weight: bold" in rule "element.style"'); | |
| Elements.StyleChanges.trackStylePropertyToggle(tracker, createNode(42), { | |
| ruleHeader: 'element.style', | |
| name: 'font-weight', | |
| value: 'bold', | |
| disabled: false, | |
| }); | |
| assert.strictEqual(lastDescription(), 'Enabled style property "font-weight: bold" in rule "element.style"'); | |
| }); | |
| it('records adding a new style rule', () => { | |
| Elements.StyleChanges.trackStyleRuleAdded(tracker, createNode(42), ' .card >\n .title '); | |
| assert.strictEqual(lastDescription(), 'Added style rule ".card > .title"'); | |
| const countBefore = commentManager.getCommentThreads().length; | |
| Elements.StyleChanges.trackStyleRuleAdded(tracker, createNode(42), ' '); | |
| assert.lengthOf(commentManager.getCommentThreads(), countBefore); | |
| }); | |
| it('records changing a style rule selector', () => { | |
| Elements.StyleChanges.trackStyleSelectorChange(tracker, createNode(42), ' .card ', | |
| ' .card:hover,\n .card:focus '); | |
| assert.strictEqual(lastDescription(), 'Changed style rule selector from ".card" to ".card:hover, .card:focus"'); | |
| const countBefore = commentManager.getCommentThreads().length; | |
| Elements.StyleChanges.trackStyleSelectorChange(tracker, createNode(42), '.card', ' .card '); | |
| Elements.StyleChanges.trackStyleSelectorChange(tracker, createNode(42), ' ', '.card'); | |
| Elements.StyleChanges.trackStyleSelectorChange(tracker, createNode(42), '.card', ' '); | |
| assert.lengthOf(commentManager.getCommentThreads(), countBefore); | |
| }); | |
| it('records changing an @property rule name', () => { | |
| Elements.StyleChanges.trackPropertyRuleNameChange(tracker, createNode(42), ' --old-prop ', ' --new-prop '); | |
| assert.strictEqual(lastDescription(), 'Changed @property rule name from "--old-prop" to "--new-prop"'); | |
| }); | |
| it('records changing a keyframe key', () => { | |
| Elements.StyleChanges.trackKeyframeKeyChange(tracker, createNode(42), ' 0% ', ' 50%,\n 100% '); | |
| assert.strictEqual(lastDescription(), 'Changed keyframe key from "0%" to "50%, 100%"'); | |
| }); | |
| it('records changing an @media query', () => { | |
| Elements.StyleChanges.trackStyleQueryChange(tracker, createNode(42), '@media', ' (max-width: 600px) ', | |
| ' (min-width: 800px) '); | |
| assert.strictEqual(lastDescription(), 'Changed @media query from "(max-width: 600px)" to "(min-width: 800px)"'); | |
| assert.deepEqual(lastAnchor(), { | |
| vePath: 'Panel: elements > Pane: styles', | |
| node: {backendNodeId: 42, targetId: 'mock-target-id'}, | |
| }); | |
| }); | |
| it('records changing an @container query', () => { | |
| Elements.StyleChanges.trackStyleQueryChange(tracker, createNode(42), '@container', 'sidebar (min-width: 400px)', | |
| 'sidebar (min-width: 500px)'); | |
| assert.strictEqual(lastDescription(), | |
| 'Changed @container query from "sidebar (min-width: 400px)" to "sidebar (min-width: 500px)"'); | |
| }); | |
| it('records changing an @supports query', () => { | |
| Elements.StyleChanges.trackStyleQueryChange(tracker, createNode(42), '@supports', '(display: grid)', | |
| '(display: flex)'); | |
| assert.strictEqual(lastDescription(), 'Changed @supports query from "(display: grid)" to "(display: flex)"'); | |
| }); | |
| it('records changing an @scope query', () => { | |
| Elements.StyleChanges.trackStyleQueryChange(tracker, createNode(42), '@scope', '(.card)', '(.content)'); | |
| assert.strictEqual(lastDescription(), 'Changed @scope query from "(.card)" to "(.content)"'); | |
| }); | |
| it('records changing an @navigation query', () => { | |
| Elements.StyleChanges.trackStyleQueryChange(tracker, createNode(42), '@navigation', '(navigation-type: push)', | |
| '(navigation-type: traverse)'); | |
| assert.strictEqual(lastDescription(), | |
| 'Changed @navigation query from "(navigation-type: push)" to "(navigation-type: traverse)"'); | |
| }); | |
| it('records clearing a style query', () => { | |
| Elements.StyleChanges.trackStyleQueryChange(tracker, createNode(42), '@scope', '(.card)', ' '); | |
| assert.strictEqual(lastDescription(), 'Cleared @scope query "(.card)"'); | |
| }); | |
| it('omits the rule clause when ruleHeader is omitted or empty', () => { | |
| trackStylePropertyEdit({ | |
| oldName: 'color', | |
| oldValue: 'red', | |
| newName: 'color', | |
| newValue: 'blue', | |
| }); | |
| assert.strictEqual(lastDescription(), 'Changed style property "color" from "red" to "blue"'); | |
| }); | |
| it('collapses multiline whitespace in ruleHeader', () => { | |
| trackStylePropertyEdit({ | |
| ruleHeader: ' h1,\n h2,\n\t h3 ', | |
| oldName: 'color', | |
| oldValue: 'red', | |
| newName: 'color', | |
| newValue: 'blue', | |
| }); | |
| assert.strictEqual(lastDescription(), 'Changed style property "color" from "red" to "blue" in rule "h1, h2, h3"'); | |
| }); | |
| it('trims CSS values that are too long', () => { | |
| const {MAX_VALUE_LENGTH} = Elements.ChangeTrackingUtils; | |
| const longValue = 'a'.repeat(MAX_VALUE_LENGTH * 2); | |
| trackStylePropertyEdit({ | |
| ruleHeader: 'element.style', | |
| oldName: '--custom', | |
| oldValue: 'initial', | |
| newName: '--custom', | |
| newValue: `url("${longValue}")`, | |
| }); | |
| const description = lastDescription() ?? ''; | |
| assert.include(description, 'Changed style property "--custom" from "initial" to "url("aaa'); | |
| assert.include(description, '…'); | |
| assert.notInclude(description, longValue); | |
| }); | |
| it('does nothing when the committed name and value match the previous state', () => { | |
| trackStylePropertyEdit({ | |
| ruleHeader: 'element.style', | |
| oldName: 'color', | |
| oldValue: 'red', | |
| newName: 'color', | |
| newValue: 'red', | |
| }); | |
| assert.isEmpty(commentManager.getCommentThreads()); | |
| }); | |
| it('does nothing while change tracking is disabled', () => { | |
| updateHostConfig({devToolsComments: {enabled: false}}); | |
| trackStylePropertyEdit({ | |
| oldName: 'color', | |
| oldValue: 'red', | |
| newName: 'color', | |
| newValue: 'blue', | |
| }); | |
| assert.isUndefined(lastDescription()); | |
| assert.isEmpty(commentManager.getCommentThreads()); | |
| }); | |
| }); |