// Copyright 2025 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 { renderElementIntoDOM } from '../../../testing/DOMHelpers.js';
import * as UI from '../../legacy/legacy.js';
import * as Lit from '../../lit/lit.js';
import * as Tooltips from './tooltips.js';
const { html, nothing } = Lit;
function renderTooltip({ variant = 'simple', attribute = 'aria-describedby', trigger = 'hover', useHotkey = false, jslogContext = undefined, id = 'tooltip-id', } = {}) {
    const container = document.createElement('div');
    // clang-format off
    Lit.render(html `
    ${attribute === 'aria-details' ?
        html `<button aria-details=${id}>Button</button>` :
        html `<button aria-describedby=${id}>Button</button>`}
    <devtools-tooltip
     id=${id}
     variant=${variant}
     .hover-delay=${0}
     trigger=${trigger}
     ?use-hotkey=${useHotkey}
     jslogContext=${jslogContext ?? nothing}
     >
      ${variant === 'rich' ? html `<p>Rich content</p>` : 'Simple content'}
    </devtools-tooltip>
  `, container);
    // clang-format on
    renderElementIntoDOM(container, { allowMultipleChildren: true });
    return container;
}
async function waitForToggle(tooltip, state) {
    return await new Promise(resolve => {
        tooltip.addEventListener('toggle', (event) => {
            if (event.newState === state) {
                resolve();
            }
        }, { once: true });
    });
}
describe('Tooltip', () => {
    let inspectorViewRootElementStub;
    beforeEach(async () => {
        inspectorViewRootElementStub = document.createElement('div');
        renderElementIntoDOM(inspectorViewRootElementStub, { allowMultipleChildren: true });
        const inspectorViewStub = sinon.createStubInstance(UI.InspectorView.InspectorView);
        Object.assign(inspectorViewStub, { element: inspectorViewRootElementStub });
        sinon.stub(UI.InspectorView.InspectorView, 'instance').returns(inspectorViewStub);
        Tooltips.Tooltip.Tooltip.lastOpenedTooltipId = null;
    });
    it('renders a simple tooltip', () => {
        const container = renderTooltip();
        const tooltip = container.querySelector('devtools-tooltip');
        assert.strictEqual(tooltip?.variant, 'simple');
        assert.strictEqual(container.querySelector('devtools-tooltip')?.textContent?.trim(), 'Simple content');
    });
    it('renders a rich tooltip', () => {
        const container = renderTooltip({ variant: 'rich', attribute: 'aria-details' });
        const tooltip = container.querySelector('devtools-tooltip');
        assert.strictEqual(tooltip?.variant, 'rich');
        assert.strictEqual(container.querySelector('devtools-tooltip')?.querySelector('p')?.textContent, 'Rich content');
    });
    it('should be activated if hovered', async () => {
        const container = renderTooltip();
        const tooltip = container.querySelector('devtools-tooltip');
        assert.exists(tooltip);
        const button = container.querySelector('button');
        const opened = waitForToggle(tooltip, 'open');
        button?.dispatchEvent(new MouseEvent('mouseenter'));
        await opened;
        assert.isTrue(tooltip.open);
    });
    it('should close if the user presses escape when it is open', async () => {
        const container = renderTooltip();
        const tooltip = container.querySelector('devtools-tooltip');
        assert.exists(tooltip);
        const button = container.querySelector('button');
        const opened = waitForToggle(tooltip, 'open');
        button?.dispatchEvent(new MouseEvent('mouseenter'));
        await opened;
        assert.isTrue(tooltip.open);
        const closed = waitForToggle(tooltip, 'closed');
        document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
        await closed;
        assert.isFalse(tooltip.open);
    });
    it('should only close the innermost tooltip on escape', async () => {
        const container = document.createElement('div');
        // clang-format off
        Lit.render(html `
      <button aria-details="outer-tooltip-id">Outer Button</button>
      <devtools-tooltip
        id="outer-tooltip-id"
        variant="rich"
        .hover-delay=${0}
      >
        <button aria-details="inner-tooltip-id">Inner Button</button>
        <devtools-tooltip
          id="inner-tooltip-id"
          variant="rich"
          .hover-delay=${0}
        >
          <p>Inner Tooltip Content</p>
        </devtools-tooltip>
      </devtools-tooltip>
    `, container);
        // clang-format on
        renderElementIntoDOM(container, { allowMultipleChildren: true });
        const outerTooltip = container.querySelector('devtools-tooltip');
        assert.exists(outerTooltip);
        const outerButton = container.querySelector('button');
        const outerOpened = waitForToggle(outerTooltip, 'open');
        outerButton?.dispatchEvent(new MouseEvent('mouseenter'));
        await outerOpened;
        assert.isTrue(outerTooltip.open);
        const innerTooltip = outerTooltip.querySelector('devtools-tooltip');
        assert.exists(innerTooltip);
        const innerButton = outerTooltip.querySelector('button');
        const innerOpened = waitForToggle(innerTooltip, 'open');
        innerButton?.dispatchEvent(new MouseEvent('mouseenter'));
        await innerOpened;
        assert.isTrue(innerTooltip.open);
        const outerClosed = waitForToggle(outerTooltip, 'closed');
        const innerClosed = waitForToggle(innerTooltip, 'closed');
        document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
        await innerClosed;
        assert.isTrue(outerTooltip.open);
        assert.isFalse(innerTooltip.open);
        document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
        await outerClosed;
        assert.isFalse(outerTooltip.open);
        assert.isFalse(innerTooltip.open);
    });
    it('should be activated if focused', async () => {
        const container = renderTooltip();
        const tooltip = container.querySelector('devtools-tooltip');
        assert.exists(tooltip);
        const opened = waitForToggle(tooltip, 'open');
        const button = container.querySelector('button');
        button?.dispatchEvent(new FocusEvent('focus'));
        await opened;
        assert.isTrue(tooltip.open);
    });
    it('should not be activated if un-hovered', async () => {
        const container = renderTooltip();
        const tooltip = container.querySelector('devtools-tooltip');
        assert.exists(tooltip);
        const button = container.querySelector('button');
        const opened = waitForToggle(tooltip, 'open');
        button?.dispatchEvent(new MouseEvent('mouseenter'));
        await opened;
        assert.isTrue(tooltip.open);
        button?.dispatchEvent(new MouseEvent('mouseleave'));
        assert.isFalse(tooltip.open);
    });
    it('should not be activated if dragged', () => {
        const clock = sinon.useFakeTimers({ toFake: ['setTimeout'] });
        const container = renderTooltip();
        const button = container.querySelector('button');
        button?.dispatchEvent(new MouseEvent('mouseenter', { buttons: 1 }));
        clock.runAll();
        assert.isFalse(container.querySelector('devtools-tooltip')?.open);
        clock.restore();
    });
    it('should not be activated if un-focused', async () => {
        const container = renderTooltip();
        const tooltip = container.querySelector('devtools-tooltip');
        assert.exists(tooltip);
        const button = container.querySelector('button');
        const opened = waitForToggle(tooltip, 'open');
        button?.dispatchEvent(new FocusEvent('focus'));
        await opened;
        assert.isTrue(tooltip.open);
        button?.dispatchEvent(new FocusEvent('blur'));
        assert.isFalse(tooltip.open);
    });
    it('should not open on hover if `trigger` is set to `click`', () => {
        const clock = sinon.useFakeTimers({ toFake: ['setTimeout'] });
        const container = renderTooltip({ trigger: 'click' });
        const button = container.querySelector('button');
        button?.dispatchEvent(new MouseEvent('mouseenter'));
        clock.runAll();
        assert.isFalse(container.querySelector('devtools-tooltip')?.open);
        clock.restore();
    });
    it('should not open on focus if `trigger` is set to `click`', () => {
        const clock = sinon.useFakeTimers({ toFake: ['setTimeout'] });
        const container = renderTooltip({ trigger: 'click' });
        const button = container.querySelector('button');
        button?.dispatchEvent(new FocusEvent('focus'));
        clock.runAll();
        assert.isFalse(container.querySelector('devtools-tooltip')?.open);
        clock.restore();
    });
    it('should open with click if `trigger` is set to `click`', () => {
        const container = renderTooltip({ trigger: 'click' });
        const button = container.querySelector('button');
        button?.click();
        assert.isTrue(container.querySelector('devtools-tooltip')?.open);
    });
    it('should open on hover if `trigger` is set to `both`', () => {
        const clock = sinon.useFakeTimers({ toFake: ['setTimeout'] });
        const container = renderTooltip({ trigger: 'both' });
        const button = container.querySelector('button');
        button?.dispatchEvent(new MouseEvent('mouseenter'));
        clock.runAll();
        assert.isTrue(container.querySelector('devtools-tooltip')?.open);
        clock.restore();
    });
    it('should open on focus if `trigger` is set to `both`', () => {
        const clock = sinon.useFakeTimers({ toFake: ['setTimeout'] });
        const container = renderTooltip({ trigger: 'both' });
        const button = container.querySelector('button');
        button?.dispatchEvent(new FocusEvent('focus'));
        clock.runAll();
        assert.isTrue(container.querySelector('devtools-tooltip')?.open);
        clock.restore();
    });
    it('should open with click if `trigger` is set to `both`', () => {
        const container = renderTooltip({ trigger: 'both' });
        const button = container.querySelector('button');
        button?.click();
        assert.isTrue(container.querySelector('devtools-tooltip')?.open);
    });
    it('should open with hotkey if use-hotkey is set', () => {
        const container = renderTooltip({ useHotkey: true });
        const button = container.querySelector('button');
        button?.dispatchEvent(new KeyboardEvent('keydown', { altKey: true, key: 'ArrowDown' }));
        assert.isTrue(container.querySelector('devtools-tooltip')?.open);
    });
    it('should not open on focus if use-hotkey is set', () => {
        const container = renderTooltip({ useHotkey: true });
        const button = container.querySelector('button');
        button?.dispatchEvent(new FocusEvent('focus'));
        assert.isFalse(container.querySelector('devtools-tooltip')?.open);
    });
    const eventsNotToPropagate = ['click', 'mouseup'];
    eventsNotToPropagate.forEach(eventName => {
        it(`should stop propagation of ${eventName} events`, () => {
            const container = renderTooltip();
            const callback = sinon.spy();
            container.addEventListener(eventName, callback);
            const tooltip = container.querySelector('devtools-tooltip');
            tooltip?.dispatchEvent(new Event(eventName, { bubbles: true }));
            sinon.assert.notCalled(callback);
            container.removeEventListener(eventName, callback);
        });
    });
    it('should print a warning if rich tooltip is defined without aria-details', () => {
        const consoleSpy = sinon.stub(console, 'warn');
        renderTooltip({ variant: 'rich' });
        sinon.assert.calledOnce(consoleSpy);
    });
    it('should hide the tooltip if anchor is removed from DOM', async () => {
        const container = renderTooltip();
        const tooltip = container.querySelector('devtools-tooltip');
        assert.exists(tooltip);
        const opened = waitForToggle(tooltip, 'open');
        const button = container.querySelector('button');
        button?.dispatchEvent(new MouseEvent('mouseenter'));
        await opened;
        const closed = waitForToggle(tooltip, 'closed');
        button?.remove();
        await closed;
        assert.isFalse(tooltip.open);
    });
    it('should not hide the tooltip if focus moves from the anchor into deep DOM within the tooltip', async () => {
        const container = renderTooltip({ variant: 'rich', attribute: 'aria-details' });
        const anchor = container.querySelector('button');
        assert.exists(anchor);
        const tooltip = container.querySelector('devtools-tooltip');
        assert.exists(tooltip);
        // Make some nested DOM for this; this test exists because of a bug where
        // the tooltip only stayed open if the focused element was an immediate
        // child, so for this test we make a nested DOM structure and test on that.
        tooltip.innerHTML = '<div><span><p class="deep-nested">nested</p></span></div>';
        const opened = waitForToggle(tooltip, 'open');
        anchor.dispatchEvent(new FocusEvent('focus'));
        await opened;
        assert.isTrue(tooltip.open);
        const richContents = container.querySelector('devtools-tooltip')?.querySelector('p.deep-nested');
        assert.exists(richContents);
        anchor.dispatchEvent(new FocusEvent('blur', { relatedTarget: richContents }));
        assert.isTrue(tooltip.open); // tooltip should still be open
    });
    it('automatically sets and updates jslog', () => {
        const container = renderTooltip({ jslogContext: 'context' });
        const tooltip = container.querySelector('devtools-tooltip');
        assert.exists(tooltip);
        assert.strictEqual(tooltip.getAttribute('jslog'), 'Popover; context: context; parent: mapped');
        tooltip.setAttribute('jslogcontext', 'context2');
        assert.strictEqual(tooltip.getAttribute('jslog'), 'Popover; context: context2; parent: mapped');
        const anchor = container.createChild('button');
        anchor.setAttribute('aria-details', 'constructed-tooltip-id');
        const constructedTooltip = new Tooltips.Tooltip.Tooltip({ id: 'constructed-tooltip-id', jslogContext: 'context3', anchor });
        container.appendChild(constructedTooltip);
        assert.strictEqual(constructedTooltip.getAttribute('jslog'), 'Popover; context: context3; parent: mapped');
    });
    it('automatically opens a new tooltip with the same id on re-attach', async () => {
        const container = renderTooltip();
        const tooltip = container.querySelector('devtools-tooltip');
        assert.exists(tooltip);
        const opened = waitForToggle(tooltip, 'open');
        tooltip.showTooltip();
        await opened;
        assert.isTrue(tooltip.open);
        container.remove();
        const container2 = renderTooltip();
        const tooltip2 = container2.querySelector('devtools-tooltip');
        assert.exists(tooltip2);
        assert.isTrue(tooltip2.open);
        tooltip2.id = 'tooltip-id-2';
        container2.remove();
        const container3 = renderTooltip({ id: 'tooltip-id-2' });
        const tooltip3 = container3.querySelector('devtools-tooltip');
        assert.exists(tooltip3);
        assert.isTrue(tooltip3.open);
    });
    describe('assigns the correct position', () => {
        const inspectorViewRect = {
            top: 0,
            bottom: 290,
            height: 290,
            left: 0,
            right: 500,
            width: 500,
        };
        const anchorRect = {
            top: 100,
            bottom: 200,
            height: 100,
            left: 200,
            right: 400,
            width: 200,
        };
        it('for default postion bottom span right', () => {
            const currentPopoverRect = {
                height: 80,
                width: 160,
            };
            const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip({ inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions: [] });
            assert.strictEqual(proposedRect.top, 200);
            assert.strictEqual(proposedRect.left, 200);
        });
        it('for preferred postion bottom span left', () => {
            const currentPopoverRect = {
                height: 80,
                width: 160,
            };
            const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip({
                inspectorViewRect,
                anchorRect,
                currentPopoverRect,
                preferredPositions: [Tooltips.Tooltip.PositionOption.BOTTOM_SPAN_LEFT]
            });
            assert.strictEqual(proposedRect.top, 200);
            assert.strictEqual(proposedRect.left, 240);
        });
        it('uses 2nd option from default order if 1st is impossible', () => {
            const currentPopoverRect = {
                height: 80,
                width: 350,
            };
            const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip({ inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions: [] });
            assert.strictEqual(proposedRect.top, 200);
            assert.strictEqual(proposedRect.left, 50);
        });
        it('uses 3rd option from default order if first 2 are impossible', () => {
            const currentPopoverRect = {
                height: 95,
                width: 160,
            };
            const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip({ inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions: [] });
            assert.strictEqual(proposedRect.top, 5);
            assert.strictEqual(proposedRect.left, 200);
        });
        it('uses 4th option from default order if first 3 are impossible', () => {
            const currentPopoverRect = {
                height: 95,
                width: 350,
            };
            const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip({ inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions: [] });
            assert.strictEqual(proposedRect.top, 5);
            assert.strictEqual(proposedRect.left, 50);
        });
        it('uses 4th option from preferred order if first 3 are impossible', () => {
            const currentPopoverRect = {
                height: 95,
                width: 350,
            };
            const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip({
                inspectorViewRect,
                anchorRect,
                currentPopoverRect,
                preferredPositions: [Tooltips.Tooltip.PositionOption.BOTTOM_SPAN_LEFT, Tooltips.Tooltip.PositionOption.TOP_SPAN_LEFT]
            });
            assert.strictEqual(proposedRect.top, 5);
            assert.strictEqual(proposedRect.left, 50);
        });
        it('moves the rect into the viewport if all 4 options are impossible', () => {
            const currentPopoverRect = {
                height: 110,
                width: 440,
            };
            const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip({ inspectorViewRect, anchorRect, currentPopoverRect, preferredPositions: [] });
            assert.strictEqual(proposedRect.top, 0);
            assert.strictEqual(proposedRect.left, 60);
        });
        it('for anchors in a corner of the viewport', () => {
            const anchorBottomLeftCorner = {
                top: 190,
                bottom: 290,
                height: 100,
                left: 0,
                right: 100,
                width: 100,
            };
            const currentPopoverRect = {
                height: 100,
                width: 200,
            };
            const proposedRect = Tooltips.Tooltip.proposedRectForRichTooltip({ inspectorViewRect, anchorRect: anchorBottomLeftCorner, currentPopoverRect, preferredPositions: [] });
            assert.strictEqual(proposedRect.top, 90);
            assert.strictEqual(proposedRect.left, 0);
        });
        it('moves a simple tooltip into the viewport', () => {
            const currentPopoverRect = {
                height: 95,
                width: 410,
            };
            const proposedRect = Tooltips.Tooltip.proposedRectForSimpleTooltip({ inspectorViewRect, anchorRect, currentPopoverRect });
            assert.strictEqual(proposedRect.top, 5);
            assert.strictEqual(proposedRect.left, 90);
        });
    });
});
//# sourceMappingURL=Tooltip.test.js.map