// 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 sinon from 'sinon';
import * as Common from '../../core/common/common.js';
import * as SDK from '../../core/sdk/sdk.js';
import { assertScreenshot, raf, renderElementIntoDOM } from '../../testing/DOMHelpers.js';
import { describeWithEnvironment } from '../../testing/EnvironmentHelpers.js';
import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Sources from './sources.js';
describeWithEnvironment('WatchExpression', () => {
    it('creates read-only object properties for watch expression', async () => {
        const object = SDK.RemoteObject.RemoteObject.fromLocalObject({ foo: 'bar' });
        const executionContext = sinon.createStubInstance(SDK.RuntimeModel.ExecutionContext);
        const debuggerModel = sinon.createStubInstance(SDK.DebuggerModel.DebuggerModel);
        debuggerModel.selectedCallFrame.returns(null);
        executionContext.debuggerModel = debuggerModel;
        executionContext.evaluate.resolves({ object, exceptionDetails: undefined });
        sinon.stub(UI.Context.Context.instance(), 'flavor').returns(executionContext);
        const expansionTracker = new ObjectUI.ObjectPropertiesSection.ObjectTreeExpansionTracker();
        const watchExpression = new Sources.WatchExpressionsSidebarPane.WatchExpression();
        await watchExpression.setExpression('obj', expansionTracker);
        assert.isTrue(watchExpression.result?.readOnly);
    });
    it('shows "No watch expressions" when empty', async () => {
        Common.Settings.Settings.instance().createLocalSetting('watch-expressions', []).set([]);
        const pane = new Sources.WatchExpressionsSidebarPane.WatchExpressionsSidebarPane();
        renderElementIntoDOM(pane);
        await raf();
        await pane.updateComplete;
        const emptyElement = pane.contentElement.querySelector('.gray-info-message');
        assert.exists(emptyElement);
        assert.strictEqual(emptyElement.textContent, 'No watch expressions');
    });
    it('adds expression via action', async () => {
        const setting = Common.Settings.Settings.instance().createLocalSetting('watch-expressions', []);
        setting.set([]);
        const pane = new Sources.WatchExpressionsSidebarPane.WatchExpressionsSidebarPane();
        renderElementIntoDOM(pane);
        await raf();
        await pane.updateComplete;
        const frame = sinon.createStubInstance(Sources.UISourceCodeFrame.UISourceCodeFrame);
        sinon.stub(frame, 'textEditor').value({ state: { sliceDoc: () => '1 + 1', selection: { main: { from: 0, to: 5 } } } });
        UI.Context.Context.instance().setFlavor(Sources.UISourceCodeFrame.UISourceCodeFrame, frame);
        sinon.stub(UI.ViewManager.ViewManager.instance(), 'showView').resolves();
        pane.handleAction(UI.Context.Context.instance(), 'sources.add-to-watch');
        await raf();
        await pane.updateComplete;
        assert.deepEqual(setting.get(), ['1 + 1']);
    });
    it('edits an expression and saves to settings', async () => {
        const setting = Common.Settings.Settings.instance().createLocalSetting('watch-expressions', []);
        setting.set(['1 + 1']);
        const executionContext = sinon.createStubInstance(SDK.RuntimeModel.ExecutionContext);
        const debuggerModel = sinon.createStubInstance(SDK.DebuggerModel.DebuggerModel);
        debuggerModel.selectedCallFrame.returns(null);
        executionContext.debuggerModel = debuggerModel;
        executionContext.evaluate.resolves({ object: SDK.RemoteObject.RemoteObject.fromLocalObject(2), exceptionDetails: undefined });
        sinon.stub(UI.Context.Context.instance(), 'flavor').returns(executionContext);
        const pane = new Sources.WatchExpressionsSidebarPane.WatchExpressionsSidebarPane();
        renderElementIntoDOM(pane);
        await raf();
        await pane.updateComplete;
        const treeElement = pane.contentElement.querySelector('devtools-tree');
        const listItemElement = treeElement?.shadowRoot?.querySelector('.watch-expression-tree-item');
        assert.exists(listItemElement);
        const headerElement = listItemElement.querySelector('.watch-expression-header');
        assert.exists(headerElement);
        // Double click to start editing
        headerElement.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
        await raf();
        await pane.updateComplete;
        const textPromptElement = listItemElement.querySelector('devtools-prompt');
        assert.exists(textPromptElement);
        textPromptElement.dispatchEvent(new CustomEvent('commit', { detail: '2 + 2' }));
        await raf();
        await pane.updateComplete;
        assert.deepEqual(setting.get(), ['2 + 2']);
    });
    it('deletes an expression and saves to settings', async () => {
        const setting = Common.Settings.Settings.instance().createLocalSetting('watch-expressions', []);
        setting.set(['1 + 1']);
        const executionContext = sinon.createStubInstance(SDK.RuntimeModel.ExecutionContext);
        const debuggerModel = sinon.createStubInstance(SDK.DebuggerModel.DebuggerModel);
        debuggerModel.selectedCallFrame.returns(null);
        executionContext.debuggerModel = debuggerModel;
        executionContext.evaluate.resolves({ object: SDK.RemoteObject.RemoteObject.fromLocalObject(2), exceptionDetails: undefined });
        sinon.stub(UI.Context.Context.instance(), 'flavor').returns(executionContext);
        const pane = new Sources.WatchExpressionsSidebarPane.WatchExpressionsSidebarPane();
        renderElementIntoDOM(pane);
        await raf();
        await pane.updateComplete;
        const treeElement = pane.contentElement.querySelector('devtools-tree');
        const listItemElement = treeElement?.shadowRoot?.querySelector('.watch-expression-tree-item');
        assert.exists(listItemElement);
        const deleteButton = listItemElement.querySelector('.watch-expression-delete-button');
        assert.exists(deleteButton);
        deleteButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
        await raf();
        await pane.updateComplete;
        assert.deepEqual(setting.get(), []);
    });
    it('screenshot for empty state', async () => {
        Common.Settings.Settings.instance().createLocalSetting('watch-expressions', []).set([]);
        const pane = new Sources.WatchExpressionsSidebarPane.WatchExpressionsSidebarPane();
        pane.element.style.width = '300px';
        pane.element.style.height = '200px';
        renderElementIntoDOM(pane, { includeCommonStyles: true });
        await raf();
        await pane.updateComplete;
        await assertScreenshot('sources/watch-expressions-empty.png');
    });
    it('screenshot for list of expressions', async () => {
        Common.Settings.Settings.instance().createLocalSetting('watch-expressions', []).set(['1 + 1', '2 + 2']);
        const object1 = SDK.RemoteObject.RemoteObject.fromLocalObject(2);
        const object2 = SDK.RemoteObject.RemoteObject.fromLocalObject(4);
        const executionContext = sinon.createStubInstance(SDK.RuntimeModel.ExecutionContext);
        const debuggerModel = sinon.createStubInstance(SDK.DebuggerModel.DebuggerModel);
        debuggerModel.selectedCallFrame.returns(null);
        executionContext.debuggerModel = debuggerModel;
        executionContext.evaluate.callsFake(async (options) => {
            if (options.expression === '1 + 1') {
                return { object: object1, exceptionDetails: undefined };
            }
            return { object: object2, exceptionDetails: undefined };
        });
        sinon.stub(UI.Context.Context.instance(), 'flavor').returns(executionContext);
        const pane = Sources.WatchExpressionsSidebarPane.WatchExpressionsSidebarPane.instance();
        UI.Context.Context.instance().setFlavor(SDK.RuntimeModel.ExecutionContext, executionContext);
        pane.element.style.width = '300px';
        pane.element.style.height = '200px';
        renderElementIntoDOM(pane, { includeCommonStyles: true });
        await raf();
        await pane.updateComplete;
        const watchExpressions = pane.watchExpressions;
        assert.lengthOf(watchExpressions, 2);
        await assertScreenshot('sources/watch-expressions-list.png');
    });
    it('screenshot for watch expression with delete button visible', async () => {
        Common.Settings.Settings.instance().createLocalSetting('watch-expressions', []).set(['1 + 1']);
        const object1 = SDK.RemoteObject.RemoteObject.fromLocalObject(2);
        const executionContext = sinon.createStubInstance(SDK.RuntimeModel.ExecutionContext);
        const debuggerModel = sinon.createStubInstance(SDK.DebuggerModel.DebuggerModel);
        debuggerModel.selectedCallFrame.returns(null);
        executionContext.debuggerModel = debuggerModel;
        executionContext.evaluate.resolves({ object: object1, exceptionDetails: undefined });
        sinon.stub(UI.Context.Context.instance(), 'flavor').returns(executionContext);
        const pane = new Sources.WatchExpressionsSidebarPane.WatchExpressionsSidebarPane();
        pane.element.style.width = '300px';
        pane.element.style.height = '200px';
        renderElementIntoDOM(pane);
        await raf();
        await pane.updateComplete;
        const watchExpressions = pane.watchExpressions;
        assert.lengthOf(watchExpressions, 1);
        const treeElement = pane.contentElement.querySelector('devtools-tree');
        const listItemElement = treeElement?.shadowRoot?.querySelector('.watch-expression-tree-item');
        assert.exists(listItemElement);
        const deleteButton = listItemElement.querySelector('.watch-expression-delete-button');
        assert.exists(deleteButton);
        // Focus the delete button to make it visible via :focus-within on the title
        deleteButton.focus();
        await pane.updateComplete;
        await assertScreenshot('sources/watch-expression-delete-button.png');
    });
    it('preserves expansion state across updates', async () => {
        const object = SDK.RemoteObject.RemoteObject.fromLocalObject({ foo: { bar: 'baz' } });
        const executionContext = sinon.createStubInstance(SDK.RuntimeModel.ExecutionContext);
        const debuggerModel = sinon.createStubInstance(SDK.DebuggerModel.DebuggerModel);
        debuggerModel.selectedCallFrame.returns(null);
        executionContext.debuggerModel = debuggerModel;
        executionContext.evaluate.resolves({ object, exceptionDetails: undefined });
        sinon.stub(UI.Context.Context.instance(), 'flavor').returns(executionContext);
        const setting = Common.Settings.Settings.instance().createLocalSetting('watch-expressions', []);
        setting.set(['obj']);
        const pane = new Sources.WatchExpressionsSidebarPane.WatchExpressionsSidebarPane();
        renderElementIntoDOM(pane);
        await raf();
        await pane.updateComplete;
        const watchExpression = pane.watchExpressions[0];
        const tree = watchExpression.result;
        assert.exists(tree);
        assert.isFalse(tree.expanded);
        // Expand the root
        tree.expanded = true;
        assert.isTrue(tree.expanded);
        // Expand the 'foo' property
        const children = await tree.populateChildrenIfNeeded();
        const fooProperty = children.properties?.find(p => p.name === 'foo');
        assert.exists(fooProperty);
        fooProperty.expanded = true;
        assert.isTrue(fooProperty.expanded);
        // Trigger update by refreshing
        UI.Context.Context.instance().setFlavor(SDK.RuntimeModel.ExecutionContext, executionContext);
        await raf();
        await pane.updateComplete;
        const newWatchExpression = pane.watchExpressions[0];
        const newTree = newWatchExpression.result;
        assert.notStrictEqual(tree, newTree);
        assert.isTrue(newTree.expanded);
        const newChildren = await newTree.populateChildrenIfNeeded();
        const newFooProperty = newChildren.properties?.find(p => p.name === 'foo');
        assert.exists(newFooProperty);
        assert.notStrictEqual(fooProperty, newFooProperty);
        assert.isTrue(newFooProperty.expanded);
    });
    it('clears expansion state when expression changes', async () => {
        const object = SDK.RemoteObject.RemoteObject.fromLocalObject({ foo: { bar: 'baz' } });
        const executionContext = sinon.createStubInstance(SDK.RuntimeModel.ExecutionContext);
        const debuggerModel = sinon.createStubInstance(SDK.DebuggerModel.DebuggerModel);
        debuggerModel.selectedCallFrame.returns(null);
        executionContext.debuggerModel = debuggerModel;
        executionContext.evaluate.resolves({ object, exceptionDetails: undefined });
        sinon.stub(UI.Context.Context.instance(), 'flavor').returns(executionContext);
        const setting = Common.Settings.Settings.instance().createLocalSetting('watch-expressions', []);
        setting.set(['obj1']);
        const pane = new Sources.WatchExpressionsSidebarPane.WatchExpressionsSidebarPane();
        renderElementIntoDOM(pane);
        await raf();
        await pane.updateComplete;
        const watchExpression = pane.watchExpressions[0];
        const tree = watchExpression.result;
        assert.exists(tree);
        // Expand the root and a property
        tree.expanded = true;
        const children = await tree.populateChildrenIfNeeded();
        const fooProperty = children.properties?.find(p => p.name === 'foo');
        assert.exists(fooProperty);
        fooProperty.expanded = true;
        assert.isTrue(tree.expanded);
        assert.isTrue(fooProperty.expanded);
        // Change expression
        const treeElement = pane.contentElement.querySelector('devtools-tree');
        const listItemElement = treeElement?.shadowRoot?.querySelector('.watch-expression-tree-item');
        const headerElement = listItemElement.querySelector('.watch-expression-header');
        headerElement.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
        await raf();
        await pane.updateComplete;
        const textPromptElement = listItemElement.querySelector('devtools-prompt');
        textPromptElement.dispatchEvent(new CustomEvent('commit', { detail: 'obj2' }));
        await raf();
        await pane.updateComplete;
        const newWatchExpression = pane.watchExpressions[0];
        const newTree = newWatchExpression.result;
        assert.notStrictEqual(tree, newTree);
        assert.isFalse(newTree.expanded);
        // Re-query fooProperty and check it's not expanded
        const newChildren = await newTree.populateChildrenIfNeeded();
        const newFooProperty = newChildren.properties?.find(p => p.name === 'foo');
        assert.exists(newFooProperty);
        assert.notStrictEqual(fooProperty, newFooProperty);
        assert.isFalse(newFooProperty.expanded);
    });
    it('clears expansion state for the root when expression changes and changes back', async () => {
        const object = SDK.RemoteObject.RemoteObject.fromLocalObject({ foo: { bar: 'baz' } });
        const executionContext = sinon.createStubInstance(SDK.RuntimeModel.ExecutionContext);
        const debuggerModel = sinon.createStubInstance(SDK.DebuggerModel.DebuggerModel);
        debuggerModel.selectedCallFrame.returns(null);
        executionContext.debuggerModel = debuggerModel;
        executionContext.evaluate.resolves({ object, exceptionDetails: undefined });
        sinon.stub(UI.Context.Context.instance(), 'flavor').returns(executionContext);
        const setting = Common.Settings.Settings.instance().createLocalSetting('watch-expressions', []);
        setting.set(['obj1']);
        const pane = new Sources.WatchExpressionsSidebarPane.WatchExpressionsSidebarPane();
        renderElementIntoDOM(pane);
        await raf();
        await pane.updateComplete;
        const watchExpression = pane.watchExpressions[0];
        const tree = watchExpression.result;
        assert.exists(tree);
        tree.expanded = true;
        const children = await tree.populateChildrenIfNeeded();
        const fooProperty = children.properties?.find(p => p.name === 'foo');
        assert.exists(fooProperty);
        fooProperty.expanded = true;
        assert.isTrue(tree.expanded);
        assert.isTrue(fooProperty.expanded);
        // Change expression to obj2
        const treeElement = pane.contentElement.querySelector('devtools-tree');
        let listItemElement = treeElement?.shadowRoot?.querySelector('.watch-expression-tree-item');
        let headerElement = listItemElement.querySelector('.watch-expression-header');
        headerElement.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
        await raf();
        await pane.updateComplete;
        let textPromptElement = listItemElement.querySelector('devtools-prompt');
        textPromptElement.dispatchEvent(new CustomEvent('commit', { detail: 'obj2' }));
        await raf();
        await pane.updateComplete;
        // Change expression back to obj1
        listItemElement = treeElement?.shadowRoot?.querySelector('.watch-expression-tree-item');
        headerElement = listItemElement.querySelector('.watch-expression-header');
        headerElement.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
        await raf();
        await pane.updateComplete;
        textPromptElement = listItemElement.querySelector('devtools-prompt');
        textPromptElement.dispatchEvent(new CustomEvent('commit', { detail: 'obj1' }));
        await raf();
        await pane.updateComplete;
        const newWatchExpression = pane.watchExpressions[0];
        const finalTree = newWatchExpression.result;
        assert.isFalse(finalTree.expanded, 'Expansion state of root should be cleared when expression changes');
        const finalChildren = await finalTree.populateChildrenIfNeeded();
        const finalFooProperty = finalChildren.properties?.find(p => p.name === 'foo');
        assert.exists(finalFooProperty);
        assert.notStrictEqual(fooProperty, finalFooProperty);
        assert.isFalse(finalFooProperty.expanded);
    });
});
//# sourceMappingURL=WatchExpressionsSidebarPane.test.js.map