diff --git a/packages/extension/manifest.json b/packages/extension/manifest.json index 31c466d..cc3d868 100644 --- a/packages/extension/manifest.json +++ b/packages/extension/manifest.json @@ -6,7 +6,8 @@ "permissions": [ "debugger", "activeTab", - "tabs" + "tabs", + "tabGroups" ], "host_permissions": [ "" diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index d89b27b..aa07cae 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -35,6 +35,7 @@ type PageMessage = { class TabShareExtension { private _activeConnection: RelayConnection | undefined; private _connectedTabId: number | null = null; + private _groupId: number | null = null; private _pendingTabSelection = new Map(); constructor() { @@ -96,6 +97,7 @@ class TabShareExtension { // TODO: show error in the selector tab? }; this._pendingTabSelection.set(selectorTabId, { connection }); + await this._addTabToGroup(selectorTabId); debugLog(`Connected to MCP relay`); } catch (error: any) { const message = `Failed to connect to MCP relay: ${error.message}`; @@ -124,10 +126,12 @@ class TabShareExtension { debugLog('MCP connection closed'); this._activeConnection = undefined; void this._setConnectedTabId(null); + chrome.tabs.ungroup([tabId]).catch(() => {}); }; await Promise.all([ this._setConnectedTabId(tabId), + this._addTabToGroup(tabId), chrome.tabs.update(tabId, { active: true }), chrome.windows.update(windowId, { focused: true }), ]); @@ -204,11 +208,31 @@ class TabShareExtension { return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme))); } + private async _addTabToGroup(tabId: number): Promise { + try { + if (this._groupId !== null) { + try { + await chrome.tabs.group({ groupId: this._groupId, tabIds: [tabId] }); + await chrome.tabGroups.update(this._groupId, { color: 'green', title: 'Playwright' }); + return; + } catch { + this._groupId = null; + } + } + this._groupId = await chrome.tabs.group({ tabIds: [tabId] }); + await chrome.tabGroups.update(this._groupId, { color: 'green', title: 'Playwright' }); + } catch (error: any) { + debugLog('Error adding tab to group:', error); + } + } + private async _onActionClicked(): Promise { - await chrome.tabs.create({ + const tab = await chrome.tabs.create({ url: chrome.runtime.getURL('status.html'), active: true }); + if (tab.id) + await this._addTabToGroup(tab.id); } private async _disconnect(): Promise { diff --git a/packages/extension/tests/extension.spec.ts b/packages/extension/tests/extension.spec.ts index 6f052ad..ce3c949 100644 --- a/packages/extension/tests/extension.spec.ts +++ b/packages/extension/tests/extension.spec.ts @@ -390,6 +390,99 @@ test(`bypass connection dialog with token`, async ({ browserWithExtension, start }); }); +test.describe('tab grouping', () => { + test('connect page is added to green Playwright group on relay connect', async ({ browserWithExtension, startClient, server }) => { + const browserContext = await browserWithExtension.launch(); + const client = await startWithExtensionFlag(browserWithExtension, startClient); + + const connectPagePromise = browserContext.waitForEvent('page', page => + page.url().startsWith(`chrome-extension://${extensionId}/connect.html`) + ); + + const navigatePromise = client.callTool({ name: 'browser_navigate', arguments: { url: server.HELLO_WORLD } }); + const connectPage = await connectPagePromise; + + // Wait for the tab list to appear — this means connectToMCPRelay was processed + // by the background and _addTabToGroup has been called. + await expect(connectPage.locator('.tab-item').first()).toBeVisible(); + + const group = await connectPage.evaluate(async () => { + const chrome = (window as any).chrome; + const tab = await chrome.tabs.getCurrent(); + if (!tab || tab.groupId === -1) + return null; + const g = await chrome.tabGroups.get(tab.groupId); + return { color: g.color, title: g.title }; + }); + + expect(group).toEqual({ color: 'green', title: 'Playwright' }); + + await connectPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click(); + await navigatePromise; + }); + + test('connected tab is added to same Playwright group', async ({ browserWithExtension, startClient, server }) => { + const browserContext = await browserWithExtension.launch(); + + const page = await browserContext.newPage(); + await page.goto(server.HELLO_WORLD); + + const client = await startWithExtensionFlag(browserWithExtension, startClient); + + const connectPagePromise = browserContext.waitForEvent('page', page => + page.url().startsWith(`chrome-extension://${extensionId}/connect.html`) + ); + + const navigatePromise = client.callTool({ name: 'browser_navigate', arguments: { url: server.HELLO_WORLD } }); + const connectPage = await connectPagePromise; + + await connectPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click(); + await navigatePromise; + + const { connectGroupId, connectedGroupId } = await connectPage.evaluate(async () => { + const chrome = (window as any).chrome; + const connectTab = await chrome.tabs.getCurrent(); + const [connectedTab] = await chrome.tabs.query({ title: 'Title' }); + return { + connectGroupId: connectTab?.groupId, + connectedGroupId: connectedTab?.groupId, + }; + }); + + expect(connectGroupId).not.toBe(-1); + expect(connectedGroupId).toBe(connectGroupId); + }); + + test('connected tab is removed from group on disconnect', async ({ browserWithExtension, startClient, server }) => { + const browserContext = await browserWithExtension.launch(); + + const page = await browserContext.newPage(); + await page.goto(server.HELLO_WORLD); + + const client = await startWithExtensionFlag(browserWithExtension, startClient); + + const connectPagePromise = browserContext.waitForEvent('page', page => + page.url().startsWith(`chrome-extension://${extensionId}/connect.html`) + ); + + const navigatePromise = client.callTool({ name: 'browser_navigate', arguments: { url: server.HELLO_WORLD } }); + const connectPage = await connectPagePromise; + + await connectPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click(); + await navigatePromise; + + await client.close(); + + await expect.poll(async () => { + return connectPage.evaluate(async () => { + const chrome = (window as any).chrome; + const [tab] = await chrome.tabs.query({ title: 'Title' }); + return tab?.groupId ?? -1; + }); + }).toBe(-1); + }); +}); + test.describe('CLI with extension', () => { test('attach --extension', async ({ browserWithExtension, cli, server }, testInfo) => { const browserContext = await browserWithExtension.launch();