blob: a0abd364487cd63fdaf7e653598875da14288a53 [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 * as CommentManager from './comment_manager.js';
describe('CommentManager', () => {
let manager: CommentManager.CommentManager.CommentManager;
beforeEach(() => {
manager = new CommentManager.CommentManager.CommentManager();
});
afterEach(() => {
manager.clear();
});
it('manages comment mode and dispatches COMMENT_MODE_CHANGED event', () => {
const modeChangedEvents: boolean[] = [];
manager.addEventListener(CommentManager.CommentManager.Events.COMMENT_MODE_CHANGED, event => {
modeChangedEvents.push(event.data);
});
assert.isFalse(manager.isCommentMode());
// Cannot enable comment mode when agent is not attached
manager.setCommentMode(true);
assert.isFalse(manager.isCommentMode());
assert.deepEqual(modeChangedEvents, []);
manager.setAgentAttached(true);
manager.setCommentMode(true);
assert.isTrue(manager.isCommentMode());
assert.deepEqual(modeChangedEvents, [true]);
// Setting same mode should not dispatch again
manager.setCommentMode(true);
assert.deepEqual(modeChangedEvents, [true]);
manager.setCommentMode(false);
assert.isFalse(manager.isCommentMode());
assert.deepEqual(modeChangedEvents, [true, false]);
});
it('manages agent attached state and dispatches AGENT_ATTACHED_CHANGED event', () => {
const attachedChangedEvents: boolean[] = [];
manager.addEventListener(CommentManager.CommentManager.Events.AGENT_ATTACHED_CHANGED, event => {
attachedChangedEvents.push(event.data);
});
assert.isFalse(manager.isAgentAttached());
manager.setAgentAttached(true);
assert.isTrue(manager.isAgentAttached());
assert.deepEqual(attachedChangedEvents, [true]);
// Setting same value should not dispatch again
manager.setAgentAttached(true);
assert.deepEqual(attachedChangedEvents, [true]);
manager.setCommentMode(true);
assert.isTrue(manager.isCommentMode());
manager.setAgentAttached(false);
assert.isFalse(manager.isAgentAttached());
assert.isFalse(manager.isCommentMode());
assert.deepEqual(attachedChangedEvents, [true, false]);
});
it('creates and retrieves comment threads and dispatches COMMENT_THREADS_CHANGED event', () => {
const threadChangedEvents: CommentManager.CommentManager.CommentThread[][] = [];
manager.addEventListener(CommentManager.CommentManager.Events.COMMENT_THREADS_CHANGED, event => {
threadChangedEvents.push(event.data);
});
const anchor: CommentManager.CommentManager.CommentAnchorSignature = {
vePath: 'Panel: elements > TreeItem: rule',
textSignature: 'color: red;',
};
const thread = manager.createCommentThread(anchor, 'Initial comment');
assert.isNotNull(thread);
assert.strictEqual(thread.anchor, anchor);
assert.lengthOf(thread.comments, 1);
assert.strictEqual(thread.comments[0].author, 'DEVELOPER');
assert.strictEqual(thread.comments[0].text, 'Initial comment');
assert.strictEqual(thread.status, 'DRAFT');
assert.lengthOf(manager.getCommentThreads(), 1);
assert.strictEqual(manager.getCommentThread(thread.id), thread);
assert.lengthOf(threadChangedEvents, 1);
assert.strictEqual(threadChangedEvents[0][0], thread);
thread.save();
assert.strictEqual(thread.status, 'ACTIVE');
assert.lengthOf(threadChangedEvents, 2);
});
it('assigns incrementing index to saved threads', () => {
const anchor: CommentManager.CommentManager.CommentAnchorSignature = {
vePath: 'Panel: elements > TreeItem: rule',
textSignature: 'color: red;',
};
const thread1 = manager.createCommentThread(anchor, 'First comment');
assert.strictEqual(thread1.index, 1);
thread1.save();
assert.strictEqual(thread1.index, 1);
const thread2 = manager.createCommentThread(anchor, 'Second comment');
assert.strictEqual(thread2.index, 2);
thread2.save();
assert.strictEqual(thread2.index, 2);
});
it('creates and retrieves comment threads with TimelineAnchorSignature', () => {
const anchor: CommentManager.CommentManager.CommentAnchorSignature = {
vePath: 'Panel: timeline > FlameChart: main',
textSignature: 'Compile Script',
timeline: {
traceId: 'trace-1',
traceEventKey: 'r-42',
entryName: 'Compile Script',
startTimeMicro: 1000,
durationMicro: 500,
chartLocation: 'main',
},
};
const thread = manager.createCommentThread(anchor, 'Flamechart comment');
assert.isNotNull(thread);
assert.deepEqual(thread.anchor.timeline, {
traceId: 'trace-1',
traceEventKey: 'r-42',
entryName: 'Compile Script',
startTimeMicro: 1000,
durationMicro: 500,
chartLocation: 'main',
});
assert.strictEqual(thread.comments[0].text, 'Flamechart comment');
});
it('supports changes metadata in created threads', () => {
const anchor: CommentManager.CommentManager.CommentAnchorSignature = {
vePath: 'Panel: elements > TreeItem: rule',
textSignature: 'margin: 0;',
};
const changes: CommentManager.CommentManager.ChangeRecord[] = [{
id: 'change-1',
description: 'Changed property "margin" from "0" to "8px"',
timestamp: 123456789,
}];
const thread = manager.createCommentThread(anchor, 'CSS fix', 'DEVELOPER', changes);
assert.strictEqual(thread.comments[0].author, 'DEVELOPER');
assert.strictEqual(thread.comments[0].text, 'CSS fix');
assert.deepEqual(thread.changes, changes);
});
it('resolves comment threads with optional reply text', () => {
const anchor: CommentManager.CommentManager.CommentAnchorSignature = {
vePath: 'Panel: elements > TreeItem: rule',
textSignature: 'color: red;',
};
const thread = manager.createCommentThread(anchor, 'Initial comment');
assert.strictEqual(thread.status, 'DRAFT');
const success = manager.resolveCommentThread(thread.id, 'Done');
assert.isTrue(success);
assert.strictEqual(thread.status, 'RESOLVED');
assert.lengthOf(thread.comments, 2);
assert.strictEqual(thread.comments[1].author, 'AGENT');
assert.strictEqual(thread.comments[1].text, 'Done');
});
it('creates comment thread without initial text leaving comments array empty', () => {
const anchor: CommentManager.CommentManager.CommentAnchorSignature = {
vePath: 'Panel: elements > TreeOutline > TreeItem',
textSignature: 'div.header',
};
const changes: CommentManager.CommentManager.ChangeRecord[] = [{
id: 'change-2',
description: 'Changed attribute "class" to "header active"',
timestamp: 123456789,
}];
const thread = manager.createCommentThread(anchor, undefined, undefined, changes);
assert.isNotNull(thread);
assert.isEmpty(thread.comments);
assert.deepEqual(thread.changes, changes);
assert.strictEqual(thread.status, 'DRAFT');
});
it('returns undefined for non-existent comment thread ID', () => {
assert.isUndefined(manager.getCommentThread('non-existent-id'));
});
it('removes comment threads and dispatches COMMENT_THREADS_CHANGED event', () => {
const anchor: CommentManager.CommentManager.CommentAnchorSignature = {
vePath: 'Panel: network > TableRow: item',
textSignature: 'GET /api',
};
const thread = manager.createCommentThread(anchor, 'Network comment');
assert.lengthOf(manager.getCommentThreads(), 1);
let threadsAfterRemoval: CommentManager.CommentManager.CommentThread[] = [];
manager.addEventListener(CommentManager.CommentManager.Events.COMMENT_THREADS_CHANGED, event => {
threadsAfterRemoval = event.data;
});
manager.removeCommentThread(thread.id);
assert.lengthOf(manager.getCommentThreads(), 0);
assert.deepEqual(threadsAfterRemoval, []);
});
it('does nothing when removing non-existent thread', () => {
let eventCount = 0;
manager.addEventListener(CommentManager.CommentManager.Events.COMMENT_THREADS_CHANGED, () => {
eventCount++;
});
manager.removeCommentThread('non-existent-id');
assert.strictEqual(eventCount, 0);
});
it('clears all threads and resets comment mode on clear()', () => {
manager.setAgentAttached(true);
manager.setCommentMode(true);
const anchor: CommentManager.CommentManager.CommentAnchorSignature = {
vePath: 'Panel: sources > TreeItem: file',
textSignature: 'app.js',
};
manager.createCommentThread(anchor, 'To be cleared');
assert.lengthOf(manager.getCommentThreads(), 1);
assert.isTrue(manager.isCommentMode());
manager.clear();
assert.lengthOf(manager.getCommentThreads(), 0);
assert.isFalse(manager.isCommentMode());
});
it('returns previously unsent comments and marks them as sent in takeComments()', () => {
const anchor: CommentManager.CommentManager.CommentAnchorSignature = {
vePath: 'Panel: elements > TreeItem: rule',
textSignature: 'color: red;',
};
const thread1 = manager.createCommentThread(anchor, 'Initial developer comment', 'DEVELOPER');
assert.isFalse(thread1.transmitted);
assert.strictEqual(thread1.status, 'DRAFT');
// Returns empty array when thread is in DRAFT state
assert.isEmpty(manager.takeComments());
assert.isFalse(thread1.transmitted);
thread1.save();
const taken = manager.takeComments();
assert.lengthOf(taken, 1);
assert.strictEqual(taken[0].id, thread1.id);
assert.isTrue(taken[0].transmitted);
assert.isTrue(thread1.transmitted);
assert.lengthOf(taken[0].comments, 1);
assert.strictEqual(taken[0].comments[0].text, 'Initial developer comment');
// Calling takeComments again returns empty array as it was marked as sent
const takenAgain = manager.takeComments();
assert.lengthOf(takenAgain, 0);
// Creating another thread makes it available in takeComments after save()
const thread2 = manager.createCommentThread(anchor, 'Second thread', 'DEVELOPER');
assert.isFalse(thread2.transmitted);
assert.isEmpty(manager.takeComments());
thread2.save();
const takenNew = manager.takeComments();
assert.lengthOf(takenNew, 1);
assert.strictEqual(takenNew[0].id, thread2.id);
assert.isTrue(thread2.transmitted);
assert.strictEqual(takenNew[0].comments[0].text, 'Second thread');
// Resolving thread with agent reply
manager.resolveCommentThread(thread2.id, 'Agent reply');
assert.lengthOf(manager.takeComments(), 0);
});
it('supports timeline anchor signatures with traceId', () => {
const anchor: CommentManager.CommentManager.CommentAnchorSignature = {
vePath: 'Panel: timeline > FlameChart: main',
textSignature: 'Task',
timeline: {
traceId: 'trace-12345',
traceEventKey: 'r-0',
entryName: 'Task',
startTimeMicro: 1000000,
chartLocation: 'main',
},
};
const thread = manager.createCommentThread(anchor, 'Timeline comment');
assert.strictEqual(thread.anchor.timeline?.traceId, 'trace-12345');
assert.strictEqual(thread.anchor.timeline?.traceEventKey, 'r-0');
});
});