Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions e2e/editor-sidepanel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,34 @@ test('the side panel opens with the inspector on', async ({
.toMatch(/h1$/);
});

test('a side panel that shows the editor stays the chosen position', async ({
context,
extension,
openPopup,
}) => {
await dockToSidePanel(extension);

const page = await context.newPage();
await page.goto(`${baseUrl}/`);
await openSidePanel(page, context, extension, openPopup);

// Past the background's wait for the panel to load.
await page.waitForTimeout(3000);
expect(
await extension.evaluate(async () => {
const items = await chrome.storage.local.get([
'options',
'sidePanelIgnoresTabs',
]);
return [
items['sidePanelIgnoresTabs'],
items['options'].layout.dockLocation,
];
})
).toEqual([undefined, 'sidepanel']);
expect(await isSidePanelOpen(extension)).toBe(true);
});

test('moving from the side panel to a window closes the panel', async ({
context,
extension,
Expand Down
14 changes: 6 additions & 8 deletions src/apps/background/contextmenu.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { openEditorSidePanel } from '@stylebot/utils';

import { defaultOptions } from '@stylebot/settings';
import type { StylebotOptions } from '@stylebot/types';

import { ContextMenu, handleContextMenuClick } from './contextmenu';
import { getAll as getAllOptions } from './options';
import { openEditorSidePanelWithFallback } from './side-panel-check';

jest.mock('@stylebot/utils', () => ({
...jest.requireActual('@stylebot/utils'),
openEditorSidePanel: jest.fn(() => Promise.resolve()),
}));
jest.mock('./messages', () => ({ OpenOptionsPage: jest.fn() }));
jest.mock('./options', () => ({ getAll: jest.fn() }));
jest.mock('./side-panel-check', () => ({
openEditorSidePanelWithFallback: jest.fn(() => Promise.resolve()),
}));

const mockOptions = (overrides: Partial<StylebotOptions>) =>
(getAllOptions as jest.Mock).mockResolvedValue({
Expand Down Expand Up @@ -90,7 +88,7 @@ describe('handleContextMenuClick', () => {
tab
);

expect(openEditorSidePanel).toBeCalledWith(7);
expect(openEditorSidePanelWithFallback).toBeCalledWith(7);
expect(chrome.tabs.sendMessage).toBeCalledWith(7, {
name: 'OpenStylebotFromContextMenu',
sidePanel: true,
Expand All @@ -103,7 +101,7 @@ describe('handleContextMenuClick', () => {
tab
);

expect(openEditorSidePanel).not.toBeCalled();
expect(openEditorSidePanelWithFallback).not.toBeCalled();
expect(chrome.tabs.sendMessage).toBeCalledWith(7, {
name: 'OpenStylebotFromContextMenu',
sidePanel: false,
Expand Down
9 changes: 3 additions & 6 deletions src/apps/background/contextmenu.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { t } from '@stylebot/i18n';
import type { OpenStylebotFromContextMenu } from '@stylebot/types';
import {
getPageSupport,
supportsEditorSidePanel,
openEditorSidePanel,
} from '@stylebot/utils';
import { getPageSupport, supportsEditorSidePanel } from '@stylebot/utils';

import type { StylebotDockLocation } from '@stylebot/types';

import { OpenOptionsPage } from './messages';
import { getAll as getAllOptions } from './options';
import { openEditorSidePanelWithFallback } from './side-panel-check';

const CONTEXT_MENU_ID = 'stylebot-contextmenu';

Expand Down Expand Up @@ -147,7 +144,7 @@ export const handleContextMenuClick = (
info.menuItemId === STYLE_ELEMENT_IN_SIDE_PANEL_MENU_ITEM_ID;

if (sidePanel) {
openEditorSidePanel(tab.id).catch(() => undefined);
openEditorSidePanelWithFallback(tab.id).catch(() => undefined);
}

const message: OpenStylebotFromContextMenu = {
Expand Down
4 changes: 4 additions & 0 deletions src/apps/background/global-commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
jest.mock('./editor-window', () => ({ close: jest.fn() }));
jest.mock('./side-panel-check', () => ({
canUseEditorSidePanel: () => !!chrome.sidePanel,
onEditorSidePanelOpened: jest.fn(),
}));
jest.mock('./styles', () => ({
toggleForPage: jest.fn(async () => undefined),
applyStylesToAllTabs: jest.fn(async () => undefined),
Expand Down
11 changes: 8 additions & 3 deletions src/apps/background/global-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import {
closeEditorSidePanel,
isEditorSidePanelOpen,
isWebPageUrl,
supportsEditorSidePanel,
} from '@stylebot/utils';

import * as editorWindow from './editor-window';
import { applyStylesToAllTabs, toggleForPage } from './styles';
import {
canUseEditorSidePanel,
onEditorSidePanelOpened,
} from './side-panel-check';

const isCommandName = (command: string): command is StylebotCommandName =>
COMMAND_NAMES.includes(command as StylebotCommandName);
Expand Down Expand Up @@ -61,7 +64,9 @@ const toggleEditorWithSidePanel = (tabId: number): void => {
Promise.all([wasOpen, opened]).then(([open, didOpen]) => {
if (open) {
closeEditorSidePanel(tabId);
} else if (!didOpen) {
} else if (didOpen) {
onEditorSidePanelOpened(tabId);
} else {
runInPage(tabId, 'stylebot');
}
});
Expand Down Expand Up @@ -96,7 +101,7 @@ export const handleCommand = (
if (command === 'stylebot') {
if (target.fromEditorWindow) {
editorWindow.close(target.tabId);
} else if (supportsEditorSidePanel()) {
} else if (canUseEditorSidePanel()) {
toggleEditorWithSidePanel(target.tabId);
} else {
runInPage(target.tabId, command);
Expand Down
8 changes: 8 additions & 0 deletions src/apps/background/listeners.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ContextMenu, handleContextMenuClick } from './contextmenu';
import { handleCommand } from './global-commands';
import { configureSidePanelTabs, initSidePanelTabs } from './side-panel-tabs';
import {
onEditorSidePanelOpened,
initSidePanelCheck,
} from './side-panel-check';
import * as editorWindow from './editor-window';
import { restoreOpenTabs } from './restore-tabs';

Expand Down Expand Up @@ -120,6 +124,7 @@ export const initListeners = (): void => {

chrome.commands.onCommand.addListener(handleCommand);
initSidePanelTabs();
initSidePanelCheck();

// Scheduled syncs run without a user in front of them, so they never open an
// auth window; a run that needs one leaves a flag for the UI instead.
Expand Down Expand Up @@ -348,6 +353,9 @@ export const initListeners = (): void => {
case 'CloseEditorSidePanel':
CloseEditorSidePanel(message, sender);
break;
case 'EditorSidePanelOpened':
onEditorSidePanelOpened(message.tabId);
break;

case 'ChatGetStatus':
ChatGetStatus(sendResponse);
Expand Down
9 changes: 6 additions & 3 deletions src/apps/background/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,12 @@ import {
} from './color-history';

import * as editorWindow from './editor-window';
import {
canUseEditorSidePanel,
openEditorSidePanelWithFallback,
} from './side-panel-check';
import {
supportsEditorSidePanel,
openEditorSidePanel,
closeEditorSidePanel,
isEditorSidePanelOpen,
} from '@stylebot/utils';
Expand Down Expand Up @@ -569,12 +572,12 @@ export const OpenEditorSidePanel = (
sendResponse: (response: OpenEditorSidePanelResponse) => void
): void => {
const tabId = message.tabId ?? sender.tab?.id;
if (tabId === undefined || !supportsEditorSidePanel()) {
if (tabId === undefined || !canUseEditorSidePanel()) {
sendResponse(false);
return;
}

openEditorSidePanel(tabId).then(
openEditorSidePanelWithFallback(tabId).then(
() => sendResponse(true),
() => sendResponse(false)
);
Expand Down
9 changes: 9 additions & 0 deletions src/apps/background/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ describe('getAll', () => {
expect((await getAll()).layout.dockLocation).toBe('sidepanel');
});

it('docks right where the browser ignores per-tab side panels', async () => {
store = { sidePanelIgnoresTabs: true };
(chrome as unknown as { sidePanel: object }).sidePanel = {
open: jest.fn(),
};

expect((await getAll()).layout.dockLocation).toBe('right');
});

it('docks right by default where the browser has no side panel', async () => {
store = {};

Expand Down
7 changes: 5 additions & 2 deletions src/apps/background/options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { StylebotOptions } from '@stylebot/types';
import { defaultOptions } from '@stylebot/settings';
import { supportsEditorSidePanel } from '@stylebot/utils';
import {
isSidePanelIgnoringTabs,
supportsEditorSidePanel,
} from '@stylebot/utils';

/**
* Layers stored options over the defaults so keys added in later releases
Expand All @@ -13,7 +16,7 @@ export const getAll = async (): Promise<StylebotOptions> => {
// Without a side panel the editor docks in the page, where it used to.
if (
options.layout.dockLocation === 'sidepanel' &&
!supportsEditorSidePanel()
(!supportsEditorSidePanel() || (await isSidePanelIgnoringTabs()))
) {
return { ...options, layout: { ...options.layout, dockLocation: 'right' } };
}
Expand Down
99 changes: 99 additions & 0 deletions src/apps/background/side-panel-check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
isEditorSidePanelOpen,
isSidePanelIgnoringTabs,
} from '@stylebot/utils';

import { get as getOption, set as setOption } from './options';
import { onEditorSidePanelOpened } from './side-panel-check';

jest.mock('@stylebot/utils', () => ({
...jest.requireActual('@stylebot/utils'),
isEditorSidePanelOpen: jest.fn(),
isSidePanelIgnoringTabs: jest.fn(),
supportsEditorSidePanel: () => true,
}));
jest.mock('./options', () => ({
get: jest.fn(),
set: jest.fn(() => Promise.resolve()),
}));

const layout = {
width: 360,
adjustPageLayout: false,
dockLocation: 'sidepanel',
};

const openStylebot = () =>
expect(chrome.tabs.sendMessage).toBeCalledWith(
7,
{ name: 'OpenStylebot' },
expect.any(Function)
);

const check = async () => {
const done = onEditorSidePanelOpened(7);
await jest.advanceTimersByTimeAsync(3000);
await done;
};

beforeEach(() => {
jest.useFakeTimers();
jest.clearAllMocks();
(getOption as jest.Mock).mockResolvedValue(layout);
(isEditorSidePanelOpen as jest.Mock).mockResolvedValue(false);
(isSidePanelIgnoringTabs as jest.Mock).mockResolvedValue(false);
global.chrome = {
tabs: { sendMessage: jest.fn() },
runtime: {},
} as unknown as typeof chrome;
});

afterEach(() => jest.useRealTimers());

describe('onEditorSidePanelOpened', () => {
it('leaves the side panel alone once it shows the editor', async () => {
(isEditorSidePanelOpen as jest.Mock)
.mockResolvedValueOnce(false)
.mockResolvedValue(true);

await check();

expect(setOption).not.toBeCalled();
expect(chrome.tabs.sendMessage).not.toBeCalled();
});

it('leaves the side panel alone when it reports nothing in time', async () => {
await check();

expect(setOption).not.toBeCalled();
expect(chrome.tabs.sendMessage).not.toBeCalled();
});

it('docks the editor in the page once the panel reports it is hosted as a tab', async () => {
(isSidePanelIgnoringTabs as jest.Mock)
.mockResolvedValueOnce(false)
.mockResolvedValue(true);

await check();

expect(setOption).toBeCalledWith('layout', {
...layout,
dockLocation: 'right',
});
openStylebot();
});

it('opens the editor in the page at once when the browser is already known', async () => {
(isSidePanelIgnoringTabs as jest.Mock).mockResolvedValue(true);
(getOption as jest.Mock).mockResolvedValue({
...layout,
dockLocation: 'right',
});

await onEditorSidePanelOpened(7);

expect(isEditorSidePanelOpen).not.toBeCalled();
expect(setOption).not.toBeCalled();
openStylebot();
});
});
Loading
Loading