blob: 495f8c704b5da8853c87c9a283a4420b5412c5e5 [file]
// 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 sinon from 'sinon';
import * as Platform from '../../core/platform/platform.js';
import {renderElementIntoDOM} from '../../testing/DOMHelpers.js';
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Elements from './elements.js';
const {urlString} = Platform.DevToolsPath;
describeWithEnvironment('ImagePreviewPopover', () => {
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
// UI.PopoverHelper.PopoverHelper uses setTimeout internally to manage popover
// show and hide timing. Using fake timers allows us to control these
// timeouts synchronously in the test without real-world delays or flakiness.
clock = sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
/**
* Periodically ticks the fake clock and flushes microtasks until the given
* condition is met. This is used to wait for asynchronous work that is
* triggered by timers (like PopoverHelper) or multiple await points.
*/
async function poll(condition: () => boolean): Promise<void> {
for (let i = 0; i < 20; i++) {
if (condition()) {
return;
}
clock.tick(1);
await Promise.resolve();
}
throw new Error('Condition not met');
}
it('shows an image preview when hovering over an element with an image URL', async () => {
const container = document.createElement('div');
renderElementIntoDOM(container);
const getLinkElement = (event: Event) => event.target as Element;
const getNodeFeatures = async () => undefined;
new Elements.ImagePreviewPopover.ImagePreviewPopover(container, getLinkElement, getNodeFeatures);
const element = document.createElement('span');
element.textContent = 'hover me';
element.boxInWindow = () => new AnchorBox(0, 0, 10, 10);
container.appendChild(element);
const imageUrl = urlString`http://example.com/image.png`;
Elements.ImagePreviewPopover.ImagePreviewPopover.setImageUrl(element, imageUrl);
const buildStub = sinon.stub(Components.ImagePreview.ImagePreview, 'build').resolves(document.createElement('div'));
const event = new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
clientX: 5,
clientY: 5,
});
element.dispatchEvent(event);
// Wait for the build stub to be called, which happens inside the async show() method
// triggered after PopoverHelper's internal timer.
await poll(() => buildStub.called);
sinon.assert.calledWith(buildStub, imageUrl, true);
});
it('does not show a preview when hovering over an element without an image URL', async () => {
const container = document.createElement('div');
renderElementIntoDOM(container);
const getLinkElement = (event: Event) => event.target as Element;
const getNodeFeatures = async () => undefined;
new Elements.ImagePreviewPopover.ImagePreviewPopover(container, getLinkElement, getNodeFeatures);
const element = document.createElement('span');
element.textContent = 'hover me';
element.boxInWindow = () => new AnchorBox(0, 0, 10, 10);
container.appendChild(element);
const buildStub = sinon.stub(Components.ImagePreview.ImagePreview, 'build').resolves(document.createElement('div'));
const event = new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
clientX: 5,
clientY: 5,
});
element.dispatchEvent(event);
// Tick the clock and flush microtasks to ensure any scheduled work has a chance to run.
clock.tick(1);
await Promise.resolve();
sinon.assert.notCalled(buildStub);
});
it('hides the popover when hide() is called', async () => {
const container = document.createElement('div');
renderElementIntoDOM(container);
const getLinkElement = (event: Event) => event.target as Element;
const getNodeFeatures = async () => undefined;
const imagePreviewPopover =
new Elements.ImagePreviewPopover.ImagePreviewPopover(container, getLinkElement, getNodeFeatures);
const element = document.createElement('span');
element.boxInWindow = () => new AnchorBox(0, 0, 10, 10);
container.appendChild(element);
const imageUrl = urlString`http://example.com/image.png`;
Elements.ImagePreviewPopover.ImagePreviewPopover.setImageUrl(element, imageUrl);
sinon.stub(Components.ImagePreview.ImagePreview, 'build').resolves(document.createElement('div'));
const glassPaneShowStub = sinon.stub(UI.GlassPane.GlassPane.prototype, 'show');
const glassPaneHideStub = sinon.stub(UI.GlassPane.GlassPane.prototype, 'hide');
const event = new MouseEvent('mousemove', {
bubbles: true,
clientX: 5,
clientY: 5,
});
element.dispatchEvent(event);
// Wait until the popover's show() method has been called. This ensures the
// async chain in PopoverHelper has finished and the hidePopoverCallback is set.
await poll(() => glassPaneShowStub.called);
imagePreviewPopover.hide();
sinon.assert.called(glassPaneHideStub);
});
});