mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2026-04-04 16:43:10 +00:00
feat(extension): group connected tabs into a green Playwright tab group (#1514)
## Summary - Add `tabGroups` permission to the extension manifest - When the connect page opens (via action button click or MCP relay connect), place it into a green tab group titled "Playwright" - When a tab is connected to an MCP session, add it to the same group - When the active connection closes, remove the connected tab from the group via `chrome.tabs.ungroup` - Add three tests: group assigned to connect page, group shared with connected tab, and group cleaned up on disconnect
This commit is contained in:
@@ -6,7 +6,8 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"debugger",
|
"debugger",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"tabs"
|
"tabs",
|
||||||
|
"tabGroups"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type PageMessage = {
|
|||||||
class TabShareExtension {
|
class TabShareExtension {
|
||||||
private _activeConnection: RelayConnection | undefined;
|
private _activeConnection: RelayConnection | undefined;
|
||||||
private _connectedTabId: number | null = null;
|
private _connectedTabId: number | null = null;
|
||||||
|
private _groupId: number | null = null;
|
||||||
private _pendingTabSelection = new Map<number, { connection: RelayConnection, timerId?: number }>();
|
private _pendingTabSelection = new Map<number, { connection: RelayConnection, timerId?: number }>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -96,6 +97,7 @@ class TabShareExtension {
|
|||||||
// TODO: show error in the selector tab?
|
// TODO: show error in the selector tab?
|
||||||
};
|
};
|
||||||
this._pendingTabSelection.set(selectorTabId, { connection });
|
this._pendingTabSelection.set(selectorTabId, { connection });
|
||||||
|
await this._addTabToGroup(selectorTabId);
|
||||||
debugLog(`Connected to MCP relay`);
|
debugLog(`Connected to MCP relay`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message = `Failed to connect to MCP relay: ${error.message}`;
|
const message = `Failed to connect to MCP relay: ${error.message}`;
|
||||||
@@ -124,10 +126,12 @@ class TabShareExtension {
|
|||||||
debugLog('MCP connection closed');
|
debugLog('MCP connection closed');
|
||||||
this._activeConnection = undefined;
|
this._activeConnection = undefined;
|
||||||
void this._setConnectedTabId(null);
|
void this._setConnectedTabId(null);
|
||||||
|
chrome.tabs.ungroup([tabId]).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._setConnectedTabId(tabId),
|
this._setConnectedTabId(tabId),
|
||||||
|
this._addTabToGroup(tabId),
|
||||||
chrome.tabs.update(tabId, { active: true }),
|
chrome.tabs.update(tabId, { active: true }),
|
||||||
chrome.windows.update(windowId, { focused: 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)));
|
return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _addTabToGroup(tabId: number): Promise<void> {
|
||||||
|
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<void> {
|
private async _onActionClicked(): Promise<void> {
|
||||||
await chrome.tabs.create({
|
const tab = await chrome.tabs.create({
|
||||||
url: chrome.runtime.getURL('status.html'),
|
url: chrome.runtime.getURL('status.html'),
|
||||||
active: true
|
active: true
|
||||||
});
|
});
|
||||||
|
if (tab.id)
|
||||||
|
await this._addTabToGroup(tab.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _disconnect(): Promise<void> {
|
private async _disconnect(): Promise<void> {
|
||||||
|
|||||||
@@ -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.describe('CLI with extension', () => {
|
||||||
test('attach <url> --extension', async ({ browserWithExtension, cli, server }, testInfo) => {
|
test('attach <url> --extension', async ({ browserWithExtension, cli, server }, testInfo) => {
|
||||||
const browserContext = await browserWithExtension.launch();
|
const browserContext = await browserWithExtension.launch();
|
||||||
|
|||||||
Reference in New Issue
Block a user