Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fac731ae19 | ||
|
|
2f2beb774a | ||
|
|
a98dc061da | ||
|
|
184ef5670c | ||
|
|
8ab8915429 | ||
|
|
a952dd2c61 | ||
|
|
ebcd52a019 | ||
|
|
b2ddf0db5c | ||
|
|
eaf4b965d5 | ||
|
|
6ba1c754a1 | ||
|
|
468613ab68 | ||
|
|
261d84bd01 | ||
|
|
ea8db593c5 | ||
|
|
130cc2fc10 | ||
|
|
3715da8fd3 | ||
|
|
100e477a95 | ||
|
|
1132c8c256 | ||
|
|
42237fcc70 | ||
|
|
ae8b3b6b64 | ||
|
|
8378491b0b | ||
|
|
20274cf8eb | ||
|
|
d25726fafd | ||
|
|
4f1973d048 | ||
|
|
d69c474655 | ||
|
|
56078307da | ||
|
|
e6291cdb8a | ||
|
|
bf0b2152c5 | ||
|
|
9771723992 | ||
|
|
156e50a864 | ||
|
|
5081fe0776 | ||
|
|
656e43d449 | ||
|
|
d135a3f0af | ||
|
|
7bd880497f | ||
|
|
d148c0d6b9 | ||
|
|
75bab7b425 | ||
|
|
8dfaebba00 | ||
|
|
f7f01dcd2f | ||
|
|
fba85a72c1 | ||
|
|
c657ec57ad | ||
|
|
f24df440d0 | ||
|
|
b73f83a4ab | ||
|
|
2700ff3526 | ||
|
|
a261f2f3bd | ||
|
|
81c4ba0800 | ||
|
|
31e72af9bf | ||
|
|
10a52521d5 | ||
|
|
c9b34ba36c | ||
|
|
87ad8f9d31 | ||
|
|
a754cfc7bb | ||
|
|
29bc0e548b | ||
|
|
378598b161 | ||
|
|
e8c1dda3cc | ||
|
|
fabc4ad54a | ||
|
|
b89ae2bb9e | ||
|
|
fb162eef6c | ||
|
|
dc2840e023 | ||
|
|
fe6d9e8d16 | ||
|
|
7ab7d83fb5 | ||
|
|
028dad2183 | ||
|
|
6b0fe60958 | ||
|
|
a7789cb631 | ||
|
|
da65876a64 | ||
|
|
f245161bfa | ||
|
|
bdffdca420 | ||
|
|
5d1199a133 | ||
|
|
4752b3d096 | ||
|
|
fc9c3700a1 | ||
|
|
a15d6841c5 | ||
|
|
9641f67e3a | ||
|
|
de2fa57c63 | ||
|
|
a658fe7863 | ||
|
|
503fb84f10 | ||
|
|
11bb8f824d | ||
|
|
8615cc6678 | ||
|
|
65e4c27f45 | ||
|
|
f9d8b87768 | ||
|
|
586a0cc86d | ||
|
|
e9cbb40559 | ||
|
|
f2c3962749 | ||
|
|
e63e113628 | ||
|
|
4b02de7358 | ||
|
|
61aab76e63 | ||
|
|
e26e37d9d7 | ||
|
|
d411da2a12 | ||
|
|
15b3c726dc | ||
|
|
ee47c801ea | ||
|
|
d0993d2963 | ||
|
|
85a6506bfd | ||
|
|
7a489e371f | ||
|
|
17b973d77c | ||
|
|
4febb998c3 | ||
|
|
04467316c2 | ||
|
|
cc0b238de9 | ||
|
|
f6904335dc | ||
|
|
2929c418fe | ||
|
|
35f52c89c4 | ||
|
|
3c32d1375b | ||
|
|
200b31ecd8 | ||
|
|
3934397799 | ||
|
|
a36af34f15 | ||
|
|
74613746c7 | ||
|
|
326cf45040 | ||
|
|
58f9e48e3e | ||
|
|
9457d842ca | ||
|
|
2b9bf8cd42 | ||
|
|
3dab2fa901 | ||
|
|
c031f28541 | ||
|
|
a92bad7485 | ||
|
|
af85076ef4 | ||
|
|
1faf311787 | ||
|
|
f568270c9f | ||
|
|
69fc59b546 | ||
|
|
3a4cdbaeb9 | ||
|
|
f111541056 | ||
|
|
4ef59b0355 | ||
|
|
50190da52f | ||
|
|
fc26e12013 |
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(find /c/Users/2jepp/WebstormProjects/pleno-vue -type f \\\\\\(-name *.ts -o -name *.js \\\\\\) -not -path */node_modules* -not -path */dist/* -not -path */.git/* -not -path */dev-dist/*)",
|
||||||
|
"Bash(find /c/Users/2jepp/WebstormProjects/pleno-vue/src -type f \\\\\\(-name *.ts -o -name *.js -o -name *.vue \\\\\\))",
|
||||||
|
"Bash(find /c/Users/2jepp/WebstormProjects/pleno-vue/app -type f \\\\\\(-name *.ts -o -name *.js -o -name *.java -o -name *.kt \\\\\\))",
|
||||||
|
"Bash(grep -r \"getAutomaticFlags\\\\|AutomaticFlags\\\\|automaticFlags\\\\|Automatic.*Flags\" /c/Users/2jepp/WebstormProjects/pleno-vue --include=*.ts --include=*.js --include=*.java --include=*.kt --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=dev-dist --exclude-dir=output --exclude-dir=.git)",
|
||||||
|
"Bash(xargs grep:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.github
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
coverage
|
||||||
|
dev-dist
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
node_modules.*
|
||||||
|
output
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
.ai
|
||||||
|
.ai-workflow
|
||||||
|
.claude
|
||||||
|
.codex
|
||||||
|
.gradle
|
||||||
|
gradle
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.webp binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
*.pdf binary
|
||||||
|
*.zip binary
|
||||||
|
*.webm binary
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
name: Frontend Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: frontend-release-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-upload-and-verify:
|
||||||
|
runs-on: [self-hosted, Linux, X64, default]
|
||||||
|
env:
|
||||||
|
RELEASE_BASE_URL: https://dev.truckwash.io
|
||||||
|
PLAYWRIGHT_BASE_URL: https://dev.truckwash.io
|
||||||
|
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
|
||||||
|
PLAYWRIGHT_RELEASE_API_PING_PATHS: /ping,/master/api/ping,/canary/api/ping,/stable/api/ping
|
||||||
|
RELEASE_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
|
RELEASE_EXPECTED_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
|
RELEASE_EXPECTED_COMMIT: ${{ github.sha }}
|
||||||
|
RELEASE_WAIT_INITIAL_SECONDS: 30
|
||||||
|
RELEASE_WAIT_TIMEOUT_SECONDS: 300
|
||||||
|
RELEASE_POLL_INTERVAL_SECONDS: 10
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v5
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Check AI workflow sync
|
||||||
|
run: node scripts/sync-ai-workflow.mjs --check
|
||||||
|
|
||||||
|
- name: Source and i18n checks
|
||||||
|
run: |
|
||||||
|
npm run text:check-encoding
|
||||||
|
npm run i18n:v2:source-check
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: npm run test:unit
|
||||||
|
env:
|
||||||
|
VITEST_BATCH_SIZE: 5
|
||||||
|
|
||||||
|
- name: Build release artifact
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Install Playwright Chromium
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Production Playwright gate
|
||||||
|
run: npm run test:e2e:prod
|
||||||
|
|
||||||
|
- name: Upload dist artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: frontend-dist-${{ env.RELEASE_BUILD_ID }}
|
||||||
|
path: dist
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
- name: Install lftp
|
||||||
|
run: |
|
||||||
|
if ! command -v lftp >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y lftp
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload hashed assets before release metadata
|
||||||
|
run: |
|
||||||
|
test -n "$RELEASE_DEPLOY_HOST" || (echo "RELEASE_DEPLOY_HOST is required" >&2; exit 1)
|
||||||
|
test -n "$RELEASE_DEPLOY_USER" || (echo "RELEASE_DEPLOY_USER is required" >&2; exit 1)
|
||||||
|
test -n "$RELEASE_DEPLOY_PASSWORD" || (echo "RELEASE_DEPLOY_PASSWORD is required" >&2; exit 1)
|
||||||
|
npm run release:upload:lftp
|
||||||
|
env:
|
||||||
|
RELEASE_DEPLOY_HOST: ${{ secrets.RELEASE_DEPLOY_HOST }}
|
||||||
|
RELEASE_DEPLOY_USER: ${{ secrets.RELEASE_DEPLOY_USER }}
|
||||||
|
RELEASE_DEPLOY_PASSWORD: ${{ secrets.RELEASE_DEPLOY_PASSWORD }}
|
||||||
|
RELEASE_DEPLOY_REMOTE_ROOT: ${{ secrets.RELEASE_DEPLOY_REMOTE_ROOT }}
|
||||||
|
|
||||||
|
- name: Wait for exact uploaded build
|
||||||
|
run: npm run release:verify-upload
|
||||||
|
|
||||||
|
- name: Public live Playwright gate
|
||||||
|
run: npm run test:e2e:live:public
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --use-system-ca
|
||||||
|
|
||||||
|
- name: Credentialed live Playwright gate
|
||||||
|
run: npm run test:e2e:live:roles
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --use-system-ca
|
||||||
|
PLAYWRIGHT_REQUIRE_LIVE_CREDENTIALS: "true"
|
||||||
|
PLAYWRIGHT_USER_CUSTOMER_NUMBER: ${{ secrets.PLAYWRIGHT_USER_CUSTOMER_NUMBER }}
|
||||||
|
PLAYWRIGHT_USER_PASSWORD: ${{ secrets.PLAYWRIGHT_USER_PASSWORD }}
|
||||||
|
PLAYWRIGHT_USER_OTP_SECRET: ${{ secrets.PLAYWRIGHT_USER_OTP_SECRET }}
|
||||||
|
PLAYWRIGHT_OPERATOR_USER_ID: ${{ secrets.PLAYWRIGHT_OPERATOR_USER_ID }}
|
||||||
|
PLAYWRIGHT_OPERATOR_PASSWORD: ${{ secrets.PLAYWRIGHT_OPERATOR_PASSWORD }}
|
||||||
|
PLAYWRIGHT_DEPARTMENT_ID: ${{ secrets.PLAYWRIGHT_DEPARTMENT_ID }}
|
||||||
|
|
||||||
|
- name: Record Release Manager gate
|
||||||
|
run: |
|
||||||
|
test -n "$RELEASE_MANAGER_GATE_TOKEN" || (echo "RELEASE_MANAGER_GATE_TOKEN is required" >&2; exit 1)
|
||||||
|
curl --fail --show-error --silent \
|
||||||
|
-X POST "$RELEASE_MANAGER_GATE_URL" \
|
||||||
|
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$RELEASE_EXPECTED_BUILD_ID\",\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
|
||||||
|
env:
|
||||||
|
RELEASE_MANAGER_GATE_URL: ${{ secrets.RELEASE_MANAGER_GATE_URL || 'https://api.truckwash.io/release/gate/test-runs' }}
|
||||||
|
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
|
||||||
|
|
||||||
|
- name: Update server version after verification
|
||||||
|
run: npm run release:update-server-version
|
||||||
|
env:
|
||||||
|
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
|
||||||
|
RELEASE_VERSION: ${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: frontend-release-playwright-${{ env.RELEASE_BUILD_ID }}
|
||||||
|
path: output/playwright
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 14
|
||||||
+10
-11
@@ -61,17 +61,18 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
VITEST_BATCH_SIZE: 5
|
VITEST_BATCH_SIZE: 5
|
||||||
|
|
||||||
e2e-smoke:
|
e2e-pr:
|
||||||
if: github.event_name != 'schedule'
|
if: github.event_name != 'schedule'
|
||||||
needs: build-and-unit
|
needs: build-and-unit
|
||||||
runs-on: [self-hosted, Linux, X64, default]
|
runs-on: [self-hosted, Linux, X64, default]
|
||||||
strategy:
|
env:
|
||||||
fail-fast: false
|
PLAYWRIGHT_PR_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||||
matrix:
|
PLAYWRIGHT_PR_HEAD: ${{ github.sha }}
|
||||||
project: [chromium-desktop, chromium-mobile]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v5
|
||||||
@@ -85,17 +86,15 @@ jobs:
|
|||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
run: npx playwright install --with-deps chromium
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Run Playwright smoke tests (${{ matrix.project }})
|
- name: Run Playwright PR tests
|
||||||
run: npx playwright test --grep @smoke --project="${{ matrix.project }}"
|
run: npm run test:e2e:pr -- --base="$PLAYWRIGHT_PR_BASE" --head="$PLAYWRIGHT_PR_HEAD"
|
||||||
|
|
||||||
- name: Upload Playwright report
|
- name: Upload Playwright report
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: playwright-report-smoke-${{ matrix.project }}
|
name: playwright-report-pr
|
||||||
path: |
|
path: output/playwright
|
||||||
output/playwright/report
|
|
||||||
output/playwright/test-results
|
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
|
|||||||
+59287
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.0
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
FROM node:24-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
ARG RELEASE_COMMIT_SHA=""
|
||||||
|
ARG COMMIT_SHA=""
|
||||||
|
ARG GITHUB_SHA=""
|
||||||
|
ARG SOURCE_COMMIT=""
|
||||||
|
ARG VITE_BASE_PATH=""
|
||||||
|
ENV RELEASE_COMMIT_SHA="${RELEASE_COMMIT_SHA}"
|
||||||
|
ENV COMMIT_SHA="${COMMIT_SHA}"
|
||||||
|
ENV GITHUB_SHA="${GITHUB_SHA}"
|
||||||
|
ENV SOURCE_COMMIT="${SOURCE_COMMIT}"
|
||||||
|
ENV VITE_BASE_PATH="${VITE_BASE_PATH}"
|
||||||
|
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --ignore-scripts
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY nginx.coolify-frontend.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_APP_VERSION: string,
|
readonly VITE_APP_VERSION: string,
|
||||||
readonly VITE_COMMIT_HASH: string,
|
readonly VITE_COMMIT_HASH: string,
|
||||||
readonly VITE_IS_DEV: string,
|
readonly VITE_IS_DEV: string,
|
||||||
|
readonly VITE_RELEASE_SOURCE: 'local' | 'deployment' | 'auto',
|
||||||
}
|
}
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv
|
||||||
|
|||||||
+5
-7
@@ -2,13 +2,11 @@
|
|||||||
<html lang="" class="theme-light" data-theme="light">
|
<html lang="" class="theme-light" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" type="image/png" href="/favicons/favicon-96x96.png" sizes="96x96" />
|
<link rel="icon" type="image/png" href="/assets/favicons/favicon-96x96.png" sizes="96x96" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicons/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/assets/favicons/favicon.svg" />
|
||||||
<link rel="shortcut icon" href="/favicons/favicon.ico" />
|
<link rel="shortcut icon" href="/assets/favicons/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
|
||||||
<meta name="apple-mobile-web-app-title" content="Truck Wash Kundeportal" />
|
<meta name="apple-mobile-web-app-title" content="Truck Wash Kundeportal" />
|
||||||
<link rel="stylesheet" href="bulma/css/bulma.min.css">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
@@ -17,6 +15,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/releaseBootstrap.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location ~ ^/(release-entry|release-manifest)\.json$ {
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(master|beta|canary|internal)/frontend/(release-entry|release-manifest)\.json$ {
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
try_files /$2.json =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(?:.+/)?(?<static_asset_path>(?:assets|resources|favicons|icons|img|sounds|\.well-known)/.+)$ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
try_files /$static_asset_path =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(?:.+/)?(?<static_file_path>manifest\.json|manifest\.webmanifest|favicon\.ico|favicon_default\.ico|pleno-favicon\.ico|release-entry\.json|release-manifest\.json|registerSW\.js|sw\.js|workbox-[^/]+\.js)$ {
|
||||||
|
try_files /$static_file_path =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.[^/]+$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /assets/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(master|beta|canary|internal)/frontend/assets/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
rewrite ^/(master|beta|canary|internal)/frontend/(.*)$ /$2 break;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(master|beta|canary|internal)/frontend$ {
|
||||||
|
try_files /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(master|beta|canary|internal)/frontend/ {
|
||||||
|
rewrite ^/(master|beta|canary|internal)/frontend/(.*)$ /$2 break;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"use strict";
|
||||||
|
if (process.env.PW_INSTRUMENT_MODULES) {
|
||||||
|
const Module = require("module");
|
||||||
|
const originalLoad = Module._load;
|
||||||
|
const root = { name: "<root>", selfMs: 0, totalMs: 0, childrenMs: 0, children: [] };
|
||||||
|
let current = root;
|
||||||
|
const stack = [];
|
||||||
|
Module._load = function(request, _parent, _isMain) {
|
||||||
|
const node = { name: request, selfMs: 0, totalMs: 0, childrenMs: 0, children: [] };
|
||||||
|
current.children.push(node);
|
||||||
|
stack.push(current);
|
||||||
|
current = node;
|
||||||
|
const start = performance.now();
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = originalLoad.apply(this, arguments);
|
||||||
|
} catch (e) {
|
||||||
|
current = stack.pop();
|
||||||
|
current.children.pop();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
const duration = performance.now() - start;
|
||||||
|
node.totalMs = duration;
|
||||||
|
node.selfMs = Math.max(0, duration - node.childrenMs);
|
||||||
|
current = stack.pop();
|
||||||
|
current.childrenMs += duration;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
process.on("exit", () => {
|
||||||
|
function printTree(node, prefix, isLast, lines2, depth) {
|
||||||
|
if (node.totalMs < 1 && depth > 0)
|
||||||
|
return;
|
||||||
|
const connector = depth === 0 ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
||||||
|
const time = `${node.totalMs.toFixed(1).padStart(8)}ms`;
|
||||||
|
const self = node.children.length ? ` (self: ${node.selfMs.toFixed(1)}ms)` : "";
|
||||||
|
lines2.push(`${time} ${prefix}${connector}${node.name}${self}`);
|
||||||
|
const childPrefix = prefix + (depth === 0 ? "" : isLast ? " " : "\u2502 ");
|
||||||
|
const sorted2 = node.children.slice().sort((a, b) => b.totalMs - a.totalMs);
|
||||||
|
for (let i = 0; i < sorted2.length; i++)
|
||||||
|
printTree(sorted2[i], childPrefix, i === sorted2.length - 1, lines2, depth + 1);
|
||||||
|
}
|
||||||
|
let totalModules = 0;
|
||||||
|
function count(n) {
|
||||||
|
totalModules++;
|
||||||
|
n.children.forEach(count);
|
||||||
|
}
|
||||||
|
root.children.forEach(count);
|
||||||
|
const lines = [];
|
||||||
|
const sorted = root.children.slice().sort((a, b) => b.totalMs - a.totalMs);
|
||||||
|
for (let i = 0; i < sorted.length; i++)
|
||||||
|
printTree(sorted[i], "", i === sorted.length - 1, lines, 0);
|
||||||
|
const totalMs = root.children.reduce((s, c) => s + c.totalMs, 0);
|
||||||
|
process.stderr.write(`
|
||||||
|
--- Module load tree: ${totalModules} modules, ${totalMs.toFixed(0)}ms total ---
|
||||||
|
` + lines.join("\n") + "\n");
|
||||||
|
const flat = /* @__PURE__ */ new Map();
|
||||||
|
function gather(n) {
|
||||||
|
const existing = flat.get(n.name);
|
||||||
|
if (existing) {
|
||||||
|
existing.selfMs += n.selfMs;
|
||||||
|
existing.totalMs += n.totalMs;
|
||||||
|
existing.count++;
|
||||||
|
} else {
|
||||||
|
flat.set(n.name, { selfMs: n.selfMs, totalMs: n.totalMs, count: 1 });
|
||||||
|
}
|
||||||
|
n.children.forEach(gather);
|
||||||
|
}
|
||||||
|
root.children.forEach(gather);
|
||||||
|
const top50 = [...flat.entries()].sort((a, b) => b[1].selfMs - a[1].selfMs).slice(0, 50);
|
||||||
|
const flatLines = top50.map(
|
||||||
|
([mod, { selfMs, totalMs: totalMs2, count: count2 }]) => `${selfMs.toFixed(1).padStart(8)}ms self ${totalMs2.toFixed(1).padStart(8)}ms total (x${String(count2).padStart(3)}) ${mod}`
|
||||||
|
);
|
||||||
|
process.stderr.write(`
|
||||||
|
--- Top 50 modules by self time ---
|
||||||
|
` + flatLines.join("\n") + "\n");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var browserActions_exports = {};
|
||||||
|
__export(browserActions_exports, {
|
||||||
|
codegen: () => codegen,
|
||||||
|
open: () => open,
|
||||||
|
pdf: () => pdf,
|
||||||
|
screenshot: () => screenshot
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(browserActions_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_os = __toESM(require("os"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var playwright = __toESM(require("../.."));
|
||||||
|
var import_utils = require("../utils");
|
||||||
|
var import_utilsBundle = require("../utilsBundle");
|
||||||
|
async function launchContext(options, extraOptions) {
|
||||||
|
validateOptions(options);
|
||||||
|
const browserType = lookupBrowserType(options);
|
||||||
|
const launchOptions = extraOptions;
|
||||||
|
if (options.channel)
|
||||||
|
launchOptions.channel = options.channel;
|
||||||
|
launchOptions.handleSIGINT = false;
|
||||||
|
const contextOptions = (
|
||||||
|
// Copy the device descriptor since we have to compare and modify the options.
|
||||||
|
options.device ? { ...playwright.devices[options.device] } : {}
|
||||||
|
);
|
||||||
|
if (!extraOptions.headless)
|
||||||
|
contextOptions.deviceScaleFactor = import_os.default.platform() === "darwin" ? 2 : 1;
|
||||||
|
if (browserType.name() === "webkit" && process.platform === "linux") {
|
||||||
|
delete contextOptions.hasTouch;
|
||||||
|
delete contextOptions.isMobile;
|
||||||
|
}
|
||||||
|
if (contextOptions.isMobile && browserType.name() === "firefox")
|
||||||
|
contextOptions.isMobile = void 0;
|
||||||
|
if (options.blockServiceWorkers)
|
||||||
|
contextOptions.serviceWorkers = "block";
|
||||||
|
if (options.proxyServer) {
|
||||||
|
launchOptions.proxy = {
|
||||||
|
server: options.proxyServer
|
||||||
|
};
|
||||||
|
if (options.proxyBypass)
|
||||||
|
launchOptions.proxy.bypass = options.proxyBypass;
|
||||||
|
}
|
||||||
|
if (options.viewportSize) {
|
||||||
|
try {
|
||||||
|
const [width, height] = options.viewportSize.split(",").map((n) => +n);
|
||||||
|
if (isNaN(width) || isNaN(height))
|
||||||
|
throw new Error("bad values");
|
||||||
|
contextOptions.viewport = { width, height };
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.geolocation) {
|
||||||
|
try {
|
||||||
|
const [latitude, longitude] = options.geolocation.split(",").map((n) => parseFloat(n.trim()));
|
||||||
|
contextOptions.geolocation = {
|
||||||
|
latitude,
|
||||||
|
longitude
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Invalid geolocation format, should be "lat,long". For example --geolocation="37.819722,-122.478611"');
|
||||||
|
}
|
||||||
|
contextOptions.permissions = ["geolocation"];
|
||||||
|
}
|
||||||
|
if (options.userAgent)
|
||||||
|
contextOptions.userAgent = options.userAgent;
|
||||||
|
if (options.lang)
|
||||||
|
contextOptions.locale = options.lang;
|
||||||
|
if (options.colorScheme)
|
||||||
|
contextOptions.colorScheme = options.colorScheme;
|
||||||
|
if (options.timezone)
|
||||||
|
contextOptions.timezoneId = options.timezone;
|
||||||
|
if (options.loadStorage)
|
||||||
|
contextOptions.storageState = options.loadStorage;
|
||||||
|
if (options.ignoreHttpsErrors)
|
||||||
|
contextOptions.ignoreHTTPSErrors = true;
|
||||||
|
if (options.saveHar) {
|
||||||
|
contextOptions.recordHar = { path: import_path.default.resolve(process.cwd(), options.saveHar), mode: "minimal" };
|
||||||
|
if (options.saveHarGlob)
|
||||||
|
contextOptions.recordHar.urlFilter = options.saveHarGlob;
|
||||||
|
contextOptions.serviceWorkers = "block";
|
||||||
|
}
|
||||||
|
let browser;
|
||||||
|
let context;
|
||||||
|
if (options.userDataDir) {
|
||||||
|
context = await browserType.launchPersistentContext(options.userDataDir, { ...launchOptions, ...contextOptions });
|
||||||
|
browser = context.browser();
|
||||||
|
} else {
|
||||||
|
browser = await browserType.launch(launchOptions);
|
||||||
|
context = await browser.newContext(contextOptions);
|
||||||
|
}
|
||||||
|
let closingBrowser = false;
|
||||||
|
async function closeBrowser() {
|
||||||
|
if (closingBrowser)
|
||||||
|
return;
|
||||||
|
closingBrowser = true;
|
||||||
|
if (options.saveStorage)
|
||||||
|
await context.storageState({ path: options.saveStorage }).catch((e) => null);
|
||||||
|
if (options.saveHar)
|
||||||
|
await context.close();
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
context.on("page", (page) => {
|
||||||
|
page.on("dialog", () => {
|
||||||
|
});
|
||||||
|
page.on("close", () => {
|
||||||
|
const hasPage = browser.contexts().some((context2) => context2.pages().length > 0);
|
||||||
|
if (hasPage)
|
||||||
|
return;
|
||||||
|
closeBrowser().catch(() => {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
process.on("SIGINT", async () => {
|
||||||
|
await closeBrowser();
|
||||||
|
(0, import_utils.gracefullyProcessExitDoNotHang)(130);
|
||||||
|
});
|
||||||
|
const timeout = options.timeout ? parseInt(options.timeout, 10) : 0;
|
||||||
|
context.setDefaultTimeout(timeout);
|
||||||
|
context.setDefaultNavigationTimeout(timeout);
|
||||||
|
delete launchOptions.headless;
|
||||||
|
delete launchOptions.executablePath;
|
||||||
|
delete launchOptions.handleSIGINT;
|
||||||
|
delete contextOptions.deviceScaleFactor;
|
||||||
|
return { browser, browserName: browserType.name(), context, contextOptions, launchOptions, closeBrowser };
|
||||||
|
}
|
||||||
|
async function openPage(context, url) {
|
||||||
|
let page = context.pages()[0];
|
||||||
|
if (!page)
|
||||||
|
page = await context.newPage();
|
||||||
|
if (url) {
|
||||||
|
if (import_fs.default.existsSync(url))
|
||||||
|
url = "file://" + import_path.default.resolve(url);
|
||||||
|
else if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:") && !url.startsWith("data:"))
|
||||||
|
url = "http://" + url;
|
||||||
|
await page.goto(url);
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
async function open(options, url) {
|
||||||
|
const { context } = await launchContext(options, { headless: !!process.env.PWTEST_CLI_HEADLESS, executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH });
|
||||||
|
await context._exposeConsoleApi();
|
||||||
|
await openPage(context, url);
|
||||||
|
}
|
||||||
|
async function codegen(options, url) {
|
||||||
|
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
|
||||||
|
const tracesDir = import_path.default.join(import_os.default.tmpdir(), `playwright-recorder-trace-${Date.now()}`);
|
||||||
|
const { context, browser, launchOptions, contextOptions, closeBrowser } = await launchContext(options, {
|
||||||
|
headless: !!process.env.PWTEST_CLI_HEADLESS,
|
||||||
|
executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
|
||||||
|
tracesDir
|
||||||
|
});
|
||||||
|
const donePromise = new import_utils.ManualPromise();
|
||||||
|
maybeSetupTestHooks(browser, closeBrowser, donePromise);
|
||||||
|
import_utilsBundle.dotenv.config({ path: "playwright.env" });
|
||||||
|
await context._enableRecorder({
|
||||||
|
language,
|
||||||
|
launchOptions,
|
||||||
|
contextOptions,
|
||||||
|
device: options.device,
|
||||||
|
saveStorage: options.saveStorage,
|
||||||
|
mode: "recording",
|
||||||
|
testIdAttributeName,
|
||||||
|
outputFile: outputFile ? import_path.default.resolve(outputFile) : void 0,
|
||||||
|
handleSIGINT: false
|
||||||
|
});
|
||||||
|
await openPage(context, url);
|
||||||
|
donePromise.resolve();
|
||||||
|
}
|
||||||
|
async function maybeSetupTestHooks(browser, closeBrowser, donePromise) {
|
||||||
|
if (!process.env.PWTEST_CLI_IS_UNDER_TEST)
|
||||||
|
return;
|
||||||
|
const logs = [];
|
||||||
|
require("playwright-core/lib/utilsBundle").debug.log = (...args) => {
|
||||||
|
const line = require("util").format(...args) + "\n";
|
||||||
|
logs.push(line);
|
||||||
|
process.stderr.write(line);
|
||||||
|
};
|
||||||
|
browser.on("disconnected", () => {
|
||||||
|
const hasCrashLine = logs.some((line) => line.includes("process did exit:") && !line.includes("process did exit: exitCode=0, signal=null"));
|
||||||
|
if (hasCrashLine) {
|
||||||
|
process.stderr.write("Detected browser crash.\n");
|
||||||
|
(0, import_utils.gracefullyProcessExitDoNotHang)(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const close = async () => {
|
||||||
|
await donePromise;
|
||||||
|
await closeBrowser();
|
||||||
|
};
|
||||||
|
if (process.env.PWTEST_CLI_EXIT_AFTER_TIMEOUT) {
|
||||||
|
setTimeout(close, +process.env.PWTEST_CLI_EXIT_AFTER_TIMEOUT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let stdin = "";
|
||||||
|
process.stdin.on("data", (data) => {
|
||||||
|
stdin += data.toString();
|
||||||
|
if (stdin.startsWith("exit")) {
|
||||||
|
process.stdin.destroy();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function waitForPage(page, captureOptions) {
|
||||||
|
if (captureOptions.waitForSelector) {
|
||||||
|
console.log(`Waiting for selector ${captureOptions.waitForSelector}...`);
|
||||||
|
await page.waitForSelector(captureOptions.waitForSelector);
|
||||||
|
}
|
||||||
|
if (captureOptions.waitForTimeout) {
|
||||||
|
console.log(`Waiting for timeout ${captureOptions.waitForTimeout}...`);
|
||||||
|
await page.waitForTimeout(parseInt(captureOptions.waitForTimeout, 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function screenshot(options, captureOptions, url, path2) {
|
||||||
|
const { context } = await launchContext(options, { headless: true });
|
||||||
|
console.log("Navigating to " + url);
|
||||||
|
const page = await openPage(context, url);
|
||||||
|
await waitForPage(page, captureOptions);
|
||||||
|
console.log("Capturing screenshot into " + path2);
|
||||||
|
await page.screenshot({ path: path2, fullPage: !!captureOptions.fullPage });
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
async function pdf(options, captureOptions, url, path2) {
|
||||||
|
if (options.browser !== "chromium")
|
||||||
|
throw new Error("PDF creation is only working with Chromium");
|
||||||
|
const { context } = await launchContext({ ...options, browser: "chromium" }, { headless: true });
|
||||||
|
console.log("Navigating to " + url);
|
||||||
|
const page = await openPage(context, url);
|
||||||
|
await waitForPage(page, captureOptions);
|
||||||
|
console.log("Saving as pdf into " + path2);
|
||||||
|
await page.pdf({ path: path2, format: captureOptions.paperFormat });
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
function lookupBrowserType(options) {
|
||||||
|
let name = options.browser;
|
||||||
|
if (options.device) {
|
||||||
|
const device = playwright.devices[options.device];
|
||||||
|
name = device.defaultBrowserType;
|
||||||
|
}
|
||||||
|
let browserType;
|
||||||
|
switch (name) {
|
||||||
|
case "chromium":
|
||||||
|
browserType = playwright.chromium;
|
||||||
|
break;
|
||||||
|
case "webkit":
|
||||||
|
browserType = playwright.webkit;
|
||||||
|
break;
|
||||||
|
case "firefox":
|
||||||
|
browserType = playwright.firefox;
|
||||||
|
break;
|
||||||
|
case "cr":
|
||||||
|
browserType = playwright.chromium;
|
||||||
|
break;
|
||||||
|
case "wk":
|
||||||
|
browserType = playwright.webkit;
|
||||||
|
break;
|
||||||
|
case "ff":
|
||||||
|
browserType = playwright.firefox;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (browserType)
|
||||||
|
return browserType;
|
||||||
|
import_utilsBundle.program.help();
|
||||||
|
}
|
||||||
|
function validateOptions(options) {
|
||||||
|
if (options.device && !(options.device in playwright.devices)) {
|
||||||
|
const lines = [`Device descriptor not found: '${options.device}', available devices are:`];
|
||||||
|
for (const name in playwright.devices)
|
||||||
|
lines.push(` "${name}"`);
|
||||||
|
throw new Error(lines.join("\n"));
|
||||||
|
}
|
||||||
|
if (options.colorScheme && !["light", "dark"].includes(options.colorScheme))
|
||||||
|
throw new Error('Invalid color scheme, should be one of "light", "dark"');
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
codegen,
|
||||||
|
open,
|
||||||
|
pdf,
|
||||||
|
screenshot
|
||||||
|
});
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var installActions_exports = {};
|
||||||
|
__export(installActions_exports, {
|
||||||
|
installBrowsers: () => installBrowsers,
|
||||||
|
installDeps: () => installDeps,
|
||||||
|
markDockerImage: () => markDockerImage,
|
||||||
|
registry: () => import_server.registry,
|
||||||
|
uninstallBrowsers: () => uninstallBrowsers
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(installActions_exports);
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_server = require("../server");
|
||||||
|
var import_utils = require("../utils");
|
||||||
|
var import_utils2 = require("../utils");
|
||||||
|
var import_ascii = require("../server/utils/ascii");
|
||||||
|
function printInstalledBrowsers(browsers) {
|
||||||
|
const browserPaths = /* @__PURE__ */ new Set();
|
||||||
|
for (const browser of browsers)
|
||||||
|
browserPaths.add(browser.browserPath);
|
||||||
|
console.log(` Browsers:`);
|
||||||
|
for (const browserPath of [...browserPaths].sort())
|
||||||
|
console.log(` ${browserPath}`);
|
||||||
|
console.log(` References:`);
|
||||||
|
const references = /* @__PURE__ */ new Set();
|
||||||
|
for (const browser of browsers)
|
||||||
|
references.add(browser.referenceDir);
|
||||||
|
for (const reference of [...references].sort())
|
||||||
|
console.log(` ${reference}`);
|
||||||
|
}
|
||||||
|
function printGroupedByPlaywrightVersion(browsers) {
|
||||||
|
const dirToVersion = /* @__PURE__ */ new Map();
|
||||||
|
for (const browser of browsers) {
|
||||||
|
if (dirToVersion.has(browser.referenceDir))
|
||||||
|
continue;
|
||||||
|
const packageJSON = require(import_path.default.join(browser.referenceDir, "package.json"));
|
||||||
|
const version = packageJSON.version;
|
||||||
|
dirToVersion.set(browser.referenceDir, version);
|
||||||
|
}
|
||||||
|
const groupedByPlaywrightMinorVersion = /* @__PURE__ */ new Map();
|
||||||
|
for (const browser of browsers) {
|
||||||
|
const version = dirToVersion.get(browser.referenceDir);
|
||||||
|
let entries = groupedByPlaywrightMinorVersion.get(version);
|
||||||
|
if (!entries) {
|
||||||
|
entries = [];
|
||||||
|
groupedByPlaywrightMinorVersion.set(version, entries);
|
||||||
|
}
|
||||||
|
entries.push(browser);
|
||||||
|
}
|
||||||
|
const sortedVersions = [...groupedByPlaywrightMinorVersion.keys()].sort((a, b) => {
|
||||||
|
const aComponents = a.split(".");
|
||||||
|
const bComponents = b.split(".");
|
||||||
|
const aMajor = parseInt(aComponents[0], 10);
|
||||||
|
const bMajor = parseInt(bComponents[0], 10);
|
||||||
|
if (aMajor !== bMajor)
|
||||||
|
return aMajor - bMajor;
|
||||||
|
const aMinor = parseInt(aComponents[1], 10);
|
||||||
|
const bMinor = parseInt(bComponents[1], 10);
|
||||||
|
if (aMinor !== bMinor)
|
||||||
|
return aMinor - bMinor;
|
||||||
|
return aComponents.slice(2).join(".").localeCompare(bComponents.slice(2).join("."));
|
||||||
|
});
|
||||||
|
for (const version of sortedVersions) {
|
||||||
|
console.log(`
|
||||||
|
Playwright version: ${version}`);
|
||||||
|
printInstalledBrowsers(groupedByPlaywrightMinorVersion.get(version));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function markDockerImage(dockerImageNameTemplate) {
|
||||||
|
(0, import_utils2.assert)(dockerImageNameTemplate, "dockerImageNameTemplate is required");
|
||||||
|
await (0, import_server.writeDockerVersion)(dockerImageNameTemplate);
|
||||||
|
}
|
||||||
|
async function installBrowsers(args, options) {
|
||||||
|
if ((0, import_utils.isLikelyNpxGlobal)()) {
|
||||||
|
console.error((0, import_ascii.wrapInASCIIBox)([
|
||||||
|
`WARNING: It looks like you are running 'npx playwright install' without first`,
|
||||||
|
`installing your project's dependencies.`,
|
||||||
|
``,
|
||||||
|
`To avoid unexpected behavior, please install your dependencies first, and`,
|
||||||
|
`then run Playwright's install command:`,
|
||||||
|
``,
|
||||||
|
` npm install`,
|
||||||
|
` npx playwright install`,
|
||||||
|
``,
|
||||||
|
`If your project does not yet depend on Playwright, first install the`,
|
||||||
|
`applicable npm package (most commonly @playwright/test), and`,
|
||||||
|
`then run Playwright's install command to download the browsers:`,
|
||||||
|
``,
|
||||||
|
` npm install @playwright/test`,
|
||||||
|
` npx playwright install`,
|
||||||
|
``
|
||||||
|
].join("\n"), 1));
|
||||||
|
}
|
||||||
|
if (options.shell === false && options.onlyShell)
|
||||||
|
throw new Error(`Only one of --no-shell and --only-shell can be specified`);
|
||||||
|
const shell = options.shell === false ? "no" : options.onlyShell ? "only" : void 0;
|
||||||
|
const executables = import_server.registry.resolveBrowsers(args, { shell });
|
||||||
|
if (options.withDeps)
|
||||||
|
await import_server.registry.installDeps(executables, !!options.dryRun);
|
||||||
|
if (options.dryRun && options.list)
|
||||||
|
throw new Error(`Only one of --dry-run and --list can be specified`);
|
||||||
|
if (options.dryRun) {
|
||||||
|
for (const executable of executables) {
|
||||||
|
console.log(import_server.registry.calculateDownloadTitle(executable));
|
||||||
|
console.log(` Install location: ${executable.directory ?? "<system>"}`);
|
||||||
|
if (executable.downloadURLs?.length) {
|
||||||
|
const [url, ...fallbacks] = executable.downloadURLs;
|
||||||
|
console.log(` Download url: ${url}`);
|
||||||
|
for (let i = 0; i < fallbacks.length; ++i)
|
||||||
|
console.log(` Download fallback ${i + 1}: ${fallbacks[i]}`);
|
||||||
|
}
|
||||||
|
console.log(``);
|
||||||
|
}
|
||||||
|
} else if (options.list) {
|
||||||
|
const browsers = await import_server.registry.listInstalledBrowsers();
|
||||||
|
printGroupedByPlaywrightVersion(browsers);
|
||||||
|
} else {
|
||||||
|
await import_server.registry.install(executables, { force: options.force });
|
||||||
|
await import_server.registry.validateHostRequirementsForExecutablesIfNeeded(executables, process.env.PW_LANG_NAME || "javascript").catch((e) => {
|
||||||
|
e.name = "Playwright Host validation warning";
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function uninstallBrowsers(options) {
|
||||||
|
delete process.env.PLAYWRIGHT_SKIP_BROWSER_GC;
|
||||||
|
await import_server.registry.uninstall(!!options.all).then(({ numberOfBrowsersLeft }) => {
|
||||||
|
if (!options.all && numberOfBrowsersLeft > 0) {
|
||||||
|
console.log("Successfully uninstalled Playwright browsers for the current Playwright installation.");
|
||||||
|
console.log(`There are still ${numberOfBrowsersLeft} browsers left, used by other Playwright installations.
|
||||||
|
To uninstall Playwright browsers for all installations, re-run with --all flag.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function installDeps(args, options) {
|
||||||
|
await import_server.registry.installDeps(import_server.registry.resolveBrowsers(args, {}), !!options.dryRun);
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
installBrowsers,
|
||||||
|
installDeps,
|
||||||
|
markDockerImage,
|
||||||
|
registry,
|
||||||
|
uninstallBrowsers
|
||||||
|
});
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var connect_exports = {};
|
||||||
|
__export(connect_exports, {
|
||||||
|
connectToBrowser: () => connectToBrowser,
|
||||||
|
connectToEndpoint: () => connectToEndpoint
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(connect_exports);
|
||||||
|
var import_time = require("../utils/isomorphic/time");
|
||||||
|
var import_timeoutRunner = require("../utils/isomorphic/timeoutRunner");
|
||||||
|
var import_browser = require("./browser");
|
||||||
|
var import_connection = require("./connection");
|
||||||
|
var import_events = require("./events");
|
||||||
|
async function connectToBrowser(playwright, params) {
|
||||||
|
const deadline = params.timeout ? (0, import_time.monotonicTime)() + params.timeout : 0;
|
||||||
|
const nameParam = params.browserName ? { "x-playwright-browser": params.browserName } : {};
|
||||||
|
const headers = { ...nameParam, ...params.headers };
|
||||||
|
const connectParams = {
|
||||||
|
endpoint: params.endpoint,
|
||||||
|
headers,
|
||||||
|
exposeNetwork: params.exposeNetwork,
|
||||||
|
slowMo: params.slowMo,
|
||||||
|
timeout: params.timeout || 0
|
||||||
|
};
|
||||||
|
if (params.__testHookRedirectPortForwarding)
|
||||||
|
connectParams.socksProxyRedirectPortForTest = params.__testHookRedirectPortForwarding;
|
||||||
|
const connection = await connectToEndpoint(playwright._connection, connectParams);
|
||||||
|
let browser;
|
||||||
|
connection.on("close", () => {
|
||||||
|
for (const context of browser?.contexts() || []) {
|
||||||
|
for (const page of context.pages())
|
||||||
|
page._onClose();
|
||||||
|
context._onClose();
|
||||||
|
}
|
||||||
|
setTimeout(() => browser?._didClose(), 0);
|
||||||
|
});
|
||||||
|
const result = await (0, import_timeoutRunner.raceAgainstDeadline)(async () => {
|
||||||
|
if (params.__testHookBeforeCreateBrowser)
|
||||||
|
await params.__testHookBeforeCreateBrowser();
|
||||||
|
const playwright2 = await connection.initializePlaywright();
|
||||||
|
if (!playwright2._initializer.preLaunchedBrowser) {
|
||||||
|
connection.close();
|
||||||
|
throw new Error("Malformed endpoint. Did you use BrowserType.launchServer method?");
|
||||||
|
}
|
||||||
|
playwright2.selectors = playwright2.selectors;
|
||||||
|
browser = import_browser.Browser.from(playwright2._initializer.preLaunchedBrowser);
|
||||||
|
browser._shouldCloseConnectionOnClose = true;
|
||||||
|
browser.on(import_events.Events.Browser.Disconnected, () => connection.close());
|
||||||
|
return browser;
|
||||||
|
}, deadline);
|
||||||
|
if (!result.timedOut) {
|
||||||
|
return result.result;
|
||||||
|
} else {
|
||||||
|
connection.close();
|
||||||
|
throw new Error(`Timeout ${params.timeout}ms exceeded`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function connectToEndpoint(parentConnection, params) {
|
||||||
|
const localUtils = parentConnection.localUtils();
|
||||||
|
const transport = localUtils ? new JsonPipeTransport(localUtils) : new WebSocketTransport();
|
||||||
|
const connectHeaders = await transport.connect(params);
|
||||||
|
const connection = new import_connection.Connection(parentConnection._platform, localUtils, parentConnection._instrumentation, connectHeaders);
|
||||||
|
connection.markAsRemote();
|
||||||
|
connection.on("close", () => transport.close());
|
||||||
|
let closeError;
|
||||||
|
const onTransportClosed = (reason) => {
|
||||||
|
connection.close(reason || closeError);
|
||||||
|
};
|
||||||
|
transport.onClose((reason) => onTransportClosed(reason));
|
||||||
|
connection.onmessage = (message) => transport.send(message).catch(() => onTransportClosed());
|
||||||
|
transport.onMessage((message) => {
|
||||||
|
try {
|
||||||
|
connection.dispatch(message);
|
||||||
|
} catch (e) {
|
||||||
|
closeError = String(e);
|
||||||
|
transport.close().catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
class JsonPipeTransport {
|
||||||
|
constructor(owner) {
|
||||||
|
this._owner = owner;
|
||||||
|
}
|
||||||
|
async connect(params) {
|
||||||
|
const { pipe, headers: connectHeaders } = await this._owner._channel.connect(params);
|
||||||
|
this._pipe = pipe;
|
||||||
|
return connectHeaders;
|
||||||
|
}
|
||||||
|
async send(message) {
|
||||||
|
await this._pipe.send({ message });
|
||||||
|
}
|
||||||
|
onMessage(callback) {
|
||||||
|
this._pipe.on("message", ({ message }) => callback(message));
|
||||||
|
}
|
||||||
|
onClose(callback) {
|
||||||
|
this._pipe.on("closed", ({ reason }) => callback(reason));
|
||||||
|
}
|
||||||
|
async close() {
|
||||||
|
await this._pipe.close().catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class WebSocketTransport {
|
||||||
|
async connect(params) {
|
||||||
|
this._ws = new window.WebSocket(params.endpoint);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
async send(message) {
|
||||||
|
this._ws.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
onMessage(callback) {
|
||||||
|
this._ws.addEventListener("message", (event) => callback(JSON.parse(event.data)));
|
||||||
|
}
|
||||||
|
onClose(callback) {
|
||||||
|
this._ws.addEventListener("close", () => callback());
|
||||||
|
}
|
||||||
|
async close() {
|
||||||
|
this._ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
connectToBrowser,
|
||||||
|
connectToEndpoint
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var debugger_exports = {};
|
||||||
|
__export(debugger_exports, {
|
||||||
|
Debugger: () => Debugger
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(debugger_exports);
|
||||||
|
var import_channelOwner = require("./channelOwner");
|
||||||
|
var import_events = require("./events");
|
||||||
|
class Debugger extends import_channelOwner.ChannelOwner {
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
this._pausedDetails = null;
|
||||||
|
this._channel.on("pausedStateChanged", ({ pausedDetails }) => {
|
||||||
|
this._pausedDetails = pausedDetails ?? null;
|
||||||
|
this.emit(import_events.Events.Debugger.PausedStateChanged);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
static from(channel) {
|
||||||
|
return channel._object;
|
||||||
|
}
|
||||||
|
async requestPause() {
|
||||||
|
await this._channel.requestPause();
|
||||||
|
}
|
||||||
|
async resume() {
|
||||||
|
await this._channel.resume();
|
||||||
|
}
|
||||||
|
async next() {
|
||||||
|
await this._channel.next();
|
||||||
|
}
|
||||||
|
async runTo(location) {
|
||||||
|
await this._channel.runTo({ location });
|
||||||
|
}
|
||||||
|
pausedDetails() {
|
||||||
|
return this._pausedDetails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
Debugger
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var disposable_exports = {};
|
||||||
|
__export(disposable_exports, {
|
||||||
|
DisposableObject: () => DisposableObject,
|
||||||
|
DisposableStub: () => DisposableStub,
|
||||||
|
disposeAll: () => disposeAll
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(disposable_exports);
|
||||||
|
var import_channelOwner = require("./channelOwner");
|
||||||
|
var import_errors = require("./errors");
|
||||||
|
class DisposableObject extends import_channelOwner.ChannelOwner {
|
||||||
|
static from(channel) {
|
||||||
|
return channel._object;
|
||||||
|
}
|
||||||
|
async [Symbol.asyncDispose]() {
|
||||||
|
await this.dispose();
|
||||||
|
}
|
||||||
|
async dispose() {
|
||||||
|
try {
|
||||||
|
await this._channel.dispose();
|
||||||
|
} catch (e) {
|
||||||
|
if ((0, import_errors.isTargetClosedError)(e))
|
||||||
|
return;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class DisposableStub {
|
||||||
|
constructor(dispose) {
|
||||||
|
this._dispose = dispose;
|
||||||
|
}
|
||||||
|
async [Symbol.asyncDispose]() {
|
||||||
|
await this.dispose();
|
||||||
|
}
|
||||||
|
async dispose() {
|
||||||
|
if (!this._dispose)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
const dispose = this._dispose;
|
||||||
|
this._dispose = void 0;
|
||||||
|
await dispose();
|
||||||
|
} catch (e) {
|
||||||
|
if ((0, import_errors.isTargetClosedError)(e))
|
||||||
|
return;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function disposeAll(disposables) {
|
||||||
|
const copy = [...disposables];
|
||||||
|
disposables.length = 0;
|
||||||
|
await Promise.all(copy.map((d) => d.dispose()));
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
DisposableObject,
|
||||||
|
DisposableStub,
|
||||||
|
disposeAll
|
||||||
|
});
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var screencast_exports = {};
|
||||||
|
__export(screencast_exports, {
|
||||||
|
Screencast: () => Screencast
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(screencast_exports);
|
||||||
|
var import_artifact = require("./artifact");
|
||||||
|
var import_disposable = require("./disposable");
|
||||||
|
class Screencast {
|
||||||
|
constructor(page) {
|
||||||
|
this._started = false;
|
||||||
|
this._onFrame = null;
|
||||||
|
this._page = page;
|
||||||
|
this._page._channel.on("screencastFrame", ({ data }) => {
|
||||||
|
void this._onFrame?.({ data });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async start(options = {}) {
|
||||||
|
if (this._started)
|
||||||
|
throw new Error("Screencast is already started");
|
||||||
|
this._started = true;
|
||||||
|
if (options.onFrame)
|
||||||
|
this._onFrame = options.onFrame;
|
||||||
|
const result = await this._page._channel.screencastStart({
|
||||||
|
size: options.size,
|
||||||
|
quality: options.quality,
|
||||||
|
sendFrames: !!options.onFrame,
|
||||||
|
record: !!options.path
|
||||||
|
});
|
||||||
|
if (result.artifact) {
|
||||||
|
this._artifact = import_artifact.Artifact.from(result.artifact);
|
||||||
|
this._savePath = options.path;
|
||||||
|
}
|
||||||
|
return new import_disposable.DisposableStub(() => this.stop());
|
||||||
|
}
|
||||||
|
async stop() {
|
||||||
|
await this._page._wrapApiCall(async () => {
|
||||||
|
this._started = false;
|
||||||
|
this._onFrame = null;
|
||||||
|
await this._page._channel.screencastStop();
|
||||||
|
if (this._savePath)
|
||||||
|
await this._artifact?.saveAs(this._savePath);
|
||||||
|
this._artifact = void 0;
|
||||||
|
this._savePath = void 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async showActions(options) {
|
||||||
|
await this._page._channel.screencastShowActions({ duration: options?.duration, position: options?.position, fontSize: options?.fontSize });
|
||||||
|
return new import_disposable.DisposableStub(() => this._page._channel.screencastHideActions());
|
||||||
|
}
|
||||||
|
async hideActions() {
|
||||||
|
await this._page._channel.screencastHideActions();
|
||||||
|
}
|
||||||
|
async showOverlay(html, options) {
|
||||||
|
const { id } = await this._page._channel.screencastShowOverlay({ html, duration: options?.duration });
|
||||||
|
return new import_disposable.DisposableStub(() => this._page._channel.screencastRemoveOverlay({ id }));
|
||||||
|
}
|
||||||
|
async showChapter(title, options) {
|
||||||
|
await this._page._channel.screencastChapter({ title, ...options });
|
||||||
|
}
|
||||||
|
async showOverlays() {
|
||||||
|
await this._page._channel.screencastSetOverlayVisible({ visible: true });
|
||||||
|
}
|
||||||
|
async hideOverlays() {
|
||||||
|
await this._page._channel.screencastSetOverlayVisible({ visible: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
Screencast
|
||||||
|
});
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,100 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var playwrightPipeServer_exports = {};
|
||||||
|
__export(playwrightPipeServer_exports, {
|
||||||
|
PlaywrightPipeServer: () => PlaywrightPipeServer
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(playwrightPipeServer_exports);
|
||||||
|
var import_net = __toESM(require("net"));
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_playwrightConnection = require("./playwrightConnection");
|
||||||
|
var import_serverTransport = require("./serverTransport");
|
||||||
|
var import_debugLogger = require("../server/utils/debugLogger");
|
||||||
|
var import_browser = require("../server/browser");
|
||||||
|
var import_utils = require("../utils");
|
||||||
|
class PlaywrightPipeServer {
|
||||||
|
constructor(browser) {
|
||||||
|
this._connections = /* @__PURE__ */ new Set();
|
||||||
|
this._connectionId = 0;
|
||||||
|
this._browser = browser;
|
||||||
|
browser.on(import_browser.Browser.Events.Disconnected, () => this.close());
|
||||||
|
}
|
||||||
|
async listen(pipeName) {
|
||||||
|
if (!pipeName.startsWith("\\\\.\\pipe\\")) {
|
||||||
|
try {
|
||||||
|
import_fs.default.unlinkSync(pipeName);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._server = import_net.default.createServer((socket) => {
|
||||||
|
const id = String(++this._connectionId);
|
||||||
|
import_debugLogger.debugLogger.log("server", `[${id}] pipe client connected`);
|
||||||
|
const transport = new import_serverTransport.SocketServerTransport(socket);
|
||||||
|
const connection = new import_playwrightConnection.PlaywrightConnection(
|
||||||
|
new import_utils.Semaphore(1),
|
||||||
|
transport,
|
||||||
|
false,
|
||||||
|
this._browser.attribution.playwright,
|
||||||
|
() => this._initPreLaunchedBrowserMode(id),
|
||||||
|
id
|
||||||
|
);
|
||||||
|
this._connections.add(connection);
|
||||||
|
transport.on("close", () => this._connections.delete(connection));
|
||||||
|
});
|
||||||
|
(0, import_utils.decorateServer)(this._server);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
this._server.listen(pipeName, () => resolve());
|
||||||
|
this._server.on("error", reject);
|
||||||
|
});
|
||||||
|
import_debugLogger.debugLogger.log("server", `Pipe server listening at ${pipeName}`);
|
||||||
|
}
|
||||||
|
async _initPreLaunchedBrowserMode(id) {
|
||||||
|
import_debugLogger.debugLogger.log("server", `[${id}] engaged pre-launched (browser) pipe mode`);
|
||||||
|
return {
|
||||||
|
preLaunchedBrowser: this._browser,
|
||||||
|
sharedBrowser: true,
|
||||||
|
denyLaunch: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async close() {
|
||||||
|
if (!this._server)
|
||||||
|
return;
|
||||||
|
import_debugLogger.debugLogger.log("server", "closing pipe server");
|
||||||
|
for (const connection of this._connections)
|
||||||
|
await connection.close({ code: 1001, reason: "Server closing" });
|
||||||
|
this._connections.clear();
|
||||||
|
await new Promise((f) => this._server.close(() => f()));
|
||||||
|
this._server = void 0;
|
||||||
|
import_debugLogger.debugLogger.log("server", "closed pipe server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
PlaywrightPipeServer
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var playwrightWebSocketServer_exports = {};
|
||||||
|
__export(playwrightWebSocketServer_exports, {
|
||||||
|
PlaywrightWebSocketServer: () => PlaywrightWebSocketServer
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(playwrightWebSocketServer_exports);
|
||||||
|
var import_playwrightConnection = require("./playwrightConnection");
|
||||||
|
var import_serverTransport = require("./serverTransport");
|
||||||
|
var import_debugLogger = require("../server/utils/debugLogger");
|
||||||
|
var import_browser = require("../server/browser");
|
||||||
|
var import_utils = require("../utils");
|
||||||
|
var import_wsServer = require("../server/utils/wsServer");
|
||||||
|
class PlaywrightWebSocketServer {
|
||||||
|
constructor(browser, path) {
|
||||||
|
this._browser = browser;
|
||||||
|
browser.on(import_browser.Browser.Events.Disconnected, () => this.close());
|
||||||
|
const semaphore = new import_utils.Semaphore(Infinity);
|
||||||
|
this._wsServer = new import_wsServer.WSServer({
|
||||||
|
onRequest: (request, response) => {
|
||||||
|
response.end("Running");
|
||||||
|
},
|
||||||
|
onUpgrade: () => void 0,
|
||||||
|
onHeaders: () => {
|
||||||
|
},
|
||||||
|
onConnection: (request, url, ws, id) => {
|
||||||
|
import_debugLogger.debugLogger.log("server", `[${id}] ws client connected`);
|
||||||
|
return new import_playwrightConnection.PlaywrightConnection(
|
||||||
|
semaphore,
|
||||||
|
new import_serverTransport.WebSocketServerTransport(ws),
|
||||||
|
false,
|
||||||
|
this._browser.attribution.playwright,
|
||||||
|
() => this._initPreLaunchedBrowserMode(id),
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async _initPreLaunchedBrowserMode(id) {
|
||||||
|
import_debugLogger.debugLogger.log("server", `[${id}] engaged pre-launched (browser) ws mode`);
|
||||||
|
return {
|
||||||
|
preLaunchedBrowser: this._browser,
|
||||||
|
sharedBrowser: true,
|
||||||
|
denyLaunch: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async listen(port = 0, hostname, path) {
|
||||||
|
return await this._wsServer.listen(port, hostname, path || "/");
|
||||||
|
}
|
||||||
|
async close() {
|
||||||
|
await this._wsServer.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
PlaywrightWebSocketServer
|
||||||
|
});
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var serverTransport_exports = {};
|
||||||
|
__export(serverTransport_exports, {
|
||||||
|
SocketServerTransport: () => SocketServerTransport,
|
||||||
|
WebSocketServerTransport: () => WebSocketServerTransport
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(serverTransport_exports);
|
||||||
|
var import_events = require("events");
|
||||||
|
class WebSocketServerTransport {
|
||||||
|
constructor(ws) {
|
||||||
|
this._ws = ws;
|
||||||
|
}
|
||||||
|
send(message) {
|
||||||
|
this._ws.send(message);
|
||||||
|
}
|
||||||
|
close(reason) {
|
||||||
|
this._ws.close(reason?.code, reason?.reason);
|
||||||
|
}
|
||||||
|
on(event, handler) {
|
||||||
|
this._ws.on(event, handler);
|
||||||
|
}
|
||||||
|
isClosed() {
|
||||||
|
return this._ws.readyState === this._ws.CLOSING || this._ws.readyState === this._ws.CLOSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class SocketServerTransport extends import_events.EventEmitter {
|
||||||
|
constructor(socket) {
|
||||||
|
super();
|
||||||
|
this._closed = false;
|
||||||
|
this._pendingBuffers = [];
|
||||||
|
this._socket = socket;
|
||||||
|
socket.on("data", (buffer) => this._dispatch(buffer));
|
||||||
|
socket.on("close", () => {
|
||||||
|
this._closed = true;
|
||||||
|
super.emit("close");
|
||||||
|
});
|
||||||
|
socket.on("error", (error) => {
|
||||||
|
super.emit("error", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
send(message) {
|
||||||
|
if (this._closed)
|
||||||
|
return;
|
||||||
|
this._socket.write(message);
|
||||||
|
this._socket.write("\0");
|
||||||
|
}
|
||||||
|
close(reason) {
|
||||||
|
if (this._closed)
|
||||||
|
return;
|
||||||
|
this._closed = true;
|
||||||
|
this._socket.end();
|
||||||
|
}
|
||||||
|
isClosed() {
|
||||||
|
return this._closed;
|
||||||
|
}
|
||||||
|
_dispatch(buffer) {
|
||||||
|
let end = buffer.indexOf("\0");
|
||||||
|
if (end === -1) {
|
||||||
|
this._pendingBuffers.push(buffer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._pendingBuffers.push(buffer.slice(0, end));
|
||||||
|
const message = Buffer.concat(this._pendingBuffers).toString();
|
||||||
|
super.emit("message", message);
|
||||||
|
let start = end + 1;
|
||||||
|
end = buffer.indexOf("\0", start);
|
||||||
|
while (end !== -1) {
|
||||||
|
super.emit("message", buffer.toString(void 0, start, end));
|
||||||
|
start = end + 1;
|
||||||
|
end = buffer.indexOf("\0", start);
|
||||||
|
}
|
||||||
|
this._pendingBuffers = [buffer.slice(start)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
SocketServerTransport,
|
||||||
|
WebSocketServerTransport
|
||||||
|
});
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var debuggerDispatcher_exports = {};
|
||||||
|
__export(debuggerDispatcher_exports, {
|
||||||
|
DebuggerDispatcher: () => DebuggerDispatcher
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(debuggerDispatcher_exports);
|
||||||
|
var import_dispatcher = require("./dispatcher");
|
||||||
|
var import_debugger = require("../debugger");
|
||||||
|
var import_protocolFormatter = require("../../utils/isomorphic/protocolFormatter");
|
||||||
|
class DebuggerDispatcher extends import_dispatcher.Dispatcher {
|
||||||
|
constructor(scope, debugger_) {
|
||||||
|
super(scope, debugger_, "Debugger", {});
|
||||||
|
this._type_EventTarget = true;
|
||||||
|
this._type_Debugger = true;
|
||||||
|
this.addObjectListener(import_debugger.Debugger.Events.PausedStateChanged, () => {
|
||||||
|
this._dispatchEvent("pausedStateChanged", { pausedDetails: this._serializePausedDetails() });
|
||||||
|
});
|
||||||
|
this._dispatchEvent("pausedStateChanged", { pausedDetails: this._serializePausedDetails() });
|
||||||
|
}
|
||||||
|
static from(scope, debugger_) {
|
||||||
|
const result = scope.connection.existingDispatcher(debugger_);
|
||||||
|
return result || new DebuggerDispatcher(scope, debugger_);
|
||||||
|
}
|
||||||
|
_serializePausedDetails() {
|
||||||
|
const details = this._object.pausedDetails();
|
||||||
|
if (!details)
|
||||||
|
return void 0;
|
||||||
|
const { metadata } = details;
|
||||||
|
return {
|
||||||
|
location: {
|
||||||
|
file: metadata.location?.file ?? "<unknown>",
|
||||||
|
line: metadata.location?.line,
|
||||||
|
column: metadata.location?.column
|
||||||
|
},
|
||||||
|
title: (0, import_protocolFormatter.renderTitleForCall)(metadata)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async requestPause(params, progress) {
|
||||||
|
if (this._object.isPaused())
|
||||||
|
throw new Error("Debugger is already paused");
|
||||||
|
this._object.setPauseBeforeWaitingActions();
|
||||||
|
this._object.setPauseAt({ next: true });
|
||||||
|
}
|
||||||
|
async resume(params, progress) {
|
||||||
|
if (!this._object.isPaused())
|
||||||
|
throw new Error("Debugger is not paused");
|
||||||
|
this._object.resume();
|
||||||
|
}
|
||||||
|
async next(params, progress) {
|
||||||
|
if (!this._object.isPaused())
|
||||||
|
throw new Error("Debugger is not paused");
|
||||||
|
this._object.setPauseBeforeWaitingActions();
|
||||||
|
this._object.setPauseAt({ next: true });
|
||||||
|
this._object.resume();
|
||||||
|
}
|
||||||
|
async runTo(params, progress) {
|
||||||
|
if (!this._object.isPaused())
|
||||||
|
throw new Error("Debugger is not paused");
|
||||||
|
this._object.setPauseBeforeWaitingActions();
|
||||||
|
this._object.setPauseAt({ location: params.location });
|
||||||
|
this._object.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
DebuggerDispatcher
|
||||||
|
});
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var disposableDispatcher_exports = {};
|
||||||
|
__export(disposableDispatcher_exports, {
|
||||||
|
DisposableDispatcher: () => DisposableDispatcher
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(disposableDispatcher_exports);
|
||||||
|
var import_dispatcher = require("./dispatcher");
|
||||||
|
class DisposableDispatcher extends import_dispatcher.Dispatcher {
|
||||||
|
constructor(scope, disposable) {
|
||||||
|
super(scope, disposable, "Disposable", {});
|
||||||
|
this._type_Disposable = true;
|
||||||
|
}
|
||||||
|
async dispose(_, progress) {
|
||||||
|
progress.metadata.potentiallyClosesScope = true;
|
||||||
|
await this._object.dispose();
|
||||||
|
this._dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
DisposableDispatcher
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var disposable_exports = {};
|
||||||
|
__export(disposable_exports, {
|
||||||
|
DisposableObject: () => DisposableObject,
|
||||||
|
disposeAll: () => disposeAll
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(disposable_exports);
|
||||||
|
var import_instrumentation = require("./instrumentation");
|
||||||
|
class DisposableObject extends import_instrumentation.SdkObject {
|
||||||
|
constructor(parent) {
|
||||||
|
super(parent, "disposable");
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function disposeAll(disposables) {
|
||||||
|
const copy = [...disposables];
|
||||||
|
disposables.length = 0;
|
||||||
|
await Promise.all(copy.map((d) => d.dispose()));
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
DisposableObject,
|
||||||
|
disposeAll
|
||||||
|
});
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var overlay_exports = {};
|
||||||
|
__export(overlay_exports, {
|
||||||
|
Overlay: () => Overlay
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(overlay_exports);
|
||||||
|
var import_utils = require("../utils");
|
||||||
|
var import_page = require("./page");
|
||||||
|
class Overlay {
|
||||||
|
constructor(page) {
|
||||||
|
this._overlays = /* @__PURE__ */ new Map();
|
||||||
|
this._page = page;
|
||||||
|
this._page.on(import_page.Page.Events.InternalFrameNavigatedToNewDocument, (frame) => {
|
||||||
|
if (frame.parentFrame())
|
||||||
|
return;
|
||||||
|
for (const [id, html] of this._overlays)
|
||||||
|
this._doAdd(id, html).catch((e) => import_utils.debugLogger.log("error", e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dispose() {
|
||||||
|
}
|
||||||
|
async show(html, duration) {
|
||||||
|
const id = (0, import_utils.createGuid)();
|
||||||
|
this._overlays.set(id, html);
|
||||||
|
await this._doAdd(id, html).catch((e) => import_utils.debugLogger.log("error", e));
|
||||||
|
if (duration) {
|
||||||
|
await new Promise((f) => setTimeout(f, duration));
|
||||||
|
await this.remove(id);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
async _doAdd(id, html) {
|
||||||
|
const utility = await this._page.mainFrame()._utilityContext();
|
||||||
|
await utility.evaluate(({ injected, html: html2, id: id2 }) => {
|
||||||
|
return injected.addUserOverlay(id2, html2);
|
||||||
|
}, { injected: await utility.injectedScript(), html, id });
|
||||||
|
}
|
||||||
|
async remove(id) {
|
||||||
|
this._overlays.delete(id);
|
||||||
|
const utility = await this._page.mainFrame()._utilityContext();
|
||||||
|
await utility.evaluate(({ injected, id: id2 }) => {
|
||||||
|
injected.removeUserOverlay(id2);
|
||||||
|
}, { injected: await utility.injectedScript(), id }).catch((e) => import_utils.debugLogger.log("error", e));
|
||||||
|
}
|
||||||
|
async chapter(options) {
|
||||||
|
const fadeDuration = 300;
|
||||||
|
const descriptionHtml = options.description ? `<div id="description">${(0, import_utils.escapeHTML)(options.description)}</div>` : "";
|
||||||
|
const styleSheet = `
|
||||||
|
@keyframes pw-chapter-fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes pw-chapter-fade-out {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
#background {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
animation: pw-chapter-fade-in ${fadeDuration}ms ease-out forwards;
|
||||||
|
}
|
||||||
|
#background.fade-out {
|
||||||
|
animation: pw-chapter-fade-out ${fadeDuration}ms ease-in forwards;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px 56px;
|
||||||
|
max-width: 560px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
#title {
|
||||||
|
color: white;
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
#description {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const duration = options.duration ?? 2e3;
|
||||||
|
const html = `<style>${styleSheet}</style><div id="background"><div id="content"><div id="title">${(0, import_utils.escapeHTML)(options.title)}</div>${descriptionHtml}</div></div>`;
|
||||||
|
const id = await this.show(html);
|
||||||
|
await new Promise((f) => setTimeout(f, duration));
|
||||||
|
const utility = await this._page.mainFrame()._utilityContext();
|
||||||
|
await utility.evaluate(({ injected, id: id2, fadeDuration: fadeDuration2 }) => {
|
||||||
|
const overlay = injected.getUserOverlay(id2);
|
||||||
|
const bg = overlay?.querySelector("#background");
|
||||||
|
if (bg)
|
||||||
|
bg.classList.add("fade-out");
|
||||||
|
return new Promise((f) => injected.utils.builtins.setTimeout(f, fadeDuration2));
|
||||||
|
}, { injected: await utility.injectedScript(), id, fadeDuration }).catch((e) => import_utils.debugLogger.log("error", e));
|
||||||
|
await this.remove(id);
|
||||||
|
}
|
||||||
|
async setVisible(visible) {
|
||||||
|
if (!this._overlays.size)
|
||||||
|
return;
|
||||||
|
const utility = await this._page.mainFrame()._utilityContext();
|
||||||
|
await utility.evaluate(({ injected, visible: visible2 }) => {
|
||||||
|
injected.setUserOverlaysVisible(visible2);
|
||||||
|
}, { injected: await utility.injectedScript(), visible }).catch((e) => import_utils.debugLogger.log("error", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
Overlay
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var disposable_exports = {};
|
||||||
|
__export(disposable_exports, {
|
||||||
|
disposeAll: () => disposeAll
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(disposable_exports);
|
||||||
|
async function disposeAll(disposables) {
|
||||||
|
const copy = [...disposables];
|
||||||
|
disposables.length = 0;
|
||||||
|
await Promise.all(copy.map((d) => d.dispose()));
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
disposeAll
|
||||||
|
});
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var serverRegistry_exports = {};
|
||||||
|
__export(serverRegistry_exports, {
|
||||||
|
serverRegistry: () => serverRegistry
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(serverRegistry_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_net = __toESM(require("net"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_os = __toESM(require("os"));
|
||||||
|
const packageVersion = require("../package.json").version;
|
||||||
|
class ServerRegistry {
|
||||||
|
async list() {
|
||||||
|
const files = await import_fs.default.promises.readdir(this._browsersDir()).catch(() => []);
|
||||||
|
const result = /* @__PURE__ */ new Map();
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const filePath = import_path.default.join(this._browsersDir(), file);
|
||||||
|
const content = await import_fs.default.promises.readFile(filePath, "utf-8");
|
||||||
|
const descriptor = JSON.parse(content);
|
||||||
|
const key = descriptor.workspaceDir ?? "";
|
||||||
|
let list = result.get(key);
|
||||||
|
if (!list) {
|
||||||
|
list = [];
|
||||||
|
result.set(key, list);
|
||||||
|
}
|
||||||
|
list.push(canConnect(descriptor).then((connectable) => ({ ...descriptor, canConnect: connectable, file: filePath })));
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const resolvedResult = /* @__PURE__ */ new Map();
|
||||||
|
for (const [key, promises] of result) {
|
||||||
|
const entries = await Promise.all(promises);
|
||||||
|
const descriptors = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.canConnect && !entry.browser.userDataDir) {
|
||||||
|
await import_fs.default.promises.unlink(entry.file).catch(() => {
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
descriptors.push(entry);
|
||||||
|
}
|
||||||
|
if (descriptors.length)
|
||||||
|
resolvedResult.set(key, descriptors);
|
||||||
|
}
|
||||||
|
return resolvedResult;
|
||||||
|
}
|
||||||
|
async create(browser, endpoint) {
|
||||||
|
const file = import_path.default.join(this._browsersDir(), browser.guid);
|
||||||
|
await import_fs.default.promises.mkdir(this._browsersDir(), { recursive: true });
|
||||||
|
const descriptor = {
|
||||||
|
playwrightVersion: packageVersion,
|
||||||
|
playwrightLib: require.resolve(".."),
|
||||||
|
title: endpoint.title,
|
||||||
|
browser,
|
||||||
|
endpoint: endpoint.endpoint,
|
||||||
|
workspaceDir: endpoint.workspaceDir
|
||||||
|
};
|
||||||
|
await import_fs.default.promises.writeFile(file, JSON.stringify(descriptor), "utf-8");
|
||||||
|
}
|
||||||
|
async delete(guid) {
|
||||||
|
const file = import_path.default.join(this._browsersDir(), guid);
|
||||||
|
await import_fs.default.promises.unlink(file).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async deleteUserData(guid) {
|
||||||
|
const filePath = import_path.default.join(this._browsersDir(), guid);
|
||||||
|
const content = await import_fs.default.promises.readFile(filePath, "utf-8");
|
||||||
|
const descriptor = JSON.parse(content);
|
||||||
|
if (descriptor.browser.userDataDir)
|
||||||
|
await import_fs.default.promises.rm(descriptor.browser.userDataDir, { recursive: true, force: true });
|
||||||
|
await import_fs.default.promises.unlink(filePath);
|
||||||
|
}
|
||||||
|
readDescriptor(guid) {
|
||||||
|
const filePath = import_path.default.join(this._browsersDir(), guid);
|
||||||
|
const content = import_fs.default.readFileSync(filePath, "utf-8");
|
||||||
|
const descriptor = JSON.parse(content);
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
async find(name) {
|
||||||
|
const entries = await this.list();
|
||||||
|
for (const [, browsers] of entries) {
|
||||||
|
for (const browser of browsers) {
|
||||||
|
if (browser.title === name)
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_browsersDir() {
|
||||||
|
return process.env.PLAYWRIGHT_SERVER_REGISTRY || registryDirectory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function canConnect(descriptor) {
|
||||||
|
if (!descriptor.endpoint)
|
||||||
|
return false;
|
||||||
|
if (descriptor.endpoint.startsWith("ws://") || descriptor.endpoint.startsWith("wss://")) {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
const url = new URL(descriptor.endpoint);
|
||||||
|
const socket = import_net.default.createConnection(Number(url.port), url.hostname, () => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
socket.on("error", () => resolve(false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
const socket = import_net.default.createConnection(descriptor.endpoint ?? descriptor.pipeName, () => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
socket.on("error", () => resolve(false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const defaultCacheDirectory = (() => {
|
||||||
|
if (process.platform === "linux")
|
||||||
|
return process.env.XDG_CACHE_HOME || import_path.default.join(import_os.default.homedir(), ".cache");
|
||||||
|
if (process.platform === "darwin")
|
||||||
|
return import_path.default.join(import_os.default.homedir(), "Library", "Caches");
|
||||||
|
if (process.platform === "win32")
|
||||||
|
return process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local");
|
||||||
|
throw new Error("Unsupported platform: " + process.platform);
|
||||||
|
})();
|
||||||
|
const registryDirectory = import_path.default.join(defaultCacheDirectory, "ms-playwright", "b");
|
||||||
|
const serverRegistry = new ServerRegistry();
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
serverRegistry
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var browserBackend_exports = {};
|
||||||
|
__export(browserBackend_exports, {
|
||||||
|
BrowserBackend: () => BrowserBackend
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(browserBackend_exports);
|
||||||
|
var import_context = require("./context");
|
||||||
|
var import_response = require("./response");
|
||||||
|
var import_sessionLog = require("./sessionLog");
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
class BrowserBackend {
|
||||||
|
constructor(config, browserContext, tools) {
|
||||||
|
this._config = config;
|
||||||
|
this._tools = tools;
|
||||||
|
this.browserContext = browserContext;
|
||||||
|
}
|
||||||
|
async initialize(clientInfo) {
|
||||||
|
this._sessionLog = this._config.saveSession ? await import_sessionLog.SessionLog.create(this._config, clientInfo.cwd) : void 0;
|
||||||
|
this._context = new import_context.Context(this.browserContext, {
|
||||||
|
config: this._config,
|
||||||
|
sessionLog: this._sessionLog,
|
||||||
|
cwd: clientInfo.cwd
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async dispose() {
|
||||||
|
await this._context?.dispose().catch((e) => (0, import_utilsBundle.debug)("pw:tools:error")(e));
|
||||||
|
}
|
||||||
|
async callTool(name, rawArguments = {}) {
|
||||||
|
const tool = this._tools.find((tool2) => tool2.schema.name === name);
|
||||||
|
if (!tool) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `### Error
|
||||||
|
Tool "${name}" not found` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const parsedArguments = tool.schema.inputSchema.parse(rawArguments);
|
||||||
|
const cwd = rawArguments._meta?.cwd;
|
||||||
|
const context = this._context;
|
||||||
|
const response = new import_response.Response(context, name, parsedArguments, cwd);
|
||||||
|
context.setRunningTool(name);
|
||||||
|
let responseObject;
|
||||||
|
try {
|
||||||
|
await tool.handle(context, parsedArguments, response);
|
||||||
|
responseObject = await response.serialize();
|
||||||
|
this._sessionLog?.logResponse(name, parsedArguments, responseObject);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `### Error
|
||||||
|
${String(error)}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
context.setRunningTool(void 0);
|
||||||
|
}
|
||||||
|
return responseObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
BrowserBackend
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var common_exports = {};
|
||||||
|
__export(common_exports, {
|
||||||
|
default: () => common_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(common_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
var import_response = require("./response");
|
||||||
|
const close = (0, import_tool.defineTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_close",
|
||||||
|
title: "Close browser",
|
||||||
|
description: "Close the page",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const result = (0, import_response.renderTabsMarkdown)([]);
|
||||||
|
response.addTextResult(result.join("\n"));
|
||||||
|
response.addCode(`await page.close()`);
|
||||||
|
response.setClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const resize = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_resize",
|
||||||
|
title: "Resize browser window",
|
||||||
|
description: "Resize the browser window",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
width: import_zodBundle.z.number().describe("Width of the browser window"),
|
||||||
|
height: import_zodBundle.z.number().describe("Height of the browser window")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
|
||||||
|
await tab.page.setViewportSize({ width: params.width, height: params.height });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var common_default = [
|
||||||
|
close,
|
||||||
|
resize
|
||||||
|
];
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var config_exports = {};
|
||||||
|
__export(config_exports, {
|
||||||
|
default: () => config_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(config_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const configShow = (0, import_tool.defineTool)({
|
||||||
|
capability: "config",
|
||||||
|
schema: {
|
||||||
|
name: "browser_get_config",
|
||||||
|
title: "Get config",
|
||||||
|
description: "Get the final resolved config after merging CLI options, environment variables and config file.",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
response.addTextResult(JSON.stringify(context.config, null, 2));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var config_default = [
|
||||||
|
configShow
|
||||||
|
];
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var console_exports = {};
|
||||||
|
__export(console_exports, {
|
||||||
|
default: () => console_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(console_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const console = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_console_messages",
|
||||||
|
title: "Get console messages",
|
||||||
|
description: "Returns all console messages",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
level: import_zodBundle.z.enum(["error", "warning", "info", "debug"]).default("info").describe('Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info".'),
|
||||||
|
all: import_zodBundle.z.boolean().optional().describe("Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false."),
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("Filename to save the console messages to. If not provided, messages are returned as text.")
|
||||||
|
}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const count = await tab.consoleMessageCount();
|
||||||
|
const header = [`Total messages: ${count.total} (Errors: ${count.errors}, Warnings: ${count.warnings})`];
|
||||||
|
const messages = await tab.consoleMessages(params.level, params.all);
|
||||||
|
if (messages.length !== count.total)
|
||||||
|
header.push(`Returning ${messages.length} messages for level "${params.level}"`);
|
||||||
|
const text = [...header, "", ...messages.map((message) => message.toString())].join("\n");
|
||||||
|
await response.addResult("Console", text, { prefix: "console", ext: "log", suggestedFilename: params.filename });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const consoleClear = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
skillOnly: true,
|
||||||
|
schema: {
|
||||||
|
name: "browser_console_clear",
|
||||||
|
title: "Clear console messages",
|
||||||
|
description: "Clear all console messages",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab) => {
|
||||||
|
await tab.clearConsoleMessages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var console_default = [
|
||||||
|
console,
|
||||||
|
consoleClear
|
||||||
|
];
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var context_exports = {};
|
||||||
|
__export(context_exports, {
|
||||||
|
Context: () => Context,
|
||||||
|
outputDir: () => outputDir,
|
||||||
|
outputFile: () => outputFile,
|
||||||
|
workspaceFile: () => workspaceFile
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(context_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
var import_stringUtils = require("../../utils/isomorphic/stringUtils");
|
||||||
|
var import__ = require("../../..");
|
||||||
|
var import_tab = require("./tab");
|
||||||
|
var import_disposable = require("../../server/utils/disposable");
|
||||||
|
var import_eventsHelper = require("../../server/utils/eventsHelper");
|
||||||
|
const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
|
||||||
|
class Context {
|
||||||
|
constructor(browserContext, options) {
|
||||||
|
this._tabs = [];
|
||||||
|
this._routes = [];
|
||||||
|
this._disposables = [];
|
||||||
|
this.config = options.config;
|
||||||
|
this.sessionLog = options.sessionLog;
|
||||||
|
this.options = options;
|
||||||
|
this._rawBrowserContext = browserContext;
|
||||||
|
testDebug("create context");
|
||||||
|
}
|
||||||
|
async dispose() {
|
||||||
|
await (0, import_disposable.disposeAll)(this._disposables);
|
||||||
|
for (const tab of this._tabs)
|
||||||
|
await tab.dispose();
|
||||||
|
this._tabs.length = 0;
|
||||||
|
this._currentTab = void 0;
|
||||||
|
await this.stopVideoRecording();
|
||||||
|
}
|
||||||
|
debugger() {
|
||||||
|
return this._rawBrowserContext.debugger;
|
||||||
|
}
|
||||||
|
tabs() {
|
||||||
|
return this._tabs;
|
||||||
|
}
|
||||||
|
currentTab() {
|
||||||
|
return this._currentTab;
|
||||||
|
}
|
||||||
|
currentTabOrDie() {
|
||||||
|
if (!this._currentTab)
|
||||||
|
throw new Error("No open pages available.");
|
||||||
|
return this._currentTab;
|
||||||
|
}
|
||||||
|
async newTab() {
|
||||||
|
const browserContext = await this.ensureBrowserContext();
|
||||||
|
const page = await browserContext.newPage();
|
||||||
|
this._currentTab = this._tabs.find((t) => t.page === page);
|
||||||
|
return this._currentTab;
|
||||||
|
}
|
||||||
|
async selectTab(index) {
|
||||||
|
const tab = this._tabs[index];
|
||||||
|
if (!tab)
|
||||||
|
throw new Error(`Tab ${index} not found`);
|
||||||
|
await tab.page.bringToFront();
|
||||||
|
this._currentTab = tab;
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
async ensureTab() {
|
||||||
|
const browserContext = await this.ensureBrowserContext();
|
||||||
|
if (!this._currentTab)
|
||||||
|
await browserContext.newPage();
|
||||||
|
return this._currentTab;
|
||||||
|
}
|
||||||
|
async closeTab(index) {
|
||||||
|
const tab = index === void 0 ? this._currentTab : this._tabs[index];
|
||||||
|
if (!tab)
|
||||||
|
throw new Error(`Tab ${index} not found`);
|
||||||
|
const url = tab.page.url();
|
||||||
|
await tab.page.close();
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
async workspaceFile(fileName, perCallWorkspaceDir) {
|
||||||
|
return await workspaceFile(this.options, fileName, perCallWorkspaceDir);
|
||||||
|
}
|
||||||
|
async outputFile(template, options) {
|
||||||
|
const baseName = template.suggestedFilename || `${template.prefix}-${(template.date ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}${template.ext ? "." + template.ext : ""}`;
|
||||||
|
return await outputFile(this.options, baseName, options);
|
||||||
|
}
|
||||||
|
async startVideoRecording(fileName, params) {
|
||||||
|
if (this._video)
|
||||||
|
throw new Error("Video recording has already been started.");
|
||||||
|
this._video = { params, fileName, fileNames: [] };
|
||||||
|
const browserContext = await this.ensureBrowserContext();
|
||||||
|
for (const page of browserContext.pages())
|
||||||
|
await this._startPageVideo(page);
|
||||||
|
}
|
||||||
|
async stopVideoRecording() {
|
||||||
|
if (!this._video)
|
||||||
|
return [];
|
||||||
|
const video = this._video;
|
||||||
|
for (const page of this._rawBrowserContext.pages())
|
||||||
|
await page.screencast.stop();
|
||||||
|
this._video = void 0;
|
||||||
|
return [...video.fileNames];
|
||||||
|
}
|
||||||
|
async _startPageVideo(page) {
|
||||||
|
if (!this._video)
|
||||||
|
return;
|
||||||
|
const suffix = this._video.fileNames.length ? `-${this._video.fileNames.length}` : "";
|
||||||
|
let fileName = this._video.fileName;
|
||||||
|
if (fileName && suffix) {
|
||||||
|
const ext = import_path.default.extname(fileName);
|
||||||
|
fileName = import_path.default.basename(fileName, ext) + suffix + ext;
|
||||||
|
}
|
||||||
|
this._video.fileNames.push(fileName);
|
||||||
|
await page.screencast.start({ path: fileName, ...this._video.params });
|
||||||
|
}
|
||||||
|
_onPageCreated(page) {
|
||||||
|
const tab = new import_tab.Tab(this, page, (tab2) => this._onPageClosed(tab2));
|
||||||
|
this._tabs.push(tab);
|
||||||
|
if (!this._currentTab)
|
||||||
|
this._currentTab = tab;
|
||||||
|
this._startPageVideo(page).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_onPageClosed(tab) {
|
||||||
|
const index = this._tabs.indexOf(tab);
|
||||||
|
if (index === -1)
|
||||||
|
return;
|
||||||
|
this._tabs.splice(index, 1);
|
||||||
|
if (this._currentTab === tab)
|
||||||
|
this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
|
||||||
|
}
|
||||||
|
routes() {
|
||||||
|
return this._routes;
|
||||||
|
}
|
||||||
|
async addRoute(entry) {
|
||||||
|
const browserContext = await this.ensureBrowserContext();
|
||||||
|
await browserContext.route(entry.pattern, entry.handler);
|
||||||
|
this._routes.push(entry);
|
||||||
|
}
|
||||||
|
async removeRoute(pattern) {
|
||||||
|
let removed = 0;
|
||||||
|
const browserContext = await this.ensureBrowserContext();
|
||||||
|
if (pattern) {
|
||||||
|
const toRemove = this._routes.filter((r) => r.pattern === pattern);
|
||||||
|
for (const route of toRemove)
|
||||||
|
await browserContext.unroute(route.pattern, route.handler);
|
||||||
|
this._routes = this._routes.filter((r) => r.pattern !== pattern);
|
||||||
|
removed = toRemove.length;
|
||||||
|
} else {
|
||||||
|
for (const route of this._routes)
|
||||||
|
await browserContext.unroute(route.pattern, route.handler);
|
||||||
|
removed = this._routes.length;
|
||||||
|
this._routes = [];
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
isRunningTool() {
|
||||||
|
return this._runningToolName !== void 0;
|
||||||
|
}
|
||||||
|
setRunningTool(name) {
|
||||||
|
this._runningToolName = name;
|
||||||
|
}
|
||||||
|
async _setupRequestInterception(context) {
|
||||||
|
if (this.config.network?.allowedOrigins?.length) {
|
||||||
|
this._disposables.push(await context.route("**", (route) => route.abort("blockedbyclient")));
|
||||||
|
for (const origin of this.config.network.allowedOrigins) {
|
||||||
|
const glob = originOrHostGlob(origin);
|
||||||
|
this._disposables.push(await context.route(glob, (route) => route.continue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.config.network?.blockedOrigins?.length) {
|
||||||
|
for (const origin of this.config.network.blockedOrigins)
|
||||||
|
this._disposables.push(await context.route(originOrHostGlob(origin), (route) => route.abort("blockedbyclient")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async ensureBrowserContext() {
|
||||||
|
if (this._browserContextPromise)
|
||||||
|
return this._browserContextPromise;
|
||||||
|
this._browserContextPromise = this._initializeBrowserContext();
|
||||||
|
return this._browserContextPromise;
|
||||||
|
}
|
||||||
|
async _initializeBrowserContext() {
|
||||||
|
if (this.config.testIdAttribute)
|
||||||
|
import__.selectors.setTestIdAttribute(this.config.testIdAttribute);
|
||||||
|
const browserContext = this._rawBrowserContext;
|
||||||
|
await this._setupRequestInterception(browserContext);
|
||||||
|
if (this.config.saveTrace) {
|
||||||
|
await browserContext.tracing.start({
|
||||||
|
name: "trace-" + Date.now(),
|
||||||
|
screenshots: true,
|
||||||
|
snapshots: true,
|
||||||
|
live: true
|
||||||
|
});
|
||||||
|
this._disposables.push({
|
||||||
|
dispose: async () => {
|
||||||
|
await browserContext.tracing.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const initScript of this.config.browser?.initScript || [])
|
||||||
|
this._disposables.push(await browserContext.addInitScript({ path: import_path.default.resolve(this.options.cwd, initScript) }));
|
||||||
|
for (const page of browserContext.pages())
|
||||||
|
this._onPageCreated(page);
|
||||||
|
this._disposables.push(import_eventsHelper.eventsHelper.addEventListener(browserContext, "page", (page) => this._onPageCreated(page)));
|
||||||
|
return browserContext;
|
||||||
|
}
|
||||||
|
checkUrlAllowed(url) {
|
||||||
|
if (this.config.allowUnrestrictedFileAccess)
|
||||||
|
return;
|
||||||
|
if (!URL.canParse(url))
|
||||||
|
return;
|
||||||
|
if (new URL(url).protocol === "file:")
|
||||||
|
throw new Error(`Access to "file:" protocol is blocked. Attempted URL: "${url}"`);
|
||||||
|
}
|
||||||
|
lookupSecret(secretName) {
|
||||||
|
if (!this.config.secrets?.[secretName])
|
||||||
|
return { value: secretName, code: (0, import_stringUtils.escapeWithQuotes)(secretName, "'") };
|
||||||
|
return {
|
||||||
|
value: this.config.secrets[secretName],
|
||||||
|
code: `process.env['${secretName}']`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function originOrHostGlob(originOrHost) {
|
||||||
|
const wildcardPortMatch = originOrHost.match(/^(https?:\/\/[^/:]+):\*$/);
|
||||||
|
if (wildcardPortMatch)
|
||||||
|
return `${wildcardPortMatch[1]}:*/**`;
|
||||||
|
try {
|
||||||
|
const url = new URL(originOrHost);
|
||||||
|
if (url.origin !== "null")
|
||||||
|
return `${url.origin}/**`;
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
return `*://${originOrHost}/**`;
|
||||||
|
}
|
||||||
|
async function workspaceFile(options, fileName, perCallWorkspaceDir) {
|
||||||
|
const workspace = perCallWorkspaceDir ?? options.cwd;
|
||||||
|
const resolvedName = import_path.default.resolve(workspace, fileName);
|
||||||
|
await checkFile(options, resolvedName, { origin: "llm" });
|
||||||
|
return resolvedName;
|
||||||
|
}
|
||||||
|
function outputDir(options) {
|
||||||
|
if (options.config.outputDir)
|
||||||
|
return import_path.default.resolve(options.config.outputDir);
|
||||||
|
return import_path.default.resolve(options.cwd, options.config.skillMode ? ".playwright-cli" : ".playwright-mcp");
|
||||||
|
}
|
||||||
|
async function outputFile(options, fileName, flags) {
|
||||||
|
const resolvedFile = import_path.default.resolve(outputDir(options), fileName);
|
||||||
|
await checkFile(options, resolvedFile, flags);
|
||||||
|
await import_fs.default.promises.mkdir(import_path.default.dirname(resolvedFile), { recursive: true });
|
||||||
|
(0, import_utilsBundle.debug)("pw:mcp:file")(resolvedFile);
|
||||||
|
return resolvedFile;
|
||||||
|
}
|
||||||
|
async function checkFile(options, resolvedFilename, flags) {
|
||||||
|
if (flags.origin === "code" || options.config.allowUnrestrictedFileAccess)
|
||||||
|
return;
|
||||||
|
const output = outputDir(options);
|
||||||
|
const workspace = options.cwd;
|
||||||
|
const withinDir = (root) => resolvedFilename === root || resolvedFilename.startsWith(root + import_path.default.sep);
|
||||||
|
if (!withinDir(output) && !withinDir(workspace))
|
||||||
|
throw new Error(`File access denied: ${resolvedFilename} is outside allowed roots. Allowed roots: ${output}, ${workspace}`);
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
Context,
|
||||||
|
outputDir,
|
||||||
|
outputFile,
|
||||||
|
workspaceFile
|
||||||
|
});
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var cookies_exports = {};
|
||||||
|
__export(cookies_exports, {
|
||||||
|
default: () => cookies_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(cookies_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const cookieList = (0, import_tool.defineTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_cookie_list",
|
||||||
|
title: "List cookies",
|
||||||
|
description: "List all cookies (optionally filtered by domain/path)",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
domain: import_zodBundle.z.string().optional().describe("Filter cookies by domain"),
|
||||||
|
path: import_zodBundle.z.string().optional().describe("Filter cookies by path")
|
||||||
|
}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
let cookies = await browserContext.cookies();
|
||||||
|
if (params.domain)
|
||||||
|
cookies = cookies.filter((c) => c.domain.includes(params.domain));
|
||||||
|
if (params.path)
|
||||||
|
cookies = cookies.filter((c) => c.path.startsWith(params.path));
|
||||||
|
if (cookies.length === 0)
|
||||||
|
response.addTextResult("No cookies found");
|
||||||
|
else
|
||||||
|
response.addTextResult(cookies.map((c) => `${c.name}=${c.value} (domain: ${c.domain}, path: ${c.path})`).join("\n"));
|
||||||
|
response.addCode(`await page.context().cookies();`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const cookieGet = (0, import_tool.defineTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_cookie_get",
|
||||||
|
title: "Get cookie",
|
||||||
|
description: "Get a specific cookie by name",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
name: import_zodBundle.z.string().describe("Cookie name to get")
|
||||||
|
}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
const cookies = await browserContext.cookies();
|
||||||
|
const cookie = cookies.find((c) => c.name === params.name);
|
||||||
|
if (!cookie)
|
||||||
|
response.addTextResult(`Cookie '${params.name}' not found`);
|
||||||
|
else
|
||||||
|
response.addTextResult(`${cookie.name}=${cookie.value} (domain: ${cookie.domain}, path: ${cookie.path}, httpOnly: ${cookie.httpOnly}, secure: ${cookie.secure}, sameSite: ${cookie.sameSite})`);
|
||||||
|
response.addCode(`await page.context().cookies();`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const cookieSet = (0, import_tool.defineTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_cookie_set",
|
||||||
|
title: "Set cookie",
|
||||||
|
description: "Set a cookie with optional flags (domain, path, expires, httpOnly, secure, sameSite)",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
name: import_zodBundle.z.string().describe("Cookie name"),
|
||||||
|
value: import_zodBundle.z.string().describe("Cookie value"),
|
||||||
|
domain: import_zodBundle.z.string().optional().describe("Cookie domain"),
|
||||||
|
path: import_zodBundle.z.string().optional().describe("Cookie path"),
|
||||||
|
expires: import_zodBundle.z.number().optional().describe("Cookie expiration as Unix timestamp"),
|
||||||
|
httpOnly: import_zodBundle.z.boolean().optional().describe("Whether the cookie is HTTP only"),
|
||||||
|
secure: import_zodBundle.z.boolean().optional().describe("Whether the cookie is secure"),
|
||||||
|
sameSite: import_zodBundle.z.enum(["Strict", "Lax", "None"]).optional().describe("Cookie SameSite attribute")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
const tab = await context.ensureTab();
|
||||||
|
const url = new URL(tab.page.url());
|
||||||
|
const cookie = {
|
||||||
|
name: params.name,
|
||||||
|
value: params.value,
|
||||||
|
domain: params.domain || url.hostname,
|
||||||
|
path: params.path || "/"
|
||||||
|
};
|
||||||
|
if (params.expires !== void 0)
|
||||||
|
cookie.expires = params.expires;
|
||||||
|
if (params.httpOnly !== void 0)
|
||||||
|
cookie.httpOnly = params.httpOnly;
|
||||||
|
if (params.secure !== void 0)
|
||||||
|
cookie.secure = params.secure;
|
||||||
|
if (params.sameSite !== void 0)
|
||||||
|
cookie.sameSite = params.sameSite;
|
||||||
|
await browserContext.addCookies([cookie]);
|
||||||
|
response.addCode(`await page.context().addCookies([${JSON.stringify(cookie)}]);`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const cookieDelete = (0, import_tool.defineTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_cookie_delete",
|
||||||
|
title: "Delete cookie",
|
||||||
|
description: "Delete a specific cookie",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
name: import_zodBundle.z.string().describe("Cookie name to delete")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
await browserContext.clearCookies({ name: params.name });
|
||||||
|
response.addCode(`await page.context().clearCookies({ name: '${params.name}' });`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const cookieClear = (0, import_tool.defineTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_cookie_clear",
|
||||||
|
title: "Clear cookies",
|
||||||
|
description: "Clear all cookies",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
await browserContext.clearCookies();
|
||||||
|
response.addCode(`await page.context().clearCookies();`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var cookies_default = [
|
||||||
|
cookieList,
|
||||||
|
cookieGet,
|
||||||
|
cookieSet,
|
||||||
|
cookieDelete,
|
||||||
|
cookieClear
|
||||||
|
];
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var devtools_exports = {};
|
||||||
|
__export(devtools_exports, {
|
||||||
|
default: () => devtools_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(devtools_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const resume = (0, import_tool.defineTool)({
|
||||||
|
capability: "devtools",
|
||||||
|
schema: {
|
||||||
|
name: "browser_resume",
|
||||||
|
title: "Resume paused script execution",
|
||||||
|
description: "Resume script execution after it was paused. When called with step set to true, execution will pause again before the next action.",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
step: import_zodBundle.z.boolean().optional().describe("When true, execution will pause again before the next action, allowing step-by-step debugging."),
|
||||||
|
location: import_zodBundle.z.string().optional().describe('Pause execution at a specific <file>:<line>, e.g. "example.spec.ts:42".')
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
const pausedPromise = new Promise((resolve) => {
|
||||||
|
const listener = () => {
|
||||||
|
if (browserContext.debugger.pausedDetails()) {
|
||||||
|
browserContext.debugger.off("pausedstatechanged", listener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
browserContext.debugger.on("pausedstatechanged", listener);
|
||||||
|
});
|
||||||
|
if (params.location) {
|
||||||
|
const [file, lineStr] = params.location.split(":");
|
||||||
|
let location;
|
||||||
|
if (lineStr) {
|
||||||
|
const line = Number(lineStr);
|
||||||
|
if (isNaN(line))
|
||||||
|
throw new Error(`Invalid location "${params.location}", expected format is <file>:<line>, e.g. "example.spec.ts:42"`);
|
||||||
|
location = { file, line };
|
||||||
|
} else {
|
||||||
|
location = { file: params.location };
|
||||||
|
}
|
||||||
|
await browserContext.debugger.runTo(location);
|
||||||
|
} else if (params.step) {
|
||||||
|
await browserContext.debugger.next();
|
||||||
|
} else {
|
||||||
|
await browserContext.debugger.resume();
|
||||||
|
}
|
||||||
|
await pausedPromise;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var devtools_default = [resume];
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var dialogs_exports = {};
|
||||||
|
__export(dialogs_exports, {
|
||||||
|
default: () => dialogs_default,
|
||||||
|
handleDialog: () => handleDialog
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(dialogs_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const handleDialog = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_handle_dialog",
|
||||||
|
title: "Handle a dialog",
|
||||||
|
description: "Handle a dialog",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
accept: import_zodBundle.z.boolean().describe("Whether to accept the dialog."),
|
||||||
|
promptText: import_zodBundle.z.string().optional().describe("The text of the prompt in case of a prompt dialog.")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const dialogState = tab.modalStates().find((state) => state.type === "dialog");
|
||||||
|
if (!dialogState)
|
||||||
|
throw new Error("No dialog visible");
|
||||||
|
tab.clearModalState(dialogState);
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
if (params.accept)
|
||||||
|
await dialogState.dialog.accept(params.promptText);
|
||||||
|
else
|
||||||
|
await dialogState.dialog.dismiss();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearsModalState: "dialog"
|
||||||
|
});
|
||||||
|
var dialogs_default = [
|
||||||
|
handleDialog
|
||||||
|
];
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
handleDialog
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var evaluate_exports = {};
|
||||||
|
__export(evaluate_exports, {
|
||||||
|
default: () => evaluate_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(evaluate_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_stringUtils = require("../../utils/isomorphic/stringUtils");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const evaluateSchema = import_zodBundle.z.object({
|
||||||
|
function: import_zodBundle.z.string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
|
||||||
|
element: import_zodBundle.z.string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
|
||||||
|
ref: import_zodBundle.z.string().optional().describe("Exact target element reference from the page snapshot"),
|
||||||
|
selector: import_zodBundle.z.string().optional().describe('CSS or role selector for the target element, when "ref" is not available.'),
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("Filename to save the result to. If not provided, result is returned as text.")
|
||||||
|
});
|
||||||
|
const evaluate = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_evaluate",
|
||||||
|
title: "Evaluate JavaScript",
|
||||||
|
description: "Evaluate JavaScript expression on page or element",
|
||||||
|
inputSchema: evaluateSchema,
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
let locator;
|
||||||
|
if (!params.function.includes("=>"))
|
||||||
|
params.function = `() => (${params.function})`;
|
||||||
|
if (params.ref) {
|
||||||
|
locator = await tab.refLocator({ ref: params.ref, selector: params.selector, element: params.element || "element" });
|
||||||
|
response.addCode(`await page.${locator.resolved}.evaluate(${(0, import_stringUtils.escapeWithQuotes)(params.function)});`);
|
||||||
|
} else {
|
||||||
|
response.addCode(`await page.evaluate(${(0, import_stringUtils.escapeWithQuotes)(params.function)});`);
|
||||||
|
}
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
const func = new Function();
|
||||||
|
func.toString = () => params.function;
|
||||||
|
const result = locator?.locator ? await locator?.locator.evaluate(func) : await tab.page.evaluate(func);
|
||||||
|
const text = JSON.stringify(result, null, 2) || "undefined";
|
||||||
|
await response.addResult("Evaluation result", text, { prefix: "result", ext: "json", suggestedFilename: params.filename });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var evaluate_default = [
|
||||||
|
evaluate
|
||||||
|
];
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var files_exports = {};
|
||||||
|
__export(files_exports, {
|
||||||
|
default: () => files_default,
|
||||||
|
uploadFile: () => uploadFile
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(files_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const uploadFile = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_file_upload",
|
||||||
|
title: "Upload files",
|
||||||
|
description: "Upload one or multiple files",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
paths: import_zodBundle.z.array(import_zodBundle.z.string()).optional().describe("The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
const modalState = tab.modalStates().find((state) => state.type === "fileChooser");
|
||||||
|
if (!modalState)
|
||||||
|
throw new Error("No file chooser visible");
|
||||||
|
if (params.paths)
|
||||||
|
await Promise.all(params.paths.map((filePath) => response.resolveClientFilename(filePath)));
|
||||||
|
response.addCode(`await fileChooser.setFiles(${JSON.stringify(params.paths)})`);
|
||||||
|
tab.clearModalState(modalState);
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
if (params.paths)
|
||||||
|
await modalState.fileChooser.setFiles(params.paths);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearsModalState: "fileChooser"
|
||||||
|
});
|
||||||
|
var files_default = [
|
||||||
|
uploadFile
|
||||||
|
];
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
uploadFile
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var form_exports = {};
|
||||||
|
__export(form_exports, {
|
||||||
|
default: () => form_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(form_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_stringUtils = require("../../utils/isomorphic/stringUtils");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const fillForm = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_fill_form",
|
||||||
|
title: "Fill form",
|
||||||
|
description: "Fill multiple form fields",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
fields: import_zodBundle.z.array(import_zodBundle.z.object({
|
||||||
|
name: import_zodBundle.z.string().describe("Human-readable field name"),
|
||||||
|
type: import_zodBundle.z.enum(["textbox", "checkbox", "radio", "combobox", "slider"]).describe("Type of the field"),
|
||||||
|
ref: import_zodBundle.z.string().describe("Exact target field reference from the page snapshot"),
|
||||||
|
selector: import_zodBundle.z.string().optional().describe('CSS or role selector for the field element, when "ref" is not available. Either "selector" or "ref" is required.'),
|
||||||
|
value: import_zodBundle.z.string().describe("Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.")
|
||||||
|
})).describe("Fields to fill in")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
for (const field of params.fields) {
|
||||||
|
const { locator, resolved } = await tab.refLocator({ element: field.name, ref: field.ref, selector: field.selector });
|
||||||
|
const locatorSource = `await page.${resolved}`;
|
||||||
|
if (field.type === "textbox" || field.type === "slider") {
|
||||||
|
const secret = tab.context.lookupSecret(field.value);
|
||||||
|
await locator.fill(secret.value, tab.actionTimeoutOptions);
|
||||||
|
response.addCode(`${locatorSource}.fill(${secret.code});`);
|
||||||
|
} else if (field.type === "checkbox" || field.type === "radio") {
|
||||||
|
await locator.setChecked(field.value === "true", tab.actionTimeoutOptions);
|
||||||
|
response.addCode(`${locatorSource}.setChecked(${field.value});`);
|
||||||
|
} else if (field.type === "combobox") {
|
||||||
|
await locator.selectOption({ label: field.value }, tab.actionTimeoutOptions);
|
||||||
|
response.addCode(`${locatorSource}.selectOption(${(0, import_stringUtils.escapeWithQuotes)(field.value)});`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var form_default = [
|
||||||
|
fillForm
|
||||||
|
];
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var keyboard_exports = {};
|
||||||
|
__export(keyboard_exports, {
|
||||||
|
default: () => keyboard_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(keyboard_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
var import_snapshot = require("./snapshot");
|
||||||
|
const press = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core-input",
|
||||||
|
schema: {
|
||||||
|
name: "browser_press_key",
|
||||||
|
title: "Press a key",
|
||||||
|
description: "Press a key on the keyboard",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.addCode(`// Press ${params.key}`);
|
||||||
|
response.addCode(`await page.keyboard.press('${params.key}');`);
|
||||||
|
if (params.key === "Enter") {
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
await tab.page.keyboard.press("Enter");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await tab.page.keyboard.press(params.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const pressSequentially = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core-input",
|
||||||
|
skillOnly: true,
|
||||||
|
schema: {
|
||||||
|
name: "browser_press_sequentially",
|
||||||
|
title: "Type text key by key",
|
||||||
|
description: "Type text key by key on the keyboard",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
text: import_zodBundle.z.string().describe("Text to type"),
|
||||||
|
submit: import_zodBundle.z.boolean().optional().describe("Whether to submit entered text (press Enter after)")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.addCode(`// Press ${params.text}`);
|
||||||
|
response.addCode(`await page.keyboard.type('${params.text}');`);
|
||||||
|
await tab.page.keyboard.type(params.text);
|
||||||
|
if (params.submit) {
|
||||||
|
response.addCode(`await page.keyboard.press('Enter');`);
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
await tab.page.keyboard.press("Enter");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const typeSchema = import_snapshot.elementSchema.extend({
|
||||||
|
text: import_zodBundle.z.string().describe("Text to type into the element"),
|
||||||
|
submit: import_zodBundle.z.boolean().optional().describe("Whether to submit entered text (press Enter after)"),
|
||||||
|
slowly: import_zodBundle.z.boolean().optional().describe("Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.")
|
||||||
|
});
|
||||||
|
const type = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core-input",
|
||||||
|
schema: {
|
||||||
|
name: "browser_type",
|
||||||
|
title: "Type text",
|
||||||
|
description: "Type text into editable element",
|
||||||
|
inputSchema: typeSchema,
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { locator, resolved } = await tab.refLocator(params);
|
||||||
|
const secret = tab.context.lookupSecret(params.text);
|
||||||
|
const action = async () => {
|
||||||
|
if (params.slowly) {
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
response.addCode(`await page.${resolved}.pressSequentially(${secret.code});`);
|
||||||
|
await locator.pressSequentially(secret.value, tab.actionTimeoutOptions);
|
||||||
|
} else {
|
||||||
|
response.addCode(`await page.${resolved}.fill(${secret.code});`);
|
||||||
|
await locator.fill(secret.value, tab.actionTimeoutOptions);
|
||||||
|
}
|
||||||
|
if (params.submit) {
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
response.addCode(`await page.${resolved}.press('Enter');`);
|
||||||
|
await locator.press("Enter", tab.actionTimeoutOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (params.submit || params.slowly)
|
||||||
|
await tab.waitForCompletion(action);
|
||||||
|
else
|
||||||
|
await action();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const keydown = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core-input",
|
||||||
|
skillOnly: true,
|
||||||
|
schema: {
|
||||||
|
name: "browser_keydown",
|
||||||
|
title: "Press a key down",
|
||||||
|
description: "Press a key down on the keyboard",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.addCode(`await page.keyboard.down('${params.key}');`);
|
||||||
|
await tab.page.keyboard.down(params.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const keyup = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core-input",
|
||||||
|
skillOnly: true,
|
||||||
|
schema: {
|
||||||
|
name: "browser_keyup",
|
||||||
|
title: "Press a key up",
|
||||||
|
description: "Press a key up on the keyboard",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.addCode(`await page.keyboard.up('${params.key}');`);
|
||||||
|
await tab.page.keyboard.up(params.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var keyboard_default = [
|
||||||
|
press,
|
||||||
|
type,
|
||||||
|
pressSequentially,
|
||||||
|
keydown,
|
||||||
|
keyup
|
||||||
|
];
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var logFile_exports = {};
|
||||||
|
__export(logFile_exports, {
|
||||||
|
LogFile: () => LogFile
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(logFile_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
class LogFile {
|
||||||
|
constructor(context, startTime, filePrefix, title) {
|
||||||
|
this._stopped = false;
|
||||||
|
this._line = 0;
|
||||||
|
this._entries = 0;
|
||||||
|
this._lastLine = 0;
|
||||||
|
this._lastEntries = 0;
|
||||||
|
this._writeChain = Promise.resolve();
|
||||||
|
this._context = context;
|
||||||
|
this._startTime = startTime;
|
||||||
|
this._filePrefix = filePrefix;
|
||||||
|
this._title = title;
|
||||||
|
}
|
||||||
|
appendLine(wallTime, text) {
|
||||||
|
this._writeChain = this._writeChain.then(() => this._write(wallTime, text)).catch((e) => (0, import_utilsBundle.debug)("pw:tools:error")(e));
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
this._stopped = true;
|
||||||
|
}
|
||||||
|
async take(relativeTo) {
|
||||||
|
const logChunk = await this._take();
|
||||||
|
if (!logChunk)
|
||||||
|
return void 0;
|
||||||
|
const logFilePath = relativeTo ? import_path.default.relative(relativeTo, logChunk.file) : logChunk.file;
|
||||||
|
const lineRange = logChunk.fromLine === logChunk.toLine ? `#L${logChunk.fromLine}` : `#L${logChunk.fromLine}-L${logChunk.toLine}`;
|
||||||
|
return `${logFilePath}${lineRange}`;
|
||||||
|
}
|
||||||
|
async _take() {
|
||||||
|
await this._writeChain;
|
||||||
|
if (!this._file || this._entries === this._lastEntries)
|
||||||
|
return void 0;
|
||||||
|
const chunk = {
|
||||||
|
type: this._title.toLowerCase(),
|
||||||
|
file: this._file,
|
||||||
|
fromLine: this._lastLine + 1,
|
||||||
|
toLine: this._line,
|
||||||
|
entryCount: this._entries - this._lastEntries
|
||||||
|
};
|
||||||
|
this._lastLine = this._line;
|
||||||
|
this._lastEntries = this._entries;
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
async _write(wallTime, text) {
|
||||||
|
if (this._stopped)
|
||||||
|
return;
|
||||||
|
this._file ??= await this._context.outputFile({ prefix: this._filePrefix, ext: "log", date: new Date(this._startTime) }, { origin: "code" });
|
||||||
|
const relativeTime = Math.round(wallTime - this._startTime);
|
||||||
|
const logLine = `[${String(relativeTime).padStart(8, " ")}ms] ${text}
|
||||||
|
`;
|
||||||
|
await import_fs.default.promises.appendFile(this._file, logLine);
|
||||||
|
const lineCount = logLine.split("\n").length - 1;
|
||||||
|
this._line += lineCount;
|
||||||
|
this._entries++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
LogFile
|
||||||
|
});
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var mouse_exports = {};
|
||||||
|
__export(mouse_exports, {
|
||||||
|
default: () => mouse_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(mouse_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_stringUtils = require("../../utils/isomorphic/stringUtils");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const mouseMove = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "vision",
|
||||||
|
schema: {
|
||||||
|
name: "browser_mouse_move_xy",
|
||||||
|
title: "Move mouse",
|
||||||
|
description: "Move mouse to a given position",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
x: import_zodBundle.z.number().describe("X coordinate"),
|
||||||
|
y: import_zodBundle.z.number().describe("Y coordinate")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.addCode(`// Move mouse to (${params.x}, ${params.y})`);
|
||||||
|
response.addCode(`await page.mouse.move(${params.x}, ${params.y});`);
|
||||||
|
await tab.page.mouse.move(params.x, params.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const mouseDown = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "vision",
|
||||||
|
schema: {
|
||||||
|
name: "browser_mouse_down",
|
||||||
|
title: "Press mouse down",
|
||||||
|
description: "Press mouse down",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
button: import_zodBundle.z.enum(["left", "right", "middle"]).optional().describe("Button to press, defaults to left")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const options = { button: params.button };
|
||||||
|
const optionsArg = (0, import_stringUtils.formatObjectOrVoid)(options);
|
||||||
|
response.addCode(`// Press mouse down`);
|
||||||
|
response.addCode(`await page.mouse.down(${optionsArg});`);
|
||||||
|
await tab.page.mouse.down(options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const mouseUp = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "vision",
|
||||||
|
schema: {
|
||||||
|
name: "browser_mouse_up",
|
||||||
|
title: "Press mouse up",
|
||||||
|
description: "Press mouse up",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
button: import_zodBundle.z.enum(["left", "right", "middle"]).optional().describe("Button to press, defaults to left")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const options = { button: params.button };
|
||||||
|
const optionsArg = (0, import_stringUtils.formatObjectOrVoid)(options);
|
||||||
|
response.addCode(`// Press mouse up`);
|
||||||
|
response.addCode(`await page.mouse.up(${optionsArg});`);
|
||||||
|
await tab.page.mouse.up(options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const mouseWheel = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "vision",
|
||||||
|
schema: {
|
||||||
|
name: "browser_mouse_wheel",
|
||||||
|
title: "Scroll mouse wheel",
|
||||||
|
description: "Scroll mouse wheel",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
deltaX: import_zodBundle.z.number().default(0).describe("X delta"),
|
||||||
|
deltaY: import_zodBundle.z.number().default(0).describe("Y delta")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.addCode(`// Scroll mouse wheel`);
|
||||||
|
response.addCode(`await page.mouse.wheel(${params.deltaX}, ${params.deltaY});`);
|
||||||
|
await tab.page.mouse.wheel(params.deltaX, params.deltaY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const mouseClick = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "vision",
|
||||||
|
schema: {
|
||||||
|
name: "browser_mouse_click_xy",
|
||||||
|
title: "Click",
|
||||||
|
description: "Click mouse button at a given position",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
x: import_zodBundle.z.number().describe("X coordinate"),
|
||||||
|
y: import_zodBundle.z.number().describe("Y coordinate"),
|
||||||
|
button: import_zodBundle.z.enum(["left", "right", "middle"]).optional().describe("Button to click, defaults to left"),
|
||||||
|
clickCount: import_zodBundle.z.number().optional().describe("Number of clicks, defaults to 1"),
|
||||||
|
delay: import_zodBundle.z.number().optional().describe("Time to wait between mouse down and mouse up in milliseconds, defaults to 0")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
const options = {
|
||||||
|
button: params.button,
|
||||||
|
clickCount: params.clickCount,
|
||||||
|
delay: params.delay
|
||||||
|
};
|
||||||
|
const formatted = (0, import_stringUtils.formatObjectOrVoid)(options);
|
||||||
|
const optionsArg = formatted ? `, ${formatted}` : "";
|
||||||
|
response.addCode(`// Click mouse at coordinates (${params.x}, ${params.y})`);
|
||||||
|
response.addCode(`await page.mouse.click(${params.x}, ${params.y}${optionsArg});`);
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
await tab.page.mouse.click(params.x, params.y, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const mouseDrag = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "vision",
|
||||||
|
schema: {
|
||||||
|
name: "browser_mouse_drag_xy",
|
||||||
|
title: "Drag mouse",
|
||||||
|
description: "Drag left mouse button to a given position",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
startX: import_zodBundle.z.number().describe("Start X coordinate"),
|
||||||
|
startY: import_zodBundle.z.number().describe("Start Y coordinate"),
|
||||||
|
endX: import_zodBundle.z.number().describe("End X coordinate"),
|
||||||
|
endY: import_zodBundle.z.number().describe("End Y coordinate")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
response.addCode(`// Drag mouse from (${params.startX}, ${params.startY}) to (${params.endX}, ${params.endY})`);
|
||||||
|
response.addCode(`await page.mouse.move(${params.startX}, ${params.startY});`);
|
||||||
|
response.addCode(`await page.mouse.down();`);
|
||||||
|
response.addCode(`await page.mouse.move(${params.endX}, ${params.endY});`);
|
||||||
|
response.addCode(`await page.mouse.up();`);
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
await tab.page.mouse.move(params.startX, params.startY);
|
||||||
|
await tab.page.mouse.down();
|
||||||
|
await tab.page.mouse.move(params.endX, params.endY);
|
||||||
|
await tab.page.mouse.up();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var mouse_default = [
|
||||||
|
mouseMove,
|
||||||
|
mouseClick,
|
||||||
|
mouseDrag,
|
||||||
|
mouseDown,
|
||||||
|
mouseUp,
|
||||||
|
mouseWheel
|
||||||
|
];
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var navigate_exports = {};
|
||||||
|
__export(navigate_exports, {
|
||||||
|
default: () => navigate_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(navigate_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const navigate = (0, import_tool.defineTool)({
|
||||||
|
capability: "core-navigation",
|
||||||
|
schema: {
|
||||||
|
name: "browser_navigate",
|
||||||
|
title: "Navigate to a URL",
|
||||||
|
description: "Navigate to a URL",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
url: import_zodBundle.z.string().describe("The URL to navigate to")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const tab = await context.ensureTab();
|
||||||
|
let url = params.url;
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
} catch (e) {
|
||||||
|
if (url.startsWith("localhost"))
|
||||||
|
url = "http://" + url;
|
||||||
|
else
|
||||||
|
url = "https://" + url;
|
||||||
|
}
|
||||||
|
context.checkUrlAllowed(url);
|
||||||
|
await tab.navigate(url);
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
response.addCode(`await page.goto('${url}');`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const goBack = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core-navigation",
|
||||||
|
schema: {
|
||||||
|
name: "browser_navigate_back",
|
||||||
|
title: "Go back",
|
||||||
|
description: "Go back to the previous page in the history",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
await tab.page.goBack(tab.navigationTimeoutOptions);
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
response.addCode(`await page.goBack();`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const goForward = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core-navigation",
|
||||||
|
skillOnly: true,
|
||||||
|
schema: {
|
||||||
|
name: "browser_navigate_forward",
|
||||||
|
title: "Go forward",
|
||||||
|
description: "Go forward to the next page in the history",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
await tab.page.goForward(tab.navigationTimeoutOptions);
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
response.addCode(`await page.goForward();`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const reload = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core-navigation",
|
||||||
|
skillOnly: true,
|
||||||
|
schema: {
|
||||||
|
name: "browser_reload",
|
||||||
|
title: "Reload the page",
|
||||||
|
description: "Reload the current page",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
await tab.page.reload(tab.navigationTimeoutOptions);
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
response.addCode(`await page.reload();`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var navigate_default = [
|
||||||
|
navigate,
|
||||||
|
goBack,
|
||||||
|
goForward,
|
||||||
|
reload
|
||||||
|
];
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var network_exports = {};
|
||||||
|
__export(network_exports, {
|
||||||
|
default: () => network_default,
|
||||||
|
isFetch: () => isFetch,
|
||||||
|
renderRequest: () => renderRequest
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(network_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const requests = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_network_requests",
|
||||||
|
title: "List network requests",
|
||||||
|
description: "Returns all network requests since loading the page",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
static: import_zodBundle.z.boolean().default(false).describe("Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false."),
|
||||||
|
requestBody: import_zodBundle.z.boolean().default(false).describe("Whether to include request body. Defaults to false."),
|
||||||
|
requestHeaders: import_zodBundle.z.boolean().default(false).describe("Whether to include request headers. Defaults to false."),
|
||||||
|
filter: import_zodBundle.z.string().optional().describe('Only return requests whose URL matches this regexp (e.g. "/api/.*user").'),
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("Filename to save the network requests to. If not provided, requests are returned as text.")
|
||||||
|
}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const requests2 = await tab.requests();
|
||||||
|
const filter = params.filter ? new RegExp(params.filter) : void 0;
|
||||||
|
const text = [];
|
||||||
|
for (const request of requests2) {
|
||||||
|
if (!params.static && !isFetch(request) && isSuccessfulResponse(request))
|
||||||
|
continue;
|
||||||
|
if (filter) {
|
||||||
|
filter.lastIndex = 0;
|
||||||
|
if (!filter.test(request.url()))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
text.push(await renderRequest(request, params.requestBody, params.requestHeaders));
|
||||||
|
}
|
||||||
|
await response.addResult("Network", text.join("\n"), { prefix: "network", ext: "log", suggestedFilename: params.filename });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const networkClear = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
skillOnly: true,
|
||||||
|
schema: {
|
||||||
|
name: "browser_network_clear",
|
||||||
|
title: "Clear network requests",
|
||||||
|
description: "Clear all network requests",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
await tab.clearRequests();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function isSuccessfulResponse(request) {
|
||||||
|
if (request.failure())
|
||||||
|
return false;
|
||||||
|
const response = request.existingResponse();
|
||||||
|
return !!response && response.status() < 400;
|
||||||
|
}
|
||||||
|
function isFetch(request) {
|
||||||
|
return ["fetch", "xhr"].includes(request.resourceType());
|
||||||
|
}
|
||||||
|
async function renderRequest(request, includeBody = false, includeHeaders = false) {
|
||||||
|
const response = request.existingResponse();
|
||||||
|
const result = [];
|
||||||
|
result.push(`[${request.method().toUpperCase()}] ${request.url()}`);
|
||||||
|
if (response)
|
||||||
|
result.push(` => [${response.status()}] ${response.statusText()}`);
|
||||||
|
else if (request.failure())
|
||||||
|
result.push(` => [FAILED] ${request.failure()?.errorText ?? "Unknown error"}`);
|
||||||
|
if (includeHeaders) {
|
||||||
|
const headers = request.headers();
|
||||||
|
const headerLines = Object.entries(headers).map(([k, v]) => ` ${k}: ${v}`).join("\n");
|
||||||
|
if (headerLines)
|
||||||
|
result.push(`
|
||||||
|
Request headers:
|
||||||
|
${headerLines}`);
|
||||||
|
}
|
||||||
|
if (includeBody) {
|
||||||
|
const postData = request.postData();
|
||||||
|
if (postData)
|
||||||
|
result.push(`
|
||||||
|
Request body: ${postData}`);
|
||||||
|
}
|
||||||
|
return result.join("");
|
||||||
|
}
|
||||||
|
const networkStateSet = (0, import_tool.defineTool)({
|
||||||
|
capability: "network",
|
||||||
|
schema: {
|
||||||
|
name: "browser_network_state_set",
|
||||||
|
title: "Set network state",
|
||||||
|
description: "Sets the browser network state to online or offline. When offline, all network requests will fail.",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
state: import_zodBundle.z.enum(["online", "offline"]).describe('Set to "offline" to simulate offline mode, "online" to restore network connectivity')
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
const offline = params.state === "offline";
|
||||||
|
await browserContext.setOffline(offline);
|
||||||
|
response.addTextResult(`Network is now ${params.state}`);
|
||||||
|
response.addCode(`await page.context().setOffline(${offline});`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var network_default = [
|
||||||
|
requests,
|
||||||
|
networkClear,
|
||||||
|
networkStateSet
|
||||||
|
];
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
isFetch,
|
||||||
|
renderRequest
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var pdf_exports = {};
|
||||||
|
__export(pdf_exports, {
|
||||||
|
default: () => pdf_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(pdf_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_stringUtils = require("../../utils/isomorphic/stringUtils");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const pdfSchema = import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified. Prefer relative file names to stay within the output directory.")
|
||||||
|
});
|
||||||
|
const pdf = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "pdf",
|
||||||
|
schema: {
|
||||||
|
name: "browser_pdf_save",
|
||||||
|
title: "Save as PDF",
|
||||||
|
description: "Save page as PDF",
|
||||||
|
inputSchema: pdfSchema,
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const data = await tab.page.pdf();
|
||||||
|
const result = await response.resolveClientFile({ prefix: "page", ext: "pdf", suggestedFilename: params.filename }, "Page as pdf");
|
||||||
|
await response.addFileResult(result, data);
|
||||||
|
response.addCode(`await page.pdf(${(0, import_stringUtils.formatObject)({ path: result.relativeName })});`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var pdf_default = [
|
||||||
|
pdf
|
||||||
|
];
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var response_exports = {};
|
||||||
|
__export(response_exports, {
|
||||||
|
Response: () => Response,
|
||||||
|
parseResponse: () => parseResponse,
|
||||||
|
renderTabMarkdown: () => renderTabMarkdown,
|
||||||
|
renderTabsMarkdown: () => renderTabsMarkdown,
|
||||||
|
requestDebug: () => requestDebug
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(response_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
var import_tab = require("./tab");
|
||||||
|
var import_screenshot = require("./screenshot");
|
||||||
|
const requestDebug = (0, import_utilsBundle.debug)("pw:mcp:request");
|
||||||
|
class Response {
|
||||||
|
constructor(context, toolName, toolArgs, relativeTo) {
|
||||||
|
this._results = [];
|
||||||
|
this._errors = [];
|
||||||
|
this._code = [];
|
||||||
|
this._includeSnapshot = "none";
|
||||||
|
this._isClose = false;
|
||||||
|
this._imageResults = [];
|
||||||
|
this._context = context;
|
||||||
|
this.toolName = toolName;
|
||||||
|
this.toolArgs = toolArgs;
|
||||||
|
this._clientWorkspace = relativeTo ?? context.options.cwd;
|
||||||
|
}
|
||||||
|
_computRelativeTo(fileName) {
|
||||||
|
return import_path.default.relative(this._clientWorkspace, fileName);
|
||||||
|
}
|
||||||
|
async resolveClientFile(template, title) {
|
||||||
|
let fileName;
|
||||||
|
if (template.suggestedFilename)
|
||||||
|
fileName = await this.resolveClientFilename(template.suggestedFilename);
|
||||||
|
else
|
||||||
|
fileName = await this._context.outputFile(template, { origin: "llm" });
|
||||||
|
const relativeName = this._computRelativeTo(fileName);
|
||||||
|
const printableLink = `- [${title}](${relativeName})`;
|
||||||
|
return { fileName, relativeName, printableLink };
|
||||||
|
}
|
||||||
|
async resolveClientFilename(filename) {
|
||||||
|
return await this._context.workspaceFile(filename, this._clientWorkspace);
|
||||||
|
}
|
||||||
|
addTextResult(text) {
|
||||||
|
this._results.push(text);
|
||||||
|
}
|
||||||
|
async addResult(title, data, file) {
|
||||||
|
if (file.suggestedFilename || typeof data !== "string") {
|
||||||
|
const resolvedFile = await this.resolveClientFile(file, title);
|
||||||
|
await this.addFileResult(resolvedFile, data);
|
||||||
|
} else {
|
||||||
|
this.addTextResult(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _writeFile(resolvedFile, data) {
|
||||||
|
if (typeof data === "string")
|
||||||
|
await import_fs.default.promises.writeFile(resolvedFile.fileName, this._redactSecrets(data), "utf-8");
|
||||||
|
else if (data)
|
||||||
|
await import_fs.default.promises.writeFile(resolvedFile.fileName, data);
|
||||||
|
}
|
||||||
|
async addFileResult(resolvedFile, data) {
|
||||||
|
await this._writeFile(resolvedFile, data);
|
||||||
|
this.addTextResult(resolvedFile.printableLink);
|
||||||
|
}
|
||||||
|
addFileLink(title, fileName) {
|
||||||
|
const relativeName = this._computRelativeTo(fileName);
|
||||||
|
this.addTextResult(`- [${title}](${relativeName})`);
|
||||||
|
}
|
||||||
|
async registerImageResult(data, imageType) {
|
||||||
|
this._imageResults.push({ data, imageType });
|
||||||
|
}
|
||||||
|
setClose() {
|
||||||
|
this._isClose = true;
|
||||||
|
}
|
||||||
|
addError(error) {
|
||||||
|
this._errors.push(error);
|
||||||
|
}
|
||||||
|
addCode(code) {
|
||||||
|
this._code.push(code);
|
||||||
|
}
|
||||||
|
setIncludeSnapshot() {
|
||||||
|
this._includeSnapshot = this._context.config.snapshot?.mode ?? "full";
|
||||||
|
}
|
||||||
|
setIncludeFullSnapshot(includeSnapshotFileName, selector, depth) {
|
||||||
|
this._includeSnapshot = "explicit";
|
||||||
|
this._includeSnapshotFileName = includeSnapshotFileName;
|
||||||
|
this._includeSnapshotDepth = depth;
|
||||||
|
this._includeSnapshotSelector = selector;
|
||||||
|
}
|
||||||
|
_redactSecrets(text) {
|
||||||
|
for (const [secretName, secretValue] of Object.entries(this._context.config.secrets ?? {})) {
|
||||||
|
if (!secretValue)
|
||||||
|
continue;
|
||||||
|
text = text.replaceAll(secretValue, `<secret>${secretName}</secret>`);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
async serialize() {
|
||||||
|
const sections = await this._build();
|
||||||
|
const text = [];
|
||||||
|
for (const section of sections) {
|
||||||
|
if (!section.content.length)
|
||||||
|
continue;
|
||||||
|
text.push(`### ${section.title}`);
|
||||||
|
if (section.codeframe)
|
||||||
|
text.push(`\`\`\`${section.codeframe}`);
|
||||||
|
text.push(...section.content);
|
||||||
|
if (section.codeframe)
|
||||||
|
text.push("```");
|
||||||
|
}
|
||||||
|
const content = [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: sanitizeUnicode(this._redactSecrets(text.join("\n")))
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if (this._context.config.imageResponses !== "omit") {
|
||||||
|
for (const imageResult of this._imageResults) {
|
||||||
|
const scaledData = (0, import_screenshot.scaleImageToFitMessage)(imageResult.data, imageResult.imageType);
|
||||||
|
content.push({ type: "image", data: scaledData.toString("base64"), mimeType: imageResult.imageType === "png" ? "image/png" : "image/jpeg" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
...this._isClose ? { isClose: true } : {},
|
||||||
|
...sections.some((section) => section.isError) ? { isError: true } : {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async _build() {
|
||||||
|
const sections = [];
|
||||||
|
const addSection = (title, content, codeframe) => {
|
||||||
|
const section = { title, content, isError: title === "Error", codeframe };
|
||||||
|
sections.push(section);
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
if (this._errors.length)
|
||||||
|
addSection("Error", this._errors);
|
||||||
|
if (this._results.length)
|
||||||
|
addSection("Result", this._results);
|
||||||
|
if (this._context.config.codegen !== "none" && this._code.length)
|
||||||
|
addSection("Ran Playwright code", this._code, "js");
|
||||||
|
const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshotSelector, this._includeSnapshotDepth, this._clientWorkspace) : void 0;
|
||||||
|
const tabHeaders = await Promise.all(this._context.tabs().map((tab) => tab.headerSnapshot()));
|
||||||
|
if (this._includeSnapshot !== "none" || tabHeaders.some((header) => header.changed)) {
|
||||||
|
if (tabHeaders.length !== 1)
|
||||||
|
addSection("Open tabs", renderTabsMarkdown(tabHeaders));
|
||||||
|
addSection("Page", renderTabMarkdown(tabHeaders.find((h) => h.current) ?? tabHeaders[0]));
|
||||||
|
}
|
||||||
|
if (this._context.tabs().length === 0)
|
||||||
|
this._isClose = true;
|
||||||
|
if (tabSnapshot?.modalStates.length)
|
||||||
|
addSection("Modal state", (0, import_tab.renderModalStates)(this._context.config, tabSnapshot.modalStates));
|
||||||
|
if (tabSnapshot && this._includeSnapshot !== "none") {
|
||||||
|
if (this._includeSnapshot !== "explicit" || this._includeSnapshotFileName) {
|
||||||
|
const suggestedFilename = this._includeSnapshotFileName === "<auto>" ? void 0 : this._includeSnapshotFileName;
|
||||||
|
const resolvedFile = await this.resolveClientFile({ prefix: "page", ext: "yml", suggestedFilename }, "Snapshot");
|
||||||
|
await this._writeFile(resolvedFile, tabSnapshot.ariaSnapshot);
|
||||||
|
addSection("Snapshot", [resolvedFile.printableLink]);
|
||||||
|
} else {
|
||||||
|
addSection("Snapshot", [tabSnapshot.ariaSnapshot], "yaml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const text = [];
|
||||||
|
if (tabSnapshot?.consoleLink)
|
||||||
|
text.push(`- New console entries: ${tabSnapshot.consoleLink}`);
|
||||||
|
if (tabSnapshot?.events.filter((event) => event.type !== "request").length) {
|
||||||
|
for (const event of tabSnapshot.events) {
|
||||||
|
if (event.type === "download-start")
|
||||||
|
text.push(`- Downloading file ${event.download.download.suggestedFilename()} ...`);
|
||||||
|
else if (event.type === "download-finish")
|
||||||
|
text.push(`- Downloaded file ${event.download.download.suggestedFilename()} to "${this._computRelativeTo(event.download.outputFile)}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text.length)
|
||||||
|
addSection("Events", text);
|
||||||
|
const pausedDetails = this._context.debugger().pausedDetails();
|
||||||
|
if (pausedDetails) {
|
||||||
|
addSection("Paused", [
|
||||||
|
`- ${pausedDetails.title} at ${this._computRelativeTo(pausedDetails.location.file)}${pausedDetails.location.line ? ":" + pausedDetails.location.line : ""}`,
|
||||||
|
"- Use any tools to explore and interact, resume by calling resume/step-over/pause-at"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function renderTabMarkdown(tab) {
|
||||||
|
const lines = [`- Page URL: ${tab.url}`];
|
||||||
|
if (tab.title)
|
||||||
|
lines.push(`- Page Title: ${tab.title}`);
|
||||||
|
if (tab.console.errors || tab.console.warnings)
|
||||||
|
lines.push(`- Console: ${tab.console.errors} errors, ${tab.console.warnings} warnings`);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
function renderTabsMarkdown(tabs) {
|
||||||
|
if (!tabs.length)
|
||||||
|
return ["No open tabs. Navigate to a URL to create one."];
|
||||||
|
const lines = [];
|
||||||
|
for (let i = 0; i < tabs.length; i++) {
|
||||||
|
const tab = tabs[i];
|
||||||
|
const current = tab.current ? " (current)" : "";
|
||||||
|
lines.push(`- ${i}:${current} [${tab.title}](${tab.url})`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
function sanitizeUnicode(text) {
|
||||||
|
return text.toWellFormed?.() ?? text;
|
||||||
|
}
|
||||||
|
function parseSections(text) {
|
||||||
|
const sections = /* @__PURE__ */ new Map();
|
||||||
|
const sectionHeaders = text.split(/^### /m).slice(1);
|
||||||
|
for (const section of sectionHeaders) {
|
||||||
|
const firstNewlineIndex = section.indexOf("\n");
|
||||||
|
if (firstNewlineIndex === -1)
|
||||||
|
continue;
|
||||||
|
const sectionName = section.substring(0, firstNewlineIndex);
|
||||||
|
const sectionContent = section.substring(firstNewlineIndex + 1).trim();
|
||||||
|
sections.set(sectionName, sectionContent);
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
function parseResponse(response, cwd) {
|
||||||
|
if (response.content?.[0].type !== "text")
|
||||||
|
return void 0;
|
||||||
|
const text = response.content[0].text;
|
||||||
|
const sections = parseSections(text);
|
||||||
|
const error = sections.get("Error");
|
||||||
|
const result = sections.get("Result");
|
||||||
|
const code = sections.get("Ran Playwright code");
|
||||||
|
const tabs = sections.get("Open tabs");
|
||||||
|
const page = sections.get("Page");
|
||||||
|
const snapshotSection = sections.get("Snapshot");
|
||||||
|
const events = sections.get("Events");
|
||||||
|
const modalState = sections.get("Modal state");
|
||||||
|
const paused = sections.get("Paused");
|
||||||
|
const codeNoFrame = code?.replace(/^```js\n/, "").replace(/\n```$/, "");
|
||||||
|
const isError = response.isError;
|
||||||
|
const attachments = response.content.length > 1 ? response.content.slice(1) : void 0;
|
||||||
|
let snapshot;
|
||||||
|
let inlineSnapshot;
|
||||||
|
if (snapshotSection) {
|
||||||
|
const match = snapshotSection.match(/\[Snapshot\]\(([^)]+)\)/);
|
||||||
|
if (match) {
|
||||||
|
if (cwd) {
|
||||||
|
try {
|
||||||
|
snapshot = import_fs.default.readFileSync(import_path.default.resolve(cwd, match[1]), "utf-8");
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inlineSnapshot = snapshotSection.replace(/^```yaml\n?/, "").replace(/\n?```$/, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
code: codeNoFrame,
|
||||||
|
tabs,
|
||||||
|
page,
|
||||||
|
snapshot,
|
||||||
|
inlineSnapshot,
|
||||||
|
events,
|
||||||
|
modalState,
|
||||||
|
paused,
|
||||||
|
isError,
|
||||||
|
attachments,
|
||||||
|
text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
Response,
|
||||||
|
parseResponse,
|
||||||
|
renderTabMarkdown,
|
||||||
|
renderTabsMarkdown,
|
||||||
|
requestDebug
|
||||||
|
});
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var route_exports = {};
|
||||||
|
__export(route_exports, {
|
||||||
|
default: () => route_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(route_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const route = (0, import_tool.defineTool)({
|
||||||
|
capability: "network",
|
||||||
|
schema: {
|
||||||
|
name: "browser_route",
|
||||||
|
title: "Mock network requests",
|
||||||
|
description: "Set up a route to mock network requests matching a URL pattern",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
pattern: import_zodBundle.z.string().describe('URL pattern to match (e.g., "**/api/users", "**/*.{png,jpg}")'),
|
||||||
|
status: import_zodBundle.z.number().optional().describe("HTTP status code to return (default: 200)"),
|
||||||
|
body: import_zodBundle.z.string().optional().describe("Response body (text or JSON string)"),
|
||||||
|
contentType: import_zodBundle.z.string().optional().describe('Content-Type header (e.g., "application/json", "text/html")'),
|
||||||
|
headers: import_zodBundle.z.array(import_zodBundle.z.string()).optional().describe('Headers to add in "Name: Value" format'),
|
||||||
|
removeHeaders: import_zodBundle.z.string().optional().describe("Comma-separated list of header names to remove from request")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const addHeaders = params.headers ? Object.fromEntries(params.headers.map((h) => {
|
||||||
|
const colonIndex = h.indexOf(":");
|
||||||
|
return [h.substring(0, colonIndex).trim(), h.substring(colonIndex + 1).trim()];
|
||||||
|
})) : void 0;
|
||||||
|
const removeHeaders = params.removeHeaders ? params.removeHeaders.split(",").map((h) => h.trim()) : void 0;
|
||||||
|
const handler = async (route2) => {
|
||||||
|
if (params.body !== void 0 || params.status !== void 0) {
|
||||||
|
await route2.fulfill({
|
||||||
|
status: params.status ?? 200,
|
||||||
|
contentType: params.contentType,
|
||||||
|
body: params.body
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const headers = { ...route2.request().headers() };
|
||||||
|
if (addHeaders) {
|
||||||
|
for (const [key, value] of Object.entries(addHeaders))
|
||||||
|
headers[key] = value;
|
||||||
|
}
|
||||||
|
if (removeHeaders) {
|
||||||
|
for (const header of removeHeaders)
|
||||||
|
delete headers[header.toLowerCase()];
|
||||||
|
}
|
||||||
|
await route2.continue({ headers });
|
||||||
|
};
|
||||||
|
const entry = {
|
||||||
|
pattern: params.pattern,
|
||||||
|
status: params.status,
|
||||||
|
body: params.body,
|
||||||
|
contentType: params.contentType,
|
||||||
|
addHeaders,
|
||||||
|
removeHeaders,
|
||||||
|
handler
|
||||||
|
};
|
||||||
|
await context.addRoute(entry);
|
||||||
|
response.addTextResult(`Route added for pattern: ${params.pattern}`);
|
||||||
|
response.addCode(`await page.context().route('${params.pattern}', async route => { /* route handler */ });`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const routeList = (0, import_tool.defineTool)({
|
||||||
|
capability: "network",
|
||||||
|
schema: {
|
||||||
|
name: "browser_route_list",
|
||||||
|
title: "List network routes",
|
||||||
|
description: "List all active network routes",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const routes = context.routes();
|
||||||
|
if (routes.length === 0) {
|
||||||
|
response.addTextResult("No active routes");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = [];
|
||||||
|
for (let i = 0; i < routes.length; i++) {
|
||||||
|
const route2 = routes[i];
|
||||||
|
const details = [];
|
||||||
|
if (route2.status !== void 0)
|
||||||
|
details.push(`status=${route2.status}`);
|
||||||
|
if (route2.body !== void 0)
|
||||||
|
details.push(`body=${route2.body.length > 50 ? route2.body.substring(0, 50) + "..." : route2.body}`);
|
||||||
|
if (route2.contentType)
|
||||||
|
details.push(`contentType=${route2.contentType}`);
|
||||||
|
if (route2.addHeaders)
|
||||||
|
details.push(`addHeaders=${JSON.stringify(route2.addHeaders)}`);
|
||||||
|
if (route2.removeHeaders)
|
||||||
|
details.push(`removeHeaders=${route2.removeHeaders.join(",")}`);
|
||||||
|
const detailsStr = details.length ? ` (${details.join(", ")})` : "";
|
||||||
|
lines.push(`${i + 1}. ${route2.pattern}${detailsStr}`);
|
||||||
|
}
|
||||||
|
response.addTextResult(lines.join("\n"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const unroute = (0, import_tool.defineTool)({
|
||||||
|
capability: "network",
|
||||||
|
schema: {
|
||||||
|
name: "browser_unroute",
|
||||||
|
title: "Remove network routes",
|
||||||
|
description: "Remove network routes matching a pattern (or all routes if no pattern specified)",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
pattern: import_zodBundle.z.string().optional().describe("URL pattern to unroute (omit to remove all routes)")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const removed = await context.removeRoute(params.pattern);
|
||||||
|
if (params.pattern)
|
||||||
|
response.addTextResult(`Removed ${removed} route(s) for pattern: ${params.pattern}`);
|
||||||
|
else
|
||||||
|
response.addTextResult(`Removed all ${removed} route(s)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var route_default = [
|
||||||
|
route,
|
||||||
|
routeList,
|
||||||
|
unroute
|
||||||
|
];
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var runCode_exports = {};
|
||||||
|
__export(runCode_exports, {
|
||||||
|
default: () => runCode_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(runCode_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_vm = __toESM(require("vm"));
|
||||||
|
var import_manualPromise = require("../../utils/isomorphic/manualPromise");
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const codeSchema = import_zodBundle.z.object({
|
||||||
|
code: import_zodBundle.z.string().optional().describe(`A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: \`async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }\``),
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("Load code from the specified file. If both code and filename are provided, code will be ignored.")
|
||||||
|
});
|
||||||
|
const runCode = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_run_code",
|
||||||
|
title: "Run Playwright code",
|
||||||
|
description: "Run Playwright code snippet",
|
||||||
|
inputSchema: codeSchema,
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
let code = params.code;
|
||||||
|
if (params.filename) {
|
||||||
|
const resolvedPath = await response.resolveClientFilename(params.filename);
|
||||||
|
code = await import_fs.default.promises.readFile(resolvedPath, "utf-8");
|
||||||
|
}
|
||||||
|
response.addCode(`await (${code})(page);`);
|
||||||
|
const __end__ = new import_manualPromise.ManualPromise();
|
||||||
|
const context = {
|
||||||
|
page: tab.page,
|
||||||
|
__end__
|
||||||
|
};
|
||||||
|
import_vm.default.createContext(context);
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
context.__fn__ = import_vm.default.runInContext("(" + code + ")", context);
|
||||||
|
const snippet = "(async () => {\n try {\n const result = await __fn__(page);\n __end__.resolve(JSON.stringify(result));\n } catch (e) {\n __end__.reject(e);\n }\n})()";
|
||||||
|
await import_vm.default.runInContext(snippet, context);
|
||||||
|
const result = await __end__;
|
||||||
|
if (typeof result === "string")
|
||||||
|
response.addTextResult(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var runCode_default = [
|
||||||
|
runCode
|
||||||
|
];
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var screenshot_exports = {};
|
||||||
|
__export(screenshot_exports, {
|
||||||
|
default: () => screenshot_default,
|
||||||
|
scaleImageToFitMessage: () => scaleImageToFitMessage
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(screenshot_exports);
|
||||||
|
var import_imageUtils = require("../../utils/isomorphic/imageUtils");
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
var import_stringUtils = require("../../utils/isomorphic/stringUtils");
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const screenshotSchema = import_zodBundle.z.object({
|
||||||
|
type: import_zodBundle.z.enum(["png", "jpeg"]).default("png").describe("Image format for the screenshot. Default is png."),
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory."),
|
||||||
|
element: import_zodBundle.z.string().optional().describe("Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too."),
|
||||||
|
ref: import_zodBundle.z.string().optional().describe("Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too."),
|
||||||
|
selector: import_zodBundle.z.string().optional().describe('CSS or role selector for the target element, when "ref" is not available.'),
|
||||||
|
fullPage: import_zodBundle.z.boolean().optional().describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.")
|
||||||
|
});
|
||||||
|
const screenshot = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_take_screenshot",
|
||||||
|
title: "Take a screenshot",
|
||||||
|
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
||||||
|
inputSchema: screenshotSchema,
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
if (params.fullPage && params.ref)
|
||||||
|
throw new Error("fullPage cannot be used with element screenshots.");
|
||||||
|
const fileType = params.type || "png";
|
||||||
|
const options = {
|
||||||
|
type: fileType,
|
||||||
|
quality: fileType === "png" ? void 0 : 90,
|
||||||
|
scale: "css",
|
||||||
|
...tab.actionTimeoutOptions,
|
||||||
|
...params.fullPage !== void 0 && { fullPage: params.fullPage }
|
||||||
|
};
|
||||||
|
const screenshotTarget = params.ref ? params.element || "element" : params.fullPage ? "full page" : "viewport";
|
||||||
|
const ref = params.ref || params.selector ? await tab.refLocator({ element: params.element || "", ref: params.ref || "", selector: params.selector }) : null;
|
||||||
|
const data = ref ? await ref.locator.screenshot(options) : await tab.page.screenshot(options);
|
||||||
|
const resolvedFile = await response.resolveClientFile({ prefix: ref ? "element" : "page", ext: fileType, suggestedFilename: params.filename }, `Screenshot of ${screenshotTarget}`);
|
||||||
|
response.addCode(`// Screenshot ${screenshotTarget} and save it as ${resolvedFile.relativeName}`);
|
||||||
|
if (ref)
|
||||||
|
response.addCode(`await page.${ref.resolved}.screenshot(${(0, import_stringUtils.formatObject)({ ...options, path: resolvedFile.relativeName })});`);
|
||||||
|
else
|
||||||
|
response.addCode(`await page.screenshot(${(0, import_stringUtils.formatObject)({ ...options, path: resolvedFile.relativeName })});`);
|
||||||
|
await response.addFileResult(resolvedFile, data);
|
||||||
|
await response.registerImageResult(data, fileType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function scaleImageToFitMessage(buffer, imageType) {
|
||||||
|
const image = imageType === "png" ? import_utilsBundle.PNG.sync.read(buffer) : import_utilsBundle.jpegjs.decode(buffer, { maxMemoryUsageInMB: 512 });
|
||||||
|
const pixels = image.width * image.height;
|
||||||
|
const shrink = Math.min(1568 / image.width, 1568 / image.height, Math.sqrt(1.15 * 1024 * 1024 / pixels));
|
||||||
|
if (shrink > 1)
|
||||||
|
return buffer;
|
||||||
|
const width = image.width * shrink | 0;
|
||||||
|
const height = image.height * shrink | 0;
|
||||||
|
const scaledImage = (0, import_imageUtils.scaleImageToSize)(image, { width, height });
|
||||||
|
return imageType === "png" ? import_utilsBundle.PNG.sync.write(scaledImage) : import_utilsBundle.jpegjs.encode(scaledImage, 80).data;
|
||||||
|
}
|
||||||
|
var screenshot_default = [
|
||||||
|
screenshot
|
||||||
|
];
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
scaleImageToFitMessage
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var sessionLog_exports = {};
|
||||||
|
__export(sessionLog_exports, {
|
||||||
|
SessionLog: () => SessionLog
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(sessionLog_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_context = require("./context");
|
||||||
|
var import_response = require("./response");
|
||||||
|
class SessionLog {
|
||||||
|
constructor(sessionFolder, cwd) {
|
||||||
|
this._sessionFileQueue = Promise.resolve();
|
||||||
|
this._folder = sessionFolder;
|
||||||
|
this._file = import_path.default.join(this._folder, "session.md");
|
||||||
|
this._cwd = cwd;
|
||||||
|
}
|
||||||
|
static async create(config, cwd) {
|
||||||
|
const sessionFolder = await (0, import_context.outputFile)({ config, cwd }, `session-${Date.now()}`, { origin: "code" });
|
||||||
|
await import_fs.default.promises.mkdir(sessionFolder, { recursive: true });
|
||||||
|
console.error(`Session: ${sessionFolder}`);
|
||||||
|
return new SessionLog(sessionFolder, cwd);
|
||||||
|
}
|
||||||
|
logResponse(toolName, toolArgs, responseObject) {
|
||||||
|
const parsed = { ...(0, import_response.parseResponse)(responseObject, this._cwd), text: void 0 };
|
||||||
|
const lines = [""];
|
||||||
|
lines.push(
|
||||||
|
`### Tool call: ${toolName}`,
|
||||||
|
`- Args`,
|
||||||
|
"```json",
|
||||||
|
JSON.stringify(toolArgs, null, 2),
|
||||||
|
"```"
|
||||||
|
);
|
||||||
|
if (parsed) {
|
||||||
|
lines.push(`- Result`);
|
||||||
|
lines.push("```json");
|
||||||
|
lines.push(JSON.stringify(parsed, null, 2));
|
||||||
|
lines.push("```");
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
SessionLog
|
||||||
|
});
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var snapshot_exports = {};
|
||||||
|
__export(snapshot_exports, {
|
||||||
|
default: () => snapshot_default,
|
||||||
|
elementSchema: () => elementSchema
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(snapshot_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_stringUtils = require("../../utils/isomorphic/stringUtils");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const snapshot = (0, import_tool.defineTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_snapshot",
|
||||||
|
title: "Page snapshot",
|
||||||
|
description: "Capture accessibility snapshot of the current page, this is better than screenshot",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("Save snapshot to markdown file instead of returning it in the response."),
|
||||||
|
selector: import_zodBundle.z.string().optional().describe("Element selector of the root element to capture a partial snapshot instead of the whole page"),
|
||||||
|
depth: import_zodBundle.z.number().optional().describe("Limit the depth of the snapshot tree")
|
||||||
|
}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
await context.ensureTab();
|
||||||
|
response.setIncludeFullSnapshot(params.filename, params.selector, params.depth);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const elementSchema = import_zodBundle.z.object({
|
||||||
|
element: import_zodBundle.z.string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
|
||||||
|
ref: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot"),
|
||||||
|
selector: import_zodBundle.z.string().optional().describe('CSS or role selector for the target element, when "ref" is not available')
|
||||||
|
});
|
||||||
|
const clickSchema = elementSchema.extend({
|
||||||
|
doubleClick: import_zodBundle.z.boolean().optional().describe("Whether to perform a double click instead of a single click"),
|
||||||
|
button: import_zodBundle.z.enum(["left", "right", "middle"]).optional().describe("Button to click, defaults to left"),
|
||||||
|
modifiers: import_zodBundle.z.array(import_zodBundle.z.enum(["Alt", "Control", "ControlOrMeta", "Meta", "Shift"])).optional().describe("Modifier keys to press")
|
||||||
|
});
|
||||||
|
const click = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_click",
|
||||||
|
title: "Click",
|
||||||
|
description: "Perform click on a web page",
|
||||||
|
inputSchema: clickSchema,
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
const { locator, resolved } = await tab.refLocator(params);
|
||||||
|
const options = {
|
||||||
|
button: params.button,
|
||||||
|
modifiers: params.modifiers,
|
||||||
|
...tab.actionTimeoutOptions
|
||||||
|
};
|
||||||
|
const optionsArg = (0, import_stringUtils.formatObjectOrVoid)(options);
|
||||||
|
if (params.doubleClick)
|
||||||
|
response.addCode(`await page.${resolved}.dblclick(${optionsArg});`);
|
||||||
|
else
|
||||||
|
response.addCode(`await page.${resolved}.click(${optionsArg});`);
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
if (params.doubleClick)
|
||||||
|
await locator.dblclick(options);
|
||||||
|
else
|
||||||
|
await locator.click(options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const drag = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_drag",
|
||||||
|
title: "Drag mouse",
|
||||||
|
description: "Perform drag and drop between two elements",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
startElement: import_zodBundle.z.string().describe("Human-readable source element description used to obtain the permission to interact with the element"),
|
||||||
|
startRef: import_zodBundle.z.string().describe("Exact source element reference from the page snapshot"),
|
||||||
|
startSelector: import_zodBundle.z.string().optional().describe("CSS or role selector for the source element, when ref is not available"),
|
||||||
|
endElement: import_zodBundle.z.string().describe("Human-readable target element description used to obtain the permission to interact with the element"),
|
||||||
|
endRef: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot"),
|
||||||
|
endSelector: import_zodBundle.z.string().optional().describe("CSS or role selector for the target element, when ref is not available")
|
||||||
|
}),
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
const [start, end] = await tab.refLocators([
|
||||||
|
{ ref: params.startRef, selector: params.startSelector, element: params.startElement },
|
||||||
|
{ ref: params.endRef, selector: params.endSelector, element: params.endElement }
|
||||||
|
]);
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
await start.locator.dragTo(end.locator, tab.actionTimeoutOptions);
|
||||||
|
});
|
||||||
|
response.addCode(`await page.${start.resolved}.dragTo(page.${end.resolved});`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const hover = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_hover",
|
||||||
|
title: "Hover mouse",
|
||||||
|
description: "Hover over element on page",
|
||||||
|
inputSchema: elementSchema,
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
const { locator, resolved } = await tab.refLocator(params);
|
||||||
|
response.addCode(`await page.${resolved}.hover();`);
|
||||||
|
await locator.hover(tab.actionTimeoutOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const selectOptionSchema = elementSchema.extend({
|
||||||
|
values: import_zodBundle.z.array(import_zodBundle.z.string()).describe("Array of values to select in the dropdown. This can be a single value or multiple values.")
|
||||||
|
});
|
||||||
|
const selectOption = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_select_option",
|
||||||
|
title: "Select option",
|
||||||
|
description: "Select an option in a dropdown",
|
||||||
|
inputSchema: selectOptionSchema,
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
const { locator, resolved } = await tab.refLocator(params);
|
||||||
|
response.addCode(`await page.${resolved}.selectOption(${(0, import_stringUtils.formatObject)(params.values)});`);
|
||||||
|
await locator.selectOption(params.values, tab.actionTimeoutOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const pickLocator = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "testing",
|
||||||
|
schema: {
|
||||||
|
name: "browser_generate_locator",
|
||||||
|
title: "Create locator for element",
|
||||||
|
description: "Generate locator for the given element to use in tests",
|
||||||
|
inputSchema: elementSchema,
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { resolved } = await tab.refLocator(params);
|
||||||
|
response.addTextResult(resolved);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const check = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core-input",
|
||||||
|
skillOnly: true,
|
||||||
|
schema: {
|
||||||
|
name: "browser_check",
|
||||||
|
title: "Check",
|
||||||
|
description: "Check a checkbox or radio button",
|
||||||
|
inputSchema: elementSchema,
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { locator, resolved } = await tab.refLocator(params);
|
||||||
|
response.addCode(`await page.${resolved}.check();`);
|
||||||
|
await locator.check(tab.actionTimeoutOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const uncheck = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "core-input",
|
||||||
|
skillOnly: true,
|
||||||
|
schema: {
|
||||||
|
name: "browser_uncheck",
|
||||||
|
title: "Uncheck",
|
||||||
|
description: "Uncheck a checkbox or radio button",
|
||||||
|
inputSchema: elementSchema,
|
||||||
|
type: "input"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { locator, resolved } = await tab.refLocator(params);
|
||||||
|
response.addCode(`await page.${resolved}.uncheck();`);
|
||||||
|
await locator.uncheck(tab.actionTimeoutOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var snapshot_default = [
|
||||||
|
snapshot,
|
||||||
|
click,
|
||||||
|
drag,
|
||||||
|
hover,
|
||||||
|
selectOption,
|
||||||
|
pickLocator,
|
||||||
|
check,
|
||||||
|
uncheck
|
||||||
|
];
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
elementSchema
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var storage_exports = {};
|
||||||
|
__export(storage_exports, {
|
||||||
|
default: () => storage_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(storage_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const storageState = (0, import_tool.defineTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_storage_state",
|
||||||
|
title: "Save storage state",
|
||||||
|
description: "Save storage state (cookies, local storage) to a file for later reuse",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("File name to save the storage state to. Defaults to `storage-state-{timestamp}.json` if not specified.")
|
||||||
|
}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
const state = await browserContext.storageState();
|
||||||
|
const serializedState = JSON.stringify(state, null, 2);
|
||||||
|
const resolvedFile = await response.resolveClientFile({ prefix: "storage-state", ext: "json", suggestedFilename: params.filename }, "Storage state");
|
||||||
|
response.addCode(`await page.context().storageState({ path: '${resolvedFile.relativeName}' });`);
|
||||||
|
await response.addFileResult(resolvedFile, serializedState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const setStorageState = (0, import_tool.defineTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_set_storage_state",
|
||||||
|
title: "Restore storage state",
|
||||||
|
description: "Restore storage state (cookies, local storage) from a file. This clears existing cookies and local storage before restoring.",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().describe("Path to the storage state file to restore from")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
const resolvedFilename = await response.resolveClientFilename(params.filename);
|
||||||
|
await browserContext.setStorageState(resolvedFilename);
|
||||||
|
response.addTextResult(`Storage state restored from ${params.filename}`);
|
||||||
|
response.addCode(`await page.context().setStorageState('${params.filename}');`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var storage_default = [
|
||||||
|
storageState,
|
||||||
|
setStorageState
|
||||||
|
];
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var tab_exports = {};
|
||||||
|
__export(tab_exports, {
|
||||||
|
Tab: () => Tab,
|
||||||
|
renderModalStates: () => renderModalStates,
|
||||||
|
shouldIncludeMessage: () => shouldIncludeMessage
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(tab_exports);
|
||||||
|
var import_url = __toESM(require("url"));
|
||||||
|
var import_events = require("events");
|
||||||
|
var import_locatorGenerators = require("../../utils/isomorphic/locatorGenerators");
|
||||||
|
var import_locatorParser = require("../../utils/isomorphic/locatorParser");
|
||||||
|
var import_manualPromise = require("../../utils/isomorphic/manualPromise");
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
var import_eventsHelper = require("../../server/utils/eventsHelper");
|
||||||
|
var import_disposable = require("../../server/utils/disposable");
|
||||||
|
var import_utils = require("./utils");
|
||||||
|
var import_logFile = require("./logFile");
|
||||||
|
var import_dialogs = require("./dialogs");
|
||||||
|
var import_files = require("./files");
|
||||||
|
const TabEvents = {
|
||||||
|
modalState: "modalState"
|
||||||
|
};
|
||||||
|
class Tab extends import_events.EventEmitter {
|
||||||
|
constructor(context, page, onPageClose) {
|
||||||
|
super();
|
||||||
|
this._lastHeader = { title: "about:blank", url: "about:blank", current: false, console: { total: 0, warnings: 0, errors: 0 } };
|
||||||
|
this._downloads = [];
|
||||||
|
this._requests = [];
|
||||||
|
this._modalStates = [];
|
||||||
|
this._recentEventEntries = [];
|
||||||
|
this.context = context;
|
||||||
|
this.page = page;
|
||||||
|
this._onPageClose = onPageClose;
|
||||||
|
const p = page;
|
||||||
|
this._disposables = [
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(p, "console", (event) => this._handleConsoleMessage(messageToConsoleMessage(event))),
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(p, "pageerror", (error) => this._handleConsoleMessage(pageErrorToConsoleMessage(error))),
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(p, "request", (request) => this._handleRequest(request)),
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(p, "response", (response) => this._handleResponse(response)),
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(p, "requestfailed", (request) => this._handleRequestFailed(request)),
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(p, "close", () => this._onClose()),
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(p, "filechooser", (chooser) => {
|
||||||
|
this.setModalState({
|
||||||
|
type: "fileChooser",
|
||||||
|
description: "File chooser",
|
||||||
|
fileChooser: chooser,
|
||||||
|
clearedBy: { tool: import_files.uploadFile.schema.name, skill: "upload" }
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(p, "dialog", (dialog) => this._dialogShown(dialog)),
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(p, "download", (download) => {
|
||||||
|
void this._downloadStarted(download);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
page[tabSymbol] = this;
|
||||||
|
const wallTime = Date.now();
|
||||||
|
this._consoleLog = new import_logFile.LogFile(this.context, wallTime, "console", "Console");
|
||||||
|
this._initializedPromise = this._initialize();
|
||||||
|
this.actionTimeoutOptions = { timeout: context.config.timeouts?.action };
|
||||||
|
this.navigationTimeoutOptions = { timeout: context.config.timeouts?.navigation };
|
||||||
|
this.expectTimeoutOptions = { timeout: context.config.timeouts?.expect };
|
||||||
|
}
|
||||||
|
async dispose() {
|
||||||
|
await (0, import_disposable.disposeAll)(this._disposables);
|
||||||
|
this._consoleLog.stop();
|
||||||
|
}
|
||||||
|
static forPage(page) {
|
||||||
|
return page[tabSymbol];
|
||||||
|
}
|
||||||
|
static async collectConsoleMessages(page) {
|
||||||
|
const result = [];
|
||||||
|
const messages = await page.consoleMessages().catch(() => []);
|
||||||
|
for (const message of messages)
|
||||||
|
result.push(messageToConsoleMessage(message));
|
||||||
|
const errors = await page.pageErrors().catch(() => []);
|
||||||
|
for (const error of errors)
|
||||||
|
result.push(pageErrorToConsoleMessage(error));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
async _initialize() {
|
||||||
|
for (const message of await Tab.collectConsoleMessages(this.page))
|
||||||
|
this._handleConsoleMessage(message);
|
||||||
|
const requests = await this.page.requests().catch(() => []);
|
||||||
|
for (const request of requests.filter((r) => r.existingResponse() || r.failure()))
|
||||||
|
this._requests.push(request);
|
||||||
|
for (const initPage of this.context.config.browser?.initPage || []) {
|
||||||
|
try {
|
||||||
|
const { default: func } = await import(import_url.default.pathToFileURL(initPage).href);
|
||||||
|
await func({ page: this.page });
|
||||||
|
} catch (e) {
|
||||||
|
(0, import_utilsBundle.debug)("pw:tools:error")(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modalStates() {
|
||||||
|
return this._modalStates;
|
||||||
|
}
|
||||||
|
setModalState(modalState) {
|
||||||
|
this._modalStates.push(modalState);
|
||||||
|
this.emit(TabEvents.modalState, modalState);
|
||||||
|
}
|
||||||
|
clearModalState(modalState) {
|
||||||
|
this._modalStates = this._modalStates.filter((state) => state !== modalState);
|
||||||
|
}
|
||||||
|
_dialogShown(dialog) {
|
||||||
|
this.setModalState({
|
||||||
|
type: "dialog",
|
||||||
|
description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
|
||||||
|
dialog,
|
||||||
|
clearedBy: { tool: import_dialogs.handleDialog.schema.name, skill: "dialog-accept or dialog-dismiss" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async _downloadStarted(download) {
|
||||||
|
const outputFile = await this.context.outputFile({ suggestedFilename: sanitizeForFilePath(download.suggestedFilename()), prefix: "download", ext: "bin" }, { origin: "code" });
|
||||||
|
const entry = {
|
||||||
|
download,
|
||||||
|
finished: false,
|
||||||
|
outputFile
|
||||||
|
};
|
||||||
|
this._downloads.push(entry);
|
||||||
|
this._addLogEntry({ type: "download-start", wallTime: Date.now(), download: entry });
|
||||||
|
await download.saveAs(entry.outputFile);
|
||||||
|
entry.finished = true;
|
||||||
|
this._addLogEntry({ type: "download-finish", wallTime: Date.now(), download: entry });
|
||||||
|
}
|
||||||
|
_clearCollectedArtifacts() {
|
||||||
|
this._downloads.length = 0;
|
||||||
|
this._requests.length = 0;
|
||||||
|
this._recentEventEntries.length = 0;
|
||||||
|
this._resetLogs();
|
||||||
|
}
|
||||||
|
_resetLogs() {
|
||||||
|
const wallTime = Date.now();
|
||||||
|
this._consoleLog.stop();
|
||||||
|
this._consoleLog = new import_logFile.LogFile(this.context, wallTime, "console", "Console");
|
||||||
|
}
|
||||||
|
_handleRequest(request) {
|
||||||
|
this._requests.push(request);
|
||||||
|
const wallTime = request.timing().startTime || Date.now();
|
||||||
|
this._addLogEntry({ type: "request", wallTime, request });
|
||||||
|
}
|
||||||
|
_handleResponse(response) {
|
||||||
|
const timing = response.request().timing();
|
||||||
|
const wallTime = timing.responseStart + timing.startTime;
|
||||||
|
this._addLogEntry({ type: "request", wallTime, request: response.request() });
|
||||||
|
}
|
||||||
|
_handleRequestFailed(request) {
|
||||||
|
this._requests.push(request);
|
||||||
|
const timing = request.timing();
|
||||||
|
const wallTime = timing.responseEnd + timing.startTime;
|
||||||
|
this._addLogEntry({ type: "request", wallTime, request });
|
||||||
|
}
|
||||||
|
_handleConsoleMessage(message) {
|
||||||
|
const wallTime = message.timestamp;
|
||||||
|
this._addLogEntry({ type: "console", wallTime, message });
|
||||||
|
if (shouldIncludeMessage(this.context.config.console?.level, message.type))
|
||||||
|
this._consoleLog.appendLine(wallTime, message.toString());
|
||||||
|
}
|
||||||
|
_addLogEntry(entry) {
|
||||||
|
this._recentEventEntries.push(entry);
|
||||||
|
}
|
||||||
|
_onClose() {
|
||||||
|
this._clearCollectedArtifacts();
|
||||||
|
this._onPageClose(this);
|
||||||
|
}
|
||||||
|
async headerSnapshot() {
|
||||||
|
let title;
|
||||||
|
await this._raceAgainstModalStates(async () => {
|
||||||
|
title = await this.page.title();
|
||||||
|
});
|
||||||
|
const newHeader = {
|
||||||
|
title: title ?? "",
|
||||||
|
url: this.page.url(),
|
||||||
|
current: this.isCurrentTab(),
|
||||||
|
console: await this.consoleMessageCount()
|
||||||
|
};
|
||||||
|
if (!tabHeaderEquals(this._lastHeader, newHeader)) {
|
||||||
|
this._lastHeader = newHeader;
|
||||||
|
return { ...this._lastHeader, changed: true };
|
||||||
|
}
|
||||||
|
return { ...this._lastHeader, changed: false };
|
||||||
|
}
|
||||||
|
isCurrentTab() {
|
||||||
|
return this === this.context.currentTab();
|
||||||
|
}
|
||||||
|
async waitForLoadState(state, options) {
|
||||||
|
await this._initializedPromise;
|
||||||
|
await this.page.waitForLoadState(state, options).catch((e) => (0, import_utilsBundle.debug)("pw:tools:error")(e));
|
||||||
|
}
|
||||||
|
async navigate(url2) {
|
||||||
|
await this._initializedPromise;
|
||||||
|
this._clearCollectedArtifacts();
|
||||||
|
const { promise: downloadEvent, abort: abortDownloadEvent } = (0, import_utils.eventWaiter)(this.page, "download", 3e3);
|
||||||
|
try {
|
||||||
|
await this.page.goto(url2, { waitUntil: "domcontentloaded", ...this.navigationTimeoutOptions });
|
||||||
|
abortDownloadEvent();
|
||||||
|
} catch (_e) {
|
||||||
|
const e = _e;
|
||||||
|
const mightBeDownload = e.message.includes("net::ERR_ABORTED") || e.message.includes("Download is starting");
|
||||||
|
if (!mightBeDownload)
|
||||||
|
throw e;
|
||||||
|
const download = await downloadEvent;
|
||||||
|
if (!download)
|
||||||
|
throw e;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.waitForLoadState("load", { timeout: 5e3 });
|
||||||
|
}
|
||||||
|
async consoleMessageCount() {
|
||||||
|
await this._initializedPromise;
|
||||||
|
const messages = await this.page.consoleMessages({ filter: "since-navigation" });
|
||||||
|
const pageErrors = await this.page.pageErrors({ filter: "since-navigation" });
|
||||||
|
let errors = pageErrors.length;
|
||||||
|
let warnings = 0;
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.type() === "error")
|
||||||
|
errors++;
|
||||||
|
else if (message.type() === "warning")
|
||||||
|
warnings++;
|
||||||
|
}
|
||||||
|
return { total: messages.length + pageErrors.length, errors, warnings };
|
||||||
|
}
|
||||||
|
async consoleMessages(level, all) {
|
||||||
|
await this._initializedPromise;
|
||||||
|
const result = [];
|
||||||
|
const messages = await this.page.consoleMessages({ filter: all ? "all" : "since-navigation" });
|
||||||
|
for (const message of messages) {
|
||||||
|
const cm = messageToConsoleMessage(message);
|
||||||
|
if (shouldIncludeMessage(level, cm.type))
|
||||||
|
result.push(cm);
|
||||||
|
}
|
||||||
|
if (shouldIncludeMessage(level, "error")) {
|
||||||
|
const errors = await this.page.pageErrors({ filter: all ? "all" : "since-navigation" });
|
||||||
|
for (const error of errors)
|
||||||
|
result.push(pageErrorToConsoleMessage(error));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
async clearConsoleMessages() {
|
||||||
|
await this._initializedPromise;
|
||||||
|
await Promise.all([
|
||||||
|
this.page.clearConsoleMessages(),
|
||||||
|
this.page.clearPageErrors()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
async requests() {
|
||||||
|
await this._initializedPromise;
|
||||||
|
return this._requests;
|
||||||
|
}
|
||||||
|
async clearRequests() {
|
||||||
|
await this._initializedPromise;
|
||||||
|
this._requests.length = 0;
|
||||||
|
}
|
||||||
|
async captureSnapshot(selector, depth, relativeTo) {
|
||||||
|
await this._initializedPromise;
|
||||||
|
let tabSnapshot;
|
||||||
|
const modalStates = await this._raceAgainstModalStates(async () => {
|
||||||
|
const ariaSnapshot = selector ? await this.page.locator(selector).ariaSnapshot({ mode: "ai", depth }) : await this.page.ariaSnapshot({ mode: "ai", depth });
|
||||||
|
tabSnapshot = {
|
||||||
|
ariaSnapshot,
|
||||||
|
modalStates: [],
|
||||||
|
events: []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (tabSnapshot) {
|
||||||
|
tabSnapshot.consoleLink = await this._consoleLog.take(relativeTo);
|
||||||
|
tabSnapshot.events = this._recentEventEntries;
|
||||||
|
this._recentEventEntries = [];
|
||||||
|
}
|
||||||
|
return tabSnapshot ?? {
|
||||||
|
ariaSnapshot: "",
|
||||||
|
modalStates,
|
||||||
|
events: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_javaScriptBlocked() {
|
||||||
|
return this._modalStates.some((state) => state.type === "dialog");
|
||||||
|
}
|
||||||
|
async _raceAgainstModalStates(action) {
|
||||||
|
if (this.modalStates().length)
|
||||||
|
return this.modalStates();
|
||||||
|
const promise = new import_manualPromise.ManualPromise();
|
||||||
|
const listener = (modalState) => promise.resolve([modalState]);
|
||||||
|
this.once(TabEvents.modalState, listener);
|
||||||
|
return await Promise.race([
|
||||||
|
action().then(() => {
|
||||||
|
this.off(TabEvents.modalState, listener);
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
promise
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
async waitForCompletion(callback) {
|
||||||
|
await this._initializedPromise;
|
||||||
|
await this._raceAgainstModalStates(() => (0, import_utils.waitForCompletion)(this, callback));
|
||||||
|
}
|
||||||
|
async refLocator(params) {
|
||||||
|
await this._initializedPromise;
|
||||||
|
return (await this.refLocators([params]))[0];
|
||||||
|
}
|
||||||
|
async refLocators(params) {
|
||||||
|
await this._initializedPromise;
|
||||||
|
return Promise.all(params.map(async (param) => {
|
||||||
|
if (param.selector) {
|
||||||
|
const selector = (0, import_locatorParser.locatorOrSelectorAsSelector)("javascript", param.selector, this.context.config.testIdAttribute || "data-testid");
|
||||||
|
const handle = await this.page.$(selector);
|
||||||
|
if (!handle)
|
||||||
|
throw new Error(`"${param.selector}" does not match any elements.`);
|
||||||
|
handle.dispose().catch(() => {
|
||||||
|
});
|
||||||
|
return { locator: this.page.locator(selector), resolved: (0, import_locatorGenerators.asLocator)("javascript", selector) };
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
let locator = this.page.locator(`aria-ref=${param.ref}`);
|
||||||
|
if (param.element)
|
||||||
|
locator = locator.describe(param.element);
|
||||||
|
const resolved = await locator.normalize();
|
||||||
|
return { locator, resolved: resolved.toString() };
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
async waitForTimeout(time) {
|
||||||
|
if (this._javaScriptBlocked()) {
|
||||||
|
await new Promise((f) => setTimeout(f, time));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.page.evaluate(() => new Promise((f) => setTimeout(f, 1e3))).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function messageToConsoleMessage(message) {
|
||||||
|
return {
|
||||||
|
type: message.type(),
|
||||||
|
timestamp: message.timestamp(),
|
||||||
|
text: message.text(),
|
||||||
|
toString: () => `[${message.type().toUpperCase()}] ${message.text()} @ ${message.location().url}:${message.location().lineNumber}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function pageErrorToConsoleMessage(errorOrValue) {
|
||||||
|
if (errorOrValue instanceof Error) {
|
||||||
|
return {
|
||||||
|
type: "error",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
text: errorOrValue.message,
|
||||||
|
toString: () => errorOrValue.stack || errorOrValue.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "error",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
text: String(errorOrValue),
|
||||||
|
toString: () => String(errorOrValue)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function renderModalStates(config, modalStates) {
|
||||||
|
const result = [];
|
||||||
|
if (modalStates.length === 0)
|
||||||
|
result.push("- There is no modal state present");
|
||||||
|
for (const state of modalStates)
|
||||||
|
result.push(`- [${state.description}]: can be handled by ${config.skillMode ? state.clearedBy.skill : state.clearedBy.tool}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const consoleMessageLevels = ["error", "warning", "info", "debug"];
|
||||||
|
function shouldIncludeMessage(thresholdLevel, type) {
|
||||||
|
const messageLevel = consoleLevelForMessageType(type);
|
||||||
|
return consoleMessageLevels.indexOf(messageLevel) <= consoleMessageLevels.indexOf(thresholdLevel || "info");
|
||||||
|
}
|
||||||
|
function consoleLevelForMessageType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case "assert":
|
||||||
|
case "error":
|
||||||
|
return "error";
|
||||||
|
case "warning":
|
||||||
|
return "warning";
|
||||||
|
case "count":
|
||||||
|
case "dir":
|
||||||
|
case "dirxml":
|
||||||
|
case "info":
|
||||||
|
case "log":
|
||||||
|
case "table":
|
||||||
|
case "time":
|
||||||
|
case "timeEnd":
|
||||||
|
return "info";
|
||||||
|
case "clear":
|
||||||
|
case "debug":
|
||||||
|
case "endGroup":
|
||||||
|
case "profile":
|
||||||
|
case "profileEnd":
|
||||||
|
case "startGroup":
|
||||||
|
case "startGroupCollapsed":
|
||||||
|
case "trace":
|
||||||
|
return "debug";
|
||||||
|
default:
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tabSymbol = Symbol("tabSymbol");
|
||||||
|
function sanitizeForFilePath(s) {
|
||||||
|
const sanitize = (s2) => s2.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, "-");
|
||||||
|
const separator = s.lastIndexOf(".");
|
||||||
|
if (separator === -1)
|
||||||
|
return sanitize(s);
|
||||||
|
return sanitize(s.substring(0, separator)) + "." + sanitize(s.substring(separator + 1));
|
||||||
|
}
|
||||||
|
function tabHeaderEquals(a, b) {
|
||||||
|
return a.title === b.title && a.url === b.url && a.current === b.current && a.console.errors === b.console.errors && a.console.warnings === b.console.warnings && a.console.total === b.console.total;
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
Tab,
|
||||||
|
renderModalStates,
|
||||||
|
shouldIncludeMessage
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var tabs_exports = {};
|
||||||
|
__export(tabs_exports, {
|
||||||
|
default: () => tabs_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(tabs_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
var import_response = require("./response");
|
||||||
|
const browserTabs = (0, import_tool.defineTool)({
|
||||||
|
capability: "core-tabs",
|
||||||
|
schema: {
|
||||||
|
name: "browser_tabs",
|
||||||
|
title: "Manage tabs",
|
||||||
|
description: "List, create, close, or select a browser tab.",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
action: import_zodBundle.z.enum(["list", "new", "close", "select"]).describe("Operation to perform"),
|
||||||
|
index: import_zodBundle.z.number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed.")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
switch (params.action) {
|
||||||
|
case "list": {
|
||||||
|
await context.ensureTab();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "new": {
|
||||||
|
await context.newTab();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "close": {
|
||||||
|
await context.closeTab(params.index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "select": {
|
||||||
|
if (params.index === void 0)
|
||||||
|
throw new Error("Tab index is required");
|
||||||
|
await context.selectTab(params.index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tabHeaders = await Promise.all(context.tabs().map((tab) => tab.headerSnapshot()));
|
||||||
|
const result = (0, import_response.renderTabsMarkdown)(tabHeaders);
|
||||||
|
response.addTextResult(result.join("\n"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var tabs_default = [
|
||||||
|
browserTabs
|
||||||
|
];
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var tool_exports = {};
|
||||||
|
__export(tool_exports, {
|
||||||
|
defineTabTool: () => defineTabTool,
|
||||||
|
defineTool: () => defineTool
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(tool_exports);
|
||||||
|
function defineTool(tool) {
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
function defineTabTool(tool) {
|
||||||
|
return {
|
||||||
|
...tool,
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const tab = await context.ensureTab();
|
||||||
|
const modalStates = tab.modalStates().map((state) => state.type);
|
||||||
|
if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
|
||||||
|
response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.`);
|
||||||
|
else if (!tool.clearsModalState && modalStates.length)
|
||||||
|
response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.`);
|
||||||
|
else
|
||||||
|
return tool.handle(tab, params, response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
defineTabTool,
|
||||||
|
defineTool
|
||||||
|
});
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var tools_exports = {};
|
||||||
|
__export(tools_exports, {
|
||||||
|
browserTools: () => browserTools,
|
||||||
|
filteredTools: () => filteredTools
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(tools_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_common = __toESM(require("./common"));
|
||||||
|
var import_config = __toESM(require("./config"));
|
||||||
|
var import_console = __toESM(require("./console"));
|
||||||
|
var import_cookies = __toESM(require("./cookies"));
|
||||||
|
var import_devtools = __toESM(require("./devtools"));
|
||||||
|
var import_dialogs = __toESM(require("./dialogs"));
|
||||||
|
var import_evaluate = __toESM(require("./evaluate"));
|
||||||
|
var import_files = __toESM(require("./files"));
|
||||||
|
var import_form = __toESM(require("./form"));
|
||||||
|
var import_keyboard = __toESM(require("./keyboard"));
|
||||||
|
var import_mouse = __toESM(require("./mouse"));
|
||||||
|
var import_navigate = __toESM(require("./navigate"));
|
||||||
|
var import_network = __toESM(require("./network"));
|
||||||
|
var import_pdf = __toESM(require("./pdf"));
|
||||||
|
var import_route = __toESM(require("./route"));
|
||||||
|
var import_runCode = __toESM(require("./runCode"));
|
||||||
|
var import_snapshot = __toESM(require("./snapshot"));
|
||||||
|
var import_screenshot = __toESM(require("./screenshot"));
|
||||||
|
var import_storage = __toESM(require("./storage"));
|
||||||
|
var import_tabs = __toESM(require("./tabs"));
|
||||||
|
var import_tracing = __toESM(require("./tracing"));
|
||||||
|
var import_verify = __toESM(require("./verify"));
|
||||||
|
var import_video = __toESM(require("./video"));
|
||||||
|
var import_wait = __toESM(require("./wait"));
|
||||||
|
var import_webstorage = __toESM(require("./webstorage"));
|
||||||
|
const browserTools = [
|
||||||
|
...import_common.default,
|
||||||
|
...import_config.default,
|
||||||
|
...import_console.default,
|
||||||
|
...import_cookies.default,
|
||||||
|
...import_devtools.default,
|
||||||
|
...import_dialogs.default,
|
||||||
|
...import_evaluate.default,
|
||||||
|
...import_files.default,
|
||||||
|
...import_form.default,
|
||||||
|
...import_keyboard.default,
|
||||||
|
...import_mouse.default,
|
||||||
|
...import_navigate.default,
|
||||||
|
...import_network.default,
|
||||||
|
...import_pdf.default,
|
||||||
|
...import_route.default,
|
||||||
|
...import_runCode.default,
|
||||||
|
...import_screenshot.default,
|
||||||
|
...import_snapshot.default,
|
||||||
|
...import_storage.default,
|
||||||
|
...import_tabs.default,
|
||||||
|
...import_tracing.default,
|
||||||
|
...import_verify.default,
|
||||||
|
...import_video.default,
|
||||||
|
...import_wait.default,
|
||||||
|
...import_webstorage.default
|
||||||
|
];
|
||||||
|
function filteredTools(config2) {
|
||||||
|
return browserTools.filter((tool) => tool.capability.startsWith("core") || config2.capabilities?.includes(tool.capability)).filter((tool) => !tool.skillOnly).map((tool) => ({
|
||||||
|
...tool,
|
||||||
|
schema: {
|
||||||
|
...tool.schema,
|
||||||
|
// Note: we first ensure that "selector" property is present, so that we can omit() it without an error.
|
||||||
|
inputSchema: tool.schema.inputSchema.extend({ selector: import_zodBundle.z.string(), startSelector: import_zodBundle.z.string(), endSelector: import_zodBundle.z.string() }).omit({ selector: true, startSelector: true, endSelector: true })
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
browserTools,
|
||||||
|
filteredTools
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var tracing_exports = {};
|
||||||
|
__export(tracing_exports, {
|
||||||
|
default: () => tracing_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(tracing_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const tracingStart = (0, import_tool.defineTool)({
|
||||||
|
capability: "devtools",
|
||||||
|
schema: {
|
||||||
|
name: "browser_start_tracing",
|
||||||
|
title: "Start tracing",
|
||||||
|
description: "Start trace recording",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
const tracesDir = await context.outputFile({ prefix: "", suggestedFilename: `traces`, ext: "" }, { origin: "code" });
|
||||||
|
const name = "trace-" + Date.now();
|
||||||
|
await browserContext.tracing.start({
|
||||||
|
name,
|
||||||
|
screenshots: true,
|
||||||
|
snapshots: true,
|
||||||
|
live: true
|
||||||
|
});
|
||||||
|
response.addTextResult(`Trace recording started`);
|
||||||
|
response.addFileLink("Action log", `${tracesDir}/${name}.trace`);
|
||||||
|
response.addFileLink("Network log", `${tracesDir}/${name}.network`);
|
||||||
|
response.addFileLink("Resources", `${tracesDir}/resources`);
|
||||||
|
browserContext.tracing[traceLegendSymbol] = { tracesDir, name };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const tracingStop = (0, import_tool.defineTool)({
|
||||||
|
capability: "devtools",
|
||||||
|
schema: {
|
||||||
|
name: "browser_stop_tracing",
|
||||||
|
title: "Stop tracing",
|
||||||
|
description: "Stop trace recording",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const browserContext = await context.ensureBrowserContext();
|
||||||
|
await browserContext.tracing.stop();
|
||||||
|
const traceLegend = browserContext.tracing[traceLegendSymbol];
|
||||||
|
if (!traceLegend)
|
||||||
|
throw new Error("Tracing is not started");
|
||||||
|
delete browserContext.tracing[traceLegendSymbol];
|
||||||
|
response.addTextResult(`Trace recording stopped.`);
|
||||||
|
response.addFileLink("Trace", `${traceLegend.tracesDir}/${traceLegend.name}.trace`);
|
||||||
|
response.addFileLink("Network log", `${traceLegend.tracesDir}/${traceLegend.name}.network`);
|
||||||
|
response.addFileLink("Resources", `${traceLegend.tracesDir}/resources`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var tracing_default = [
|
||||||
|
tracingStart,
|
||||||
|
tracingStop
|
||||||
|
];
|
||||||
|
const traceLegendSymbol = Symbol("tracesDir");
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var utils_exports = {};
|
||||||
|
__export(utils_exports, {
|
||||||
|
eventWaiter: () => eventWaiter,
|
||||||
|
waitForCompletion: () => waitForCompletion
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(utils_exports);
|
||||||
|
async function waitForCompletion(tab, callback) {
|
||||||
|
const requests = [];
|
||||||
|
const requestListener = (request) => requests.push(request);
|
||||||
|
const disposeListeners = () => {
|
||||||
|
tab.page.off("request", requestListener);
|
||||||
|
};
|
||||||
|
tab.page.on("request", requestListener);
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await callback();
|
||||||
|
await tab.waitForTimeout(500);
|
||||||
|
} finally {
|
||||||
|
disposeListeners();
|
||||||
|
}
|
||||||
|
const requestedNavigation = requests.some((request) => request.isNavigationRequest());
|
||||||
|
if (requestedNavigation) {
|
||||||
|
await tab.page.mainFrame().waitForLoadState("load", { timeout: 1e4 }).catch(() => {
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const promises = [];
|
||||||
|
for (const request of requests) {
|
||||||
|
if (["document", "stylesheet", "script", "xhr", "fetch"].includes(request.resourceType()))
|
||||||
|
promises.push(request.response().then((r) => r?.finished()).catch(() => {
|
||||||
|
}));
|
||||||
|
else
|
||||||
|
promises.push(request.response().catch(() => {
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const timeout = new Promise((resolve) => setTimeout(resolve, 5e3));
|
||||||
|
await Promise.race([Promise.all(promises), timeout]);
|
||||||
|
if (requests.length)
|
||||||
|
await tab.waitForTimeout(500);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function eventWaiter(page, event, timeout) {
|
||||||
|
const disposables = [];
|
||||||
|
const eventPromise = new Promise((resolve, reject) => {
|
||||||
|
page.on(event, resolve);
|
||||||
|
disposables.push(() => page.off(event, resolve));
|
||||||
|
});
|
||||||
|
let abort;
|
||||||
|
const abortPromise = new Promise((resolve, reject) => {
|
||||||
|
abort = () => resolve(void 0);
|
||||||
|
});
|
||||||
|
const timeoutPromise = new Promise((f) => {
|
||||||
|
const timeoutId = setTimeout(() => f(void 0), timeout);
|
||||||
|
disposables.push(() => clearTimeout(timeoutId));
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
promise: Promise.race([eventPromise, abortPromise, timeoutPromise]).finally(() => disposables.forEach((dispose) => dispose())),
|
||||||
|
abort
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
eventWaiter,
|
||||||
|
waitForCompletion
|
||||||
|
});
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var verify_exports = {};
|
||||||
|
__export(verify_exports, {
|
||||||
|
default: () => verify_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(verify_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_stringUtils = require("../../utils/isomorphic/stringUtils");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const verifyElement = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "testing",
|
||||||
|
schema: {
|
||||||
|
name: "browser_verify_element_visible",
|
||||||
|
title: "Verify element visible",
|
||||||
|
description: "Verify element is visible on the page",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
role: import_zodBundle.z.string().describe('ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":`'),
|
||||||
|
accessibleName: import_zodBundle.z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"`')
|
||||||
|
}),
|
||||||
|
type: "assertion"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
for (const frame of tab.page.frames()) {
|
||||||
|
const locator = frame.getByRole(params.role, { name: params.accessibleName });
|
||||||
|
if (await locator.count() > 0) {
|
||||||
|
const resolved = await locator.normalize();
|
||||||
|
response.addCode(`await expect(page.${resolved}).toBeVisible();`);
|
||||||
|
response.addTextResult("Done");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.addError(`Element with role "${params.role}" and accessible name "${params.accessibleName}" not found`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const verifyText = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "testing",
|
||||||
|
schema: {
|
||||||
|
name: "browser_verify_text_visible",
|
||||||
|
title: "Verify text visible",
|
||||||
|
description: `Verify text is visible on the page. Prefer ${verifyElement.schema.name} if possible.`,
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
text: import_zodBundle.z.string().describe('TEXT to verify. Can be found in the snapshot like this: `- role "Accessible Name": {TEXT}` or like this: `- text: {TEXT}`')
|
||||||
|
}),
|
||||||
|
type: "assertion"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
for (const frame of tab.page.frames()) {
|
||||||
|
const locator = frame.getByText(params.text).filter({ visible: true });
|
||||||
|
if (await locator.count() > 0) {
|
||||||
|
const resolved = await locator.normalize();
|
||||||
|
response.addCode(`await expect(page.${resolved}).toBeVisible();`);
|
||||||
|
response.addTextResult("Done");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.addError("Text not found");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const verifyList = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "testing",
|
||||||
|
schema: {
|
||||||
|
name: "browser_verify_list_visible",
|
||||||
|
title: "Verify list visible",
|
||||||
|
description: "Verify list is visible on the page",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
element: import_zodBundle.z.string().describe("Human-readable list description"),
|
||||||
|
ref: import_zodBundle.z.string().describe("Exact target element reference that points to the list"),
|
||||||
|
selector: import_zodBundle.z.string().optional().describe('CSS or role selector for the target list, when "ref" is not available.'),
|
||||||
|
items: import_zodBundle.z.array(import_zodBundle.z.string()).describe("Items to verify")
|
||||||
|
}),
|
||||||
|
type: "assertion"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { locator } = await tab.refLocator({ ref: params.ref, selector: params.selector, element: params.element });
|
||||||
|
const itemTexts = [];
|
||||||
|
for (const item of params.items) {
|
||||||
|
const itemLocator = locator.getByText(item);
|
||||||
|
if (await itemLocator.count() === 0) {
|
||||||
|
response.addError(`Item "${item}" not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
itemTexts.push(await itemLocator.textContent(tab.expectTimeoutOptions));
|
||||||
|
}
|
||||||
|
const ariaSnapshot = `\`
|
||||||
|
- list:
|
||||||
|
${itemTexts.map((t) => ` - listitem: ${(0, import_stringUtils.escapeWithQuotes)(t, '"')}`).join("\n")}
|
||||||
|
\``;
|
||||||
|
response.addCode(`await expect(page.locator('body')).toMatchAriaSnapshot(${ariaSnapshot});`);
|
||||||
|
response.addTextResult("Done");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const verifyValue = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "testing",
|
||||||
|
schema: {
|
||||||
|
name: "browser_verify_value",
|
||||||
|
title: "Verify value",
|
||||||
|
description: "Verify element value",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
type: import_zodBundle.z.enum(["textbox", "checkbox", "radio", "combobox", "slider"]).describe("Type of the element"),
|
||||||
|
element: import_zodBundle.z.string().describe("Human-readable element description"),
|
||||||
|
ref: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot"),
|
||||||
|
selector: import_zodBundle.z.string().optional().describe('CSS or role selector for the target element, when "ref" is not available'),
|
||||||
|
value: import_zodBundle.z.string().describe('Value to verify. For checkbox, use "true" or "false".')
|
||||||
|
}),
|
||||||
|
type: "assertion"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { locator, resolved } = await tab.refLocator({ ref: params.ref, selector: params.selector, element: params.element });
|
||||||
|
const locatorSource = `page.${resolved}`;
|
||||||
|
if (params.type === "textbox" || params.type === "slider" || params.type === "combobox") {
|
||||||
|
const value = await locator.inputValue(tab.expectTimeoutOptions);
|
||||||
|
if (value !== params.value) {
|
||||||
|
response.addError(`Expected value "${params.value}", but got "${value}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.addCode(`await expect(${locatorSource}).toHaveValue(${(0, import_stringUtils.escapeWithQuotes)(params.value)});`);
|
||||||
|
} else if (params.type === "checkbox" || params.type === "radio") {
|
||||||
|
const value = await locator.isChecked(tab.expectTimeoutOptions);
|
||||||
|
if (value !== (params.value === "true")) {
|
||||||
|
response.addError(`Expected value "${params.value}", but got "${value}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const matcher = value ? "toBeChecked" : "not.toBeChecked";
|
||||||
|
response.addCode(`await expect(${locatorSource}).${matcher}();`);
|
||||||
|
}
|
||||||
|
response.addTextResult("Done");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var verify_default = [
|
||||||
|
verifyElement,
|
||||||
|
verifyText,
|
||||||
|
verifyList,
|
||||||
|
verifyValue
|
||||||
|
];
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var video_exports = {};
|
||||||
|
__export(video_exports, {
|
||||||
|
default: () => video_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(video_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const videoStart = (0, import_tool.defineTool)({
|
||||||
|
capability: "devtools",
|
||||||
|
schema: {
|
||||||
|
name: "browser_start_video",
|
||||||
|
title: "Start video",
|
||||||
|
description: "Start video recording",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("Filename to save the video."),
|
||||||
|
size: import_zodBundle.z.object({
|
||||||
|
width: import_zodBundle.z.number().describe("Video width"),
|
||||||
|
height: import_zodBundle.z.number().describe("Video height")
|
||||||
|
}).optional().describe("Video size")
|
||||||
|
}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const resolvedFile = await response.resolveClientFile({ prefix: "video", ext: "webm", suggestedFilename: params.filename }, "Video");
|
||||||
|
await context.startVideoRecording(resolvedFile.fileName, { size: params.size });
|
||||||
|
response.addTextResult("Video recording started.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const videoStop = (0, import_tool.defineTool)({
|
||||||
|
capability: "devtools",
|
||||||
|
schema: {
|
||||||
|
name: "browser_stop_video",
|
||||||
|
title: "Stop video",
|
||||||
|
description: "Stop video recording",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const fileNames = await context.stopVideoRecording();
|
||||||
|
if (!fileNames.length) {
|
||||||
|
response.addTextResult("No videos were recorded.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const fileName of fileNames) {
|
||||||
|
const resolvedFile = await response.resolveClientFile({
|
||||||
|
prefix: "video",
|
||||||
|
ext: "webm",
|
||||||
|
suggestedFilename: fileName
|
||||||
|
}, "Video");
|
||||||
|
await response.addFileResult(resolvedFile, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const videoChapter = (0, import_tool.defineTool)({
|
||||||
|
capability: "devtools",
|
||||||
|
schema: {
|
||||||
|
name: "browser_video_chapter",
|
||||||
|
title: "Video chapter",
|
||||||
|
description: "Add a chapter marker to the video recording. Shows a full-screen chapter card with blurred backdrop.",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
title: import_zodBundle.z.string().describe("Chapter title"),
|
||||||
|
description: import_zodBundle.z.string().optional().describe("Chapter description"),
|
||||||
|
duration: import_zodBundle.z.number().optional().describe("Duration in milliseconds to show the chapter card")
|
||||||
|
}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
const tab = context.currentTabOrDie();
|
||||||
|
await tab.page.screencast.showChapter(params.title, {
|
||||||
|
description: params.description,
|
||||||
|
duration: params.duration
|
||||||
|
});
|
||||||
|
response.addTextResult(`Chapter '${params.title}' added.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var video_default = [
|
||||||
|
videoStart,
|
||||||
|
videoStop,
|
||||||
|
videoChapter
|
||||||
|
];
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var wait_exports = {};
|
||||||
|
__export(wait_exports, {
|
||||||
|
default: () => wait_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(wait_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const wait = (0, import_tool.defineTool)({
|
||||||
|
capability: "core",
|
||||||
|
schema: {
|
||||||
|
name: "browser_wait_for",
|
||||||
|
title: "Wait for",
|
||||||
|
description: "Wait for text to appear or disappear or a specified time to pass",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
time: import_zodBundle.z.number().optional().describe("The time to wait in seconds"),
|
||||||
|
text: import_zodBundle.z.string().optional().describe("The text to wait for"),
|
||||||
|
textGone: import_zodBundle.z.string().optional().describe("The text to wait for to disappear")
|
||||||
|
}),
|
||||||
|
type: "assertion"
|
||||||
|
},
|
||||||
|
handle: async (context, params, response) => {
|
||||||
|
if (!params.text && !params.textGone && !params.time)
|
||||||
|
throw new Error("Either time, text or textGone must be provided");
|
||||||
|
if (params.time) {
|
||||||
|
response.addCode(`await new Promise(f => setTimeout(f, ${params.time} * 1000));`);
|
||||||
|
await new Promise((f) => setTimeout(f, Math.min(3e4, params.time * 1e3)));
|
||||||
|
}
|
||||||
|
const tab = context.currentTabOrDie();
|
||||||
|
const locator = params.text ? tab.page.getByText(params.text).first() : void 0;
|
||||||
|
const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : void 0;
|
||||||
|
if (goneLocator) {
|
||||||
|
response.addCode(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
|
||||||
|
await goneLocator.waitFor({ state: "hidden" });
|
||||||
|
}
|
||||||
|
if (locator) {
|
||||||
|
response.addCode(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
|
||||||
|
await locator.waitFor({ state: "visible" });
|
||||||
|
}
|
||||||
|
response.addTextResult(`Waited for ${params.text || params.textGone || params.time}`);
|
||||||
|
response.setIncludeSnapshot();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var wait_default = [
|
||||||
|
wait
|
||||||
|
];
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var webstorage_exports = {};
|
||||||
|
__export(webstorage_exports, {
|
||||||
|
default: () => webstorage_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(webstorage_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_tool = require("./tool");
|
||||||
|
const localStorageList = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_localstorage_list",
|
||||||
|
title: "List localStorage",
|
||||||
|
description: "List all localStorage key-value pairs",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const items = await tab.page.evaluate(() => {
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (key !== null)
|
||||||
|
result.push({ key, value: localStorage.getItem(key) || "" });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
if (items.length === 0)
|
||||||
|
response.addTextResult("No localStorage items found");
|
||||||
|
else
|
||||||
|
response.addTextResult(items.map((item) => `${item.key}=${item.value}`).join("\n"));
|
||||||
|
response.addCode(`await page.evaluate(() => ({ ...localStorage }));`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const localStorageGet = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_localstorage_get",
|
||||||
|
title: "Get localStorage item",
|
||||||
|
description: "Get a localStorage item by key",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to get")
|
||||||
|
}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const value = await tab.page.evaluate((key) => localStorage.getItem(key), params.key);
|
||||||
|
if (value === null)
|
||||||
|
response.addTextResult(`localStorage key '${params.key}' not found`);
|
||||||
|
else
|
||||||
|
response.addTextResult(`${params.key}=${value}`);
|
||||||
|
response.addCode(`await page.evaluate(() => localStorage.getItem('${params.key}'));`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const localStorageSet = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_localstorage_set",
|
||||||
|
title: "Set localStorage item",
|
||||||
|
description: "Set a localStorage item",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to set"),
|
||||||
|
value: import_zodBundle.z.string().describe("Value to set")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
await tab.page.evaluate(({ key, value }) => localStorage.setItem(key, value), params);
|
||||||
|
response.addCode(`await page.evaluate(() => localStorage.setItem('${params.key}', '${params.value}'));`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const localStorageDelete = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_localstorage_delete",
|
||||||
|
title: "Delete localStorage item",
|
||||||
|
description: "Delete a localStorage item",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to delete")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
await tab.page.evaluate((key) => localStorage.removeItem(key), params.key);
|
||||||
|
response.addCode(`await page.evaluate(() => localStorage.removeItem('${params.key}'));`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const localStorageClear = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_localstorage_clear",
|
||||||
|
title: "Clear localStorage",
|
||||||
|
description: "Clear all localStorage",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
await tab.page.evaluate(() => localStorage.clear());
|
||||||
|
response.addCode(`await page.evaluate(() => localStorage.clear());`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sessionStorageList = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_sessionstorage_list",
|
||||||
|
title: "List sessionStorage",
|
||||||
|
description: "List all sessionStorage key-value pairs",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const items = await tab.page.evaluate(() => {
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < sessionStorage.length; i++) {
|
||||||
|
const key = sessionStorage.key(i);
|
||||||
|
if (key !== null)
|
||||||
|
result.push({ key, value: sessionStorage.getItem(key) || "" });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
if (items.length === 0)
|
||||||
|
response.addTextResult("No sessionStorage items found");
|
||||||
|
else
|
||||||
|
response.addTextResult(items.map((item) => `${item.key}=${item.value}`).join("\n"));
|
||||||
|
response.addCode(`await page.evaluate(() => ({ ...sessionStorage }));`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sessionStorageGet = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_sessionstorage_get",
|
||||||
|
title: "Get sessionStorage item",
|
||||||
|
description: "Get a sessionStorage item by key",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to get")
|
||||||
|
}),
|
||||||
|
type: "readOnly"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const value = await tab.page.evaluate((key) => sessionStorage.getItem(key), params.key);
|
||||||
|
if (value === null)
|
||||||
|
response.addTextResult(`sessionStorage key '${params.key}' not found`);
|
||||||
|
else
|
||||||
|
response.addTextResult(`${params.key}=${value}`);
|
||||||
|
response.addCode(`await page.evaluate(() => sessionStorage.getItem('${params.key}'));`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sessionStorageSet = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_sessionstorage_set",
|
||||||
|
title: "Set sessionStorage item",
|
||||||
|
description: "Set a sessionStorage item",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to set"),
|
||||||
|
value: import_zodBundle.z.string().describe("Value to set")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
await tab.page.evaluate(({ key, value }) => sessionStorage.setItem(key, value), params);
|
||||||
|
response.addCode(`await page.evaluate(() => sessionStorage.setItem('${params.key}', '${params.value}'));`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sessionStorageDelete = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_sessionstorage_delete",
|
||||||
|
title: "Delete sessionStorage item",
|
||||||
|
description: "Delete a sessionStorage item",
|
||||||
|
inputSchema: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to delete")
|
||||||
|
}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
await tab.page.evaluate((key) => sessionStorage.removeItem(key), params.key);
|
||||||
|
response.addCode(`await page.evaluate(() => sessionStorage.removeItem('${params.key}'));`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sessionStorageClear = (0, import_tool.defineTabTool)({
|
||||||
|
capability: "storage",
|
||||||
|
schema: {
|
||||||
|
name: "browser_sessionstorage_clear",
|
||||||
|
title: "Clear sessionStorage",
|
||||||
|
description: "Clear all sessionStorage",
|
||||||
|
inputSchema: import_zodBundle.z.object({}),
|
||||||
|
type: "action"
|
||||||
|
},
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
await tab.page.evaluate(() => sessionStorage.clear());
|
||||||
|
response.addCode(`await page.evaluate(() => sessionStorage.clear());`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var webstorage_default = [
|
||||||
|
localStorageList,
|
||||||
|
localStorageGet,
|
||||||
|
localStorageSet,
|
||||||
|
localStorageDelete,
|
||||||
|
localStorageClear,
|
||||||
|
sessionStorageList,
|
||||||
|
sessionStorageGet,
|
||||||
|
sessionStorageSet,
|
||||||
|
sessionStorageDelete,
|
||||||
|
sessionStorageClear
|
||||||
|
];
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"use strict";
|
||||||
|
var import_program = require("./program");
|
||||||
|
(0, import_program.program)().catch((e) => {
|
||||||
|
console.error(e.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
{
|
||||||
|
"global": "Usage: playwright-cli <command> [args] [options]\nUsage: playwright-cli -s=<session> <command> [args] [options]\n\nCore:\n open [url] open the browser\n attach <name> attach to a running playwright browser\n close close the browser\n goto <url> navigate to a url\n type <text> type text into editable element\n click <target> [button] perform click on a web page\n dblclick <target> [button] perform double click on a web page\n fill <target> <text> fill text into editable element\n drag <startElement> <endElement> perform drag and drop between two elements\n hover <target> hover over element on page\n select <target> <val> select an option in a dropdown\n upload <file> upload one or multiple files\n check <target> check a checkbox or radio button\n uncheck <target> uncheck a checkbox or radio button\n snapshot [element] capture page snapshot to obtain element ref\n eval <func> [element] evaluate javascript expression on page or element\n dialog-accept [prompt] accept a dialog\n dialog-dismiss dismiss a dialog\n resize <w> <h> resize the browser window\n delete-data delete session data\n\nNavigation:\n go-back go back to the previous page\n go-forward go forward to the next page\n reload reload the current page\n\nKeyboard:\n press <key> press a key on the keyboard, `a`, `arrowleft`\n keydown <key> press a key down on the keyboard\n keyup <key> press a key up on the keyboard\n\nMouse:\n mousemove <x> <y> move mouse to a given position\n mousedown [button] press mouse down\n mouseup [button] press mouse up\n mousewheel <dx> <dy> scroll mouse wheel\n\nSave as:\n screenshot [target] screenshot of the current page or element\n pdf save page as pdf\n\nTabs:\n tab-list list all tabs\n tab-new [url] create a new tab\n tab-close [index] close a browser tab\n tab-select <index> select a browser tab\n\nStorage:\n state-load <filename> loads browser storage (authentication) state from a file\n state-save [filename] saves the current storage (authentication) state to a file\n cookie-list list all cookies (optionally filtered by domain/path)\n cookie-get <name> get a specific cookie by name\n cookie-set <name> <value> set a cookie with optional flags\n cookie-delete <name> delete a specific cookie\n cookie-clear clear all cookies\n localstorage-list list all localstorage key-value pairs\n localstorage-get <key> get a localstorage item by key\n localstorage-set <key> <value> set a localstorage item\n localstorage-delete <key> delete a localstorage item\n localstorage-clear clear all localstorage\n sessionstorage-list list all sessionstorage key-value pairs\n sessionstorage-get <key> get a sessionstorage item by key\n sessionstorage-set <key> <value> set a sessionstorage item\n sessionstorage-delete <key> delete a sessionstorage item\n sessionstorage-clear clear all sessionstorage\n\nNetwork:\n route <pattern> mock network requests matching a url pattern\n route-list list all active network routes\n unroute [pattern] remove routes matching a pattern (or all routes)\n network-state-set <state> set the browser network state to online or offline\n\nDevTools:\n console [min-level] list console messages\n run-code [code] run playwright code snippet\n network list all network requests since loading the page\n tracing-start start trace recording\n tracing-stop stop trace recording\n video-start [filename] start video recording\n video-stop stop video recording\n video-chapter <title> add a chapter marker to the video recording\n show show browser devtools\n pause-at <location> run the test up to a specific location and pause there\n resume resume the test execution\n step-over step over the next call in the test\n\nInstall:\n install initialize workspace\n install-browser [browser] install browser\n\nBrowser sessions:\n list list browser sessions\n close-all close all browser sessions\n kill-all forcefully kill all browser sessions (for stale/zombie processes)\n\nGlobal options:\n --help [command] print help\n --version print version",
|
||||||
|
"commands": {
|
||||||
|
"open": {
|
||||||
|
"help": "playwright-cli open [url]\n\nOpen the browser\n\nArguments:\n [url] the url to navigate to\nOptions:\n --browser browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.\n --config path to the configuration file, defaults to .playwright/cli.config.json\n --extension connect to browser extension\n --headed run browser in headed mode\n --persistent use persistent browser profile\n --profile use persistent browser profile, store profile in specified directory.",
|
||||||
|
"flags": {
|
||||||
|
"browser": "string",
|
||||||
|
"config": "string",
|
||||||
|
"extension": "boolean",
|
||||||
|
"headed": "boolean",
|
||||||
|
"persistent": "boolean",
|
||||||
|
"profile": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attach": {
|
||||||
|
"help": "playwright-cli attach <name>\n\nAttach to a running Playwright browser\n\nArguments:\n <name> name or endpoint of the browser to attach to\nOptions:\n --config path to the configuration file, defaults to .playwright/cli.config.json\n --session session name alias (defaults to the attach target name)",
|
||||||
|
"flags": {
|
||||||
|
"config": "string",
|
||||||
|
"session": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"close": {
|
||||||
|
"help": "playwright-cli close \n\nClose the browser\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"goto": {
|
||||||
|
"help": "playwright-cli goto <url>\n\nNavigate to a URL\n\nArguments:\n <url> the url to navigate to",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"help": "playwright-cli type <text>\n\nType text into editable element\n\nArguments:\n <text> text to type into the element\nOptions:\n --submit whether to submit entered text (press enter after)",
|
||||||
|
"flags": {
|
||||||
|
"submit": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"help": "playwright-cli click <target> [button]\n\nPerform click on a web page\n\nArguments:\n <target> exact target element reference from the page snapshot, or a unique element selector\n [button] button to click, defaults to left\nOptions:\n --modifiers modifier keys to press",
|
||||||
|
"flags": {
|
||||||
|
"modifiers": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dblclick": {
|
||||||
|
"help": "playwright-cli dblclick <target> [button]\n\nPerform double click on a web page\n\nArguments:\n <target> exact target element reference from the page snapshot, or a unique element selector\n [button] button to click, defaults to left\nOptions:\n --modifiers modifier keys to press",
|
||||||
|
"flags": {
|
||||||
|
"modifiers": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fill": {
|
||||||
|
"help": "playwright-cli fill <target> <text>\n\nFill text into editable element\n\nArguments:\n <target> exact target element reference from the page snapshot, or a unique element selector\n <text> text to fill into the element\nOptions:\n --submit whether to submit entered text (press enter after)",
|
||||||
|
"flags": {
|
||||||
|
"submit": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"drag": {
|
||||||
|
"help": "playwright-cli drag <startElement> <endElement>\n\nPerform drag and drop between two elements\n\nArguments:\n <startElement> exact source element reference from the page snapshot, or a unique element selector\n <endElement> exact target element reference from the page snapshot, or a unique element selector",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"hover": {
|
||||||
|
"help": "playwright-cli hover <target>\n\nHover over element on page\n\nArguments:\n <target> exact target element reference from the page snapshot, or a unique element selector",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"select": {
|
||||||
|
"help": "playwright-cli select <target> <val>\n\nSelect an option in a dropdown\n\nArguments:\n <target> exact target element reference from the page snapshot, or a unique element selector\n <val> value to select in the dropdown",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"help": "playwright-cli upload <file>\n\nUpload one or multiple files\n\nArguments:\n <file> the absolute paths to the files to upload",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"check": {
|
||||||
|
"help": "playwright-cli check <target>\n\nCheck a checkbox or radio button\n\nArguments:\n <target> exact target element reference from the page snapshot, or a unique element selector",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"uncheck": {
|
||||||
|
"help": "playwright-cli uncheck <target>\n\nUncheck a checkbox or radio button\n\nArguments:\n <target> exact target element reference from the page snapshot, or a unique element selector",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"snapshot": {
|
||||||
|
"help": "playwright-cli snapshot [element]\n\nCapture page snapshot to obtain element ref\n\nArguments:\n [element] element selector of the root element to capture a partial snapshot instead of the whole page\nOptions:\n --filename save snapshot to markdown file instead of returning it in the response.\n --depth limit snapshot depth, unlimited by default.",
|
||||||
|
"flags": {
|
||||||
|
"filename": "string",
|
||||||
|
"depth": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eval": {
|
||||||
|
"help": "playwright-cli eval <func> [element]\n\nEvaluate JavaScript expression on page or element\n\nArguments:\n <func> () => { /* code */ } or (element) => { /* code */ } when element is provided\n [element] exact target element reference from the page snapshot, or a unique element selector\nOptions:\n --filename save evaluation result to a file instead of returning it in the response.",
|
||||||
|
"flags": {
|
||||||
|
"filename": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"console": {
|
||||||
|
"help": "playwright-cli console [min-level]\n\nList console messages\n\nArguments:\n [min-level] level of the console messages to return. each level includes the messages of more severe levels. defaults to \"info\".\nOptions:\n --clear whether to clear the console list",
|
||||||
|
"flags": {
|
||||||
|
"clear": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dialog-accept": {
|
||||||
|
"help": "playwright-cli dialog-accept [prompt]\n\nAccept a dialog\n\nArguments:\n [prompt] the text of the prompt in case of a prompt dialog.",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"dialog-dismiss": {
|
||||||
|
"help": "playwright-cli dialog-dismiss \n\nDismiss a dialog\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"resize": {
|
||||||
|
"help": "playwright-cli resize <w> <h>\n\nResize the browser window\n\nArguments:\n <w> width of the browser window\n <h> height of the browser window",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"run-code": {
|
||||||
|
"help": "playwright-cli run-code [code]\n\nRun Playwright code snippet\n\nArguments:\n [code] a javascript function containing playwright code to execute. it will be invoked with a single argument, page, which you can use for any page interaction.\nOptions:\n --filename load code from the specified file.",
|
||||||
|
"flags": {
|
||||||
|
"filename": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete-data": {
|
||||||
|
"help": "playwright-cli delete-data \n\nDelete session data\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"go-back": {
|
||||||
|
"help": "playwright-cli go-back \n\nGo back to the previous page\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"go-forward": {
|
||||||
|
"help": "playwright-cli go-forward \n\nGo forward to the next page\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"help": "playwright-cli reload \n\nReload the current page\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"press": {
|
||||||
|
"help": "playwright-cli press <key>\n\nPress a key on the keyboard, `a`, `ArrowLeft`\n\nArguments:\n <key> name of the key to press or a character to generate, such as `arrowleft` or `a`",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"keydown": {
|
||||||
|
"help": "playwright-cli keydown <key>\n\nPress a key down on the keyboard\n\nArguments:\n <key> name of the key to press or a character to generate, such as `arrowleft` or `a`",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"keyup": {
|
||||||
|
"help": "playwright-cli keyup <key>\n\nPress a key up on the keyboard\n\nArguments:\n <key> name of the key to press or a character to generate, such as `arrowleft` or `a`",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"mousemove": {
|
||||||
|
"help": "playwright-cli mousemove <x> <y>\n\nMove mouse to a given position\n\nArguments:\n <x> x coordinate\n <y> y coordinate",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"mousedown": {
|
||||||
|
"help": "playwright-cli mousedown [button]\n\nPress mouse down\n\nArguments:\n [button] button to press, defaults to left",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"mouseup": {
|
||||||
|
"help": "playwright-cli mouseup [button]\n\nPress mouse up\n\nArguments:\n [button] button to press, defaults to left",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"mousewheel": {
|
||||||
|
"help": "playwright-cli mousewheel <dx> <dy>\n\nScroll mouse wheel\n\nArguments:\n <dx> x delta\n <dy> y delta",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"screenshot": {
|
||||||
|
"help": "playwright-cli screenshot [target]\n\nscreenshot of the current page or element\n\nArguments:\n [target] exact target element reference from the page snapshot, or a unique element selector.\nOptions:\n --filename file name to save the screenshot to. defaults to `page-{timestamp}.{png|jpeg}` if not specified.\n --full-page when true, takes a screenshot of the full scrollable page, instead of the currently visible viewport.",
|
||||||
|
"flags": {
|
||||||
|
"filename": "string",
|
||||||
|
"full-page": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pdf": {
|
||||||
|
"help": "playwright-cli pdf \n\nSave page as PDF\n\nOptions:\n --filename file name to save the pdf to. defaults to `page-{timestamp}.pdf` if not specified.",
|
||||||
|
"flags": {
|
||||||
|
"filename": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tab-list": {
|
||||||
|
"help": "playwright-cli tab-list \n\nList all tabs\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"tab-new": {
|
||||||
|
"help": "playwright-cli tab-new [url]\n\nCreate a new tab\n\nArguments:\n [url] the url to navigate to in the new tab. if omitted, the new tab will be blank.",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"tab-close": {
|
||||||
|
"help": "playwright-cli tab-close [index]\n\nClose a browser tab\n\nArguments:\n [index] tab index. if omitted, current tab is closed.",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"tab-select": {
|
||||||
|
"help": "playwright-cli tab-select <index>\n\nSelect a browser tab\n\nArguments:\n <index> tab index",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"state-load": {
|
||||||
|
"help": "playwright-cli state-load <filename>\n\nLoads browser storage (authentication) state from a file\n\nArguments:\n <filename> file name to load the storage state from.",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"state-save": {
|
||||||
|
"help": "playwright-cli state-save [filename]\n\nSaves the current storage (authentication) state to a file\n\nArguments:\n [filename] file name to save the storage state to.",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"cookie-list": {
|
||||||
|
"help": "playwright-cli cookie-list \n\nList all cookies (optionally filtered by domain/path)\n\nOptions:\n --domain filter cookies by domain\n --path filter cookies by path",
|
||||||
|
"flags": {
|
||||||
|
"domain": "string",
|
||||||
|
"path": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cookie-get": {
|
||||||
|
"help": "playwright-cli cookie-get <name>\n\nGet a specific cookie by name\n\nArguments:\n <name> cookie name",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"cookie-set": {
|
||||||
|
"help": "playwright-cli cookie-set <name> <value>\n\nSet a cookie with optional flags\n\nArguments:\n <name> cookie name\n <value> cookie value\nOptions:\n --domain cookie domain\n --path cookie path\n --expires cookie expiration as unix timestamp\n --httpOnly whether the cookie is http only\n --secure whether the cookie is secure\n --sameSite cookie samesite attribute",
|
||||||
|
"flags": {
|
||||||
|
"domain": "string",
|
||||||
|
"path": "string",
|
||||||
|
"expires": "string",
|
||||||
|
"httpOnly": "boolean",
|
||||||
|
"secure": "boolean",
|
||||||
|
"sameSite": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cookie-delete": {
|
||||||
|
"help": "playwright-cli cookie-delete <name>\n\nDelete a specific cookie\n\nArguments:\n <name> cookie name",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"cookie-clear": {
|
||||||
|
"help": "playwright-cli cookie-clear \n\nClear all cookies\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"localstorage-list": {
|
||||||
|
"help": "playwright-cli localstorage-list \n\nList all localStorage key-value pairs\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"localstorage-get": {
|
||||||
|
"help": "playwright-cli localstorage-get <key>\n\nGet a localStorage item by key\n\nArguments:\n <key> key to get",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"localstorage-set": {
|
||||||
|
"help": "playwright-cli localstorage-set <key> <value>\n\nSet a localStorage item\n\nArguments:\n <key> key to set\n <value> value to set",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"localstorage-delete": {
|
||||||
|
"help": "playwright-cli localstorage-delete <key>\n\nDelete a localStorage item\n\nArguments:\n <key> key to delete",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"localstorage-clear": {
|
||||||
|
"help": "playwright-cli localstorage-clear \n\nClear all localStorage\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"sessionstorage-list": {
|
||||||
|
"help": "playwright-cli sessionstorage-list \n\nList all sessionStorage key-value pairs\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"sessionstorage-get": {
|
||||||
|
"help": "playwright-cli sessionstorage-get <key>\n\nGet a sessionStorage item by key\n\nArguments:\n <key> key to get",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"sessionstorage-set": {
|
||||||
|
"help": "playwright-cli sessionstorage-set <key> <value>\n\nSet a sessionStorage item\n\nArguments:\n <key> key to set\n <value> value to set",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"sessionstorage-delete": {
|
||||||
|
"help": "playwright-cli sessionstorage-delete <key>\n\nDelete a sessionStorage item\n\nArguments:\n <key> key to delete",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"sessionstorage-clear": {
|
||||||
|
"help": "playwright-cli sessionstorage-clear \n\nClear all sessionStorage\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"help": "playwright-cli route <pattern>\n\nMock network requests matching a URL pattern\n\nArguments:\n <pattern> url pattern to match (e.g., \"**/api/users\")\nOptions:\n --status http status code (default: 200)\n --body response body (text or json string)\n --content-type content-type header\n --header header to add in \"name: value\" format (repeatable)\n --remove-header comma-separated header names to remove",
|
||||||
|
"flags": {
|
||||||
|
"status": "string",
|
||||||
|
"body": "string",
|
||||||
|
"content-type": "string",
|
||||||
|
"header": "string",
|
||||||
|
"remove-header": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"route-list": {
|
||||||
|
"help": "playwright-cli route-list \n\nList all active network routes\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"unroute": {
|
||||||
|
"help": "playwright-cli unroute [pattern]\n\nRemove routes matching a pattern (or all routes)\n\nArguments:\n [pattern] url pattern to unroute (omit to remove all)",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"network-state-set": {
|
||||||
|
"help": "playwright-cli network-state-set <state>\n\nSet the browser network state to online or offline\n\nArguments:\n <state> set to \"offline\" to simulate offline mode, \"online\" to restore network connectivity",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"config-print": {
|
||||||
|
"help": "playwright-cli config-print \n\nPrint the final resolved config after merging CLI options, environment variables and config file.\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"help": "playwright-cli install \n\nInitialize workspace\n\nOptions:\n --skills install skills to \".claude\" (default) or \".agents\" dir",
|
||||||
|
"flags": {
|
||||||
|
"skills": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"install-browser": {
|
||||||
|
"help": "playwright-cli install-browser [browser]\n\nInstall browser\n\nArguments:\n [browser] browser to install\nOptions:\n --with-deps install system dependencies for browsers\n --dry-run do not execute installation, only print information\n --list prints list of browsers from all playwright installations\n --force force reinstall of already installed browsers\n --only-shell only install headless shell when installing chromium\n --no-shell do not install chromium headless shell",
|
||||||
|
"flags": {
|
||||||
|
"with-deps": "boolean",
|
||||||
|
"dry-run": "boolean",
|
||||||
|
"list": "boolean",
|
||||||
|
"force": "boolean",
|
||||||
|
"only-shell": "boolean",
|
||||||
|
"no-shell": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"help": "playwright-cli network \n\nList all network requests since loading the page\n\nOptions:\n --static whether to include successful static resources like images, fonts, scripts, etc. defaults to false.\n --request-body whether to include request body. defaults to false.\n --request-headers whether to include request headers. defaults to false.\n --filter only return requests whose url matches this regexp (e.g. \"/api/.*user\").\n --clear whether to clear the network list",
|
||||||
|
"flags": {
|
||||||
|
"static": "boolean",
|
||||||
|
"request-body": "boolean",
|
||||||
|
"request-headers": "boolean",
|
||||||
|
"filter": "string",
|
||||||
|
"clear": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tracing-start": {
|
||||||
|
"help": "playwright-cli tracing-start \n\nStart trace recording\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"tracing-stop": {
|
||||||
|
"help": "playwright-cli tracing-stop \n\nStop trace recording\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"video-start": {
|
||||||
|
"help": "playwright-cli video-start [filename]\n\nStart video recording\n\nArguments:\n [filename] filename to save the video.\nOptions:\n --size video frame size, e.g. \"800x600\". if not specified, the size of the recorded video will fit 800x800.",
|
||||||
|
"flags": {
|
||||||
|
"size": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"video-stop": {
|
||||||
|
"help": "playwright-cli video-stop \n\nStop video recording\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"video-chapter": {
|
||||||
|
"help": "playwright-cli video-chapter <title>\n\nAdd a chapter marker to the video recording\n\nArguments:\n <title> chapter title.\nOptions:\n --description chapter description.\n --duration duration in milliseconds to show the chapter card.",
|
||||||
|
"flags": {
|
||||||
|
"description": "string",
|
||||||
|
"duration": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"help": "playwright-cli show \n\nShow browser DevTools\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"pause-at": {
|
||||||
|
"help": "playwright-cli pause-at <location>\n\nRun the test up to a specific location and pause there\n\nArguments:\n <location> location to pause at. format is <file>:<line>, e.g. \"example.spec.ts:42\".",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"resume": {
|
||||||
|
"help": "playwright-cli resume \n\nResume the test execution\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"step-over": {
|
||||||
|
"help": "playwright-cli step-over \n\nStep over the next call in the test\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"help": "playwright-cli list \n\nList browser sessions\n\nOptions:\n --all list all browser sessions across all workspaces",
|
||||||
|
"flags": {
|
||||||
|
"all": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"close-all": {
|
||||||
|
"help": "playwright-cli close-all \n\nClose all browser sessions\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"kill-all": {
|
||||||
|
"help": "playwright-cli kill-all \n\nForcefully kill all browser sessions (for stale/zombie processes)\n",
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
"tray": {
|
||||||
|
"help": "playwright-cli tray \n\nRun tray\n",
|
||||||
|
"flags": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"booleanOptions": [
|
||||||
|
"extension",
|
||||||
|
"headed",
|
||||||
|
"persistent",
|
||||||
|
"submit",
|
||||||
|
"clear",
|
||||||
|
"full-page",
|
||||||
|
"httpOnly",
|
||||||
|
"secure",
|
||||||
|
"with-deps",
|
||||||
|
"dry-run",
|
||||||
|
"list",
|
||||||
|
"force",
|
||||||
|
"only-shell",
|
||||||
|
"no-shell",
|
||||||
|
"static",
|
||||||
|
"request-body",
|
||||||
|
"request-headers",
|
||||||
|
"all"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var minimist_exports = {};
|
||||||
|
__export(minimist_exports, {
|
||||||
|
minimist: () => minimist
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(minimist_exports);
|
||||||
|
function minimist(args, opts) {
|
||||||
|
if (!opts)
|
||||||
|
opts = {};
|
||||||
|
const bools = {};
|
||||||
|
const strings = {};
|
||||||
|
for (const key of toArray(opts.boolean))
|
||||||
|
bools[key] = true;
|
||||||
|
for (const key of toArray(opts.string))
|
||||||
|
strings[key] = true;
|
||||||
|
const argv = { _: [] };
|
||||||
|
function setArg(key, val) {
|
||||||
|
if (argv[key] === void 0 || bools[key] || typeof argv[key] === "boolean")
|
||||||
|
argv[key] = val;
|
||||||
|
else if (Array.isArray(argv[key]))
|
||||||
|
argv[key].push(val);
|
||||||
|
else
|
||||||
|
argv[key] = [argv[key], val];
|
||||||
|
}
|
||||||
|
let notFlags = [];
|
||||||
|
const doubleDashIndex = args.indexOf("--");
|
||||||
|
if (doubleDashIndex !== -1) {
|
||||||
|
notFlags = args.slice(doubleDashIndex + 1);
|
||||||
|
args = args.slice(0, doubleDashIndex);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
let key;
|
||||||
|
let next;
|
||||||
|
if (/^--.+=/.test(arg)) {
|
||||||
|
const m = arg.match(/^--([^=]+)=([\s\S]*)$/);
|
||||||
|
key = m[1];
|
||||||
|
if (bools[key])
|
||||||
|
throw new Error(`boolean option '--${key}' should not be passed with '=value', use '--${key}' or '--no-${key}' instead`);
|
||||||
|
setArg(key, m[2]);
|
||||||
|
} else if (/^--no-.+/.test(arg)) {
|
||||||
|
key = arg.match(/^--no-(.+)/)[1];
|
||||||
|
setArg(key, false);
|
||||||
|
} else if (/^--.+/.test(arg)) {
|
||||||
|
key = arg.match(/^--(.+)/)[1];
|
||||||
|
next = args[i + 1];
|
||||||
|
if (next !== void 0 && !/^(-|--)[^-]/.test(next) && !bools[key]) {
|
||||||
|
setArg(key, next);
|
||||||
|
i += 1;
|
||||||
|
} else if (/^(true|false)$/.test(next)) {
|
||||||
|
setArg(key, next === "true");
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
setArg(key, strings[key] ? "" : true);
|
||||||
|
}
|
||||||
|
} else if (/^-[^-]+/.test(arg)) {
|
||||||
|
const letters = arg.slice(1, -1).split("");
|
||||||
|
let broken = false;
|
||||||
|
for (let j = 0; j < letters.length; j++) {
|
||||||
|
next = arg.slice(j + 2);
|
||||||
|
if (next === "-") {
|
||||||
|
setArg(letters[j], next);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/[A-Za-z]/.test(letters[j]) && next[0] === "=") {
|
||||||
|
setArg(letters[j], next.slice(1));
|
||||||
|
broken = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (/[A-Za-z]/.test(letters[j]) && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
|
||||||
|
setArg(letters[j], next);
|
||||||
|
broken = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
|
||||||
|
setArg(letters[j], arg.slice(j + 2));
|
||||||
|
broken = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
setArg(letters[j], strings[letters[j]] ? "" : true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key = arg.slice(-1)[0];
|
||||||
|
if (!broken && key !== "-") {
|
||||||
|
if (args[i + 1] && !/^(-|--)[^-]/.test(args[i + 1]) && !bools[key]) {
|
||||||
|
setArg(key, args[i + 1]);
|
||||||
|
i += 1;
|
||||||
|
} else if (args[i + 1] && /^(true|false)$/.test(args[i + 1])) {
|
||||||
|
setArg(key, args[i + 1] === "true");
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
setArg(key, strings[key] ? "" : true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
argv._.push(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const k of notFlags)
|
||||||
|
argv._.push(k);
|
||||||
|
return argv;
|
||||||
|
}
|
||||||
|
function toArray(value) {
|
||||||
|
if (!value)
|
||||||
|
return [];
|
||||||
|
return Array.isArray(value) ? value : [value];
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
minimist
|
||||||
|
});
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var program_exports = {};
|
||||||
|
__export(program_exports, {
|
||||||
|
calculateSha1: () => calculateSha1,
|
||||||
|
program: () => program
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(program_exports);
|
||||||
|
var import_child_process = require("child_process");
|
||||||
|
var import_crypto = __toESM(require("crypto"));
|
||||||
|
var import_os = __toESM(require("os"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_registry = require("./registry");
|
||||||
|
var import_session = require("./session");
|
||||||
|
var import_serverRegistry = require("../../serverRegistry");
|
||||||
|
var import_minimist = require("./minimist");
|
||||||
|
const globalOptions = [
|
||||||
|
"endpoint",
|
||||||
|
"browser",
|
||||||
|
"config",
|
||||||
|
"extension",
|
||||||
|
"headed",
|
||||||
|
"help",
|
||||||
|
"persistent",
|
||||||
|
"profile",
|
||||||
|
"session",
|
||||||
|
"version"
|
||||||
|
];
|
||||||
|
const booleanOptions = [
|
||||||
|
"all",
|
||||||
|
"help",
|
||||||
|
"version"
|
||||||
|
];
|
||||||
|
async function program(options) {
|
||||||
|
const clientInfo = (0, import_registry.createClientInfo)();
|
||||||
|
const help = require("./help.json");
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
const boolean = [...help.booleanOptions, ...booleanOptions];
|
||||||
|
const args = (0, import_minimist.minimist)(argv, { boolean, string: ["_"] });
|
||||||
|
if (args.s) {
|
||||||
|
args.session = args.s;
|
||||||
|
delete args.s;
|
||||||
|
}
|
||||||
|
const commandName = args._?.[0];
|
||||||
|
if (args.version || args.v) {
|
||||||
|
console.log(options?.embedderVersion ?? clientInfo.version);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const command = commandName && help.commands[commandName];
|
||||||
|
if (args.help || args.h) {
|
||||||
|
if (command) {
|
||||||
|
console.log(command.help);
|
||||||
|
} else {
|
||||||
|
console.log("playwright-cli - run playwright mcp commands from terminal\n");
|
||||||
|
console.log(help.global);
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
if (!command) {
|
||||||
|
console.error(`Unknown command: ${commandName}
|
||||||
|
`);
|
||||||
|
console.log(help.global);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
validateFlags(args, command);
|
||||||
|
const registry = await import_registry.Registry.load();
|
||||||
|
const sessionName = (0, import_registry.resolveSessionName)(args.session);
|
||||||
|
switch (commandName) {
|
||||||
|
case "list": {
|
||||||
|
await listSessions(registry, clientInfo, !!args.all);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "close-all": {
|
||||||
|
const entries = registry.entries(clientInfo);
|
||||||
|
for (const entry of entries)
|
||||||
|
await new import_session.Session(entry).stop(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "delete-data": {
|
||||||
|
const entry = registry.entry(clientInfo, sessionName);
|
||||||
|
if (!entry) {
|
||||||
|
console.log(`No user data found for browser '${sessionName}'.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new import_session.Session(entry).deleteData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "kill-all": {
|
||||||
|
await killAllDaemons();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "open": {
|
||||||
|
await startSession(sessionName, registry, clientInfo, args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "attach": {
|
||||||
|
const attachTarget = args._[1];
|
||||||
|
const attachSessionName = (0, import_registry.explicitSessionName)(args.session) ?? attachTarget;
|
||||||
|
args.endpoint = attachTarget;
|
||||||
|
args.session = attachSessionName;
|
||||||
|
await startSession(attachSessionName, registry, clientInfo, args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "close":
|
||||||
|
const closeEntry = registry.entry(clientInfo, sessionName);
|
||||||
|
const session = closeEntry ? new import_session.Session(closeEntry) : void 0;
|
||||||
|
if (!session || !await session.canConnect()) {
|
||||||
|
console.log(`Browser '${sessionName}' is not open.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await session.stop();
|
||||||
|
return;
|
||||||
|
case "install":
|
||||||
|
await runInitWorkspace(args);
|
||||||
|
return;
|
||||||
|
case "install-browser":
|
||||||
|
await installBrowser();
|
||||||
|
return;
|
||||||
|
case "show": {
|
||||||
|
const daemonScript = require.resolve("../dashboard/dashboardApp.js");
|
||||||
|
const child = (0, import_child_process.spawn)(process.execPath, [daemonScript], {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore"
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const entry = registry.entry(clientInfo, sessionName);
|
||||||
|
if (!entry) {
|
||||||
|
console.log(`The browser '${sessionName}' is not open, please run open first`);
|
||||||
|
console.log("");
|
||||||
|
console.log(` playwright-cli${sessionName !== "default" ? ` -s=${sessionName}` : ""} open [params]`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await runInSession(entry, clientInfo, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function startSession(sessionName, registry, clientInfo, args) {
|
||||||
|
const entry = registry.entry(clientInfo, sessionName);
|
||||||
|
if (entry)
|
||||||
|
await new import_session.Session(entry).stop(true);
|
||||||
|
await import_session.Session.startDaemon(clientInfo, args);
|
||||||
|
const newEntry = await registry.loadEntry(clientInfo, sessionName);
|
||||||
|
await runInSession(newEntry, clientInfo, args);
|
||||||
|
}
|
||||||
|
async function runInSession(entry, clientInfo, args) {
|
||||||
|
for (const globalOption of globalOptions)
|
||||||
|
delete args[globalOption];
|
||||||
|
const session = new import_session.Session(entry);
|
||||||
|
const result = await session.run(clientInfo, args);
|
||||||
|
console.log(result.text);
|
||||||
|
}
|
||||||
|
async function runInitWorkspace(args) {
|
||||||
|
const cliPath = require.resolve("../cli-daemon/program.js");
|
||||||
|
const daemonArgs = [cliPath, "--init-workspace", ...args.skills ? ["--init-skills", String(args.skills)] : []];
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const child = (0, import_child_process.spawn)(process.execPath, daemonArgs, {
|
||||||
|
stdio: "inherit",
|
||||||
|
cwd: process.cwd()
|
||||||
|
});
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code === 0)
|
||||||
|
resolve();
|
||||||
|
else
|
||||||
|
reject(new Error(`Workspace initialization failed with exit code ${code}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function installBrowser() {
|
||||||
|
const { program: program2 } = require("../../cli/program");
|
||||||
|
const argv = process.argv.map((arg) => arg === "install-browser" ? "install" : arg);
|
||||||
|
program2.parse(argv);
|
||||||
|
}
|
||||||
|
async function killAllDaemons() {
|
||||||
|
const platform = import_os.default.platform();
|
||||||
|
let killed = 0;
|
||||||
|
try {
|
||||||
|
if (platform === "win32") {
|
||||||
|
const result = (0, import_child_process.execSync)(
|
||||||
|
`powershell -NoProfile -NonInteractive -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*run-mcp-server*' -or $_.CommandLine -like '*run-cli-server*' -or $_.CommandLine -like '*cli-daemon*' -or $_.CommandLine -like '*dashboardApp.js*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; $_.ProcessId }"`,
|
||||||
|
{ encoding: "utf-8" }
|
||||||
|
);
|
||||||
|
const pids = result.split("\n").map((line) => line.trim()).filter((line) => /^\d+$/.test(line));
|
||||||
|
for (const pid of pids)
|
||||||
|
console.log(`Killed daemon process ${pid}`);
|
||||||
|
killed = pids.length;
|
||||||
|
} else {
|
||||||
|
const result = (0, import_child_process.execSync)("ps aux", { encoding: "utf-8" });
|
||||||
|
const lines = result.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes("run-mcp-server") || line.includes("run-cli-server") || line.includes("cli-daemon") || line.includes("dashboardApp.js")) {
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
const pid = parts[1];
|
||||||
|
if (pid && /^\d+$/.test(pid)) {
|
||||||
|
try {
|
||||||
|
process.kill(parseInt(pid, 10), "SIGKILL");
|
||||||
|
console.log(`Killed daemon process ${pid}`);
|
||||||
|
killed++;
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
if (killed === 0)
|
||||||
|
console.log("No daemon processes found.");
|
||||||
|
else if (killed > 0)
|
||||||
|
console.log(`Killed ${killed} daemon process${killed === 1 ? "" : "es"}.`);
|
||||||
|
}
|
||||||
|
async function listSessions(registry, clientInfo, all) {
|
||||||
|
console.log("### Browsers");
|
||||||
|
let count = 0;
|
||||||
|
const runningSessions = /* @__PURE__ */ new Set();
|
||||||
|
const entries = registry.entryMap();
|
||||||
|
for (const [workspace, list] of entries) {
|
||||||
|
if (!all && workspace !== clientInfo.workspaceDir)
|
||||||
|
continue;
|
||||||
|
count += await gcAndPrintSessions(clientInfo, list.map((entry) => new import_session.Session(entry)), all ? `${import_path.default.relative(process.cwd(), workspace) || "/"}:` : void 0, runningSessions);
|
||||||
|
}
|
||||||
|
const serverEntries = await import_serverRegistry.serverRegistry.list();
|
||||||
|
const filteredServerEntries = /* @__PURE__ */ new Map();
|
||||||
|
for (const [workspace, list] of serverEntries) {
|
||||||
|
if (!all && workspace !== clientInfo.workspaceDir)
|
||||||
|
continue;
|
||||||
|
const unattached = list.filter((d) => !runningSessions.has(d.title));
|
||||||
|
if (unattached.length)
|
||||||
|
filteredServerEntries.set(workspace, unattached);
|
||||||
|
}
|
||||||
|
if (filteredServerEntries.size) {
|
||||||
|
if (count)
|
||||||
|
console.log("");
|
||||||
|
console.log("### Browser servers available for attach");
|
||||||
|
}
|
||||||
|
for (const [workspace, list] of filteredServerEntries)
|
||||||
|
count += await gcAndPrintBrowserSessions(workspace, list);
|
||||||
|
if (!count)
|
||||||
|
console.log(" (no browsers)");
|
||||||
|
}
|
||||||
|
async function gcAndPrintSessions(clientInfo, sessions, header, runningSessions) {
|
||||||
|
const running = [];
|
||||||
|
const stopped = [];
|
||||||
|
for (const session of sessions) {
|
||||||
|
const canConnect = await session.canConnect();
|
||||||
|
if (canConnect) {
|
||||||
|
running.push(session);
|
||||||
|
runningSessions?.add(session.name);
|
||||||
|
} else {
|
||||||
|
if (session.config.cli.persistent)
|
||||||
|
stopped.push(session);
|
||||||
|
else
|
||||||
|
await session.deleteSessionConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (header && (running.length || stopped.length))
|
||||||
|
console.log(header);
|
||||||
|
for (const session of running)
|
||||||
|
console.log(await renderSessionStatus(clientInfo, session));
|
||||||
|
for (const session of stopped)
|
||||||
|
console.log(await renderSessionStatus(clientInfo, session));
|
||||||
|
return running.length + stopped.length;
|
||||||
|
}
|
||||||
|
async function gcAndPrintBrowserSessions(workspace, list) {
|
||||||
|
if (!list.length)
|
||||||
|
return 0;
|
||||||
|
if (workspace)
|
||||||
|
console.log(`${import_path.default.relative(process.cwd(), workspace) || "/"}:`);
|
||||||
|
for (const descriptor of list) {
|
||||||
|
const text = [];
|
||||||
|
text.push(`- browser "${descriptor.title}":`);
|
||||||
|
text.push(` - browser: ${descriptor.browser.browserName}`);
|
||||||
|
text.push(` - version: v${descriptor.playwrightVersion}`);
|
||||||
|
text.push(` - status: ${descriptor.canConnect ? "open" : "closed"}`);
|
||||||
|
if (descriptor.browser.userDataDir)
|
||||||
|
text.push(` - data-dir: ${descriptor.browser.userDataDir}`);
|
||||||
|
else
|
||||||
|
text.push(` - data-dir: <in-memory>`);
|
||||||
|
text.push(` - run \`playwright-cli attach "${descriptor.title}"\` to attach`);
|
||||||
|
console.log(text.join("\n"));
|
||||||
|
}
|
||||||
|
return list.length;
|
||||||
|
}
|
||||||
|
async function renderSessionStatus(clientInfo, session) {
|
||||||
|
const text = [];
|
||||||
|
const config = session.config;
|
||||||
|
const canConnect = await session.canConnect();
|
||||||
|
text.push(`- ${session.name}:`);
|
||||||
|
text.push(` - status: ${canConnect ? "open" : "closed"}`);
|
||||||
|
if (canConnect && !session.isCompatible(clientInfo))
|
||||||
|
text.push(` - version: v${config.version} [incompatible please re-open]`);
|
||||||
|
if (config.browser)
|
||||||
|
text.push(...(0, import_session.renderResolvedConfig)(config));
|
||||||
|
return text.join("\n");
|
||||||
|
}
|
||||||
|
function validateFlags(args, command) {
|
||||||
|
const unknownFlags = [];
|
||||||
|
for (const key of Object.keys(args)) {
|
||||||
|
if (key === "_")
|
||||||
|
continue;
|
||||||
|
if (globalOptions.includes(key))
|
||||||
|
continue;
|
||||||
|
if (!(key in command.flags))
|
||||||
|
unknownFlags.push(key);
|
||||||
|
}
|
||||||
|
if (unknownFlags.length) {
|
||||||
|
console.error(`Unknown option${unknownFlags.length > 1 ? "s" : ""}: ${unknownFlags.map((f) => `--${f}`).join(", ")}`);
|
||||||
|
console.log("");
|
||||||
|
console.log(command.help);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function calculateSha1(buffer) {
|
||||||
|
const hash = import_crypto.default.createHash("sha1");
|
||||||
|
hash.update(buffer);
|
||||||
|
return hash.digest("hex");
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
calculateSha1,
|
||||||
|
program
|
||||||
|
});
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var registry_exports = {};
|
||||||
|
__export(registry_exports, {
|
||||||
|
Registry: () => Registry,
|
||||||
|
baseDaemonDir: () => baseDaemonDir,
|
||||||
|
createClientInfo: () => createClientInfo,
|
||||||
|
explicitSessionName: () => explicitSessionName,
|
||||||
|
resolveSessionName: () => resolveSessionName
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(registry_exports);
|
||||||
|
var import_crypto = __toESM(require("crypto"));
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_os = __toESM(require("os"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
class Registry {
|
||||||
|
constructor(files) {
|
||||||
|
this._files = files;
|
||||||
|
}
|
||||||
|
entry(clientInfo, sessionName) {
|
||||||
|
const key = clientInfo.workspaceDir || clientInfo.workspaceDirHash;
|
||||||
|
const entries = this._files.get(key) || [];
|
||||||
|
return entries.find((entry) => entry.config.name === sessionName);
|
||||||
|
}
|
||||||
|
entries(clientInfo) {
|
||||||
|
const key = clientInfo.workspaceDir || clientInfo.workspaceDirHash;
|
||||||
|
return this._files.get(key) || [];
|
||||||
|
}
|
||||||
|
entryMap() {
|
||||||
|
return this._files;
|
||||||
|
}
|
||||||
|
async loadEntry(clientInfo, sessionName) {
|
||||||
|
const entry = await Registry._loadSessionEntry(clientInfo.daemonProfilesDir, sessionName + ".session");
|
||||||
|
if (!entry)
|
||||||
|
throw new Error(`Could not start the session "${sessionName}"`);
|
||||||
|
const key = clientInfo.workspaceDir || clientInfo.workspaceDirHash;
|
||||||
|
let list = this._files.get(key);
|
||||||
|
if (!list) {
|
||||||
|
list = [];
|
||||||
|
this._files.set(key, list);
|
||||||
|
}
|
||||||
|
const oldIndex = list.findIndex((e) => e.config.name === sessionName);
|
||||||
|
if (oldIndex !== -1)
|
||||||
|
list.splice(oldIndex, 1);
|
||||||
|
list.push(entry);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
static async _loadSessionEntry(daemonDir, file) {
|
||||||
|
try {
|
||||||
|
const fileName = import_path.default.join(daemonDir, file);
|
||||||
|
const data = await import_fs.default.promises.readFile(fileName, "utf-8");
|
||||||
|
const config = JSON.parse(data);
|
||||||
|
if (!config.name)
|
||||||
|
config.name = import_path.default.basename(file, ".session");
|
||||||
|
if (!config.timestamp)
|
||||||
|
config.timestamp = 0;
|
||||||
|
return { file: fileName, config, daemonDir };
|
||||||
|
} catch {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async load() {
|
||||||
|
const sessions = /* @__PURE__ */ new Map();
|
||||||
|
const hashDirs = await import_fs.default.promises.readdir(baseDaemonDir).catch(() => []);
|
||||||
|
for (const workspaceDirHash of hashDirs) {
|
||||||
|
const daemonDir = import_path.default.join(baseDaemonDir, workspaceDirHash);
|
||||||
|
const stat = await import_fs.default.promises.stat(daemonDir);
|
||||||
|
if (!stat.isDirectory())
|
||||||
|
continue;
|
||||||
|
const files = await import_fs.default.promises.readdir(daemonDir).catch(() => []);
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.endsWith(".session"))
|
||||||
|
continue;
|
||||||
|
const entry = await Registry._loadSessionEntry(daemonDir, file);
|
||||||
|
if (!entry)
|
||||||
|
continue;
|
||||||
|
const key = entry.config.workspaceDir || workspaceDirHash;
|
||||||
|
let list = sessions.get(key);
|
||||||
|
if (!list) {
|
||||||
|
list = [];
|
||||||
|
sessions.set(key, list);
|
||||||
|
}
|
||||||
|
list.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Registry(sessions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const baseDaemonDir = (() => {
|
||||||
|
if (process.env.PLAYWRIGHT_DAEMON_SESSION_DIR)
|
||||||
|
return process.env.PLAYWRIGHT_DAEMON_SESSION_DIR;
|
||||||
|
let localCacheDir;
|
||||||
|
if (process.platform === "linux")
|
||||||
|
localCacheDir = process.env.XDG_CACHE_HOME || import_path.default.join(import_os.default.homedir(), ".cache");
|
||||||
|
if (process.platform === "darwin")
|
||||||
|
localCacheDir = import_path.default.join(import_os.default.homedir(), "Library", "Caches");
|
||||||
|
if (process.platform === "win32")
|
||||||
|
localCacheDir = process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local");
|
||||||
|
if (!localCacheDir)
|
||||||
|
throw new Error("Unsupported platform: " + process.platform);
|
||||||
|
return import_path.default.join(localCacheDir, "ms-playwright", "daemon");
|
||||||
|
})();
|
||||||
|
function createClientInfo() {
|
||||||
|
const packageLocation = require.resolve("../../../package.json");
|
||||||
|
const packageJSON = require(packageLocation);
|
||||||
|
const workspaceDir = findWorkspaceDir(process.cwd());
|
||||||
|
const version = process.env.PLAYWRIGHT_CLI_VERSION_FOR_TEST || packageJSON.version;
|
||||||
|
const hash = import_crypto.default.createHash("sha1");
|
||||||
|
hash.update(workspaceDir || packageLocation);
|
||||||
|
const workspaceDirHash = hash.digest("hex").substring(0, 16);
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
workspaceDir,
|
||||||
|
workspaceDirHash,
|
||||||
|
daemonProfilesDir: daemonProfilesDir(workspaceDirHash)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function findWorkspaceDir(startDir) {
|
||||||
|
let dir = startDir;
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
if (import_fs.default.existsSync(import_path.default.join(dir, ".playwright")))
|
||||||
|
return dir;
|
||||||
|
const parentDir = import_path.default.dirname(dir);
|
||||||
|
if (parentDir === dir)
|
||||||
|
break;
|
||||||
|
dir = parentDir;
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
const daemonProfilesDir = (workspaceDirHash) => {
|
||||||
|
return import_path.default.join(baseDaemonDir, workspaceDirHash);
|
||||||
|
};
|
||||||
|
function explicitSessionName(sessionName) {
|
||||||
|
return sessionName || process.env.PLAYWRIGHT_CLI_SESSION;
|
||||||
|
}
|
||||||
|
function resolveSessionName(sessionName) {
|
||||||
|
if (sessionName)
|
||||||
|
return sessionName;
|
||||||
|
if (process.env.PLAYWRIGHT_CLI_SESSION)
|
||||||
|
return process.env.PLAYWRIGHT_CLI_SESSION;
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
Registry,
|
||||||
|
baseDaemonDir,
|
||||||
|
createClientInfo,
|
||||||
|
explicitSessionName,
|
||||||
|
resolveSessionName
|
||||||
|
});
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var session_exports = {};
|
||||||
|
__export(session_exports, {
|
||||||
|
Session: () => Session,
|
||||||
|
renderResolvedConfig: () => renderResolvedConfig
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(session_exports);
|
||||||
|
var import_child_process = require("child_process");
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_net = __toESM(require("net"));
|
||||||
|
var import_os = __toESM(require("os"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_socketConnection = require("../utils/socketConnection");
|
||||||
|
var import_registry = require("./registry");
|
||||||
|
class Session {
|
||||||
|
constructor(sessionFile) {
|
||||||
|
this.config = sessionFile.config;
|
||||||
|
this.name = this.config.name;
|
||||||
|
this._sessionFile = sessionFile;
|
||||||
|
}
|
||||||
|
isCompatible(clientInfo) {
|
||||||
|
return (0, import_socketConnection.compareSemver)(clientInfo.version, this.config.version) >= 0;
|
||||||
|
}
|
||||||
|
async run(clientInfo, args) {
|
||||||
|
if (!this.isCompatible(clientInfo))
|
||||||
|
throw new Error(`Client is v${clientInfo.version}, session '${this.name}' is v${this.config.version}. Run
|
||||||
|
|
||||||
|
playwright-cli${this.name !== "default" ? ` -s=${this.name}` : ""} open
|
||||||
|
|
||||||
|
to restart the browser session.`);
|
||||||
|
const { socket } = await this._connect();
|
||||||
|
if (!socket)
|
||||||
|
throw new Error(`Browser '${this.name}' is not open. Run
|
||||||
|
|
||||||
|
playwright-cli${this.name !== "default" ? ` -s=${this.name}` : ""} open
|
||||||
|
|
||||||
|
to start the browser session.`);
|
||||||
|
return await SocketConnectionClient.sendAndClose(socket, "run", { args, cwd: process.cwd() });
|
||||||
|
}
|
||||||
|
async stop(quiet = false) {
|
||||||
|
if (!await this.canConnect()) {
|
||||||
|
if (!quiet)
|
||||||
|
console.log(`Browser '${this.name}' is not open.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this._stopDaemon();
|
||||||
|
if (!quiet)
|
||||||
|
console.log(`Browser '${this.name}' closed
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
async deleteData() {
|
||||||
|
await this.stop();
|
||||||
|
const dataDirs = await import_fs.default.promises.readdir(this._sessionFile.daemonDir).catch(() => []);
|
||||||
|
const matchingEntries = dataDirs.filter((file) => file === `${this.name}.session` || file.startsWith(`ud-${this.name}-`));
|
||||||
|
if (matchingEntries.length === 0) {
|
||||||
|
console.log(`No user data found for browser '${this.name}'.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of matchingEntries) {
|
||||||
|
const userDataDir = import_path.default.resolve(this._sessionFile.daemonDir, entry);
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
try {
|
||||||
|
await import_fs.default.promises.rm(userDataDir, { recursive: true });
|
||||||
|
if (entry.startsWith("ud-"))
|
||||||
|
console.log(`Deleted user data for browser '${this.name}'.`);
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === "ENOENT") {
|
||||||
|
console.log(`No user data found for browser '${this.name}'.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
||||||
|
if (i === 4)
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _connect() {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
const socket = import_net.default.createConnection(this.config.socketPath, () => {
|
||||||
|
resolve({ socket });
|
||||||
|
});
|
||||||
|
socket.on("error", (error) => {
|
||||||
|
if (import_os.default.platform() !== "win32")
|
||||||
|
void import_fs.default.promises.unlink(this.config.socketPath).catch(() => {
|
||||||
|
}).then(() => resolve({ error }));
|
||||||
|
else
|
||||||
|
resolve({ error });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async canConnect() {
|
||||||
|
const { socket } = await this._connect();
|
||||||
|
if (socket) {
|
||||||
|
socket.destroy();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static async startDaemon(clientInfo, cliArgs) {
|
||||||
|
await import_fs.default.promises.mkdir(clientInfo.daemonProfilesDir, { recursive: true });
|
||||||
|
const cliPath = require.resolve("../cli-daemon/program.js");
|
||||||
|
const sessionName = (0, import_registry.resolveSessionName)(cliArgs.session);
|
||||||
|
const errLog = import_path.default.join(clientInfo.daemonProfilesDir, sessionName + ".err");
|
||||||
|
const err = import_fs.default.openSync(errLog, "w");
|
||||||
|
const args = [
|
||||||
|
cliPath,
|
||||||
|
sessionName
|
||||||
|
];
|
||||||
|
if (cliArgs.headed)
|
||||||
|
args.push("--headed");
|
||||||
|
if (cliArgs.extension)
|
||||||
|
args.push("--extension");
|
||||||
|
if (cliArgs.browser)
|
||||||
|
args.push(`--browser=${cliArgs.browser}`);
|
||||||
|
if (cliArgs.persistent)
|
||||||
|
args.push("--persistent");
|
||||||
|
if (cliArgs.profile)
|
||||||
|
args.push(`--profile=${cliArgs.profile}`);
|
||||||
|
if (cliArgs.config)
|
||||||
|
args.push(`--config=${cliArgs.config}`);
|
||||||
|
if (cliArgs.endpoint || process.env.PLAYWRIGHT_CLI_SESSION)
|
||||||
|
args.push(`--endpoint=${cliArgs.endpoint || process.env.PLAYWRIGHT_CLI_SESSION}`);
|
||||||
|
const child = (0, import_child_process.spawn)(process.execPath, args, {
|
||||||
|
detached: true,
|
||||||
|
stdio: ["ignore", "pipe", err],
|
||||||
|
cwd: process.cwd()
|
||||||
|
// Will be used as root.
|
||||||
|
});
|
||||||
|
let signalled = false;
|
||||||
|
const sigintHandler = () => {
|
||||||
|
signalled = true;
|
||||||
|
child.kill("SIGINT");
|
||||||
|
};
|
||||||
|
const sigtermHandler = () => {
|
||||||
|
signalled = true;
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
};
|
||||||
|
process.on("SIGINT", sigintHandler);
|
||||||
|
process.on("SIGTERM", sigtermHandler);
|
||||||
|
let outLog = "";
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
child.stdout.on("data", (data) => {
|
||||||
|
outLog += data.toString();
|
||||||
|
if (!outLog.includes("<EOF>"))
|
||||||
|
return;
|
||||||
|
const errorMatch = outLog.match(/### Error\n([\s\S]*)<EOF>/);
|
||||||
|
const error = errorMatch ? errorMatch[1].trim() : void 0;
|
||||||
|
if (error) {
|
||||||
|
const errLogContent = import_fs.default.readFileSync(errLog, "utf-8");
|
||||||
|
const message = error + (errLogContent ? "\n" + errLogContent : "");
|
||||||
|
reject(new Error(message));
|
||||||
|
}
|
||||||
|
const successMatch = outLog.match(/### Success\nDaemon listening on (.*)\n<EOF>/);
|
||||||
|
if (successMatch)
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (!signalled) {
|
||||||
|
const errLogContent = import_fs.default.readFileSync(errLog, "utf-8");
|
||||||
|
const message = `Daemon process exited with code ${code}` + (errLogContent ? "\n" + errLogContent : "");
|
||||||
|
reject(new Error(message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
process.off("SIGINT", sigintHandler);
|
||||||
|
process.off("SIGTERM", sigtermHandler);
|
||||||
|
child.stdout.destroy();
|
||||||
|
child.unref();
|
||||||
|
if (cliArgs["endpoint"]) {
|
||||||
|
console.log(`### Session \`${sessionName}\` created, attached to \`${cliArgs["endpoint"]}\`.`);
|
||||||
|
console.log(`Run commands with: playwright-cli --session=${sessionName} <command>`);
|
||||||
|
} else {
|
||||||
|
console.log(`### Browser \`${sessionName}\` opened with pid ${child.pid}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _stopDaemon() {
|
||||||
|
const { socket, error: socketError } = await this._connect();
|
||||||
|
if (!socket) {
|
||||||
|
console.log(`Browser '${this.name}' is not open.${socketError ? " Error: " + socketError.message : ""}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let error;
|
||||||
|
await SocketConnectionClient.sendAndClose(socket, "stop", {}).catch((e) => error = e);
|
||||||
|
if (error && !error?.message?.includes("Session closed"))
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
async deleteSessionConfig() {
|
||||||
|
await import_fs.default.promises.rm(this._sessionFile.file).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function renderResolvedConfig(config) {
|
||||||
|
const channel = config.browser.launchOptions.channel ?? config.browser.browserName;
|
||||||
|
const lines = [];
|
||||||
|
if (channel)
|
||||||
|
lines.push(` - browser-type: ${channel}`);
|
||||||
|
if (!config.cli.persistent)
|
||||||
|
lines.push(` - user-data-dir: <in-memory>`);
|
||||||
|
else
|
||||||
|
lines.push(` - user-data-dir: ${config.browser.userDataDir}`);
|
||||||
|
lines.push(` - headed: ${!config.browser.launchOptions.headless}`);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
class SocketConnectionClient {
|
||||||
|
constructor(socket) {
|
||||||
|
this._nextMessageId = 1;
|
||||||
|
this._callbacks = /* @__PURE__ */ new Map();
|
||||||
|
this._connection = new import_socketConnection.SocketConnection(socket);
|
||||||
|
this._connection.onmessage = (message) => this._onMessage(message);
|
||||||
|
this._connection.onclose = () => this._rejectCallbacks();
|
||||||
|
}
|
||||||
|
async send(method, params = {}) {
|
||||||
|
const messageId = this._nextMessageId++;
|
||||||
|
const message = {
|
||||||
|
id: messageId,
|
||||||
|
method,
|
||||||
|
params
|
||||||
|
};
|
||||||
|
const responsePromise = new Promise((resolve, reject) => {
|
||||||
|
this._callbacks.set(messageId, { resolve, reject, method, params });
|
||||||
|
});
|
||||||
|
const [result] = await Promise.all([responsePromise, this._connection.send(message)]);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
static async sendAndClose(socket, method, params = {}) {
|
||||||
|
const connection = new SocketConnectionClient(socket);
|
||||||
|
try {
|
||||||
|
return await connection.send(method, params);
|
||||||
|
} finally {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close() {
|
||||||
|
this._connection.close();
|
||||||
|
}
|
||||||
|
_onMessage(object) {
|
||||||
|
if (object.id && this._callbacks.has(object.id)) {
|
||||||
|
const callback = this._callbacks.get(object.id);
|
||||||
|
this._callbacks.delete(object.id);
|
||||||
|
if (object.error)
|
||||||
|
callback.reject(new Error(object.error));
|
||||||
|
else
|
||||||
|
callback.resolve(object.result);
|
||||||
|
} else if (object.id) {
|
||||||
|
throw new Error(`Unexpected message id: ${object.id}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unexpected message without id: ${JSON.stringify(object)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_rejectCallbacks() {
|
||||||
|
for (const callback of this._callbacks.values())
|
||||||
|
callback.reject(new Error("Session closed"));
|
||||||
|
this._callbacks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
Session,
|
||||||
|
renderResolvedConfig
|
||||||
|
});
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
---
|
||||||
|
name: playwright-cli
|
||||||
|
description: Automate browser interactions, test web pages and work with Playwright tests.
|
||||||
|
allowed-tools: Bash(playwright-cli:*) Bash(npx:*) Bash(npm:*)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Browser Automation with playwright-cli
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# open new browser
|
||||||
|
playwright-cli open
|
||||||
|
# navigate to a page
|
||||||
|
playwright-cli goto https://playwright.dev
|
||||||
|
# interact with the page using refs from the snapshot
|
||||||
|
playwright-cli click e15
|
||||||
|
playwright-cli type "page.click"
|
||||||
|
playwright-cli press Enter
|
||||||
|
# take a screenshot (rarely used, as snapshot is more common)
|
||||||
|
playwright-cli screenshot
|
||||||
|
# close the browser
|
||||||
|
playwright-cli close
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Core
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli open
|
||||||
|
# open and navigate right away
|
||||||
|
playwright-cli open https://example.com/
|
||||||
|
playwright-cli goto https://playwright.dev
|
||||||
|
playwright-cli type "search query"
|
||||||
|
playwright-cli click e3
|
||||||
|
playwright-cli dblclick e7
|
||||||
|
# --submit presses Enter after filling the element
|
||||||
|
playwright-cli fill e5 "user@example.com" --submit
|
||||||
|
playwright-cli drag e2 e8
|
||||||
|
playwright-cli hover e4
|
||||||
|
playwright-cli select e9 "option-value"
|
||||||
|
playwright-cli upload ./document.pdf
|
||||||
|
playwright-cli check e12
|
||||||
|
playwright-cli uncheck e12
|
||||||
|
playwright-cli snapshot
|
||||||
|
playwright-cli eval "document.title"
|
||||||
|
playwright-cli eval "el => el.textContent" e5
|
||||||
|
# get element id, class, or any attribute not visible in the snapshot
|
||||||
|
playwright-cli eval "el => el.id" e5
|
||||||
|
playwright-cli eval "el => el.getAttribute('data-testid')" e5
|
||||||
|
playwright-cli dialog-accept
|
||||||
|
playwright-cli dialog-accept "confirmation text"
|
||||||
|
playwright-cli dialog-dismiss
|
||||||
|
playwright-cli resize 1920 1080
|
||||||
|
playwright-cli close
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli go-back
|
||||||
|
playwright-cli go-forward
|
||||||
|
playwright-cli reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli press Enter
|
||||||
|
playwright-cli press ArrowDown
|
||||||
|
playwright-cli keydown Shift
|
||||||
|
playwright-cli keyup Shift
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mouse
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli mousemove 150 300
|
||||||
|
playwright-cli mousedown
|
||||||
|
playwright-cli mousedown right
|
||||||
|
playwright-cli mouseup
|
||||||
|
playwright-cli mouseup right
|
||||||
|
playwright-cli mousewheel 0 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Save as
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli screenshot
|
||||||
|
playwright-cli screenshot e5
|
||||||
|
playwright-cli screenshot --filename=page.png
|
||||||
|
playwright-cli pdf --filename=page.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli tab-list
|
||||||
|
playwright-cli tab-new
|
||||||
|
playwright-cli tab-new https://example.com/page
|
||||||
|
playwright-cli tab-close
|
||||||
|
playwright-cli tab-close 2
|
||||||
|
playwright-cli tab-select 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli state-save
|
||||||
|
playwright-cli state-save auth.json
|
||||||
|
playwright-cli state-load auth.json
|
||||||
|
|
||||||
|
# Cookies
|
||||||
|
playwright-cli cookie-list
|
||||||
|
playwright-cli cookie-list --domain=example.com
|
||||||
|
playwright-cli cookie-get session_id
|
||||||
|
playwright-cli cookie-set session_id abc123
|
||||||
|
playwright-cli cookie-set session_id abc123 --domain=example.com --httpOnly --secure
|
||||||
|
playwright-cli cookie-delete session_id
|
||||||
|
playwright-cli cookie-clear
|
||||||
|
|
||||||
|
# LocalStorage
|
||||||
|
playwright-cli localstorage-list
|
||||||
|
playwright-cli localstorage-get theme
|
||||||
|
playwright-cli localstorage-set theme dark
|
||||||
|
playwright-cli localstorage-delete theme
|
||||||
|
playwright-cli localstorage-clear
|
||||||
|
|
||||||
|
# SessionStorage
|
||||||
|
playwright-cli sessionstorage-list
|
||||||
|
playwright-cli sessionstorage-get step
|
||||||
|
playwright-cli sessionstorage-set step 3
|
||||||
|
playwright-cli sessionstorage-delete step
|
||||||
|
playwright-cli sessionstorage-clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli route "**/*.jpg" --status=404
|
||||||
|
playwright-cli route "https://api.example.com/**" --body='{"mock": true}'
|
||||||
|
playwright-cli route-list
|
||||||
|
playwright-cli unroute "**/*.jpg"
|
||||||
|
playwright-cli unroute
|
||||||
|
```
|
||||||
|
|
||||||
|
### DevTools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli console
|
||||||
|
playwright-cli console warning
|
||||||
|
playwright-cli network
|
||||||
|
playwright-cli run-code "async page => await page.context().grantPermissions(['geolocation'])"
|
||||||
|
playwright-cli run-code --filename=script.js
|
||||||
|
playwright-cli tracing-start
|
||||||
|
playwright-cli tracing-stop
|
||||||
|
playwright-cli video-start video.webm
|
||||||
|
playwright-cli video-chapter "Chapter Title" --description="Details" --duration=2000
|
||||||
|
playwright-cli video-stop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open parameters
|
||||||
|
```bash
|
||||||
|
# Use specific browser when creating session
|
||||||
|
playwright-cli open --browser=chrome
|
||||||
|
playwright-cli open --browser=firefox
|
||||||
|
playwright-cli open --browser=webkit
|
||||||
|
playwright-cli open --browser=msedge
|
||||||
|
# Connect to browser via extension
|
||||||
|
playwright-cli open --extension
|
||||||
|
|
||||||
|
# Use persistent profile (by default profile is in-memory)
|
||||||
|
playwright-cli open --persistent
|
||||||
|
# Use persistent profile with custom directory
|
||||||
|
playwright-cli open --profile=/path/to/profile
|
||||||
|
|
||||||
|
# Start with config file
|
||||||
|
playwright-cli open --config=my-config.json
|
||||||
|
|
||||||
|
# Close the browser
|
||||||
|
playwright-cli close
|
||||||
|
# Delete user data for the default session
|
||||||
|
playwright-cli delete-data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Snapshots
|
||||||
|
|
||||||
|
After each command, playwright-cli provides a snapshot of the current browser state.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> playwright-cli goto https://example.com
|
||||||
|
### Page
|
||||||
|
- Page URL: https://example.com/
|
||||||
|
- Page Title: Example Domain
|
||||||
|
### Snapshot
|
||||||
|
[Snapshot](.playwright-cli/page-2026-02-14T19-22-42-679Z.yml)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also take a snapshot on demand using `playwright-cli snapshot` command. All the options below can be combined as needed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# default - save to a file with timestamp-based name
|
||||||
|
playwright-cli snapshot
|
||||||
|
|
||||||
|
# save to file, use when snapshot is a part of the workflow result
|
||||||
|
playwright-cli snapshot --filename=after-click.yaml
|
||||||
|
|
||||||
|
# snapshot an element instead of the whole page
|
||||||
|
playwright-cli snapshot "#main"
|
||||||
|
|
||||||
|
# limit snapshot depth for efficiency, take a partial snapshot afterwards
|
||||||
|
playwright-cli snapshot --depth=4
|
||||||
|
playwright-cli snapshot e34
|
||||||
|
```
|
||||||
|
|
||||||
|
## Targeting elements
|
||||||
|
|
||||||
|
By default, use refs from the snapshot to interact with page elements.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# get snapshot with refs
|
||||||
|
playwright-cli snapshot
|
||||||
|
|
||||||
|
# interact using a ref
|
||||||
|
playwright-cli click e15
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use css selectors or Playwright locators.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# css selector
|
||||||
|
playwright-cli click "#main > button.submit"
|
||||||
|
|
||||||
|
# role locator
|
||||||
|
playwright-cli click "getByRole('button', { name: 'Submit' })"
|
||||||
|
|
||||||
|
# test id
|
||||||
|
playwright-cli click "getByTestId('submit-button')"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Sessions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create new browser session named "mysession" with persistent profile
|
||||||
|
playwright-cli -s=mysession open example.com --persistent
|
||||||
|
# same with manually specified profile directory (use when requested explicitly)
|
||||||
|
playwright-cli -s=mysession open example.com --profile=/path/to/profile
|
||||||
|
playwright-cli -s=mysession click e6
|
||||||
|
playwright-cli -s=mysession close # stop a named browser
|
||||||
|
playwright-cli -s=mysession delete-data # delete user data for persistent session
|
||||||
|
|
||||||
|
playwright-cli list
|
||||||
|
# Close all browsers
|
||||||
|
playwright-cli close-all
|
||||||
|
# Forcefully kill all browser processes
|
||||||
|
playwright-cli kill-all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
If global `playwright-cli` command is not available, try a local version via `npx playwright-cli`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx --no-install playwright-cli --version
|
||||||
|
```
|
||||||
|
|
||||||
|
When local version is available, use `npx playwright-cli` in all commands. Otherwise, install `playwright-cli` as a global command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @playwright/cli@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Form submission
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli open https://example.com/form
|
||||||
|
playwright-cli snapshot
|
||||||
|
|
||||||
|
playwright-cli fill e1 "user@example.com"
|
||||||
|
playwright-cli fill e2 "password123"
|
||||||
|
playwright-cli click e3
|
||||||
|
playwright-cli snapshot
|
||||||
|
playwright-cli close
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Multi-tab workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli open https://example.com
|
||||||
|
playwright-cli tab-new https://example.com/other
|
||||||
|
playwright-cli tab-list
|
||||||
|
playwright-cli tab-select 0
|
||||||
|
playwright-cli snapshot
|
||||||
|
playwright-cli close
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Debugging with DevTools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli open https://example.com
|
||||||
|
playwright-cli click e4
|
||||||
|
playwright-cli fill e7 "test"
|
||||||
|
playwright-cli console
|
||||||
|
playwright-cli network
|
||||||
|
playwright-cli close
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli open https://example.com
|
||||||
|
playwright-cli tracing-start
|
||||||
|
playwright-cli click e4
|
||||||
|
playwright-cli fill e7 "test"
|
||||||
|
playwright-cli tracing-stop
|
||||||
|
playwright-cli close
|
||||||
|
```
|
||||||
|
|
||||||
|
## Specific tasks
|
||||||
|
|
||||||
|
* **Running and Debugging Playwright tests** [references/playwright-tests.md](references/playwright-tests.md)
|
||||||
|
* **Request mocking** [references/request-mocking.md](references/request-mocking.md)
|
||||||
|
* **Running Playwright code** [references/running-code.md](references/running-code.md)
|
||||||
|
* **Browser session management** [references/session-management.md](references/session-management.md)
|
||||||
|
* **Storage state (cookies, localStorage)** [references/storage-state.md](references/storage-state.md)
|
||||||
|
* **Test generation** [references/test-generation.md](references/test-generation.md)
|
||||||
|
* **Tracing** [references/tracing.md](references/tracing.md)
|
||||||
|
* **Video recording** [references/video-recording.md](references/video-recording.md)
|
||||||
|
* **Inspecting element attributes** [references/element-attributes.md](references/element-attributes.md)
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# Inspecting Element Attributes
|
||||||
|
|
||||||
|
When the snapshot doesn't show an element's `id`, `class`, `data-*` attributes, or other DOM properties, use `eval` to inspect them.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli snapshot
|
||||||
|
# snapshot shows a button as e7 but doesn't reveal its id or data attributes
|
||||||
|
|
||||||
|
# get the element's id
|
||||||
|
playwright-cli eval "el => el.id" e7
|
||||||
|
|
||||||
|
# get all CSS classes
|
||||||
|
playwright-cli eval "el => el.className" e7
|
||||||
|
|
||||||
|
# get a specific attribute
|
||||||
|
playwright-cli eval "el => el.getAttribute('data-testid')" e7
|
||||||
|
playwright-cli eval "el => el.getAttribute('aria-label')" e7
|
||||||
|
|
||||||
|
# get a computed style property
|
||||||
|
playwright-cli eval "el => getComputedStyle(el).display" e7
|
||||||
|
```
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
# Running Playwright Tests
|
||||||
|
|
||||||
|
To run Playwright tests, use the `npx playwright test` command, or a package manager script. To avoid opening the interactive html report, use `PLAYWRIGHT_HTML_OPEN=never` environment variable.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
PLAYWRIGHT_HTML_OPEN=never npx playwright test
|
||||||
|
|
||||||
|
# Run all tests through a custom npm script
|
||||||
|
PLAYWRIGHT_HTML_OPEN=never npm run special-test-command
|
||||||
|
```
|
||||||
|
|
||||||
|
# Debugging Playwright Tests
|
||||||
|
|
||||||
|
To debug a failing Playwright test, run it with `--debug=cli` option. This command will pause the test at the start and print the debugging instructions.
|
||||||
|
|
||||||
|
**IMPORTANT**: run the command in the background and check the output until "Debugging Instructions" is printed.
|
||||||
|
|
||||||
|
Once instructions containing a session name are printed, use `playwright-cli` to attach the session and explore the page.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the test
|
||||||
|
PLAYWRIGHT_HTML_OPEN=never npx playwright test --debug=cli
|
||||||
|
# ...
|
||||||
|
# ... debugging instructions for "tw-abcdef" session ...
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# Attach to the test
|
||||||
|
playwright-cli attach tw-abcdef
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the test running in the background while you explore and look for a fix.
|
||||||
|
The test is paused at the start, so you should step over or pause at a particular location
|
||||||
|
where the problem is most likely to be.
|
||||||
|
|
||||||
|
Every action you perform with `playwright-cli` generates corresponding Playwright TypeScript code.
|
||||||
|
This code appears in the output and can be copied directly into the test. Most of the time, a specific locator or an expectation should be updated, but it could also be a bug in the app. Use your judgement.
|
||||||
|
|
||||||
|
After fixing the test, stop the background test run. Rerun to check that test passes.
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
# Request Mocking
|
||||||
|
|
||||||
|
Intercept, mock, modify, and block network requests.
|
||||||
|
|
||||||
|
## CLI Route Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mock with custom status
|
||||||
|
playwright-cli route "**/*.jpg" --status=404
|
||||||
|
|
||||||
|
# Mock with JSON body
|
||||||
|
playwright-cli route "**/api/users" --body='[{"id":1,"name":"Alice"}]' --content-type=application/json
|
||||||
|
|
||||||
|
# Mock with custom headers
|
||||||
|
playwright-cli route "**/api/data" --body='{"ok":true}' --header="X-Custom: value"
|
||||||
|
|
||||||
|
# Remove headers from requests
|
||||||
|
playwright-cli route "**/*" --remove-header=cookie,authorization
|
||||||
|
|
||||||
|
# List active routes
|
||||||
|
playwright-cli route-list
|
||||||
|
|
||||||
|
# Remove a route or all routes
|
||||||
|
playwright-cli unroute "**/*.jpg"
|
||||||
|
playwright-cli unroute
|
||||||
|
```
|
||||||
|
|
||||||
|
## URL Patterns
|
||||||
|
|
||||||
|
```
|
||||||
|
**/api/users - Exact path match
|
||||||
|
**/api/*/details - Wildcard in path
|
||||||
|
**/*.{png,jpg,jpeg} - Match file extensions
|
||||||
|
**/search?q=* - Match query parameters
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Mocking with run-code
|
||||||
|
|
||||||
|
For conditional responses, request body inspection, response modification, or delays:
|
||||||
|
|
||||||
|
### Conditional Response Based on Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.route('**/api/login', route => {
|
||||||
|
const body = route.request().postDataJSON();
|
||||||
|
if (body.username === 'admin') {
|
||||||
|
route.fulfill({ body: JSON.stringify({ token: 'mock-token' }) });
|
||||||
|
} else {
|
||||||
|
route.fulfill({ status: 401, body: JSON.stringify({ error: 'Invalid' }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modify Real Response
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.route('**/api/user', async route => {
|
||||||
|
const response = await route.fetch();
|
||||||
|
const json = await response.json();
|
||||||
|
json.isPremium = true;
|
||||||
|
await route.fulfill({ response, json });
|
||||||
|
});
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Simulate Network Failures
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.route('**/api/offline', route => route.abort('internetdisconnected'));
|
||||||
|
}"
|
||||||
|
# Options: connectionrefused, timedout, connectionreset, internetdisconnected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delayed Response
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.route('**/api/slow', async route => {
|
||||||
|
await new Promise(r => setTimeout(r, 3000));
|
||||||
|
route.fulfill({ body: JSON.stringify({ data: 'loaded' }) });
|
||||||
|
});
|
||||||
|
}"
|
||||||
|
```
|
||||||
+231
@@ -0,0 +1,231 @@
|
|||||||
|
# Running Custom Playwright Code
|
||||||
|
|
||||||
|
Use `run-code` to execute arbitrary Playwright code for advanced scenarios not covered by CLI commands.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
// Your Playwright code here
|
||||||
|
// Access page.context() for browser context operations
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Geolocation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Grant geolocation permission and set location
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.context().grantPermissions(['geolocation']);
|
||||||
|
await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 });
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Set location to London
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.context().grantPermissions(['geolocation']);
|
||||||
|
await page.context().setGeolocation({ latitude: 51.5074, longitude: -0.1278 });
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Clear geolocation override
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.context().clearPermissions();
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Grant multiple permissions
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.context().grantPermissions([
|
||||||
|
'geolocation',
|
||||||
|
'notifications',
|
||||||
|
'camera',
|
||||||
|
'microphone'
|
||||||
|
]);
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Grant permissions for specific origin
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.context().grantPermissions(['clipboard-read'], {
|
||||||
|
origin: 'https://example.com'
|
||||||
|
});
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Media Emulation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Emulate dark color scheme
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.emulateMedia({ colorScheme: 'dark' });
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Emulate light color scheme
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.emulateMedia({ colorScheme: 'light' });
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Emulate reduced motion
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Emulate print media
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.emulateMedia({ media: 'print' });
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wait Strategies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Wait for network idle
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Wait for specific element
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.locator('.loading').waitFor({ state: 'hidden' });
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Wait for function to return true
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.waitForFunction(() => window.appReady === true);
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Wait with timeout
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.locator('.result').waitFor({ timeout: 10000 });
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frames and Iframes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Work with iframe
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
const frame = page.locator('iframe#my-iframe').contentFrame();
|
||||||
|
await frame.locator('button').click();
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Get all frames
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
const frames = page.frames();
|
||||||
|
return frames.map(f => f.url());
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Downloads
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Handle file download
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
const downloadPromise = page.waitForEvent('download');
|
||||||
|
await page.getByRole('link', { name: 'Download' }).click();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
await download.saveAs('./downloaded-file.pdf');
|
||||||
|
return download.suggestedFilename();
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clipboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Read clipboard (requires permission)
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.context().grantPermissions(['clipboard-read']);
|
||||||
|
return await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Write to clipboard
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.evaluate(text => navigator.clipboard.writeText(text), 'Hello clipboard!');
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Page Information
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get page title
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
return await page.title();
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Get current URL
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
return page.url();
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Get page content
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
return await page.content();
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Get viewport size
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
return page.viewportSize();
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript Execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Execute JavaScript and return result
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
return await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
language: navigator.language,
|
||||||
|
cookiesEnabled: navigator.cookieEnabled
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Pass arguments to evaluate
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
const multiplier = 5;
|
||||||
|
return await page.evaluate(m => document.querySelectorAll('li').length * m, multiplier);
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Try-catch in run-code
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
try {
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click({ timeout: 1000 });
|
||||||
|
return 'clicked';
|
||||||
|
} catch (e) {
|
||||||
|
return 'element not found';
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complex Workflows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Login and save state
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.goto('https://example.com/login');
|
||||||
|
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
|
||||||
|
await page.getByRole('textbox', { name: 'Password' }).fill('secret');
|
||||||
|
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||||
|
await page.waitForURL('**/dashboard');
|
||||||
|
await page.context().storageState({ path: 'auth.json' });
|
||||||
|
return 'Login successful';
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Scrape data from multiple pages
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
const results = [];
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
await page.goto(\`https://example.com/page/\${i}\`);
|
||||||
|
const items = await page.locator('.item').allTextContents();
|
||||||
|
results.push(...items);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}"
|
||||||
|
```
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
# Browser Session Management
|
||||||
|
|
||||||
|
Run multiple isolated browser sessions concurrently with state persistence.
|
||||||
|
|
||||||
|
## Named Browser Sessions
|
||||||
|
|
||||||
|
Use `-s` flag to isolate browser contexts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Browser 1: Authentication flow
|
||||||
|
playwright-cli -s=auth open https://app.example.com/login
|
||||||
|
|
||||||
|
# Browser 2: Public browsing (separate cookies, storage)
|
||||||
|
playwright-cli -s=public open https://example.com
|
||||||
|
|
||||||
|
# Commands are isolated by browser session
|
||||||
|
playwright-cli -s=auth fill e1 "user@example.com"
|
||||||
|
playwright-cli -s=public snapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Session Isolation Properties
|
||||||
|
|
||||||
|
Each browser session has independent:
|
||||||
|
- Cookies
|
||||||
|
- LocalStorage / SessionStorage
|
||||||
|
- IndexedDB
|
||||||
|
- Cache
|
||||||
|
- Browsing history
|
||||||
|
- Open tabs
|
||||||
|
|
||||||
|
## Browser Session Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all browser sessions
|
||||||
|
playwright-cli list
|
||||||
|
|
||||||
|
# Stop a browser session (close the browser)
|
||||||
|
playwright-cli close # stop the default browser
|
||||||
|
playwright-cli -s=mysession close # stop a named browser
|
||||||
|
|
||||||
|
# Stop all browser sessions
|
||||||
|
playwright-cli close-all
|
||||||
|
|
||||||
|
# Forcefully kill all daemon processes (for stale/zombie processes)
|
||||||
|
playwright-cli kill-all
|
||||||
|
|
||||||
|
# Delete browser session user data (profile directory)
|
||||||
|
playwright-cli delete-data # delete default browser data
|
||||||
|
playwright-cli -s=mysession delete-data # delete named browser data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variable
|
||||||
|
|
||||||
|
Set a default browser session name via environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PLAYWRIGHT_CLI_SESSION="mysession"
|
||||||
|
playwright-cli open example.com # Uses "mysession" automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Concurrent Scraping
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Scrape multiple sites concurrently
|
||||||
|
|
||||||
|
# Start all browsers
|
||||||
|
playwright-cli -s=site1 open https://site1.com &
|
||||||
|
playwright-cli -s=site2 open https://site2.com &
|
||||||
|
playwright-cli -s=site3 open https://site3.com &
|
||||||
|
wait
|
||||||
|
|
||||||
|
# Take snapshots from each
|
||||||
|
playwright-cli -s=site1 snapshot
|
||||||
|
playwright-cli -s=site2 snapshot
|
||||||
|
playwright-cli -s=site3 snapshot
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
playwright-cli close-all
|
||||||
|
```
|
||||||
|
|
||||||
|
### A/B Testing Sessions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test different user experiences
|
||||||
|
playwright-cli -s=variant-a open "https://app.com?variant=a"
|
||||||
|
playwright-cli -s=variant-b open "https://app.com?variant=b"
|
||||||
|
|
||||||
|
# Compare
|
||||||
|
playwright-cli -s=variant-a screenshot
|
||||||
|
playwright-cli -s=variant-b screenshot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Persistent Profile
|
||||||
|
|
||||||
|
By default, browser profile is kept in memory only. Use `--persistent` flag on `open` to persist the browser profile to disk:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use persistent profile (auto-generated location)
|
||||||
|
playwright-cli open https://example.com --persistent
|
||||||
|
|
||||||
|
# Use persistent profile with custom directory
|
||||||
|
playwright-cli open https://example.com --profile=/path/to/profile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Default Browser Session
|
||||||
|
|
||||||
|
When `-s` is omitted, commands use the default browser session:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# These use the same default browser session
|
||||||
|
playwright-cli open https://example.com
|
||||||
|
playwright-cli snapshot
|
||||||
|
playwright-cli close # Stops default browser
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Session Configuration
|
||||||
|
|
||||||
|
Configure a browser session with specific settings when opening:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open with config file
|
||||||
|
playwright-cli open https://example.com --config=.playwright/my-cli.json
|
||||||
|
|
||||||
|
# Open with specific browser
|
||||||
|
playwright-cli open https://example.com --browser=firefox
|
||||||
|
|
||||||
|
# Open in headed mode
|
||||||
|
playwright-cli open https://example.com --headed
|
||||||
|
|
||||||
|
# Open with persistent profile
|
||||||
|
playwright-cli open https://example.com --persistent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Name Browser Sessions Semantically
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GOOD: Clear purpose
|
||||||
|
playwright-cli -s=github-auth open https://github.com
|
||||||
|
playwright-cli -s=docs-scrape open https://docs.example.com
|
||||||
|
|
||||||
|
# AVOID: Generic names
|
||||||
|
playwright-cli -s=s1 open https://github.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Always Clean Up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop browsers when done
|
||||||
|
playwright-cli -s=auth close
|
||||||
|
playwright-cli -s=scrape close
|
||||||
|
|
||||||
|
# Or stop all at once
|
||||||
|
playwright-cli close-all
|
||||||
|
|
||||||
|
# If browsers become unresponsive or zombie processes remain
|
||||||
|
playwright-cli kill-all
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Delete Stale Browser Data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove old browser data to free disk space
|
||||||
|
playwright-cli -s=oldsession delete-data
|
||||||
|
```
|
||||||
+275
@@ -0,0 +1,275 @@
|
|||||||
|
# Storage Management
|
||||||
|
|
||||||
|
Manage cookies, localStorage, sessionStorage, and browser storage state.
|
||||||
|
|
||||||
|
## Storage State
|
||||||
|
|
||||||
|
Save and restore complete browser state including cookies and storage.
|
||||||
|
|
||||||
|
### Save Storage State
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Save to auto-generated filename (storage-state-{timestamp}.json)
|
||||||
|
playwright-cli state-save
|
||||||
|
|
||||||
|
# Save to specific filename
|
||||||
|
playwright-cli state-save my-auth-state.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Storage State
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Load storage state from file
|
||||||
|
playwright-cli state-load my-auth-state.json
|
||||||
|
|
||||||
|
# Reload page to apply cookies
|
||||||
|
playwright-cli open https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage State File Format
|
||||||
|
|
||||||
|
The saved file contains:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cookies": [
|
||||||
|
{
|
||||||
|
"name": "session_id",
|
||||||
|
"value": "abc123",
|
||||||
|
"domain": "example.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1735689600,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": true,
|
||||||
|
"sameSite": "Lax"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"origins": [
|
||||||
|
{
|
||||||
|
"origin": "https://example.com",
|
||||||
|
"localStorage": [
|
||||||
|
{ "name": "theme", "value": "dark" },
|
||||||
|
{ "name": "user_id", "value": "12345" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cookies
|
||||||
|
|
||||||
|
### List All Cookies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli cookie-list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter Cookies by Domain
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli cookie-list --domain=example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter Cookies by Path
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli cookie-list --path=/api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Specific Cookie
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli cookie-get session_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set a Cookie
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic cookie
|
||||||
|
playwright-cli cookie-set session abc123
|
||||||
|
|
||||||
|
# Cookie with options
|
||||||
|
playwright-cli cookie-set session abc123 --domain=example.com --path=/ --httpOnly --secure --sameSite=Lax
|
||||||
|
|
||||||
|
# Cookie with expiration (Unix timestamp)
|
||||||
|
playwright-cli cookie-set remember_me token123 --expires=1735689600
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete a Cookie
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli cookie-delete session_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clear All Cookies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli cookie-clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced: Multiple Cookies or Custom Options
|
||||||
|
|
||||||
|
For complex scenarios like adding multiple cookies at once, use `run-code`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.context().addCookies([
|
||||||
|
{ name: 'session_id', value: 'sess_abc123', domain: 'example.com', path: '/', httpOnly: true },
|
||||||
|
{ name: 'preferences', value: JSON.stringify({ theme: 'dark' }), domain: 'example.com', path: '/' }
|
||||||
|
]);
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Storage
|
||||||
|
|
||||||
|
### List All localStorage Items
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli localstorage-list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Single Value
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli localstorage-get token
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Value
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli localstorage-set theme dark
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set JSON Value
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli localstorage-set user_settings '{"theme":"dark","language":"en"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Single Item
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli localstorage-delete token
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clear All localStorage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli localstorage-clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced: Multiple Operations
|
||||||
|
|
||||||
|
For complex scenarios like setting multiple values at once, use `run-code`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
localStorage.setItem('token', 'jwt_abc123');
|
||||||
|
localStorage.setItem('user_id', '12345');
|
||||||
|
localStorage.setItem('expires_at', Date.now() + 3600000);
|
||||||
|
});
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Storage
|
||||||
|
|
||||||
|
### List All sessionStorage Items
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli sessionstorage-list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Single Value
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli sessionstorage-get form_data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Value
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli sessionstorage-set step 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Single Item
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli sessionstorage-delete step
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clear sessionStorage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli sessionstorage-clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## IndexedDB
|
||||||
|
|
||||||
|
### List Databases
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
return await page.evaluate(async () => {
|
||||||
|
const databases = await indexedDB.databases();
|
||||||
|
return databases;
|
||||||
|
});
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli run-code "async page => {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
indexedDB.deleteDatabase('myDatabase');
|
||||||
|
});
|
||||||
|
}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Authentication State Reuse
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Login and save state
|
||||||
|
playwright-cli open https://app.example.com/login
|
||||||
|
playwright-cli snapshot
|
||||||
|
playwright-cli fill e1 "user@example.com"
|
||||||
|
playwright-cli fill e2 "password123"
|
||||||
|
playwright-cli click e3
|
||||||
|
|
||||||
|
# Save the authenticated state
|
||||||
|
playwright-cli state-save auth.json
|
||||||
|
|
||||||
|
# Step 2: Later, restore state and skip login
|
||||||
|
playwright-cli state-load auth.json
|
||||||
|
playwright-cli open https://app.example.com/dashboard
|
||||||
|
# Already logged in!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Save and Restore Roundtrip
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set up authentication state
|
||||||
|
playwright-cli open https://example.com
|
||||||
|
playwright-cli eval "() => { document.cookie = 'session=abc123'; localStorage.setItem('user', 'john'); }"
|
||||||
|
|
||||||
|
# Save state to file
|
||||||
|
playwright-cli state-save my-session.json
|
||||||
|
|
||||||
|
# ... later, in a new session ...
|
||||||
|
|
||||||
|
# Restore state
|
||||||
|
playwright-cli state-load my-session.json
|
||||||
|
playwright-cli open https://example.com
|
||||||
|
# Cookies and localStorage are restored!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Never commit storage state files containing auth tokens
|
||||||
|
- Add `*.auth-state.json` to `.gitignore`
|
||||||
|
- Delete state files after automation completes
|
||||||
|
- Use environment variables for sensitive data
|
||||||
|
- By default, sessions run in-memory mode which is safer for sensitive operations
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
# Test Generation
|
||||||
|
|
||||||
|
Generate Playwright test code automatically as you interact with the browser.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Every action you perform with `playwright-cli` generates corresponding Playwright TypeScript code.
|
||||||
|
This code appears in the output and can be copied directly into your test files.
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start a session
|
||||||
|
playwright-cli open https://example.com/login
|
||||||
|
|
||||||
|
# Take a snapshot to see elements
|
||||||
|
playwright-cli snapshot
|
||||||
|
# Output shows: e1 [textbox "Email"], e2 [textbox "Password"], e3 [button "Sign In"]
|
||||||
|
|
||||||
|
# Fill form fields - generates code automatically
|
||||||
|
playwright-cli fill e1 "user@example.com"
|
||||||
|
# Ran Playwright code:
|
||||||
|
# await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
|
||||||
|
|
||||||
|
playwright-cli fill e2 "password123"
|
||||||
|
# Ran Playwright code:
|
||||||
|
# await page.getByRole('textbox', { name: 'Password' }).fill('password123');
|
||||||
|
|
||||||
|
playwright-cli click e3
|
||||||
|
# Ran Playwright code:
|
||||||
|
# await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building a Test File
|
||||||
|
|
||||||
|
Collect the generated code into a Playwright test:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('login flow', async ({ page }) => {
|
||||||
|
// Generated code from playwright-cli session:
|
||||||
|
await page.goto('https://example.com/login');
|
||||||
|
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
|
||||||
|
await page.getByRole('textbox', { name: 'Password' }).fill('password123');
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||||
|
|
||||||
|
// Add assertions
|
||||||
|
await expect(page).toHaveURL(/.*dashboard/);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use Semantic Locators
|
||||||
|
|
||||||
|
The generated code uses role-based locators when possible, which are more resilient:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Generated (good - semantic)
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
// Avoid (fragile - CSS selectors)
|
||||||
|
await page.locator('#submit-btn').click();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Explore Before Recording
|
||||||
|
|
||||||
|
Take snapshots to understand the page structure before recording actions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli open https://example.com
|
||||||
|
playwright-cli snapshot
|
||||||
|
# Review the element structure
|
||||||
|
playwright-cli click e5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add Assertions Manually
|
||||||
|
|
||||||
|
Generated code captures actions but not assertions. Add expectations in your test:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Generated action
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
// Manual assertion
|
||||||
|
await expect(page.getByText('Success')).toBeVisible();
|
||||||
|
```
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
# Tracing
|
||||||
|
|
||||||
|
Capture detailed execution traces for debugging and analysis. Traces include DOM snapshots, screenshots, network activity, and console logs.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start trace recording
|
||||||
|
playwright-cli tracing-start
|
||||||
|
|
||||||
|
# Perform actions
|
||||||
|
playwright-cli open https://example.com
|
||||||
|
playwright-cli click e1
|
||||||
|
playwright-cli fill e2 "test"
|
||||||
|
|
||||||
|
# Stop trace recording
|
||||||
|
playwright-cli tracing-stop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trace Output Files
|
||||||
|
|
||||||
|
When you start tracing, Playwright creates a `traces/` directory with several files:
|
||||||
|
|
||||||
|
### `trace-{timestamp}.trace`
|
||||||
|
|
||||||
|
**Action log** - The main trace file containing:
|
||||||
|
- Every action performed (clicks, fills, navigations)
|
||||||
|
- DOM snapshots before and after each action
|
||||||
|
- Screenshots at each step
|
||||||
|
- Timing information
|
||||||
|
- Console messages
|
||||||
|
- Source locations
|
||||||
|
|
||||||
|
### `trace-{timestamp}.network`
|
||||||
|
|
||||||
|
**Network log** - Complete network activity:
|
||||||
|
- All HTTP requests and responses
|
||||||
|
- Request headers and bodies
|
||||||
|
- Response headers and bodies
|
||||||
|
- Timing (DNS, connect, TLS, TTFB, download)
|
||||||
|
- Resource sizes
|
||||||
|
- Failed requests and errors
|
||||||
|
|
||||||
|
### `resources/`
|
||||||
|
|
||||||
|
**Resources directory** - Cached resources:
|
||||||
|
- Images, fonts, stylesheets, scripts
|
||||||
|
- Response bodies for replay
|
||||||
|
- Assets needed to reconstruct page state
|
||||||
|
|
||||||
|
## What Traces Capture
|
||||||
|
|
||||||
|
| Category | Details |
|
||||||
|
|----------|---------|
|
||||||
|
| **Actions** | Clicks, fills, hovers, keyboard input, navigations |
|
||||||
|
| **DOM** | Full DOM snapshot before/after each action |
|
||||||
|
| **Screenshots** | Visual state at each step |
|
||||||
|
| **Network** | All requests, responses, headers, bodies, timing |
|
||||||
|
| **Console** | All console.log, warn, error messages |
|
||||||
|
| **Timing** | Precise timing for each operation |
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### Debugging Failed Actions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli tracing-start
|
||||||
|
playwright-cli open https://app.example.com
|
||||||
|
|
||||||
|
# This click fails - why?
|
||||||
|
playwright-cli click e5
|
||||||
|
|
||||||
|
playwright-cli tracing-stop
|
||||||
|
# Open trace to see DOM state when click was attempted
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analyzing Performance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
playwright-cli tracing-start
|
||||||
|
playwright-cli open https://slow-site.com
|
||||||
|
playwright-cli tracing-stop
|
||||||
|
|
||||||
|
# View network waterfall to identify slow resources
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capturing Evidence
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Record a complete user flow for documentation
|
||||||
|
playwright-cli tracing-start
|
||||||
|
|
||||||
|
playwright-cli open https://app.example.com/checkout
|
||||||
|
playwright-cli fill e1 "4111111111111111"
|
||||||
|
playwright-cli fill e2 "12/25"
|
||||||
|
playwright-cli fill e3 "123"
|
||||||
|
playwright-cli click e4
|
||||||
|
|
||||||
|
playwright-cli tracing-stop
|
||||||
|
# Trace shows exact sequence of events
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trace vs Video vs Screenshot
|
||||||
|
|
||||||
|
| Feature | Trace | Video | Screenshot |
|
||||||
|
|---------|-------|-------|------------|
|
||||||
|
| **Format** | .trace file | .webm video | .png/.jpeg image |
|
||||||
|
| **DOM inspection** | Yes | No | No |
|
||||||
|
| **Network details** | Yes | No | No |
|
||||||
|
| **Step-by-step replay** | Yes | Continuous | Single frame |
|
||||||
|
| **File size** | Medium | Large | Small |
|
||||||
|
| **Best for** | Debugging | Demos | Quick capture |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Start Tracing Before the Problem
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trace the entire flow, not just the failing step
|
||||||
|
playwright-cli tracing-start
|
||||||
|
playwright-cli open https://example.com
|
||||||
|
# ... all steps leading to the issue ...
|
||||||
|
playwright-cli tracing-stop
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Clean Up Old Traces
|
||||||
|
|
||||||
|
Traces can consume significant disk space:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove traces older than 7 days
|
||||||
|
find .playwright-cli/traces -mtime +7 -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Traces add overhead to automation
|
||||||
|
- Large traces can consume significant disk space
|
||||||
|
- Some dynamic content may not replay perfectly
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
# Video Recording
|
||||||
|
|
||||||
|
Capture browser automation sessions as video for debugging, documentation, or verification. Produces WebM (VP8/VP9 codec).
|
||||||
|
|
||||||
|
## Basic Recording
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open browser first
|
||||||
|
playwright-cli open
|
||||||
|
|
||||||
|
# Start recording
|
||||||
|
playwright-cli video-start demo.webm
|
||||||
|
|
||||||
|
# Add a chapter marker for section transitions
|
||||||
|
playwright-cli video-chapter "Getting Started" --description="Opening the homepage" --duration=2000
|
||||||
|
|
||||||
|
# Navigate and perform actions
|
||||||
|
playwright-cli goto https://example.com
|
||||||
|
playwright-cli snapshot
|
||||||
|
playwright-cli click e1
|
||||||
|
|
||||||
|
# Add another chapter
|
||||||
|
playwright-cli video-chapter "Filling Form" --description="Entering test data" --duration=2000
|
||||||
|
playwright-cli fill e2 "test input"
|
||||||
|
|
||||||
|
# Stop and save
|
||||||
|
playwright-cli video-stop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use Descriptive Filenames
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Include context in filename
|
||||||
|
playwright-cli video-start recordings/login-flow-2024-01-15.webm
|
||||||
|
playwright-cli video-start recordings/checkout-test-run-42.webm
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Record entire hero scripts.
|
||||||
|
|
||||||
|
When recording a video for the user or as a proof of work, it is best to create a code snippet and execute it with run-code.
|
||||||
|
It allows pulling appropriate pauses between the actions and annotating the video. There are new Playwright APIs for that.
|
||||||
|
|
||||||
|
1) Perform scenario using CLI and take note of all locators and actions. You'll need those locators to request thier bounding boxes for highlight.
|
||||||
|
2) Create a file with the intended script for video (below). Use pressSequentially w/ delay for nice typing, make reasonable pauses.
|
||||||
|
3) Use playwright-cli run-code --file your-script.js
|
||||||
|
|
||||||
|
**Important**: Overlays are `pointer-events: none` — they do not interfere with page interactions. You can safely keep sticky overlays visible while clicking, filling, or performing any actions on the page.
|
||||||
|
|
||||||
|
```js
|
||||||
|
async page => {
|
||||||
|
await page.screencast.start({ path: 'video.webm', size: { width: 1280, height: 800 } });
|
||||||
|
await page.goto('https://demo.playwright.dev/todomvc');
|
||||||
|
|
||||||
|
// Show a chapter card — blurs the page and shows a dialog.
|
||||||
|
// Blocks until duration expires, then auto-removes.
|
||||||
|
// Use this for simple use cases, but always feel free to hand-craft your own beautiful
|
||||||
|
// overlay via await page.screencast.showOverlay().
|
||||||
|
await page.screencast.showChapter('Adding Todo Items', {
|
||||||
|
description: 'We will add several items to the todo list.',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Perform action
|
||||||
|
await page.getByRole('textbox', { name: 'What needs to be done?' }).pressSequentially('Walk the dog', { delay: 60 });
|
||||||
|
await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter');
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Show next chapter
|
||||||
|
await page.screencast.showChapter('Verifying Results', {
|
||||||
|
description: 'Checking the item appeared in the list.',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a sticky annotation that stays while you perform actions.
|
||||||
|
// Overlays are pointer-events: none, so they won't block clicks.
|
||||||
|
const annotation = await page.screencast.showOverlay(`
|
||||||
|
<div style="position: absolute; top: 8px; right: 8px;
|
||||||
|
padding: 6px 12px; background: rgba(0,0,0,0.7);
|
||||||
|
border-radius: 8px; font-size: 13px; color: white;">
|
||||||
|
✓ Item added successfully
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Perform more actions while the annotation is visible
|
||||||
|
await page.getByRole('textbox', { name: 'What needs to be done?' }).pressSequentially('Buy groceries', { delay: 60 });
|
||||||
|
await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter');
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
|
// Remove the annotation when done
|
||||||
|
await annotation.dispose();
|
||||||
|
|
||||||
|
// You can also highlight relevant locators and provide contextual annotations.
|
||||||
|
const bounds = await page.getByText('Walk the dog').boundingBox();
|
||||||
|
await page.screencast.showOverlay(`
|
||||||
|
<div style="position: absolute;
|
||||||
|
top: ${bounds.y}px;
|
||||||
|
left: ${bounds.x}px;
|
||||||
|
width: ${bounds.width}px;
|
||||||
|
height: ${bounds.height}px;
|
||||||
|
border: 1px solid red;">
|
||||||
|
</div>
|
||||||
|
<div style="position: absolute;
|
||||||
|
top: ${bounds.y + bounds.height + 5}px;
|
||||||
|
left: ${bounds.x + bounds.width / 2}px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 6px;
|
||||||
|
background: #808080;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;">Check it out, it is right above this text
|
||||||
|
</div>
|
||||||
|
`, { duration: 2000 });
|
||||||
|
|
||||||
|
await page.screencast.stop();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Embrace creativity, overlays are powerful.
|
||||||
|
|
||||||
|
### Overlay API Summary
|
||||||
|
|
||||||
|
| Method | Use Case |
|
||||||
|
|--------|----------|
|
||||||
|
| `page.screencast.showChapter(title, { description?, duration?, styleSheet? })` | Full-screen chapter card with blurred backdrop — ideal for section transitions |
|
||||||
|
| `page.screencast.showOverlay(html, { duration? })` | Custom HTML overlay — use for callouts, labels, highlights |
|
||||||
|
| `disposable.dispose()` | Remove a sticky overlay added without duration |
|
||||||
|
| `page.screencast.hideOverlays()` / `page.screencast.showOverlays()` | Temporarily hide/show all overlays |
|
||||||
|
|
||||||
|
## Tracing vs Video
|
||||||
|
|
||||||
|
| Feature | Video | Tracing |
|
||||||
|
|---------|-------|---------|
|
||||||
|
| Output | WebM file | Trace file (viewable in Trace Viewer) |
|
||||||
|
| Shows | Visual recording | DOM snapshots, network, console, actions |
|
||||||
|
| Use case | Demos, documentation | Debugging, analysis |
|
||||||
|
| Size | Larger | Smaller |
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Recording adds slight overhead to automation
|
||||||
|
- Large recordings can consume significant disk space
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var command_exports = {};
|
||||||
|
__export(command_exports, {
|
||||||
|
declareCommand: () => declareCommand,
|
||||||
|
parseCommand: () => parseCommand
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(command_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
function declareCommand(command) {
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
const kEmptyOptions = import_zodBundle.z.object({});
|
||||||
|
const kEmptyArgs = import_zodBundle.z.object({});
|
||||||
|
function parseCommand(command, args) {
|
||||||
|
const optionsObject = { ...args };
|
||||||
|
delete optionsObject["_"];
|
||||||
|
const optionsSchema = (command.options ?? kEmptyOptions).strict();
|
||||||
|
const options = zodParse(optionsSchema, optionsObject, "option");
|
||||||
|
const argsSchema = (command.args ?? kEmptyArgs).strict();
|
||||||
|
const argNames = [...Object.keys(argsSchema.shape)];
|
||||||
|
const argv = args["_"].slice(1);
|
||||||
|
if (argv.length > argNames.length)
|
||||||
|
throw new Error(`error: too many arguments: expected ${argNames.length}, received ${argv.length}`);
|
||||||
|
const argsObject = {};
|
||||||
|
argNames.forEach((name, index) => argsObject[name] = argv[index]);
|
||||||
|
const parsedArgsObject = zodParse(argsSchema, argsObject, "argument");
|
||||||
|
const toolName = typeof command.toolName === "function" ? command.toolName({ ...parsedArgsObject, ...options }) : command.toolName;
|
||||||
|
const toolParams = command.toolParams({ ...parsedArgsObject, ...options });
|
||||||
|
return { toolName, toolParams };
|
||||||
|
}
|
||||||
|
function zodParse(schema, data, type) {
|
||||||
|
try {
|
||||||
|
return schema.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e.issues.map((issue) => {
|
||||||
|
const keys = issue.code === "unrecognized_keys" ? issue.keys : [""];
|
||||||
|
const props = keys.map((key) => [...issue.path, key].filter(Boolean).join("."));
|
||||||
|
return props.map((prop) => {
|
||||||
|
const label = type === "option" ? `'--${prop}' option` : `'${prop}' argument`;
|
||||||
|
switch (issue.code) {
|
||||||
|
case "invalid_type":
|
||||||
|
return "error: " + label + ": " + issue.message.replace(/Invalid input:/, "").trim();
|
||||||
|
case "unrecognized_keys":
|
||||||
|
return "error: unknown " + label;
|
||||||
|
default:
|
||||||
|
return "error: " + label + ": " + issue.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).flat().join("\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
declareCommand,
|
||||||
|
parseCommand
|
||||||
|
});
|
||||||
@@ -0,0 +1,956 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var commands_exports = {};
|
||||||
|
__export(commands_exports, {
|
||||||
|
commands: () => commands
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(commands_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_command = require("./command");
|
||||||
|
const numberArg = import_zodBundle.z.preprocess((val, ctx) => {
|
||||||
|
const number = Number(val);
|
||||||
|
if (Number.isNaN(number)) {
|
||||||
|
ctx.issues.push({
|
||||||
|
code: "custom",
|
||||||
|
message: `expected number, received '${val}'`,
|
||||||
|
input: val
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}, import_zodBundle.z.number());
|
||||||
|
function asRef(refOrSelector) {
|
||||||
|
if (refOrSelector === void 0)
|
||||||
|
return {};
|
||||||
|
if (refOrSelector.match(/^(f\d+)?e\d+$/))
|
||||||
|
return { ref: refOrSelector };
|
||||||
|
return { ref: "", selector: refOrSelector };
|
||||||
|
}
|
||||||
|
const open = (0, import_command.declareCommand)({
|
||||||
|
name: "open",
|
||||||
|
description: "Open the browser",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
url: import_zodBundle.z.string().optional().describe("The URL to navigate to")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
browser: import_zodBundle.z.string().optional().describe("Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge."),
|
||||||
|
config: import_zodBundle.z.string().optional().describe("Path to the configuration file, defaults to .playwright/cli.config.json"),
|
||||||
|
extension: import_zodBundle.z.boolean().optional().describe("Connect to browser extension"),
|
||||||
|
headed: import_zodBundle.z.boolean().optional().describe("Run browser in headed mode"),
|
||||||
|
persistent: import_zodBundle.z.boolean().optional().describe("Use persistent browser profile"),
|
||||||
|
profile: import_zodBundle.z.string().optional().describe("Use persistent browser profile, store profile in specified directory.")
|
||||||
|
}),
|
||||||
|
toolName: ({ url }) => url ? "browser_navigate" : "browser_snapshot",
|
||||||
|
toolParams: ({ url }) => url ? { url: url || "about:blank" } : { filename: "<auto>" }
|
||||||
|
});
|
||||||
|
const attach = (0, import_command.declareCommand)({
|
||||||
|
name: "attach",
|
||||||
|
description: "Attach to a running Playwright browser",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
name: import_zodBundle.z.string().describe("Name or endpoint of the browser to attach to")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
config: import_zodBundle.z.string().optional().describe("Path to the configuration file, defaults to .playwright/cli.config.json"),
|
||||||
|
session: import_zodBundle.z.string().optional().describe("Session name alias (defaults to the attach target name)")
|
||||||
|
}),
|
||||||
|
toolName: "browser_snapshot",
|
||||||
|
toolParams: () => ({ filename: "<auto>" })
|
||||||
|
});
|
||||||
|
const close = (0, import_command.declareCommand)({
|
||||||
|
name: "close",
|
||||||
|
description: "Close the browser",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const goto = (0, import_command.declareCommand)({
|
||||||
|
name: "goto",
|
||||||
|
description: "Navigate to a URL",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
url: import_zodBundle.z.string().describe("The URL to navigate to")
|
||||||
|
}),
|
||||||
|
toolName: "browser_navigate",
|
||||||
|
toolParams: ({ url }) => ({ url })
|
||||||
|
});
|
||||||
|
const goBack = (0, import_command.declareCommand)({
|
||||||
|
name: "go-back",
|
||||||
|
description: "Go back to the previous page",
|
||||||
|
category: "navigation",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_navigate_back",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const goForward = (0, import_command.declareCommand)({
|
||||||
|
name: "go-forward",
|
||||||
|
description: "Go forward to the next page",
|
||||||
|
category: "navigation",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_navigate_forward",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const reload = (0, import_command.declareCommand)({
|
||||||
|
name: "reload",
|
||||||
|
description: "Reload the current page",
|
||||||
|
category: "navigation",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_reload",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const pressKey = (0, import_command.declareCommand)({
|
||||||
|
name: "press",
|
||||||
|
description: "Press a key on the keyboard, `a`, `ArrowLeft`",
|
||||||
|
category: "keyboard",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
|
||||||
|
}),
|
||||||
|
toolName: "browser_press_key",
|
||||||
|
toolParams: ({ key }) => ({ key })
|
||||||
|
});
|
||||||
|
const type = (0, import_command.declareCommand)({
|
||||||
|
name: "type",
|
||||||
|
description: "Type text into editable element",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
text: import_zodBundle.z.string().describe("Text to type into the element")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
submit: import_zodBundle.z.boolean().optional().describe("Whether to submit entered text (press Enter after)")
|
||||||
|
}),
|
||||||
|
toolName: "browser_press_sequentially",
|
||||||
|
toolParams: ({ text, submit }) => ({ text, submit })
|
||||||
|
});
|
||||||
|
const keydown = (0, import_command.declareCommand)({
|
||||||
|
name: "keydown",
|
||||||
|
description: "Press a key down on the keyboard",
|
||||||
|
category: "keyboard",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
|
||||||
|
}),
|
||||||
|
toolName: "browser_keydown",
|
||||||
|
toolParams: ({ key }) => ({ key })
|
||||||
|
});
|
||||||
|
const keyup = (0, import_command.declareCommand)({
|
||||||
|
name: "keyup",
|
||||||
|
description: "Press a key up on the keyboard",
|
||||||
|
category: "keyboard",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
|
||||||
|
}),
|
||||||
|
toolName: "browser_keyup",
|
||||||
|
toolParams: ({ key }) => ({ key })
|
||||||
|
});
|
||||||
|
const mouseMove = (0, import_command.declareCommand)({
|
||||||
|
name: "mousemove",
|
||||||
|
description: "Move mouse to a given position",
|
||||||
|
category: "mouse",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
x: numberArg.describe("X coordinate"),
|
||||||
|
y: numberArg.describe("Y coordinate")
|
||||||
|
}),
|
||||||
|
toolName: "browser_mouse_move_xy",
|
||||||
|
toolParams: ({ x, y }) => ({ x, y })
|
||||||
|
});
|
||||||
|
const mouseDown = (0, import_command.declareCommand)({
|
||||||
|
name: "mousedown",
|
||||||
|
description: "Press mouse down",
|
||||||
|
category: "mouse",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
button: import_zodBundle.z.string().optional().describe("Button to press, defaults to left")
|
||||||
|
}),
|
||||||
|
toolName: "browser_mouse_down",
|
||||||
|
toolParams: ({ button }) => ({ button })
|
||||||
|
});
|
||||||
|
const mouseUp = (0, import_command.declareCommand)({
|
||||||
|
name: "mouseup",
|
||||||
|
description: "Press mouse up",
|
||||||
|
category: "mouse",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
button: import_zodBundle.z.string().optional().describe("Button to press, defaults to left")
|
||||||
|
}),
|
||||||
|
toolName: "browser_mouse_up",
|
||||||
|
toolParams: ({ button }) => ({ button })
|
||||||
|
});
|
||||||
|
const mouseWheel = (0, import_command.declareCommand)({
|
||||||
|
name: "mousewheel",
|
||||||
|
description: "Scroll mouse wheel",
|
||||||
|
category: "mouse",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
dx: numberArg.describe("X delta"),
|
||||||
|
dy: numberArg.describe("Y delta")
|
||||||
|
}),
|
||||||
|
toolName: "browser_mouse_wheel",
|
||||||
|
toolParams: ({ dx: deltaX, dy: deltaY }) => ({ deltaX, deltaY })
|
||||||
|
});
|
||||||
|
const click = (0, import_command.declareCommand)({
|
||||||
|
name: "click",
|
||||||
|
description: "Perform click on a web page",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
target: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
|
||||||
|
button: import_zodBundle.z.string().optional().describe("Button to click, defaults to left")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
modifiers: import_zodBundle.z.array(import_zodBundle.z.string()).optional().describe("Modifier keys to press")
|
||||||
|
}),
|
||||||
|
toolName: "browser_click",
|
||||||
|
toolParams: ({ target, button, modifiers }) => ({ ...asRef(target), button, modifiers })
|
||||||
|
});
|
||||||
|
const doubleClick = (0, import_command.declareCommand)({
|
||||||
|
name: "dblclick",
|
||||||
|
description: "Perform double click on a web page",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
target: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
|
||||||
|
button: import_zodBundle.z.string().optional().describe("Button to click, defaults to left")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
modifiers: import_zodBundle.z.array(import_zodBundle.z.string()).optional().describe("Modifier keys to press")
|
||||||
|
}),
|
||||||
|
toolName: "browser_click",
|
||||||
|
toolParams: ({ target, button, modifiers }) => ({ ...asRef(target), button, modifiers, doubleClick: true })
|
||||||
|
});
|
||||||
|
const drag = (0, import_command.declareCommand)({
|
||||||
|
name: "drag",
|
||||||
|
description: "Perform drag and drop between two elements",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
startElement: import_zodBundle.z.string().describe("Exact source element reference from the page snapshot, or a unique element selector"),
|
||||||
|
endElement: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot, or a unique element selector")
|
||||||
|
}),
|
||||||
|
toolName: "browser_drag",
|
||||||
|
toolParams: ({ startElement, endElement }) => {
|
||||||
|
const start = asRef(startElement);
|
||||||
|
const end = asRef(endElement);
|
||||||
|
return { startRef: start.ref, startSelector: start.selector, endRef: end.ref, endSelector: end.selector };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const fill = (0, import_command.declareCommand)({
|
||||||
|
name: "fill",
|
||||||
|
description: "Fill text into editable element",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
target: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
|
||||||
|
text: import_zodBundle.z.string().describe("Text to fill into the element")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
submit: import_zodBundle.z.boolean().optional().describe("Whether to submit entered text (press Enter after)")
|
||||||
|
}),
|
||||||
|
toolName: "browser_type",
|
||||||
|
toolParams: ({ target, text, submit }) => ({ ...asRef(target), text, submit })
|
||||||
|
});
|
||||||
|
const hover = (0, import_command.declareCommand)({
|
||||||
|
name: "hover",
|
||||||
|
description: "Hover over element on page",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
target: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot, or a unique element selector")
|
||||||
|
}),
|
||||||
|
toolName: "browser_hover",
|
||||||
|
toolParams: ({ target }) => ({ ...asRef(target) })
|
||||||
|
});
|
||||||
|
const select = (0, import_command.declareCommand)({
|
||||||
|
name: "select",
|
||||||
|
description: "Select an option in a dropdown",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
target: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
|
||||||
|
val: import_zodBundle.z.string().describe("Value to select in the dropdown")
|
||||||
|
}),
|
||||||
|
toolName: "browser_select_option",
|
||||||
|
toolParams: ({ target, val: value }) => ({ ...asRef(target), values: [value] })
|
||||||
|
});
|
||||||
|
const fileUpload = (0, import_command.declareCommand)({
|
||||||
|
name: "upload",
|
||||||
|
description: "Upload one or multiple files",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
file: import_zodBundle.z.string().describe("The absolute paths to the files to upload")
|
||||||
|
}),
|
||||||
|
toolName: "browser_file_upload",
|
||||||
|
toolParams: ({ file }) => ({ paths: [file] })
|
||||||
|
});
|
||||||
|
const check = (0, import_command.declareCommand)({
|
||||||
|
name: "check",
|
||||||
|
description: "Check a checkbox or radio button",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
target: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot, or a unique element selector")
|
||||||
|
}),
|
||||||
|
toolName: "browser_check",
|
||||||
|
toolParams: ({ target }) => ({ ...asRef(target) })
|
||||||
|
});
|
||||||
|
const uncheck = (0, import_command.declareCommand)({
|
||||||
|
name: "uncheck",
|
||||||
|
description: "Uncheck a checkbox or radio button",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
target: import_zodBundle.z.string().describe("Exact target element reference from the page snapshot, or a unique element selector")
|
||||||
|
}),
|
||||||
|
toolName: "browser_uncheck",
|
||||||
|
toolParams: ({ target }) => ({ ...asRef(target) })
|
||||||
|
});
|
||||||
|
const snapshot = (0, import_command.declareCommand)({
|
||||||
|
name: "snapshot",
|
||||||
|
description: "Capture page snapshot to obtain element ref",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
element: import_zodBundle.z.string().optional().describe("Element selector of the root element to capture a partial snapshot instead of the whole page")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("Save snapshot to markdown file instead of returning it in the response."),
|
||||||
|
depth: numberArg.optional().describe("Limit snapshot depth, unlimited by default.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_snapshot",
|
||||||
|
toolParams: ({ filename, element, depth }) => ({ filename, selector: element, depth })
|
||||||
|
});
|
||||||
|
const evaluate = (0, import_command.declareCommand)({
|
||||||
|
name: "eval",
|
||||||
|
description: "Evaluate JavaScript expression on page or element",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
func: import_zodBundle.z.string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
|
||||||
|
element: import_zodBundle.z.string().optional().describe("Exact target element reference from the page snapshot, or a unique element selector")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("Save evaluation result to a file instead of returning it in the response.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_evaluate",
|
||||||
|
toolParams: ({ func, element, filename }) => ({ function: func, filename, ...asRef(element) })
|
||||||
|
});
|
||||||
|
const dialogAccept = (0, import_command.declareCommand)({
|
||||||
|
name: "dialog-accept",
|
||||||
|
description: "Accept a dialog",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
prompt: import_zodBundle.z.string().optional().describe("The text of the prompt in case of a prompt dialog.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_handle_dialog",
|
||||||
|
toolParams: ({ prompt: promptText }) => ({ accept: true, promptText })
|
||||||
|
});
|
||||||
|
const dialogDismiss = (0, import_command.declareCommand)({
|
||||||
|
name: "dialog-dismiss",
|
||||||
|
description: "Dismiss a dialog",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_handle_dialog",
|
||||||
|
toolParams: () => ({ accept: false })
|
||||||
|
});
|
||||||
|
const resize = (0, import_command.declareCommand)({
|
||||||
|
name: "resize",
|
||||||
|
description: "Resize the browser window",
|
||||||
|
category: "core",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
w: numberArg.describe("Width of the browser window"),
|
||||||
|
h: numberArg.describe("Height of the browser window")
|
||||||
|
}),
|
||||||
|
toolName: "browser_resize",
|
||||||
|
toolParams: ({ w: width, h: height }) => ({ width, height })
|
||||||
|
});
|
||||||
|
const runCode = (0, import_command.declareCommand)({
|
||||||
|
name: "run-code",
|
||||||
|
description: "Run Playwright code snippet",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
code: import_zodBundle.z.string().optional().describe("A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction.")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("Load code from the specified file.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_run_code",
|
||||||
|
toolParams: ({ code, filename }) => ({ code, filename })
|
||||||
|
});
|
||||||
|
const tabList = (0, import_command.declareCommand)({
|
||||||
|
name: "tab-list",
|
||||||
|
description: "List all tabs",
|
||||||
|
category: "tabs",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_tabs",
|
||||||
|
toolParams: () => ({ action: "list" })
|
||||||
|
});
|
||||||
|
const tabNew = (0, import_command.declareCommand)({
|
||||||
|
name: "tab-new",
|
||||||
|
description: "Create a new tab",
|
||||||
|
category: "tabs",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
url: import_zodBundle.z.string().optional().describe("The URL to navigate to in the new tab. If omitted, the new tab will be blank.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_tabs",
|
||||||
|
toolParams: ({ url }) => ({ action: "new", url })
|
||||||
|
});
|
||||||
|
const tabClose = (0, import_command.declareCommand)({
|
||||||
|
name: "tab-close",
|
||||||
|
description: "Close a browser tab",
|
||||||
|
category: "tabs",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
index: numberArg.optional().describe("Tab index. If omitted, current tab is closed.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_tabs",
|
||||||
|
toolParams: ({ index }) => ({ action: "close", index })
|
||||||
|
});
|
||||||
|
const tabSelect = (0, import_command.declareCommand)({
|
||||||
|
name: "tab-select",
|
||||||
|
description: "Select a browser tab",
|
||||||
|
category: "tabs",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
index: numberArg.describe("Tab index")
|
||||||
|
}),
|
||||||
|
toolName: "browser_tabs",
|
||||||
|
toolParams: ({ index }) => ({ action: "select", index })
|
||||||
|
});
|
||||||
|
const stateLoad = (0, import_command.declareCommand)({
|
||||||
|
name: "state-load",
|
||||||
|
description: "Loads browser storage (authentication) state from a file",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().describe("File name to load the storage state from.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_set_storage_state",
|
||||||
|
toolParams: ({ filename }) => ({ filename })
|
||||||
|
});
|
||||||
|
const stateSave = (0, import_command.declareCommand)({
|
||||||
|
name: "state-save",
|
||||||
|
description: "Saves the current storage (authentication) state to a file",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("File name to save the storage state to.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_storage_state",
|
||||||
|
toolParams: ({ filename }) => ({ filename })
|
||||||
|
});
|
||||||
|
const cookieList = (0, import_command.declareCommand)({
|
||||||
|
name: "cookie-list",
|
||||||
|
description: "List all cookies (optionally filtered by domain/path)",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
domain: import_zodBundle.z.string().optional().describe("Filter cookies by domain"),
|
||||||
|
path: import_zodBundle.z.string().optional().describe("Filter cookies by path")
|
||||||
|
}),
|
||||||
|
toolName: "browser_cookie_list",
|
||||||
|
toolParams: ({ domain, path }) => ({ domain, path })
|
||||||
|
});
|
||||||
|
const cookieGet = (0, import_command.declareCommand)({
|
||||||
|
name: "cookie-get",
|
||||||
|
description: "Get a specific cookie by name",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
name: import_zodBundle.z.string().describe("Cookie name")
|
||||||
|
}),
|
||||||
|
toolName: "browser_cookie_get",
|
||||||
|
toolParams: ({ name }) => ({ name })
|
||||||
|
});
|
||||||
|
const cookieSet = (0, import_command.declareCommand)({
|
||||||
|
name: "cookie-set",
|
||||||
|
description: "Set a cookie with optional flags",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
name: import_zodBundle.z.string().describe("Cookie name"),
|
||||||
|
value: import_zodBundle.z.string().describe("Cookie value")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
domain: import_zodBundle.z.string().optional().describe("Cookie domain"),
|
||||||
|
path: import_zodBundle.z.string().optional().describe("Cookie path"),
|
||||||
|
expires: numberArg.optional().describe("Cookie expiration as Unix timestamp"),
|
||||||
|
httpOnly: import_zodBundle.z.boolean().optional().describe("Whether the cookie is HTTP only"),
|
||||||
|
secure: import_zodBundle.z.boolean().optional().describe("Whether the cookie is secure"),
|
||||||
|
sameSite: import_zodBundle.z.enum(["Strict", "Lax", "None"]).optional().describe("Cookie SameSite attribute")
|
||||||
|
}),
|
||||||
|
toolName: "browser_cookie_set",
|
||||||
|
toolParams: ({ name, value, domain, path, expires, httpOnly, secure, sameSite }) => ({ name, value, domain, path, expires, httpOnly, secure, sameSite })
|
||||||
|
});
|
||||||
|
const cookieDelete = (0, import_command.declareCommand)({
|
||||||
|
name: "cookie-delete",
|
||||||
|
description: "Delete a specific cookie",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
name: import_zodBundle.z.string().describe("Cookie name")
|
||||||
|
}),
|
||||||
|
toolName: "browser_cookie_delete",
|
||||||
|
toolParams: ({ name }) => ({ name })
|
||||||
|
});
|
||||||
|
const cookieClear = (0, import_command.declareCommand)({
|
||||||
|
name: "cookie-clear",
|
||||||
|
description: "Clear all cookies",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_cookie_clear",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const localStorageList = (0, import_command.declareCommand)({
|
||||||
|
name: "localstorage-list",
|
||||||
|
description: "List all localStorage key-value pairs",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_localstorage_list",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const localStorageGet = (0, import_command.declareCommand)({
|
||||||
|
name: "localstorage-get",
|
||||||
|
description: "Get a localStorage item by key",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to get")
|
||||||
|
}),
|
||||||
|
toolName: "browser_localstorage_get",
|
||||||
|
toolParams: ({ key }) => ({ key })
|
||||||
|
});
|
||||||
|
const localStorageSet = (0, import_command.declareCommand)({
|
||||||
|
name: "localstorage-set",
|
||||||
|
description: "Set a localStorage item",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to set"),
|
||||||
|
value: import_zodBundle.z.string().describe("Value to set")
|
||||||
|
}),
|
||||||
|
toolName: "browser_localstorage_set",
|
||||||
|
toolParams: ({ key, value }) => ({ key, value })
|
||||||
|
});
|
||||||
|
const localStorageDelete = (0, import_command.declareCommand)({
|
||||||
|
name: "localstorage-delete",
|
||||||
|
description: "Delete a localStorage item",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to delete")
|
||||||
|
}),
|
||||||
|
toolName: "browser_localstorage_delete",
|
||||||
|
toolParams: ({ key }) => ({ key })
|
||||||
|
});
|
||||||
|
const localStorageClear = (0, import_command.declareCommand)({
|
||||||
|
name: "localstorage-clear",
|
||||||
|
description: "Clear all localStorage",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_localstorage_clear",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const sessionStorageList = (0, import_command.declareCommand)({
|
||||||
|
name: "sessionstorage-list",
|
||||||
|
description: "List all sessionStorage key-value pairs",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_sessionstorage_list",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const sessionStorageGet = (0, import_command.declareCommand)({
|
||||||
|
name: "sessionstorage-get",
|
||||||
|
description: "Get a sessionStorage item by key",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to get")
|
||||||
|
}),
|
||||||
|
toolName: "browser_sessionstorage_get",
|
||||||
|
toolParams: ({ key }) => ({ key })
|
||||||
|
});
|
||||||
|
const sessionStorageSet = (0, import_command.declareCommand)({
|
||||||
|
name: "sessionstorage-set",
|
||||||
|
description: "Set a sessionStorage item",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to set"),
|
||||||
|
value: import_zodBundle.z.string().describe("Value to set")
|
||||||
|
}),
|
||||||
|
toolName: "browser_sessionstorage_set",
|
||||||
|
toolParams: ({ key, value }) => ({ key, value })
|
||||||
|
});
|
||||||
|
const sessionStorageDelete = (0, import_command.declareCommand)({
|
||||||
|
name: "sessionstorage-delete",
|
||||||
|
description: "Delete a sessionStorage item",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
key: import_zodBundle.z.string().describe("Key to delete")
|
||||||
|
}),
|
||||||
|
toolName: "browser_sessionstorage_delete",
|
||||||
|
toolParams: ({ key }) => ({ key })
|
||||||
|
});
|
||||||
|
const sessionStorageClear = (0, import_command.declareCommand)({
|
||||||
|
name: "sessionstorage-clear",
|
||||||
|
description: "Clear all sessionStorage",
|
||||||
|
category: "storage",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_sessionstorage_clear",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const routeMock = (0, import_command.declareCommand)({
|
||||||
|
name: "route",
|
||||||
|
description: "Mock network requests matching a URL pattern",
|
||||||
|
category: "network",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
pattern: import_zodBundle.z.string().describe('URL pattern to match (e.g., "**/api/users")')
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
status: numberArg.optional().describe("HTTP status code (default: 200)"),
|
||||||
|
body: import_zodBundle.z.string().optional().describe("Response body (text or JSON string)"),
|
||||||
|
["content-type"]: import_zodBundle.z.string().optional().describe("Content-Type header"),
|
||||||
|
header: import_zodBundle.z.union([import_zodBundle.z.string(), import_zodBundle.z.array(import_zodBundle.z.string())]).optional().transform((v) => v ? Array.isArray(v) ? v : [v] : void 0).describe('Header to add in "Name: Value" format (repeatable)'),
|
||||||
|
["remove-header"]: import_zodBundle.z.string().optional().describe("Comma-separated header names to remove")
|
||||||
|
}),
|
||||||
|
toolName: "browser_route",
|
||||||
|
toolParams: ({ pattern, status, body, ["content-type"]: contentType, header: headers, ["remove-header"]: removeHeaders }) => ({
|
||||||
|
pattern,
|
||||||
|
status,
|
||||||
|
body,
|
||||||
|
contentType,
|
||||||
|
headers,
|
||||||
|
removeHeaders
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const routeList = (0, import_command.declareCommand)({
|
||||||
|
name: "route-list",
|
||||||
|
description: "List all active network routes",
|
||||||
|
category: "network",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_route_list",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const unroute = (0, import_command.declareCommand)({
|
||||||
|
name: "unroute",
|
||||||
|
description: "Remove routes matching a pattern (or all routes)",
|
||||||
|
category: "network",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
pattern: import_zodBundle.z.string().optional().describe("URL pattern to unroute (omit to remove all)")
|
||||||
|
}),
|
||||||
|
toolName: "browser_unroute",
|
||||||
|
toolParams: ({ pattern }) => ({ pattern })
|
||||||
|
});
|
||||||
|
const networkStateSet = (0, import_command.declareCommand)({
|
||||||
|
name: "network-state-set",
|
||||||
|
description: "Set the browser network state to online or offline",
|
||||||
|
category: "network",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
state: import_zodBundle.z.enum(["online", "offline"]).describe('Set to "offline" to simulate offline mode, "online" to restore network connectivity')
|
||||||
|
}),
|
||||||
|
toolName: "browser_network_state_set",
|
||||||
|
toolParams: ({ state }) => ({ state })
|
||||||
|
});
|
||||||
|
const screenshot = (0, import_command.declareCommand)({
|
||||||
|
name: "screenshot",
|
||||||
|
description: "screenshot of the current page or element",
|
||||||
|
category: "export",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
target: import_zodBundle.z.string().optional().describe("Exact target element reference from the page snapshot, or a unique element selector.")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified."),
|
||||||
|
["full-page"]: import_zodBundle.z.boolean().optional().describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_take_screenshot",
|
||||||
|
toolParams: ({ target, filename, ["full-page"]: fullPage }) => ({ filename, ...asRef(target), fullPage })
|
||||||
|
});
|
||||||
|
const pdfSave = (0, import_command.declareCommand)({
|
||||||
|
name: "pdf",
|
||||||
|
description: "Save page as PDF",
|
||||||
|
category: "export",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_pdf_save",
|
||||||
|
toolParams: ({ filename }) => ({ filename })
|
||||||
|
});
|
||||||
|
const consoleList = (0, import_command.declareCommand)({
|
||||||
|
name: "console",
|
||||||
|
description: "List console messages",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
["min-level"]: import_zodBundle.z.string().optional().describe('Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info".')
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
clear: import_zodBundle.z.boolean().optional().describe("Whether to clear the console list")
|
||||||
|
}),
|
||||||
|
toolName: ({ clear }) => clear ? "browser_console_clear" : "browser_console_messages",
|
||||||
|
toolParams: ({ ["min-level"]: level, clear }) => clear ? {} : { level }
|
||||||
|
});
|
||||||
|
const networkRequests = (0, import_command.declareCommand)({
|
||||||
|
name: "network",
|
||||||
|
description: "List all network requests since loading the page",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
static: import_zodBundle.z.boolean().optional().describe("Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false."),
|
||||||
|
["request-body"]: import_zodBundle.z.boolean().optional().describe("Whether to include request body. Defaults to false."),
|
||||||
|
["request-headers"]: import_zodBundle.z.boolean().optional().describe("Whether to include request headers. Defaults to false."),
|
||||||
|
filter: import_zodBundle.z.string().optional().describe('Only return requests whose URL matches this regexp (e.g. "/api/.*user").'),
|
||||||
|
clear: import_zodBundle.z.boolean().optional().describe("Whether to clear the network list")
|
||||||
|
}),
|
||||||
|
toolName: ({ clear }) => clear ? "browser_network_clear" : "browser_network_requests",
|
||||||
|
toolParams: ({ static: s, "request-body": requestBody, "request-headers": requestHeaders, filter, clear }) => clear ? {} : { static: s, requestBody, requestHeaders, filter }
|
||||||
|
});
|
||||||
|
const tracingStart = (0, import_command.declareCommand)({
|
||||||
|
name: "tracing-start",
|
||||||
|
description: "Start trace recording",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_start_tracing",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const tracingStop = (0, import_command.declareCommand)({
|
||||||
|
name: "tracing-stop",
|
||||||
|
description: "Stop trace recording",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_stop_tracing",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const videoStart = (0, import_command.declareCommand)({
|
||||||
|
name: "video-start",
|
||||||
|
description: "Start video recording",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
filename: import_zodBundle.z.string().optional().describe("Filename to save the video.")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
size: import_zodBundle.z.string().optional().describe('Video frame size, e.g. "800x600". If not specified, the size of the recorded video will fit 800x800.')
|
||||||
|
}),
|
||||||
|
toolName: "browser_start_video",
|
||||||
|
toolParams: ({ filename, size }) => {
|
||||||
|
const parsedSize = size ? size.split("x").map(Number) : void 0;
|
||||||
|
return { filename, size: parsedSize ? { width: parsedSize[0], height: parsedSize[1] } : void 0 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const videoStop = (0, import_command.declareCommand)({
|
||||||
|
name: "video-stop",
|
||||||
|
description: "Stop video recording",
|
||||||
|
category: "devtools",
|
||||||
|
toolName: "browser_stop_video",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const videoChapter = (0, import_command.declareCommand)({
|
||||||
|
name: "video-chapter",
|
||||||
|
description: "Add a chapter marker to the video recording",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
title: import_zodBundle.z.string().describe("Chapter title.")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
description: import_zodBundle.z.string().optional().describe("Chapter description."),
|
||||||
|
duration: numberArg.optional().describe("Duration in milliseconds to show the chapter card.")
|
||||||
|
}),
|
||||||
|
toolName: "browser_video_chapter",
|
||||||
|
toolParams: ({ title, description, duration }) => ({ title, description, duration })
|
||||||
|
});
|
||||||
|
const devtoolsShow = (0, import_command.declareCommand)({
|
||||||
|
name: "show",
|
||||||
|
description: "Show browser DevTools",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const resume = (0, import_command.declareCommand)({
|
||||||
|
name: "resume",
|
||||||
|
description: "Resume the test execution",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_resume",
|
||||||
|
toolParams: ({ step }) => ({ step })
|
||||||
|
});
|
||||||
|
const stepOver = (0, import_command.declareCommand)({
|
||||||
|
name: "step-over",
|
||||||
|
description: "Step over the next call in the test",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
toolName: "browser_resume",
|
||||||
|
toolParams: ({}) => ({ step: true })
|
||||||
|
});
|
||||||
|
const pauseAt = (0, import_command.declareCommand)({
|
||||||
|
name: "pause-at",
|
||||||
|
description: "Run the test up to a specific location and pause there",
|
||||||
|
category: "devtools",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
location: import_zodBundle.z.string().describe('Location to pause at. Format is <file>:<line>, e.g. "example.spec.ts:42".')
|
||||||
|
}),
|
||||||
|
toolName: "browser_resume",
|
||||||
|
toolParams: ({ location }) => ({ location })
|
||||||
|
});
|
||||||
|
const sessionList = (0, import_command.declareCommand)({
|
||||||
|
name: "list",
|
||||||
|
description: "List browser sessions",
|
||||||
|
category: "browsers",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
all: import_zodBundle.z.boolean().optional().describe("List all browser sessions across all workspaces")
|
||||||
|
}),
|
||||||
|
toolName: "",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const sessionCloseAll = (0, import_command.declareCommand)({
|
||||||
|
name: "close-all",
|
||||||
|
description: "Close all browser sessions",
|
||||||
|
category: "browsers",
|
||||||
|
toolName: "",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const killAll = (0, import_command.declareCommand)({
|
||||||
|
name: "kill-all",
|
||||||
|
description: "Forcefully kill all browser sessions (for stale/zombie processes)",
|
||||||
|
category: "browsers",
|
||||||
|
toolName: "",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const deleteData = (0, import_command.declareCommand)({
|
||||||
|
name: "delete-data",
|
||||||
|
description: "Delete session data",
|
||||||
|
category: "core",
|
||||||
|
toolName: "",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const configPrint = (0, import_command.declareCommand)({
|
||||||
|
name: "config-print",
|
||||||
|
description: "Print the final resolved config after merging CLI options, environment variables and config file.",
|
||||||
|
category: "config",
|
||||||
|
hidden: true,
|
||||||
|
toolName: "browser_get_config",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const install = (0, import_command.declareCommand)({
|
||||||
|
name: "install",
|
||||||
|
description: "Initialize workspace",
|
||||||
|
category: "install",
|
||||||
|
args: import_zodBundle.z.object({}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
skills: import_zodBundle.z.string().optional().describe('Install skills to ".claude" (default) or ".agents" dir')
|
||||||
|
}),
|
||||||
|
toolName: "",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const installBrowser = (0, import_command.declareCommand)({
|
||||||
|
name: "install-browser",
|
||||||
|
description: "Install browser",
|
||||||
|
category: "install",
|
||||||
|
args: import_zodBundle.z.object({
|
||||||
|
browser: import_zodBundle.z.string().optional().describe("Browser to install")
|
||||||
|
}),
|
||||||
|
options: import_zodBundle.z.object({
|
||||||
|
["with-deps"]: import_zodBundle.z.boolean().optional().describe("Install system dependencies for browsers"),
|
||||||
|
["dry-run"]: import_zodBundle.z.boolean().optional().describe("Do not execute installation, only print information"),
|
||||||
|
list: import_zodBundle.z.boolean().optional().describe("Prints list of browsers from all Playwright installations"),
|
||||||
|
force: import_zodBundle.z.boolean().optional().describe("Force reinstall of already installed browsers"),
|
||||||
|
["only-shell"]: import_zodBundle.z.boolean().optional().describe("Only install headless shell when installing Chromium"),
|
||||||
|
["no-shell"]: import_zodBundle.z.boolean().optional().describe("Do not install Chromium headless shell")
|
||||||
|
}),
|
||||||
|
toolName: "",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const tray = (0, import_command.declareCommand)({
|
||||||
|
name: "tray",
|
||||||
|
description: "Run tray",
|
||||||
|
category: "config",
|
||||||
|
hidden: true,
|
||||||
|
toolName: "",
|
||||||
|
toolParams: () => ({})
|
||||||
|
});
|
||||||
|
const commandsArray = [
|
||||||
|
// core category
|
||||||
|
open,
|
||||||
|
attach,
|
||||||
|
close,
|
||||||
|
goto,
|
||||||
|
type,
|
||||||
|
click,
|
||||||
|
doubleClick,
|
||||||
|
fill,
|
||||||
|
drag,
|
||||||
|
hover,
|
||||||
|
select,
|
||||||
|
fileUpload,
|
||||||
|
check,
|
||||||
|
uncheck,
|
||||||
|
snapshot,
|
||||||
|
evaluate,
|
||||||
|
consoleList,
|
||||||
|
dialogAccept,
|
||||||
|
dialogDismiss,
|
||||||
|
resize,
|
||||||
|
runCode,
|
||||||
|
deleteData,
|
||||||
|
// navigation category
|
||||||
|
goBack,
|
||||||
|
goForward,
|
||||||
|
reload,
|
||||||
|
// keyboard category
|
||||||
|
pressKey,
|
||||||
|
keydown,
|
||||||
|
keyup,
|
||||||
|
// mouse category
|
||||||
|
mouseMove,
|
||||||
|
mouseDown,
|
||||||
|
mouseUp,
|
||||||
|
mouseWheel,
|
||||||
|
// export category
|
||||||
|
screenshot,
|
||||||
|
pdfSave,
|
||||||
|
// tabs category
|
||||||
|
tabList,
|
||||||
|
tabNew,
|
||||||
|
tabClose,
|
||||||
|
tabSelect,
|
||||||
|
// storage category
|
||||||
|
stateLoad,
|
||||||
|
stateSave,
|
||||||
|
cookieList,
|
||||||
|
cookieGet,
|
||||||
|
cookieSet,
|
||||||
|
cookieDelete,
|
||||||
|
cookieClear,
|
||||||
|
localStorageList,
|
||||||
|
localStorageGet,
|
||||||
|
localStorageSet,
|
||||||
|
localStorageDelete,
|
||||||
|
localStorageClear,
|
||||||
|
sessionStorageList,
|
||||||
|
sessionStorageGet,
|
||||||
|
sessionStorageSet,
|
||||||
|
sessionStorageDelete,
|
||||||
|
sessionStorageClear,
|
||||||
|
// network category
|
||||||
|
routeMock,
|
||||||
|
routeList,
|
||||||
|
unroute,
|
||||||
|
networkStateSet,
|
||||||
|
// config category
|
||||||
|
configPrint,
|
||||||
|
// install category
|
||||||
|
install,
|
||||||
|
installBrowser,
|
||||||
|
// devtools category
|
||||||
|
networkRequests,
|
||||||
|
tracingStart,
|
||||||
|
tracingStop,
|
||||||
|
videoStart,
|
||||||
|
videoStop,
|
||||||
|
videoChapter,
|
||||||
|
devtoolsShow,
|
||||||
|
pauseAt,
|
||||||
|
resume,
|
||||||
|
stepOver,
|
||||||
|
// session category
|
||||||
|
sessionList,
|
||||||
|
sessionCloseAll,
|
||||||
|
killAll,
|
||||||
|
// Hidden commands
|
||||||
|
tray
|
||||||
|
];
|
||||||
|
const commands = Object.fromEntries(commandsArray.map((cmd) => [cmd.name, cmd]));
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
commands
|
||||||
|
});
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var daemon_exports = {};
|
||||||
|
__export(daemon_exports, {
|
||||||
|
startCliDaemonServer: () => startCliDaemonServer
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(daemon_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_net = __toESM(require("net"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_network = require("../../server/utils/network");
|
||||||
|
var import_fileUtils = require("../../server/utils/fileUtils");
|
||||||
|
var import_processLauncher = require("../../server/utils/processLauncher");
|
||||||
|
var import_browserBackend = require("../backend/browserBackend");
|
||||||
|
var import_tools = require("../backend/tools");
|
||||||
|
var import_command = require("./command");
|
||||||
|
var import_commands = require("./commands");
|
||||||
|
var import_socketConnection = require("../utils/socketConnection");
|
||||||
|
var import_registry = require("../cli-client/registry");
|
||||||
|
async function socketExists(socketPath) {
|
||||||
|
try {
|
||||||
|
const stat = await import_fs.default.promises.stat(socketPath);
|
||||||
|
if (stat?.isSocket())
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
async function startCliDaemonServer(sessionName, browserContext, browserInfo, contextConfig = {}, clientInfo = (0, import_registry.createClientInfo)(), options) {
|
||||||
|
const sessionConfig = createSessionConfig(clientInfo, sessionName, browserInfo, options);
|
||||||
|
const { socketPath } = sessionConfig;
|
||||||
|
if (process.platform !== "win32" && await socketExists(socketPath)) {
|
||||||
|
try {
|
||||||
|
await import_fs.default.promises.unlink(socketPath);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const backend = new import_browserBackend.BrowserBackend(contextConfig, browserContext, import_tools.browserTools);
|
||||||
|
await backend.initialize({ cwd: process.cwd() });
|
||||||
|
if (browserContext.isClosed())
|
||||||
|
throw new Error("Browser context was closed before the daemon could start");
|
||||||
|
const server = import_net.default.createServer((socket) => {
|
||||||
|
const connection = new import_socketConnection.SocketConnection(socket);
|
||||||
|
connection.onmessage = async (message) => {
|
||||||
|
const { id, method, params } = message;
|
||||||
|
try {
|
||||||
|
if (method === "stop") {
|
||||||
|
await deleteSessionFile(clientInfo, sessionConfig);
|
||||||
|
const sendAck = async () => connection.send({ id, result: "ok" }).catch(() => {
|
||||||
|
});
|
||||||
|
if (options?.exitOnClose)
|
||||||
|
(0, import_processLauncher.gracefullyProcessExitDoNotHang)(0, () => sendAck());
|
||||||
|
else
|
||||||
|
await sendAck();
|
||||||
|
} else if (method === "run") {
|
||||||
|
const { toolName, toolParams } = parseCliCommand(params.args);
|
||||||
|
if (params.cwd)
|
||||||
|
toolParams._meta = { cwd: params.cwd };
|
||||||
|
const response = await backend.callTool(toolName, toolParams);
|
||||||
|
await connection.send({ id, result: formatResult(response) });
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown method: ${method}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const error = process.env.PWDEBUGIMPL ? e.stack || e.message : e.message;
|
||||||
|
connection.send({ id, error }).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
(0, import_network.decorateServer)(server);
|
||||||
|
browserContext.on("close", () => Promise.resolve().then(async () => {
|
||||||
|
await deleteSessionFile(clientInfo, sessionConfig);
|
||||||
|
if (options?.exitOnClose)
|
||||||
|
(0, import_processLauncher.gracefullyProcessExitDoNotHang)(0);
|
||||||
|
}));
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
server.on("error", reject);
|
||||||
|
server.listen(socketPath, () => resolve());
|
||||||
|
});
|
||||||
|
await saveSessionFile(clientInfo, sessionConfig);
|
||||||
|
return socketPath;
|
||||||
|
}
|
||||||
|
async function saveSessionFile(clientInfo, sessionConfig) {
|
||||||
|
await import_fs.default.promises.mkdir(clientInfo.daemonProfilesDir, { recursive: true });
|
||||||
|
const sessionFile = import_path.default.join(clientInfo.daemonProfilesDir, `${sessionConfig.name}.session`);
|
||||||
|
await import_fs.default.promises.writeFile(sessionFile, JSON.stringify(sessionConfig, null, 2));
|
||||||
|
}
|
||||||
|
async function deleteSessionFile(clientInfo, sessionConfig) {
|
||||||
|
await import_fs.default.promises.unlink(sessionConfig.socketPath).catch(() => {
|
||||||
|
});
|
||||||
|
if (!sessionConfig.cli.persistent) {
|
||||||
|
const sessionFile = import_path.default.join(clientInfo.daemonProfilesDir, `${sessionConfig.name}.session`);
|
||||||
|
await import_fs.default.promises.rm(sessionFile).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function formatResult(result) {
|
||||||
|
const isError = result.isError;
|
||||||
|
const text = result.content[0].type === "text" ? result.content[0].text : void 0;
|
||||||
|
return { isError, text };
|
||||||
|
}
|
||||||
|
function parseCliCommand(args) {
|
||||||
|
const command = import_commands.commands[args._[0]];
|
||||||
|
if (!command)
|
||||||
|
throw new Error("Command is required");
|
||||||
|
return (0, import_command.parseCommand)(command, args);
|
||||||
|
}
|
||||||
|
function daemonSocketPath(clientInfo, sessionName) {
|
||||||
|
return (0, import_fileUtils.makeSocketPath)("cli", `${clientInfo.workspaceDirHash}-${sessionName}`);
|
||||||
|
}
|
||||||
|
function createSessionConfig(clientInfo, sessionName, browserInfo, options = {}) {
|
||||||
|
return {
|
||||||
|
name: sessionName,
|
||||||
|
version: clientInfo.version,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
socketPath: daemonSocketPath(clientInfo, sessionName),
|
||||||
|
workspaceDir: clientInfo.workspaceDir,
|
||||||
|
cli: { persistent: options.persistent },
|
||||||
|
browser: {
|
||||||
|
browserName: browserInfo.browserName,
|
||||||
|
launchOptions: browserInfo.launchOptions,
|
||||||
|
userDataDir: browserInfo.userDataDir
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
startCliDaemonServer
|
||||||
|
});
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var helpGenerator_exports = {};
|
||||||
|
__export(helpGenerator_exports, {
|
||||||
|
generateHelp: () => generateHelp,
|
||||||
|
generateHelpJSON: () => generateHelpJSON,
|
||||||
|
generateReadme: () => generateReadme
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(helpGenerator_exports);
|
||||||
|
var import_zodBundle = require("../../zodBundle");
|
||||||
|
var import_commands = require("./commands");
|
||||||
|
function commandArgs(command) {
|
||||||
|
const args = [];
|
||||||
|
const shape = command.args ? command.args.shape : {};
|
||||||
|
for (const [name, schema] of Object.entries(shape)) {
|
||||||
|
const zodSchema = schema;
|
||||||
|
const description = zodSchema.description ?? "";
|
||||||
|
args.push({ name, description, optional: zodSchema.safeParse(void 0).success });
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
function commandArgsText(args) {
|
||||||
|
return args.map((a) => a.optional ? `[${a.name}]` : `<${a.name}>`).join(" ");
|
||||||
|
}
|
||||||
|
function generateCommandHelp(command) {
|
||||||
|
const args = commandArgs(command);
|
||||||
|
const lines = [
|
||||||
|
`playwright-cli ${command.name} ${commandArgsText(args)}`,
|
||||||
|
"",
|
||||||
|
command.description,
|
||||||
|
""
|
||||||
|
];
|
||||||
|
if (args.length) {
|
||||||
|
lines.push("Arguments:");
|
||||||
|
lines.push(...args.map((a) => formatWithGap(` ${a.optional ? `[${a.name}]` : `<${a.name}>`}`, a.description.toLowerCase())));
|
||||||
|
}
|
||||||
|
if (command.options) {
|
||||||
|
lines.push("Options:");
|
||||||
|
const optionsShape = command.options.shape;
|
||||||
|
for (const [name, schema] of Object.entries(optionsShape)) {
|
||||||
|
const zodSchema = schema;
|
||||||
|
const description = (zodSchema.description ?? "").toLowerCase();
|
||||||
|
lines.push(formatWithGap(` --${name}`, description));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
const categories = [
|
||||||
|
{ name: "core", title: "Core" },
|
||||||
|
{ name: "navigation", title: "Navigation" },
|
||||||
|
{ name: "keyboard", title: "Keyboard" },
|
||||||
|
{ name: "mouse", title: "Mouse" },
|
||||||
|
{ name: "export", title: "Save as" },
|
||||||
|
{ name: "tabs", title: "Tabs" },
|
||||||
|
{ name: "storage", title: "Storage" },
|
||||||
|
{ name: "network", title: "Network" },
|
||||||
|
{ name: "devtools", title: "DevTools" },
|
||||||
|
{ name: "install", title: "Install" },
|
||||||
|
{ name: "config", title: "Configuration" },
|
||||||
|
{ name: "browsers", title: "Browser sessions" }
|
||||||
|
];
|
||||||
|
function generateHelp() {
|
||||||
|
const lines = [];
|
||||||
|
lines.push("Usage: playwright-cli <command> [args] [options]");
|
||||||
|
lines.push("Usage: playwright-cli -s=<session> <command> [args] [options]");
|
||||||
|
const commandsByCategory = /* @__PURE__ */ new Map();
|
||||||
|
for (const c of categories)
|
||||||
|
commandsByCategory.set(c.name, []);
|
||||||
|
for (const command of Object.values(import_commands.commands)) {
|
||||||
|
if (command.hidden)
|
||||||
|
continue;
|
||||||
|
commandsByCategory.get(command.category).push(command);
|
||||||
|
}
|
||||||
|
for (const c of categories) {
|
||||||
|
const cc = commandsByCategory.get(c.name);
|
||||||
|
if (!cc.length)
|
||||||
|
continue;
|
||||||
|
lines.push(`
|
||||||
|
${c.title}:`);
|
||||||
|
for (const command of cc)
|
||||||
|
lines.push(generateHelpEntry(command));
|
||||||
|
}
|
||||||
|
lines.push("\nGlobal options:");
|
||||||
|
lines.push(formatWithGap(" --help [command]", "print help"));
|
||||||
|
lines.push(formatWithGap(" --version", "print version"));
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
function generateReadme() {
|
||||||
|
const lines = [];
|
||||||
|
lines.push("\n## Commands");
|
||||||
|
const commandsByCategory = /* @__PURE__ */ new Map();
|
||||||
|
for (const c of categories)
|
||||||
|
commandsByCategory.set(c.name, []);
|
||||||
|
for (const command of Object.values(import_commands.commands))
|
||||||
|
commandsByCategory.get(command.category).push(command);
|
||||||
|
for (const c of categories) {
|
||||||
|
const cc = commandsByCategory.get(c.name);
|
||||||
|
if (!cc.length)
|
||||||
|
continue;
|
||||||
|
lines.push(`
|
||||||
|
### ${c.title}
|
||||||
|
`);
|
||||||
|
lines.push("```bash");
|
||||||
|
for (const command of cc)
|
||||||
|
lines.push(generateReadmeEntry(command));
|
||||||
|
lines.push("```");
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
function generateHelpEntry(command) {
|
||||||
|
const args = commandArgs(command);
|
||||||
|
const prefix = ` ${command.name} ${commandArgsText(args)}`;
|
||||||
|
const suffix = command.description.toLowerCase();
|
||||||
|
return formatWithGap(prefix, suffix);
|
||||||
|
}
|
||||||
|
function generateReadmeEntry(command) {
|
||||||
|
const args = commandArgs(command);
|
||||||
|
const prefix = `playwright-cli ${command.name} ${commandArgsText(args)}`;
|
||||||
|
const suffix = "# " + command.description.toLowerCase();
|
||||||
|
return formatWithGap(prefix, suffix, 40);
|
||||||
|
}
|
||||||
|
function unwrapZodType(schema) {
|
||||||
|
if ("unwrap" in schema && typeof schema.unwrap === "function")
|
||||||
|
return unwrapZodType(schema.unwrap());
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
function isBooleanSchema(schema) {
|
||||||
|
return unwrapZodType(schema) instanceof import_zodBundle.z.ZodBoolean;
|
||||||
|
}
|
||||||
|
function generateHelpJSON() {
|
||||||
|
const booleanOptions = /* @__PURE__ */ new Set();
|
||||||
|
const commandEntries = {};
|
||||||
|
for (const [name, command] of Object.entries(import_commands.commands)) {
|
||||||
|
const flags = {};
|
||||||
|
if (command.options) {
|
||||||
|
const optionsShape = command.options.shape;
|
||||||
|
for (const [flagName, schema] of Object.entries(optionsShape)) {
|
||||||
|
const isBoolean = isBooleanSchema(schema);
|
||||||
|
flags[flagName] = isBoolean ? "boolean" : "string";
|
||||||
|
if (isBoolean)
|
||||||
|
booleanOptions.add(flagName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commandEntries[name] = { help: generateCommandHelp(command), flags };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
global: generateHelp(),
|
||||||
|
commands: commandEntries,
|
||||||
|
booleanOptions: [...booleanOptions]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function formatWithGap(prefix, text, threshold = 30) {
|
||||||
|
const indent = Math.max(1, threshold - prefix.length);
|
||||||
|
return prefix + " ".repeat(indent) + text;
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
generateHelp,
|
||||||
|
generateHelpJSON,
|
||||||
|
generateReadme
|
||||||
|
});
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_os = __toESM(require("os"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_daemon = require("./daemon");
|
||||||
|
var import_watchdog = require("../mcp/watchdog");
|
||||||
|
var import_browserFactory = require("../mcp/browserFactory");
|
||||||
|
var configUtils = __toESM(require("../mcp/config"));
|
||||||
|
var import_registry = require("../cli-client/registry");
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
var import_registry2 = require("../../server/registry/index");
|
||||||
|
import_utilsBundle.program.argument("[session-name]", "name of the session to create or connect to", "default").option("--headed", "run in headed mode (non-headless)").option("--extension", "run with the extension").option("--browser <name>", "browser to use (chromium, chrome, firefox, webkit)").option("--persistent", "use a persistent browser context").option("--profile <path>", "path to the user data dir").option("--config <path>", "path to the config file; by default uses .playwright/cli.config.json in the project directory and ~/.playwright/cli.config.json as global config").option("--endpoint <endpoint>", "attach to a running Playwright browser endpoint").option("--init-workspace", "initialize workspace").option("--init-skills <value>", 'install skills for the given agent type ("claude" or "agents")').action(async (sessionName, options) => {
|
||||||
|
if (options.initWorkspace) {
|
||||||
|
await initWorkspace(options.initSkills);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(0, import_watchdog.setupExitWatchdog)();
|
||||||
|
const clientInfo = (0, import_registry.createClientInfo)();
|
||||||
|
const mcpConfig = await configUtils.resolveCLIConfigForCLI(clientInfo.daemonProfilesDir, sessionName, options);
|
||||||
|
const clientInfoEx = {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
sessionName,
|
||||||
|
workspaceDir: clientInfo.workspaceDir
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const { browser, browserInfo } = await (0, import_browserFactory.createBrowserWithInfo)(mcpConfig, clientInfoEx);
|
||||||
|
const browserContext = mcpConfig.browser.isolated ? await browser.newContext(mcpConfig.browser.contextOptions) : browser.contexts()[0];
|
||||||
|
if (!browserContext)
|
||||||
|
throw new Error("Error: unable to connect to a browser that does not have any contexts");
|
||||||
|
const persistent = options.persistent || options.profile || mcpConfig.browser.userDataDir ? true : void 0;
|
||||||
|
const socketPath = await (0, import_daemon.startCliDaemonServer)(sessionName, browserContext, browserInfo, mcpConfig, clientInfo, { persistent, exitOnClose: true });
|
||||||
|
console.log(`### Success
|
||||||
|
Daemon listening on ${socketPath}`);
|
||||||
|
console.log("<EOF>");
|
||||||
|
} catch (error) {
|
||||||
|
const message = process.env.PWDEBUGIMPL ? error.stack || error.message : error.message;
|
||||||
|
console.log(`### Error
|
||||||
|
${message}`);
|
||||||
|
console.log("<EOF>");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
void import_utilsBundle.program.parseAsync();
|
||||||
|
function defaultConfigFile() {
|
||||||
|
return import_path.default.resolve(".playwright", "cli.config.json");
|
||||||
|
}
|
||||||
|
function globalConfigFile() {
|
||||||
|
return import_path.default.join(process.env["PWTEST_CLI_GLOBAL_CONFIG"] ?? import_os.default.homedir(), ".playwright", "cli.config.json");
|
||||||
|
}
|
||||||
|
async function initWorkspace(initSkills) {
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const playwrightDir = import_path.default.join(cwd, ".playwright");
|
||||||
|
await import_fs.default.promises.mkdir(playwrightDir, { recursive: true });
|
||||||
|
console.log(`\u2705 Workspace initialized at \`${cwd}\`.`);
|
||||||
|
if (initSkills) {
|
||||||
|
const skillSourceDir = import_path.default.join(__dirname, "../cli-client/skill");
|
||||||
|
const target = initSkills === "agents" ? "agents" : "claude";
|
||||||
|
const skillDestDir = import_path.default.join(cwd, `.${target}`, "skills", "playwright-cli");
|
||||||
|
if (!import_fs.default.existsSync(skillSourceDir)) {
|
||||||
|
console.error("\u274C Skills source directory not found:", skillSourceDir);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await import_fs.default.promises.cp(skillSourceDir, skillDestDir, { recursive: true });
|
||||||
|
console.log(`\u2705 Skills installed to \`${import_path.default.relative(cwd, skillDestDir)}\`.`);
|
||||||
|
}
|
||||||
|
await ensureConfiguredBrowserInstalled();
|
||||||
|
}
|
||||||
|
async function ensureConfiguredBrowserInstalled() {
|
||||||
|
if (import_fs.default.existsSync(defaultConfigFile()) || import_fs.default.existsSync(globalConfigFile())) {
|
||||||
|
const clientInfo = (0, import_registry.createClientInfo)();
|
||||||
|
const config = await configUtils.resolveCLIConfigForCLI(clientInfo.daemonProfilesDir, "default", {});
|
||||||
|
const browserName = config.browser.browserName;
|
||||||
|
const channel = config.browser.launchOptions.channel;
|
||||||
|
if (!channel || channel.startsWith("chromium")) {
|
||||||
|
const executable = import_registry2.registry.findExecutable(channel ?? browserName);
|
||||||
|
if (executable && !import_fs.default.existsSync(executable.executablePath()))
|
||||||
|
await import_registry2.registry.install([executable]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const channel = await findOrInstallDefaultBrowser();
|
||||||
|
if (channel !== "chrome")
|
||||||
|
await createDefaultConfig(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function findOrInstallDefaultBrowser() {
|
||||||
|
const channels = ["chrome", "msedge"];
|
||||||
|
for (const channel of channels) {
|
||||||
|
const executable = import_registry2.registry.findExecutable(channel);
|
||||||
|
if (!executable?.executablePath())
|
||||||
|
continue;
|
||||||
|
console.log(`\u2705 Found ${channel}, will use it as the default browser.`);
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
const chromiumExecutable = import_registry2.registry.findExecutable("chromium");
|
||||||
|
if (!import_fs.default.existsSync(chromiumExecutable?.executablePath()))
|
||||||
|
await import_registry2.registry.install([chromiumExecutable]);
|
||||||
|
return "chromium";
|
||||||
|
}
|
||||||
|
async function createDefaultConfig(channel) {
|
||||||
|
const config = {
|
||||||
|
browser: {
|
||||||
|
browserName: "chromium",
|
||||||
|
launchOptions: { channel }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await import_fs.default.promises.writeFile(defaultConfigFile(), JSON.stringify(config, null, 2));
|
||||||
|
console.log(`\u2705 Created default config for ${channel} at ${import_path.default.relative(process.cwd(), defaultConfigFile())}.`);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,284 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var dashboardApp_exports = {};
|
||||||
|
__export(dashboardApp_exports, {
|
||||||
|
syncLocalStorageWithSettings: () => syncLocalStorageWithSettings
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(dashboardApp_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_net = __toESM(require("net"));
|
||||||
|
var import__ = require("../../..");
|
||||||
|
var import_httpServer = require("../../server/utils/httpServer");
|
||||||
|
var import_fileUtils = require("../../server/utils/fileUtils");
|
||||||
|
var import_processLauncher = require("../../server/utils/processLauncher");
|
||||||
|
var import_registry = require("../../server/registry/index");
|
||||||
|
var import_dashboardController = require("./dashboardController");
|
||||||
|
var import_serverRegistry = require("../../serverRegistry");
|
||||||
|
var import_connect = require("../utils/connect");
|
||||||
|
function readBody(request) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks = [];
|
||||||
|
request.on("data", (chunk) => chunks.push(chunk));
|
||||||
|
request.on("end", () => {
|
||||||
|
try {
|
||||||
|
const text = Buffer.concat(chunks).toString();
|
||||||
|
resolve(text ? JSON.parse(text) : {});
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
request.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function parseRequest(request) {
|
||||||
|
const body = await readBody(request);
|
||||||
|
if (!body.guid)
|
||||||
|
throw new Error("Dashboard app is too old, please close it and open again");
|
||||||
|
return { guid: body.guid };
|
||||||
|
}
|
||||||
|
function sendJSON(response, data, statusCode = 200) {
|
||||||
|
response.statusCode = statusCode;
|
||||||
|
response.setHeader("Content-Type", "application/json");
|
||||||
|
response.end(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
async function loadBrowserDescriptorSessions(wsPath) {
|
||||||
|
const entriesByWorkspace = await import_serverRegistry.serverRegistry.list();
|
||||||
|
const sessions = [];
|
||||||
|
for (const [, entries] of entriesByWorkspace) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
let wsUrl;
|
||||||
|
if (entry.canConnect) {
|
||||||
|
const url = new URL(wsPath, "http://localhost");
|
||||||
|
url.searchParams.set("guid", entry.browser.guid);
|
||||||
|
wsUrl = url.pathname + url.search;
|
||||||
|
}
|
||||||
|
sessions.push({ ...entry, wsUrl });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
const browserGuidToDashboardConnection = /* @__PURE__ */ new Map();
|
||||||
|
async function handleApiRequest(httpServer, request, response) {
|
||||||
|
const url = new URL(request.url, httpServer.urlPrefix("human-readable"));
|
||||||
|
const apiPath = url.pathname;
|
||||||
|
if (apiPath === "/api/sessions/list" && request.method === "GET") {
|
||||||
|
const sessions = await loadBrowserDescriptorSessions(httpServer.wsGuid());
|
||||||
|
sendJSON(response, { sessions });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (apiPath === "/api/sessions/close" && request.method === "POST") {
|
||||||
|
const { guid } = await parseRequest(request);
|
||||||
|
let browser;
|
||||||
|
try {
|
||||||
|
const browserDescriptor = import_serverRegistry.serverRegistry.readDescriptor(guid);
|
||||||
|
browser = await (0, import_connect.connectToBrowserAcrossVersions)(browserDescriptor);
|
||||||
|
} catch (e) {
|
||||||
|
sendJSON(response, { error: "Failed to connect to browser socket: " + e.message }, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Promise.all(browser.contexts().map((context) => context.close()));
|
||||||
|
await browser.close();
|
||||||
|
sendJSON(response, { success: true });
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
sendJSON(response, { error: "Failed to close browser: " + e.message }, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (apiPath === "/api/sessions/delete-data" && request.method === "POST") {
|
||||||
|
const { guid } = await parseRequest(request);
|
||||||
|
try {
|
||||||
|
await import_serverRegistry.serverRegistry.deleteUserData(guid);
|
||||||
|
} catch (e) {
|
||||||
|
sendJSON(response, { error: "Failed to delete session data: " + e.message }, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendJSON(response, { success: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.statusCode = 404;
|
||||||
|
response.end(JSON.stringify({ error: "Not found" }));
|
||||||
|
}
|
||||||
|
async function openDashboardApp() {
|
||||||
|
const httpServer = new import_httpServer.HttpServer();
|
||||||
|
const libDir = require.resolve("playwright-core/package.json");
|
||||||
|
const dashboardDir = import_path.default.join(import_path.default.dirname(libDir), "lib/vite/dashboard");
|
||||||
|
httpServer.routePrefix("/api/", (request, response) => {
|
||||||
|
handleApiRequest(httpServer, request, response).catch((e) => {
|
||||||
|
response.statusCode = 500;
|
||||||
|
response.end(JSON.stringify({ error: e.message }));
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
httpServer.createWebSocket((url2) => {
|
||||||
|
const guid = url2.searchParams.get("guid");
|
||||||
|
if (!guid)
|
||||||
|
throw new Error("Unsupported WebSocket URL: " + url2.toString());
|
||||||
|
const browserDescriptor = import_serverRegistry.serverRegistry.readDescriptor(guid);
|
||||||
|
const cdpPageId = url2.searchParams.get("cdpPageId");
|
||||||
|
if (cdpPageId) {
|
||||||
|
const connection2 = browserGuidToDashboardConnection.get(guid);
|
||||||
|
if (!connection2)
|
||||||
|
throw new Error("CDP connection not found for session: " + guid);
|
||||||
|
const page2 = connection2.pageForId(cdpPageId);
|
||||||
|
if (!page2)
|
||||||
|
throw new Error("Page not found for page ID: " + cdpPageId);
|
||||||
|
return new import_dashboardController.CDPConnection(page2);
|
||||||
|
}
|
||||||
|
const cdpUrl = new URL(httpServer.urlPrefix("human-readable"));
|
||||||
|
cdpUrl.pathname = httpServer.wsGuid();
|
||||||
|
cdpUrl.searchParams.set("guid", guid);
|
||||||
|
const connection = new import_dashboardController.DashboardConnection(browserDescriptor, cdpUrl, () => browserGuidToDashboardConnection.delete(guid));
|
||||||
|
browserGuidToDashboardConnection.set(guid, connection);
|
||||||
|
return connection;
|
||||||
|
});
|
||||||
|
httpServer.routePrefix("/", (request, response) => {
|
||||||
|
const pathname = new URL(request.url, `http://${request.headers.host}`).pathname;
|
||||||
|
const filePath = pathname === "/" ? "index.html" : pathname.substring(1);
|
||||||
|
const resolved = import_path.default.join(dashboardDir, filePath);
|
||||||
|
if (!resolved.startsWith(dashboardDir))
|
||||||
|
return false;
|
||||||
|
return httpServer.serveFile(request, response, resolved);
|
||||||
|
});
|
||||||
|
await httpServer.start();
|
||||||
|
const url = httpServer.urlPrefix("human-readable");
|
||||||
|
const { page } = await launchApp("dashboard");
|
||||||
|
await page.goto(url);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
async function launchApp(appName) {
|
||||||
|
const channel = (0, import_registry.findChromiumChannelBestEffort)("javascript");
|
||||||
|
const debugPort = parseInt(process.env.PLAYWRIGHT_DASHBOARD_DEBUG_PORT, 10) || void 0;
|
||||||
|
const context = await import__.chromium.launchPersistentContext("", {
|
||||||
|
ignoreDefaultArgs: ["--enable-automation"],
|
||||||
|
channel,
|
||||||
|
headless: debugPort !== void 0,
|
||||||
|
args: [
|
||||||
|
"--app=data:text/html,",
|
||||||
|
"--test-type=",
|
||||||
|
`--window-size=1280,800`,
|
||||||
|
`--window-position=100,100`,
|
||||||
|
...debugPort !== void 0 ? [`--remote-debugging-port=${debugPort}`] : []
|
||||||
|
],
|
||||||
|
viewport: null
|
||||||
|
});
|
||||||
|
const [page] = context.pages();
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
context.on("page", async (newPage) => {
|
||||||
|
if (newPage.mainFrame().url() === "chrome://new-tab-page/") {
|
||||||
|
await page.bringToFront();
|
||||||
|
await newPage.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
page.on("close", () => {
|
||||||
|
(0, import_processLauncher.gracefullyProcessExitDoNotHang)(0);
|
||||||
|
});
|
||||||
|
const image = await import_fs.default.promises.readFile(import_path.default.join(__dirname, "appIcon.png"));
|
||||||
|
await page._setDockTile?.(image);
|
||||||
|
await syncLocalStorageWithSettings(page, appName);
|
||||||
|
return { context, page };
|
||||||
|
}
|
||||||
|
async function syncLocalStorageWithSettings(page, appName) {
|
||||||
|
const settingsFile = import_path.default.join(import_registry.registryDirectory, ".settings", `${appName}.json`);
|
||||||
|
await page.exposeBinding("_saveSerializedSettings", (_, settings2) => {
|
||||||
|
import_fs.default.mkdirSync(import_path.default.dirname(settingsFile), { recursive: true });
|
||||||
|
import_fs.default.writeFileSync(settingsFile, settings2);
|
||||||
|
});
|
||||||
|
const settings = await import_fs.default.promises.readFile(settingsFile, "utf-8").catch(() => "{}");
|
||||||
|
await page.addInitScript(
|
||||||
|
`(${String((settings2) => {
|
||||||
|
if (location && location.protocol === "data:")
|
||||||
|
return;
|
||||||
|
if (window.top !== window)
|
||||||
|
return;
|
||||||
|
Object.entries(settings2).map(([k, v]) => localStorage[k] = v);
|
||||||
|
window.saveSettings = () => {
|
||||||
|
window._saveSerializedSettings(JSON.stringify({ ...localStorage }));
|
||||||
|
};
|
||||||
|
})})(${settings});
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function dashboardSocketPath() {
|
||||||
|
return (0, import_fileUtils.makeSocketPath)("dashboard", "app");
|
||||||
|
}
|
||||||
|
async function acquireSingleton() {
|
||||||
|
const socketPath = dashboardSocketPath();
|
||||||
|
if (process.platform !== "win32")
|
||||||
|
await import_fs.default.promises.mkdir(import_path.default.dirname(socketPath), { recursive: true });
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const server = import_net.default.createServer();
|
||||||
|
server.listen(socketPath, () => resolve(server));
|
||||||
|
server.on("error", (err) => {
|
||||||
|
if (err.code !== "EADDRINUSE")
|
||||||
|
return reject(err);
|
||||||
|
const client = import_net.default.connect(socketPath, () => {
|
||||||
|
client.write("bringToFront");
|
||||||
|
client.end();
|
||||||
|
reject(new Error("already running"));
|
||||||
|
});
|
||||||
|
client.on("error", () => {
|
||||||
|
if (process.platform !== "win32")
|
||||||
|
import_fs.default.unlinkSync(socketPath);
|
||||||
|
server.listen(socketPath, () => resolve(server));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function main() {
|
||||||
|
let server;
|
||||||
|
process.on("exit", () => server?.close());
|
||||||
|
const underTest = !!process.env.PLAYWRIGHT_DASHBOARD_DEBUG_PORT;
|
||||||
|
if (!underTest) {
|
||||||
|
try {
|
||||||
|
server = await acquireSingleton();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const page = await openDashboardApp();
|
||||||
|
server?.on("connection", (socket) => {
|
||||||
|
socket.on("data", (data) => {
|
||||||
|
if (data.toString() === "bringToFront")
|
||||||
|
page?.bringToFront().catch(() => {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
process.on("unhandledRejection", (error) => {
|
||||||
|
console.error("Unhandled promise rejection:", error);
|
||||||
|
});
|
||||||
|
void main();
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
syncLocalStorageWithSettings
|
||||||
|
});
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var dashboardController_exports = {};
|
||||||
|
__export(dashboardController_exports, {
|
||||||
|
CDPConnection: () => CDPConnection,
|
||||||
|
DashboardConnection: () => DashboardConnection
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(dashboardController_exports);
|
||||||
|
var import_eventsHelper = require("../../server/utils/eventsHelper");
|
||||||
|
var import_connect = require("../utils/connect");
|
||||||
|
class DashboardConnection {
|
||||||
|
constructor(browserDescriptor, cdpUrl, onclose) {
|
||||||
|
this.version = 1;
|
||||||
|
this.selectedPage = null;
|
||||||
|
this._lastFrameData = null;
|
||||||
|
this._lastViewportSize = null;
|
||||||
|
this._pageListeners = [];
|
||||||
|
this._contextListeners = [];
|
||||||
|
this._eventListeners = /* @__PURE__ */ new Map();
|
||||||
|
this._browserDescriptor = browserDescriptor;
|
||||||
|
this._cdpUrl = cdpUrl;
|
||||||
|
this._onclose = onclose;
|
||||||
|
}
|
||||||
|
on(event, listener) {
|
||||||
|
let set = this._eventListeners.get(event);
|
||||||
|
if (!set) {
|
||||||
|
set = /* @__PURE__ */ new Set();
|
||||||
|
this._eventListeners.set(event, set);
|
||||||
|
}
|
||||||
|
set.add(listener);
|
||||||
|
}
|
||||||
|
off(event, listener) {
|
||||||
|
this._eventListeners.get(event)?.delete(listener);
|
||||||
|
}
|
||||||
|
_emit(event, params) {
|
||||||
|
this.sendEvent?.(event, params);
|
||||||
|
const set = this._eventListeners.get(event);
|
||||||
|
if (set) {
|
||||||
|
for (const fn of set)
|
||||||
|
fn(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onconnect() {
|
||||||
|
this._initPromise = this._init();
|
||||||
|
this._initPromise.catch(() => this.close?.());
|
||||||
|
}
|
||||||
|
async _init() {
|
||||||
|
this._browser = await (0, import_connect.connectToBrowserAcrossVersions)(this._browserDescriptor);
|
||||||
|
this._context = this._browser.contexts()[0];
|
||||||
|
this._contextListeners.push(
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(this._context, "page", (page) => {
|
||||||
|
this._sendTabList();
|
||||||
|
if (!this.selectedPage)
|
||||||
|
this._selectPage(page);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const pages = this._context.pages();
|
||||||
|
if (pages.length > 0)
|
||||||
|
this._selectPage(pages[0]);
|
||||||
|
this._sendCachedState();
|
||||||
|
}
|
||||||
|
onclose() {
|
||||||
|
this._deselectPage();
|
||||||
|
this._contextListeners.forEach((d) => d.dispose());
|
||||||
|
this._contextListeners = [];
|
||||||
|
this._onclose();
|
||||||
|
this._browser?.close().catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async dispatch(method, params) {
|
||||||
|
await this._initPromise;
|
||||||
|
return this[method]?.(params);
|
||||||
|
}
|
||||||
|
async selectTab(params) {
|
||||||
|
const page = this._context.pages().find((p) => this._pageId(p) === params.pageId);
|
||||||
|
if (page)
|
||||||
|
await this._selectPage(page);
|
||||||
|
}
|
||||||
|
async closeTab(params) {
|
||||||
|
const page = this._context.pages().find((p) => this._pageId(p) === params.pageId);
|
||||||
|
if (page)
|
||||||
|
await page.close({ reason: "Closed in Dashboard" });
|
||||||
|
}
|
||||||
|
async newTab() {
|
||||||
|
const page = await this._context.newPage();
|
||||||
|
await this._selectPage(page);
|
||||||
|
}
|
||||||
|
async navigate(params) {
|
||||||
|
if (!this.selectedPage || !params.url)
|
||||||
|
return;
|
||||||
|
const page = this.selectedPage;
|
||||||
|
await page.goto(params.url);
|
||||||
|
}
|
||||||
|
async back() {
|
||||||
|
await this.selectedPage?.goBack();
|
||||||
|
}
|
||||||
|
async forward() {
|
||||||
|
await this.selectedPage?.goForward();
|
||||||
|
}
|
||||||
|
async reload() {
|
||||||
|
await this.selectedPage?.reload();
|
||||||
|
}
|
||||||
|
async mousemove(params) {
|
||||||
|
await this.selectedPage?.mouse.move(params.x, params.y);
|
||||||
|
}
|
||||||
|
async mousedown(params) {
|
||||||
|
await this.selectedPage?.mouse.move(params.x, params.y);
|
||||||
|
await this.selectedPage?.mouse.down({ button: params.button || "left" });
|
||||||
|
}
|
||||||
|
async mouseup(params) {
|
||||||
|
await this.selectedPage?.mouse.move(params.x, params.y);
|
||||||
|
await this.selectedPage?.mouse.up({ button: params.button || "left" });
|
||||||
|
}
|
||||||
|
async wheel(params) {
|
||||||
|
await this.selectedPage?.mouse.wheel(params.deltaX, params.deltaY);
|
||||||
|
}
|
||||||
|
async keydown(params) {
|
||||||
|
await this.selectedPage?.keyboard.down(params.key);
|
||||||
|
}
|
||||||
|
async keyup(params) {
|
||||||
|
await this.selectedPage?.keyboard.up(params.key);
|
||||||
|
}
|
||||||
|
async _selectPage(page) {
|
||||||
|
if (this.selectedPage === page)
|
||||||
|
return;
|
||||||
|
if (this.selectedPage) {
|
||||||
|
this._pageListeners.forEach((d) => d.dispose());
|
||||||
|
this._pageListeners = [];
|
||||||
|
await this.selectedPage.screencast.stop();
|
||||||
|
}
|
||||||
|
this.selectedPage = page;
|
||||||
|
this._lastFrameData = null;
|
||||||
|
this._lastViewportSize = null;
|
||||||
|
this._sendTabList();
|
||||||
|
this._pageListeners.push(
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(page, "close", () => {
|
||||||
|
this._deselectPage();
|
||||||
|
const pages = page.context().pages();
|
||||||
|
if (pages.length > 0)
|
||||||
|
this._selectPage(pages[0]);
|
||||||
|
this._sendTabList();
|
||||||
|
}),
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(page, "framenavigated", (frame) => {
|
||||||
|
if (frame === page.mainFrame())
|
||||||
|
this._sendTabList();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const size = { width: 1280, height: 800 };
|
||||||
|
await page.screencast.start({
|
||||||
|
onFrame: ({ data }) => this._writeFrame(data, page.viewportSize()?.width ?? 0, page.viewportSize()?.height ?? 0),
|
||||||
|
size
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_deselectPage() {
|
||||||
|
if (!this.selectedPage)
|
||||||
|
return;
|
||||||
|
this._pageListeners.forEach((d) => d.dispose());
|
||||||
|
this._pageListeners = [];
|
||||||
|
this.selectedPage.screencast.stop().catch(() => {
|
||||||
|
});
|
||||||
|
this.selectedPage = null;
|
||||||
|
this._lastFrameData = null;
|
||||||
|
this._lastViewportSize = null;
|
||||||
|
}
|
||||||
|
async pickLocator() {
|
||||||
|
if (!this.selectedPage)
|
||||||
|
return;
|
||||||
|
const locator = await this.selectedPage.pickLocator();
|
||||||
|
this._emit("elementPicked", { selector: locator.toString() });
|
||||||
|
}
|
||||||
|
async cancelPickLocator() {
|
||||||
|
await this.selectedPage?.cancelPickLocator();
|
||||||
|
}
|
||||||
|
_sendCachedState() {
|
||||||
|
if (this._lastFrameData && this._lastViewportSize)
|
||||||
|
this._emit("frame", { data: this._lastFrameData, viewportWidth: this._lastViewportSize.width, viewportHeight: this._lastViewportSize.height });
|
||||||
|
this._sendTabList();
|
||||||
|
}
|
||||||
|
async tabs() {
|
||||||
|
return { tabs: await this._tabList() };
|
||||||
|
}
|
||||||
|
async _tabList() {
|
||||||
|
const pages = this._context.pages();
|
||||||
|
if (pages.length === 0)
|
||||||
|
return [];
|
||||||
|
const devtoolsUrl = await this._devtoolsUrl(pages[0]);
|
||||||
|
return await Promise.all(pages.map(async (page) => {
|
||||||
|
const title = await page.title();
|
||||||
|
return {
|
||||||
|
pageId: this._pageId(page),
|
||||||
|
title,
|
||||||
|
url: page.url(),
|
||||||
|
selected: page === this.selectedPage,
|
||||||
|
inspectorUrl: devtoolsUrl ? await this._pageInspectorUrl(page, devtoolsUrl) : "data:text/plain,Dashboard only supported in Chromium based browsers"
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
pageForId(pageId) {
|
||||||
|
return this._context?.pages().find((p) => this._pageId(p) === pageId);
|
||||||
|
}
|
||||||
|
_pageId(p) {
|
||||||
|
return p._guid;
|
||||||
|
}
|
||||||
|
async _devtoolsUrl(page) {
|
||||||
|
const cdpPort = this._browserDescriptor.browser.launchOptions.cdpPort;
|
||||||
|
if (cdpPort)
|
||||||
|
return new URL(`http://localhost:${cdpPort}/devtools/`);
|
||||||
|
const browserRevision = await getBrowserRevision(page);
|
||||||
|
if (!browserRevision)
|
||||||
|
return null;
|
||||||
|
return new URL(`https://chrome-devtools-frontend.appspot.com/serve_rev/${browserRevision}/`);
|
||||||
|
}
|
||||||
|
async _pageInspectorUrl(page, devtoolsUrl) {
|
||||||
|
const inspector = new URL("./devtools_app.html", devtoolsUrl);
|
||||||
|
const cdp = new URL(this._cdpUrl);
|
||||||
|
cdp.searchParams.set("cdpPageId", this._pageId(page));
|
||||||
|
inspector.searchParams.set("ws", `${cdp.host}${cdp.pathname}${cdp.search}`);
|
||||||
|
const url = inspector.toString();
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
_sendTabList() {
|
||||||
|
this._tabList().then((tabs) => this._emit("tabs", { tabs }));
|
||||||
|
}
|
||||||
|
_writeFrame(frame, viewportWidth, viewportHeight) {
|
||||||
|
const data = frame.toString("base64");
|
||||||
|
this._lastFrameData = data;
|
||||||
|
this._lastViewportSize = { width: viewportWidth, height: viewportHeight };
|
||||||
|
this._emit("frame", { data, viewportWidth, viewportHeight });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function getBrowserRevision(page) {
|
||||||
|
try {
|
||||||
|
const session = await page.context().newCDPSession(page);
|
||||||
|
const version = await session.send("Browser.getVersion");
|
||||||
|
await session.detach();
|
||||||
|
return version.revision;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class CDPConnection {
|
||||||
|
constructor(page) {
|
||||||
|
this._rawSession = null;
|
||||||
|
this._rawSessionListeners = [];
|
||||||
|
this._page = page;
|
||||||
|
}
|
||||||
|
onconnect() {
|
||||||
|
this._initializePromise = this._initializeRawSession();
|
||||||
|
}
|
||||||
|
async dispatch(method, params) {
|
||||||
|
await this._initializePromise;
|
||||||
|
if (!this._rawSession)
|
||||||
|
throw new Error("CDP session is not initialized");
|
||||||
|
return await this._rawSession.send(method, params);
|
||||||
|
}
|
||||||
|
onclose() {
|
||||||
|
this._rawSessionListeners.forEach((listener) => listener.dispose());
|
||||||
|
this._rawSession?.detach().catch(() => {
|
||||||
|
});
|
||||||
|
this._rawSession = null;
|
||||||
|
this._initializePromise = void 0;
|
||||||
|
}
|
||||||
|
async _initializeRawSession() {
|
||||||
|
const session = await this._page.context().newCDPSession(this._page);
|
||||||
|
this._rawSession = session;
|
||||||
|
this._rawSessionListeners = [
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(session, "event", ({ method, params }) => {
|
||||||
|
this.sendEvent?.(method, params);
|
||||||
|
}),
|
||||||
|
import_eventsHelper.eventsHelper.addEventListener(session, "close", () => {
|
||||||
|
this.close?.();
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
CDPConnection,
|
||||||
|
DashboardConnection
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var exports_exports = {};
|
||||||
|
__export(exports_exports, {
|
||||||
|
BrowserBackend: () => import_browserBackend.BrowserBackend,
|
||||||
|
Tab: () => import_tab.Tab,
|
||||||
|
browserTools: () => import_tools.browserTools,
|
||||||
|
createClientInfo: () => import_registry.createClientInfo,
|
||||||
|
createConnection: () => import_mcp.createConnection,
|
||||||
|
filteredTools: () => import_tools.filteredTools,
|
||||||
|
logUnhandledError: () => import_log.logUnhandledError,
|
||||||
|
parseResponse: () => import_response.parseResponse,
|
||||||
|
setupExitWatchdog: () => import_watchdog.setupExitWatchdog,
|
||||||
|
start: () => import_server.start,
|
||||||
|
startCliDaemonServer: () => import_daemon.startCliDaemonServer,
|
||||||
|
toMcpTool: () => import_tool.toMcpTool
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(exports_exports);
|
||||||
|
var import_registry = require("./cli-client/registry");
|
||||||
|
var import_daemon = require("./cli-daemon/daemon");
|
||||||
|
var import_log = require("./mcp/log");
|
||||||
|
var import_watchdog = require("./mcp/watchdog");
|
||||||
|
var import_tool = require("./utils/mcp/tool");
|
||||||
|
var import_browserBackend = require("./backend/browserBackend");
|
||||||
|
var import_response = require("./backend/response");
|
||||||
|
var import_tab = require("./backend/tab");
|
||||||
|
var import_tools = require("./backend/tools");
|
||||||
|
var import_server = require("./utils/mcp/server");
|
||||||
|
var import_mcp = require("./mcp/index");
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
BrowserBackend,
|
||||||
|
Tab,
|
||||||
|
browserTools,
|
||||||
|
createClientInfo,
|
||||||
|
createConnection,
|
||||||
|
filteredTools,
|
||||||
|
logUnhandledError,
|
||||||
|
parseResponse,
|
||||||
|
setupExitWatchdog,
|
||||||
|
start,
|
||||||
|
startCliDaemonServer,
|
||||||
|
toMcpTool
|
||||||
|
});
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var browserFactory_exports = {};
|
||||||
|
__export(browserFactory_exports, {
|
||||||
|
createBrowser: () => createBrowser,
|
||||||
|
createBrowserWithInfo: () => createBrowserWithInfo,
|
||||||
|
isProfileLocked: () => isProfileLocked
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(browserFactory_exports);
|
||||||
|
var import_crypto = __toESM(require("crypto"));
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_net = __toESM(require("net"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var playwright = __toESM(require("../../.."));
|
||||||
|
var import_registry = require("../../server/registry/index");
|
||||||
|
var import_log = require("./log");
|
||||||
|
var import_context = require("../backend/context");
|
||||||
|
var import_extensionContextFactory = require("./extensionContextFactory");
|
||||||
|
var import_connect = require("../utils/connect");
|
||||||
|
var import_serverRegistry = require("../../serverRegistry");
|
||||||
|
var import_connect2 = require("../../client/connect");
|
||||||
|
async function createBrowser(config, clientInfo) {
|
||||||
|
const { browser } = await createBrowserWithInfo(config, clientInfo);
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
async function createBrowserWithInfo(config, clientInfo) {
|
||||||
|
if (config.browser.remoteEndpoint)
|
||||||
|
return await createRemoteBrowser(config);
|
||||||
|
let browser;
|
||||||
|
if (config.browser.cdpEndpoint)
|
||||||
|
browser = await createCDPBrowser(config, clientInfo);
|
||||||
|
else if (config.browser.isolated)
|
||||||
|
browser = await createIsolatedBrowser(config, clientInfo);
|
||||||
|
else if (config.extension)
|
||||||
|
browser = await (0, import_extensionContextFactory.createExtensionBrowser)(config, clientInfo);
|
||||||
|
else
|
||||||
|
browser = await createPersistentBrowser(config, clientInfo);
|
||||||
|
return { browser, browserInfo: browserInfo(browser, config) };
|
||||||
|
}
|
||||||
|
function browserInfo(browser, config) {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
guid: browser._guid,
|
||||||
|
browserName: config.browser.browserName,
|
||||||
|
launchOptions: config.browser.launchOptions,
|
||||||
|
userDataDir: config.browser.userDataDir
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function createIsolatedBrowser(config, clientInfo) {
|
||||||
|
(0, import_log.testDebug)("create browser (isolated)");
|
||||||
|
await injectCdpPort(config.browser);
|
||||||
|
const browserType = playwright[config.browser.browserName];
|
||||||
|
const tracesDir = await computeTracesDir(config, clientInfo);
|
||||||
|
const browser = await browserType.launch({
|
||||||
|
tracesDir,
|
||||||
|
...config.browser.launchOptions,
|
||||||
|
handleSIGINT: false,
|
||||||
|
handleSIGTERM: false
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error.message.includes("Executable doesn't exist"))
|
||||||
|
throwBrowserIsNotInstalledError(config);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
await startServer(browser, clientInfo);
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
async function createCDPBrowser(config, clientInfo) {
|
||||||
|
(0, import_log.testDebug)("create browser (cdp)");
|
||||||
|
const browser = await playwright.chromium.connectOverCDP(config.browser.cdpEndpoint, {
|
||||||
|
headers: config.browser.cdpHeaders,
|
||||||
|
timeout: config.browser.cdpTimeout
|
||||||
|
});
|
||||||
|
await startServer(browser, clientInfo);
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
async function createRemoteBrowser(config) {
|
||||||
|
(0, import_log.testDebug)("create browser (remote)");
|
||||||
|
const descriptor = await import_serverRegistry.serverRegistry.find(config.browser.remoteEndpoint);
|
||||||
|
if (descriptor) {
|
||||||
|
const browser2 = await (0, import_connect.connectToBrowserAcrossVersions)(descriptor);
|
||||||
|
return {
|
||||||
|
browser: browser2,
|
||||||
|
browserInfo: {
|
||||||
|
guid: descriptor.browser.guid,
|
||||||
|
browserName: descriptor.browser.browserName,
|
||||||
|
launchOptions: descriptor.browser.launchOptions,
|
||||||
|
userDataDir: descriptor.browser.userDataDir
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const endpoint = config.browser.remoteEndpoint;
|
||||||
|
const playwrightObject = playwright;
|
||||||
|
const browser = await (0, import_connect2.connectToBrowser)(playwrightObject, { endpoint });
|
||||||
|
browser._connectToBrowserType(playwrightObject[browser._browserName], {}, void 0);
|
||||||
|
return { browser, browserInfo: browserInfo(browser, config) };
|
||||||
|
}
|
||||||
|
async function createPersistentBrowser(config, clientInfo) {
|
||||||
|
(0, import_log.testDebug)("create browser (persistent)");
|
||||||
|
await injectCdpPort(config.browser);
|
||||||
|
const userDataDir = config.browser.userDataDir ?? await createUserDataDir(config, clientInfo);
|
||||||
|
const tracesDir = await computeTracesDir(config, clientInfo);
|
||||||
|
if (await isProfileLocked5Times(userDataDir))
|
||||||
|
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
|
||||||
|
const browserType = playwright[config.browser.browserName];
|
||||||
|
const launchOptions = {
|
||||||
|
tracesDir,
|
||||||
|
...config.browser.launchOptions,
|
||||||
|
...config.browser.contextOptions,
|
||||||
|
handleSIGINT: false,
|
||||||
|
handleSIGTERM: false,
|
||||||
|
ignoreDefaultArgs: [
|
||||||
|
"--disable-extensions"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
|
||||||
|
const browser = browserContext.browser();
|
||||||
|
await startServer(browser, clientInfo);
|
||||||
|
return browser;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes("Executable doesn't exist"))
|
||||||
|
throwBrowserIsNotInstalledError(config);
|
||||||
|
if (error.message.includes("cannot open shared object file: No such file or directory")) {
|
||||||
|
const browserName = launchOptions.channel ?? config.browser.browserName;
|
||||||
|
throw new Error(`Missing system dependencies required to run browser ${browserName}. Install them with: sudo npx playwright install-deps ${browserName}`);
|
||||||
|
}
|
||||||
|
if (error.message.includes("ProcessSingleton") || error.message.includes("exitCode=21"))
|
||||||
|
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function createUserDataDir(config, clientInfo) {
|
||||||
|
const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? import_registry.registryDirectory;
|
||||||
|
const browserToken = config.browser.launchOptions?.channel ?? config.browser?.browserName;
|
||||||
|
const rootPathToken = createHash(clientInfo.cwd);
|
||||||
|
const result = import_path.default.join(dir, `mcp-${browserToken}-${rootPathToken}`);
|
||||||
|
await import_fs.default.promises.mkdir(result, { recursive: true });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
async function injectCdpPort(browserConfig) {
|
||||||
|
if (browserConfig.browserName === "chromium")
|
||||||
|
browserConfig.launchOptions.cdpPort = await findFreePort();
|
||||||
|
}
|
||||||
|
async function findFreePort() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = import_net.default.createServer();
|
||||||
|
server.listen(0, "127.0.0.1", () => {
|
||||||
|
const { port } = server.address();
|
||||||
|
server.close(() => resolve(port));
|
||||||
|
});
|
||||||
|
server.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function createHash(data) {
|
||||||
|
return import_crypto.default.createHash("sha256").update(data).digest("hex").slice(0, 7);
|
||||||
|
}
|
||||||
|
async function computeTracesDir(config, clientInfo) {
|
||||||
|
return import_path.default.resolve((0, import_context.outputDir)({ config, cwd: clientInfo.cwd }), "traces");
|
||||||
|
}
|
||||||
|
async function isProfileLocked5Times(userDataDir) {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if (!isProfileLocked(userDataDir))
|
||||||
|
return false;
|
||||||
|
await new Promise((f) => setTimeout(f, 1e3));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function isProfileLocked(userDataDir) {
|
||||||
|
const lockFile = process.platform === "win32" ? "lockfile" : "SingletonLock";
|
||||||
|
const lockPath = import_path.default.join(userDataDir, lockFile);
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
try {
|
||||||
|
const fd = import_fs.default.openSync(lockPath, "r+");
|
||||||
|
import_fs.default.closeSync(fd);
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return e.code !== "ENOENT";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const target = import_fs.default.readlinkSync(lockPath);
|
||||||
|
const pid = parseInt(target.split("-").pop() || "", 10);
|
||||||
|
if (isNaN(pid))
|
||||||
|
return false;
|
||||||
|
process.kill(pid, 0);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function throwBrowserIsNotInstalledError(config) {
|
||||||
|
const channel = config.browser.launchOptions?.channel ?? config.browser.browserName;
|
||||||
|
if (config.skillMode)
|
||||||
|
throw new Error(`Browser "${channel}" is not installed. Run \`playwright-cli install-browser ${channel}\` to install`);
|
||||||
|
else
|
||||||
|
throw new Error(`Browser "${channel}" is not installed. Run \`npx @playwright/mcp install-browser ${channel}\` to install`);
|
||||||
|
}
|
||||||
|
async function startServer(browser, clientInfo) {
|
||||||
|
if (clientInfo.sessionName)
|
||||||
|
await browser.bind(clientInfo.sessionName, { workspaceDir: clientInfo.workspaceDir });
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
createBrowser,
|
||||||
|
createBrowserWithInfo,
|
||||||
|
isProfileLocked
|
||||||
|
});
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var cdpRelay_exports = {};
|
||||||
|
__export(cdpRelay_exports, {
|
||||||
|
CDPRelayServer: () => CDPRelayServer
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(cdpRelay_exports);
|
||||||
|
var import_child_process = require("child_process");
|
||||||
|
var import_os = __toESM(require("os"));
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
var import_registry = require("../../server/registry/index");
|
||||||
|
var import_manualPromise = require("../../utils/isomorphic/manualPromise");
|
||||||
|
var import_http2 = require("../utils/mcp/http");
|
||||||
|
var import_log = require("./log");
|
||||||
|
var protocol = __toESM(require("./protocol"));
|
||||||
|
const debugLogger = (0, import_utilsBundle.debug)("pw:mcp:relay");
|
||||||
|
class CDPRelayServer {
|
||||||
|
constructor(server, browserChannel, userDataDir, executablePath) {
|
||||||
|
this._playwrightConnection = null;
|
||||||
|
this._extensionConnection = null;
|
||||||
|
this._nextSessionId = 1;
|
||||||
|
this._wsHost = (0, import_http2.addressToString)(server.address(), { protocol: "ws" });
|
||||||
|
this._browserChannel = browserChannel;
|
||||||
|
this._userDataDir = userDataDir;
|
||||||
|
this._executablePath = executablePath;
|
||||||
|
const uuid = crypto.randomUUID();
|
||||||
|
this._cdpPath = `/cdp/${uuid}`;
|
||||||
|
this._extensionPath = `/extension/${uuid}`;
|
||||||
|
this._resetExtensionConnection();
|
||||||
|
this._wss = new import_utilsBundle.wsServer({ server });
|
||||||
|
this._wss.on("connection", this._onConnection.bind(this));
|
||||||
|
}
|
||||||
|
cdpEndpoint() {
|
||||||
|
return `${this._wsHost}${this._cdpPath}`;
|
||||||
|
}
|
||||||
|
extensionEndpoint() {
|
||||||
|
return `${this._wsHost}${this._extensionPath}`;
|
||||||
|
}
|
||||||
|
async ensureExtensionConnectionForMCPContext(clientInfo) {
|
||||||
|
debugLogger("Ensuring extension connection for MCP context");
|
||||||
|
if (this._extensionConnection)
|
||||||
|
return;
|
||||||
|
this._connectBrowser(clientInfo);
|
||||||
|
debugLogger("Waiting for incoming extension connection");
|
||||||
|
await Promise.race([
|
||||||
|
this._extensionConnectionPromise,
|
||||||
|
new Promise((_, reject) => setTimeout(() => {
|
||||||
|
reject(new Error(`Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed. See https://github.com/microsoft/playwright-mcp/blob/main/packages/extension/README.md for installation instructions.`));
|
||||||
|
}, process.env.PWMCP_TEST_CONNECTION_TIMEOUT ? parseInt(process.env.PWMCP_TEST_CONNECTION_TIMEOUT, 10) : 5e3))
|
||||||
|
]);
|
||||||
|
debugLogger("Extension connection established");
|
||||||
|
}
|
||||||
|
_connectBrowser(clientInfo) {
|
||||||
|
const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
|
||||||
|
const url = new URL("chrome-extension://mmlmfjhmonkocbjadbfplnigmagldckm/connect.html");
|
||||||
|
url.searchParams.set("mcpRelayUrl", mcpRelayEndpoint);
|
||||||
|
const client = {
|
||||||
|
name: "Playwright Agent",
|
||||||
|
version: require("../../../package.json").version
|
||||||
|
};
|
||||||
|
url.searchParams.set("client", JSON.stringify(client));
|
||||||
|
url.searchParams.set("protocolVersion", process.env.PWMCP_TEST_PROTOCOL_VERSION ?? protocol.VERSION.toString());
|
||||||
|
const token = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
||||||
|
if (token)
|
||||||
|
url.searchParams.set("token", token);
|
||||||
|
const href = url.toString();
|
||||||
|
const channel = import_registry.registry.isChromiumAlias(this._browserChannel) ? "chromium" : this._browserChannel;
|
||||||
|
let executablePath = this._executablePath;
|
||||||
|
if (!executablePath) {
|
||||||
|
const executableInfo = import_registry.registry.findExecutable(channel);
|
||||||
|
if (!executableInfo)
|
||||||
|
throw new Error(`Unsupported channel: "${this._browserChannel}"`);
|
||||||
|
executablePath = executableInfo.executablePath();
|
||||||
|
if (!executablePath)
|
||||||
|
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
|
||||||
|
}
|
||||||
|
const args = [];
|
||||||
|
if (this._userDataDir)
|
||||||
|
args.push(`--user-data-dir=${this._userDataDir}`);
|
||||||
|
if (import_os.default.platform() === "linux" && channel === "chromium")
|
||||||
|
args.push("--no-sandbox");
|
||||||
|
args.push(href);
|
||||||
|
(0, import_child_process.spawn)(executablePath, args, {
|
||||||
|
windowsHide: true,
|
||||||
|
detached: true,
|
||||||
|
shell: false,
|
||||||
|
stdio: "ignore"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
this.closeConnections("Server stopped");
|
||||||
|
this._wss.close();
|
||||||
|
}
|
||||||
|
closeConnections(reason) {
|
||||||
|
this._closePlaywrightConnection(reason);
|
||||||
|
this._closeExtensionConnection(reason);
|
||||||
|
}
|
||||||
|
_onConnection(ws2, request) {
|
||||||
|
const url = new URL(`http://localhost${request.url}`);
|
||||||
|
debugLogger(`New connection to ${url.pathname}`);
|
||||||
|
if (url.pathname === this._cdpPath) {
|
||||||
|
this._handlePlaywrightConnection(ws2);
|
||||||
|
} else if (url.pathname === this._extensionPath) {
|
||||||
|
this._handleExtensionConnection(ws2);
|
||||||
|
} else {
|
||||||
|
debugLogger(`Invalid path: ${url.pathname}`);
|
||||||
|
ws2.close(4004, "Invalid path");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_handlePlaywrightConnection(ws2) {
|
||||||
|
if (this._playwrightConnection) {
|
||||||
|
debugLogger("Rejecting second Playwright connection");
|
||||||
|
ws2.close(1e3, "Another CDP client already connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._playwrightConnection = ws2;
|
||||||
|
ws2.on("message", async (data) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(data.toString());
|
||||||
|
await this._handlePlaywrightMessage(message);
|
||||||
|
} catch (error) {
|
||||||
|
debugLogger(`Error while handling Playwright message
|
||||||
|
${data.toString()}
|
||||||
|
`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ws2.on("close", () => {
|
||||||
|
if (this._playwrightConnection !== ws2)
|
||||||
|
return;
|
||||||
|
this._playwrightConnection = null;
|
||||||
|
this._closeExtensionConnection("Playwright client disconnected");
|
||||||
|
debugLogger("Playwright WebSocket closed");
|
||||||
|
});
|
||||||
|
ws2.on("error", (error) => {
|
||||||
|
debugLogger("Playwright WebSocket error:", error);
|
||||||
|
});
|
||||||
|
debugLogger("Playwright MCP connected");
|
||||||
|
}
|
||||||
|
_closeExtensionConnection(reason) {
|
||||||
|
this._extensionConnection?.close(reason);
|
||||||
|
this._extensionConnectionPromise.reject(new Error(reason));
|
||||||
|
this._resetExtensionConnection();
|
||||||
|
}
|
||||||
|
_resetExtensionConnection() {
|
||||||
|
this._connectedTabInfo = void 0;
|
||||||
|
this._extensionConnection = null;
|
||||||
|
this._extensionConnectionPromise = new import_manualPromise.ManualPromise();
|
||||||
|
void this._extensionConnectionPromise.catch(import_log.logUnhandledError);
|
||||||
|
}
|
||||||
|
_closePlaywrightConnection(reason) {
|
||||||
|
if (this._playwrightConnection?.readyState === import_utilsBundle.ws.OPEN)
|
||||||
|
this._playwrightConnection.close(1e3, reason);
|
||||||
|
this._playwrightConnection = null;
|
||||||
|
}
|
||||||
|
_handleExtensionConnection(ws2) {
|
||||||
|
if (this._extensionConnection) {
|
||||||
|
ws2.close(1e3, "Another extension connection already established");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._extensionConnection = new ExtensionConnection(ws2);
|
||||||
|
this._extensionConnection.onclose = (c, reason) => {
|
||||||
|
debugLogger("Extension WebSocket closed:", reason, c === this._extensionConnection);
|
||||||
|
if (this._extensionConnection !== c)
|
||||||
|
return;
|
||||||
|
this._resetExtensionConnection();
|
||||||
|
this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
|
||||||
|
};
|
||||||
|
this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
|
||||||
|
this._extensionConnectionPromise.resolve();
|
||||||
|
}
|
||||||
|
_handleExtensionMessage(method, params) {
|
||||||
|
switch (method) {
|
||||||
|
case "forwardCDPEvent":
|
||||||
|
const sessionId = params.sessionId || this._connectedTabInfo?.sessionId;
|
||||||
|
this._sendToPlaywright({
|
||||||
|
sessionId,
|
||||||
|
method: params.method,
|
||||||
|
params: params.params
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _handlePlaywrightMessage(message) {
|
||||||
|
debugLogger("\u2190 Playwright:", `${message.method} (id=${message.id})`);
|
||||||
|
const { id, sessionId, method, params } = message;
|
||||||
|
try {
|
||||||
|
const result = await this._handleCDPCommand(method, params, sessionId);
|
||||||
|
this._sendToPlaywright({ id, sessionId, result });
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger("Error in the extension:", e);
|
||||||
|
this._sendToPlaywright({
|
||||||
|
id,
|
||||||
|
sessionId,
|
||||||
|
error: { message: e.message }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _handleCDPCommand(method, params, sessionId) {
|
||||||
|
switch (method) {
|
||||||
|
case "Browser.getVersion": {
|
||||||
|
return {
|
||||||
|
protocolVersion: "1.3",
|
||||||
|
product: "Chrome/Extension-Bridge",
|
||||||
|
userAgent: "CDP-Bridge-Server/1.0.0"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "Browser.setDownloadBehavior": {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
case "Target.setAutoAttach": {
|
||||||
|
if (sessionId)
|
||||||
|
break;
|
||||||
|
const { targetInfo } = await this._extensionConnection.send("attachToTab", {});
|
||||||
|
this._connectedTabInfo = {
|
||||||
|
targetInfo,
|
||||||
|
sessionId: `pw-tab-${this._nextSessionId++}`
|
||||||
|
};
|
||||||
|
debugLogger("Simulating auto-attach");
|
||||||
|
this._sendToPlaywright({
|
||||||
|
method: "Target.attachedToTarget",
|
||||||
|
params: {
|
||||||
|
sessionId: this._connectedTabInfo.sessionId,
|
||||||
|
targetInfo: {
|
||||||
|
...this._connectedTabInfo.targetInfo,
|
||||||
|
attached: true
|
||||||
|
},
|
||||||
|
waitingForDebugger: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
case "Target.getTargetInfo": {
|
||||||
|
return this._connectedTabInfo?.targetInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await this._forwardToExtension(method, params, sessionId);
|
||||||
|
}
|
||||||
|
async _forwardToExtension(method, params, sessionId) {
|
||||||
|
if (!this._extensionConnection)
|
||||||
|
throw new Error("Extension not connected");
|
||||||
|
if (this._connectedTabInfo?.sessionId === sessionId)
|
||||||
|
sessionId = void 0;
|
||||||
|
return await this._extensionConnection.send("forwardCDPCommand", { sessionId, method, params });
|
||||||
|
}
|
||||||
|
_sendToPlaywright(message) {
|
||||||
|
debugLogger("\u2192 Playwright:", `${message.method ?? `response(id=${message.id})`}`);
|
||||||
|
this._playwrightConnection?.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ExtensionConnection {
|
||||||
|
constructor(ws2) {
|
||||||
|
this._callbacks = /* @__PURE__ */ new Map();
|
||||||
|
this._lastId = 0;
|
||||||
|
this._ws = ws2;
|
||||||
|
this._ws.on("message", this._onMessage.bind(this));
|
||||||
|
this._ws.on("close", this._onClose.bind(this));
|
||||||
|
this._ws.on("error", this._onError.bind(this));
|
||||||
|
}
|
||||||
|
async send(method, params) {
|
||||||
|
if (this._ws.readyState !== import_utilsBundle.ws.OPEN)
|
||||||
|
throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`);
|
||||||
|
const id = ++this._lastId;
|
||||||
|
this._ws.send(JSON.stringify({ id, method, params }));
|
||||||
|
const error = new Error(`Protocol error: ${method}`);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._callbacks.set(id, { resolve, reject, error });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
close(message) {
|
||||||
|
debugLogger("closing extension connection:", message);
|
||||||
|
if (this._ws.readyState === import_utilsBundle.ws.OPEN)
|
||||||
|
this._ws.close(1e3, message);
|
||||||
|
}
|
||||||
|
_onMessage(event) {
|
||||||
|
const eventData = event.toString();
|
||||||
|
let parsedJson;
|
||||||
|
try {
|
||||||
|
parsedJson = JSON.parse(eventData);
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger(`<closing ws> Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`);
|
||||||
|
this._ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this._handleParsedMessage(parsedJson);
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger(`<closing ws> Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`);
|
||||||
|
this._ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_handleParsedMessage(object) {
|
||||||
|
if (object.id && this._callbacks.has(object.id)) {
|
||||||
|
const callback = this._callbacks.get(object.id);
|
||||||
|
this._callbacks.delete(object.id);
|
||||||
|
if (object.error) {
|
||||||
|
const error = callback.error;
|
||||||
|
error.message = object.error;
|
||||||
|
callback.reject(error);
|
||||||
|
} else {
|
||||||
|
callback.resolve(object.result);
|
||||||
|
}
|
||||||
|
} else if (object.id) {
|
||||||
|
debugLogger("\u2190 Extension: unexpected response", object);
|
||||||
|
} else {
|
||||||
|
this.onmessage?.(object.method, object.params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_onClose(event) {
|
||||||
|
debugLogger(`<ws closed> code=${event.code} reason=${event.reason}`);
|
||||||
|
this._dispose();
|
||||||
|
this.onclose?.(this, event.reason);
|
||||||
|
}
|
||||||
|
_onError(event) {
|
||||||
|
debugLogger(`<ws error> message=${event.message} type=${event.type} target=${event.target}`);
|
||||||
|
this._dispose();
|
||||||
|
}
|
||||||
|
_dispose() {
|
||||||
|
for (const callback of this._callbacks.values())
|
||||||
|
callback.reject(new Error("WebSocket closed"));
|
||||||
|
this._callbacks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
CDPRelayServer
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
"use strict";
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
var import_program = require("./program");
|
||||||
|
const packageJSON = require("../../../package.json");
|
||||||
|
const p = import_utilsBundle.program.version("Version " + packageJSON.version).name("Playwright MCP");
|
||||||
|
(0, import_program.decorateMCPCommand)(p);
|
||||||
|
void import_utilsBundle.program.parseAsync(process.argv);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var config_d_exports = {};
|
||||||
|
module.exports = __toCommonJS(config_d_exports);
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var config_exports = {};
|
||||||
|
__export(config_exports, {
|
||||||
|
commaSeparatedList: () => commaSeparatedList,
|
||||||
|
configFromEnv: () => configFromEnv,
|
||||||
|
dotenvFileLoader: () => dotenvFileLoader,
|
||||||
|
enumParser: () => enumParser,
|
||||||
|
headerParser: () => headerParser,
|
||||||
|
loadConfig: () => loadConfig,
|
||||||
|
numberParser: () => numberParser,
|
||||||
|
resolutionParser: () => resolutionParser,
|
||||||
|
resolveCLIConfigForCLI: () => resolveCLIConfigForCLI,
|
||||||
|
resolveCLIConfigForMCP: () => resolveCLIConfigForMCP,
|
||||||
|
resolveConfig: () => resolveConfig,
|
||||||
|
semicolonSeparatedList: () => semicolonSeparatedList
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(config_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_path = __toESM(require("path"));
|
||||||
|
var import_os = __toESM(require("os"));
|
||||||
|
var import__ = require("../../..");
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
var import_configIni = require("./configIni");
|
||||||
|
async function fileExistsAsync(resolved) {
|
||||||
|
try {
|
||||||
|
return (await import_fs.default.promises.stat(resolved)).isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const defaultConfig = {
|
||||||
|
browser: {
|
||||||
|
launchOptions: {},
|
||||||
|
contextOptions: {}
|
||||||
|
},
|
||||||
|
timeouts: {
|
||||||
|
action: 5e3,
|
||||||
|
navigation: 6e4,
|
||||||
|
expect: 5e3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
async function resolveConfig(config) {
|
||||||
|
const merged = mergeConfig(defaultConfig, config);
|
||||||
|
const browser = await validateBrowserConfig(merged.browser);
|
||||||
|
return { ...merged, browser };
|
||||||
|
}
|
||||||
|
async function resolveCLIConfigForMCP(cliOptions, env) {
|
||||||
|
const envOverrides = configFromEnv(env);
|
||||||
|
const cliOverrides = configFromCLIOptions(cliOptions);
|
||||||
|
const configFile = cliOverrides.configFile ?? envOverrides.configFile;
|
||||||
|
const configInFile = await loadConfig(configFile);
|
||||||
|
let result = defaultConfig;
|
||||||
|
result = mergeConfig(result, configInFile);
|
||||||
|
result = mergeConfig(result, envOverrides);
|
||||||
|
result = mergeConfig(result, cliOverrides);
|
||||||
|
const browser = await validateBrowserConfig(result.browser);
|
||||||
|
if (browser.launchOptions.headless === void 0)
|
||||||
|
browser.launchOptions.headless = import_os.default.platform() === "linux" && !process.env.DISPLAY;
|
||||||
|
return { ...result, browser, configFile };
|
||||||
|
}
|
||||||
|
async function resolveCLIConfigForCLI(daemonProfilesDir, sessionName, options, env) {
|
||||||
|
const config = options.config ? import_path.default.resolve(options.config) : void 0;
|
||||||
|
try {
|
||||||
|
const defaultConfigFile = import_path.default.resolve(".playwright", "cli.config.json");
|
||||||
|
if (!config && import_fs.default.existsSync(defaultConfigFile))
|
||||||
|
options.config = defaultConfigFile;
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
const daemonOverrides = configFromCLIOptions({
|
||||||
|
endpoint: options.endpoint,
|
||||||
|
config: options.config,
|
||||||
|
browser: options.browser,
|
||||||
|
headless: options.headed ? false : void 0,
|
||||||
|
extension: options.extension,
|
||||||
|
userDataDir: options.profile,
|
||||||
|
snapshotMode: "full"
|
||||||
|
});
|
||||||
|
const envOverrides = configFromEnv(env);
|
||||||
|
const configFile = daemonOverrides.configFile ?? envOverrides.configFile;
|
||||||
|
const configInFile = await loadConfig(configFile);
|
||||||
|
const globalConfigPath = import_path.default.join((env ?? process.env)["PWTEST_CLI_GLOBAL_CONFIG"] ?? import_os.default.homedir(), ".playwright", "cli.config.json");
|
||||||
|
const globalConfigInFile = await loadConfig(import_fs.default.existsSync(globalConfigPath) ? globalConfigPath : void 0);
|
||||||
|
let result = defaultConfig;
|
||||||
|
result = mergeConfig(result, globalConfigInFile);
|
||||||
|
result = mergeConfig(result, configInFile);
|
||||||
|
result = mergeConfig(result, envOverrides);
|
||||||
|
result = mergeConfig(result, daemonOverrides);
|
||||||
|
if (result.browser.isolated === void 0)
|
||||||
|
result.browser.isolated = !options.profile && !options.persistent && !result.browser.userDataDir && !result.browser.remoteEndpoint && !result.extension;
|
||||||
|
if (!result.extension && !result.browser.isolated && !result.browser.userDataDir && !result.browser.remoteEndpoint) {
|
||||||
|
const browserToken = result.browser.launchOptions?.channel ?? result.browser?.browserName;
|
||||||
|
const userDataDir = import_path.default.resolve(daemonProfilesDir, `ud-${sessionName}-${browserToken}`);
|
||||||
|
result.browser.userDataDir = userDataDir;
|
||||||
|
}
|
||||||
|
if (result.browser.launchOptions.headless === void 0)
|
||||||
|
result.browser.launchOptions.headless = true;
|
||||||
|
const browser = await validateBrowserConfig(result.browser);
|
||||||
|
return { ...result, browser, configFile, skillMode: true };
|
||||||
|
}
|
||||||
|
async function validateBrowserConfig(browser) {
|
||||||
|
let browserName = browser.browserName;
|
||||||
|
if (!browserName) {
|
||||||
|
browserName = "chromium";
|
||||||
|
if (browser.launchOptions.channel === void 0)
|
||||||
|
browser.launchOptions.channel = "chrome";
|
||||||
|
}
|
||||||
|
if (browser.browserName === "chromium" && browser.launchOptions.chromiumSandbox === void 0) {
|
||||||
|
if (process.platform === "linux")
|
||||||
|
browser.launchOptions.chromiumSandbox = browser.launchOptions.channel !== "chromium" && browser.launchOptions.channel !== "chrome-for-testing";
|
||||||
|
else
|
||||||
|
browser.launchOptions.chromiumSandbox = true;
|
||||||
|
}
|
||||||
|
if (browser.isolated && browser.userDataDir)
|
||||||
|
throw new Error("Browser userDataDir is not supported in isolated mode.");
|
||||||
|
if (browser.initScript) {
|
||||||
|
for (const script of browser.initScript) {
|
||||||
|
if (!await fileExistsAsync(script))
|
||||||
|
throw new Error(`Init script file does not exist: ${script}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (browser.initPage) {
|
||||||
|
for (const page of browser.initPage) {
|
||||||
|
if (!await fileExistsAsync(page))
|
||||||
|
throw new Error(`Init page file does not exist: ${page}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (browser.contextOptions.viewport === void 0) {
|
||||||
|
if (browser.launchOptions.headless)
|
||||||
|
browser.contextOptions.viewport = { width: 1280, height: 720 };
|
||||||
|
else
|
||||||
|
browser.contextOptions.viewport = null;
|
||||||
|
}
|
||||||
|
return { ...browser, browserName };
|
||||||
|
}
|
||||||
|
function configFromCLIOptions(cliOptions) {
|
||||||
|
let browserName;
|
||||||
|
let channel;
|
||||||
|
switch (cliOptions.browser) {
|
||||||
|
case "chrome":
|
||||||
|
case "chrome-beta":
|
||||||
|
case "chrome-canary":
|
||||||
|
case "chrome-dev":
|
||||||
|
case "msedge":
|
||||||
|
case "msedge-beta":
|
||||||
|
case "msedge-canary":
|
||||||
|
case "msedge-dev":
|
||||||
|
browserName = "chromium";
|
||||||
|
channel = cliOptions.browser;
|
||||||
|
break;
|
||||||
|
case "chromium":
|
||||||
|
browserName = "chromium";
|
||||||
|
channel = "chrome-for-testing";
|
||||||
|
break;
|
||||||
|
case "firefox":
|
||||||
|
browserName = "firefox";
|
||||||
|
break;
|
||||||
|
case "webkit":
|
||||||
|
browserName = "webkit";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const launchOptions = {
|
||||||
|
channel,
|
||||||
|
executablePath: cliOptions.executablePath,
|
||||||
|
headless: cliOptions.headless
|
||||||
|
};
|
||||||
|
if (cliOptions.sandbox !== void 0)
|
||||||
|
launchOptions.chromiumSandbox = cliOptions.sandbox;
|
||||||
|
if (cliOptions.proxyServer) {
|
||||||
|
launchOptions.proxy = {
|
||||||
|
server: cliOptions.proxyServer
|
||||||
|
};
|
||||||
|
if (cliOptions.proxyBypass)
|
||||||
|
launchOptions.proxy.bypass = cliOptions.proxyBypass;
|
||||||
|
}
|
||||||
|
if (cliOptions.device && cliOptions.cdpEndpoint)
|
||||||
|
throw new Error("Device emulation is not supported with cdpEndpoint.");
|
||||||
|
const contextOptions = cliOptions.device ? import__.devices[cliOptions.device] : {};
|
||||||
|
if (cliOptions.storageState)
|
||||||
|
contextOptions.storageState = cliOptions.storageState;
|
||||||
|
if (cliOptions.userAgent)
|
||||||
|
contextOptions.userAgent = cliOptions.userAgent;
|
||||||
|
if (cliOptions.viewportSize)
|
||||||
|
contextOptions.viewport = cliOptions.viewportSize;
|
||||||
|
if (cliOptions.ignoreHttpsErrors)
|
||||||
|
contextOptions.ignoreHTTPSErrors = true;
|
||||||
|
if (cliOptions.blockServiceWorkers)
|
||||||
|
contextOptions.serviceWorkers = "block";
|
||||||
|
if (cliOptions.grantPermissions)
|
||||||
|
contextOptions.permissions = cliOptions.grantPermissions;
|
||||||
|
const config = {
|
||||||
|
browser: {
|
||||||
|
browserName,
|
||||||
|
isolated: cliOptions.isolated,
|
||||||
|
userDataDir: cliOptions.userDataDir,
|
||||||
|
launchOptions,
|
||||||
|
contextOptions,
|
||||||
|
cdpEndpoint: cliOptions.cdpEndpoint,
|
||||||
|
cdpHeaders: cliOptions.cdpHeader,
|
||||||
|
cdpTimeout: cliOptions.cdpTimeout,
|
||||||
|
initPage: cliOptions.initPage,
|
||||||
|
initScript: cliOptions.initScript,
|
||||||
|
remoteEndpoint: cliOptions.endpoint
|
||||||
|
},
|
||||||
|
extension: cliOptions.extension,
|
||||||
|
server: {
|
||||||
|
port: cliOptions.port,
|
||||||
|
host: cliOptions.host,
|
||||||
|
allowedHosts: cliOptions.allowedHosts
|
||||||
|
},
|
||||||
|
capabilities: cliOptions.caps,
|
||||||
|
console: {
|
||||||
|
level: cliOptions.consoleLevel
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
allowedOrigins: cliOptions.allowedOrigins,
|
||||||
|
blockedOrigins: cliOptions.blockedOrigins
|
||||||
|
},
|
||||||
|
allowUnrestrictedFileAccess: cliOptions.allowUnrestrictedFileAccess,
|
||||||
|
codegen: cliOptions.codegen,
|
||||||
|
saveSession: cliOptions.saveSession,
|
||||||
|
secrets: cliOptions.secrets,
|
||||||
|
sharedBrowserContext: cliOptions.sharedBrowserContext,
|
||||||
|
snapshot: cliOptions.snapshotMode ? { mode: cliOptions.snapshotMode } : void 0,
|
||||||
|
outputDir: cliOptions.outputDir,
|
||||||
|
imageResponses: cliOptions.imageResponses,
|
||||||
|
testIdAttribute: cliOptions.testIdAttribute,
|
||||||
|
timeouts: {
|
||||||
|
action: cliOptions.timeoutAction,
|
||||||
|
navigation: cliOptions.timeoutNavigation
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { ...config, configFile: cliOptions.config };
|
||||||
|
}
|
||||||
|
function configFromEnv(env) {
|
||||||
|
const e = env ?? process.env;
|
||||||
|
const options = {};
|
||||||
|
options.allowedHosts = commaSeparatedList(e.PLAYWRIGHT_MCP_ALLOWED_HOSTS);
|
||||||
|
options.allowedOrigins = semicolonSeparatedList(e.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
|
||||||
|
options.allowUnrestrictedFileAccess = envToBoolean(e.PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS);
|
||||||
|
options.blockedOrigins = semicolonSeparatedList(e.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
|
||||||
|
options.blockServiceWorkers = envToBoolean(e.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
|
||||||
|
options.browser = envToString(e.PLAYWRIGHT_MCP_BROWSER);
|
||||||
|
options.caps = commaSeparatedList(e.PLAYWRIGHT_MCP_CAPS);
|
||||||
|
options.cdpEndpoint = envToString(e.PLAYWRIGHT_MCP_CDP_ENDPOINT);
|
||||||
|
options.cdpHeader = headerParser(envToString(e.PLAYWRIGHT_MCP_CDP_HEADERS));
|
||||||
|
options.cdpTimeout = numberParser(e.PLAYWRIGHT_MCP_CDP_TIMEOUT);
|
||||||
|
options.config = envToString(e.PLAYWRIGHT_MCP_CONFIG);
|
||||||
|
if (e.PLAYWRIGHT_MCP_CONSOLE_LEVEL)
|
||||||
|
options.consoleLevel = enumParser("--console-level", ["error", "warning", "info", "debug"], e.PLAYWRIGHT_MCP_CONSOLE_LEVEL);
|
||||||
|
options.device = envToString(e.PLAYWRIGHT_MCP_DEVICE);
|
||||||
|
options.executablePath = envToString(e.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
|
||||||
|
options.extension = envToBoolean(e.PLAYWRIGHT_MCP_EXTENSION);
|
||||||
|
options.grantPermissions = commaSeparatedList(e.PLAYWRIGHT_MCP_GRANT_PERMISSIONS);
|
||||||
|
options.headless = envToBoolean(e.PLAYWRIGHT_MCP_HEADLESS);
|
||||||
|
options.host = envToString(e.PLAYWRIGHT_MCP_HOST);
|
||||||
|
options.ignoreHttpsErrors = envToBoolean(e.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
|
||||||
|
const initPage = envToString(e.PLAYWRIGHT_MCP_INIT_PAGE);
|
||||||
|
if (initPage)
|
||||||
|
options.initPage = [initPage];
|
||||||
|
const initScript = envToString(e.PLAYWRIGHT_MCP_INIT_SCRIPT);
|
||||||
|
if (initScript)
|
||||||
|
options.initScript = [initScript];
|
||||||
|
options.isolated = envToBoolean(e.PLAYWRIGHT_MCP_ISOLATED);
|
||||||
|
if (e.PLAYWRIGHT_MCP_IMAGE_RESPONSES)
|
||||||
|
options.imageResponses = enumParser("--image-responses", ["allow", "omit"], e.PLAYWRIGHT_MCP_IMAGE_RESPONSES);
|
||||||
|
options.sandbox = envToBoolean(e.PLAYWRIGHT_MCP_SANDBOX);
|
||||||
|
options.outputDir = envToString(e.PLAYWRIGHT_MCP_OUTPUT_DIR);
|
||||||
|
options.port = numberParser(e.PLAYWRIGHT_MCP_PORT);
|
||||||
|
options.proxyBypass = envToString(e.PLAYWRIGHT_MCP_PROXY_BYPASS);
|
||||||
|
options.proxyServer = envToString(e.PLAYWRIGHT_MCP_PROXY_SERVER);
|
||||||
|
options.secrets = dotenvFileLoader(e.PLAYWRIGHT_MCP_SECRETS_FILE);
|
||||||
|
options.storageState = envToString(e.PLAYWRIGHT_MCP_STORAGE_STATE);
|
||||||
|
options.testIdAttribute = envToString(e.PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE);
|
||||||
|
options.timeoutAction = numberParser(e.PLAYWRIGHT_MCP_TIMEOUT_ACTION);
|
||||||
|
options.timeoutNavigation = numberParser(e.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION);
|
||||||
|
options.userAgent = envToString(e.PLAYWRIGHT_MCP_USER_AGENT);
|
||||||
|
options.userDataDir = envToString(e.PLAYWRIGHT_MCP_USER_DATA_DIR);
|
||||||
|
options.viewportSize = resolutionParser("--viewport-size", e.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
|
||||||
|
return configFromCLIOptions(options);
|
||||||
|
}
|
||||||
|
async function loadConfig(configFile) {
|
||||||
|
if (!configFile)
|
||||||
|
return {};
|
||||||
|
if (configFile.endsWith(".ini"))
|
||||||
|
return (0, import_configIni.configFromIniFile)(configFile);
|
||||||
|
try {
|
||||||
|
const data = await import_fs.default.promises.readFile(configFile, "utf8");
|
||||||
|
return JSON.parse(data.charCodeAt(0) === 65279 ? data.slice(1) : data);
|
||||||
|
} catch {
|
||||||
|
return (0, import_configIni.configFromIniFile)(configFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function pickDefined(obj) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj ?? {}).filter(([_, v]) => v !== void 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function mergeConfig(base, overrides) {
|
||||||
|
const browser = {
|
||||||
|
...pickDefined(base.browser),
|
||||||
|
...pickDefined(overrides.browser),
|
||||||
|
browserName: overrides.browser?.browserName ?? base.browser?.browserName,
|
||||||
|
isolated: overrides.browser?.isolated ?? base.browser?.isolated,
|
||||||
|
launchOptions: {
|
||||||
|
...pickDefined(base.browser?.launchOptions),
|
||||||
|
...pickDefined(overrides.browser?.launchOptions),
|
||||||
|
// Assistant mode is not a part of the public API.
|
||||||
|
...{ assistantMode: true }
|
||||||
|
},
|
||||||
|
contextOptions: {
|
||||||
|
...pickDefined(base.browser?.contextOptions),
|
||||||
|
...pickDefined(overrides.browser?.contextOptions)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (browser.browserName !== "chromium" && browser.launchOptions)
|
||||||
|
delete browser.launchOptions.channel;
|
||||||
|
return {
|
||||||
|
...pickDefined(base),
|
||||||
|
...pickDefined(overrides),
|
||||||
|
browser,
|
||||||
|
console: {
|
||||||
|
...pickDefined(base.console),
|
||||||
|
...pickDefined(overrides.console)
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
...pickDefined(base.network),
|
||||||
|
...pickDefined(overrides.network)
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
...pickDefined(base.server),
|
||||||
|
...pickDefined(overrides.server)
|
||||||
|
},
|
||||||
|
snapshot: {
|
||||||
|
...pickDefined(base.snapshot),
|
||||||
|
...pickDefined(overrides.snapshot)
|
||||||
|
},
|
||||||
|
timeouts: {
|
||||||
|
...pickDefined(base.timeouts),
|
||||||
|
...pickDefined(overrides.timeouts)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function semicolonSeparatedList(value) {
|
||||||
|
if (!value)
|
||||||
|
return void 0;
|
||||||
|
return value.split(";").map((v) => v.trim());
|
||||||
|
}
|
||||||
|
function commaSeparatedList(value) {
|
||||||
|
if (!value)
|
||||||
|
return void 0;
|
||||||
|
return value.split(",").map((v) => v.trim());
|
||||||
|
}
|
||||||
|
function dotenvFileLoader(value) {
|
||||||
|
if (!value)
|
||||||
|
return void 0;
|
||||||
|
return import_utilsBundle.dotenv.parse(import_fs.default.readFileSync(value, "utf8"));
|
||||||
|
}
|
||||||
|
function numberParser(value) {
|
||||||
|
if (!value)
|
||||||
|
return void 0;
|
||||||
|
return +value;
|
||||||
|
}
|
||||||
|
function resolutionParser(name, value) {
|
||||||
|
if (!value)
|
||||||
|
return void 0;
|
||||||
|
if (value.includes("x")) {
|
||||||
|
const [width, height] = value.split("x").map((v) => +v);
|
||||||
|
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
|
||||||
|
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
if (value.includes(",")) {
|
||||||
|
const [width, height] = value.split(",").map((v) => +v);
|
||||||
|
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
|
||||||
|
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
|
||||||
|
}
|
||||||
|
function headerParser(arg, previous) {
|
||||||
|
if (!arg)
|
||||||
|
return previous;
|
||||||
|
const result = { ...previous ?? {} };
|
||||||
|
const colonIndex = arg.indexOf(":");
|
||||||
|
const name = colonIndex === -1 ? arg.trim() : arg.substring(0, colonIndex).trim();
|
||||||
|
const value = colonIndex === -1 ? "" : arg.substring(colonIndex + 1).trim();
|
||||||
|
result[name] = value;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function enumParser(name, options, value) {
|
||||||
|
if (!options.includes(value))
|
||||||
|
throw new Error(`Invalid ${name}: ${value}. Valid values are: ${options.join(", ")}`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function envToBoolean(value) {
|
||||||
|
if (value === "true" || value === "1")
|
||||||
|
return true;
|
||||||
|
if (value === "false" || value === "0")
|
||||||
|
return false;
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
function envToString(value) {
|
||||||
|
return value ? value.trim() : void 0;
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
commaSeparatedList,
|
||||||
|
configFromEnv,
|
||||||
|
dotenvFileLoader,
|
||||||
|
enumParser,
|
||||||
|
headerParser,
|
||||||
|
loadConfig,
|
||||||
|
numberParser,
|
||||||
|
resolutionParser,
|
||||||
|
resolveCLIConfigForCLI,
|
||||||
|
resolveCLIConfigForMCP,
|
||||||
|
resolveConfig,
|
||||||
|
semicolonSeparatedList
|
||||||
|
});
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var configIni_exports = {};
|
||||||
|
__export(configIni_exports, {
|
||||||
|
configFromIniFile: () => configFromIniFile,
|
||||||
|
configsFromIniFile: () => configsFromIniFile
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(configIni_exports);
|
||||||
|
var import_fs = __toESM(require("fs"));
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
function configFromIniFile(filePath) {
|
||||||
|
const content = import_fs.default.readFileSync(filePath, "utf8");
|
||||||
|
const parsed = import_utilsBundle.ini.parse(content);
|
||||||
|
return iniEntriesToConfig(parsed);
|
||||||
|
}
|
||||||
|
function configsFromIniFile(filePath) {
|
||||||
|
const content = import_fs.default.readFileSync(filePath, "utf8");
|
||||||
|
const parsed = import_utilsBundle.ini.parse(content);
|
||||||
|
const result = /* @__PURE__ */ new Map();
|
||||||
|
for (const [sectionName, sectionData] of Object.entries(parsed)) {
|
||||||
|
if (typeof sectionData !== "object" || sectionData === null)
|
||||||
|
continue;
|
||||||
|
result.set(sectionName, iniEntriesToConfig(sectionData));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function iniEntriesToConfig(entries) {
|
||||||
|
const config = {};
|
||||||
|
for (const [targetPath, rawValue] of Object.entries(entries)) {
|
||||||
|
const type = longhandTypes[targetPath];
|
||||||
|
const value = type ? coerceToType(rawValue, type) : coerceIniValue(rawValue);
|
||||||
|
setNestedValue(config, targetPath, value);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
function coerceToType(value, type) {
|
||||||
|
switch (type) {
|
||||||
|
case "string":
|
||||||
|
return String(value);
|
||||||
|
case "number":
|
||||||
|
return Number(value);
|
||||||
|
case "boolean":
|
||||||
|
if (typeof value === "boolean")
|
||||||
|
return value;
|
||||||
|
return value === "true" || value === "1";
|
||||||
|
case "string[]":
|
||||||
|
if (Array.isArray(value))
|
||||||
|
return value.map(String);
|
||||||
|
return [String(value)];
|
||||||
|
case "size": {
|
||||||
|
if (typeof value === "string" && value.includes("x")) {
|
||||||
|
const [w, h] = value.split("x").map(Number);
|
||||||
|
if (!isNaN(w) && !isNaN(h) && w > 0 && h > 0)
|
||||||
|
return { width: w, height: h };
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function coerceIniValue(value) {
|
||||||
|
if (typeof value !== "string")
|
||||||
|
return value;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed === "")
|
||||||
|
return trimmed;
|
||||||
|
const num = Number(trimmed);
|
||||||
|
if (!isNaN(num))
|
||||||
|
return num;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function setNestedValue(obj, dotPath, value) {
|
||||||
|
const parts = dotPath.split(".");
|
||||||
|
let current = obj;
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
if (!(part in current) || typeof current[part] !== "object" || current[part] === null)
|
||||||
|
current[part] = {};
|
||||||
|
current = current[part];
|
||||||
|
}
|
||||||
|
current[parts[parts.length - 1]] = value;
|
||||||
|
}
|
||||||
|
const longhandTypes = {
|
||||||
|
// browser direct
|
||||||
|
"browser.browserName": "string",
|
||||||
|
"browser.isolated": "boolean",
|
||||||
|
"browser.userDataDir": "string",
|
||||||
|
"browser.cdpEndpoint": "string",
|
||||||
|
"browser.cdpTimeout": "number",
|
||||||
|
"browser.remoteEndpoint": "string",
|
||||||
|
"browser.initPage": "string[]",
|
||||||
|
"browser.initScript": "string[]",
|
||||||
|
// browser.launchOptions
|
||||||
|
"browser.launchOptions.channel": "string",
|
||||||
|
"browser.launchOptions.headless": "boolean",
|
||||||
|
"browser.launchOptions.executablePath": "string",
|
||||||
|
"browser.launchOptions.chromiumSandbox": "boolean",
|
||||||
|
"browser.launchOptions.args": "string[]",
|
||||||
|
"browser.launchOptions.downloadsPath": "string",
|
||||||
|
"browser.launchOptions.handleSIGHUP": "boolean",
|
||||||
|
"browser.launchOptions.handleSIGINT": "boolean",
|
||||||
|
"browser.launchOptions.handleSIGTERM": "boolean",
|
||||||
|
"browser.launchOptions.slowMo": "number",
|
||||||
|
"browser.launchOptions.timeout": "number",
|
||||||
|
"browser.launchOptions.tracesDir": "string",
|
||||||
|
"browser.launchOptions.proxy.server": "string",
|
||||||
|
"browser.launchOptions.proxy.bypass": "string",
|
||||||
|
"browser.launchOptions.proxy.username": "string",
|
||||||
|
"browser.launchOptions.proxy.password": "string",
|
||||||
|
// browser.contextOptions
|
||||||
|
"browser.contextOptions.acceptDownloads": "boolean",
|
||||||
|
"browser.contextOptions.baseURL": "string",
|
||||||
|
"browser.contextOptions.bypassCSP": "boolean",
|
||||||
|
"browser.contextOptions.colorScheme": "string",
|
||||||
|
"browser.contextOptions.contrast": "string",
|
||||||
|
"browser.contextOptions.deviceScaleFactor": "number",
|
||||||
|
"browser.contextOptions.forcedColors": "string",
|
||||||
|
"browser.contextOptions.hasTouch": "boolean",
|
||||||
|
"browser.contextOptions.ignoreHTTPSErrors": "boolean",
|
||||||
|
"browser.contextOptions.isMobile": "boolean",
|
||||||
|
"browser.contextOptions.javaScriptEnabled": "boolean",
|
||||||
|
"browser.contextOptions.locale": "string",
|
||||||
|
"browser.contextOptions.offline": "boolean",
|
||||||
|
"browser.contextOptions.permissions": "string[]",
|
||||||
|
"browser.contextOptions.reducedMotion": "string",
|
||||||
|
"browser.contextOptions.screen": "size",
|
||||||
|
"browser.contextOptions.serviceWorkers": "string",
|
||||||
|
"browser.contextOptions.storageState": "string",
|
||||||
|
"browser.contextOptions.strictSelectors": "boolean",
|
||||||
|
"browser.contextOptions.timezoneId": "string",
|
||||||
|
"browser.contextOptions.userAgent": "string",
|
||||||
|
"browser.contextOptions.viewport": "size",
|
||||||
|
// top-level
|
||||||
|
"extension": "boolean",
|
||||||
|
"capabilities": "string[]",
|
||||||
|
"saveSession": "boolean",
|
||||||
|
"saveTrace": "boolean",
|
||||||
|
"saveVideo": "size",
|
||||||
|
"sharedBrowserContext": "boolean",
|
||||||
|
"outputDir": "string",
|
||||||
|
"imageResponses": "string",
|
||||||
|
"allowUnrestrictedFileAccess": "boolean",
|
||||||
|
"codegen": "string",
|
||||||
|
"testIdAttribute": "string",
|
||||||
|
// server
|
||||||
|
"server.port": "number",
|
||||||
|
"server.host": "string",
|
||||||
|
"server.allowedHosts": "string[]",
|
||||||
|
// console
|
||||||
|
"console.level": "string",
|
||||||
|
// network
|
||||||
|
"network.allowedOrigins": "string[]",
|
||||||
|
"network.blockedOrigins": "string[]",
|
||||||
|
// timeouts
|
||||||
|
"timeouts.action": "number",
|
||||||
|
"timeouts.navigation": "number",
|
||||||
|
// snapshot
|
||||||
|
"snapshot.mode": "string"
|
||||||
|
};
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
configFromIniFile,
|
||||||
|
configsFromIniFile
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"use strict";
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||||
|
// If the importer is in node compatibility mode or this is not an ESM
|
||||||
|
// file that has been converted to a CommonJS file using a Babel-
|
||||||
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||||
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||||
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||||
|
mod
|
||||||
|
));
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
var extensionContextFactory_exports = {};
|
||||||
|
__export(extensionContextFactory_exports, {
|
||||||
|
createExtensionBrowser: () => createExtensionBrowser
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(extensionContextFactory_exports);
|
||||||
|
var playwright = __toESM(require("../../.."));
|
||||||
|
var import_utilsBundle = require("../../utilsBundle");
|
||||||
|
var import_network = require("../../server/utils/network");
|
||||||
|
var import_cdpRelay = require("./cdpRelay");
|
||||||
|
const debugLogger = (0, import_utilsBundle.debug)("pw:mcp:relay");
|
||||||
|
async function createExtensionBrowser(config, clientInfo) {
|
||||||
|
const httpServer = (0, import_network.createHttpServer)();
|
||||||
|
await (0, import_network.startHttpServer)(httpServer, {});
|
||||||
|
const relay = new import_cdpRelay.CDPRelayServer(
|
||||||
|
httpServer,
|
||||||
|
config.browser.launchOptions.channel || "chrome",
|
||||||
|
config.browser.userDataDir,
|
||||||
|
config.browser.launchOptions.executablePath
|
||||||
|
);
|
||||||
|
debugLogger(`CDP relay server started, extension endpoint: ${relay.extensionEndpoint()}.`);
|
||||||
|
await relay.ensureExtensionConnectionForMCPContext(clientInfo);
|
||||||
|
return await playwright.chromium.connectOverCDP(relay.cdpEndpoint(), { isLocal: true });
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
createExtensionBrowser
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user