Compare commits

..
Author SHA1 Message Date
Jeppe B d42b58c4fe test: harden flaky chromium smoke specs 2026-06-10 15:27:14 +02:00
Jeppe B 724ad8e7a1 ci: stabilize Playwright runner installs and smoke tests 2026-06-10 02:57:02 +02:00
Jeppe B 2fe50729a5 ci: fall back for unsupported Playwright deps install 2026-06-10 00:52:14 +02:00
Jeppe B eb4eee1aef test: stabilize github runner e2e checks 2026-06-09 23:02:28 +02:00
Jeppe Bundgaard db972e2923 test: refresh invoicing flag counts from scoped flags 2026-06-09 16:03:23 +02:00
Jeppe Bundgaard 00817925e4 Add path case answer mapping and matching logic for improved path verification 2026-06-09 15:56:59 +02:00
Jeppe Bundgaard 347ab3c811 ci: allow e2e jobs on linux runner pool 2026-06-09 15:53:58 +02:00
Jeppe Bundgaard a3d05b15db test: stabilize edge gateway route readiness 2026-06-09 15:46:48 +02:00
Jeppe Bundgaard 9093d0d601 test: isolate request queue shortcut listeners 2026-06-09 15:20:15 +02:00
Jeppe Bundgaard 8c57b98627 Fix i18n CI runtime menu checks 2026-06-09 15:14:22 +02:00
Jeppe Bundgaard 4b99751c0e test: reset POS department route in unit setup 2026-06-09 15:13:06 +02:00
Jeppe Bundgaard 66f4331b4e Refactor machine allowance logic in self-serve studio and remove unused controls 2026-06-09 14:52:20 +02:00
Jeppe Bundgaard e1a824d565 Fix path editor answer row layout 2026-06-09 14:18:15 +02:00
Jeppe Bundgaard 883a88a551 ci: split playwright pr e2e matrix 2026-06-09 14:07:25 +02:00
Jeppe Bundgaard a328bc3767 Filter path editor to machine start cases 2026-06-09 13:57:33 +02:00
Jeppe Bundgaard 255447f788 Hide placeholder departments in admin navigation 2026-06-09 13:34:08 +02:00
Jeppe Bundgaard b3d57ca717 test: cover collected invoice move checks 2026-06-09 13:24:23 +02:00
Jeppe Bundgaard 45d21c4372 fix: restore self-serve lane step hook 2026-06-09 13:19:22 +02:00
Jeppe Bundgaard 7f7b5ae082 Add Swedish invoice collection move translation 2026-06-09 13:17:55 +02:00
Jeppe Bundgaard 028d8f1fc8 Add move to customer functionality for invoice collections in multiple languages 2026-06-09 13:16:26 +02:00
Jeppe Bundgaard 8d01ca1363 Implement move collected invoice to customer feature with API integration and tests 2026-06-09 13:16:05 +02:00
Jeppe Bundgaard a36403d279 Refactor self-serve fetch logic to use refs for state management; implement abort controller for active requests 2026-06-09 13:07:21 +02:00
Jeppe Bundgaard a78ad69c0c Add selfserve dynamic image size selector 2026-06-09 12:46:49 +02:00
Jeppe Bundgaard 22c856afb6 test: stabilize superuser firefox e2e 2026-06-09 12:34:11 +02:00
Jeppe Bundgaard 71a073ebf9 Add hidden frontend maintenance refresh 2026-06-09 12:32:58 +02:00
Jeppe Bundgaard f709cac95e Test wash type service sync 2026-06-09 12:13:35 +02:00
Jeppe Bundgaard 27867e48df test: update my wash start unit stubs 2026-06-09 11:50:21 +02:00
Jeppe Bundgaard f1a7e1dfcd test: stabilize POS draft full slice 2026-06-09 11:29:13 +02:00
Jeppe Bundgaard 31e9566f2d Refactor API route handling in tests to use centralized path patterns; enhance error handling in self-serve components 2026-06-09 10:52:20 +02:00
Jeppe Bundgaard f7f749e230 Fix PWA install scope 2026-06-08 20:56:32 +02:00
Jeppe Bundgaard f5b7da7b0a test: skip WebKit mobile invoice monitor header 2026-06-08 20:42:30 +02:00
Jeppe Bundgaard 2311188b13 test: align self-serve stop failure e2e 2026-06-08 20:25:01 +02:00
Jeppe Bundgaard 25637b52d9 test: update self-serve completion contract 2026-06-08 20:20:46 +02:00
Jeppe Bundgaard fddcc86cd6 fix: preserve self-serve stop failure banner 2026-06-08 20:17:44 +02:00
Jeppe Bundgaard c1b2be7d70 Enhance task ID handling in updateLaneAllowedServices; improve error handling in wash process 2026-06-08 19:56:02 +02:00
Jeppe Bundgaard 0a7b3a36aa test: stabilize release and department e2e mocks 2026-06-08 19:51:17 +02:00
Jeppe Bundgaard 57f102de71 test: skip desktop-only superuser specs on compact projects 2026-06-08 19:47:54 +02:00
Jeppe Bundgaard 6b22887a90 Remove guidance text for unavailable machine wash in self-serve flow 2026-06-08 18:58:33 +02:00
Jeppe Bundgaard 3b3f212ceb ci: use polling for Playwright Vite server 2026-06-08 18:54:25 +02:00
Jeppe Bundgaard 1a11e31836 ci: raise Playwright file descriptor limit 2026-06-08 18:46:56 +02:00
Jeppe Bundgaard d4b69c3990 test: align release runtime inspector privileges 2026-06-08 18:38:49 +02:00
Jeppe Bundgaard 4cbccd533a fix: stabilize POS route and action menu tests 2026-06-08 18:09:51 +02:00
Jeppe Bundgaard a9bc3ab0fc Fix POS booking match recursion 2026-06-08 17:29:06 +02:00
Jeppe Bundgaard d9f929df04 fix: restore Slack i18n compatibility 2026-06-08 17:25:56 +02:00
Jeppe Bundgaard bc154a4e6d Keep release wait after auto sync gateway timeout 2026-06-08 17:12:34 +02:00
Jeppe Bundgaard 7d48e40e05 Enhance release workflow with current commit check and enable concurrency cancellation 2026-06-08 17:02:42 +02:00
Jeppe Bundgaard 47af253bf8 Enhance self-serve logic to ignore stale answer sync responses and implement mutation request tracking 2026-06-08 16:53:03 +02:00
Jeppe Bundgaard e126dfccf9 Enhance accessibility and linting configuration
- Add tabindex attributes to input fields for better keyboard navigation
- Introduce ESLint configuration for improved code quality and consistency
- Update package.json to include linting scripts and dependencies
- Add tests for desktop tab navigation in POS flow
2026-06-08 16:35:56 +02:00
Jeppe Bundgaard be5191825d fix: distinguish Slack config permission errors 2026-06-08 16:33:52 +02:00
Jeppe Bundgaard 68cf9c4c06 Continue release wait after auto sync timeout 2026-06-08 16:25:24 +02:00
Jeppe Bundgaard e46b7ae58d Request release auto sync before artifact wait 2026-06-08 16:18:45 +02:00
Jeppe Bundgaard 3d81fb8dd7 Fix release verification for external build ids 2026-06-08 15:59:39 +02:00
Jeppe Bundgaard ff983dca60 Trigger frontend release 2026-06-08 15:48:21 +02:00
Jeppe Bundgaard fb0fceb6c4 Add booking count permission checks and handle API response failures gracefully 2026-06-08 15:26:57 +02:00
Jeppe Bundgaard 9591afb9a2 Add booking count permission checks and handle API response failures gracefully 2026-06-08 13:41:57 +02:00
Jeppe Bundgaard 0afde8ac8e Add support for webmanifest files in .htaccess and nginx configuration 2026-06-08 13:11:21 +02:00
Jeppe Bundgaard 4a83486b30 Update Danish translations for various UI elements and API keys 2026-06-08 12:51:10 +02:00
Jeppe Bundgaard a3ceac1636 Add Slack configuration module with customer registration webhook settings 2026-06-08 12:43:02 +02:00
Jeppe Bundgaard 8666d17a42 Add superuser new customer email notification settings 2026-06-08 12:19:32 +02:00
gold-dev 96d13b2400 Feat: Produktivitet overview 2026-06-08 11:32:30 +03:00
gold-dev 12fc1f80cc Feat: Rename ”Charts” til ”Grafer” 2026-06-08 10:50:37 +03:00
Jeppe B 2dfe4e9b37 Merge pull request #119 from copenhagentruckwash/fix-github-runner-test-failures
Fix self-serve wash progress actions, guided-wash emits and guest fallback UI; install Playwright deps for CI
2026-06-08 09:26:26 +02:00
Jeppe B 46c634e5a7 Fix self-serve wash test regressions 2026-06-08 09:13:24 +02:00
Jeppe Bundgaard 880680d7f8 Add reusable ErrorBanner component and integrate into MyWashStart 2026-06-07 22:07:56 +02:00
Jeppe Bundgaard 7d5ed772c2 Avoid empty MyWash lane service sync 2026-06-04 07:40:33 +02:00
Jeppe Bundgaard 83fbcdeaa8 Refresh MyWash eligibility before service sync 2026-06-04 07:31:41 +02:00
Jeppe Bundgaard 55a761d3a5 Publish MyWash tasks before hydration 2026-06-04 07:27:12 +02:00
Jeppe Bundgaard abe8e7750f Preserve MyWash machine services during sync 2026-06-04 07:21:07 +02:00
Jeppe Bundgaard aeef75b831 Clear stale MyWash summary after preview 2026-06-04 07:13:00 +02:00
Jeppe Bundgaard d6356d0244 Stop MyWash eligibility refresh after completion 2026-06-04 06:35:26 +02:00
Jeppe Bundgaard bbf2aeb95f Keep MyWash completion visible after stop 2026-06-04 06:05:41 +02:00
Jeppe Bundgaard 3a7d78ae05 Serve release-prefixed index shell 2026-06-04 04:08:33 +02:00
Jeppe Bundgaard 2a2f752692 Refresh recent completion marker during startup 2026-06-04 03:51:43 +02:00
Jeppe Bundgaard 8883bf8a07 Seed recently completed wash state before startup awaits 2026-06-04 03:44:29 +02:00
Jeppe Bundgaard 6ec80e2796 Recover recently completed self-serve screen 2026-06-04 03:29:55 +02:00
Jeppe Bundgaard 849a7b77cf Adopt resolved self-serve vehicle type 2026-06-04 03:16:59 +02:00
Jeppe Bundgaard 36ccd0c381 Keep completed wash fallback visible 2026-06-04 03:01:09 +02:00
Jeppe Bundgaard 58e7c8d6c9 Keep completed wash after lane refresh 2026-06-04 02:44:48 +02:00
Jeppe Bundgaard d04fc8cc0f Keep completed self-serve wash visible 2026-06-04 02:29:16 +02:00
Jeppe Bundgaard 339a1b1452 Recover completed wash after stop timeout 2026-06-04 02:15:15 +02:00
Jeppe Bundgaard 2d2d63d09e Merge remote-tracking branch 'origin/master' 2026-06-04 02:02:26 +02:00
Jeppe B 8241f7fd2f Treat unoccupied stop invoice errors as completed washes 2026-06-04 02:01:35 +02:00
Jeppe Bundgaard 985571af87 Treat unoccupied stop invoice errors as completed washes 2026-06-04 01:55:35 +02:00
Jeppe Bundgaard 82e1e4a466 Ignore preview-only active wash reservations 2026-06-04 01:21:25 +02:00
Jeppe Bundgaard 9a7f7bbb78 Prevent preview-only self-serve sessions from auto-restoring 2026-06-04 01:08:04 +02:00
Jeppe Bundgaard 359f29b75e Keep self-serve preview questions visible during summary load 2026-06-04 00:45:20 +02:00
Jeppe B ceced7cbaa Show MyWash completion when Buefy hides step panel 2026-06-04 00:21:33 +02:00
Jeppe Bundgaard 6b5cd00b10 Show completed wash outside step panels 2026-06-04 00:10:36 +02:00
Jeppe Bundgaard 02eae47d34 Refactor toggle-task event emission in SelfServeTaskList component 2026-06-04 00:00:28 +02:00
Jeppe Bundgaard 3f01d38676 Enhance self-serve image loading with preload and error handling 2026-06-03 23:53:54 +02:00
Jeppe Bundgaard 390a29ef55 Merge remote-tracking branch 'origin/master' 2026-06-03 23:34:05 +02:00
Jeppe Bundgaard b7f5786747 Add vehicle type selection logic for departments without product allow-list 2026-06-03 23:33:54 +02:00
Jeppe B 814c535757 Use relative release manifest asset paths 2026-06-03 23:13:26 +02:00
Jeppe Bundgaard ed0e27e4fd Use relative release manifest asset paths 2026-06-03 23:07:17 +02:00
Jeppe Bundgaard 659df480d7 Merge remote-tracking branch 'origin/master' 2026-06-03 23:04:43 +02:00
Jeppe B d0c45f6180 Fix release frontend origin routing 2026-06-03 22:45:32 +02:00
Jeppe Bundgaard 5a20461bd4 Fix release frontend origin routing 2026-06-03 22:40:49 +02:00
Jeppe Bundgaard 849f52fb8e Refactor error message display and update test descriptions for clarity 2026-06-03 22:16:27 +02:00
Jeppe Bundgaard da0c699b88 Add self-serve session handling and lane selection normalization 2026-06-03 22:07:47 +02:00
Jeppe Bundgaard af5514c982 Implement dynamic base path handling for frontend assets and update routing logic 2026-06-03 22:00:18 +02:00
Jeppe Bundgaard cc689e80a9 Use release base for frontend HTML assets 2026-06-03 21:45:25 +02:00
Jeppe Bundgaard 3b51374678 Add dynamic release frontend base path handling and update static asset references 2026-06-03 21:44:27 +02:00
Jeppe Bundgaard f07569db98 Allow active self-serve lane selection 2026-06-03 21:32:58 +02:00
Jeppe Bundgaard c5fcb90683 Include emitted chunks in release manifest 2026-06-03 21:12:08 +02:00
Jeppe Bundgaard 270f87dee1 Split release smoke static and app targets 2026-06-03 21:03:03 +02:00
Jeppe Bundgaard 765b8f3b35 Fix self-serve entry regressions 2026-06-03 20:51:06 +02:00
Jeppe Bundgaard 3ff695899c Restore self-serve task phase from active sessions 2026-06-03 20:32:40 +02:00
Jeppe Bundgaard be35baa838 Refactor self-serve registration field for improved layout and accessibility 2026-06-03 20:06:31 +02:00
Jeppe B 470d4d7314 Normalize release bootstrap cleanup 2026-06-03 19:38:54 +02:00
Jeppe B 0976608a25 Refresh release bootstrap asset hash 2026-06-03 19:34:48 +02:00
Jeppe B e7f4dfa07e Trigger frontend release after asset mismatch 2026-06-03 19:23:01 +02:00
Jeppe B 2c68f335c6 Trigger frontend release for self-serve vehicle fix 2026-06-03 18:08:25 +02:00
Jeppe Bundgaard cf29589e29 Fix self-serve vehicle step progression 2026-06-03 17:51:46 +02:00
Jeppe Bundgaard 05cabd8edc Add margin and padding adjustments to self-serve task checkbox 2026-06-02 20:09:18 +02:00
Jeppe Bundgaard 2f4ecba51a Merge remote-tracking branch 'origin/master' 2026-06-02 20:04:11 +02:00
Jeppe Bundgaard 74a7c9f23f Refactor self-serve task list to improve task title handling and remove unnecessary description checks 2026-06-02 20:03:55 +02:00
Jeppe B b075a717f7 Cover hidden customer number self-serve flow 2026-06-02 19:59:30 +02:00
Jeppe B a3dcb888d3 Cover self-serve task title presentation 2026-06-02 19:59:12 +02:00
Jeppe B 02706af508 Allow QR self-serve vehicle step without customer field 2026-06-02 19:58:50 +02:00
Jeppe B 1f2c007776 Tighten self-serve task presentation 2026-06-02 19:58:28 +02:00
Jeppe Bundgaard 42dfdfef13 Add mocks for department daily report components and update Vitest configuration for SCSS 2026-06-02 17:54:37 +02:00
Jeppe B b40bf0b9e1 Merge pull request #110 from copenhagentruckwash/create-playwright-test-suite-for-mywashstart
Add mocked Playwright tests for MyWashStart flow and improve answer-sync & department fallback
2026-06-02 13:52:00 +02:00
Jeppe B cc15a9c468 Merge branch 'master' into create-playwright-test-suite-for-mywashstart 2026-06-02 13:51:52 +02:00
Jeppe B 5d8b1eecb4 Merge pull request #111 from copenhagentruckwash/create-playwright-test-suite-for-mywashstart-6chtnh
Add mocked Playwright tests for MyWashStart flow and improve answer-sync & department fallback
2026-06-02 13:50:32 +02:00
Jeppe B 093d82d762 Merge branch 'master' into create-playwright-test-suite-for-mywashstart-6chtnh 2026-06-02 13:50:24 +02:00
Jeppe B 686577c209 Merge pull request #114 from copenhagentruckwash/update-mywashstart.vue-with-timeout-handling
Fix delayed active wash restore cleanup
2026-06-02 13:48:47 +02:00
Jeppe B c2b333a78b Merge branch 'master' into update-mywashstart.vue-with-timeout-handling 2026-06-02 13:48:33 +02:00
Jeppe B cef8b8bf0f Merge pull request #115 from copenhagentruckwash/update-userdashboard-to-inspect-lane-readiness
Guard wash entry self-serve CTA
2026-06-02 13:47:39 +02:00
Jeppe B 62a46710ba Merge branch 'master' into update-userdashboard-to-inspect-lane-readiness 2026-06-02 13:47:31 +02:00
Jeppe B a3e6feb07e Merge pull request #113 from copenhagentruckwash/refactor-self-serve-data-loading-with-fetch-scheduler
Debounce self-serve wash data fetches
2026-06-02 13:43:40 +02:00
Jeppe B 2ad093d4f1 Merge pull request #112 from copenhagentruckwash/update-mywashstart.vue-for-answer-sync-state
Block wash question flow on failed answer sync
2026-06-02 13:43:22 +02:00
Jeppe B 6017d576fc Merge pull request #116 from copenhagentruckwash/update-wash-department-handling-and-ui
Handle self-serve wash when geolocation is unavailable
2026-06-02 13:42:51 +02:00
Jeppe B 08ac38dc87 Merge pull request #117 from copenhagentruckwash/add-and-extend-unit-and-e2e-tests
Add production-ready tests for user self-serve wash start (unit + mocked e2e)
2026-06-02 13:42:36 +02:00
Jeppe B 3960daa556 Add production wash start test matrix 2026-06-02 13:42:22 +02:00
Jeppe B 395cbd87dd Handle self-serve wash without geolocation 2026-06-02 13:42:16 +02:00
Jeppe B 23429c047f Guard wash entry self-serve CTA 2026-06-02 13:42:09 +02:00
Jeppe B af68748c16 Fix active wash restore timeout cleanup 2026-06-02 13:42:02 +02:00
Jeppe B 18ab007f87 Debounce self-serve wash data fetches 2026-06-02 13:41:55 +02:00
Jeppe B 63942045be Block wash question flow on failed answer sync 2026-06-02 13:41:51 +02:00
Jeppe B a4cdc6e4f3 Add mocked MyWashStart flow coverage 2026-06-02 13:41:44 +02:00
Jeppe B 07c611bdbe Add mocked MyWashStart flow coverage 2026-06-02 13:41:32 +02:00
Jeppe Bundgaard 5170139a32 Switch CI to self-hosted runners
Updated all GitHub Actions workflows to use self-hosted runners instead of `ubuntu-latest`. This change ensures better control over the CI environment and aligns with internal infrastructure requirements.
2026-06-02 10:36:18 +02:00
Jeppe Bundgaard 90952a68b7 Refactor SubusersTable to use ActionSettingsWheelButton for row actions 2026-06-02 09:23:49 +02:00
Jeppe Bundgaard 1cc14d7b41 Merge remote-tracking branch 'origin/master' 2026-06-02 08:36:42 +02:00
Jeppe B aa4d6576f7 Merge pull request #108 from copenhagentruckwash/fix-page-crash-issues
Fix wash start recovery from API failures
2026-06-02 08:35:28 +02:00
Jeppe B 2a46984f9c Merge pull request #109 from copenhagentruckwash/fix-playwright-exit-code-error
Stabilize release sidebar E2E coverage
2026-06-02 08:34:47 +02:00
Jeppe Bundgaard 4a9c721c55 Remove debug message for beta frontend in build output 2026-06-02 08:12:50 +02:00
Jeppe B 7be9995c87 Stabilize release sidebar e2e coverage 2026-06-02 08:10:51 +02:00
Jeppe B 85529de3f8 Fix wash start recovery from API failures 2026-06-02 08:07:03 +02:00
Jeppe B 114ca9d956 Merge pull request #107 from copenhagentruckwash/investigate-and-fix-failing-tests
Resolve failing frontend CI: fix merge conflicts, restore edge cache & tests
2026-06-02 07:15:41 +02:00
Jeppe B 29f858685a Fix self-serve wash smoke restore fixture 2026-06-02 07:15:26 +02:00
Jeppe B 4112faf682 Fix edge gateway broker config e2e 2026-06-02 02:38:37 +02:00
Jeppe B a13b73c414 Harden edge gateway service parsing 2026-06-02 02:06:32 +02:00
Jeppe B 667e93af8f Resolve failing frontend checks 2026-06-02 01:13:28 +02:00
Jeppe B 54a888026e Merge pull request #104 from copenhagentruckwash/propose-fix-for-price-tampering-vulnerability
Fix trusted booking prices in POS hydration
2026-06-02 00:48:56 +02:00
Jeppe B a0fb02baa1 Merge pull request #106 from copenhagentruckwash/fix-booking-delete-access-permission
Fix booking delete action visibility
2026-06-02 00:47:38 +02:00
Jeppe B c445b932db Fix booking delete action visibility 2026-06-02 00:47:19 +02:00
copilot-swe-agent[bot] ffccb1683b Merge origin/master and resolve POS pricing conflict 2026-06-01 22:44:56 +00:00
Jeppe B 0d590fc9d6 Merge pull request #105 from copenhagentruckwash/fix-postinstall-script-vulnerability
Remove unsafe postinstall hook
2026-06-02 00:40:46 +02:00
Jeppe B 1d4910f947 Remove unsafe postinstall hook 2026-06-02 00:40:34 +02:00
Jeppe B 7e457ed961 Fix trusted booking prices in POS hydration 2026-06-02 00:39:55 +02:00
Jeppe B 34722c6985 Merge pull request #103 from copenhagentruckwash/fix-vulnerability-in-pos-order-pricing
Fix POS booking price hydration
2026-06-02 00:39:37 +02:00
Jeppe B c5a13b50ab Fix POS booking price hydration 2026-06-02 00:39:15 +02:00
Jeppe B 387bd50281 Merge pull request #102 from copenhagentruckwash/fix-unsandboxed-blob-attachment-previews
Sanitize and sandbox order attachment previews to prevent XSS
2026-06-02 00:38:15 +02:00
Jeppe B 81016fd898 Fix unsafe order attachment previews 2026-06-02 00:38:03 +02:00
Jeppe B 39fc1612ac Merge pull request #101 from copenhagentruckwash/fix-duplicate-stripe-invoice-submission-issue
Prevent duplicate Stripe invoice queue submissions
2026-06-02 00:30:18 +02:00
Jeppe B 7ab12a3fff Prevent duplicate Stripe invoice queue submissions 2026-06-02 00:29:53 +02:00
Jeppe B b4d869c469 Merge pull request #100 from copenhagentruckwash/fix-autosave-to-prevent-infinite-retry-loop
Fix autosave failed-save retry loop
2026-06-02 00:28:52 +02:00
Jeppe B 799b667828 Fix autosave failed-save retry loop 2026-06-02 00:28:39 +02:00
Jeppe B ae3513edf3 Merge pull request #99 from copenhagentruckwash/fix-repeated-booking-prefetch-flooding-api
Avoid duplicate booking prefetch by honoring exact-plate cache
2026-06-02 00:27:37 +02:00
Jeppe B 6db1861b78 Fix repeated booking prefetch requests 2026-06-02 00:27:25 +02:00
Jeppe B a8e839ba18 Merge pull request #98 from copenhagentruckwash/fix-mobile-pos-orders-completion-issue
Fix mobile POS order completion
2026-06-02 00:27:07 +02:00
Jeppe B b224cf67d1 Fix mobile POS order completion 2026-06-02 00:26:53 +02:00
Jeppe B 222237ab34 Merge pull request #96 from copenhagentruckwash/fix-edge-gateway-localstorage-vulnerability
Harden edge gateway workspace cache
2026-06-02 00:16:07 +02:00
copilot-swe-agent[bot] 433cfe521e Planning merge conflict resolution 2026-06-01 22:14:58 +00:00
Jeppe B 1e4a776fa5 Merge pull request #97 from copenhagentruckwash/fix-auto-selection-of-customer-suggestions
Fix stale POS customer suggestion auto-selection
2026-06-02 00:13:59 +02:00
Jeppe B e88a424a7d Fix stale POS customer suggestion auto-selection 2026-06-02 00:13:46 +02:00
Jeppe B 9450c7902d Harden edge gateway workspace cache 2026-06-02 00:08:20 +02:00
Jeppe B 75b5883afe Merge pull request #95 from copenhagentruckwash/fix-stripe-email-invoice-payment-misclassification
Fix Stripe email invoice paid validation
2026-06-02 00:07:49 +02:00
Jeppe B c26f0184d9 Fix Stripe email invoice paid validation 2026-06-02 00:07:35 +02:00
Jeppe B cb74b4bbe7 Merge pull request #93 from copenhagentruckwash/propose-fix-for-cached-gateway-details-leak
Scope edge gateway cache to session and prevent auth-blind cached fallbacks
2026-06-01 23:48:26 +02:00
Jeppe B 326eca656f Fix edge gateway cache authorization fallback 2026-06-01 23:48:17 +02:00
Jeppe B f0a5058b68 Merge pull request #92 from copenhagentruckwash/propose-fix-for-self-hosted-ci-vulnerability
Use GitHub-hosted runners for tests workflow
2026-06-01 23:46:15 +02:00
copilot-swe-agent[bot] d39cd36995 Merge remote-tracking branch 'origin/master' into propose-fix-for-self-hosted-ci-vulnerability
# Conflicts:
#	.github/workflows/tests.yml
2026-06-01 21:43:04 +00:00
Jeppe B 0548018b5c Use GitHub-hosted runners for tests workflow 2026-06-01 23:40:30 +02:00
Jeppe B 699e10856e Merge pull request #91 from copenhagentruckwash/propose-fix-for-self-hosted-runner-vulnerability
Fix CI runner exposure for pull requests
2026-06-01 23:40:09 +02:00
Jeppe B 36b399cb2f Fix CI runner exposure for pull requests 2026-06-01 23:40:00 +02:00
Jeppe B f613d58727 Merge pull request #90 from copenhagentruckwash/propose-fix-for-product-catalog-vulnerability
Restrict vehicle type product edits to superusers
2026-06-01 23:35:14 +02:00
Jeppe B feccfada77 Restrict vehicle type product edits to superusers 2026-06-01 23:35:04 +02:00
Jeppe B 4127e66114 Merge pull request #89 from copenhagentruckwash/fix-unvalidated-attachment-download-links
Validate self-serve attachment download links
2026-06-01 23:34:45 +02:00
Jeppe B b2a9b59399 Validate self-serve attachment download links 2026-06-01 23:34:34 +02:00
Jeppe B 6ffed9c2f2 Merge pull request #88 from copenhagentruckwash/fix-lane-toggle-guard-permission-issue
Fix lane toggle permission guards
2026-06-01 23:34:14 +02:00
Jeppe B 53e193d8d1 Fix lane toggle permission guards 2026-06-01 23:34:03 +02:00
Jeppe B 7835691bc9 Merge pull request #87 from copenhagentruckwash/fix-websocket-token-exposure-vulnerability
Secure edge gateway WebSocket session tokens
2026-06-01 23:33:44 +02:00
Jeppe B 91715541ec Secure edge gateway websocket sessions 2026-06-01 23:33:34 +02:00
Jeppe B e3f9e99129 Merge pull request #86 from copenhagentruckwash/fix-stored-xss-in-relay-options
Fix select option HTML escaping
2026-06-01 23:33:20 +02:00
Jeppe B 4634cbad89 Fix select option HTML escaping 2026-06-01 23:33:10 +02:00
Jeppe B 5db2a36da0 Merge pull request #85 from copenhagentruckwash/propose-fix-for-relay-failure-issue
Rollback wash start when relay activation fails
2026-06-01 22:53:44 +02:00
Jeppe B cd7b273da4 Rollback wash start when relay activation fails 2026-06-01 22:53:34 +02:00
Jeppe B cbbad1a6e2 Merge pull request #84 from copenhagentruckwash/fix-pos-url-order-reassignment-vulnerability
Prevent implicit POS order department changes
2026-06-01 22:44:24 +02:00
Jeppe B 9661d4efa7 Prevent implicit POS order department changes 2026-06-01 22:44:14 +02:00
Jeppe B 31762c23c5 Merge pull request #83 from copenhagentruckwash/fix-unescaped-order-delete-reasons
Fix protected order delete reason escaping
2026-06-01 22:43:55 +02:00
Jeppe B c71a6b4d14 Fix protected order delete reason escaping 2026-06-01 22:43:45 +02:00
Jeppe B e8ddb33f21 Merge pull request #82 from copenhagentruckwash/fix-vulnerability-in-edge-broker-config
Secure edge gateway broker configuration
2026-06-01 22:28:28 +02:00
Jeppe B ea368d19bb Secure edge gateway broker configuration 2026-06-01 22:28:18 +02:00
Jeppe B 7ab50b6290 Merge pull request #81 from copenhagentruckwash/fix-unredacted-broker-diagnostics-exposure
Redact edge gateway diagnostic secrets
2026-06-01 22:27:33 +02:00
Jeppe B fd7cfbd3c3 Redact edge gateway diagnostics 2026-06-01 22:27:19 +02:00
Jeppe B 90725af10a Merge pull request #80 from copenhagentruckwash/fix-ungated-destructive-actions-in-ui
Gate self-serve studio gateway actions and require confirmation for destructive operations
2026-06-01 22:26:58 +02:00
Jeppe B d63a62a921 Gate self-serve gateway actions 2026-06-01 22:26:44 +02:00
Jeppe B 4bc5481941 Merge pull request #79 from copenhagentruckwash/propose-fix-for-deferred-start-relay-issue
Require successful machine relay activation before marking wash in progress
2026-06-01 22:15:16 +02:00
Jeppe B 9feede3b0d Require machine relay activation before wash progress 2026-06-01 22:15:02 +02:00
Jeppe B 5aad9eda8d Merge pull request #78 from copenhagentruckwash/fix-raw-edge-log-context-exposure
Redact edge gateway log context secrets
2026-06-01 22:13:42 +02:00
Jeppe B 91b15faba6 Redact edge gateway log context secrets 2026-06-01 22:13:32 +02:00
Jeppe B 98c1c61cbd Merge pull request #77 from copenhagentruckwash/fix-active-wash-endpoint-information-leak
Scope active-wash restore to authenticated customer (my-active-wash)
2026-06-01 22:06:25 +02:00
Jeppe B 4463a5326a Fix scoped active wash restoration 2026-06-01 22:06:14 +02:00
Jeppe B bdb23513a4 Merge pull request #76 from copenhagentruckwash/propose-fix-for-security-vulnerability
Harden self-serve vehicle condition mutations
2026-06-01 22:05:37 +02:00
Jeppe B ee40f16232 Harden self-serve vehicle condition mutations 2026-06-01 22:05:27 +02:00
Jeppe B a76944b00c Merge pull request #75 from copenhagentruckwash/fix-sessionstorage-vulnerability-in-invoicing-period
Secure privileged session caches
2026-06-01 21:52:53 +02:00
Jeppe B cf074ff893 Secure privileged session caches 2026-06-01 21:52:42 +02:00
Jeppe B 712eb19db5 Merge pull request #74 from copenhagentruckwash/propose-fix-for-release-manager-token-leak
Restrict release manager control API origins
2026-06-01 21:52:20 +02:00
Jeppe B de9bc9b170 Restrict release manager control API origins 2026-06-01 21:52:09 +02:00
Jeppe B 95e2404307 Merge pull request #73 from copenhagentruckwash/fix-unredacted-deployment-failures-exposure
Redact release deployment failure diagnostics
2026-06-01 21:27:42 +02:00
Jeppe B 2c01feb3be Redact release deployment failure diagnostics 2026-06-01 21:27:28 +02:00
Jeppe B f416e0d462 Merge pull request #72 from copenhagentruckwash/fix-telemetry-that-leaks-passwords
Redact serialized request bodies and omit failed-request bodies when capture is disabled
2026-06-01 21:26:45 +02:00
Jeppe B 7f1308c8ce Redact release timeline failed request bodies 2026-06-01 21:26:32 +02:00
Jeppe B fd8d45e437 Merge pull request #71 from copenhagentruckwash/fix-vulnerabilities-in-release-runtime-urls
Harden release runtime URL trust boundaries
2026-06-01 21:25:51 +02:00
copilot-swe-agent[bot] 20390342d5 Merge origin/master and resolve release timeline spec conflict 2026-06-01 19:21:12 +00:00
Jeppe B c47bf229f0 Harden release runtime URL trust boundaries 2026-06-01 21:14:39 +02:00
Jeppe B 3616e197e1 Merge pull request #70 from copenhagentruckwash/propose-fix-for-isolated-stack-vulnerability
Prevent active isolated stack reuse during release deploys
2026-06-01 21:12:26 +02:00
Jeppe B 3d8544de21 Prevent isolated stack deployment reuse 2026-06-01 21:11:17 +02:00
Jeppe B 504abb7a15 Merge pull request #69 from copenhagentruckwash/fix-release-subject-autocomplete-exposure
Guard release assignment subject search
2026-06-01 21:11:02 +02:00
Jeppe B 37021533b9 Guard release assignment search by manage permission 2026-06-01 21:10:51 +02:00
Jeppe B 59eab84f03 Merge pull request #68 from copenhagentruckwash/fix-cross-origin-framing-vulnerability-in-nginx
Add anti-framing headers to Coolify Nginx frontend
2026-06-01 21:10:26 +02:00
Jeppe B e67033711c Add anti-framing headers to Coolify Nginx config 2026-06-01 21:10:14 +02:00
Jeppe B 85fd00f020 Merge pull request #67 from copenhagentruckwash/fix-metadata-exposure-for-low-privileged-users
Restrict release runtime metadata inspector
2026-06-01 21:09:54 +02:00
Jeppe B 4f064a1625 Restrict release runtime metadata inspector 2026-06-01 21:09:42 +02:00
Jeppe B 3bf7ada226 Merge pull request #66 from copenhagentruckwash/fix-tls-verification-in-dev-proxy
Secure Vite API proxy TLS by default
2026-06-01 21:09:15 +02:00
Jeppe B 3b90e17a1e Secure Vite API proxy TLS by default 2026-06-01 21:09:05 +02:00
Jeppe B c1fb5b72ee Merge pull request #65 from copenhagentruckwash/fix-privilege-escalation-in-daily-report
Guard department lane toggles by wash-lane permission
2026-06-01 21:08:43 +02:00
Jeppe B 29da26cc08 Guard department lane toggles by permission 2026-06-01 21:08:32 +02:00
Jeppe B 807fc09b1e Merge pull request #64 from copenhagentruckwash/fix-dgnvask-toggle-issue
Fix Døgnvask toggle state with configuration warnings
2026-06-01 21:08:03 +02:00
Jeppe B 528e4994db Fix Dognvask warning toggle state 2026-06-01 21:07:53 +02:00
Jeppe B c339122f4f Merge pull request #63 from copenhagentruckwash/fix-release-gate-binding-to-build
Bind frontend release gate to build id
2026-06-01 21:07:33 +02:00
Jeppe B feeef46354 Bind frontend release gate to build id 2026-06-01 21:07:23 +02:00
Jeppe B 066bd14554 Merge pull request #62 from copenhagentruckwash/fix-active-wash-refresh-vulnerability
Harden active-wash refresh ownership check
2026-06-01 21:06:53 +02:00
Jeppe B bffaa2ab88 Fix active wash refresh ownership check 2026-06-01 21:06:39 +02:00
Jeppe B 31495ea67b Merge pull request #61 from copenhagentruckwash/fix-ftp-deploy-password-exposure
Secure lftp release upload credentials
2026-06-01 21:02:13 +02:00
Jeppe B 8f2533f1b2 Secure lftp release upload credentials 2026-06-01 21:02:01 +02:00
Jeppe B 13b0742deb Merge pull request #60 from copenhagentruckwash/fix-cropping-issue-in-path-editor
Fix self-serve path editor answer switch layout
2026-06-01 20:45:04 +02:00
Jeppe B e2b2d50e5e Fix self-serve path editor answer switch layout 2026-06-01 20:19:35 +02:00
Jeppe B 55d61e37f3 Merge pull request #59 from copenhagentruckwash/fix-e2e-tests-in-i18n-v2-catalog
Fix i18n v2 catalog integrity and split Release Manager source fragments
2026-06-01 19:45:57 +02:00
Jeppe B fa19f5a704 Fix i18n v2 catalog integrity 2026-06-01 19:44:50 +02:00
Jeppe Bundgaard b995031255 Enable "Selv vask" button in MobileFooter.vue 2026-06-01 19:43:57 +02:00
Jeppe Bundgaard d096444ca0 Remove unused lane unavailable warning message from SelfServeLaneStep.vue 2026-06-01 19:41:16 +02:00
Jeppe B 10358d4f53 Merge pull request #58 from copenhagentruckwash/fix-self-serve-path-editor-for-all-departments
Fix self-serve path editor vehicle scope
2026-06-01 18:32:11 +02:00
Jeppe B 78f41b8380 Fix self-serve path editor vehicle scope 2026-06-01 18:31:53 +02:00
Jeppe B 90d4bc7c04 Merge pull request #57 from copenhagentruckwash/fix-bugs-in-start-wash-flow
Harden customer wash start flow
2026-06-01 18:01:29 +02:00
Jeppe B b59c17f120 Harden customer wash start flow 2026-06-01 17:54:42 +02:00
Jeppe Bundgaard c279e7fd38 Merge remote-tracking branch 'origin/master' 2026-06-01 17:23:06 +02:00
Jeppe Bundgaard 8a4fcb3e62 Remove unused return messages and cleanup peer dependency flags in package-lock.json 2026-06-01 17:22:33 +02:00
Jeppe B 680ef7088b Merge pull request #56 from copenhagentruckwash/investigate-playwright-exit-code-1-issue
Stabilize POS email notification E2E setup
2026-06-01 17:20:54 +02:00
Jeppe B 83935af973 Stabilize POS email notification E2E setup 2026-06-01 16:45:40 +02:00
Jeppe B 37973f8fcb Add x-api-coverage happy path flag to listOrders operation 2026-06-01 13:31:48 +02:00
Jeppe B acf19c281a Add email notification actions for booking confirmations and wash certificates 2026-06-01 13:24:29 +02:00
Jeppe B 683fce32da Fix POS step 1 customer lookup loop 2026-06-01 12:41:08 +02:00
Jeppe B 4c9ba7c30d Add unit tests for Playwright full-slice ownership 2026-06-01 12:28:42 +02:00
Jeppe B ca1f3ab477 Merge pull request #55 from copenhagentruckwash/codex/frontend-release-gate-coolify-20260530
Fix frontend release gate for Coolify deploys
2026-05-31 10:22:29 +02:00
Jeppe B 2ab09076f5 Stabilize edge terminal smoke navigation 2026-05-30 21:51:07 +02:00
Jeppe B 97e17fadd4 Respect mobile self-serve studio layout in e2e 2026-05-30 20:33:27 +02:00
Jeppe B ef780e5542 Keep completed self-serve wash step visible 2026-05-30 19:44:52 +02:00
Jeppe B 931c31c7be Stabilize self-serve question loading layout 2026-05-30 18:37:37 +02:00
Jeppe B 25f2be875c Wait for self-serve completion command response 2026-05-30 17:45:06 +02:00
Jeppe B ff70326665 Stabilize frontend PR smoke gates 2026-05-30 16:58:04 +02:00
Jeppe B 52ed49211a Fix frontend release gate for Coolify deploys 2026-05-30 15:39:03 +02:00
Jeppe B 0565fc2a7c Merge pull request #54 from copenhagentruckwash/codex/master-tests-pass-frontend-20260528
[codex] Keep user vehicle actions in wheel menu
2026-05-29 16:33:25 +02:00
Jeppe B d30273cda8 Map user vehicle changes to PR Playwright coverage 2026-05-29 14:46:52 +02:00
Jeppe B b893e69b54 Keep user vehicle actions in wheel menu 2026-05-28 23:35:14 +02:00
Jeppe B eabb12ef97 Merge pull request #53 from copenhagentruckwash/fix-issues-and-verify-with-tests
Format DepartmentSelfServeStudio.vue and update vehicle actions test to reflect wheel menu behavior
2026-05-28 19:52:53 +02:00
Jeppe Bundgaard 225741cf7b Refactor unit tests and clean up Vue component formatting
- Update `self-serve-studio-managed-inspector.spec.js` to improve node kind checks.
- Tidy up and standardize `SessionUser.request` calls in `vehiclesTable.vue`.
- Format code consistently in tables, conditions, and task specs for clarity and maintenance.
2026-05-28 19:47:02 +02:00
Jeppe B dad42da6d9 Keep vehicle actions in wheel menu 2026-05-28 19:01:27 +02:00
Jeppe Bundgaard 0fa2284e89 Add connectivity issue handling logic and tests
- Add `ConnectivityIssue.vue` component for displaying server connection issues.
- Implement polling logic with retry intervals to check server health.
- Update i18n with new connectivity issue messages.
- Add unit tests to cover connectivity failure and recovery scenarios.
- Update test suite for breadth of coverage, including router contract and timer cleanup.
2026-05-28 18:11:21 +02:00
Jeppe Bundgaard cc241e5e19 Add subuser support and require notes for specific products 2026-05-28 16:36:42 +02:00
Jeppe Bundgaard c5a536fbea Add subuser support and require notes for specific products 2026-05-28 16:12:41 +02:00
Jeppe Bundgaard 3a7fef11ef Implement subuser password policy validation and update related components 2026-05-27 19:16:26 +02:00
Jeppe Bundgaard 625757fafc Fix Danish translations and enhance mobile department auto-selection logic 2026-05-27 17:34:51 +02:00
Jeppe Bundgaard e1a8af5a72 Extend i18n with new self-serve prompts and lane availability messages, and add E2E test for lane confirmation logic. 2026-05-27 13:25:31 +02:00
Jeppe Bundgaard c437c89cfa Add release management enhancements, runtime-aware frontend updates, and extensive test coverage
- Implement release update detection, asset preloading, and frontend version management.
- Add unit and E2E tests for release update workflows, widget behavior, and failure scenarios.
- Introduce new services for handling release ping, error reporting, and update installation workflows.
- Extend i18n for release-related components and error report localization.
- Add `ReleaseFrontendVersionBadge.vue` and related styles to display frontend update statuses.
2026-05-27 13:23:22 +02:00
Jeppe Bundgaard 184ef5670c Enhance session management by normalizing session payload and resetting session state on logout. Update login redirect logic and improve runtime API URL handling for selected release channels. 2026-05-26 17:28:21 +02:00
Jeppe Bundgaard 8ab8915429 Add release management components and update routing logic. Introduce keyboard shortcuts, enhance release data grid, and improve asset handling in Nginx configuration. 2026-05-26 14:14:13 +02:00
Jeppe B a952dd2c61 Merge pull request #52
Add endpoint mode options and enhance runtime refresh handling
2026-05-21 14:48:44 +02:00
Jeppe Bundgaard ebcd52a019 Add endpoint mode options, release channel rollback handling, and runtime refresh improvements
- Implement `endpoint_mode` (manual/auto) and related configuration in i18n across multiple locales.
- Add new utility methods for channel rollback and runtime confirmation workflows.
- Update unit and E2E tests to cover runtime refresh behavior, channel switching mechanics, and release references.
- Enhance `SessionUser` and related services to improve error handling during release runtime refresh.
- Add data-test attributes for frontend components to support improved test coverage.
2026-05-21 14:47:46 +02:00
Jeppe Bundgaard b2ddf0db5c Add beta frontend debug display in build info 2026-05-21 11:45:20 +02:00
Jeppe Bundgaard eaf4b965d5 Add runtime support for service sets, release management enhancements, and new unit tests
- Extend `releaseTimeline.js` and related modules for runtime-aware service sets and URLs.
- Update the release session builder to include `service_set` handling.
- Introduce release service definitions, status tones, and enhanced runtime display text.
- Add new dataset mode options, preview rows, and warnings for release configurations in `ConfigurationReleaseManager.vue`.
- Implement extensive utility methods for data service handling, target context, and deployment endpoints.
- Add `vite-api-proxy.spec.js` and extend `release-bootstrap.spec.js` to ensure coverage for new functionality.
2026-05-21 11:41:50 +02:00
Jeppe Bundgaard 6ba1c754a1 Serve frontend releases under channel prefixes 2026-05-20 19:27:01 +02:00
Jeppe Bundgaard 468613ab68 Add Coolify frontend release Docker image 2026-05-20 18:53:17 +02:00
Jeppe Bundgaard 261d84bd01 Route selected release runtime through gateway 2026-05-20 18:32:21 +02:00
Jeppe Bundgaard ea8db593c5 Add release headers service, runtime-aware API header handling, and i18n updates for release configuration
- Implement `releaseHeaders.js` to manage release-related HTTP headers.
- Update requests to include runtime-based release headers.
- Extend i18n phrases across multiple locales for release runtime and configuration labels.
- Enhance unit tests for header building and API availability error handling.
2026-05-20 17:52:54 +02:00
Jeppe Bundgaard 130cc2fc10 Add release runtime bootstrap implementation with unit and E2E tests
- Create `releaseBootstrap.js` service to handle runtime configuration and remote frontend loading.
- Integrate runtime-aware API URL resolution across components and services.
- Update i18n phrases to clarify new domains for `ssl_domain_message`.
- Enhance unit and E2E tests to validate runtime-based frontend loading and fallback behavior.
- Refactor session-related API calls and incorporate runtime-configured base URLs.
2026-05-20 11:26:21 +02:00
Jeppe Bundgaard 3715da8fd3 Add release channel selector components, subject autocomplete, and related tests
- Implement components for `ReleaseChannelSelector`, including panel and sidebar variants.
- Add `ReleaseAssignmentSubjectAutocomplete` for user, subuser, and customer search functionality.
- Update i18n phrases for channel selector and subject assignment.
- Enhance unit and E2E tests for release channel availability, switching, and runtime configuration.
- Modify modules for channel-related state handling and runtime refresh mechanics.
2026-05-20 10:31:49 +02:00
Jeppe Bundgaard 100e477a95 Add "Delete" functionality for release service sets in isolated stacks
- Implement deletion of inactive isolated stacks via API and UI.
- Normalize build pack handling, defaulting frontend to "static".
- Update E2E tests to verify deletion and adjusted UI behavior.
- Refactor related release manager methods and validation logic.
2026-05-19 16:55:20 +02:00
Jeppe Bundgaard 1132c8c256 Merge remote-tracking branch 'origin/master' 2026-05-19 14:47:01 +02:00
Jeppe Bundgaard 42237fcc70 Add unit tests for release manager i18n validation and implement ReleaseReplayInspector Vue component for timeline inspection and replay functionality. 2026-05-19 14:46:42 +02:00
gold-dev ae8b3b6b64 Feat: Hvidovre only one toggler 2026-05-19 14:26:23 +03:00
Jeppe B 8378491b0b Merge pull request #51
Fix chromium mobile runner flakes
2026-05-19 13:12:11 +02:00
Jeppe Bundgaard 20274cf8eb Update i18n phrases for ssl_domain fields to clarify load balancer routing details and add new methods for release service sets and bundles in SuperUser Release Manager. 2026-05-19 13:11:17 +02:00
Jeppe Bundgaard d25726fafd Add unit and E2E tests for release channel availability and switched-notice functionality. Configure runtime mocks and implement Vue components for channel-specific handling. 2026-05-19 12:05:25 +02:00
Jeppe Bundgaard 4f1973d048 Add SuperUser Release Manager view with channel management, deployments, replay, and integrations 2026-05-19 10:04:00 +02:00
Jeppe Bundgaard d69c474655 Add multilingual glossary and phrases for i18n, including terms, attributes, and compatibility across da, de, en, no, and sv locales. 2026-05-19 08:03:39 +02:00
Jeppe Bundgaard 56078307da Update vehicles table action visibility and adjust E2E test timeouts and MinIO configuration
- Change `displayActionsDirectly` prop to `false` in vehicles table for compact mode consistency.
- Refactor E2E tests with reusable `PAGE_READY_TIMEOUT` constant to prevent flaky behavior.
- Enhance MinIO configuration in replication management and system status tests to include additional environment variables and form fields.
2026-05-18 15:07:25 +02:00
Jeppe Bundgaard e6291cdb8a Add response handling for MinIO provisioning in replication E2E test 2026-05-18 13:52:01 +02:00
Jeppe Bundgaard bf0b2152c5 Enhance replication management to support MinIO alongside database and Redis, update UI labels, and improve status reporting 2026-05-18 13:48:16 +02:00
Jeppe Bundgaard 9771723992 Fix chromium mobile runner flakes 2026-05-18 12:28:23 +02:00
Jeppe Bundgaard 156e50a864 Merge remote-tracking branch 'origin/master' 2026-05-18 11:01:12 +02:00
Jeppe Bundgaard 5081fe0776 Add Playwright PR runner and Vitest unit test fast runner scripts; integrate with test commands and update specs/tests for PR-based selection. 2026-05-18 10:59:56 +02:00
Jeppe Bundgaard 656e43d449 Add SuperUser replication management view, including UI for database and Redis hosts, replication status, provisioning, and Compose template generation 2026-05-18 09:55:41 +02:00
Jeppe Bundgaard d135a3f0af Add database and Redis replication support to System Status Dashboard, including UI updates, new routes, localization, and unit/E2E tests. 2026-05-17 15:32:45 +02:00
gold-dev 7bd880497f Feat: Disable click redirect, setup checkbox counts 2026-05-13 22:15:28 +03:00
Jeppe Bundgaard d148c0d6b9 Enhance ActionSettingsWheelButton functionality with fixed positioning logic, ResizeObserver integration, and dropdown layout syncing; refactor unit tests for improved mock structure. 2026-05-13 17:36:15 +02:00
Jeppe Bundgaard 75bab7b425 Update UI styles and logic for ActionSettingsWheelButton, invoicing period components, and desktop section layout, adding flyout positioning logic and syncing. 2026-05-13 16:47:31 +02:00
Jeppe Bundgaard 8dfaebba00 Add unit test for user vehicle actions visibility and enhancements for department 75 loading UI, default department handling, and translations. 2026-05-13 15:35:21 +02:00
Jeppe Bundgaard f7f01dcd2f Add E2E and unit tests for invoicing period flag handling, enhance duplicate groups filtering by date, and implement Excel export for department 75 distribution 2026-05-13 13:58:00 +02:00
Jeppe Bundgaard fba85a72c1 Refactor pagination components to use provide and usePaginatedList, optimize fast link handling with queuing and concurrency limits, and update XLVask content generation logic and caching. 2026-05-12 15:03:03 +02:00
Jeppe Bundgaard c657ec57ad Refactor DatePeriodSelector and invoicing period tests: adjust visibility condition for mobile update button, remove redundant test skips, and enhance logic for date shortcut handling across layouts. 2026-05-12 05:38:50 +02:00
Jeppe Bundgaard f24df440d0 Enhance ActionSettingsWheelButton and invoicing period tests with improved structure and visibility checks 2026-05-12 04:52:05 +02:00
Jeppe Bundgaard b73f83a4ab Clean up debug logs in invoicing statistics, add isDateLoading state for period paging, update date change handling with refresh logic, and enhance locale translations and tests. 2026-05-12 04:03:36 +02:00
Jeppe Bundgaard 2700ff3526 Add caching, component updates, and unit tests for invoicing period and XLVask functionality 2026-05-12 03:41:40 +02:00
Jeppe Bundgaard a261f2f3bd Add unit tests and components for invoicing period flag handling and self-wash view rendering 2026-05-11 21:40:51 +02:00
Jeppe Bundgaard 81c4ba0800 Refine safety seal normalization, enhance booking completion flow, and update E2E tests for improved validation and popup handling. 2026-05-11 04:37:34 +02:00
Jeppe Bundgaard 31e72af9bf Warm smoke route chunks before Playwright runs 2026-05-11 03:47:05 +02:00
Jeppe Bundgaard 10a52521d5 Fix invoicing period invoice action 2026-05-11 03:35:36 +02:00
Jeppe Bundgaard c9b34ba36c Fix frontend smoke test races 2026-05-11 03:15:56 +02:00
Jeppe Bundgaard 87ad8f9d31 Merge remote-tracking branch 'origin/master' 2026-05-11 01:57:06 +02:00
Jeppe Bundgaard a754cfc7bb Add delay support for booking completion flow, refine safety seal handling, and enhance popup submission logic with better action management and validation. Update related E2E tests. 2026-05-11 01:56:15 +02:00
gold-dev 29bc0e548b Feat: Header & Daily report block switcher 2026-05-07 16:39:11 +03:00
Jeppe Bundgaard 378598b161 Restrict unassigned department visibility test to desktop, refactor test selectors, and simplify department loading logic. 2026-05-07 14:12:17 +02:00
Jeppe Bundgaard e8c1dda3cc Add E2E test for unassigned department visibility in the overview list. Refactor department sorting and UI logic with priority-based filtering for improved accessibility. 2026-05-07 13:54:53 +02:00
Jeppe Bundgaard fabc4ad54a Refactor department access logic to use canAccessAssignedDepartment. Update related components, tests, and UI for consistency. Add tests for unassigned department handling and align E2E layouts. 2026-05-07 13:50:12 +02:00
Jeppe Bundgaard b89ae2bb9e Sort departments by priority order in UI and API logic. Add support for "order_priority" and normalize fields consistently throughout components and tests. 2026-05-07 13:22:07 +02:00
Jeppe Bundgaard fb162eef6c Add archived department filtering feature for superusers, including E2E tests, UI updates, and OpenAPI enhancements. Extend department table with archived toggle switch and update translations. 2026-05-07 12:54:31 +02:00
Jeppe Bundgaard dc2840e023 Merge remote-tracking branch 'origin/master' 2026-05-07 10:43:02 +02:00
Jeppe Bundgaard fe6d9e8d16 Add E2E tests for department branding management and refine supporting API mocks. Implement branding creation, editing, assignment, and clearing workflows. Extend DepartmentProfile with branding UI and logic updates. 2026-05-07 10:42:45 +02:00
Jeppe Bundgaard 7ab7d83fb5 Align invoicing period specs with formatted date contract 2026-05-07 10:15:49 +02:00
gold-dev 028dad2183 Feat: Time ago to local time hours 2026-05-06 17:08:57 +03:00
gold-dev 6b0fe60958 Feat: Change texts 2026-05-06 16:49:06 +03:00
Jeppe Bundgaard a7789cb631 Merge remote-tracking branch 'origin/master' 2026-05-06 14:15:01 +02:00
Jeppe Bundgaard da65876a64 Add responsive tests for "/qr/new-customer" layout, extend API with split_by_month logic, and update E2E/admin tests with session mocking and department sync fixes. Normalize POS department handling, improve invoicing period split logic, and enhance receipt export safety. 2026-05-06 14:14:34 +02:00
Jeppe Bundgaard f245161bfa Align stale unit specs with current self-serve and invoices views 2026-05-06 09:08:23 +02:00
Jeppe Bundgaard bdffdca420 Handle "STOP" command failures by interpreting "lane not occupied" as a safe-to-clear state. Refactor error handling, add clearActiveWashState, and extend unit/E2E tests with this scenario. 2026-04-30 14:52:40 +02:00
Jeppe Bundgaard 5d1199a133 Update unit test to include lane click event trigger 2026-04-29 17:43:29 +02:00
Jeppe Bundgaard 4752b3d096 Add self-serve feature logic, including lane filtering, normalization, and OpenAPI updates. Enhance Self-Serve Studio with progress indicators, UI controls, path projection streaming, and abort handling. Update E2E and unit tests for enhanced self-serve behaviors. 2026-04-29 17:27:53 +02:00
Jeppe Bundgaard fc9c3700a1 Add Danish self-serve labels and guided wash flow steps. Update UI with guided step actions and bottom control integration. Fix translations and refine E2E/unit test coverage for self-serve flow behavior. 2026-04-29 16:01:34 +02:00
Jeppe Bundgaard a15d6841c5 Add task attachment management to Self-Serve Studio. Extend graph, path outcomes, and flow logic. Update E2E/unit tests with attachment creation, upload, download, deletion flows, and related validations. 2026-04-29 14:41:29 +02:00
Jeppe Bundgaard 9641f67e3a Extend Self-Serve Studio with "action" nodes, supporting creation, focus, and editing workflows. Add unit tests, normalize forms, and enhance virtual binding/gateway logic. Update E2E tests and node visualization settings. 2026-04-29 10:56:40 +02:00
Jeppe Bundgaard de2fa57c63 Add audit issue navigation and task-button normalization to Self-Serve Studio. Implement node ID resolution, audit visualization logic, and reusable task button helpers. Extend E2E and unit tests. 2026-04-29 09:51:09 +02:00
Jeppe Bundgaard a658fe7863 Resume active self-serve washes for authenticated customers across devices. Update OpenAPI, UI components, and extend E2E/unit tests. 2026-04-28 18:00:09 +02:00
Jeppe Bundgaard 503fb84f10 Extend Self-Serve Studio with deferred relay logic and Bookinger-style loading states. Update OpenAPI, UI components, and related E2E/unit tests. 2026-04-28 17:09:50 +02:00
Jeppe Bundgaard 11bb8f824d Extend Self-Serve Studio with vehicle type management, mutation options, and enhanced simulation/debug tools. Update OpenAPI, UI components, and related E2E/unit tests. 2026-04-28 16:54:27 +02:00
Jeppe Bundgaard 8615cc6678 Introduce task-service logic and relay coverage to Self-Serve Studio. Add service normalization, binding service visualizations, and task-service coverage mapping. Extend E2E tests for tasks and bindings. 2026-04-28 15:42:06 +02:00
Jeppe Bundgaard 65e4c27f45 Enhance Self-Serve Studio with dynamic filters, scoped layouts, and optimized auto-layout logic. Update filtering behavior, node visibility, and edge handling. Extend E2E tests for vehicle-specific scenarios. 2026-04-28 15:01:30 +02:00
Jeppe Bundgaard f9d8b87768 Add new table and enhance studio layout logic
Introduce `department_selfserve_studio_layouts` table for department-specific layouts and implement advanced auto-layout functionality in the DepartmentSelfServeStudio module. Added custom node definitions, updated styling, and integrated new logics for sorting and visualizing nodes in the Vue Flow interface.
2026-04-28 14:21:26 +02:00
Jeppe Bundgaard 586a0cc86d Add fullscreen mode to Self-Serve Studio with improved canvas controls and responsive layout adjustments. Extend related E2E tests. 2026-04-28 13:46:47 +02:00
Jeppe Bundgaard e9cbb40559 Add all-in-one self-serve studio with flow editor, simulator, versioning, and enhanced E2E/OpenAPI support. Extend legacy edge gateway workflows. 2026-04-28 13:36:16 +02:00
Jeppe Bundgaard f2c3962749 Improve error handling for lane service updates and add comprehensive tests for edge gateway failures. Refactor route handling and enhance lane/session binding logic. 2026-04-28 12:29:56 +02:00
Jeppe Bundgaard e63e113628 Load EdgeGateway components asynchronously in SuperUserDashboard self-serve view to optimize performance. 2026-04-28 11:37:35 +02:00
Jeppe Bundgaard 4b02de7358 Add self-serve Edge Agents management with navigation, routes, and tabs. Update legacy redirects and extend unit and E2E tests. 2026-04-28 11:32:23 +02:00
Jeppe Bundgaard 61aab76e63 Add self-serve sessions management with filtering, force-stop functionality, and enhanced E2E test coverage. Extend edge gateway module configuration tests. 2026-04-28 10:03:36 +02:00
Jeppe Bundgaard e26e37d9d7 Add enhanced admin access checks and error handling for draft transaction customers and self-serve processes. Extend edge gateway diagnostics, refine terminal readiness detection, and improve live session test coverage. 2026-04-28 09:01:52 +02:00
Jeppe Bundgaard d411da2a12 Enhance edge gateway live sessions, improve WebSocket and error diagnostics, optimize lane relay testing, and extend terminal and command handling features. 2026-04-27 17:39:24 +02:00
Jeppe Bundgaard 15b3c726dc Add relay test modes (local/cloud), enhance lane and self-serve relay testing, and refine UI/UX for department hardware workspace. 2026-04-27 16:25:18 +02:00
Jeppe Bundgaard ee47c801ea Enhance edge gateway live session handling, extend telemetry updates, refine department lane bindings, and replace legacy invoice components. 2026-04-27 14:14:52 +02:00
Jeppe B d0993d2963 Merge pull request #50 from copenhagentruckwash/codex/github-actions-green-20260424-local
Stabilize frontend unit test environments
2026-04-24 21:09:18 +02:00
Jeppe Bundgaard 85a6506bfd Stabilize frontend unit test environments 2026-04-24 19:22:24 +02:00
Jeppe B 7a489e371f Merge pull request #49 from copenhagentruckwash/run-tests-on-mobile-and-desktop
Stabilize mobile/desktop Playwright smoke tests and JSON warmup
2026-04-24 18:59:29 +02:00
Jeppe B 17b973d77c Stabilize cross-device Playwright smoke coverage 2026-04-24 15:13:18 +02:00
Jeppe Bundgaard 4febb998c3 Add mass-import feature for customer data including UI, parsing enhancements, and E2E testing 2026-04-23 19:28:13 +02:00
Jeppe Bundgaard 04467316c2 Add gateway health handling to unit and E2E tests, refine department lanes table logic, and enhance advanced view behavior. 2026-04-23 18:45:01 +02:00
Jeppe Bundgaard cc0b238de9 Align unit contracts with latest master routes and POS UI 2026-04-23 15:47:02 +02:00
Jeppe Bundgaard f6904335dc Merge remote-tracking branch 'origin/master' 2026-04-23 15:38:43 +02:00
Jeppe Bundgaard 2929c418fe Enhance edge gateway diagnostics, refine relay management, extend terminal features, and update tests with new scenarios. 2026-04-23 15:38:27 +02:00
Jeppe Bundgaard 35f52c89c4 Format stale unit spec updates with Prettier 2026-04-23 15:33:56 +02:00
Jeppe Bundgaard 3c32d1375b Fix stale unit specs for current router and edge workspace 2026-04-23 15:26:43 +02:00
Jeppe Bundgaard 200b31ecd8 Fix mojibake in POS flow spec 2026-04-23 15:14:02 +02:00
Jeppe Bundgaard 3934397799 Merge remote-tracking branch 'origin/master' 2026-04-23 15:02:43 +02:00
Jeppe Bundgaard a36af34f15 Expand edge gateway diagnostics, refine installer session logic, update E2E tests, and introduce task/log/statistics views. 2026-04-23 15:01:24 +02:00
Jeppe Bundgaard 74613746c7 Fix self-serve type modules for Vite build 2026-04-23 14:58:43 +02:00
Jeppe Bundgaard 326cf45040 Improve edge gateway styling, enhance relay management logic, and expand E2E tests with new diagnostics and onboarding scenariosEnhance edge gateway UI styling, refine relay bindings management, introduce transport diagnostics, and update E2E tests with new scenarios. 2026-04-23 14:47:55 +02:00
Jeppe Bundgaard 58f9e48e3e Improve edge gateway styling, enhance relay management logic, and expand E2E tests with new diagnostics and onboarding scenarios 2026-04-23 14:47:40 +02:00
Jeppe Bundgaard 9457d842ca Merge remote-tracking branch 'origin/master'
# Conflicts:
#	tests/unit/edge-gateway-workspace.spec.js
2026-04-23 14:38:19 +02:00
Jeppe Bundgaard 2b9bf8cd42 Relax statistics merge source assertion 2026-04-23 14:36:05 +02:00
Jeppe Bundgaard 3dab2fa901 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/features/edgeGateways/EdgeGatewayManager.vue
#	tests/unit/edge-gateway-service.spec.js
#	tests/unit/edge-gateway-workflow-helpers.spec.js
#	tests/unit/edge-gateway-workspace.spec.js
#	vite.config.js
2026-04-23 14:34:21 +02:00
Jeppe Bundgaard c031f28541 Format workspace spec with pinned Prettier 2026-04-23 14:24:00 +02:00
Jeppe Bundgaard a92bad7485 Format edge gateway workspace regression spec 2026-04-23 14:16:15 +02:00
Jeppe Bundgaard af85076ef4 Enhance lane relay management in edge gateway workspace, refine installer polling logic, and accelerate test execution 2026-04-23 14:13:15 +02:00
Jeppe Bundgaard 1faf311787 Preserve staged gateway version in statistics 2026-04-23 14:09:57 +02:00
Jeppe Bundgaard f568270c9f Normalize rotated edge gateway credentials 2026-04-23 13:50:58 +02:00
Jeppe Bundgaard 69fc59b546 Update edge gateway smoke landing expectations 2026-04-23 13:37:06 +02:00
Jeppe Bundgaard 3a4cdbaeb9 Expand edge gateway workspace and streamline app loading 2026-04-23 13:33:12 +02:00
Jeppe Bundgaard f111541056 Restore Vite dependency discovery for Playwright 2026-04-23 13:21:59 +02:00
Jeppe Bundgaard 4ef59b0355 Replace vue-toast-notification with custom useAppToast across components and enhance Edge Gateway installer state management. 2026-04-23 11:07:12 +02:00
Jeppe Bundgaard 50190da52f Revise QR code generation library, enhance POS flow, and update Edge Gateway management views and tests 2026-04-23 10:48:08 +02:00
Jeppe Bundgaard fc26e12013 Remove configuration cache problems report 2026-04-23 10:32:08 +02:00
1589 changed files with 268398 additions and 12896 deletions
+11
View File
@@ -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:*)"
]
}
}
+17
View File
@@ -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
+15
View File
@@ -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
-28
View File
@@ -1,28 +0,0 @@
name: Qodana Configuration Upload
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
upload-qodana-config:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Run Qodana Configuration Uploader
env:
QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }}
run: |
docker run --rm \
-v $(pwd):/workspace \
-w /workspace \
-e QODANA_CONFIGURATIONS_TOKEN=$QODANA_CONFIGURATIONS_TOKEN \
jetbrains/qodana-configuration-uploader:latest \
--global-configs-file qodana-global-configurations.yaml \
--qodana-host https://qodana.cloud
+8 -4
View File
@@ -7,9 +7,13 @@ on:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
upload-qodana-config:
runs-on: ubuntu-latest
runs-on: [self-hosted, Linux, X64, default]
timeout-minutes: 10
steps:
- name: Checkout repository
@@ -20,9 +24,9 @@ jobs:
QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }}
run: |
docker run --rm \
-v $(pwd):/workspace \
-v "$(pwd):/workspace" \
-w /workspace \
-e QODANA_CONFIGURATIONS_TOKEN=$QODANA_CONFIGURATIONS_TOKEN \
jetbrains/qodana-configuration-uploader:latest \
-e QODANA_CONFIGURATIONS_TOKEN \
jetbrains/qodana-configuration-uploader@sha256:f4786ceea616048c3401cf0b0345d2220d22a2ec7b046fd48cbbfc522e6efe30 \
--global-configs-file qodana-global-configurations.yaml \
--qodana-host https://qodana.cloud
+192
View File
@@ -0,0 +1,192 @@
name: Frontend Release
on:
workflow_run:
workflows:
- Automated Tests
types:
- completed
branches:
- master
permissions:
contents: read
actions: read
concurrency:
group: frontend-release-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
build-upload-and-verify:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push'
runs-on: [self-hosted, Linux, X64, default]
env:
RELEASE_BASE_URL: https://api-v2.truckwash.io/master/frontend
PLAYWRIGHT_BASE_URL: https://dev.truckwash.io
PLAYWRIGHT_RELEASE_STATIC_BASE_URL: https://api-v2.truckwash.io/master/frontend
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
PLAYWRIGHT_RELEASE_API_PING_PATHS: /master/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.event.workflow_run.head_sha }}
RELEASE_WAIT_INITIAL_SECONDS: 45
RELEASE_WAIT_TIMEOUT_SECONDS: 600
RELEASE_POLL_INTERVAL_SECONDS: 10
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha }}
- name: Check release commit is current
id: branch-head
run: |
latest_sha="$(git ls-remote origin "refs/heads/$RELEASE_BRANCH" | awk '{print $1}')"
if [[ -z "$latest_sha" ]]; then
echo "Could not resolve origin/$RELEASE_BRANCH." >&2
exit 1
fi
if [[ "$latest_sha" != "$RELEASE_EXPECTED_COMMIT" ]]; then
echo "current=false" >> "$GITHUB_OUTPUT"
echo "Skipping stale release for $RELEASE_EXPECTED_COMMIT; origin/$RELEASE_BRANCH is $latest_sha."
exit 0
fi
echo "current=true" >> "$GITHUB_OUTPUT"
echo "Release commit is current for $RELEASE_BRANCH."
env:
RELEASE_BRANCH: ${{ github.event.workflow_run.head_branch }}
- name: Setup Node.js
if: steps.branch-head.outputs.current == 'true'
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
- name: Install dependencies
if: steps.branch-head.outputs.current == 'true'
run: npm ci --legacy-peer-deps
- name: Check AI workflow sync
if: steps.branch-head.outputs.current == 'true'
run: node scripts/sync-ai-workflow.mjs --check
- name: Source and i18n checks
if: steps.branch-head.outputs.current == 'true'
run: |
npm run text:check-encoding
npm run i18n:v2:source-check
- name: Unit tests
if: steps.branch-head.outputs.current == 'true'
run: npm run test:unit
env:
VITEST_BATCH_SIZE: 5
- name: Build release artifact
if: steps.branch-head.outputs.current == 'true'
run: npm run build
- name: Install Playwright Chromium
if: steps.branch-head.outputs.current == 'true'
run: node scripts/install-playwright-browsers.mjs chromium
- name: Production Playwright gate
if: steps.branch-head.outputs.current == 'true'
run: npm run test:e2e:prod
- name: Upload dist artifact
if: steps.branch-head.outputs.current == 'true'
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: frontend-dist-${{ env.RELEASE_BUILD_ID }}
path: dist
retention-days: 3
- name: Request Release Manager auto sync
if: steps.branch-head.outputs.current == 'true'
run: |
test -n "$RELEASE_MANAGER_GATE_TOKEN" || (echo "RELEASE_MANAGER_GATE_TOKEN is required" >&2; exit 1)
response_file="$(mktemp)"
status_code="$(curl --show-error --silent \
--output "$response_file" \
--write-out "%{http_code}" \
-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\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$RELEASE_EXPECTED_BUILD_ID\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"api_gateway\"]}")"
if [[ "$status_code" =~ ^2 ]]; then
cat "$response_file"
elif [[ "$status_code" == "504" ]]; then
echo "Release Manager auto sync request reached the gateway timeout; continuing to artifact wait."
else
cat "$response_file" >&2
echo "Release Manager auto sync request failed with HTTP $status_code." >&2
exit 1
fi
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 }}
RELEASE_REPOSITORY: ${{ github.repository }}
RELEASE_BRANCH: ${{ github.event.workflow_run.head_branch }}
RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Wait for Coolify release artifact
if: steps.branch-head.outputs.current == 'true'
run: npm run release:verify-upload
- name: Public live Playwright gate
if: steps.branch-head.outputs.current == 'true'
run: npm run test:e2e:live:public
env:
NODE_OPTIONS: --use-system-ca
- name: Credentialed live Playwright gate
if: steps.branch-head.outputs.current == 'true'
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
if: steps.branch-head.outputs.current == 'true'
run: |
test -n "$RELEASE_MANAGER_GATE_TOKEN" || (echo "RELEASE_MANAGER_GATE_TOKEN is required" >&2; exit 1)
release_gate_build_id="${RELEASE_VERIFIED_BUILD_ID:-$RELEASE_EXPECTED_BUILD_ID}"
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\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$release_gate_build_id\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"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 }}
RELEASE_REPOSITORY: ${{ github.repository }}
RELEASE_BRANCH: ${{ github.event.workflow_run.head_branch }}
RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Update server version after verification
if: steps.branch-head.outputs.current == 'true'
run: npm run release:update-server-version
env:
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
RELEASE_VERSION: ${{ github.event.workflow_run.head_sha }}
- name: Upload Playwright report
if: failure() && steps.branch-head.outputs.current == 'true'
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: frontend-release-playwright-${{ env.RELEASE_BUILD_ID }}
path: output/playwright
if-no-files-found: ignore
retention-days: 3
+83 -61
View File
@@ -16,8 +16,9 @@ concurrency:
jobs:
format-tests:
# Match the labels exposed by the Coolify-managed GitHub runner.
# CI runs on the repository's self-hosted runner pool.
runs-on: [self-hosted, Linux, X64, default]
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v5
@@ -40,6 +41,7 @@ jobs:
build-and-unit:
needs: format-tests
runs-on: [self-hosted, Linux, X64, default]
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v5
@@ -53,6 +55,12 @@ jobs:
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Lint
run: npm run lint
- name: Check i18n source consistency
run: npm run i18n:v2:check
- name: Build sanity check
run: npm run build
@@ -61,17 +69,49 @@ jobs:
env:
VITEST_BATCH_SIZE: 5
e2e-smoke:
e2e-pr:
if: github.event_name != 'schedule'
needs: build-and-unit
runs-on: [self-hosted, Linux, X64, default]
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 30
strategy:
fail-fast: false
max-parallel: 4
matrix:
suite: [core, changed]
project: [chromium-desktop, chromium-mobile]
env:
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-pr-${{ matrix.suite }}-${{ matrix.project }}
PLAYWRIGHT_REPORTER_MODE: line-html
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Resolve Playwright diff refs
id: playwright-diff
shell: bash
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
EVENT_NAME: ${{ github.event_name }}
HEAD_SHA: ${{ github.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
zero_sha="0000000000000000000000000000000000000000"
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
base_ref="$PR_BASE_SHA"
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
base_ref="origin/$DEFAULT_BRANCH"
else
base_ref="$PUSH_BEFORE_SHA"
fi
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v5
@@ -83,72 +123,51 @@ jobs:
run: npm ci --legacy-peer-deps
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
run: node scripts/install-playwright-browsers.mjs chromium
- name: Run Playwright smoke tests (${{ matrix.project }})
run: npx playwright test --grep @smoke --project="${{ matrix.project }}"
- name: Run Playwright smoke tests
if: matrix.suite == 'core'
run: |
ulimit -n 16384 || true
npx playwright test --grep @smoke --project="${{ matrix.project }}"
- name: Run Playwright PR core tests
if: matrix.suite == 'core'
run: |
ulimit -n 16384 || true
npm run test:e2e:pr -- --core-only --project="${{ matrix.project }}"
- name: Run Playwright changed-area tests
if: matrix.suite == 'changed'
run: |
ulimit -n 16384 || true
npm run test:e2e:pr -- --changed-only --project="${{ matrix.project }}" --base="${{ steps.playwright-diff.outputs.base }}" --head="${{ steps.playwright-diff.outputs.head }}"
- name: Upload Playwright report
if: failure()
if: failure() || cancelled()
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: playwright-report-smoke-${{ matrix.project }}
name: playwright-report-pr-${{ matrix.suite }}-${{ matrix.project }}
path: |
output/playwright/report
output/playwright/test-results
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}-*
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}
if-no-files-found: ignore
retention-days: 7
e2e-edge-gateway:
if: github.event_name != 'schedule'
needs: build-and-unit
runs-on: [self-hosted, Linux, X64, default]
strategy:
fail-fast: false
matrix:
project: [chromium-desktop, chromium-mobile]
steps:
- name: Checkout repository
uses: actions/checkout@v5
- 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: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run Edge Gateway route and visual tests (${{ matrix.project }})
run: >-
npx playwright test
tests/e2e/edge-gateways.routes.spec.js
tests/e2e/edge-gateways.visual.spec.js
--project="${{ matrix.project }}"
--workers=1
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-edge-gateway-${{ matrix.project }}
path: |
output/playwright/report
output/playwright/test-results
if-no-files-found: ignore
retention-days: 7
retention-days: 3
e2e-full:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch
needs: build-and-unit
if: >
always() &&
(github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch) &&
needs.build-and-unit.result == 'success' &&
(github.event_name == 'schedule' || needs.e2e-pr.result == 'success')
needs: [build-and-unit, e2e-pr]
name: E2E-full-${{ matrix.role }}-${{ matrix.browser_label }}-${{ matrix.device }}
runs-on: [self-hosted, Linux, X64, default]
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 60
strategy:
fail-fast: false
max-parallel: 4
matrix:
role: [customer, subuser, admin, superuser]
browser: [chromium, firefox, webkit]
@@ -180,13 +199,16 @@ jobs:
run: npm ci --legacy-peer-deps
- name: Install Playwright browsers
run: npx playwright install --with-deps ${{ matrix.browser_install }}
run: node scripts/install-playwright-browsers.mjs ${{ matrix.browser_install }}
- name: Run full Playwright slice
run: npm run test:e2e:full:slice -- --role="${{ matrix.role }}" --project="${{ matrix.browser }}-${{ matrix.device }}"
run: |
ulimit -n 16384 || true
npm run test:e2e:full:slice -- --role="${{ matrix.role }}" --project="${{ matrix.browser }}-${{ matrix.device }}"
- name: Upload Playwright report
if: failure()
if: failure() || cancelled()
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: playwright-report-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}
@@ -195,4 +217,4 @@ jobs:
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/test-results
output/playwright/test-lists/${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}.txt
if-no-files-found: ignore
retention-days: 14
retention-days: 3
+59287
View File
File diff suppressed because it is too large Load Diff
View File
View File
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
3.0
View File
+4
View File
@@ -0,0 +1,4 @@
{
"version": 1,
"setupCompletedAt": "2026-06-07T11:37:04.444Z"
}
+26
View File
@@ -0,0 +1,26 @@
FROM node:24-alpine AS build
WORKDIR /app
RUN apk add --no-cache git
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY . .
ARG SOURCE_COMMIT
ARG RELEASE_COMMIT_SHA
ARG COMMIT_SHA
ARG GITHUB_SHA
ENV SOURCE_COMMIT=$SOURCE_COMMIT
ENV RELEASE_COMMIT_SHA=$RELEASE_COMMIT_SHA
ENV COMMIT_SHA=$COMMIT_SHA
ENV GITHUB_SHA=$GITHUB_SHA
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
+30
View File
@@ -22,6 +22,36 @@ npm install
npm run dev
```
By default, the Vite dev server proxies `/api/*` to the remote stable API at
`https://api-v2.truckwash.io/master/api`. This lets the Vue app run locally
without a local PHP API container.
To develop against a local PHP API instead:
```powershell
$env:VITE_API_PROXY_TARGET="http://localhost"; npm run dev
```
To use another remote API route:
```powershell
$env:VITE_API_PROXY_BASE_PATH="/canary/api"; npm run dev
```
TLS certificate validation is enabled for proxied HTTPS APIs by default. If you
are using a trusted local HTTPS API with a self-signed certificate, you can opt
out explicitly:
```powershell
$env:VITE_API_PROXY_TARGET="https://local-api.test"; $env:VITE_API_PROXY_SECURE="false"; npm run dev
```
For compatible local gateways that expect the `/api` prefix to be preserved:
```powershell
$env:VITE_API_PROXY_TARGET="http://localhost"; $env:VITE_API_PROXY_STRIP_PREFIX="false"; npm run dev
```
### Compile and Minify for Production
```sh
+3 -3
View File
@@ -56,9 +56,9 @@ android {
defaultConfig {
applicationId "io.truckwash.twa"
minSdkVersion 21
targetSdkVersion 35
versionCode 6
versionName "6"
targetSdkVersion 36
versionCode 7
versionName "7"
// The name for the application
resValue "string", "appName", twaManifest.name
+1 -1
View File
@@ -1 +1 @@
{"name":"Truck Wash Kundeportal","short_name":"Truck Wash","description":"Access your Truck Wash accounts and transactions from anywhere.","start_url":"/","display":"standalone","background_color":"#0787bb","theme_color":"#063651","lang":"en","scope":"/","orientation":"portrait","launch_handler":{"client_mode":"navigate-existing"},"icons":[{"src":"favicons/web-app-manifest-192x192.png","sizes":"192x192","type":"image/png"},{"src":"favicons/web-app-manifest-512x512.png","sizes":"512x512","type":"image/png"}],"id":"pleno-pwa-test"}
{"name":"Truck Wash Kundeportal","short_name":"Truck Wash","description":"Access your Truck Wash accounts and transactions from anywhere.","start_url":"/","display":"standalone","background_color":"#0787bb","theme_color":"#063651","lang":"en","scope":"/","orientation":"portrait","launch_handler":{"client_mode":"navigate-existing"},"icons":[{"src":"favicons/web-app-manifest-192x192.png","sizes":"192x192","type":"image/png"},{"src":"favicons/web-app-manifest-512x512.png","sizes":"512x512","type":"image/png"}],"id":"/"}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
# User wash start production-readiness QA matrix
This note documents deterministic coverage for the self-serve user wash start flow. The `@dynamic-image` Playwright case remains separated because it validates rendered image behavior in addition to deterministic state and API transitions.
## Unit matrix
| Area | Required scenario | Coverage |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useWashFlowState` | Step transitions for vehicle, questions, lane, tasks, in-progress, completed | `tests/unit/use-wash-flow-state-production.spec.js` validates step clickability, next-button gating, questions-to-lane updates, lane-to-start target selection, task completion transition, in-progress navigation, and completed-step non-clickability contract. |
| `useWashSessionActions` | START success and failure | `tests/unit/use-wash-session-actions-production.spec.js` covers successful manual START state mutation and failed START retryability without active-wash mutation. |
| `useWashSessionActions` | Relay enable success and relay enable failure with STOP rollback | `tests/unit/use-wash-session-actions-production.spec.js` covers machine relay success and relay failure rollback via STOP. |
| `useWashSessionActions` | STOP failure and already-stopped STOP recovery | `tests/unit/use-wash-session-actions-production.spec.js` covers failed STOP preserving active state and already-not-occupied STOP clearing local state. |
| `useSelfServeLogic` | Preview/summary merge | `tests/unit/use-self-serve-logic-production.spec.js` covers preview questions merging with summary questions, answer maps, visible question order, tasks, and allowed services. |
| `useSelfServeLogic` | Request race handling | `tests/unit/use-self-serve-logic-production.spec.js` covers stale preview/summary responses being ignored when a newer request wins. |
| `useSelfServeLogic` | Allowed services updates | `tests/unit/use-self-serve-logic-production.spec.js` covers lane allowed-service endpoint updates and first-failure fallback behavior. |
| `useSelfServeLogic` | Answer sync failures | `tests/unit/use-self-serve-logic-production.spec.js` covers error reporting while preserving the caller's optimistic local answer. |
| `MyWashStart.vue` | Local progress restore, server active-wash restore, recent-completion suppression, unmount cleanup, duplicate-fetch prevention | `tests/unit/my-wash-start-production.spec.js` locks the component contracts for restore ordering, authenticated active-wash application, recent-completion suppression across restore/polling, unmount cleanup, and duplicate-fetch/sync guards. |
## E2E mocked matrix
| Required scenario | Coverage |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Full manual flow | `tests/e2e/self-serve-wash.spec.js` covers direct manual wash start, guided completion, STOP, exit-gate open, completed state, and close/reset. |
| Full machine flow | `tests/e2e/self-serve-wash.spec.js` covers machine task rendering, dynamic image progress, required task completion, machine start path, and reload persistence. |
| Reload/resume active wash | `tests/e2e/self-serve-wash.spec.js` covers local progress reload and authenticated server active-wash resume from another device. |
| Backend says wash completed during polling | `tests/e2e/self-serve-wash.spec.js` covers server polling of active wash and local transition to completed when the backend no longer reports the matching in-progress wash. |
| Network failures for preview, answer sync, START, relay enable, STOP | `tests/e2e/self-serve-wash.spec.js` covers retryable START failure, STOP failure preservation, allowed-services gateway timeout/retry, answer sync background resilience, and unit-level relay rollback. Add mocked network route overrides when expanding browser-level failure assertions. |
## Optional live smoke
| Optional scenario | Coverage |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Dynamic image smoke | The `@dynamic-image` Playwright test in `tests/e2e/self-serve-wash.spec.js` is tagged separately from deterministic CI coverage so it can be included or excluded explicitly with Playwright grep controls. |
Vendored
+1
View File
@@ -5,6 +5,7 @@ interface ImportMetaEnv {
readonly VITE_APP_VERSION: string,
readonly VITE_COMMIT_HASH: string,
readonly VITE_IS_DEV: string,
readonly VITE_RELEASE_SOURCE: 'local' | 'deployment' | 'auto',
}
interface ImportMeta {
readonly env: ImportMetaEnv
+139
View File
@@ -0,0 +1,139 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import vue from "eslint-plugin-vue";
const commonGlobals = {
...globals.browser,
...globals.node,
...globals.es2024,
grecaptcha: "readonly",
};
const vitestGlobals = {
...globals.vitest,
};
const lintTargets = ["**/*.{js,mjs,cjs,ts,tsx,vue}"];
const tsTargets = ["**/*.{ts,tsx,mts,cts}"];
function warningRules(rules = {}) {
return Object.fromEntries(
Object.entries(rules).map(([name, value]) => {
if (value === "off" || value === 0) {
return [name, value];
}
if (Array.isArray(value)) {
return [name, ["warn", ...value.slice(1)]];
}
return [name, "warn"];
}),
);
}
function warningConfig(config, files) {
return {
...config,
files: config.files ?? files,
rules: warningRules(config.rules),
};
}
const jsRecommended = warningConfig(js.configs.recommended, lintTargets);
const tsRecommended = tseslint.configs.recommended.map((config) =>
warningConfig(config, tsTargets),
);
const vueEssential = vue.configs["flat/essential"].map((config) =>
warningConfig(config, ["**/*.vue"]),
);
const ignoredPaths = [
"app/build/**",
"coverage/**",
"dist/**",
"node_modules/**",
"node_modules.codex-backup/**",
"output/**",
"public/build/**",
"src/assets/test/**",
"test/**",
"vendor/**",
"*.timestamp-*.mjs",
"**/*.timestamp-*.mjs",
];
const commonUnusedOptions = {
argsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
varsIgnorePattern: "^_",
};
export default [
{
ignores: ignoredPaths,
},
jsRecommended,
...tsRecommended,
...vueEssential,
{
files: ["**/*.vue"],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
extraFileExtensions: [".vue"],
ecmaVersion: "latest",
sourceType: "module",
},
},
},
{
files: lintTargets,
plugins: {
vue,
},
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: commonGlobals,
},
rules: {
"no-console": "off",
"no-debugger": "warn",
"no-empty": "warn",
"no-undef": "warn",
"no-unused-vars": ["warn", commonUnusedOptions],
"no-useless-assignment": "warn",
"vue/multi-word-component-names": "off",
"vue/no-mutating-props": "warn",
"vue/no-unused-components": "warn",
"vue/no-unused-vars": "warn",
"vue/no-v-html": "off",
},
},
{
files: ["**/*.{ts,tsx}"],
plugins: {
"@typescript-eslint": tseslint.plugin,
},
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["warn", commonUnusedOptions],
},
},
{
files: ["tests/**/*.{js,ts}", "playwright*.ts", "scripts/**/*.{js,mjs}"],
languageOptions: {
globals: {
...commonGlobals,
...globals.node,
...vitestGlobals,
},
},
rules: {
"no-empty": "warn",
},
},
];
+14 -7
View File
@@ -2,13 +2,20 @@
<html lang="" class="theme-light" data-theme="light">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicons/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicons/favicon.svg" />
<link rel="shortcut icon" href="/favicons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<script>
(function () {
var match = window.location.pathname.match(/^\/[^/]+\/frontend(?:\/|$)/);
var href = match ? match[0].replace(/\/+$/, '') + '/' : '/';
var base = document.createElement('base');
base.href = href;
document.currentScript.after(base);
})();
</script>
<link rel="icon" type="image/png" href="%BASE_URL%assets/favicons/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="%BASE_URL%assets/favicons/favicon.svg" />
<link rel="shortcut icon" href="%BASE_URL%assets/favicons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="%BASE_URL%assets/favicons/apple-touch-icon.png" />
<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="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
@@ -17,6 +24,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/releaseBootstrap.js"></script>
</body>
</html>
View File
+75
View File
@@ -0,0 +1,75 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
location ~ ^/(?:.+/)?(?<webmanifest_path>(?:assets/)?manifest\.webmanifest)$ {
types { application/manifest+json webmanifest; }
default_type application/manifest+json;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
try_files /$webmanifest_path =404;
}
location ~ ^/(release-entry|release-manifest)\.json$ {
add_header Cache-Control "no-store";
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
try_files $uri =404;
}
location ~ ^/(master|beta|canary|internal)/frontend/(release-entry|release-manifest)\.json$ {
add_header Cache-Control "no-store";
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
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";
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
try_files /$static_asset_path =404;
}
location ~ ^/(?:.+/)?(?<static_file_path>index\.html|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";
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
try_files $uri =404;
}
location ~ ^/(master|beta|canary|internal)/frontend/assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
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
});
@@ -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
});
@@ -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)
@@ -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
```
@@ -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.
@@ -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' }) });
});
}"
```
@@ -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;
}"
```
@@ -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
```
@@ -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
@@ -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();
```
@@ -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
@@ -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
});

Some files were not shown because too many files have changed in this diff Show More