blob: 21928c331d7f5bfa82c9a4693ebcec8c84aceecf [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 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('DOMChanges', () => {
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 tracker.getLastChange()?.description;
}
function trackAttributeEdit(edit: Elements.DOMChanges.AttributeEdit): void {
Elements.DOMChanges.trackAttributeEdit(tracker, createNode(), 'div.main', edit);
}
it('records an added attribute together with an anchor for the node', () => {
Elements.DOMChanges.trackAttributeEdit(tracker, createNode(42), 'div.main',
{attributeName: '', oldText: '', newText: 'data-test="value"'});
assert.strictEqual(lastDescription(), 'Added attribute data-test="value"');
assert.deepEqual(lastAnchor(), {
vePath: 'Panel: elements > Tree: elements > TreeItem',
textSignature: 'div.main',
node: {backendNodeId: 42, targetId: 'mock-target-id'},
});
});
it('records an added valueless attribute', () => {
trackAttributeEdit({attributeName: '', oldText: '', newText: 'disabled'});
assert.strictEqual(lastDescription(), 'Added attribute "disabled"');
});
it('records a removed attribute', () => {
trackAttributeEdit({attributeName: 'class', oldText: 'class="container"', newText: ''});
assert.strictEqual(lastDescription(), 'Removed attribute "class"');
});
it('records a changed attribute value', () => {
trackAttributeEdit({attributeName: 'class', oldText: 'class="container"', newText: 'class="container active"'});
assert.strictEqual(lastDescription(), 'Changed attribute "class" from "container" to "container active"');
});
it('records a changed attribute value without a previous value', () => {
trackAttributeEdit({attributeName: 'class', oldText: null, newText: 'class="btn primary"'});
assert.strictEqual(lastDescription(), 'Changed attribute "class" to "btn primary"');
});
it('records a renamed attribute', () => {
trackAttributeEdit({attributeName: 'data-foo', oldText: 'data-foo="12"', newText: 'data-bar="12"'});
assert.strictEqual(lastDescription(), 'Renamed attribute "data-foo" to "data-bar"');
});
it('records an attribute that is renamed and given a new value', () => {
trackAttributeEdit({attributeName: 'cls', oldText: 'cls="bar"', newText: 'class="foo"'});
assert.strictEqual(lastDescription(), 'Renamed attribute "cls"="bar" to "class"="foo"');
});
it('records a tag name edit', () => {
Elements.DOMChanges.trackTagNameEdit(tracker, createNode(), 'div.main', 'div', 'span');
assert.strictEqual(lastDescription(), 'Renamed tag from <div> to <span>');
});
it('records a text node edit', () => {
Elements.DOMChanges.trackTextNodeEdit(tracker, createNode(), 'div.main', 'Hello', 'Hello World');
assert.strictEqual(lastDescription(), 'Changed text from "Hello" to "Hello World"');
});
it('records an HTML edit', () => {
Elements.DOMChanges.trackHTMLEdit(tracker, createNode(), 'div.main', '<div></div>', '<span></span>');
assert.strictEqual(lastDescription(), 'Changed HTML from "<div></div>" to "<span></span>"');
});
it('derives the tag name of a removed node from the node itself', () => {
Elements.DOMChanges.trackNodeRemoval(tracker, createNode(1, 'SPAN'), 'span.child');
assert.strictEqual(lastDescription(), 'Removed node <span>');
assert.strictEqual(lastAnchor()?.textSignature, 'span.child');
});
it('records hiding and unhiding an element', () => {
Elements.DOMChanges.trackVisibilityToggle(tracker, createNode(), 'div.main', /* hidden= */ true);
assert.strictEqual(lastDescription(), 'Hid element <div>');
Elements.DOMChanges.trackVisibilityToggle(tracker, createNode(), 'div.main', /* hidden= */ false);
assert.strictEqual(lastDescription(), 'Unhid element <div>');
});
it('records a duplicated node', () => {
Elements.DOMChanges.trackNodeDuplication(tracker, createNode(1, 'SPAN'), 'span.child');
assert.strictEqual(lastDescription(), 'Duplicated node <span>');
});
it('records a node moved up or down', () => {
Elements.DOMChanges.trackNodeMove(tracker, createNode(), 'div.main', /* directionUp= */ true);
assert.strictEqual(lastDescription(), 'Moved node <div> up');
Elements.DOMChanges.trackNodeMove(tracker, createNode(), 'div.main', /* directionUp= */ false);
assert.strictEqual(lastDescription(), 'Moved node <div> down');
});
it('records a node moved via drag and drop', () => {
Elements.DOMChanges.trackNodeDrop(tracker, createNode(), 'div.main');
assert.strictEqual(lastDescription(), 'Moved node <div> via drag and drop');
});
it('records a pasted node', () => {
Elements.DOMChanges.trackNodePaste(tracker, createNode(), 'div.main', /* isCut= */ false);
assert.strictEqual(lastDescription(), 'Pasted node <div>');
Elements.DOMChanges.trackNodePaste(tracker, createNode(), 'div.main', /* isCut= */ true);
assert.strictEqual(lastDescription(), 'Pasted (moved) node <div>');
});
it('falls back to a generic text signature without a selector', () => {
Elements.DOMChanges.trackNodeRemoval(tracker, createNode(), undefined);
assert.strictEqual(lastAnchor()?.textSignature, 'element');
});
it('trims page controlled data that is too long', () => {
const {MAX_VALUE_LENGTH} = Elements.DOMChanges;
const longValue = 'a'.repeat(MAX_VALUE_LENGTH * 2);
Elements.DOMChanges.trackHTMLEdit(tracker, createNode(), 'div.main', '<div></div>', `<div>${longValue}</div>`);
const description = lastDescription() ?? '';
assert.include(description, 'Changed HTML from "<div></div>" to "<div>aaa');
assert.include(description, '…');
assert.notInclude(description, longValue);
assert.isBelow(description.length, `Changed HTML from "<div></div>" to "<div>${longValue}</div>"`.length);
});
it('does not trim page controlled data that fits', () => {
Elements.DOMChanges.trackTextNodeEdit(tracker, createNode(), 'div.main', 'Hello', 'Hello World');
assert.strictEqual(lastDescription(), 'Changed text from "Hello" to "Hello World"');
});
it('does nothing without a tracker', () => {
Elements.DOMChanges.trackNodeRemoval(undefined, createNode(), 'div.main');
assert.isEmpty(commentManager.getCommentThreads());
});
it('does nothing while change tracking is disabled', () => {
updateHostConfig({devToolsComments: {enabled: false}});
Elements.DOMChanges.trackNodeRemoval(tracker, createNode(), 'div.main');
assert.isUndefined(tracker.getLastChange());
assert.isEmpty(commentManager.getCommentThreads());
});
});