@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/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 LOCK_DIR = path.join(os.tmpdir(), 'opencli-mcp-lock');
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 jsonRpcRequest(method, params = {}) {
150
- return JSON.stringify({ jsonrpc: '2.0', id: _nextId++, method, params }) + '\n';
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
- _send;
157
- _recv;
158
- constructor(_send, _recv) {
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._send(jsonRpcRequest(method, params));
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
- _waiters = [];
384
- _lockAcquired = false;
385
- _initialTabCount = 0;
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
- await this._acquireLock();
389
- const timeout = opts.timeout ?? CONNECT_TIMEOUT;
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((msg) => { if (this._proc?.stdin?.writable)
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
- const waiter = this._waiters.shift();
486
- if (waiter)
487
- waiter(parsed);
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 += text;
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
- const initMsg = jsonRpcRequest('initialize', {
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
- if (typeof tabs === 'string') {
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
- try {
562
- // Close tabs opened during this session (site tabs + extension tabs)
563
- if (this._page && this._proc && !this._proc.killed) {
564
- try {
565
- const tabs = await this._page.tabs();
566
- const tabStr = typeof tabs === 'string' ? tabs : JSON.stringify(tabs);
567
- const allTabs = tabStr.match(/Tab (\d+)/g) || [];
568
- const currentTabCount = allTabs.length;
569
- // Close tabs in reverse order to avoid index shifting issues
570
- // Keep the original tabs that existed before the command started
571
- if (currentTabCount > this._initialTabCount && this._initialTabCount > 0) {
572
- for (let i = currentTabCount - 1; i >= this._initialTabCount; i--) {
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(i);
619
+ await this._page.closeTab(index);
575
620
  }
576
621
  catch { }
577
622
  }
578
623
  }
624
+ catch { }
579
625
  }
580
- catch { }
581
- }
582
- if (this._proc && !this._proc.killed) {
583
- this._proc.kill('SIGTERM');
584
- await new Promise((res) => { this._proc?.on('exit', () => res()); setTimeout(res, 3000); });
585
- }
586
- }
587
- finally {
588
- this._page = null;
589
- this._releaseLock();
590
- PlaywrightMCP._activeInsts.delete(this);
591
- }
592
- }
593
- async _acquireLock() {
594
- const start = Date.now();
595
- while (true) {
596
- try {
597
- fs.mkdirSync(LOCK_DIR, { recursive: false });
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
- _releaseLock() {
617
- if (this._lockAcquired) {
618
- try {
619
- fs.rmdirSync(LOCK_DIR);
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
- catch { }
622
- this._lockAcquired = false;
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
- return localMcp;
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
- return projectMcp;
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
- return result;
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
- return result;
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
- return found;
785
+ if (found) {
786
+ _cachedMcpServerPath = found;
787
+ return _cachedMcpServerPath;
788
+ }
664
789
  }
665
790
  catch { }
666
791
  }
667
- return null;
792
+ _cachedMcpServerPath = null;
793
+ return _cachedMcpServerPath;
668
794
  }
@@ -1,43 +1,56 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { formatBrowserConnectError, getTokenFingerprint } from './browser.js';
3
- describe('getTokenFingerprint', () => {
4
- it('returns null for empty token', () => {
5
- expect(getTokenFingerprint(undefined)).toBeNull();
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('returns stable short fingerprint for token', () => {
8
- expect(getTokenFingerprint('abc123')).toBe('6ca13d52');
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('formatBrowserConnectError', () => {
12
- it('explains missing extension token clearly', () => {
13
- const err = formatBrowserConnectError({
14
- kind: 'missing-token',
15
- mode: 'extension',
16
- timeout: 30,
17
- hasExtensionToken: false,
18
- });
19
- expect(err.message).toContain('PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set');
20
- expect(err.message).toContain('manual approval dialog');
21
- });
22
- it('mentions token mismatch as likely cause for extension timeout', () => {
23
- const err = formatBrowserConnectError({
24
- kind: 'extension-timeout',
25
- mode: 'extension',
26
- timeout: 30,
27
- hasExtensionToken: true,
28
- tokenFingerprint: 'deadbeef',
29
- });
30
- expect(err.message).toContain('does not match the token currently shown by the browser extension');
31
- expect(err.message).toContain('deadbeef');
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackwener/opencli",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },