blob: ad028b178396a97e0a1becef45949fd7279b9c50 [file] [log] [blame]
// 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 * as Common from '../../core/common/common.js';
import {
renderElementIntoDOM,
} from '../../testing/DOMHelpers.js';
import {describeWithEnvironment, updateHostConfig} from '../../testing/EnvironmentHelpers.js';
import {createViewFunctionStub} from '../../testing/ViewFunctionHelpers.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Main from './main.js';
const CLICK_COUNT_LIMIT = 2;
const DELAY_BEFORE_PROMOTION_COLLAPSE_IN_MS = 5000;
const {GlobalAiButton} = Main.GlobalAiButton;
describeWithEnvironment('GlobalAiButton', () => {
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
Common.Settings.Settings.instance().settingForTest('global-ai-button-click-count').set(0);
});
afterEach(() => {
clock?.restore();
});
async function createWidget() {
const view = createViewFunctionStub(GlobalAiButton);
const widget = new GlobalAiButton(undefined, view);
widget.markAsRoot();
renderElementIntoDOM(widget);
await view.nextInput;
return {view, widget};
}
it('renders in its DEFAULT state initially', async () => {
const {view} = await createWidget();
assert.strictEqual(view.input.state, Main.GlobalAiButton.GlobalAiButtonState.DEFAULT);
});
describe('onClick', () => {
let inspectorView: UI.InspectorView.InspectorView;
let isUserExplicitlyUpdatedDrawerOrientationStub: sinon.SinonStub;
let toggleDrawerOrientationSpy: sinon.SinonSpy;
let showViewStub: sinon.SinonStub;
let showDrawerStub: sinon.SinonStub;
beforeEach(() => {
inspectorView = UI.InspectorView.InspectorView.instance();
isUserExplicitlyUpdatedDrawerOrientationStub =
sinon.stub(inspectorView, 'isUserExplicitlyUpdatedDrawerOrientation');
toggleDrawerOrientationSpy = sinon.spy(inspectorView, 'toggleDrawerOrientation');
showViewStub = sinon.stub(UI.ViewManager.ViewManager.instance(), 'showViewInLocation');
showDrawerStub = sinon.stub(inspectorView, 'showDrawer');
});
it('shows freestyler in drawer and increases click count', async () => {
const {view} = await createWidget();
const setting = Common.Settings.Settings.instance().settingForTest('global-ai-button-click-count');
setting.set(0);
view.input.onClick();
sinon.assert.calledOnceWithExactly(showViewStub, 'freestyler', 'drawer-view');
assert.strictEqual(setting.get(), 1);
});
describe('with vertical drawer experiment', () => {
beforeEach(() => {
updateHostConfig({
devToolsFlexibleLayout: {
verticalDrawerEnabled: true,
},
});
});
it('toggles drawer if experiment is on and user has no preference', async () => {
const {view} = await createWidget();
isUserExplicitlyUpdatedDrawerOrientationStub.returns(false);
view.input.onClick();
sinon.assert.calledOnceWithExactly(showDrawerStub, {focus: true, hasTargetDrawer: false});
sinon.assert.calledOnceWithExactly(toggleDrawerOrientationSpy, {force: 'vertical'});
});
it('does not toggle drawer if user has preference', async () => {
const {view} = await createWidget();
isUserExplicitlyUpdatedDrawerOrientationStub.returns(true);
view.input.onClick();
sinon.assert.notCalled(showDrawerStub);
sinon.assert.notCalled(toggleDrawerOrientationSpy);
});
});
it('does not toggle drawer if experiment is off', async () => {
updateHostConfig({
devToolsFlexibleLayout: {
verticalDrawerEnabled: false,
},
});
const {view} = await createWidget();
isUserExplicitlyUpdatedDrawerOrientationStub.returns(false);
view.input.onClick();
sinon.assert.notCalled(showDrawerStub);
sinon.assert.notCalled(toggleDrawerOrientationSpy);
});
});
describe('promotion lifecycle', () => {
it('transitions to PROMOTION state when promotion should be triggered', async () => {
updateHostConfig({
devToolsGlobalAiButton: {
promotionEnabled: true,
},
});
clock = sinon.useFakeTimers({now: new Date('2025-01-01'), toFake: ['setTimeout', 'Date']});
Common.Settings.Settings.instance().settingForTest('global-ai-button-click-count').set(CLICK_COUNT_LIMIT - 1);
const {view} = await createWidget();
assert.strictEqual(view.input.state, Main.GlobalAiButton.GlobalAiButtonState.PROMOTION);
});
it('reverts from PROMOTION to DEFAULT state after a delay', async () => {
updateHostConfig({
devToolsGlobalAiButton: {
promotionEnabled: true,
},
});
clock = sinon.useFakeTimers({now: new Date('2025-01-01'), toFake: ['setTimeout', 'Date']});
Common.Settings.Settings.instance().settingForTest('global-ai-button-click-count').set(CLICK_COUNT_LIMIT - 1);
const {view} = await createWidget();
clock.tick(DELAY_BEFORE_PROMOTION_COLLAPSE_IN_MS);
const finalInput = await view.nextInput;
assert.strictEqual(finalInput.state, Main.GlobalAiButton.GlobalAiButtonState.DEFAULT);
});
it('does not trigger promotion if the feature flag is off', async () => {
updateHostConfig({
devToolsGlobalAiButton: {
promotionEnabled: false,
},
});
clock = sinon.useFakeTimers({now: new Date('2025-01-01'), toFake: ['setTimeout', 'Date']});
Common.Settings.Settings.instance().settingForTest('global-ai-button-click-count').set(CLICK_COUNT_LIMIT - 1);
const {view} = await createWidget();
assert.strictEqual(view.input.state, Main.GlobalAiButton.GlobalAiButtonState.DEFAULT);
});
it('does not trigger promotion if the date has expired', async () => {
updateHostConfig({
devToolsGlobalAiButton: {
promotionEnabled: true,
},
});
clock = sinon.useFakeTimers({
now: new Date('2026-10-01'), // After 2026-09-30
toFake: ['setTimeout', 'Date']
});
Common.Settings.Settings.instance().settingForTest('global-ai-button-click-count').set(CLICK_COUNT_LIMIT - 1);
const {view} = await createWidget();
assert.strictEqual(view.input.state, Main.GlobalAiButton.GlobalAiButtonState.DEFAULT);
});
it('does not trigger promotion if the click count is more than or equal to 2', async () => {
updateHostConfig({
devToolsGlobalAiButton: {
promotionEnabled: true,
},
});
clock = sinon.useFakeTimers({now: new Date('2025-01-01'), toFake: ['setTimeout', 'Date']});
Common.Settings.Settings.instance().settingForTest('global-ai-button-click-count').set(CLICK_COUNT_LIMIT);
const {view} = await createWidget();
assert.strictEqual(view.input.state, Main.GlobalAiButton.GlobalAiButtonState.DEFAULT);
});
});
describe('promotion lifecycle with toolbar hover', () => {
let headerElement: HTMLElement;
beforeEach(() => {
headerElement = document.createElement('div');
sinon.stub(UI.InspectorView.InspectorView.instance().tabbedPane, 'headerElement').returns(headerElement);
});
it('does not revert from PROMOTION to DEFAULT state while the toolbar is hovered', async () => {
updateHostConfig({
devToolsGlobalAiButton: {
promotionEnabled: true,
},
});
clock = sinon.useFakeTimers({now: new Date('2025-01-01'), toFake: ['setTimeout', 'Date']});
Common.Settings.Settings.instance().settingForTest('global-ai-button-click-count').set(CLICK_COUNT_LIMIT - 1);
const {view} = await createWidget();
assert.strictEqual(view.input.state, Main.GlobalAiButton.GlobalAiButtonState.PROMOTION);
// Simulate hovering over the toolbar.
headerElement.dispatchEvent(new MouseEvent('mouseenter'));
clock.tick(DELAY_BEFORE_PROMOTION_COLLAPSE_IN_MS);
// The button should still be in the promotion state.
assert.strictEqual(view.input.state, Main.GlobalAiButton.GlobalAiButtonState.PROMOTION);
// Simulate hovering out of the toolbar.
headerElement.dispatchEvent(new MouseEvent('mouseleave'));
clock.tick(DELAY_BEFORE_PROMOTION_COLLAPSE_IN_MS);
// The button should now be in the default state.
const finalInput = await view.nextInput;
assert.strictEqual(finalInput.state, Main.GlobalAiButton.GlobalAiButtonState.DEFAULT);
});
});
});