Skip to content

Commit 8964909

Browse files
committed
error page
1 parent 03b4afe commit 8964909

File tree

4 files changed

+83
-52
lines changed

4 files changed

+83
-52
lines changed

extension/src/background.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { RelayConnection, debugLog } from './relayConnection.js';
1919
type PageMessage = {
2020
type: 'connectToMCPRelay';
2121
mcpRelayUrl: string;
22+
pwMcpVersion: string | null;
2223
} | {
2324
type: 'getTabs';
2425
} | {
@@ -49,7 +50,7 @@ class TabShareExtension {
4950
private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) {
5051
switch (message.type) {
5152
case 'connectToMCPRelay':
52-
this._connectToRelay(sender.tab!.id!, message.mcpRelayUrl!).then(
53+
this._connectToRelay(sender.tab!.id!, message.mcpRelayUrl, message.pwMcpVersion).then(
5354
() => sendResponse({ success: true }),
5455
(error: any) => sendResponse({ success: false, error: error.message }));
5556
return true;
@@ -77,7 +78,11 @@ class TabShareExtension {
7778
return false;
7879
}
7980

80-
private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string): Promise<void> {
81+
private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string, pwMcpVersion: string | null): Promise<void> {
82+
const version = chrome.runtime.getManifest().version;
83+
if (pwMcpVersion !== version)
84+
throw new Error(`Incompatible Playwright MCP version: ${pwMcpVersion} (extension version: ${version}). Please install the latest version of the extension.`);
85+
8186
try {
8287
debugLog(`Connecting to relay at ${mcpRelayUrl}`);
8388
const socket = new WebSocket(mcpRelayUrl);
@@ -96,8 +101,9 @@ class TabShareExtension {
96101
this._pendingTabSelection.set(selectorTabId, { connection });
97102
debugLog(`Connected to MCP relay`);
98103
} catch (error: any) {
99-
debugLog(`Failed to connect to MCP relay:`, error.message);
100-
throw error;
104+
const message = `Failed to connect to MCP relay: ${error.message}`;
105+
debugLog(message);
106+
throw new Error(message);
101107
}
102108
}
103109

extension/src/ui/connect.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,22 @@ const ConnectApp: React.FC = () => {
5454
return;
5555
}
5656

57-
void connectToMCPRelay(relayUrl);
57+
void connectToMCPRelay(relayUrl, params.get('pwMcpVersion'));
5858
void loadTabs();
5959
}, []);
6060

61-
const connectToMCPRelay = useCallback(async (mcpRelayUrl: string) => {
62-
const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl });
63-
if (!response.success)
64-
setStatus({ type: 'error', message: 'Failed to connect to MCP relay: ' + response.error });
61+
const handleReject = useCallback((message: string) => {
62+
setShowButtons(false);
63+
setShowTabList(false);
64+
setStatus({ type: 'error', message });
6565
}, []);
6666

67+
const connectToMCPRelay = useCallback(async (mcpRelayUrl: string, pwMcpVersion: string | null) => {
68+
const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl, pwMcpVersion });
69+
if (!response.success)
70+
handleReject(response.error);
71+
}, [handleReject]);
72+
6773
const loadTabs = useCallback(async () => {
6874
const response = await chrome.runtime.sendMessage({ type: 'getTabs' });
6975
if (response.success)
@@ -100,22 +106,16 @@ const ConnectApp: React.FC = () => {
100106
}
101107
}, [clientInfo, mcpRelayUrl]);
102108

103-
const handleReject = useCallback(() => {
104-
setShowButtons(false);
105-
setShowTabList(false);
106-
setStatus({ type: 'error', message: 'Connection rejected. This tab can be closed.' });
107-
}, []);
108-
109109
useEffect(() => {
110110
const listener = (message: any) => {
111111
if (message.type === 'connectionTimeout')
112-
handleReject();
112+
handleReject('Connection timed out.');
113113
};
114114
chrome.runtime.onMessage.addListener(listener);
115115
return () => {
116116
chrome.runtime.onMessage.removeListener(listener);
117117
};
118-
}, []);
118+
}, [handleReject]);
119119

120120
return (
121121
<div className='app-container'>
@@ -124,7 +124,7 @@ const ConnectApp: React.FC = () => {
124124
<div className='status-container'>
125125
<StatusBanner type={status.type} message={status.message} />
126126
{showButtons && (
127-
<Button variant='reject' onClick={handleReject}>
127+
<Button variant='reject' onClick={() => handleReject('Connection rejected. This tab can be closed.')}>
128128
Reject
129129
</Button>
130130
)}

extension/tests/extension.spec.ts

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,15 @@ type BrowserWithExtension = {
3030
launch: (mode?: 'disable-extension') => Promise<BrowserContext>;
3131
};
3232

33-
const test = base.extend<{ browserWithExtension: BrowserWithExtension, pathToExtension: string }>({
34-
pathToExtension: async () => {
35-
return fileURLToPath(new URL('../dist', import.meta.url));
33+
type TestFixtures = {
34+
browserWithExtension: BrowserWithExtension,
35+
pathToExtension: string,
36+
useShortConnectionTimeout: (timeoutMs: number) => void
37+
};
38+
39+
const test = base.extend<TestFixtures>({
40+
pathToExtension: async ({}, use) => {
41+
await use(fileURLToPath(new URL('../dist', import.meta.url)));
3642
},
3743

3844
browserWithExtension: async ({ mcpBrowser, pathToExtension }, use, testInfo) => {
@@ -65,9 +71,16 @@ const test = base.extend<{ browserWithExtension: BrowserWithExtension, pathToExt
6571
return browserContext;
6672
}
6773
});
68-
6974
await browserContext?.close();
7075
},
76+
77+
useShortConnectionTimeout: async ({}, use) => {
78+
await use((timeoutMs: number) => {
79+
process.env.PWMCP_TEST_CONNECTION_TIMEOUT = timeoutMs.toString();
80+
});
81+
process.env.PWMCP_TEST_CONNECTION_TIMEOUT = undefined;
82+
},
83+
7184
});
7285

7386
async function startAndCallConnectTool(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise<Client> {
@@ -104,6 +117,21 @@ async function startWithExtensionFlag(browserWithExtension: BrowserWithExtension
104117
return client;
105118
}
106119

120+
const testWithOldVersion = test.extend({
121+
pathToExtension: async ({}, use, testInfo) => {
122+
const extensionDir = testInfo.outputPath('extension');
123+
const oldPath = fileURLToPath(new URL('../dist', import.meta.url));
124+
125+
await fs.promises.cp(oldPath, extensionDir, { recursive: true });
126+
const manifestPath = path.join(extensionDir, 'manifest.json');
127+
const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
128+
manifest.version = '0.0.1';
129+
await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
130+
131+
await use(extensionDir);
132+
},
133+
});
134+
107135
for (const [mode, startClientMethod] of [
108136
['connect-tool', startAndCallConnectTool],
109137
['extension-flag', startWithExtensionFlag],
@@ -165,8 +193,8 @@ for (const [mode, startClientMethod] of [
165193
expect(browserContext.pages()).toHaveLength(4);
166194
});
167195

168-
test(`extension not installed timeout (${mode})`, async ({ browserWithExtension, startClient, server }) => {
169-
process.env.PWMCP_TEST_CONNECTION_TIMEOUT = '100';
196+
test(`extension not installed timeout (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => {
197+
useShortConnectionTimeout(100);
170198

171199
const browserContext = await browserWithExtension.launch();
172200

@@ -185,40 +213,32 @@ for (const [mode, startClientMethod] of [
185213
});
186214

187215
await confirmationPagePromise;
188-
189-
process.env.PWMCP_TEST_CONNECTION_TIMEOUT = undefined;
190216
});
191217

192-
}
218+
testWithOldVersion(`extension version mismatch (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => {
219+
useShortConnectionTimeout(500);
193220

194-
const testWithOldVersion = test.extend({
195-
pathToExtension: async ({}, use, testInfo) => {
196-
const extensionDir = testInfo.outputPath('extension');
197-
const oldPath = fileURLToPath(new URL('../dist', import.meta.url));
221+
// Prelaunch the browser, so that it is properly closed after the test.
222+
const browserContext = await browserWithExtension.launch();
198223

199-
await fs.promises.cp(oldPath, extensionDir, { recursive: true });
200-
const manifestPath = path.join(extensionDir, 'manifest.json');
201-
const manifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8'));
202-
manifest.version = '0.0.1';
203-
await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
224+
const client = await startClientMethod(browserWithExtension, startClient);
204225

205-
await use(extensionDir);
206-
},
207-
});
226+
const confirmationPagePromise = browserContext.waitForEvent('page', page => {
227+
return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html');
228+
});
208229

209-
testWithOldVersion(`extension version mismatch`, async ({ browserWithExtension, startClient, server }) => {
210-
// Prelaunch the browser, so that it is properly closed after the test.
211-
await browserWithExtension.launch();
230+
const navigateResponse = client.callTool({
231+
name: 'browser_navigate',
232+
arguments: { url: server.HELLO_WORLD },
233+
});
212234

213-
const client = await startWithExtensionFlag(browserWithExtension, startClient);
235+
const confirmationPage = await confirmationPagePromise;
236+
await expect(confirmationPage.locator('.status-banner')).toHaveText(`Incompatible Playwright MCP version: ${packageJSON.version} (extension version: 0.0.1). Please install the latest version of the extension.`);
214237

215-
const navigateResponse = client.callTool({
216-
name: 'browser_navigate',
217-
arguments: { url: server.HELLO_WORLD },
238+
expect(await navigateResponse).toHaveResponse({
239+
result: expect.stringContaining('Extension connection timeout.'),
240+
isError: true,
241+
});
218242
});
219243

220-
expect(await navigateResponse).toHaveResponse({
221-
result: expect.stringContaining('Extension version mismatch: expected ' + packageJSON.version + ', got 0.0.1. Make sure the extension is up to date.'),
222-
isError: true,
223-
});
224-
});
244+
}

src/extension/cdpRelay.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@ export class CDPRelayServer {
122122
// Need to specify "key" in the manifest.json to make the id stable when loading from file.
123123
const url = new URL('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html');
124124
url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint);
125-
url.searchParams.set('client', JSON.stringify(clientInfo));
125+
const client = {
126+
name: clientInfo.name,
127+
version: clientInfo.version,
128+
};
129+
url.searchParams.set('client', JSON.stringify(client));
130+
url.searchParams.set('pwMcpVersion', packageJSON.version);
126131
const href = url.toString();
127132
const executableInfo = registry.findExecutable(this._browserChannel);
128133
if (!executableInfo)

0 commit comments

Comments
 (0)