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.
- package/dist/aria-snapshot.d.ts +7 -3
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +76 -18
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/aria-snapshot.test.js +94 -0
- package/dist/aria-snapshot.test.js.map +1 -1
- package/dist/bippy.js +1 -1
- package/dist/browser-config.d.ts +12 -1
- package/dist/browser-config.d.ts.map +1 -1
- package/dist/browser-config.js +101 -38
- package/dist/browser-config.js.map +1 -1
- package/dist/browser-launch.d.ts +17 -0
- package/dist/browser-launch.d.ts.map +1 -0
- package/dist/browser-launch.js +44 -0
- package/dist/browser-launch.js.map +1 -0
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +34 -10
- package/dist/cdp-relay.js.map +1 -1
- package/dist/channel-owner-inspect.test.d.ts +2 -0
- package/dist/channel-owner-inspect.test.d.ts.map +1 -0
- package/dist/channel-owner-inspect.test.js +75 -0
- package/dist/channel-owner-inspect.test.js.map +1 -0
- package/dist/chrome-discovery.d.ts +65 -0
- package/dist/chrome-discovery.d.ts.map +1 -0
- package/dist/chrome-discovery.js +165 -0
- package/dist/chrome-discovery.js.map +1 -0
- package/dist/chrome-discovery.test.d.ts +2 -0
- package/dist/chrome-discovery.test.d.ts.map +1 -0
- package/dist/chrome-discovery.test.js +41 -0
- package/dist/chrome-discovery.test.js.map +1 -0
- package/dist/cli.js +357 -53
- package/dist/cli.js.map +1 -1
- package/dist/executor.d.ts +44 -11
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +121 -55
- package/dist/executor.js.map +1 -1
- package/dist/executor.unit.test.js +29 -1
- package/dist/executor.unit.test.js.map +1 -1
- package/dist/extension/background.js +1704 -0
- package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
- package/dist/extension/icons/icon-black-128.png +0 -0
- package/dist/extension/icons/icon-black-16.png +0 -0
- package/dist/extension/icons/icon-black-32.png +0 -0
- package/dist/extension/icons/icon-black-48.png +0 -0
- package/dist/extension/icons/icon-gray-128.png +0 -0
- package/dist/extension/icons/icon-gray-16.png +0 -0
- package/dist/extension/icons/icon-gray-32.png +0 -0
- package/dist/extension/icons/icon-gray-48.png +0 -0
- package/dist/extension/icons/icon-green-128.png +0 -0
- package/dist/extension/icons/icon-green-16.png +0 -0
- package/dist/extension/icons/icon-green-32.png +0 -0
- package/dist/extension/icons/icon-green-48.png +0 -0
- package/dist/extension/icons/playwriter-icon-black.png +0 -0
- package/dist/extension/icons/playwriter-icon-gray-disabled.png +0 -0
- package/dist/extension/icons/playwriter-icon-gray-disabled.svg +1 -0
- package/dist/extension/manifest.json +40 -0
- package/dist/extension/offscreen.js +214 -0
- package/dist/extension/src/offscreen.html +9 -0
- package/dist/extension/src/prism-bash.min.js +1 -0
- package/dist/extension/src/prism.min.js +1 -0
- package/dist/extension/src/welcome.html +399 -0
- package/dist/kitty-graphics.d.ts +22 -0
- package/dist/kitty-graphics.d.ts.map +1 -0
- package/dist/kitty-graphics.js +71 -0
- package/dist/kitty-graphics.js.map +1 -0
- package/dist/kitty-graphics.test.d.ts +2 -0
- package/dist/kitty-graphics.test.d.ts.map +1 -0
- package/dist/kitty-graphics.test.js +125 -0
- package/dist/kitty-graphics.test.js.map +1 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +76 -12
- package/dist/mcp.js.map +1 -1
- package/dist/package-paths.d.ts +3 -0
- package/dist/package-paths.d.ts.map +1 -0
- package/dist/package-paths.js +29 -0
- package/dist/package-paths.js.map +1 -0
- package/dist/prompt.md +4 -2
- package/dist/readability.js +1 -1
- package/dist/relay-core.test.js +14 -4
- package/dist/relay-core.test.js.map +1 -1
- package/dist/relay-navigation.test.d.ts.map +1 -1
- package/dist/relay-navigation.test.js +95 -1
- package/dist/relay-navigation.test.js.map +1 -1
- package/dist/scoped-fs.d.ts +5 -4
- package/dist/scoped-fs.d.ts.map +1 -1
- package/dist/scoped-fs.js +11 -9
- package/dist/scoped-fs.js.map +1 -1
- package/dist/scoped-fs.test.d.ts +5 -0
- package/dist/scoped-fs.test.d.ts.map +1 -0
- package/dist/scoped-fs.test.js +70 -0
- package/dist/scoped-fs.test.js.map +1 -0
- package/dist/screen-recording.d.ts +3 -6
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +7 -21
- package/dist/screen-recording.js.map +1 -1
- package/dist/selector-generator.js +1 -1
- package/dist/snapshot-tools.test.js +3 -4
- package/dist/snapshot-tools.test.js.map +1 -1
- package/dist/test-utils.d.ts +3 -1
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +24 -12
- package/dist/test-utils.js.map +1 -1
- package/package.json +6 -5
- package/src/aria-snapshot.test.ts +110 -0
- package/src/aria-snapshot.ts +86 -19
- package/src/aria-snapshots/contenteditable-interactive.txt +7 -0
- package/src/aria-snapshots/contenteditable-raw.txt +14 -0
- package/src/aria-snapshots/github-interactive.txt +0 -2
- package/src/aria-snapshots/github-raw.txt +0 -4
- package/src/aria-snapshots/hackernews-interactive.txt +240 -237
- package/src/aria-snapshots/hackernews-raw.txt +262 -258
- package/src/aria-snapshots/prosemirror-interactive.txt +63 -0
- package/src/aria-snapshots/prosemirror-raw.txt +141 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/browser-config.ts +143 -37
- package/src/browser-launch.ts +75 -0
- package/src/cdp-relay.ts +46 -11
- package/src/channel-owner-inspect.test.ts +94 -0
- package/src/chrome-discovery.test.ts +50 -0
- package/src/chrome-discovery.ts +211 -0
- package/src/cli.ts +432 -59
- package/src/executor.ts +171 -72
- package/src/executor.unit.test.ts +32 -1
- package/src/kitty-graphics.test.ts +139 -0
- package/src/kitty-graphics.ts +79 -0
- package/src/mcp.ts +82 -12
- package/src/package-paths.ts +36 -0
- package/src/relay-core.test.ts +14 -4
- package/src/relay-navigation.test.ts +106 -1
- package/src/scoped-fs.test.ts +77 -0
- package/src/scoped-fs.ts +12 -9
- package/src/screen-recording.ts +7 -22
- package/src/skill.md +32 -2
- package/src/snapshot-tools.test.ts +3 -4
- package/src/snapshots/shadcn-ui-accessibility-full.md +181 -183
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +119 -121
- 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"]
|
|
Binary file
|
package/src/browser-config.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
'
|
|
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/
|
|
76
|
+
'~/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
33
77
|
]
|
|
34
78
|
}
|
|
35
|
-
|
|
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
|
-
'
|
|
38
|
-
'
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
94
|
+
|
|
45
95
|
return [
|
|
46
|
-
'/
|
|
47
|
-
'/usr/bin/
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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) => {
|