Skip to content

Commit 58d2bc9

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
Add unit tests for ImagePreviewPopover
This CL replaces the elements-img-tooltip layout test which can be deleted from Chromium in another CL. Bug: 490314415 Change-Id: I0e60a39fa4aa16ea42297f4be175681b51ea6c0f Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7711976 Reviewed-by: Kim-Anh Tran <kimanh@chromium.org> Commit-Queue: Kim-Anh Tran <kimanh@chromium.org> Auto-Submit: Jack Franklin <jacktfranklin@chromium.org>
1 parent 760967c commit 58d2bc9

3 files changed

Lines changed: 146 additions & 0 deletions

File tree

front_end/panels/elements/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ devtools_ui_module("unittests") {
188188
"ElementsTreeElement.test.ts",
189189
"ElementsTreeOutline.test.ts",
190190
"EventListenersWidget.test.ts",
191+
"ImagePreviewPopover.test.ts",
191192
"InspectElementModeController.test.ts",
192193
"LayoutPane.test.ts",
193194
"PlatformFontsWidget.test.ts",
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2026 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as Platform from '../../core/platform/platform.js';
6+
import {renderElementIntoDOM} from '../../testing/DOMHelpers.js';
7+
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
8+
import * as Components from '../../ui/legacy/components/utils/utils.js';
9+
import * as UI from '../../ui/legacy/legacy.js';
10+
11+
import * as Elements from './elements.js';
12+
13+
const {urlString} = Platform.DevToolsPath;
14+
15+
describeWithEnvironment('ImagePreviewPopover', () => {
16+
let clock: sinon.SinonFakeTimers;
17+
18+
beforeEach(() => {
19+
// UI.PopoverHelper.PopoverHelper uses setTimeout internally to manage popover
20+
// show and hide timing. Using fake timers allows us to control these
21+
// timeouts synchronously in the test without real-world delays or flakiness.
22+
clock = sinon.useFakeTimers();
23+
});
24+
25+
afterEach(() => {
26+
clock.restore();
27+
});
28+
29+
/**
30+
* Periodically ticks the fake clock and flushes microtasks until the given
31+
* condition is met. This is used to wait for asynchronous work that is
32+
* triggered by timers (like PopoverHelper) or multiple await points.
33+
*/
34+
async function poll(condition: () => boolean): Promise<void> {
35+
for (let i = 0; i < 20; i++) {
36+
if (condition()) {
37+
return;
38+
}
39+
clock.tick(1);
40+
await Promise.resolve();
41+
}
42+
throw new Error('Condition not met');
43+
}
44+
45+
it('shows an image preview when hovering over an element with an image URL', async () => {
46+
const container = document.createElement('div');
47+
renderElementIntoDOM(container);
48+
49+
const getLinkElement = (event: Event) => event.target as Element;
50+
const getNodeFeatures = async () => undefined;
51+
new Elements.ImagePreviewPopover.ImagePreviewPopover(container, getLinkElement, getNodeFeatures);
52+
53+
const element = document.createElement('span');
54+
element.textContent = 'hover me';
55+
element.boxInWindow = () => new AnchorBox(0, 0, 10, 10);
56+
container.appendChild(element);
57+
58+
const imageUrl = urlString`http://example.com/image.png`;
59+
Elements.ImagePreviewPopover.ImagePreviewPopover.setImageUrl(element, imageUrl);
60+
61+
const buildStub = sinon.stub(Components.ImagePreview.ImagePreview, 'build').resolves(document.createElement('div'));
62+
63+
const event = new MouseEvent('mousemove', {
64+
bubbles: true,
65+
cancelable: true,
66+
clientX: 5,
67+
clientY: 5,
68+
});
69+
element.dispatchEvent(event);
70+
71+
// Wait for the build stub to be called, which happens inside the async show() method
72+
// triggered after PopoverHelper's internal timer.
73+
await poll(() => buildStub.called);
74+
75+
sinon.assert.calledWith(buildStub, imageUrl, true);
76+
});
77+
78+
it('does not show a preview when hovering over an element without an image URL', async () => {
79+
const container = document.createElement('div');
80+
renderElementIntoDOM(container);
81+
82+
const getLinkElement = (event: Event) => event.target as Element;
83+
const getNodeFeatures = async () => undefined;
84+
new Elements.ImagePreviewPopover.ImagePreviewPopover(container, getLinkElement, getNodeFeatures);
85+
86+
const element = document.createElement('span');
87+
element.textContent = 'hover me';
88+
element.boxInWindow = () => new AnchorBox(0, 0, 10, 10);
89+
container.appendChild(element);
90+
91+
const buildStub = sinon.stub(Components.ImagePreview.ImagePreview, 'build').resolves(document.createElement('div'));
92+
93+
const event = new MouseEvent('mousemove', {
94+
bubbles: true,
95+
cancelable: true,
96+
clientX: 5,
97+
clientY: 5,
98+
});
99+
element.dispatchEvent(event);
100+
101+
// Tick the clock and flush microtasks to ensure any scheduled work has a chance to run.
102+
clock.tick(1);
103+
await Promise.resolve();
104+
105+
sinon.assert.notCalled(buildStub);
106+
});
107+
108+
it('hides the popover when hide() is called', async () => {
109+
const container = document.createElement('div');
110+
renderElementIntoDOM(container);
111+
112+
const getLinkElement = (event: Event) => event.target as Element;
113+
const getNodeFeatures = async () => undefined;
114+
const imagePreviewPopover =
115+
new Elements.ImagePreviewPopover.ImagePreviewPopover(container, getLinkElement, getNodeFeatures);
116+
117+
const element = document.createElement('span');
118+
element.boxInWindow = () => new AnchorBox(0, 0, 10, 10);
119+
container.appendChild(element);
120+
121+
const imageUrl = urlString`http://example.com/image.png`;
122+
Elements.ImagePreviewPopover.ImagePreviewPopover.setImageUrl(element, imageUrl);
123+
124+
sinon.stub(Components.ImagePreview.ImagePreview, 'build').resolves(document.createElement('div'));
125+
const glassPaneShowStub = sinon.stub(UI.GlassPane.GlassPane.prototype, 'show');
126+
const glassPaneHideStub = sinon.stub(UI.GlassPane.GlassPane.prototype, 'hide');
127+
128+
const event = new MouseEvent('mousemove', {
129+
bubbles: true,
130+
clientX: 5,
131+
clientY: 5,
132+
});
133+
element.dispatchEvent(event);
134+
135+
// Wait until the popover's show() method has been called. This ensures the
136+
// async chain in PopoverHelper has finished and the hidePopoverCallback is set.
137+
await poll(() => glassPaneShowStub.called);
138+
139+
imagePreviewPopover.hide();
140+
sinon.assert.called(glassPaneHideStub);
141+
});
142+
});

front_end/panels/elements/elements.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import './ElementsTreeElement.js';
1010
import './AdoptedStyleSheetTreeElement.js';
1111
import './TopLayerContainer.js';
1212
import './ElementsTreeOutline.js';
13+
import './ImagePreviewPopover.js';
1314
import './EventListenersWidget.js';
1415
import './MarkerDecorator.js';
1516
import './MetricsSidebarPane.js';
@@ -45,6 +46,7 @@ import * as ElementsTreeElement from './ElementsTreeElement.js';
4546
import * as ElementsTreeOutline from './ElementsTreeOutline.js';
4647
import * as ElementsTreeOutlineRenderer from './ElementsTreeOutlineRenderer.js';
4748
import * as EventListenersWidget from './EventListenersWidget.js';
49+
import * as ImagePreviewPopover from './ImagePreviewPopover.js';
4850
import * as InspectElementModeController from './InspectElementModeController.js';
4951
import * as LayersWidget from './LayersWidget.js';
5052
import * as LayoutPane from './LayoutPane.js';
@@ -82,6 +84,7 @@ export {
8284
ElementsTreeOutline,
8385
ElementsTreeOutlineRenderer,
8486
EventListenersWidget,
87+
ImagePreviewPopover,
8588
InspectElementModeController,
8689
LayersWidget,
8790
LayoutPane,

0 commit comments

Comments
 (0)