blob: 890b709b7a539210f3f6b164aee2eee1bf75383c [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 {assert} from 'chai';
import type * as puppeteer from 'puppeteer-core';
import {
DRAWER_PANEL_SELECTOR,
MAIN_PANEL_SELECTOR,
tabExistsInDrawer,
tabExistsInMainPanel,
} from '../helpers/cross-tool-helper.js';
import type {DevToolsPage} from '../shared/frontend-helper.js';
const PANEL_PLUS_BUTTON_SELECTOR = 'devtools-menu-button[jslog*="context: plus-button-panel"]';
const DRAWER_PLUS_BUTTON_SELECTOR = 'devtools-menu-button[jslog*="context: plus-button-drawer"]';
const SOFT_MENU_SELECTOR = '.soft-context-menu';
const SOFT_MENU_ITEM_SELECTOR = '.soft-context-menu-item';
async function enablePlusButton(devToolsPage: DevToolsPage): Promise<void> {
await devToolsPage.setupMockHostConfigAndReload({devToolsPlusButton: {enabled: true}});
// E2e tests must opt in to the soft (in-page) context menu; without
// this the OS-native menu is used and `.soft-context-menu` never
// appears in the DOM. Must be called AFTER the reload so the static
// flag survives the new document load.
await devToolsPage.useSoftMenu();
}
async function openPlusButtonMenu(devToolsPage: DevToolsPage, root: puppeteer.ElementHandle<Element>,
selector: string): Promise<puppeteer.ElementHandle<Element>> {
// `MenuButton.#triggerContextMenu` uses a 50ms `setTimeout` before
// showing the menu (Windows double-click guard, crbug/339560549) so
// the menu does NOT appear synchronously after the click resolves;
// `waitFor` polls until it is in the DOM. Do NOT wrap this in a retry
// loop: a second click while the menu is open toggles it closed.
await devToolsPage.click(selector, {root});
return await devToolsPage.waitFor(SOFT_MENU_SELECTOR);
}
async function softMenuItemLabels(devToolsPage: DevToolsPage,
menu: puppeteer.ElementHandle<Element>): Promise<string[]> {
const items = await devToolsPage.$$(SOFT_MENU_ITEM_SELECTOR, menu);
return await Promise.all(items.map(item => item.evaluate(el => el.getAttribute('aria-label') ?? '')));
}
describe('Plus button', () => {
it('is installed in the main panel when the feature is enabled', async ({devToolsPage}) => {
await enablePlusButton(devToolsPage);
const mainPanel = await devToolsPage.waitFor(MAIN_PANEL_SELECTOR);
await devToolsPage.waitFor(PANEL_PLUS_BUTTON_SELECTOR, mainPanel);
});
it('is installed in the drawer when the feature is enabled', async ({devToolsPage}) => {
await enablePlusButton(devToolsPage);
await devToolsPage.evaluate(`
(async () => {
const UI = await import('./ui/legacy/legacy.js');
UI.InspectorView.InspectorView.instance().showDrawer({focus: false, hasTargetDrawer: false});
})()
`);
const drawer = await devToolsPage.waitFor(DRAWER_PANEL_SELECTOR);
await devToolsPage.waitFor(DRAWER_PLUS_BUTTON_SELECTOR, drawer);
});
it('uses distinct jslog contexts for the panel and drawer plus buttons', async ({devToolsPage}) => {
// Telemetry must be able to distinguish clicks on the panel plus
// button from clicks on the drawer plus button.
await enablePlusButton(devToolsPage);
await devToolsPage.evaluate(`
(async () => {
const UI = await import('./ui/legacy/legacy.js');
UI.InspectorView.InspectorView.instance().showDrawer({focus: false, hasTargetDrawer: false});
})()
`);
const mainPanel = await devToolsPage.waitFor(MAIN_PANEL_SELECTOR);
const drawer = await devToolsPage.waitFor(DRAWER_PANEL_SELECTOR);
await devToolsPage.waitFor(PANEL_PLUS_BUTTON_SELECTOR, mainPanel);
await devToolsPage.waitForNone(DRAWER_PLUS_BUTTON_SELECTOR, mainPanel);
await devToolsPage.waitFor(DRAWER_PLUS_BUTTON_SELECTOR, drawer);
await devToolsPage.waitForNone(PANEL_PLUS_BUTTON_SELECTOR, drawer);
});
it('is not installed when the feature is disabled', async ({devToolsPage}) => {
// No setupMockHostConfigAndReload — `devToolsPlusButton.enabled` is
// absent, so the gate in ViewManager skips installation.
const mainPanel = await devToolsPage.waitFor(MAIN_PANEL_SELECTOR);
await devToolsPage.waitForNone(PANEL_PLUS_BUTTON_SELECTOR, mainPanel);
});
it('opens a menu listing tools that are not currently visible', async ({devToolsPage}) => {
await enablePlusButton(devToolsPage);
const mainPanel = await devToolsPage.waitFor(MAIN_PANEL_SELECTOR);
const menu = await openPlusButtonMenu(devToolsPage, mainPanel, PANEL_PLUS_BUTTON_SELECTOR);
const labels = await softMenuItemLabels(devToolsPage, menu);
assert.isNotEmpty(labels, 'plus-button menu should list at least one addable tool');
// The menu should NOT include tabs that are already pinned in the main
// tab strip (e.g. Elements is always visible by default).
assert.notInclude(labels, 'Elements');
});
it('adds a tool to the main panel when an entry is selected', async ({devToolsPage}) => {
await enablePlusButton(devToolsPage);
const mainPanel = await devToolsPage.waitFor(MAIN_PANEL_SELECTOR);
const menu = await openPlusButtonMenu(devToolsPage, mainPanel, PANEL_PLUS_BUTTON_SELECTOR);
const labels = await softMenuItemLabels(devToolsPage, menu);
// `Animations` is a closeable drawer-resident view (see
// `animation-meta.ts`); the panel plus button must advertise it as
// a cross-location candidate so the user can pull it into the main
// tab strip.
assert.include(labels, 'Animations');
await devToolsPage.click(`${SOFT_MENU_ITEM_SELECTOR}[aria-label="Animations"]`, {root: menu});
await tabExistsInMainPanel(devToolsPage, '#tab-animations');
});
it('drawer plus button moves a panel tool into the drawer', async ({devToolsPage}) => {
await enablePlusButton(devToolsPage);
await devToolsPage.evaluate(`
(async () => {
const UI = await import('./ui/legacy/legacy.js');
UI.InspectorView.InspectorView.instance().showDrawer({focus: false, hasTargetDrawer: false});
})()
`);
const drawer = await devToolsPage.waitFor(DRAWER_PANEL_SELECTOR);
const menu = await openPlusButtonMenu(devToolsPage, drawer, DRAWER_PLUS_BUTTON_SELECTOR);
const labels = await softMenuItemLabels(devToolsPage, menu);
// `Recorder` is a closeable panel-resident view (see
// `recorder-meta.ts`); the drawer plus button must advertise it as
// a cross-location candidate so the user can move it into the drawer.
assert.include(labels, 'Recorder');
await devToolsPage.click(`${SOFT_MENU_ITEM_SELECTOR}[aria-label="Recorder"]`, {root: menu});
await tabExistsInDrawer(devToolsPage, '#tab-chrome-recorder');
});
});