@jackwener/opencli 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{CLI-CREATOR.md → CLI-EXPLORER.md} +5 -1
- package/CLI-ONESHOT.md +216 -0
- package/README.md +4 -3
- package/README.zh-CN.md +4 -3
- package/SKILL.md +6 -4
- package/dist/browser.d.ts +32 -8
- package/dist/browser.js +233 -107
- package/dist/browser.test.js +51 -38
- package/package.json +1 -1
- package/src/browser.test.ts +69 -43
- package/src/browser.ts +232 -86
package/dist/browser.js
CHANGED
|
@@ -79,10 +79,10 @@ const PKG_VERSION = (() => { try {
|
|
|
79
79
|
catch {
|
|
80
80
|
return '0.0.0';
|
|
81
81
|
} })();
|
|
82
|
-
const EXTENSION_LOCK_TIMEOUT = parseInt(process.env.OPENCLI_EXTENSION_LOCK_TIMEOUT ?? '120', 10);
|
|
83
|
-
const EXTENSION_LOCK_POLL = parseInt(process.env.OPENCLI_EXTENSION_LOCK_POLL_INTERVAL ?? '1', 10);
|
|
84
82
|
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
85
|
-
const
|
|
83
|
+
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
84
|
+
const TAB_CLEANUP_TIMEOUT_MS = 2000;
|
|
85
|
+
let _cachedMcpServerPath;
|
|
86
86
|
export function getTokenFingerprint(token) {
|
|
87
87
|
if (!token)
|
|
88
88
|
return null;
|
|
@@ -146,22 +146,23 @@ function inferConnectFailureKind(args) {
|
|
|
146
146
|
}
|
|
147
147
|
// JSON-RPC helpers
|
|
148
148
|
let _nextId = 1;
|
|
149
|
-
function
|
|
150
|
-
|
|
149
|
+
function createJsonRpcRequest(method, params = {}) {
|
|
150
|
+
const id = _nextId++;
|
|
151
|
+
return {
|
|
152
|
+
id,
|
|
153
|
+
message: JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n',
|
|
154
|
+
};
|
|
151
155
|
}
|
|
152
156
|
/**
|
|
153
157
|
* Page abstraction wrapping JSON-RPC calls to Playwright MCP.
|
|
154
158
|
*/
|
|
155
159
|
export class Page {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this._send = _send;
|
|
160
|
-
this._recv = _recv;
|
|
160
|
+
_request;
|
|
161
|
+
constructor(_request) {
|
|
162
|
+
this._request = _request;
|
|
161
163
|
}
|
|
162
164
|
async call(method, params = {}) {
|
|
163
|
-
this.
|
|
164
|
-
const resp = await this._recv();
|
|
165
|
+
const resp = await this._request(method, params);
|
|
165
166
|
if (resp.error)
|
|
166
167
|
throw new Error(`page.${method}: ${resp.error.message ?? JSON.stringify(resp.error)}`);
|
|
167
168
|
// Extract text content from MCP result
|
|
@@ -359,13 +360,6 @@ export class PlaywrightMCP {
|
|
|
359
360
|
this._cleanupRegistered = true;
|
|
360
361
|
const cleanup = () => {
|
|
361
362
|
for (const inst of this._activeInsts) {
|
|
362
|
-
if (inst._lockAcquired) {
|
|
363
|
-
try {
|
|
364
|
-
fs.rmdirSync(LOCK_DIR);
|
|
365
|
-
}
|
|
366
|
-
catch { }
|
|
367
|
-
inst._lockAcquired = false;
|
|
368
|
-
}
|
|
369
363
|
if (inst._proc && !inst._proc.killed) {
|
|
370
364
|
try {
|
|
371
365
|
inst._proc.kill('SIGKILL');
|
|
@@ -380,16 +374,67 @@ export class PlaywrightMCP {
|
|
|
380
374
|
}
|
|
381
375
|
_proc = null;
|
|
382
376
|
_buffer = '';
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
377
|
+
_pending = new Map();
|
|
378
|
+
_initialTabIdentities = [];
|
|
379
|
+
_closingPromise = null;
|
|
380
|
+
_state = 'idle';
|
|
386
381
|
_page = null;
|
|
382
|
+
get state() {
|
|
383
|
+
return this._state;
|
|
384
|
+
}
|
|
385
|
+
_sendRequest(method, params = {}) {
|
|
386
|
+
return new Promise((resolve, reject) => {
|
|
387
|
+
if (!this._proc?.stdin?.writable) {
|
|
388
|
+
reject(new Error('Playwright MCP process is not writable'));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const { id, message } = createJsonRpcRequest(method, params);
|
|
392
|
+
this._pending.set(id, { resolve, reject });
|
|
393
|
+
this._proc.stdin.write(message, (err) => {
|
|
394
|
+
if (!err)
|
|
395
|
+
return;
|
|
396
|
+
this._pending.delete(id);
|
|
397
|
+
reject(err);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
_rejectPendingRequests(error) {
|
|
402
|
+
const pending = [...this._pending.values()];
|
|
403
|
+
this._pending.clear();
|
|
404
|
+
for (const waiter of pending)
|
|
405
|
+
waiter.reject(error);
|
|
406
|
+
}
|
|
407
|
+
_resetAfterFailedConnect() {
|
|
408
|
+
const proc = this._proc;
|
|
409
|
+
this._page = null;
|
|
410
|
+
this._proc = null;
|
|
411
|
+
this._buffer = '';
|
|
412
|
+
this._initialTabIdentities = [];
|
|
413
|
+
this._rejectPendingRequests(new Error('Playwright MCP connect failed'));
|
|
414
|
+
PlaywrightMCP._activeInsts.delete(this);
|
|
415
|
+
if (proc && !proc.killed) {
|
|
416
|
+
try {
|
|
417
|
+
proc.kill('SIGKILL');
|
|
418
|
+
}
|
|
419
|
+
catch { }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
387
422
|
async connect(opts = {}) {
|
|
388
|
-
|
|
389
|
-
|
|
423
|
+
if (this._state === 'connected' && this._page)
|
|
424
|
+
return this._page;
|
|
425
|
+
if (this._state === 'connecting')
|
|
426
|
+
throw new Error('Playwright MCP is already connecting');
|
|
427
|
+
if (this._state === 'closing')
|
|
428
|
+
throw new Error('Playwright MCP is closing');
|
|
429
|
+
if (this._state === 'closed')
|
|
430
|
+
throw new Error('Playwright MCP session is closed');
|
|
390
431
|
const mcpPath = findMcpServerPath();
|
|
391
432
|
if (!mcpPath)
|
|
392
433
|
throw new Error('Playwright MCP server not found. Install: npm install -D @playwright/mcp');
|
|
434
|
+
PlaywrightMCP._registerGlobalCleanup();
|
|
435
|
+
PlaywrightMCP._activeInsts.add(this);
|
|
436
|
+
this._state = 'connecting';
|
|
437
|
+
const timeout = opts.timeout ?? CONNECT_TIMEOUT;
|
|
393
438
|
// Connection priority:
|
|
394
439
|
// 1. OPENCLI_CDP_ENDPOINT env var → explicit CDP endpoint
|
|
395
440
|
// 2. OPENCLI_USE_CDP=1 → auto-discover via DevToolsActivePort
|
|
@@ -417,7 +462,9 @@ export class PlaywrightMCP {
|
|
|
417
462
|
if (settled)
|
|
418
463
|
return;
|
|
419
464
|
settled = true;
|
|
465
|
+
this._state = 'idle';
|
|
420
466
|
clearTimeout(timer);
|
|
467
|
+
this._resetAfterFailedConnect();
|
|
421
468
|
reject(formatBrowserConnectError({
|
|
422
469
|
kind,
|
|
423
470
|
mode,
|
|
@@ -433,6 +480,7 @@ export class PlaywrightMCP {
|
|
|
433
480
|
if (settled)
|
|
434
481
|
return;
|
|
435
482
|
settled = true;
|
|
483
|
+
this._state = 'connected';
|
|
436
484
|
clearTimeout(timer);
|
|
437
485
|
resolve(pageToResolve);
|
|
438
486
|
};
|
|
@@ -469,8 +517,7 @@ export class PlaywrightMCP {
|
|
|
469
517
|
this._proc.setMaxListeners(20);
|
|
470
518
|
if (this._proc.stdout)
|
|
471
519
|
this._proc.stdout.setMaxListeners(20);
|
|
472
|
-
const page = new Page((
|
|
473
|
-
this._proc.stdin.write(msg); }, () => new Promise((res) => { this._waiters.push(res); }));
|
|
520
|
+
const page = new Page((method, params = {}) => this._sendRequest(method, params));
|
|
474
521
|
this._page = page;
|
|
475
522
|
this._proc.stdout?.on('data', (chunk) => {
|
|
476
523
|
this._buffer += chunk.toString();
|
|
@@ -482,9 +529,13 @@ export class PlaywrightMCP {
|
|
|
482
529
|
debugLog(`RECV: ${line}`);
|
|
483
530
|
try {
|
|
484
531
|
const parsed = JSON.parse(line);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
waiter
|
|
532
|
+
if (typeof parsed?.id === 'number') {
|
|
533
|
+
const waiter = this._pending.get(parsed.id);
|
|
534
|
+
if (waiter) {
|
|
535
|
+
this._pending.delete(parsed.id);
|
|
536
|
+
waiter.resolve(parsed);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
488
539
|
}
|
|
489
540
|
catch (e) {
|
|
490
541
|
debugLog(`Parse error: ${e}`);
|
|
@@ -493,15 +544,17 @@ export class PlaywrightMCP {
|
|
|
493
544
|
});
|
|
494
545
|
this._proc.stderr?.on('data', (chunk) => {
|
|
495
546
|
const text = chunk.toString();
|
|
496
|
-
stderrBuffer
|
|
547
|
+
stderrBuffer = appendLimited(stderrBuffer, text, STDERR_BUFFER_LIMIT);
|
|
497
548
|
debugLog(`STDERR: ${text}`);
|
|
498
549
|
});
|
|
499
550
|
this._proc.on('error', (err) => {
|
|
500
551
|
debugLog(`Subprocess error: ${err.message}`);
|
|
552
|
+
this._rejectPendingRequests(new Error(`Playwright MCP process error: ${err.message}`));
|
|
501
553
|
settleError('process-exit', { rawMessage: err.message });
|
|
502
554
|
});
|
|
503
555
|
this._proc.on('close', (code) => {
|
|
504
556
|
debugLog(`Subprocess closed with code ${code}`);
|
|
557
|
+
this._rejectPendingRequests(new Error(`Playwright MCP process exited before response${code == null ? '' : ` (code ${code})`}`));
|
|
505
558
|
if (!settled) {
|
|
506
559
|
settleError(inferConnectFailureKind({
|
|
507
560
|
mode,
|
|
@@ -512,17 +565,12 @@ export class PlaywrightMCP {
|
|
|
512
565
|
}
|
|
513
566
|
});
|
|
514
567
|
// Initialize: send initialize request
|
|
515
|
-
|
|
568
|
+
debugLog('Waiting for initialize response...');
|
|
569
|
+
this._sendRequest('initialize', {
|
|
516
570
|
protocolVersion: '2024-11-05',
|
|
517
571
|
capabilities: {},
|
|
518
572
|
clientInfo: { name: 'opencli', version: PKG_VERSION },
|
|
519
|
-
})
|
|
520
|
-
debugLog(`SEND: ${initMsg.trim()}`);
|
|
521
|
-
this._proc.stdin?.write(initMsg);
|
|
522
|
-
// Wait for initialize response, then send initialized notification
|
|
523
|
-
const origRecv = () => new Promise((res) => { this._waiters.push(res); });
|
|
524
|
-
debugLog('Waiting for initialize response...');
|
|
525
|
-
origRecv().then((resp) => {
|
|
573
|
+
}).then((resp) => {
|
|
526
574
|
debugLog('Got initialize response');
|
|
527
575
|
if (resp.error) {
|
|
528
576
|
settleError(inferConnectFailureKind({
|
|
@@ -540,12 +588,7 @@ export class PlaywrightMCP {
|
|
|
540
588
|
debugLog('Fetching initial tabs count...');
|
|
541
589
|
page.tabs().then((tabs) => {
|
|
542
590
|
debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
|
|
543
|
-
|
|
544
|
-
this._initialTabCount = (tabs.match(/Tab \d+/g) || []).length;
|
|
545
|
-
}
|
|
546
|
-
else if (Array.isArray(tabs)) {
|
|
547
|
-
this._initialTabCount = tabs.length;
|
|
548
|
-
}
|
|
591
|
+
this._initialTabIdentities = extractTabIdentities(tabs);
|
|
549
592
|
settleSuccess(page);
|
|
550
593
|
}).catch((err) => {
|
|
551
594
|
debugLog(`Tabs fetch error: ${err.message}`);
|
|
@@ -558,81 +601,157 @@ export class PlaywrightMCP {
|
|
|
558
601
|
});
|
|
559
602
|
}
|
|
560
603
|
async close() {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
604
|
+
if (this._closingPromise)
|
|
605
|
+
return this._closingPromise;
|
|
606
|
+
if (this._state === 'closed')
|
|
607
|
+
return;
|
|
608
|
+
this._state = 'closing';
|
|
609
|
+
this._closingPromise = (async () => {
|
|
610
|
+
try {
|
|
611
|
+
// Close tabs opened during this session (site tabs + extension tabs)
|
|
612
|
+
if (this._page && this._proc && !this._proc.killed) {
|
|
613
|
+
try {
|
|
614
|
+
const tabs = await withTimeout(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup');
|
|
615
|
+
const tabEntries = extractTabEntries(tabs);
|
|
616
|
+
const tabsToClose = diffTabIndexes(this._initialTabIdentities, tabEntries);
|
|
617
|
+
for (const index of tabsToClose) {
|
|
573
618
|
try {
|
|
574
|
-
await this._page.closeTab(
|
|
619
|
+
await this._page.closeTab(index);
|
|
575
620
|
}
|
|
576
621
|
catch { }
|
|
577
622
|
}
|
|
578
623
|
}
|
|
624
|
+
catch { }
|
|
579
625
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
this._lockAcquired = true;
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
catch (e) {
|
|
602
|
-
if (e.code !== 'EEXIST')
|
|
603
|
-
throw e;
|
|
604
|
-
if ((Date.now() - start) / 1000 > EXTENSION_LOCK_TIMEOUT) {
|
|
605
|
-
// Force remove stale lock
|
|
606
|
-
try {
|
|
607
|
-
fs.rmdirSync(LOCK_DIR);
|
|
626
|
+
if (this._proc && !this._proc.killed) {
|
|
627
|
+
this._proc.kill('SIGTERM');
|
|
628
|
+
const exited = await new Promise((res) => {
|
|
629
|
+
let done = false;
|
|
630
|
+
const finish = (value) => {
|
|
631
|
+
if (done)
|
|
632
|
+
return;
|
|
633
|
+
done = true;
|
|
634
|
+
res(value);
|
|
635
|
+
};
|
|
636
|
+
this._proc?.once('exit', () => finish(true));
|
|
637
|
+
setTimeout(() => finish(false), 3000);
|
|
638
|
+
});
|
|
639
|
+
if (!exited && this._proc && !this._proc.killed) {
|
|
640
|
+
try {
|
|
641
|
+
this._proc.kill('SIGKILL');
|
|
642
|
+
}
|
|
643
|
+
catch { }
|
|
608
644
|
}
|
|
609
|
-
catch { }
|
|
610
|
-
continue;
|
|
611
645
|
}
|
|
612
|
-
await new Promise(r => setTimeout(r, EXTENSION_LOCK_POLL * 1000));
|
|
613
646
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
647
|
+
finally {
|
|
648
|
+
this._rejectPendingRequests(new Error('Playwright MCP session closed'));
|
|
649
|
+
this._page = null;
|
|
650
|
+
this._proc = null;
|
|
651
|
+
this._state = 'closed';
|
|
652
|
+
PlaywrightMCP._activeInsts.delete(this);
|
|
620
653
|
}
|
|
621
|
-
|
|
622
|
-
|
|
654
|
+
})();
|
|
655
|
+
return this._closingPromise;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function extractTabEntries(raw) {
|
|
659
|
+
if (Array.isArray(raw)) {
|
|
660
|
+
return raw.map((tab, index) => ({
|
|
661
|
+
index,
|
|
662
|
+
identity: [
|
|
663
|
+
tab?.id ?? '',
|
|
664
|
+
tab?.url ?? '',
|
|
665
|
+
tab?.title ?? '',
|
|
666
|
+
tab?.name ?? '',
|
|
667
|
+
].join('|'),
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
if (typeof raw === 'string') {
|
|
671
|
+
return raw
|
|
672
|
+
.split('\n')
|
|
673
|
+
.map(line => line.trim())
|
|
674
|
+
.filter(Boolean)
|
|
675
|
+
.map(line => {
|
|
676
|
+
const match = line.match(/Tab\s+(\d+)\s*(.*)$/);
|
|
677
|
+
if (!match)
|
|
678
|
+
return null;
|
|
679
|
+
return {
|
|
680
|
+
index: parseInt(match[1], 10),
|
|
681
|
+
identity: match[2].trim() || `tab-${match[1]}`,
|
|
682
|
+
};
|
|
683
|
+
})
|
|
684
|
+
.filter((entry) => entry !== null);
|
|
685
|
+
}
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
function extractTabIdentities(raw) {
|
|
689
|
+
return extractTabEntries(raw).map(tab => tab.identity);
|
|
690
|
+
}
|
|
691
|
+
function diffTabIndexes(initialIdentities, currentTabs) {
|
|
692
|
+
if (initialIdentities.length === 0 || currentTabs.length === 0)
|
|
693
|
+
return [];
|
|
694
|
+
const remaining = new Map();
|
|
695
|
+
for (const identity of initialIdentities) {
|
|
696
|
+
remaining.set(identity, (remaining.get(identity) ?? 0) + 1);
|
|
697
|
+
}
|
|
698
|
+
const tabsToClose = [];
|
|
699
|
+
for (const tab of currentTabs) {
|
|
700
|
+
const count = remaining.get(tab.identity) ?? 0;
|
|
701
|
+
if (count > 0) {
|
|
702
|
+
remaining.set(tab.identity, count - 1);
|
|
703
|
+
continue;
|
|
623
704
|
}
|
|
705
|
+
tabsToClose.push(tab.index);
|
|
624
706
|
}
|
|
707
|
+
return tabsToClose.sort((a, b) => b - a);
|
|
708
|
+
}
|
|
709
|
+
function appendLimited(current, chunk, limit) {
|
|
710
|
+
const next = current + chunk;
|
|
711
|
+
if (next.length <= limit)
|
|
712
|
+
return next;
|
|
713
|
+
return next.slice(-limit);
|
|
714
|
+
}
|
|
715
|
+
function withTimeout(promise, timeoutMs, message) {
|
|
716
|
+
return new Promise((resolve, reject) => {
|
|
717
|
+
const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
718
|
+
promise.then((value) => {
|
|
719
|
+
clearTimeout(timer);
|
|
720
|
+
resolve(value);
|
|
721
|
+
}, (error) => {
|
|
722
|
+
clearTimeout(timer);
|
|
723
|
+
reject(error);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
625
726
|
}
|
|
727
|
+
export const __test__ = {
|
|
728
|
+
createJsonRpcRequest,
|
|
729
|
+
extractTabEntries,
|
|
730
|
+
diffTabIndexes,
|
|
731
|
+
appendLimited,
|
|
732
|
+
withTimeout,
|
|
733
|
+
};
|
|
626
734
|
function findMcpServerPath() {
|
|
735
|
+
if (_cachedMcpServerPath !== undefined)
|
|
736
|
+
return _cachedMcpServerPath;
|
|
737
|
+
const envMcp = process.env.OPENCLI_MCP_SERVER_PATH;
|
|
738
|
+
if (envMcp && fs.existsSync(envMcp)) {
|
|
739
|
+
_cachedMcpServerPath = envMcp;
|
|
740
|
+
return _cachedMcpServerPath;
|
|
741
|
+
}
|
|
627
742
|
// Check local node_modules first (@playwright/mcp is the modern package)
|
|
628
743
|
const localMcp = path.resolve('node_modules', '@playwright', 'mcp', 'cli.js');
|
|
629
|
-
if (fs.existsSync(localMcp))
|
|
630
|
-
|
|
744
|
+
if (fs.existsSync(localMcp)) {
|
|
745
|
+
_cachedMcpServerPath = localMcp;
|
|
746
|
+
return _cachedMcpServerPath;
|
|
747
|
+
}
|
|
631
748
|
// Check project-relative path
|
|
632
749
|
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
633
750
|
const projectMcp = path.resolve(__dirname2, '..', 'node_modules', '@playwright', 'mcp', 'cli.js');
|
|
634
|
-
if (fs.existsSync(projectMcp))
|
|
635
|
-
|
|
751
|
+
if (fs.existsSync(projectMcp)) {
|
|
752
|
+
_cachedMcpServerPath = projectMcp;
|
|
753
|
+
return _cachedMcpServerPath;
|
|
754
|
+
}
|
|
636
755
|
// Check common locations
|
|
637
756
|
const candidates = [
|
|
638
757
|
path.join(os.homedir(), '.npm', '_npx'),
|
|
@@ -642,15 +761,19 @@ function findMcpServerPath() {
|
|
|
642
761
|
// Try npx resolution (legacy package name)
|
|
643
762
|
try {
|
|
644
763
|
const result = execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
645
|
-
if (result && fs.existsSync(result))
|
|
646
|
-
|
|
764
|
+
if (result && fs.existsSync(result)) {
|
|
765
|
+
_cachedMcpServerPath = result;
|
|
766
|
+
return _cachedMcpServerPath;
|
|
767
|
+
}
|
|
647
768
|
}
|
|
648
769
|
catch { }
|
|
649
770
|
// Try which
|
|
650
771
|
try {
|
|
651
772
|
const result = execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
652
|
-
if (result && fs.existsSync(result))
|
|
653
|
-
|
|
773
|
+
if (result && fs.existsSync(result)) {
|
|
774
|
+
_cachedMcpServerPath = result;
|
|
775
|
+
return _cachedMcpServerPath;
|
|
776
|
+
}
|
|
654
777
|
}
|
|
655
778
|
catch { }
|
|
656
779
|
// Search in common npx cache
|
|
@@ -659,10 +782,13 @@ function findMcpServerPath() {
|
|
|
659
782
|
continue;
|
|
660
783
|
try {
|
|
661
784
|
const found = execSync(`find "${base}" -name "cli.js" -path "*playwright*mcp*" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
662
|
-
if (found)
|
|
663
|
-
|
|
785
|
+
if (found) {
|
|
786
|
+
_cachedMcpServerPath = found;
|
|
787
|
+
return _cachedMcpServerPath;
|
|
788
|
+
}
|
|
664
789
|
}
|
|
665
790
|
catch { }
|
|
666
791
|
}
|
|
667
|
-
|
|
792
|
+
_cachedMcpServerPath = null;
|
|
793
|
+
return _cachedMcpServerPath;
|
|
668
794
|
}
|
package/dist/browser.test.js
CHANGED
|
@@ -1,43 +1,56 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
3
|
-
describe('
|
|
4
|
-
it('
|
|
5
|
-
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { PlaywrightMCP, __test__ } from './browser.js';
|
|
3
|
+
describe('browser helpers', () => {
|
|
4
|
+
it('creates JSON-RPC requests with unique ids', () => {
|
|
5
|
+
const first = __test__.createJsonRpcRequest('tools/call', { name: 'browser_tabs' });
|
|
6
|
+
const second = __test__.createJsonRpcRequest('tools/call', { name: 'browser_snapshot' });
|
|
7
|
+
expect(second.id).toBe(first.id + 1);
|
|
8
|
+
expect(first.message).toContain(`"id":${first.id}`);
|
|
9
|
+
expect(second.message).toContain(`"id":${second.id}`);
|
|
6
10
|
});
|
|
7
|
-
it('
|
|
8
|
-
|
|
11
|
+
it('extracts tab entries from string snapshots', () => {
|
|
12
|
+
const entries = __test__.extractTabEntries('Tab 0 https://example.com\nTab 1 Chrome Extension');
|
|
13
|
+
expect(entries).toEqual([
|
|
14
|
+
{ index: 0, identity: 'https://example.com' },
|
|
15
|
+
{ index: 1, identity: 'Chrome Extension' },
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
it('closes only tabs that were opened during the session', () => {
|
|
19
|
+
const tabsToClose = __test__.diffTabIndexes(['https://example.com', 'Chrome Extension'], [
|
|
20
|
+
{ index: 0, identity: 'https://example.com' },
|
|
21
|
+
{ index: 1, identity: 'Chrome Extension' },
|
|
22
|
+
{ index: 2, identity: 'https://target.example/page' },
|
|
23
|
+
{ index: 3, identity: 'chrome-extension://bridge' },
|
|
24
|
+
]);
|
|
25
|
+
expect(tabsToClose).toEqual([3, 2]);
|
|
26
|
+
});
|
|
27
|
+
it('keeps only the tail of stderr buffers', () => {
|
|
28
|
+
expect(__test__.appendLimited('12345', '67890', 8)).toBe('34567890');
|
|
29
|
+
});
|
|
30
|
+
it('times out slow promises', async () => {
|
|
31
|
+
await expect(__test__.withTimeout(new Promise(() => { }), 10, 'timeout')).rejects.toThrow('timeout');
|
|
9
32
|
});
|
|
10
33
|
});
|
|
11
|
-
describe('
|
|
12
|
-
it('
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
expect(
|
|
32
|
-
});
|
|
33
|
-
it('keeps CDP timeout guidance separate', () => {
|
|
34
|
-
const err = formatBrowserConnectError({
|
|
35
|
-
kind: 'cdp-timeout',
|
|
36
|
-
mode: 'cdp',
|
|
37
|
-
timeout: 30,
|
|
38
|
-
hasExtensionToken: false,
|
|
39
|
-
});
|
|
40
|
-
expect(err.message).toContain('via CDP');
|
|
41
|
-
expect(err.message).toContain('chrome://inspect#remote-debugging');
|
|
34
|
+
describe('PlaywrightMCP state', () => {
|
|
35
|
+
it('transitions to closed after close()', async () => {
|
|
36
|
+
const mcp = new PlaywrightMCP();
|
|
37
|
+
expect(mcp.state).toBe('idle');
|
|
38
|
+
await mcp.close();
|
|
39
|
+
expect(mcp.state).toBe('closed');
|
|
40
|
+
});
|
|
41
|
+
it('rejects connect() after the session has been closed', async () => {
|
|
42
|
+
const mcp = new PlaywrightMCP();
|
|
43
|
+
await mcp.close();
|
|
44
|
+
await expect(mcp.connect()).rejects.toThrow('Playwright MCP session is closed');
|
|
45
|
+
});
|
|
46
|
+
it('rejects connect() while already connecting', async () => {
|
|
47
|
+
const mcp = new PlaywrightMCP();
|
|
48
|
+
mcp._state = 'connecting';
|
|
49
|
+
await expect(mcp.connect()).rejects.toThrow('Playwright MCP is already connecting');
|
|
50
|
+
});
|
|
51
|
+
it('rejects connect() while closing', async () => {
|
|
52
|
+
const mcp = new PlaywrightMCP();
|
|
53
|
+
mcp._state = 'closing';
|
|
54
|
+
await expect(mcp.connect()).rejects.toThrow('Playwright MCP is closing');
|
|
42
55
|
});
|
|
43
56
|
});
|