playwriter 0.0.89 → 0.0.103

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.
Files changed (137) hide show
  1. package/dist/aria-snapshot.d.ts +7 -3
  2. package/dist/aria-snapshot.d.ts.map +1 -1
  3. package/dist/aria-snapshot.js +76 -18
  4. package/dist/aria-snapshot.js.map +1 -1
  5. package/dist/aria-snapshot.test.js +94 -0
  6. package/dist/aria-snapshot.test.js.map +1 -1
  7. package/dist/bippy.js +1 -1
  8. package/dist/browser-config.d.ts +12 -1
  9. package/dist/browser-config.d.ts.map +1 -1
  10. package/dist/browser-config.js +101 -38
  11. package/dist/browser-config.js.map +1 -1
  12. package/dist/browser-launch.d.ts +17 -0
  13. package/dist/browser-launch.d.ts.map +1 -0
  14. package/dist/browser-launch.js +44 -0
  15. package/dist/browser-launch.js.map +1 -0
  16. package/dist/cdp-relay.d.ts.map +1 -1
  17. package/dist/cdp-relay.js +34 -10
  18. package/dist/cdp-relay.js.map +1 -1
  19. package/dist/channel-owner-inspect.test.d.ts +2 -0
  20. package/dist/channel-owner-inspect.test.d.ts.map +1 -0
  21. package/dist/channel-owner-inspect.test.js +75 -0
  22. package/dist/channel-owner-inspect.test.js.map +1 -0
  23. package/dist/chrome-discovery.d.ts +65 -0
  24. package/dist/chrome-discovery.d.ts.map +1 -0
  25. package/dist/chrome-discovery.js +165 -0
  26. package/dist/chrome-discovery.js.map +1 -0
  27. package/dist/chrome-discovery.test.d.ts +2 -0
  28. package/dist/chrome-discovery.test.d.ts.map +1 -0
  29. package/dist/chrome-discovery.test.js +41 -0
  30. package/dist/chrome-discovery.test.js.map +1 -0
  31. package/dist/cli.js +357 -53
  32. package/dist/cli.js.map +1 -1
  33. package/dist/executor.d.ts +44 -11
  34. package/dist/executor.d.ts.map +1 -1
  35. package/dist/executor.js +121 -55
  36. package/dist/executor.js.map +1 -1
  37. package/dist/executor.unit.test.js +29 -1
  38. package/dist/executor.unit.test.js.map +1 -1
  39. package/dist/extension/background.js +1704 -0
  40. package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
  41. package/dist/extension/icons/icon-black-128.png +0 -0
  42. package/dist/extension/icons/icon-black-16.png +0 -0
  43. package/dist/extension/icons/icon-black-32.png +0 -0
  44. package/dist/extension/icons/icon-black-48.png +0 -0
  45. package/dist/extension/icons/icon-gray-128.png +0 -0
  46. package/dist/extension/icons/icon-gray-16.png +0 -0
  47. package/dist/extension/icons/icon-gray-32.png +0 -0
  48. package/dist/extension/icons/icon-gray-48.png +0 -0
  49. package/dist/extension/icons/icon-green-128.png +0 -0
  50. package/dist/extension/icons/icon-green-16.png +0 -0
  51. package/dist/extension/icons/icon-green-32.png +0 -0
  52. package/dist/extension/icons/icon-green-48.png +0 -0
  53. package/dist/extension/icons/playwriter-icon-black.png +0 -0
  54. package/dist/extension/icons/playwriter-icon-gray-disabled.png +0 -0
  55. package/dist/extension/icons/playwriter-icon-gray-disabled.svg +1 -0
  56. package/dist/extension/manifest.json +40 -0
  57. package/dist/extension/offscreen.js +214 -0
  58. package/dist/extension/src/offscreen.html +9 -0
  59. package/dist/extension/src/prism-bash.min.js +1 -0
  60. package/dist/extension/src/prism.min.js +1 -0
  61. package/dist/extension/src/welcome.html +399 -0
  62. package/dist/kitty-graphics.d.ts +22 -0
  63. package/dist/kitty-graphics.d.ts.map +1 -0
  64. package/dist/kitty-graphics.js +71 -0
  65. package/dist/kitty-graphics.js.map +1 -0
  66. package/dist/kitty-graphics.test.d.ts +2 -0
  67. package/dist/kitty-graphics.test.d.ts.map +1 -0
  68. package/dist/kitty-graphics.test.js +125 -0
  69. package/dist/kitty-graphics.test.js.map +1 -0
  70. package/dist/mcp.d.ts.map +1 -1
  71. package/dist/mcp.js +76 -12
  72. package/dist/mcp.js.map +1 -1
  73. package/dist/package-paths.d.ts +3 -0
  74. package/dist/package-paths.d.ts.map +1 -0
  75. package/dist/package-paths.js +29 -0
  76. package/dist/package-paths.js.map +1 -0
  77. package/dist/prompt.md +4 -2
  78. package/dist/readability.js +1 -1
  79. package/dist/relay-core.test.js +14 -4
  80. package/dist/relay-core.test.js.map +1 -1
  81. package/dist/relay-navigation.test.d.ts.map +1 -1
  82. package/dist/relay-navigation.test.js +95 -1
  83. package/dist/relay-navigation.test.js.map +1 -1
  84. package/dist/scoped-fs.d.ts +5 -4
  85. package/dist/scoped-fs.d.ts.map +1 -1
  86. package/dist/scoped-fs.js +11 -9
  87. package/dist/scoped-fs.js.map +1 -1
  88. package/dist/scoped-fs.test.d.ts +5 -0
  89. package/dist/scoped-fs.test.d.ts.map +1 -0
  90. package/dist/scoped-fs.test.js +70 -0
  91. package/dist/scoped-fs.test.js.map +1 -0
  92. package/dist/screen-recording.d.ts +3 -6
  93. package/dist/screen-recording.d.ts.map +1 -1
  94. package/dist/screen-recording.js +7 -21
  95. package/dist/screen-recording.js.map +1 -1
  96. package/dist/selector-generator.js +1 -1
  97. package/dist/snapshot-tools.test.js +3 -4
  98. package/dist/snapshot-tools.test.js.map +1 -1
  99. package/dist/test-utils.d.ts +3 -1
  100. package/dist/test-utils.d.ts.map +1 -1
  101. package/dist/test-utils.js +24 -12
  102. package/dist/test-utils.js.map +1 -1
  103. package/package.json +6 -5
  104. package/src/aria-snapshot.test.ts +110 -0
  105. package/src/aria-snapshot.ts +86 -19
  106. package/src/aria-snapshots/contenteditable-interactive.txt +7 -0
  107. package/src/aria-snapshots/contenteditable-raw.txt +14 -0
  108. package/src/aria-snapshots/github-interactive.txt +0 -2
  109. package/src/aria-snapshots/github-raw.txt +0 -4
  110. package/src/aria-snapshots/hackernews-interactive.txt +240 -237
  111. package/src/aria-snapshots/hackernews-raw.txt +262 -258
  112. package/src/aria-snapshots/prosemirror-interactive.txt +63 -0
  113. package/src/aria-snapshots/prosemirror-raw.txt +141 -0
  114. package/src/assets/aria-labels-hacker-news.png +0 -0
  115. package/src/browser-config.ts +143 -37
  116. package/src/browser-launch.ts +75 -0
  117. package/src/cdp-relay.ts +46 -11
  118. package/src/channel-owner-inspect.test.ts +94 -0
  119. package/src/chrome-discovery.test.ts +50 -0
  120. package/src/chrome-discovery.ts +211 -0
  121. package/src/cli.ts +432 -59
  122. package/src/executor.ts +171 -72
  123. package/src/executor.unit.test.ts +32 -1
  124. package/src/kitty-graphics.test.ts +139 -0
  125. package/src/kitty-graphics.ts +79 -0
  126. package/src/mcp.ts +82 -12
  127. package/src/package-paths.ts +36 -0
  128. package/src/relay-core.test.ts +14 -4
  129. package/src/relay-navigation.test.ts +106 -1
  130. package/src/scoped-fs.test.ts +77 -0
  131. package/src/scoped-fs.ts +12 -9
  132. package/src/screen-recording.ts +7 -22
  133. package/src/skill.md +32 -2
  134. package/src/snapshot-tools.test.ts +3 -4
  135. package/src/snapshots/shadcn-ui-accessibility-full.md +181 -183
  136. package/src/snapshots/shadcn-ui-accessibility-interactive.md +119 -121
  137. package/src/test-utils.ts +25 -11
@@ -0,0 +1,63 @@
1
+ - banner:
2
+ - navigation:
3
+ - role=link[name="ProseMirror"]
4
+ - role=link[name="Examples"]
5
+ - role=link[name="Documentation"]
6
+ - role=link[name="Discuss"] >> nth=0
7
+ - role=link[name="Code"]
8
+ - role=button[name="Toggle strong style"]
9
+ - role=button[name="Toggle emphasis"]
10
+ - role=button[name="Toggle code font"]
11
+ - role=button[name="Add or remove link"]
12
+ - role=button[name="Insert"]
13
+ - role=button[name="Type..."]
14
+ - role=button[name="Undo last change"]
15
+ - role=button[name="Redo last undone change"]
16
+ - role=button[name="Wrap in bullet list"]
17
+ - role=button[name="Wrap in ordered list"]
18
+ - role=button[name="Wrap in block quote"]
19
+ - role=button[name="⬚"]
20
+ - textbox [contenteditable="true"]:
21
+ - role=link[name="examples"] >> nth=0
22
+ - role=link[name="Markdown"]
23
+ - role=link[name="WYSIWYG"]
24
+ - list:
25
+ - listitem
26
+ - listitem
27
+ - listitem
28
+ - listitem
29
+ - listitem
30
+ - listitem
31
+ - role=link[name="open source"]
32
+ - role=link[name="Start here."]
33
+ - role=link[name="Forgejo"]
34
+ - role=link[name="welcome"]
35
+ - role=link[name="npm"]
36
+ - role=link[name="examples"] >> nth=1
37
+ - role=link[name="forum"]
38
+ - role=link[name="issue tracker"]
39
+ - role=link[name="code of conduct"]
40
+ - role=link[name="crowd-funded"]
41
+ - role=link[name="individuals and organizations"]
42
+ - role=link[name="Posit"]
43
+ - role=link[name="The New York Times"]
44
+ - role=link[name="Tiptap"]
45
+ - role=link[name="Progress"]
46
+ - role=link[name="Scripto"]
47
+ - role=link[name="Asana"]
48
+ - role=link[name="BlockNote"]
49
+ - role=link[name="Box"]
50
+ - role=link[name="Desmos"]
51
+ - role=link[name="Nextjournal"]
52
+ - role=link[name="Plickers"]
53
+ - role=link[name="The Guardian"]
54
+ - role=link[name="Yarn"]
55
+ - role=link[name="Fellow"]
56
+ - role=link[name="Sunsama"]
57
+ - contentinfo:
58
+ - navigation:
59
+ - role=link
60
+ - role=link[name="Backers"]
61
+ - role=link[name="Code of Conduct"]
62
+ - role=link[name="Discuss"] >> nth=1
63
+ - role=link[name="Report an Issue"]
@@ -0,0 +1,141 @@
1
+ - banner:
2
+ - navigation:
3
+ - role=link[name="ProseMirror"]
4
+ - role=link[name="Examples"]
5
+ - role=link[name="Documentation"]
6
+ - role=link[name="Discuss"] >> nth=0
7
+ - role=link[name="Code"]
8
+ - heading "A toolkit for building rich-text editors on the web"
9
+ - toolbar:
10
+ - role=button[name="Toggle strong style"]
11
+ - role=button[name="Toggle emphasis"]
12
+ - role=button[name="Toggle code font"]
13
+ - role=button[name="Add or remove link"]
14
+ - role=button[name="Insert"]
15
+ - role=button[name="Type..."]
16
+ - role=button[name="Undo last change"]
17
+ - role=button[name="Redo last undone change"]
18
+ - role=button[name="Wrap in bullet list"]
19
+ - role=button[name="Wrap in ordered list"]
20
+ - role=button[name="Wrap in block quote"]
21
+ - role=button[name="⬚"]
22
+ - textbox [contenteditable="true"]:
23
+ - paragraph:
24
+ - text: "Like this one!"
25
+ - paragraph:
26
+ - text: "Try it out by typing in here or see more"
27
+ - role=link[name="examples"] >> nth=0
28
+ - text: "."
29
+ - article:
30
+ - paragraph:
31
+ - text: "Rich content editors should, ideally, produce clean, semantically meaningful documents while still being easy for users to understand. ProseMirror tries to bridge the gap between editing explicit, unambiguous content like"
32
+ - role=link[name="Markdown"]
33
+ - text: "or XML, and classical"
34
+ - role=link[name="WYSIWYG"]
35
+ - text: "editors."
36
+ - paragraph:
37
+ - text: "It does this by implementing a WYSIWYG-style editing interface for documents more constrained and structured than plain HTML. You can customize the shape and structure of the documents your editor creates, and tailor them to your application's needs."
38
+ - heading "Features"
39
+ - list:
40
+ - listitem:
41
+ - heading "Collaborative editing"
42
+ - paragraph:
43
+ - text: "ProseMirror has built-in, ground-up, rock solid support for collaborative editing, where multiple people work on the same document in real time."
44
+ - listitem:
45
+ - heading "Extensible schemas"
46
+ - paragraph:
47
+ - text: "Document schemas allow editing documents with a custom structure without writing your own editor from scratch."
48
+ - listitem:
49
+ - heading "Modular"
50
+ - paragraph:
51
+ - text: "A modular architecture makes sure you only load the code you need, and can replace parts of the system as needed."
52
+ - listitem:
53
+ - heading "Pluggable"
54
+ - paragraph:
55
+ - text: "A plugin system allows you to easily enable additional functionality, and package your own extensions in a convenient format."
56
+ - listitem:
57
+ - heading "Functional"
58
+ - paragraph:
59
+ - text: "A largely functional and immutable architecture makes it easy to integrate ProseMirror into modern web apps, and confidently implement complex behavior."
60
+ - listitem:
61
+ - heading "Unopinionated"
62
+ - paragraph:
63
+ - text: "The core library is small and generic, providing a base onto which very different types of editors can be built."
64
+ - heading "About"
65
+ - paragraph:
66
+ - text: "ProseMirror is"
67
+ - role=link[name="open source"]
68
+ - text: ", and you are legally free to use it commercially. Yet, writing, maintaining, supporting, and setting up infrastructure for such a project takes a lot of work and energy. Therefore..."
69
+ - paragraph:
70
+ - text: "If you are using ProseMirror to make profit, there is a"
71
+ - text: "social"
72
+ - text: "expectation that you help fund its maintenance."
73
+ - role=link[name="Start here."]
74
+ - paragraph:
75
+ - text: "ProseMirror is being developed on"
76
+ - role=link[name="Forgejo"]
77
+ - text: ". Contributions are"
78
+ - role=link[name="welcome"]
79
+ - text: "."
80
+ - paragraph:
81
+ - text: "The easiest way to install ProseMirror is with"
82
+ - role=link[name="npm"]
83
+ - text: ". Take a look at the"
84
+ - role=link[name="examples"] >> nth=1
85
+ - text: "to get started."
86
+ - paragraph:
87
+ - text: "The library works on recent versions of Firefox, Chrome, Safari, and Edge, as well as on Internet Explorer 11."
88
+ - paragraph:
89
+ - text: "Discussing the project, or asking questions, is best done on the"
90
+ - role=link[name="forum"]
91
+ - text: ". Bugs should be reported through the"
92
+ - role=link[name="issue tracker"]
93
+ - text: ". We aim to be an inclusive, welcoming community. To make that explicit, we have a"
94
+ - role=link[name="code of conduct"]
95
+ - text: "that applies to communication around the project."
96
+ - heading "Sponsors"
97
+ - paragraph:
98
+ - text: "Initial development of the software was"
99
+ - role=link[name="crowd-funded"]
100
+ - text: "by these 414 excellent"
101
+ - role=link[name="individuals and organizations"]
102
+ - text: "."
103
+ - paragraph:
104
+ - text: "Ongoing development is supported by these companies:"
105
+ - role=link[name="Posit"]:
106
+ - image "Posit"
107
+ - role=link[name="The New York Times"]:
108
+ - image "The New York Times"
109
+ - role=link[name="Tiptap"]:
110
+ - image "Tiptap"
111
+ - role=link[name="Progress"]:
112
+ - image "Progress"
113
+ - role=link[name="Scripto"]:
114
+ - image "Scripto"
115
+ - role=link[name="Asana"]:
116
+ - image "Asana"
117
+ - role=link[name="BlockNote"]:
118
+ - image "BlockNote"
119
+ - role=link[name="Box"]:
120
+ - image "Box"
121
+ - role=link[name="Desmos"]:
122
+ - image "Desmos"
123
+ - role=link[name="Nextjournal"]:
124
+ - image "Nextjournal"
125
+ - role=link[name="Plickers"]:
126
+ - image "Plickers"
127
+ - role=link[name="The Guardian"]:
128
+ - image "The Guardian"
129
+ - role=link[name="Yarn"]:
130
+ - image "Yarn"
131
+ - role=link[name="Fellow"]:
132
+ - image "Fellow"
133
+ - role=link[name="Sunsama"]:
134
+ - image "Sunsama"
135
+ - contentinfo:
136
+ - navigation:
137
+ - role=link
138
+ - role=link[name="Backers"]
139
+ - role=link[name="Code of Conduct"]
140
+ - role=link[name="Discuss"] >> nth=1
141
+ - role=link[name="Report an Issue"]
@@ -1,64 +1,170 @@
1
1
  import fs from 'node:fs'
2
2
  import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { chromium } from '@xmorse/playwright-core'
3
5
 
4
- // Function to get the browser executable path
5
- // Can be overridden by environment variable PLAYWRITER_BROWSER_PATH
6
- export function getBrowserExecutablePath(): string {
7
- // Check environment variable first
8
- const envPath = process.env.PLAYWRITER_BROWSER_PATH
9
- if (envPath && fs.existsSync(envPath)) {
10
- return envPath
6
+ type BrowserLookupOptions = {
7
+ browserPath?: string
8
+ env?: NodeJS.ProcessEnv
9
+ platform?: NodeJS.Platform
10
+ homeDir?: string
11
+ existsSync?: (filePath: string) => boolean
12
+ }
13
+
14
+ function expandHomeDirectory({ filePath, homeDir }: { filePath: string; homeDir: string }): string {
15
+ if (!filePath.startsWith('~/')) {
16
+ return filePath
17
+ }
18
+ return path.join(homeDir, filePath.slice(2))
19
+ }
20
+
21
+ function dedupePaths(pathsToCheck: string[]): string[] {
22
+ return Array.from(new Set(pathsToCheck))
23
+ }
24
+
25
+ function getPlaywrightChromiumCandidate(): string[] {
26
+ const executablePath = chromium.executablePath()
27
+ if (!executablePath) {
28
+ return []
11
29
  }
30
+ return [executablePath]
31
+ }
12
32
 
13
- // Check for Ghost Browser on macOS
14
- // const ghostBrowserPath = '/Applications/Ghost Browser.app/Contents/MacOS/Ghost Browser'
15
- // if (fs.existsSync(ghostBrowserPath)) {
16
- // return ghostBrowserPath
17
- // }
33
+ function getPathEntries(env: NodeJS.ProcessEnv): string[] {
34
+ const rawPath = env.PATH || env.Path || ''
35
+ return rawPath
36
+ .split(path.delimiter)
37
+ .filter(Boolean)
38
+ .map((entry) => {
39
+ return entry.trim()
40
+ })
41
+ }
42
+
43
+ function getExecutableNames(platform: NodeJS.Platform): string[] {
44
+ if (platform === 'win32') {
45
+ return ['chrome.exe', 'chromium.exe']
46
+ }
47
+ return ['chrome', 'chromium', 'chromium-browser']
48
+ }
18
49
 
19
- // Fall back to finding Chrome
20
- return findChromeExecutablePath()
50
+ function getPathExecutableCandidates({
51
+ platform,
52
+ env,
53
+ }: {
54
+ platform: NodeJS.Platform
55
+ env: NodeJS.ProcessEnv
56
+ }): string[] {
57
+ const executableNames = getExecutableNames(platform)
58
+ return getPathEntries(env).flatMap((entry) => {
59
+ return executableNames.map((name) => {
60
+ return path.join(entry, name)
61
+ })
62
+ })
21
63
  }
22
64
 
23
- // Original Chrome finding logic
24
- function findChromeExecutablePath(): string {
25
- const osPlatform = os.platform()
26
- const paths = (() => {
27
- if (osPlatform === 'darwin') {
65
+ export function getBrowserExecutableCandidates({
66
+ platform = os.platform(),
67
+ env = process.env,
68
+ homeDir = os.homedir(),
69
+ }: Omit<BrowserLookupOptions, 'browserPath' | 'existsSync'> = {}): string[] {
70
+ const platformCandidates = (() => {
71
+ if (platform === 'darwin') {
28
72
  return [
29
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
30
- '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
73
+ '/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
74
+ '~/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
31
75
  '/Applications/Chromium.app/Contents/MacOS/Chromium',
32
- '~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
76
+ '~/Applications/Chromium.app/Contents/MacOS/Chromium',
33
77
  ]
34
78
  }
35
- if (osPlatform === 'win32') {
79
+
80
+ if (platform === 'win32') {
81
+ const localAppData = env.LOCALAPPDATA || ''
82
+ const programFiles = env.PROGRAMFILES || 'C:\\Program Files'
83
+ const programFilesX86 = env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)'
84
+
36
85
  return [
37
- 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
38
- 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
39
- `${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
40
- `${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`,
41
- `${process.env['PROGRAMFILES(X86)']}\\Google\\Chrome\\Application\\chrome.exe`,
42
- ].filter(Boolean)
86
+ path.join(programFiles, 'Google', 'Chrome for Testing', 'Application', 'chrome.exe'),
87
+ path.join(programFilesX86, 'Google', 'Chrome for Testing', 'Application', 'chrome.exe'),
88
+ path.join(localAppData, 'Google', 'Chrome for Testing', 'Application', 'chrome.exe'),
89
+ path.join(programFiles, 'Chromium', 'Application', 'chromium.exe'),
90
+ path.join(programFilesX86, 'Chromium', 'Application', 'chromium.exe'),
91
+ path.join(localAppData, 'Chromium', 'Application', 'chromium.exe'),
92
+ ]
43
93
  }
44
- // Linux
94
+
45
95
  return [
46
- '/usr/bin/google-chrome',
47
- '/usr/bin/google-chrome-stable',
96
+ '/opt/google/chrome-for-testing/chrome',
97
+ '/usr/local/bin/chrome',
98
+ '/usr/bin/chrome',
48
99
  '/usr/bin/chromium',
49
100
  '/usr/bin/chromium-browser',
50
101
  '/snap/bin/chromium',
51
102
  ]
52
103
  })()
53
104
 
54
- for (const path of paths) {
55
- const resolvedPath = path.startsWith('~') ? path.replace('~', os.homedir()) : path
56
- if (fs.existsSync(resolvedPath)) {
57
- return resolvedPath
105
+ const pathCandidates = getPathExecutableCandidates({ platform, env })
106
+ return dedupePaths(
107
+ [...platformCandidates, ...pathCandidates, ...getPlaywrightChromiumCandidate()].map((filePath) => {
108
+ return expandHomeDirectory({ filePath, homeDir })
109
+ }),
110
+ )
111
+ }
112
+
113
+ export function resolveBrowserExecutablePath({
114
+ browserPath,
115
+ env = process.env,
116
+ platform = os.platform(),
117
+ homeDir = os.homedir(),
118
+ existsSync = fs.existsSync,
119
+ }: BrowserLookupOptions = {}): string {
120
+ const explicitPath = browserPath?.trim()
121
+ if (explicitPath) {
122
+ const resolvedExplicitPath = expandHomeDirectory({ filePath: explicitPath, homeDir })
123
+ if (!existsSync(resolvedExplicitPath)) {
124
+ throw new Error(`Browser binary not found at: ${resolvedExplicitPath}`)
125
+ }
126
+ return resolvedExplicitPath
127
+ }
128
+
129
+ const envPath = env.PLAYWRITER_BROWSER_PATH?.trim()
130
+ if (envPath) {
131
+ const resolvedEnvPath = expandHomeDirectory({ filePath: envPath, homeDir })
132
+ if (!existsSync(resolvedEnvPath)) {
133
+ throw new Error(`PLAYWRITER_BROWSER_PATH does not exist: ${resolvedEnvPath}`)
58
134
  }
135
+ return resolvedEnvPath
136
+ }
137
+
138
+ const candidates = getBrowserExecutableCandidates({ platform, env, homeDir })
139
+ const resolvedPath = candidates.find((candidate) => {
140
+ return existsSync(candidate)
141
+ })
142
+
143
+ if (resolvedPath) {
144
+ return resolvedPath
59
145
  }
60
146
 
147
+ const searchedPathsText = candidates.map((candidate) => {
148
+ return `- ${candidate}`
149
+ })
150
+
61
151
  throw new Error(
62
- 'Could not find Chrome executable. Please install Google Chrome or set PLAYWRITER_BROWSER_PATH environment variable.',
152
+ 'Could not find a supported browser binary. Install Chrome for Testing or Chromium, or pass a binary path to `playwriter browser start`.' +
153
+ `\n\nSearched paths:\n${searchedPathsText.join('\n')}`,
63
154
  )
64
155
  }
156
+
157
+ export function shouldUseHeadlessByDefault({
158
+ platform = os.platform(),
159
+ env = process.env,
160
+ }: Omit<BrowserLookupOptions, 'browserPath' | 'homeDir' | 'existsSync'> = {}): boolean {
161
+ if (platform !== 'linux') {
162
+ return false
163
+ }
164
+
165
+ return !env.DISPLAY && !env.WAYLAND_DISPLAY
166
+ }
167
+
168
+ export function getBrowserExecutablePath(browserPath?: string): string {
169
+ return resolveBrowserExecutablePath({ browserPath })
170
+ }
@@ -0,0 +1,75 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { spawn } from 'node:child_process'
5
+ import { EXTENSION_IDS } from './utils.js'
6
+
7
+ export type BrowserLaunchOptions = {
8
+ extensionPath: string
9
+ userDataDir: string
10
+ headless: boolean
11
+ noSandbox?: boolean
12
+ url?: string
13
+ }
14
+
15
+ export function getDefaultBrowserUserDataDir(): string {
16
+ return path.join(os.homedir(), '.playwriter', 'browser-profile')
17
+ }
18
+
19
+ export function getBrowserLaunchArgs({
20
+ extensionPath,
21
+ userDataDir,
22
+ headless,
23
+ noSandbox = false,
24
+ url = 'about:blank',
25
+ }: BrowserLaunchOptions): string[] {
26
+ const recordingFlags = EXTENSION_IDS.map((extensionId) => {
27
+ return `--allowlisted-extension-id=${extensionId}`
28
+ })
29
+
30
+ const args = [
31
+ `--user-data-dir=${path.resolve(userDataDir)}`,
32
+ '--profile-directory=Default',
33
+ '--no-first-run',
34
+ '--no-default-browser-check',
35
+ '--auto-accept-this-tab-capture',
36
+ ...recordingFlags,
37
+ `--disable-extensions-except=${path.resolve(extensionPath)}`,
38
+ `--load-extension=${path.resolve(extensionPath)}`,
39
+ ]
40
+
41
+ if (headless) {
42
+ args.push('--headless=new')
43
+ }
44
+
45
+ if (noSandbox) {
46
+ args.push('--no-sandbox', '--disable-setuid-sandbox')
47
+ }
48
+
49
+ args.push(url)
50
+ return args
51
+ }
52
+
53
+ export function startBrowserProcess({
54
+ browserPath,
55
+ args,
56
+ userDataDir,
57
+ }: {
58
+ browserPath: string
59
+ args: string[]
60
+ userDataDir: string
61
+ }): { pid: number } {
62
+ fs.mkdirSync(path.resolve(userDataDir), { recursive: true })
63
+
64
+ const browserProcess = spawn(browserPath, args, {
65
+ detached: true,
66
+ stdio: 'ignore',
67
+ })
68
+ browserProcess.unref()
69
+
70
+ if (!browserProcess.pid) {
71
+ throw new Error(`Failed to start browser process for ${browserPath}`)
72
+ }
73
+
74
+ return { pid: browserProcess.pid }
75
+ }
package/src/cdp-relay.ts CHANGED
@@ -28,6 +28,7 @@ import { EventEmitter } from 'node:events'
28
28
  import { VERSION, EXTENSION_IDS } from './utils.js'
29
29
  import { createCdpLogger, type CdpLogEntry, type CdpLogger } from './cdp-log.js'
30
30
  import { RecordingRelay } from './recording-relay.js'
31
+ import { appendSessionToWsUrl } from './chrome-discovery.js'
31
32
  import * as relayState from './relay-state.js'
32
33
 
33
34
  /**
@@ -1827,7 +1828,7 @@ export async function startPlayWriterCDPRelayServer({
1827
1828
  const existingExecutor = manager.getSession(sessionId)
1828
1829
  if (!existingExecutor) {
1829
1830
  return c.json(
1830
- { text: `Session ${sessionId} not found. Run 'playwriter session new' first.`, images: [], isError: true },
1831
+ { text: `Session ${sessionId} not found. Run 'playwriter session new' first.`, images: [], screenshots: [], isError: true },
1831
1832
  404,
1832
1833
  )
1833
1834
  }
@@ -1836,7 +1837,7 @@ export async function startPlayWriterCDPRelayServer({
1836
1837
  return c.json(result)
1837
1838
  } catch (error: any) {
1838
1839
  logger?.error('Execute endpoint error:', error)
1839
- return c.json({ text: `Server error: ${error.message}`, images: [], isError: true }, 500)
1840
+ return c.json({ text: `Server error: ${error.message}`, images: [], screenshots: [], isError: true }, 500)
1840
1841
  }
1841
1842
  })
1842
1843
 
@@ -1877,10 +1878,49 @@ export async function startPlayWriterCDPRelayServer({
1877
1878
  })
1878
1879
 
1879
1880
  app.post('/cli/session/new', async (c) => {
1880
- const body = (await c.req.json().catch(() => ({}))) as { extensionId?: string | null; cwd?: string }
1881
+ const body = (await c.req.json().catch(() => ({}))) as {
1882
+ extensionId?: string | null
1883
+ cwd?: string
1884
+ /** Direct CDP WebSocket URL — bypasses extension, connects straight to Chrome */
1885
+ cdpEndpoint?: string
1886
+ /** Browser name from discovery (e.g. "Chrome", "Brave") */
1887
+ browser?: string
1888
+ /** Profile info from discovery */
1889
+ profiles?: Array<{ name: string; email: string }>
1890
+ }
1881
1891
  const sessionId = String(nextSessionNumber++)
1882
- const extensionId = body.extensionId || null
1883
1892
  const cwd = body.cwd
1893
+
1894
+ // Direct CDP mode: skip extension lookup, pass direct WebSocket URL to executor
1895
+ if (body.cdpEndpoint) {
1896
+ if (!body.cdpEndpoint.startsWith('ws://') && !body.cdpEndpoint.startsWith('wss://')) {
1897
+ return c.json({ error: `Invalid cdpEndpoint: must start with ws:// or wss:// (got: ${body.cdpEndpoint})` }, 400)
1898
+ }
1899
+ // Use first profile from discovery for session metadata (if available)
1900
+ const firstProfile = body.profiles?.[0]
1901
+ const manager = await getExecutorManager()
1902
+ const executor = manager.getExecutor({
1903
+ sessionId,
1904
+ cwd,
1905
+ cdpConfig: { directCdpUrl: appendSessionToWsUrl(body.cdpEndpoint, sessionId) },
1906
+ sessionMetadata: {
1907
+ extensionId: null,
1908
+ browser: body.browser || null,
1909
+ profile: firstProfile ? { email: firstProfile.email, id: firstProfile.name } : null,
1910
+ },
1911
+ })
1912
+ const metadata = executor.getSessionMetadata()
1913
+ return c.json({
1914
+ id: sessionId,
1915
+ mode: 'direct' as const,
1916
+ extensionId: metadata.extensionId,
1917
+ browser: metadata.browser,
1918
+ profile: metadata.profile,
1919
+ })
1920
+ }
1921
+
1922
+ // Extension mode (existing behavior)
1923
+ const extensionId = body.extensionId || null
1884
1924
  const allowDefault = !extensionId && store.getState().extensions.size === 1
1885
1925
  const conn = getExtensionConnection(extensionId, { allowFallback: allowDefault })
1886
1926
  if (!conn) {
@@ -1902,6 +1942,7 @@ export async function startPlayWriterCDPRelayServer({
1902
1942
  const metadata = executor.getSessionMetadata()
1903
1943
  return c.json({
1904
1944
  id: sessionId,
1945
+ mode: 'extension' as const,
1905
1946
  extensionId: metadata.extensionId,
1906
1947
  browser: metadata.browser,
1907
1948
  profile: metadata.profile,
@@ -1915,13 +1956,7 @@ export async function startPlayWriterCDPRelayServer({
1915
1956
  if (!executor) {
1916
1957
  return c.json({ error: 'not found' }, 404)
1917
1958
  }
1918
- const metadata = executor.getSessionMetadata()
1919
- return c.json({
1920
- id: sessionId,
1921
- extensionId: metadata.extensionId,
1922
- browser: metadata.browser,
1923
- profile: metadata.profile,
1924
- })
1959
+ return c.json(executor.getSessionInfo({ id: sessionId }))
1925
1960
  })
1926
1961
 
1927
1962
  app.post('/cli/session/delete', async (c) => {