Compare commits

...
Author SHA1 Message Date
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 20183b36e1 Update edge gateway workflow helper contract 2026-04-23 12:42:43 +02:00
Jeppe Bundgaard 1a2218704d Format updated edge gateway unit specs 2026-04-23 12:32:30 +02:00
Jeppe Bundgaard 90eb67ba8c Align edge gateway workspace with route contract 2026-04-23 12:20:33 +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
Jeppe Bundgaard 85f24bdb0c Improve readability in edge gateway tests and helper functions through consistent formatting adjustments 2026-04-23 09:02:41 +02:00
Jeppe Bundgaard 0f8b0273c1 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/features/edgeGateways/EdgeGatewayManager.vue
2026-04-23 09:01:11 +02:00
Jeppe Bundgaard 7efa9f172c Introduce Edge Gateway management pages and implement WebSocket session handling 2026-04-23 09:00:43 +02:00
Jeppe Bundgaard b576465f50 Stabilize tablet admin auth mocks 2026-04-22 19:38:41 +02:00
Jeppe Bundgaard 8094356488 Make edge gateway inventory reactive 2026-04-22 19:01:10 +02:00
Jeppe Bundgaard 0432564367 Force refresh gateway discovery polling 2026-04-22 18:45:49 +02:00
Jeppe Bundgaard 8391557f2d Repair gateway workspace event wiring 2026-04-22 18:31:50 +02:00
Jeppe Bundgaard c3381aea39 Hydrate stored sessions before guarded routes 2026-04-22 18:02:57 +02:00
Jeppe Bundgaard c72da3879b Repair edge gateway manage page contract 2026-04-22 17:00:38 +02:00
Jeppe Bundgaard f6c8e8123a Match edge gateway service endpoint literals 2026-04-22 16:43:53 +02:00
Jeppe Bundgaard 03bb16309a Fix edge gateway unit contracts 2026-04-22 16:39:15 +02:00
Jeppe Bundgaard b5f9210c42 Restore edge gateway service compatibility 2026-04-22 16:30:48 +02:00
Jeppe Bundgaard bee7d699d3 Introduce Edge Gateway module with configuration support and API refinements. 2026-04-22 16:24:54 +02:00
Jeppe Bundgaard ab1d52da0f Align edge gateway smoke fixture container health 2026-04-22 16:11:01 +02:00
Jeppe Bundgaard 40f1b0e208 Stabilize edge gateway smoke operation sequencing 2026-04-22 16:00:29 +02:00
Jeppe Bundgaard 04ee87b3ad Fix encoding issue in EdgeGatewayOverviewPage by replacing non-standard character with a proper middle dot (·). 2026-04-22 15:42:00 +02:00
Jeppe Bundgaard ba1288ae10 Add tests for new container services, health status, and "Cancel Operation" flow in Edge Gateway operations 2026-04-22 15:39:55 +02:00
Jeppe Bundgaard 7ebcac6fa0 Add tests for "Cancel Operation" behavior and UI elements in Edge Gateway services and workspace. Fix formatting in E2E network tests. 2026-04-22 15:35:37 +02:00
Jeppe Bundgaard 1e21df7c68 Add "Cancel Operation" functionality for Edge Gateways with UI, API, and state management integration. 2026-04-22 15:35:17 +02:00
Jeppe Bundgaard dfea80034a Update Danish localization for "select_customer_invoice" field in i18n files 2026-04-22 15:05:16 +02:00
Jeppe Bundgaard c4af52ad39 Add readJsonFile helper to strip UTF-8 BOM and refactor tests to use it for consistent JSON parsing 2026-04-22 14:47:30 +02:00
Jeppe Bundgaard ce2a18d203 Fix formatting issues across multiple E2E test files to standardize indentation, string quoting, and line breaks, aligning with project conventions. 2026-04-22 14:28:39 +02:00
Jeppe Bundgaard bff949a1f0 Add "Copy Last Wash" matched state with visual feedback and localization adjustments in mobile POS flow. 2026-04-22 14:04:32 +02:00
Jeppe Bundgaard f139cd1001 Improve focus styles and platform consistency for interactive elements in PosDepartmentStepMobile2LastOrder.vue and PosDepartmentStepMobile2Product.vue. 2026-04-22 13:44:07 +02:00
Jeppe Bundgaard 2380b64758 Cache edge gateway data locally, enhance fleet revalidation logic, optimize gateway operations, and improve layout in PosDepartmentStepMobile2Product.vue. 2026-04-22 13:32:24 +02:00
Jeppe Bundgaard 21c42fa443 Introduce caching and workspace persistence for Edge Gateways, optimize API requests with in-flight request tracking, and implement UI layout adjustments in PosDepartmentStepMobile2Product.vue. 2026-04-22 13:25:19 +02:00
Jeppe Bundgaard c03603f3f3 Refactor PosDepartmentStepMobile2Product.vue to improve layout consistency, introduce vehicle-card__additional-items-status container, and ensure responsive design; add bounding box assertions to validate button dimensions in mobile order flow E2E tests. 2026-04-22 13:10:50 +02:00
Jeppe Bundgaard 94f56d9cd0 Standardize and fix formatting issues in PosDepartmentStepMobileFlow.vue, including consistent spacing, indentation, semicolon usage, and string quoting styles, ensuring adherence to project conventions. 2026-04-22 13:05:11 +02:00
Jeppe Bundgaard a24bb41df2 Implement long-press functionality for mobile POS actions, including "Copy Last Wash" and primary product selection, with localization and visual feedback enhancements. 2026-04-22 12:42:21 +02:00
Jeppe Bundgaard d7832910e0 Enhance Playwright dev server setup, add Linux-specific process handling, introduce server output buffering, and expand edge gateway diagnostics and E2E tests. 2026-04-22 12:02:03 +02:00
Jeppe B aa1da3ec4d Merge pull request #48 from copenhagentruckwash/run-tests-and-fix-issues
Run tests and fix issues
2026-04-22 11:58:52 +02:00
Jeppe Bundgaard 9cee2f70ba Merge remote-tracking branch 'origin/run-tests-and-fix-issues' into run-tests-and-fix-issues
# Conflicts:
#	playwright.global-setup.mjs
2026-04-22 11:50:27 +02:00
Jeppe Bundgaard 6e3506ff28 Refactor: Remove unused readJsonFile helper, improve server process handling in Playwright setup, and optimize superuser booking and draft count logic. 2026-04-22 11:49:44 +02:00
Jeppe Bundgaard 827d8a3c39 Classify full E2E draft and superuser specs 2026-04-22 09:42:01 +02:00
Jeppe Bundgaard 892471bd3a Classify full E2E draft and superuser specs 2026-04-22 09:39:37 +02:00
Jeppe B f13bff2ff5 Merge pull request #47 from copenhagentruckwash/run-tests-and-fix-issues
Fix BOM-safe locale parsing in i18n tests
2026-04-21 22:52:29 +02:00
Jeppe Bundgaard 75b8dff0f8 Fix Playwright dev server startup on runner 2026-04-21 22:27:33 +02:00
Jeppe B 13528b07af Update tests.yml 2026-04-21 19:33:10 +02:00
Jeppe B 7bdfb43635 Fix locale test JSON parsing with UTF-8 BOM support 2026-04-21 17:36:37 +02:00
Jeppe Bundgaard 9f9d6de03f Clean up: Remove unused styles in PosDepartmentStepMobile2Product.vue. 2026-04-21 15:32:05 +02:00
Jeppe Bundgaard 83bfdf5f0a Self-heal legacy POS snapshots to preserve normalized vehicle shape. 2026-04-21 15:23:35 +02:00
Jeppe Bundgaard 817c174e0e Add unit tests for Orders navigation count refresh and fallback customer caching, update POS mobile accessibility labels:
- Introduced `orders-draft-count-refresh.spec.js` for testing `Orders` navigation hooks on customer changes and order deletions.
- Enhanced `use-draft-transaction-customer.spec.js` with tests for clearing cached fallback customers.
- Updated POS mobile components with accessibility labels and localization for image viewer and customer suggestion buttons.
- Added support for `vehicleCustomerSuggestionsGet` handling in e2e mocks and tests.
2026-04-21 15:20:50 +02:00
Jeppe Bundgaard 44f77eadc1 Restrict draft export action to superusers and update localization for "send to e-conomic". 2026-04-21 15:04:33 +02:00
Jeppe Bundgaard 46bf518a0e Add E2E tests and harness for customer assignment modal:
- Implemented end-to-end test cases for validating `AssignDraftOrderCustomerModal` workflows, including error handling and behavior consistency.
- Introduced test harness in `change-customer-modal.html` and `change-customer-modal.ts` to facilitate testing of modal interactions.
- Added supporting unit tests for modal behavior and integration with session data.
2026-04-21 14:52:22 +02:00
Jeppe Bundgaard 84e3b0e982 Add Stripe invoice actions and order booking counts:
- Introduced support for managing Stripe invoices in `DepartmentPosOrder.vue`, including helper functions for status handling, action visibility, and cancellation.
- Added `isStripeHostedInvoiceOpen` and `shouldShowStripeInvoiceAction` checks to control panel visibility.
- Enhanced `fetchSuperUserBookingCounts` with a new endpoint for department-scoped booking totals and streamlined logic for grouped counts.
- Updated e2e tests to reflect booking badge tooltips and streamlined department-based count requests.
2026-04-21 13:38:26 +02:00
Jeppe Bundgaard 1bd1b992a2 Add assign draft order customer modal and related e2e tests:
- Introduced `AssignDraftOrderCustomerModal` component with supporting localization for managing customer assignments.
- Added e2e harness for modal testing in `assign-draft-order-customer-modal.html` and `assign-draft-order-customer-modal.ts`.
- Implemented e2e test cases for customer assignment workflows in `assign-draft-order-modal-create.spec.ts` and `assign-draft-order-modal-layout.spec.ts`.
2026-04-21 13:16:22 +02:00
Jeppe Bundgaard 72fdc4c490 Remove EdgeGatewayManager and related assets:
- Deleted `EdgeGatewayManager.vue` alongside its components, templates, styles, and functionality.
- Added tests for `fetchDepartmentOrderBookingCount`.
- Introduced `adminBookingCount.js` for department booking count logic.
2026-04-21 12:46:58 +02:00
Jeppe Bundgaard d7ff64bf4e Add encoding repair utilities and tests:
- Introduced `repairText` and `containsSuspiciousEncoding` utilities for handling corrupted text encodings.
- Added unit tests (`encoding-repair.spec.js`) to validate encoding repair and encoding marker detection.
- Integrated encoding check into unit test workflows (`text:check-encoding` and `text:fix-encoding` scripts).
- Replaced corrupted strings across multiple components and locales with proper UTF-8 encodings.
2026-04-21 10:24:35 +02:00
Jeppe Bundgaard 1a784bbbfd Remove edge gateway components and templates associated with outdated workflows. 2026-04-20 14:51:53 +02:00
Jeppe Bundgaard 5d30690dfd Remove deprecated edge gateway components and associated unit tests:
- Deleted `EdgeGatewayAdvancedOperations.vue` and `EdgeGatewayContextPanel.vue`.
- Removed related unit tests in `use-edge-gateway-terminal.spec.js`.
- Cleaned up unused exports in `useEdgeGatewayTerminal.js`.
2026-04-20 11:20:27 +02:00
Jeppe Bundgaard f3f6237c16 Add step validation for desktop POS workflow and update e2e tests:
- Added `isDesktopStep1Active` computed property to validate step context in `SelectVehicleFormPOS.vue`.
- Updated booking state resolution methods and watchers to respect step validation.
- Extended e2e tests in `pos-desktop-card-payments.spec.js` to validate order reference visibility.
2026-04-20 10:01:45 +02:00
Jeppe Bundgaard 031b1e7563 Hydrate customer details in POS orders and add e2e tests:
- Updated `POSDepartmentProcess.vue` to resolve and hydrate customer details during order processing.
- Added validation and fallback logic for customer state management.
- Introduced new e2e test cases to ensure customer data persists after reloading.
2026-04-20 09:55:44 +02:00
Jeppe Bundgaard 56282a8093 Add Stripe Terminal Reader selection and unit tests:
- Introduced `StripeTerminalReaderSelect` component for terminal reader management.
- Implemented utility functions for normalizing, sorting, and selecting Stripe readers.
- Added unit tests for reader status mapping, normalization, and preferred reader selection.
- Updated `PayWithStripeButton` with new reader selection logic and localization.
2026-04-20 09:46:50 +02:00
Jeppe Bundgaard e0d6f44d0b Update PayWithStripeButton layout for improved responsiveness:
- Added `is-full` and `is-fullwidth` classes to columns and buttons for consistent spacing and alignment.
- Improved readability and visual hierarchy within payment flow states and action buttons.
2026-04-20 09:22:05 +02:00
Jeppe Bundgaard faa7ebae48 Remove redundant context panel references from edge gateway workspace tests 2026-04-16 14:59:48 +02:00
Jeppe Bundgaard 4071363454 Refactor and format test files for edge gateway workflows:
- Improved readability for conditional expressions and multiline function calls in `network.js`.
- Reformatted unit tests in `edge-gateway-workspace.spec.js` for consistent string handling and clarity.
- Enhanced e2e smoke test descriptions and multiline parameter formatting in `edge-gateways.smoke.spec.js`.
2026-04-16 14:46:38 +02:00
Jeppe Bundgaard bef9eaeb72 Add unit tests and new components for edge gateway workflows:
- Introduced unit tests for edge gateway workflow helpers, including workflow step resolution, incident action mapping, relay health row formatting, and workspace state merging.
- Added new components for advanced operations, configuration panel, context panel, fleet rail, and health summary.
- Enhanced gateway management UI with support for advanced actions, fallback operations, relay health visualization, and device binding features.
2026-04-16 13:44:36 +02:00
Jeppe Bundgaard 2d94322021 Refactor hasConnectivityIssue for improved readability and error handling 2026-04-16 10:55:56 +02:00
Jeppe Bundgaard 28bee86ce9 Merge remote-tracking branch 'origin/master' 2026-04-16 10:53:07 +02:00
Jeppe Bundgaard 6c4b9cb599 Add and improve e2e tests for Edge Gateway flows and configure pre-commit hooks:
- Introduced live smoke tests for Edge Gateways, verifying gateway routes and destructive-action prevention.
- Added e2e scenarios for deep-link navigation, unavailable gateway recovery, token rotation, and background page refresh.
- Refactored test helpers for streamlined functional validation in gateway scenarios.
- Updated `EdgeGatewayTerminal.vue` with test IDs for enhanced testability.
- Added and configured Husky pre-commit hooks for automated test file formatting and validation.
2026-04-16 10:52:56 +02:00
Jeppe B 55ec54fae6 Merge pull request #46 from copenhagentruckwash/edge-and-workers
Update workflows, add pre-commit hooks, and enhance edge handling
2026-04-15 13:44:16 +02:00
Jeppe Bundgaard c06808af33 Update e2e-full workflow to use default_branch for conditional checks 2026-04-15 12:41:22 +02:00
Jeppe Bundgaard a9a5137515 Add pre-commit formatting hooks for test files and configure Husky integration:
- Added Husky pre-commit hook to enforce formatting on JavaScript and TypeScript test files.
- Implemented `pre-commit-format-tests.mjs` to check for unstaged changes, format test files using Prettier, and re-stage them.
- Included `prepare-husky.mjs` to verify Husky installation in the git worktree.
- Updated `package-lock.json` to include `husky` as a dependency.
2026-04-15 12:18:49 +02:00
Jeppe Bundgaard a0c8574399 Add edge agent update handling and verification mechanisms
Refactored the edge agent to include robust update handling, verification, and rollback procedures. Enhanced test coverage for critical update flows and introduced support for background edge gateway refresh without disrupting current user actions.
2026-04-15 12:07:32 +02:00
Jeppe Bundgaard 71b8272eb8 Add tests and functionality for edge gateway updates and lifecycle
This commit introduces unit tests, E2E tests, and implementation updates related to edge gateway lifecycle management, including update handling, artifact validation, and rollback mechanisms. It also refines routing, component interaction, and backend methods to improve update tracking, status transitions, and artifact management.
2026-04-15 11:53:45 +02:00
Jeppe Bundgaard 95a8f114bc Refactor e2e test fixtures and formatting for consistency:
- Removed unnecessary string quotes from numeric keys in `cvrSearchResponses` across `mobilePos.js` and related test files.
- Reformatted multiline function chaining and object initialization for improved readability.
- Enhanced fixture setup logic to ensure consistent mock data handling across test scenarios.
2026-04-15 09:23:25 +02:00
Jeppe Bundgaard 6b91097f98 Add unit and e2e tests for auto-selecting non-barred customer suggestions and update POS vehicle forms:
- Implemented unit tests for `VehicleCustomerSuggestionsPos` to validate auto-selection and manual override behaviors.
- Added e2e scenarios to verify auto-application of non-barred customer suggestions in desktop POS.
- Refactored customer suggestion handling logic to support new `selectionSource`.
- Updated POS and booking components to improve multi-department booking handling and prevent cross-department conflicts.
- Enhanced booking match validation and streamlined customer suggestion methods.
2026-04-15 09:16:43 +02:00
Jeppe Bundgaard 00151ec237 Add unit tests and e2e coverage for POS mobile add-customer autofill functionality:
- Added unit tests for `applyPosAddCustomerSearchResult` to handle autofill field updates, manual overrides, and stale response rejection.
- Implemented a new e2e scenario for preserving manual edits during autofill updates and ignoring stale delayed responses.
- Refactored `mobilePos.js` fixture setup to include CVR search and customer registration mock logic.
- Added and integrated `PosDesktopCustomerConflictModal.vue` for UI handling of customer conflicts.
2026-04-15 08:56:30 +02:00
Jeppe Bundgaard b5a8199472 Reduce default Vitest batch size to 5, enforce single-threaded execution, and refactor unit test wait conditions:
- Adjusted `run-vitest-unit-batches.mjs` to lower default batch size from 10 to 5 and enforce single-threaded execution for more predictable test behavior.
- Updated GitHub workflows to align with new batch size defaults.
- Introduced `waitForCondition` helper in unit tests to replace arbitrary delays with predicate-based waiting.
- Refactored `useEdgeGatewayTerminal` for improved session handling, including idle delay for shell polling and cleanup of request queuing logic.
2026-04-14 17:55:38 +02:00
Jeppe Bundgaard c8f917ac30 Add batch support for Vitest unit tests and update workflows:
- Introduced `run-vitest-unit-batches.mjs` script for running unit tests in batches with configurable batch size.
- Updated `package.json` to replace `test:unit` command with the batch runner and added `test:unit:single` for direct execution.
- Tweaked GitHub workflows (`tests.yml`, `code_quality.yml`) to use updated actions and support batch testing.
- Minor formatting improvements in Vue components and workflows for consistent style.
2026-04-14 17:46:00 +02:00
Jeppe Bundgaard 586aba8531 Refactor collectedOrderInvoiceManageStripe.vue to integrate economic queue job for Stripe invoice booking:
- Replaced refs with computed properties for status determination (paid, draft, booked, etc.).
- Integrated `useEconomicQueueJob` for stripe transfer management, including enqueue, retry, and transport error handling.
- Streamlined Stripe payment data mapping with `computed` for enhanced maintainability.
- Updated template to include improved UI elements for transfer status and error feedback.
- Added progress indicators and notifications for queue operations.
2026-04-14 17:34:46 +02:00
Jeppe Bundgaard bf7c01ce25 Update Vite SCSS config: enable quietDeps and expand silenceDeprecations list 2026-04-14 17:30:39 +02:00
Jeppe Bundgaard 1236770513 Refactor e2e tests for improved readability and formatting consistency:
- Reformatted callback and function parameters across booking flow, POS, and visual test files.
- Enhanced readability by aligning multiline arguments with consistent indentation.
- Updated test messages for clarity and better context.
2026-04-14 17:24:54 +02:00
Jeppe Bundgaard 3d8203945d Skip unstable test scenarios for Firefox desktop and WebKit mobile; update visual snapshots. 2026-04-14 17:10:28 +02:00
Jeppe Bundgaard 2be33c89a7 Add Edge Broker service and configuration defaults
Introduced the Edge Broker service in the Docker Compose setup, configured to run on port 4300. Updated `.env.example` to include default environment variables for the Edge Broker and adjusted tests to validate the new configuration. Updated visual snapshot tests to reflect related UI changes.
2026-04-14 16:48:40 +02:00
Jeppe Bundgaard 631e9c4488 Enhance POS mobile e2e tests:
- Added fallback actions to handle missing customer selection and attachment toggles.
- Introduced visibility checks for crucial elements (e.g., "Take Picture" and "Close" actions).
- Updated visual test snapshots for duplicate warnings on desktop.
2026-04-14 16:32:16 +02:00
Jeppe Bundgaard 1afdda2650 Refactor POS workflows and improve booking flow:
- Updated `PayWithStripeButton` to handle setup requirements and improved error state handling.
- Enhanced POS mobile popup completion flow with added callback support and fallback actions.
- Introduced booking safety seal and improved step navigation logic.
- Added new e2e tests for booking confirmation, order completion, and error scenarios.
- Adjusted POS e2e and visual tests for updated workflows and UI changes.
- Refactored POS component logic for consistency and improved readability.
2026-04-14 16:19:11 +02:00
Jeppe Bundgaard 6f23941ea4 Add unit tests for ActionSettingsWheelButton i18n key coverage:
- Introduced validation to ensure all literal translation keys in the component are present across active locales.
- Enhanced coverage by integrating locale JSONs and adding missing key detection.
- Refactored button test setup with extracted `SETTINGS_WHEEL_TRANSLATION_KEYS` and reusable helper methods.
2026-04-14 14:32:01 +02:00
Jeppe Bundgaard 983585ede7 Refactor and cleanup:
- Migrate postinstall script to `postinstall-sync-playwright-root-links.mjs` for streamlined path resolution.
- Replace `axios` v1.15.0 with v1.13.5 and downgrade `vite` from v8.0.5 to v7.1.11.
- Update testing code for consistent formatting and enhanced readability (e.g., `poll` and `catch` calls).
- Remove unused or redundant dependency flags and align `package-lock.json` with new configuration.
2026-04-14 14:17:28 +02:00
Jeppe Bundgaard b01f400d2c Add e2e visual snapshots for Edge Gateways and POS workflows. 2026-04-14 13:50:25 +02:00
Jeppe Bundgaard ecec624aea Update axios to v1.15.0 and vite to v8.0.5; modify dependency flags for improved build configuration. 2026-04-14 13:48:43 +02:00
Jeppe Bundgaard bbbad2c634 Remove dependency on the ws module.
Deleted the entire `ws` library and its associated files. This likely reflects a move away from the WebSocket library, possibly signaling an alternate implementation or unused/legacy code cleanup.
2026-04-14 13:45:15 +02:00
Jeppe Bundgaard 4963fed0b5 Add unit and e2e tests for POS mobile popups:
- Introduced `headerCloseTestId` and `showHeaderClose` properties in visible-header popups for improved testability.
- Added unit tests to validate header close button behavior across popups.
- Enhanced e2e tests to verify popup visibility and header close functionality in mobile workflows.
2026-04-14 10:58:27 +02:00
Jeppe Bundgaard a4676f8f87 Add e2e and unit tests for i18n coverage and POS workflows:
- Introduced i18n key usage validation with `tests/e2e/i18n.views.spec.ts` for deterministic locale coverage.
- Added `viewI18nKeyScanner` utility to scan and validate view translation keys.
- Created `PosDepartmentStep1` unit tests for duplicate warnings and booking selection flow.
- Enhanced POS mobile popup with `SelectOrderBookingPopupProps` and new header close options.
- Updated e2e tests with scenarios to verify booking selections, duplicate handling, and locale alignment.
- Added new `test:e2e:i18n:views` npm script for targeted i18n test execution.
2026-04-14 10:49:20 +02:00
Jeppe Bundgaard 7533622fb1 Please provide the corresponding diff so I can generate an appropriate commit message. 2026-04-14 10:28:46 +02:00
Jeppe Bundgaard 90518308f8 Add e2e tests for POS desktop card payment flow:
- Added tests for handling misconfigured terminal locations with superuser and operator guidance.
- Verified behavior for zero readers, existing payment intents (`requires_capture` and `succeeded`), and happy path payment flows.
- Introduced `bootDesktopCardPayment` helper for streamlined test setup.
2026-04-14 10:28:29 +02:00
Jeppe Bundgaard e88c2d14d5 Refactor POS booking flow and error handling:
- Improved error state management with `normalizeErrorState` and `clearErrorState` utilities.
- Enhanced Stripe payment flow with clear status messaging and support for terminal setup requirements.
- Updated POS component templates (`PosDepartmentStep3.vue`, `PayWithStripeButton.vue`) for better error messages and UX.
- Added `pos-step-4` navigation and tests in e2e flows.
- Introduced utility `toPositiveInteger` for robust numeric validation in bookings.
2026-04-14 10:27:29 +02:00
Jeppe Bundgaard c14f0cc1c4 Add utilities and tests for mobile order management and Subuser navigation:
- Introduced `mobileOrderCompletion.js` for mobile order metadata handling with safety seal syncing, metadata persistence, and booking linking utilities.
- Added tests for Subuser navigation menu (`navigation-menu-items-user-subusers.spec.js`) to validate links and permission gating.
- Created unit and end-to-end tests for Subuser grant permissions and management, including permission nodes and invites (`subuser-grant-permission-nodes.spec.js`, `subuser-management.spec.ts`).
- Implemented `SubuserCompleteRegistrationPage.vue` with validation, token handling, and registration flow for Subuser setup.
- Enhanced Playwright e2e tests for Subuser features (`subuserCompleteRegistration.spec.ts`).
2026-04-14 10:09:10 +02:00
Jeppe Bundgaard a93b0dc954 Add tests and utilities for dynamic booking preload handling in POS flow:
- Introduced logic and utilities to fetch exact plate bookings beyond initial preload (`ensureVehiclePlateBookingsLoaded`, `mergePendingBookings`).
- Enhanced booking dropdown and chooser to handle dynamically loaded data for desktop POS.
- Updated e2e tests (`pos-flow.spec.js`) with scenarios for exact and partial plate matches in booking preload handling.
- Refactored `network.js` to support paginated and filtered responses for bookings based on registration fields.
- Improved UX and display logic in booking selectors (`LicensePlateReg1Input.vue`, `PosDepartmentProcess.vue`).
2026-04-13 21:53:16 +02:00
Jeppe Bundgaard 6549eb5015 Refactor POS booking flow and dropdown logic:
- Enhanced order booking sorting with improved date handling and validation (`getOrderBookingSortDateValue`, `hasOrderBookingSortDateValue`).
- Updated dropdown interaction in `LicensePlateReg1Input.vue` for better selection behavior and UX.
- Revamped test data and assertions in e2e tests with added coverage for new booking scenarios.
- Improved dropdown styling and responsiveness in `LicensePlateReg1Input.vue`.
2026-04-13 21:42:18 +02:00
Jeppe Bundgaard 0f37b0632a Add POS order booking modal and utilities with tests:
- Introduced `PosDesktopOrderBookingSelectorModal.vue` to streamline booking selection for desktop POS.
- Added `orderBookingDisplay.js` utility for handling booking display and reference value normalization.
- Created unit test `select-vehicle-form-pos.spec.js` to validate booking selection flow and registration matching.
- Implemented `run-playwright-ci-parallel.mjs` script to optimize Playwright CI with parallel testing.
- Updated styles and responsiveness for enhanced booking modal UX.
2026-04-13 21:12:34 +02:00
Jeppe Bundgaard 7d9d928a2d Normalize license plate input value handling in LicensePlateInput.vue:
- Ensured input field values are updated consistently with `props.actualValue`.
- Replaced raw `newValue` usage with normalized string values for better data integrity.
- Improved checks for synchronization and validation logic (`checkLicensePlateValid`, `searchCustomerRegistrationNumbers`).
2026-04-13 18:02:06 +02:00
Jeppe Bundgaard ce049d9196 Enhance POS booking flow logic and tests:
- Added support for trailers (`reg_2`) in booking selection and vehicle matching logic.
- Implemented logic to prioritize bookings from registered vehicles and trailers.
- Enhanced booking dropdown with date labels and validation for booked vehicles.
- Improved e2e tests to cover booking date, trailer registration, and vehicle-search scenarios.
- Refactored Vue components for consistency in booking handling and dropdown interaction.
2026-04-13 17:56:19 +02:00
Jeppe Bundgaard 8f3ec2cf09 Add booking selection popup and refactor POS booking flow logic:
- Introduced `PosDepartmentStepMobilePopupSelectOrderBooking.vue` for mobile booking selection.
- Refactored vehicle and booking handling in `SelectVehicleFormPOS.vue` with better plate matching, booking persistence, and customer association logic.
- Updated `PosVehicle` object type to include booking matches.
- Enhanced POS transaction history and booking views with improved readability, functionality, and `data-testid` attributes.
- Adjusted `PosPopup.vue` components to support the new booking popup.
2026-04-13 17:27:37 +02:00
Jeppe Bundgaard ecfe27571c Refactor booking flow tests and enhance product category filtering logic:
- Improved vehicle type selection handling with better validation and polling in `bookingFlow.ts`.
- Updated product card selectors for enhanced visibility and interaction in booking flow tests.
- Added `matchesProductCategory` utility in `network.js` for flexible and accurate product category filtering.
2026-04-13 15:44:15 +02:00
Jeppe Bundgaard 53d1ae114f Enhance booking flow and add customer validation handling:
- Introduced `ensureVehicleTypeSelection` utility to streamline vehicle type selection in `bookingFlow.ts`.
- Updated booking flow to conditionally handle vehicle type selection for desktop wizards.
- Added customer number validation logic in `network.js` to simulate backend responses for `/guest/validation/customer-number`.
2026-04-13 15:34:17 +02:00
Jeppe Bundgaard 653aa63899 Refactor POS components and implement new features:
- Replaced `POSOrderReference.vue` with `OrderAttachmentsActionButton.vue` and added new features like attachment preview, upload, and management.
- Introduced `POSOrderCustomerWishes.vue` for handling customer wishes fields with autosave behavior.
- Added Playwright global setup/teardown scripts for managing dev server lifecycle during tests.
- Enhanced booking flow test utilities and adjusted visibility rules in `bookingFlow.ts`.
2026-04-13 15:27:34 +02:00
Jeppe Bundgaard 8a25dcd97d Update attachment actions in orders table and e2e tests:
- Modified attachment controls to display "+" button when no attachments exist and ensured proper alignment with settings button.
- Refactored `shouldShowOrderAttachmentsButton` logic for improved clarity and access determination.
- Enhanced Vue component (`ordersTable.vue`) to handle attachment addition and visibility, adding support for dynamic labels and icons.
- Updated e2e tests (`admin-pos-orders.spec.ts`) to verify alignment, visibility, and interaction of attachment controls in desktop order list.
- Added CSS rules to improve positioning and responsiveness of order actions.
2026-04-13 14:45:29 +02:00
Jeppe Bundgaard 3e8a7ee3db Update POS styling and refactor Vue component for consistency:
- **CSS Enhancements:** Added `.pos-registration-control` class, improved styling for registration fields, and enhanced responsiveness for `.pos-order-items--order-detail`.
- **Vue Refactor:** Standardized imports, object keys, and quotes in `ActionSettingsWheelButton.vue` to align with consistent formatting and readability.
2026-04-13 14:28:12 +02:00
Jeppe Bundgaard 295f69ee41 Implement POS registration field and autosave utility:
- Added `PosOrderRegistrationField.vue` for managing inline registration field inputs with autosave behavior.
- Introduced `useOrderMetadataAutosave.js` composable for seamless value normalization, dirty tracking, and delayed persistence.
- Refactored mobile and desktop POS components to integrate `useOrderMetadataAutosave` for improved field management (e.g., `POSOrderReference.vue`, `PosOrderLicensePlates.vue`).
- Updated e2e tests to cover registration field scenarios, including input normalization and persistence (`pos-mobile-order-flow.spec.js`, `admin-pos-orders.spec.ts`).
- Enhanced styling and responsiveness for registration fields in `pos.css`.
2026-04-13 14:20:36 +02:00
Jeppe Bundgaard ade565cbca Introduce completeBookingCreationFlow utility and refactor booking flow tests:
- Added `completeBookingCreationFlow` utility to streamline booking creation logic in `bookingFlow.ts`.
- Refactored `userBookings.spec.ts` to replace inline booking creation steps with the new utility for improved readability and consistency.
- Applied `data-testid` attributes across booking-related components for enhanced testability and test coverage.
- Enhanced Playwright navigation error handling with `isBenignNavigationError` and `settleAuthenticatedNavigation` utilities to improve resilience and test robustness.
2026-04-13 13:39:48 +02:00
Jeppe Bundgaard 828e2fd312 Add test cases for internationalization, orders table rendering, and error handling:
- **Internationalization:** Added end-to-end test `i18n.smoke.spec.ts` to validate Danish locale translations. Ensured normalization and removed unexpected strings in the `da` locale file.
- **Orders Table:** Created unit test `orders-table.spec.js` to verify sparse data handling, invoice collection preloading, and rendering correctness.
- **Error Handling:** Enhanced connectivity issue UI (`ConnectivityIssue.vue`) with `data-testid` attributes for improved testability.
- **E2E Playwright Tests:** Updated and simplified e2e test utilities. Replaced `seedAuthenticatedState` with `primeMockSession` for session preparation. Added new tests for `/user/orders` and `/user/invoices` pages focusing on edge cases and maintaining structure consistency.
2026-04-13 12:44:06 +02:00
Jeppe Bundgaard 2969305dd5 Refactor POS desktop order workspace layout and add phone normalization utilities:
- Improved structure and layout in `PosDesktopOrderWorkspace.vue` with refined column handling and responsiveness.
- Added `resolveNormalizedPhoneNumber` utility in `POSDepartmentProcess.vue` for consistent phone number normalization across customer records.
- Updated e2e tests to validate layout changes and phone handling behavior.
- Upgraded Playwright to version 1.58.2 and adjusted related configurations.
- Ensured consistent formatting and added `postinstall` script for syncing Playwright root links.
- Strengthened POS customer rule tests with edge case validations for phone number objects.
2026-04-13 09:44:50 +02:00
Jeppe Bundgaard b39b458f4f Add .prettierrc.json and refactor test files for improved formatting consistency:
- Introduced `.prettierrc.json` to enforce consistent code formatting across the project.
- Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
2026-04-13 09:13:27 +02:00
Jeppe Bundgaard f040614f46 Refactor module paths, layout responsiveness, and testing configurations:
- Replaced repetitive department module URL logic with `buildDepartmentModulePath` utility in `DepartmentModulesDisplay.vue`.
- Enhanced layouts and responsive behavior across components, including `PosDepartmentStep1MobileTransactionHistory.vue` and `DepartmentDailyReport.vue`.
- Removed unused `SuperuserInvoicingLocalStore.vue` and refactored to `SuperuserInvoicingLocalStore.ts`.
- Updated media query handling and card layout adjustments in `DepartmentDailyReport.vue`.
- Added new Playwright configurations (`playwright.prod.config.ts` and `playwright.live.config.ts`) for e2e release workflows.
- Extended e2e and unit test coverage for mobile POS and department modules.
2026-04-09 16:32:25 +02:00
Jeppe Bundgaard e8bd2f5478 Add employee fixtures, mobile POS customer note popup, and alignment updates:
- Introduced employee fixtures in `mobilePos.js` for testing workflows involving employee data.
- Enhanced `PosDepartmentStep1MobileCustomerNotes.vue` with translated labels and data-test attributes.
- Added e2e tests for customer notes popup and translated label validation.
- Improved alignment and responsiveness for `PaginationDisplayGeneralSearchReload.vue` and related e2e tests.
- Updated button logic and styles in `GenericButton.vue` and other mobile POS components for consistent UI behavior.
2026-04-09 14:47:43 +02:00
Jeppe Bundgaard 64aa94ebc3 Add alignment utilities and enhance mobile POS e2e tests:
- Introduced helper functions (`expectHorizontallyCentered`, `expectSameRow`, `expectAboveFixedActions`) for alignment checks in `pos-mobile-order-flow.spec.js`.
- Enhanced e2e tests with layout validations for titles, subtitles, and form inputs.
- Added `reserveSpace` prop and CSS variable handling to `PosDepartmentStepMobileFixedBottomControl.vue` for better spacing control.
- Improved layouts and responsiveness in mobile POS components like `PosDepartmentStepMobile2.vue` and `ScannerInstructions.vue`.
2026-04-09 14:14:56 +02:00
Jeppe Bundgaard 157c3dbf60 Add enhanced POS desktop workspace with customer rail and order summary:
- Introduced `PosDesktopOrderWorkspace.vue` for improved order and customer management.
- Added `PosOrderItemsDefaultTable.vue` with detailed row actions, price tooltips, and order item grouping.
- Enhanced `LicensePlateReg1Input.vue` to preserve customer selection when switching license plates.
- Improved `PosDepartmentStep3.vue` with updated desktop layout using the new POS workspace.
- Added e2e test for customer rules tab to verify dynamic attribute reloading.
2026-04-09 14:03:46 +02:00
Jeppe Bundgaard 2bf1f5b385 Add enhanced department complaint functionality with category and date support alongside POS improvements:
- Introduced `departmentDailyReportComplaintCategories.js` for managing complaint categories with validation utilities.
- Added category and wash date fields to the complaint editing interface with validation in `DepartmentDailyReportComplaints.vue`.
- Developed `PosOrderDetailItemsTable.vue` for displaying order details with inline actions and price highlighting.
- Implemented `PosOrderItemEditModal.vue` for editable order items with validations for price and quantity.
- Created unit tests for `PosLastScannedLicensePlatesV2.vue` to verify scanning, caching, and error handling behaviors.
2026-04-09 12:55:30 +02:00
Jeppe Bundgaard 78648a0b7f Add customer complaints, edge gateway terminal, and mobile POS functionalities with e2e and unit test coverage:
- Implemented `departmentDailyReportComplaintCustomers.js` module for handling customer complaint data, including search, normalization, and API integration.
- Added `EdgeGatewayTerminal.vue` for root shell access with session approval, dynamic status handling, and user feedback.
- Developed `mobilePos.js` for simulating mobile POS fixtures and workflows in e2e tests.
- Introduced relevant e2e and unit tests to ensure functionality and error handling across modules.
2026-04-09 11:35:18 +02:00
Jeppe Bundgaard 90427c631f Add customer complaints page for superusers with pagination, filtering, and e2e test coverage:
- Implemented `CustomerComplaints.vue` and `CustomerComplaintsPagination.vue` for managing complaints with department filters, search functionality, and sorting.
- Added `CustomerComplaintsTable.vue` for displaying complaint details with edit and delete actions.
- Introduced `DepartmentDailyReportComplaints` module for managing CRUD operations and validation.
- Added e2e tests for complaint workflows, including pagination, filtering, and customer interaction validation (`superuser-customer-complaints.spec.ts`).
- Included validation logic and error handling for phone call gate configurations (`DepartmentGates.vue`).
- Enhanced department gate e2e tests with validation scenarios (`superuser-department-gates.spec.ts`).
2026-04-08 20:41:14 +02:00
Jeppe Bundgaard 29c6c9d135 Add e2e tests for admin module functionalities:
- Introduced tests for admin daily report features, including tile rendering, complaint management, and stale response handling.
- Added department visibility tests to ensure hidden departments are excluded from UI controls.
- Implemented night wash overview tests for breakdown validation, missing hours detection, and stable range updates.
2026-04-08 19:00:24 +02:00
Jeppe Bundgaard 8eb315f439 Add comprehensive unit and e2e tests for invoice collection, queue logic, and modal interactions:
- Introduced e2e test for "Change Invoice Collection" in `change-invoice-collection.spec.ts`.
- Added unit tests for `CollectedOrderInvoiceOverview.vue` including edit flow for `closed_at`.
- Implemented tests for queue reliability, state handling, and customer actions:
  - `CollectedOrderInvoicesQueueHistory.vue`: polling, error handling, richer diagnostics, and retry logic.
  - `InvoicingBillingPeriod` views: refresh and queue state handling.
- Enhanced test coverage for Stripe queue functionality and related actions.
2026-04-08 15:53:52 +02:00
Jeppe Bundgaard 2b6129db29 Add economic queue functionality with Vue component, unit tests, and e2e tests:
- Introduced `CollectedOrderInvoicesQueueHistory.vue` for displaying and managing job queue history.
- Added queue management features: polling with active status detection, error handling, retry logic, and pagination controls.
- Implemented e2e tests for economic queue workflows, including export polling, retries, validation errors, and component unmount behavior.
- Added unit tests for queue logic, payload normalization, and retry scenarios.
- Created `EconomicQueuePlaywrightHarness.vue` to support e2e testing with mock workflows.
2026-04-08 11:19:16 +02:00
Jeppe Bundgaard 1705d0d0ed Add unit tests and enhance logic for department gate capabilities and XLVask usage filtering:
- Introduced `getDepartmentOutsideGateCapabilities` with caching, input normalization, and error handling.
- Added tests for department gate detection, normalization, and fallback scenarios.
- Implemented `XLVaskUsageFilters` to filter and detect attached usage orders.
- Enhanced `XLVaskUsagePagination` with computed visible object counts and filtering functionality.
2026-04-07 14:02:34 +02:00
Jeppe Bundgaard beaf95a5b1 Add outside gate controls to SelfServeMachineRelayControls with comprehensive tests:
- Introduced `openOutsideEntranceGate` and `openOutsideExitGate` actions.
- Added dynamic button rendering based on department gate capabilities.
- Implemented unit tests for gate visibility and action triggers.
2026-04-01 17:12:00 +02:00
Jeppe Bundgaard 19501896e1 Add comprehensive test coverage for department gates and self-serve wash actions:
- Introduced unit tests for `DepartmentGates` config modal and gate commands in `useWashSessionActions`.
- Added E2E tests to verify gate commands and button functionality in self-serve in-progress flow.
- Updated localization files to include gate control labels (`open_property_access_gate`, `open_property_exit_gate`).
- Enhanced `DepartmentGates.vue` with config modal logic, field validation, and dynamic input rendering.
2026-03-26 16:32:34 +01:00
Jeppe Bundgaard ce9a209d73 Add property gate controls to self-serve wash functionality:
- Introduced `openPropertyAccessGate` and `openPropertyExitGate` actions in `useWashSessionActions.js`.
- Added Vue button components for gate control in `MyWashStart.vue`.
- Updated localization files with translations for gate control labels.
2026-03-26 16:00:32 +01:00
Jeppe Bundgaard 0fe2e19211 Extend MachineStatusType and enhance self-serve machine status logic:
- Added `UNKNOWN` and `VALID` statuses to `MachineStatusType` with corresponding icon and style mappings.
- Updated `SelfServeMachineRelayControls` to use `UNKNOWN` and `VALID` statuses for machine and customer displays.
- Introduced `decimalRound` function for precise rounding in `DepartmentWeather.vue` data rendering.
- Applied `decimalRound` to display hours with two decimal precision in department reports.
2026-03-26 15:31:28 +01:00
Jeppe Bundgaard d354ce7737 Enhance self-serve functionality with dynamic images, gate handling, and machine-scoped vehicle type filtering:
- Added dynamic image previews with query parameter handling and error recovery in `SelfServeTryModal.vue`.
- Introduced outside gate controls (`Entrance` and `Exit`) in `SelfServeMachineRelayControls.vue` with corresponding command handling logic.
- Enhanced vehicle type scoping logic in `SelfServeTasksPagination.vue` and `SelfServeConditionsPagination.vue` to ensure machine-type filtering consistency.
- Updated related unit tests to verify new dynamic image logic, gate controls, and scoping behaviors.
2026-03-26 14:13:15 +01:00
Jeppe Bundgaard 3e84421913 Introduce drag-and-drop task reordering in DepartmentSelfServeStudio.vue:
- Added drag-and-drop functionality for task prioritization, with visual indicators and reordering logic.
- Updated UI to support draggable tasks and ensure accessibility for admin users.
- Included unit tests to verify drag-and-drop behavior, task priority updates, and error handling.
2026-03-26 11:40:01 +01:00
Jeppe Bundgaard 7368c476f1 Add task priority, gate type normalization, and vehicle type logic to self-serve functionalities:
- Introduced task order priority, dynamic image thumb position, and improved task gate type handling in the self-serve studio.
- Enhanced task payload normalization with nullable fields and parameter verification.
- Updated UI with order priority controls, dynamic images configuration, and machine type-specific scoping options.
- Added and refined unit tests for task sorting, form controls, and validation rules.
2026-03-26 11:35:24 +01:00
Jeppe Bundgaard 21be6c0efd Add unit tests for legacy and machine-type scoping in DepartmentSelfServeStudio.vue:
- Introduced tests for legacy scope selectors and task submission validation.
- Added guards for vehicle type, lane, and department scoping in questions and tasks.
- Verified cache clearing and consistent data loading for self-serve studio.
- Updated navigation and localization for new self-serve paths.
2026-03-26 09:39:03 +01:00
Jeppe Bundgaard 173a0becdf Add unit tests for DepartmentSelfServeStudio.vue condition and vehicle type scoping:
- Added tests to verify legacy scope selectors for lanes and vehicle types.
- Ensured condition submission is guarded for both machine type and legacy scopes.
- Validated vehicle type scoping with product query filtering and new record defaults.
2026-03-25 19:33:08 +01:00
Jeppe Bundgaard 4d810247c5 Add vehicle type scoping to DepartmentSelfServeStudio.vue:
- Introduced `scopedProductId` for vehicle-specific filtering and configuration adjustments.
- Added `Vehicle Type` dropdown for filtering questions, conditions, and tasks.
- Enhanced validation for department, lane, and vehicle type scoping.
- Updated UI to include vehicle type scoping indicators and selection components.
- Added alignment fixes for action buttons and corresponding unit test coverage.
2026-03-25 19:32:59 +01:00
Jeppe Bundgaard 30d23aeaf8 Add DepartmentSelfServeStudio.vue for advanced self-serve configuration management:
- Introduced a new Vue component supporting versioned configuration authoring for questions, conditions, tasks, and rules.
- Implemented scoped editing with lane and machine type-specific configurations.
- Added version tracking features, including validation, publication, and rollback.
- Enhanced UI with tabbed navigation, modals for adding/editing records, and notifications for actions.
2026-03-25 17:55:17 +01:00
Jeppe Bundgaard 2c22a548e1 Extend self-serve functionality with new machine statuses, config version tracking, and detailed session displays:
- Added `INCLUDED` and `BILLABLE` statuses to `MachineStatusType` for enhanced session tracking.
- Introduced `configVersionId` and `evaluationTrace` support in self-serve logic and UI.
- Enhanced `SelfServeMachineRelayControls` with billable session parsing and UI updates.
- Updated `SelfServeMachineStatus` to reflect new statuses with appropriate icons and labels.
- Improved session summary displays with detailed evaluation trace output.
2026-03-25 16:15:19 +01:00
Jeppe Bundgaard 5b181087ff Upgrade dependencies in package-lock.json:
- Updated `@vitejs/plugin-vue`, `@vitejs/plugin-vue-jsx`, `@asamuzakjp/dom-selector`, `@azure/msal-browser`, and various `@babel` packages.
- Introduced new sub-dependencies and licenses.
- Removed legacy nested dependencies.
2026-03-25 15:23:17 +01:00
Jeppe Bundgaard c184054380 Merge remote-tracking branch 'origin/master' 2026-03-25 15:12:05 +01:00
Jeppe Bundgaard 5b440a6948 Add tooltips to relay controls for improved self-serve machine guidance:
- Enhanced `SelfServeMachineRelayControls` with detailed `b-tooltip` descriptions for Machine, Program Picker, and Cleaner relays.
- Added `b-tooltip` to `SelfServeMachine` for the "Allow Self-Serve" toggle, explaining its impact on machine accessibility.
- Updated imports to include `BTooltip` from Buefy.
2026-03-25 15:11:34 +01:00
Jeppe Bundgaard 844d57b1a5 Add unit tests and logic for department self-serve enablement:
- Implemented department self-serve status management in `SelfServeMachine` with fetching, updating, and error handling.
- Added unit tests for status retrieval, toggle behavior, and error scenarios.
- Enhanced UI with `b-switch` for self-serve enablement control.
2026-03-25 14:50:07 +01:00
Jeppe Bundgaard caec066c6c Add department self-serve management with API integration and tests:
- Introduced `DepartmentModulesDisplay` and `DepartmentModulesSetup` components for self-serve enablement management.
- Implemented `getDepartmentSelfServeEnabled` and `setDepartmentSelfServeEnabled` helpers.
- Added unit tests for self-serve status fetching, updating, and UI synchronization.
- Enhanced connectivity and relay logic with in-progress wash session handling and detailed displays.
2026-03-25 14:06:22 +01:00
gold-dev 2877979660 Feat: Charts dashboard 2026-03-25 15:00:57 +02:00
Jeppe Bundgaard d03b7d86bf Add shared gate control logic and in-progress wash API integration:
- Implement shared gate relay handling in `SelfServeMachineRelayControls` with unified button for entrance/exit.
- Add unit tests for gate control variations (shared vs. separate relays).
- Extend OpenAPI with endpoints for in-progress wash session details and gate operations.
- Update `MachineType` with relay configurations for comprehensive gate handling.
2026-03-25 13:39:38 +01:00
Jeppe Bundgaard 2c0f012446 Add gate control functionality to self-serve connectivity:
- Implemented `openEntranceGate` and `openExitGate` actions in connectivity relay.
- Added gate normalization logic and error handling for invalid gate types.
- Updated components and unit tests to support entrance/exit gate opening.
2026-03-25 13:25:20 +01:00
Jeppe Bundgaard 620c4f0859 Add comprehensive unit tests for connectivity relays and machine store:
- Implemented tests for self-serve connectivity relay API wrappers, covering status retrieval, set operations, and lane commands.
- Added tests for the machine connectivity store, including relay toggling, polling, force operations, and error handling.
- Refactored relay functionality to improve maintainability and testability, introducing helper functions for lane and relay commands.
- Updated machine and connectivity-related components to align with new relay definitions and shared store logic.
2026-03-25 13:20:51 +01:00
Jeppe Bundgaard de28f2cd49 Add MachineRelayType and refactor relay handling for self-serve dashboard:
- Added `MachineRelayType` for defining relay types and statuses.
- Refactored relay logic into `SelfServeConnectivityRelay` for cleaner separation of concerns.
- Updated `MachineType` to include relay configurations for machine, program picker, and cleaner.
- Replaced inline relay handling in `SelfServeMachineConnectivity` with the reusable `SelfServeConnectivityRelay` module.
2026-03-25 12:38:34 +01:00
Jeppe Bundgaard 81bd4c8583 Add interval-based status polling and toggle functionality to SelfServeMachineRelayControls:
- Refactored relay status retrieval with a polling mechanism using `setInterval`.
- Added relay toggle controls (`b-switch`) for machine, program picker, and cleaner relays.
- Improved error handling for relay status fetch and toggle operations.
- Updated lifecycle hooks to clean up polling on component unmount.
2026-03-25 12:24:10 +01:00
Jeppe Bundgaard 97bcf73db8 Add relay machine controls and program picker integration:
- Added `relay_machine_program_picker_id` and `relay_machine_cleaner_id` fields across components, views, and localization files.
- Updated relay control logic to include program picker and cleaner relays.
- Extended e2e and unit tests to validate new relay functionalities.
- Refactored related components to handle expanded relay configurations.
2026-03-25 11:42:02 +01:00
Jeppe Bundgaard c6531076ae Add Department Self-Serve dashboard with machine relay controls and API integration:
- Implemented `DepartmentSelfServe` dashboard for managing machine statuses and relays.
- Added components for machine display, controls, and relay connectivity (`SelfServeMachine`, `SelfServeMachineControls`, `SelfServeMachineRelayControls`).
- Integrated machine relay API endpoints (`/modules/self-serve/lane/relay/machine/*`) for status retrieval and updates.
- Updated router configuration with a new route for the self-serve dashboard.
- Extended OpenAPI definition to include machine relay APIs and enhance department weather targets documentation.
2026-03-25 11:24:20 +01:00
Jeppe Bundgaard 1b97332c38 Add unit tests for shared date range synchronization and refactor DepartmentDashboardOverviewNavigation components:
- Added behavior tests covering shared date range updates across multiple components, including `DepartmentDailyReportThisWeek` and `DepartmentDailyReportSmall`.
- Updated `DepartmentDashboardOverviewNavigation` to route date updates via the shared daily report store.
- Refactored components to use shared `DepartmentDailyReportObject` for date state management.
- Adjusted e2e tests to validate date range synchronization and department goal progress tracking.
2026-03-24 16:53:16 +01:00
Jeppe Bundgaard 477688d16e Add cadence target calculation for timeframe-based goal progress resolution:
- Implemented `computeCadenceTargetForTimeframe` utility to calculate dynamic targets based on timeframe and cadence criteria.
- Extended tests to validate timeframe-specific progress key selection, slice resolution, and cadence-based targets.
- Refactored `DepartmentGoals` to integrate cadence-aware target resolution and fallback logic.
- Enhanced e2e tests to cover bi-weekly cadence goals and dynamic target updates across timeframes.
2026-03-24 16:32:35 +01:00
Jeppe Bundgaard bc043b95f9 Refactor DepartmentWeather status computation with work-hour-based thresholds: add health categories and fallback for zero hours. 2026-03-24 15:07:55 +01:00
Jeppe Bundgaard 1e6590e2df Add goal progress resolver utility with extensive tests and DepartmentGoals refactor.
- Implemented `goalProgressResolver` utility for resolving progress keys, slices, and periods.
- Refactored `DepartmentGoals` to integrate `goalProgressResolver` for improved clarity and modularity.
- Updated progress calculations with cadence-aware key handling and fallback resolution.
- Add isLoading handling and loader animations to `DepartmentWeather` and improve `GET` request concurrency.
2026-03-24 14:58:48 +01:00
Jeppe Bundgaard 8b8780bbaf Refactor DepartmentWeather for key-based rendering and summary toggle improvements:
- Added `getEntryKey` function to generate unique keys for dynamic entries.
- Enhanced `isMultiDaySelection` watcher with immediate execution option.
- Updated DOM structure for action buttons with inline export button support.
- Adjusted unit tests to cover functional changes and new test case for summary mode toggling.
2026-03-24 13:59:38 +01:00
Jeppe Bundgaard 5a0d1b7712 Add daily summary toggle to DepartmentWeather with tests and refactor:
- Implemented a daily summary toggle for multi-day selections in `DepartmentWeather`.
- Refactored state computation to support hourly and daily views with weather/status aggregation.
- Added unit tests for summary toggle functionality and entry summarization logic.
2026-03-24 13:39:15 +01:00
Jeppe Bundgaard 3cc0a5bb15 Add tests for DepartmentWeather and update daily report wiring:
- Added unit tests for `DepartmentWeather` component to validate weather API calls, state handling, and edge cases (e.g., no departments, stale responses, hidden hours).
- Updated `DepartmentDailyReport` wiring to pass `department_ids`, `date_from`, and `date_to` to `DepartmentWeather`.
- Enhanced OpenAPI definition for weather timeline API with refined query parameters (`date_from`, `date_to`) and updated descriptions.
- Improved `GoalFormModal` layout, adding `department-grid--single-scroll` for better scrolling behavior and responsiveness.
2026-03-24 13:33:17 +01:00
Jeppe Bundgaard ff944ad1f2 Refactor goal form modal, update internationalization strings, and enhance API parameters:
- Restructured goal form modal with new tabbed layout for better usability.
- Updated and corrected i18n strings across multiple languages.
- Enhanced API descriptions for department weather timeline, added support for multiple department IDs, and refined query parameter logic.
2026-03-24 13:07:26 +01:00
Jeppe Bundgaard 1ee2da7ad8 Add advanced goal criteria handling with input normalization, duration modes, and improved validation:
- Introduced target duration modes (`weeks`, `months`, `years`, `legacy`) with optional cadence.
- Implemented input normalization for department daily targets and progress alerts.
- Added validations for duration cadence and date ranges.
- Updated internationalization strings for new form fields and validation messages.
- Extended tests for goal criteria serialization, normalization, and API payload generation.
2026-03-24 12:48:54 +01:00
Jeppe Bundgaard f372e59a07 Enhance Workfeed diagnostics and localization:
- Updated `listShifts` diagnostic modal to include new filters (date range, employee, release state) with improved UI/UX.
- Simplified employee and department lookup diagnostics by removing unnecessary search inputs.
- Standardized error handling and validation messages for shift diagnostics.
- Extended localization files across all supported languages with new diagnostic strings.
- Adjusted Workfeed module logic to reflect updated query parameters (`startFrom`, `startTo`, `employeeID`, `released`).
- Updated and added unit and e2e tests to validate new diagnostic flows and parameter constraints.
2026-03-24 11:52:15 +01:00
Jeppe Bundgaard eeef782705 Simplify Workfeed schemas and endpoint descriptions:
- Removed unnecessary query parameters and pagination fields from `employees`, `shifts`, and `departments` endpoints.
- Standardized `description` fields with explicit API request details.
- Consolidated schema components (e.g., `WorkfeedPagination`) and enhanced examples for clarity.
- Updated API URL from `https://api.workfeed.io` to the new production endpoint.
- Improved shift schemas with granular fields for approvals, breaks, and comments.
2026-03-24 11:37:16 +01:00
Jeppe Bundgaard 44031da7b1 Add support for CompanyID in Workfeed configuration and update localization files:
- Extend i18n locales to include `company_id` and `company_id_desc` strings for API synchronization.
- Add `CompanyID` field to Workfeed configuration UI in `ConfigurationWorkfeed.vue`.
- Update unit and e2e tests to validate `CompanyID` functionality.
- Adjust Workfeed module logic to handle `CompanyID` get/set operations.
2026-03-24 11:22:36 +01:00
Jeppe Bundgaard 943bc8ec97 Add Workfeed module with configuration management, diagnostics, and API integration:
- Implement `Workfeed` module with endpoints for employees, shifts, departments, and diagnostics.
- Add configuration UI for enabling Workfeed, API settings (`api_url`, `api_key`), and integration diagnostics.
- Add unit tests for module schema validation, API endpoints, and diagnostics contract.
- Introduce smoke test for Workfeed configuration and modal diagnostics.
- Extend i18n locales to support Workfeed integration.
2026-03-24 11:19:09 +01:00
Jeppe Bundgaard 0949d861f9 Add detailed examples and extend schema definitions for Workfeed configurations:
- Introduced examples for `WorkfeedConfigResponse` to demonstrate configurations like `enabled`, `api_url`, `api_key`, and `CompanyID`.
- Split `WorkfeedConfigEntry` into specific schema components with updated discriminator logic for easier validation and understanding.
- Enhanced OpenAPI documentation for `Workfeed` module configuration endpoints.
2026-03-24 10:56:45 +01:00
Jeppe Bundgaard 2903cdf57d Add Workfeed module endpoints and schemas to OpenAPI:
- Introduced `Workfeed` API endpoints for employees, shifts, and departments with optional filtering and pagination.
- Added configuration endpoints for Workfeed including `getWorkfeedConfig` and `updateWorkfeedConfig`.
- Defined schema components for `WorkfeedEmployee`, `WorkfeedShift`, `WorkfeedDepartment`, and related list/pagination responses.
- Updated tags to include new `Security` and `Modules` categories.
2026-03-24 10:37:05 +01:00
Jeppe Bundgaard 66682040e7 Merge remote-tracking branch 'origin/master' 2026-03-20 13:18:39 +01:00
Jeppe Bundgaard 2685888e24 Add unit tests for ActionSettingsWheelButton and refactor user resolution logic:
- Add unit tests covering `ActionSettingsWheelButton` behavior for resolving user ID and customer lookups.
- Refactor `ActionSettingsWheelButton` to use `watch` for reactive user ID and customer number changes.
- Replace `getUserId` with `resolveUserId` for improved error handling and cancellation of outdated requests.
- Simplify `hasUser` logic using a computed property and update dependent logic accordingly.
2026-03-20 13:16:33 +01:00
Jeppe Bundgaard 43cf2b302d Add tests for EditableTableColumn and fix tooltip multiline logic:
- Add unit tests for `EditableTableColumn` covering tooltip multiline behavior with `hoverHtml`.
- Update tooltip logic to use `isTooltipMultiline` computed property for slot checks.
- Adjust `OrderBookingsTable` to fix incorrect usage of `permissionCheckFunction` arguments.
- Optimize `RequestQueueProgress` state handling by removing unused visibility logic.
- Add unit test to ensure no recursive update errors occur during large request processing.
2026-03-20 13:10:35 +01:00
gold-dev add9839916 Feat: 202603 - truckwash.io dashboard V3 - slides 2-5 2026-03-20 11:23:01 +02:00
Jeppe Bundgaard f9bde54956 Add component-level permission tracking and visibility handling:
- Introduce `RequiresPermission` component for permission-based UI controls.
- Add missing permission reporting logic with throttling to `requestQueueState`.
- Update `DepartmentGoals.vue` to wrap actions with `RequiresPermission`.
- Extend unit and e2e tests to validate permission handling scenarios.
2026-03-19 18:29:00 +01:00
Jeppe Bundgaard 6ea51a70d8 Add system-search:close event handling and triple-Shift dispatch for superuser:
- Handle `system-search:close` events in `NavigationMenuGlobalSearch` to close modal.
- Implement `closeSystemSearchForSuperuser` dispatch from `RequestQueueProgress` on triple-Shift.
- Update booking API logic in `NavigationMenuItemsAdmin` for refined date filtering.
- Extend unit tests to validate `system-search:close` behavior and triple-Shift handling.
2026-03-19 16:36:58 +01:00
Jeppe Bundgaard 9cccf49a4d Enhance network tracking and insights in RequestQueue and RequestQueueProgress:
- Add network totals tracking (requests, responses, and bandwidth) in `requestQueueState`.
- Introduce byte estimation functions for requests and responses.
- Update `RequestQueueProgress` layout to display network metrics and runtime insights.
- Add dedicated bottom panel for recent request latency insights (e.g., Ping, Bookings).
- Extend unit tests to verify runtime metrics and insight behavior.
2026-03-19 16:23:53 +01:00
Jeppe Bundgaard af7d894f5c Update RequestQueueProgress layout: adjust alignment and add flexbox styling for improved responsiveness 2026-03-19 15:37:08 +01:00
Jeppe Bundgaard 3ac49b4c65 Add Escape key support to close RequestQueueProgress and corresponding unit test:
- Update keyboard handler to close and reset state when Escape is pressed.
- Add unit test to validate Escape key behavior.
2026-03-19 15:33:45 +01:00
Jeppe Bundgaard cb81058611 Replace double-Alt shortcut with triple-Shift for expanding and displaying RequestQueueProgress:
- Update shortcut detection logic to use triple-Shift within a configurable time window.
- Rename variables and constants for clarity (`lastShiftKeyPressedAtMs`, `SHIFT_MULTI_PRESS_WINDOW_MS`, etc.).
- Adjust unit tests to validate new triple-Shift behavior and visibility changes.
2026-03-19 15:27:54 +01:00
Jeppe Bundgaard 23f5fdfcbf Add double-Alt shortcut to expand and display RequestQueueProgress:
- Update visibility logic with a double-Alt keypress detection.
- Introduce `isShortcutActivated` and `lastAltKeyPressedAtMs` for shortcut tracking.
- Adjust template rendering to depend on the shortcut activation state.
- Add unit tests to validate double-Alt behavior and its impact on visibility and expansion.
2026-03-19 15:25:30 +01:00
Jeppe Bundgaard 159da161d7 Add runtime metadata and session user details to RequestQueueProgress:
- Display runtime information (API URL, environment, commit hash, version, build time).
- Introduce session user details with support for subusers, including grants and customer numbers.
- Update layout with new dedicated side panels for runtime and user information.
- Extend tests to validate new sections and session user behavior.
2026-03-19 15:22:43 +01:00
Jeppe Bundgaard 076bddab90 Enhance request tracking and error management in RequestQueue:
- Add missing permissions tracking with configurable limits.
- Introduce `clearMissingPermissions` and `pushMissingPermissions` functions.
- Update `RequestQueueProgress` layout to display missing permissions.
- Improve error handling with permission extraction from response payloads.
- Update tests for new functionality and expanded edge cases.
2026-03-19 15:18:31 +01:00
Jeppe Bundgaard 2011887a4b Redesign and improve RequestQueueProgress layout and functionality:
- Introduce a new expanded layout using a responsive grid structure.
- Add separate side panels to display errors and missing permissions.
- Reorganize and enhance error handling with detailed payload previews.
- Adjust styling for better alignment and scalability.
- Update unit tests to validate new structure and functionality.
2026-03-19 15:02:04 +01:00
Jeppe Bundgaard b4c5c8a62f Update RequestQueueProgress to remain visible when errors occur and add corresponding tests 2026-03-19 14:39:35 +01:00
Jeppe Bundgaard ae75a954a4 Expand request queue functionality with enhanced tracking and error management:
- Add request tracking with active, recent, and error request histories, along with configurable retention limits.
- Introduce payload sanitization tools, including request/response redaction, truncation, and safe text handling.
- Update `RequestQueueProgress` to display detailed request stats (ping latency, active requests, errors with payload previews).
- Enhance `installAxiosRequestQueue` to standardize URLs and include request metadata.
- Add tests for updated queue behavior, detailed request tracking, and error storage limits.
2026-03-19 14:30:57 +01:00
Jeppe Bundgaard 861e263dfa Add configurable retry logic to request queue:
- Introduce retry settings for specific HTTP status codes with exponential backoff and jitter.
- Refactor `requestQueue` to support method-based concurrency limits and retry configuration.
- Update `RequestQueueProgress` to respect minimum batch size for progress visibility.
- Add unit tests for retry logic, progress visibility, and updated queue behavior.
- Centralize request queue configuration in `REQUEST_QUEUE_CONFIG`.
2026-03-19 14:15:12 +01:00
Jeppe Bundgaard 73b838cf28 Add support for separate max concurrency limits for GET and non-GET requests:
- Update `requestQueue` to differentiate between GET and other request types with configurable limits.
- Refactor `enqueueRequest` and queue draining logic for type-based concurrent handling.
- Add unit tests to validate GET-specific concurrency and proper queue functionality.
2026-03-19 14:07:59 +01:00
Jeppe Bundgaard 3f1502184a Add axios request queue interceptor with unit tests:
- Introduce `installAxiosRequestQueue` to manage and queue requests globally.
- Update `main.js` to install the interceptor at app initialization.
- Add `__skipRequestQueue` configuration in `authenticatedRequest` for bypassing the queue.
- Create unit tests to validate queue behavior and ensure request sequencing.
2026-03-19 14:01:25 +01:00
Jeppe Bundgaard a73c28795b Add request queue system with progress tracking:
- Introduce `enqueueRequest` function and reactive state for managing request queues.
- Add `RequestQueueProgress` Vue component to display request queue states (active, queued, completed).
- Update `authenticatedRequest` to utilize the new request queue system for sequential request handling.
- Add unit tests for `RequestQueueProgress` and enhanced `authenticatedRequest` behavior.
- Remove debug `console.log` statements from affected components.
2026-03-19 13:52:03 +01:00
Jeppe Bundgaard 17036a0ae7 Refactor navigation route handling for admin menu:
- Replace `router` reference with reactive `routeContext` to remove router dependency.
- Add `syncAdminNavigationRoute` for syncing route data.
- Update `NavigationMenuItems.vue` to utilize the new syncing approach.
- Adjust unit tests to validate route context handling changes.
2026-03-19 13:25:42 +01:00
Jeppe Bundgaard 1493261ef7 Refactor department ID handling across components:
- Update `NavigationMenuItemsAdmin` and `NavigationMenuDepartment` to improve route-based department ID resolution.
- Enhance `authenticatedRequest` to suppress specific errors with custom error responses.
2026-03-19 13:23:05 +01:00
Jeppe Bundgaard cef187956e Add unit tests for authenticatedRequest error handling and NavigationMenuDepartment functionality:
- `authenticatedRequest`: Test suppression of specific errors, rejection of non-suppressed errors, and general error handling.
- `NavigationMenuDepartment`: Test dynamic department label updates based on route changes.
- Add contract tests for `NavigationMenuItemsAdmin` reactive route context usage.
2026-03-19 13:22:55 +01:00
Jeppe Bundgaard 9745b106e6 Update translations for booking statuses in locales (sv, de, no) and remove redundant console.log statements in dashboard components. 2026-03-19 13:13:24 +01:00
Jeppe Bundgaard 27bd9df995 Remove debug console.log and console.warn statements, and update translations for booking statuses in locales (en, da). 2026-03-19 13:06:06 +01:00
Jeppe Bundgaard 70440e8855 Optimize attachment preview handling in POS:
- Add `createEmbeddablePreviewUrl` utility to generate blob URLs for attachment previews.
- Introduce `releaseObjectUrl` and `onBeforeUnmount` cleanup to manage and revoke blob URLs.
- Enhance error handling and URL parsing logic for preview link generation in `POSDepartmentProcess`.
2026-03-19 12:50:00 +01:00
Jeppe Bundgaard 60742bf947 Refactor and migrate Playwright E2E tests:
- Remove `playwright.config.js` to clean up configuration.
- Add new E2E test suites (`admin-pos-orders`, `admin-module-goals`, `admin-module-pos-mobile-order-flow`) to structure Admin module tests.
- Introduce reusable authentication and data seeding utilities in `fixtures` for streamlined test flows and maintainability.
2026-03-19 12:46:47 +01:00
Jeppe Bundgaard acbeef438b Add "add_to_cart" translations for multiple locales (sv, de, no, en, da) in POS strings. 2026-03-19 12:17:05 +01:00
Jeppe Bundgaard 5b411de9e6 Refactor SelectProductsFormPOS.vue to use optional chaining for route parameter access and improve department ID fallback logic. 2026-03-19 12:12:20 +01:00
Jeppe Bundgaard 79e6ec61c2 Remove debug console.log and console.warn statements across multiple components to clean up logging and improve production readiness. 2026-03-19 12:09:52 +01:00
Jeppe Bundgaard 87b8814bcf Remove redundant console.log and console.warn statements across components to improve production readiness and clean up logging. 2026-03-19 12:00:20 +01:00
Jeppe Bundgaard 2e6b420913 Remove debug console.log and console.warn statements across POS-related components to improve production readiness and streamline logging. 2026-03-19 11:51:20 +01:00
Jeppe Bundgaard 23a72b08cf Add restoreStoredPosOrderId functionality with validations, update dependent POS logic, and introduce unit tests for order restoration scenarios. 2026-03-19 11:43:17 +01:00
Jeppe Bundgaard 8db78ec937 Clean up service workers and caches in development mode:
- Add `unregisterDevServiceWorkers` utility to remove service worker registrations and stale caches during development.
- Disable service worker registration injection in `vite.config.js` for non-production environments.
2026-03-19 11:30:35 +01:00
Jeppe Bundgaard 835438049c Expand allowed origins in configuration to support local environments 2026-03-19 11:25:36 +01:00
Jeppe Bundgaard 6a8e19b3e2 Refactor and optimize component logic and tests:
- Consolidate route search handling in `PosDepartmentMVP.vue` using `applyPosRouteSearch` for improved maintainability.
- Refactor tab visibility logic in `ElementTabsBox.vue` with reusable `isHidden` utility, removing redundant code.
- Enhance `SessionUser.vue` and `superUserObject.vue` modules with lazy bindings for improved initialization and performance.
- Update `NavigationMenuItemsAdmin.vue` to validate department IDs with `hasValidDepartmentId` and watchers for reactive booking count updates.
- Improve error handling logic in `HandleGlobalError.vue` and `loadOrderItems` for robustness against invalid inputs.
- Add and update regression tests for session and superuser initialization to ensure compliance with binding contracts.
- Refactor POS step components to streamline order-related logic and support immediate reactivity.
2026-03-19 11:25:17 +01:00
Jeppe Bundgaard a02ee3a700 Add unit tests for POS department processes, route states, and session token handling:
- Add tests for `POSDepartmentProcess.loadOrderItems` to validate order item loading, skipping invalid IDs, and handling malformed responses.
- Add tests for `applyPosRouteSearch` and `parsePosRouteSearch` to ensure correct state parsing and application from route search params.
- Introduce contract tests for session token and superuser initialization to verify decoupling and lazy binding logic.
2026-03-19 11:19:00 +01:00
Jeppe Bundgaard 95a371375f Introduce origin migration handling with outdated route guards, redirect logic, and migration-related error views.
- Add `originMigration.js` for handling allowed origins, standalone contexts, and redirect routes.
- Create `OutdatedGateway.vue` and `OutdatedInstallation.vue` error views with timer-based redirection and user instructions.
- Update router to include `/outdated-installation` and `/outdated-gateway` paths.
- Implement unit tests for migration utility methods and route guards.
- Remove debug `console.log` statements across multiple components.
2026-03-19 10:51:45 +01:00
Jeppe Bundgaard efed5ac557 Introduce origin migration handling with outdated route guards, redirect logic, and migration-related error views.
- Add `originMigration.js` for handling allowed origins, standalone contexts, and redirect routes.
- Create `OutdatedGateway.vue` and `OutdatedInstallation.vue` error views with timer-based redirection and user instructions.
- Update router to include `/outdated-installation` and `/outdated-gateway` paths.
- Implement unit tests for migration utility methods and route guards.
- Remove debug `console.log` statements across multiple components.
2026-03-19 10:44:10 +01:00
Jeppe Bundgaard 7e9489573a Add E2E tests for POS and Superuser modules; refactor POS step components for consistent data-test attributes. 2026-03-18 23:11:59 +01:00
Jeppe Bundgaard 8f7e8b565b Refactor and enhance order item functionality:
- Add unit tests for `createOrderItem` to validate behavior under various conditions (e.g., missing token, invalid order ID).
- Introduce `getAttachmentContent` utility in `PosDepartmentStepMobileAttachment.vue` for streamlined attachment property handling.
- Refactor vehicle wash configuration logic in `VehicleDisplay.vue` with null-safe checks.
- Improve invoice-related logic in `ordersSyncTable.vue` using utility functions for consistent property access.
- Enhance computed properties for customer and product handling in order displays (e.g., `MyOrder.vue`, `ProductBox.vue`).
- Consolidate department lookup logic in `departmentTabs.vue` with safer handling of missing data.
- Add utility functions in `orderItemsTable.vue` for consistent product property access and computation.
2026-03-18 22:25:28 +01:00
Jeppe Bundgaard bd0f212662 Refactor lane selection and restoration logic in MyWashStart.vue:
- Introduce `normalizeLaneId` utility for consistent lane ID parsing.
- Enhance `getEffectiveLaneId` and `ensureEffectiveLaneSelection` to dynamically handle stale lanes and effective selection.
- Modify `fetchSelfServeData` to validate and normalize license plate inputs.
- Add `isRestoring` handling throughout workflow to improve transitions and prevent unnecessary data fetches.
- Update unit tests to cover restoration scenarios and stale lane fallback behavior.
2026-03-18 16:06:38 +01:00
Jeppe Bundgaard 8917893f43 Enhance Self-Serve progress display in MyWashStart.vue and SelfServeDepartmentHeader.vue:
- Add `progressRegistration` and `progressVehicleProduct` props for improved progress details.
- Update `SelfServeDepartmentHeader.vue` template and styles for better layout and item wrapping.
- Refactor computed properties in `MyWashStart.vue` to include `selectedVehicleTypeProductName`.
2026-03-18 15:06:12 +01:00
Jeppe Bundgaard d3ed03a4a1 Refactor Self-Serve progress and department header components:
- Consolidate progress display logic into `SelfServeDepartmentHeader.vue` for better reusability.
- Remove redundant progress headers and styles in `MyWashStart.vue` and `SelfServeTasksStep.vue`.
- Update scoped styles in `SelfServeTasksStep.vue` and `SelfServeDepartmentHeader.vue` to improve layout and readability.
- Fix API URL in `vite.config.js` for version update endpoint.
2026-03-18 14:35:42 +01:00
Jeppe Bundgaard e785275c24 Refactor Self-Serve task handling and enhance UI components:
- Update `SelfServeTaskList.vue` to handle image and non-image task attachments separately and improve task card styling.
- Add utility methods for filtering task services, attachments, and images.
- Modify `MyWashStart.vue` and `SelfServeTasksStep.vue` headers for better readability, styling, and alignment.
- Improve layout responsiveness with scoped styles for task cards and progress headers.
2026-03-18 13:56:39 +01:00
Jeppe Bundgaard 65b173094f Refactor task description handling in SelfServeTaskList.vue:
- Add `hasTaskDescription` utility to validate and handle task descriptions more robustly.
- Update template to conditionally render task descriptions using the new utility.
2026-03-18 13:49:28 +01:00
Jeppe Bundgaard 9a62417981 Refactor Self-Serve logic and POS order creation handling:
- Enhance `useSelfServeLogic.js` with `setSummaryVisibleQuestions` for streamlined question visibility and ordering updates.
- Update logic to retain preview questions when summary data is unavailable.
- Add unit test to validate fallback behavior and preview question handling.
- Prevent duplicate POS orders by introducing `isCreatingOrder` flag in `POSDepartmentProcess.vue`.
- Adjust `PosDepartmentStepMobileButtonNextStep.vue` to reflect button loading state during order creation.
2026-03-18 13:42:15 +01:00
Jeppe Bundgaard ed1a23bac6 Improve lane selection logic and guided instructions in Self-Serve module:
- Refactor `MyWashStart.vue` to ensure effective lane selection is applied dynamically when transitioning steps.
- Enhance `SelfServeGuidedInstructions.vue` by normalizing step management, simplifying the layout, and improving computed property usage.
- Update `useSelfServeLogic.js` to handle missing questions via fallback to lane/registration summary.
- Add and update unit tests for lane selection, step transitions, guided instructions rendering, and fallback logic.
2026-03-18 13:29:56 +01:00
Jeppe Bundgaard dc130229b5 Refactor ViewportResponsiveWrapper to use defineComponent and simplify slot rendering logic. 2026-03-18 13:10:49 +01:00
Jeppe Bundgaard e53b751f81 Refactor Self-Serve logic and improve question visibility handling:
- Refactor `useSelfServeLogic.js` to ensure summary-driven question and condition synchronization.
- Update tests to validate correct question visibility, ordering, and condition evaluation.
- Replace hardcoded logic with dynamic IDs for conditional handling.
- Introduce `summaryVisibleQuestionIds` and `summaryQuestionOrder` to manage question filtering and sorting.
- Enhance workflow for task visibility and activation based on updated rules.
- Migrate dropdown and action handling to `PaginationDisplayGeneralSearchReload.vue` for better readability and feature expansion.
2026-03-18 13:05:06 +01:00
Jeppe Bundgaard f619487f6f Implement automatic table Excel exports with unit tests:
- Add `AutoTableExportService` for dynamic Excel export button generation and visible row extraction.
- Integrate auto-export initialization in `main.js` for legacy non-paginated tables.
- Extend `PaginationDisplay.vue` with support for paginated Excel export.
- Add export functionality to vehicle views and dependencies, including full-table export handling.
- Add tests for `TableExcelExportService.js`, export workflows, and normalization logic.
- Enhance localization with `download_excel` translation key.
2026-03-18 12:46:40 +01:00
Jeppe Bundgaard 25f9d7c071 Add unit tests and services for Self-Serve Vehicle Step and Excel export:
- Introduce `TableExcelExportService` with functions for workbook creation and export (`normalizeExcelValue`, `createWorkbookFromRows`, `downloadWorkbook`).
- Add unit tests for `SelfServeVehicleStep` to test vehicle selection persistence, registration normalization, and emission behavior.
- Extend logic and tests for Self-Serve vehicle management with error handling, dynamic state updates, and step transitions (`MyWashStart.vue`).
- Integrate Excel export in `PaginatedList.vue` with user-defined transformations and error resilience.
2026-03-18 12:05:40 +01:00
Jeppe Bundgaard ad33cfedf8 Merge remote-tracking branch 'origin/master' 2026-03-18 11:06:11 +01:00
Jeppe Bundgaard 36dfed1c3c Improve department display and loading states across components:
- Add computed properties for loading data messages and department visibility in `MyWashStart.vue` to enhance user feedback and state management.
- Introduce department list feature to `GuestQRNewCustomer.vue` and `GuestQRNewDriver.vue`, including support for addresses, lanes, and self-serve availability.
- Refactor shared department-related methods (e.g., navigation links, lane descriptions) for reusability.
- Update UI styles for better consistency and user experience in department and registration views.
- Add unit test for self-serve disabled warning in `MyWashStart.vue` to ensure accurate feedback rendering.
2026-03-18 10:53:15 +01:00
Jeppe Bundgaard b6f6cfd0aa Modularize registration forms and refactor MyWashStart component:
- Extract customer and driver registration logic into reusable `CustomerRegistrationForm` and `DriverRegistrationForm` components for improved modularity and reuse.
- Replace inline forms with new components in `CustomerCreationPage`, `GuestQRNewCustomer`, and `GuestQRNewDriver`.
- Update `MyWashStart.vue` to include developer access conditions alongside superuser checks.
- Improve UI consistency and styling across registration views.
2026-03-18 09:39:34 +01:00
Jeppe Bundgaard abc033bb05 Remove self-serve-logic.spec.js and related integration tests.
- Delete outdated tests for `useSelfServeLogic` composable, step navigation, and validation related to the Self-Serve module.
- These tests have been deprecated in favor of newer, streamlined testing strategies and refactored logic.
2026-03-17 16:49:36 +01:00
gold-dev 6223fc8af4 Feat: 202603 - truckwash.io dashboard V3 - slides 2-5 2026-03-17 13:44:14 +02:00
gold-dev 356bf7147e Feat: 202603 - truckwash.io dashboard V3 - slide 6 2026-03-17 12:37:13 +02:00
Jeppe Bundgaard c68215a451 Add unit tests for Self-Serve alignment features and refactor logic for task normalization, configuration management, and lane services:
- Introduce comprehensive unit tests for machine types, lane wiring, config management, rule conditions, and runtime data integration in the Self-Serve module.
- Refactor configuration handling in `ConfigurationSelfServe.vue` to use normalized mappings and improve payload parsing.
- Enhance `useSelfServeLogic.js` with functions for task preview, summary synchronization, attachment handling, and service updates.
- Optimize wash start flow in `MyWashStart.vue` with streamlined condition saving and endpoint integrations.
2026-03-16 14:01:35 +01:00
Jeppe Bundgaard 5b3e59bbb4 Introduce scope filtering and machine type integration for Self-Serve module:
- Add scope label computation for tasks, questions, and conditions based on department, lane, product, and machine type.
- Introduce filters for machine type, lane, and department in Self-Serve pagination.
- Replace `EditableTableColumn` with scoped rendering for improved flexibility.
- Extend API and schema to support machine type integration in department redistribution.
- Optimize UI for task, question, and condition management with machine type-aware workflows.
2026-03-16 13:43:08 +01:00
Jeppe Bundgaard f6050740ec Introduce machine types management for Self-Serve module:
- Add `DepartmentSelfServeMachineTypes` component with pagination and table for machine types.
- Extend Self-Serve objects to support machine type attributes and CRUD operations.
- Update UI flows for managing machine types, tasks, and conditions.
- Implement reusable forms for creating and editing machine types, tasks, and conditions.
- Add API integration for machine type operations.
- Enhance translations and component reuse across Self-Serve views.
2026-03-16 13:39:43 +01:00
Jeppe Bundgaard 260fb8d8e9 Introduce machine type support for Self-Serve module:
- Add `DepartmentSelfServeMachineTypes` routes and components.
- Extend condition and rule objects to support machine type attributes.
- Update UI flows for managing machine types and conditions.
- Adjust creation forms to handle machine type associations.
- Implement API changes for machine type retrieval and assignment.
- Add utility functions and async data parsing for machine type handling.
2026-03-16 13:39:25 +01:00
Jeppe Bundgaard b7b19d1ad9 Add comprehensive documentation for Self-Serve Module.
- Describe architecture, domain models, eligibility rules, API endpoints, runtime status, and lane control.
- Include end-to-end examples for happy paths and failure scenarios.
- Provide PHP and Bash usage samples for integration.
- Add detailed flow diagrams and entity definitions for clarity.
2026-03-16 13:08:12 +01:00
Jeppe Bundgaard 16eb2730b8 Merge remote-tracking branch 'origin/master' 2026-03-16 13:07:51 +01:00
Jeppe Bundgaard 412339744f Introduce footer totals for department allocations:
- Add `buildDepartmentAllocationFooterTotals` utility to compute department footer totals, excluding customer prices from the total column.
- Update department allocation summaries to prioritize actual distributions for fixed and subscription amounts.
- Adjust distribution metrics calculation logic and related tests for consistency.
- Enhance footer visuals with clearly marked totals and accompanying notes.
- Expand `buildDepartmentAllocations` to properly account for totals in UI rendering.
- Update associated unit tests to cover footer totals and exclude customer price impacts.
2026-03-16 13:04:56 +01:00
Jeppe Bundgaard aba9f63788 Introduce department goal support in global search UI:
- Add department goal progress retrieval and display in visible result cards.
- Include progress bar, period, and metadata for department goals in the UI.
- Implement caching and lazy loading for department goal data to optimize performance.
- Extend unit tests to cover department goal overview functionality.
- Update translations for department goals in English, Danish, and German locales.
2026-03-16 09:28:32 +01:00
Jeppe Bundgaard cef7a38b0e Refactor global search UI and enhance keyboard navigation:
- Add "shouldBypassTextEntryForSystemSearchKey" utility for improved keyboard event handling.
- Implement "scrollSelectedResultIntoView" for precise result focus management.
- Remove grouped summary views and associated settings (e.g., "showGrouped").
- Refactor entity label resolution with "humanize" and "resolveEntityTypeLabel" utilities.
- Simplify result toolbar with a compact layout and improved accessibility.
- Expand locale updates for entity type translations in English, Danish, and German.
- Update unit tests for new navigation logic, label resolution, and result handling.
2026-03-13 14:01:51 +01:00
Jeppe Bundgaard b28b95d40f Add unit tests and keyboard navigation enhancements for system search UI:
- Add unit tests for result sorting, grouping, and keyboard state synchronization.
- Implement keyboard navigation improvements for selections and group/result highlighting.
- Update sidebar and result pane interaction with visual indicators and scroll handling.
- Refactor payload extraction logic for nested keys and improve related action routing.
2026-03-13 13:47:16 +01:00
Jeppe Bundgaard cc260cf6c3 Add sidebar and results keyboard navigation with visual indicators:
- Implement navigation pane switching and active selection states.
- Add visual markers for sidebar and result pane interaction.
- Display object IDs and restructure result footer for clarity.
- Extend unit tests to validate navigation and UI updates.
2026-03-13 13:09:22 +01:00
Jeppe Bundgaard 489a64d5ee Enhance global search with keyboard navigation and improved selection:
- Introduce keyboard navigation for sidebar and results, including pane switching and item selection.
- Add visual indicators for selected sidebar groups and results.
- Refine result sorting and grouping logic with utility methods.
- Update modal accessibility with focus management and keyboard event handling.
- Extend unit tests to cover keyboard navigation and UI state sync.
- Apply scoped styles for interactive states and selected elements.
2026-03-13 13:06:33 +01:00
gold-dev 6ee08b6333 Merge branch 'master' of https://github.com/copenhagentruckwash/pleno-vue 2026-03-13 11:03:21 +00:00
gold-dev 7515db2d80 Feat: 202603 - truckwash.dk and truckwash.io edits - V10 8-13 2026-03-13 10:58:24 +00:00
Jeppe Bundgaard 2b3cf2010a Enhance global search functionality with shortcut support and extended result actions:
- Add double-shift keyboard shortcut for quick modal search access.
- Extend visible result cards with related targets and action handling for orders, departments, vehicles, and customers.
- Add tools for copying key identifiers like references and registrations to clipboard.
- Improve attachment handling with download actions and caching for order-related files.
- Update UI to include contextual action panels and accessibility improvements.
- Add additional unit tests for related targets and action handling.
2026-03-13 01:52:02 +01:00
Jeppe Bundgaard dfe8eaa882 Refactor global search UI and functionality:
- Enhance entity alias resolution with extended mappings and alias normalization.
- Add display labels, icons, created timestamps, and cancellation status to result cards.
- Update styles to highlight cancelled results and improve visual hierarchy.
- Expand i18n support for new labels in multiple locales, including `created`, `status`, and `cancelled`.
2026-03-13 00:50:54 +01:00
Jeppe Bundgaard 16baa28b85 Refactor global search UI:
- Replace `<b-tag>` components with simplified HTML elements for better control over styling and layout.
- Introduce utility methods and scoped styles for improved badge handling and consistent theming.
- Update group and result layouts to enhance readability and reduce visual noise.
- Remove unused imports for cleaner component structure.
2026-03-13 00:37:00 +01:00
Jeppe Bundgaard 96fdb4573d Refactor global search UI structure and styling:
- Introduce improved layout, with structured toolbars, responsive design, and grid-based result lists.
- Add scoped styles with consistent theming using CSS variables.
- Enhance accessibility and readability through defined interactive states and typography updates.
2026-03-13 00:29:03 +01:00
Jeppe Bundgaard 53ee2a0271 Correct casing and improve descriptions in Danish locale JSON for search-related UI labels. 2026-03-13 00:22:28 +01:00
Jeppe Bundgaard 93bf309626 Fix encoding issues and correct terminology in Danish locale JSON. 2026-03-13 00:17:54 +01:00
Jeppe Bundgaard 965e79e5f5 Expand system search support for new object types and aliases, improve routing logic, and add unit tests:
- Enhance alias resolution with expanded mappings for objects like `attachment`, `item`, `discount`, `task`, and more.
- Refactor navigation resolver logic for better routing of `department_variable` and attachment-like payloads.
- Add unit tests to ensure registry coverage, alias routing behavior, and fallback handling.
2026-03-13 00:16:58 +01:00
Jeppe Bundgaard bc62040670 Add inline editing functionality for advanced search results and update German locale translations:
- Introduce inline editor types for `order_item`, `customer_discount`, `customer_fixed_price`, `department_variable`, and `module_config`.
- Implement inline editing logic, including entity resolution, value validation, and API integration.
- Add "Edit" button for search result cards with permission checks.
- Update German translations for improved encoding and terminology consistency.
2026-03-13 00:16:38 +01:00
Jeppe Bundgaard e259b20548 Fix encoding and translation issues in Danish locale JSON. 2026-03-13 00:16:01 +01:00
Jeppe Bundgaard 3c4fdaeb3a Add Danish i18n translations for advanced system search, including UI labels, settings, actions, errors, and diagnostic tools. 2026-03-13 00:10:04 +01:00
Jeppe Bundgaard 024125ff1f Update German i18n translations to fix encoding and correct terminology. 2026-03-13 00:07:10 +01:00
Jeppe Bundgaard 77ecd5e656 Update German i18n translations to fix encoding and correct terminology. 2026-03-13 00:04:30 +01:00
Jeppe Bundgaard 843ab12ffa Update Danish i18n translations to fix encoding and correct terminology. 2026-03-13 00:04:25 +01:00
Jeppe Bundgaard d42fb2da18 Update Swedish i18n translations to fix encoding and correct terminology. 2026-03-12 23:58:55 +01:00
Jeppe Bundgaard 8b631898b2 Add unit tests for superuser-vehicle-analytics, superuser-vehicle-view, and advanced system search components:
- Cover query builders, data normalization, and analytics aggregations for vehicle view utilities.
- Validate route contracts, tab structure, and OpenAPI integration for `superuser-vehicle-view`.
- Test advanced search modal logic, route registration, registry compliance, and UI behavior.
- Ensure system search registry and navigation resolvers provide complete type coverage and fallback handling.
2026-03-12 23:57:01 +01:00
Jeppe Bundgaard 880031de1a Expand system search support to include users entity type:
- Add `users` to `SYSTEM_SEARCH_ENTITY_TYPES` and related navigation resolvers.
- Implement alias resolution for `users` and associated delegation strategies for search results.
- Extend support definitions, title/description keys, and navigation strategies for `users`.
- Refactor fallback handling and unknown entity resolution logic.
2026-03-12 23:49:40 +01:00
Jeppe Bundgaard 2977523d0b Update Swedish i18n translations to fix encoding and correct terminology 2026-03-12 23:49:30 +01:00
Jeppe Bundgaard 31c0aa739e Fix encoding and translation issues in English locale JSON:
- Replace invalid characters with standard ASCII equivalents.
- Correct translations for booking-related terms and general text.
- Update system configuration options and user-facing labels.
2026-03-12 23:40:02 +01:00
Jeppe Bundgaard 6c3e04e8b8 Update OpenAPI schema to include users resource in SystemSearchRequest 2026-03-12 23:34:16 +01:00
Jeppe Bundgaard 253dc1d357 Add unit tests for superuser-vehicle-analytics, superuser-vehicle-view, and advanced system search components:
- Cover query builders, data normalization, and analytics aggregations for vehicle view utilities.
- Validate route contracts, tab structure, and OpenAPI integration for `superuser-vehicle-view`.
- Test advanced search modal logic, route registration, registry compliance, and UI behavior.
- Ensure system search registry and navigation resolvers provide complete type coverage and fallback handling.
2026-03-12 23:10:20 +01:00
Jeppe Bundgaard bade01b92a Update i18n translations for Swedish locale to fix incorrect encoding issues. 2026-03-12 22:57:48 +01:00
Jeppe Bundgaard b1a2d4bcda Enhance NavigationMenuGlobalSearch with multi-page fetching and loading progress:
- Introduce multi-page result fetching for large data sets with progress tracking.
- Add progress bar UI to show loading status and fetched results count.
- Refactor offsets, pagination logic, and result grouping for improved scalability.
- Update UI elements and computed properties to handle enhanced result handling and pagination.
2026-03-12 22:13:41 +01:00
Jeppe Bundgaard a80422dab8 Add pagination to NavigationMenuGlobalSearch with b-pagination component:
- Replace previous offset-based navigation with paginated navigation.
- Adjust layout and styles for improved usability and flexibility.
- Refactor result handling to integrate pagination events and model updates.
2026-03-12 22:00:27 +01:00
Jeppe Bundgaard 70bba3f77c Refactor NavigationMenuGlobalSearch: add input focus handling, enhance entity type selectors with checklists, and improve responsive layout. 2026-03-12 21:46:53 +01:00
Jeppe Bundgaard a9972c5203 Expand NavigationMenuGlobalSearch with advanced filtering, grouping, and settings management:
- Add new entity types for enhanced filtering.
- Introduce result grouping, configurable settings, and group-specific visible results.
- Refactor sidebar and main panel for improved user experience.
- Enhance settings UI with pagination, filters, and manage tools.
2026-03-12 21:33:04 +01:00
Jeppe Bundgaard 4e65aad0fc Add Vehicle Analytics Chart component and utility functions:
- Implement `VehicleAnalyticsChart.vue` to support line/bar chart displays for vehicle data.
- Add `vehicleViewUtils.js` with reusable utility functions for data normalization, query building, and aggregation.
2026-03-12 21:20:51 +01:00
Jeppe Bundgaard 2a9a2ea93d Update OpenAPI schema to include additional resource types in SystemSearchRequest 2026-03-12 21:20:30 +01:00
Jeppe Bundgaard 5cd7598e7e Integrate advanced system-wide search feature:
- Implement an advanced search component with filters, grouped results, and caching capabilities.
- Add modal-based UI for user-friendly interaction.
- Enable cache management tools for superusers, including clearing and rebuilding caches.
- Update desktop navigation to include the new search functionality.
2026-03-12 21:10:39 +01:00
Jeppe Bundgaard d197f7f8cf Add system-wide search endpoints and integrate compare visibility filter:
- Add new search endpoints to OpenAPI schema with cache management functionality.
- Extend invoice distribution functionality with `compareVisibility` filter for result categorization.
- Update unit tests and UI components for compare visibility options.
2026-03-12 20:30:09 +01:00
Jeppe Bundgaard c97d5c09d7 Update i18n translations for Swedish, German, and Norwegian locales; refactor invoice distribution API with enhanced fallback logic for v2 split and legacy endpoints; and add unit tests for distribution methods and UI integration. 2026-03-12 19:53:52 +01:00
Jeppe Bundgaard 75b7aad56a Add unit tests for invoice distribution API, enhance v2 bulk comparison handling, and update UI/locale for customer price metrics. 2026-03-12 18:50:54 +01:00
Jeppe Bundgaard e1e3187d69 Add unit and e2e tests for superuser invoice distribution, update i18n translations, and implement distribution API/logic helpers. 2026-03-12 18:03:44 +01:00
Jeppe Bundgaard db2040570e Add superuser invoice distribution module:
- Integrate departmental distribution view with monthly breakdowns, filters, and metrics.
- Add support for comparisons, source blends, and allocation trends.
- Update i18n translations for English, Swedish, German, and Norwegian.
- Extend router with distribution-specific routes.
- Add unit tests for route mapping, tabs, and view integrations.
- Update e2e tests for API routes.
2026-03-12 17:34:26 +01:00
Jeppe Bundgaard 203123e73b Add invoicing billing period calculations module with sum, net amount, and view total functions; include comprehensive unit tests 2026-03-12 16:03:22 +01:00
Jeppe Bundgaard 80fcf8e4ce Add unit tests for invoice orders pagination, introduce safe items-per-page logic in list pagination, and refactor InvoicingBillingPeriod views for improved date handling and modularity. 2026-03-12 15:10:16 +01:00
Jeppe Bundgaard 4852a8cc6b Add unit tests for period view navigation, runtime view switching, and view existence checks; fix incorrect view keys in special arrangements and per-order views; extend global object setup in tests. 2026-03-12 14:39:08 +01:00
Jeppe Bundgaard e48c52a9b7 Add unit tests for superuser invoices route wiring, tabs, and API contracts; enhance calculations and logic in InvoicingBillingPeriodStatistics.vue; update OpenAPI schema with WeatherAPI and fixed pricing distribution functionality. 2026-03-12 14:24:12 +01:00
Jeppe Bundgaard 7c383a1663 Merge remote-tracking branch 'origin/master' 2026-03-12 11:29:02 +01:00
gold-dev 7196bd1ca8 Feat: 202603 - truckwash.dk and truckwash.io edits - V10 2-7 2026-03-12 12:03:49 +02:00
Jeppe Bundgaard 0ce9dc85ce Add tooltip and booked amount column to InvoicingBillingPeriodStatistics table 2026-03-12 09:35:21 +01:00
Jeppe Bundgaard 0d8de69fed Add unit tests for WeatherAPI module contracts and superuser wiring 2026-03-12 09:19:08 +01:00
972 changed files with 239700 additions and 28921 deletions
+186
View File
@@ -0,0 +1,186 @@
{
"version": 1,
"snapshot_of": "front-end-vue",
"projects": {
"front-end-vue": {
"display_name": "Truckwash Frontend",
"relative_root": ".",
"commands": {
"setup": {
"default": "npm ci\nnpx playwright install"
},
"run": {
"default": "npm run dev"
},
"test_unit": {
"default": "npm run test:unit"
},
"test_e2e": {
"default": "npm run test:e2e:smoke"
},
"test_full": {
"default": "npm run test:e2e:ci"
},
"debug": {
"default": "npx playwright test --debug"
}
},
"codex_environment": {
"name": "pleno-vue",
"actions": [
{
"name": "Run dev",
"icon": "run",
"command_id": "run"
},
{
"name": "Vitest unit tests",
"icon": "test",
"command_id": "test_unit"
},
{
"name": "Playwright smoke",
"icon": "test",
"command_id": "test_e2e"
},
{
"name": "Playwright full suite",
"icon": "test",
"command_id": "test_full"
},
{
"name": "Playwright debug",
"icon": "debug",
"command_id": "debug"
},
{
"name": "AI workflow check",
"icon": "debug",
"literal_command": {
"default": "node scripts/sync-ai-workflow.mjs --check"
}
}
]
},
"generated_content": {
"aiassistant_tests_lines": [
"# Frontend Testing Rules",
"",
"These rules apply to Vue changes, browser flows, and frontend test updates.",
"",
"1. Add or update tests for every new feature, bug fix, API integration change, or auth, booking, POS, or i18n workflow change.",
"2. Browser validation runs through Playwright. Legacy browser-driver tooling is outside the supported workflow.",
"3. Use `npm run test:unit` for Vitest coverage of isolated logic and component behavior.",
"4. Use `npm run test:e2e:smoke` for the narrow Playwright browser pass on changed user flows.",
"5. Use `npm run test:e2e:ci` when a change affects shared navigation, authentication, release-critical flows, or multiple browser projects.",
"6. Use `npx playwright test --debug` for interactive debugging and trace capture.",
"7. Prefer stable selectors, explicit fixtures, and assertions about user-visible behavior.",
"8. Avoid manual sleeps. Let Playwright waiting and expectations drive synchronization.",
"9. If a bug fix needs a browser regression test, prove the failure first and then keep the test with the fix.",
"",
"Canonical workflow reference: `.ai-workflow/workflow.md`."
],
"junie_lines": [
"# Truckwash Frontend Development Guidelines",
"",
"This file is generated from the canonical AI workflow and is the supported Junie-facing reference for the frontend repository.",
"",
"## Build And Run",
"",
"- Setup: `npm ci` and `npx playwright install`.",
"- Start development server: `npm run dev`.",
"- Production build sanity check: `npm run build`.",
"",
"## Testing",
"",
"- Vitest unit tests: `npm run test:unit`.",
"- Playwright smoke: `npm run test:e2e:smoke`.",
"- Playwright full suite: `npm run test:e2e:ci`.",
"- Playwright debug: `npx playwright test --debug`.",
"- Browser validation is Playwright-first. Legacy browser-driver tooling is not part of the supported workflow.",
"",
"## Workflow Notes",
"",
"- Generated assistant metadata is checked with `node scripts/sync-ai-workflow.mjs --check`.",
"- Keep frontend browser verification aligned with the existing Playwright projects and runner scripts in `package.json`.",
"- Use the narrowest relevant test command first, then expand to the broader suite when the change risk requires it.",
"",
"Canonical workflow reference: `.ai-workflow/workflow.md`."
]
}
}
},
"assistants": {
"codex": {
"description": "Codex environment files and actions."
},
"aiassistant": {
"description": "Project-specific AI Assistant rules."
},
"junie": {
"description": "Project-specific Junie guidance."
},
"copilot": {
"description": "Standardized Copilot task dispatch workflow."
}
},
"commands": {
"setup": {
"description": "Install dependencies and prepare the supported local environment."
},
"run": {
"description": "Start the primary local development entrypoint for the project."
},
"test_unit": {
"description": "Run the project's narrow unit-style verification command."
},
"test_e2e": {
"description": "Run the project's targeted browser or end-to-end verification command."
},
"test_full": {
"description": "Run the broader high-confidence verification command when the project defines one."
},
"debug": {
"description": "Run the supported debug entrypoint."
}
},
"generated_outputs": [
{
"id": "frontend_codex_environment",
"template": "codex_project_environment",
"project": "front-end-vue",
"path": ".codex/environments/environment.toml"
},
{
"id": "frontend_aiassistant_tests",
"template": "aiassistant_frontend_tests_rule",
"project": "front-end-vue",
"path": ".aiassistant/rules/Creating and maintaining tests.md"
},
{
"id": "frontend_junie_guidelines",
"template": "junie_frontend_guidelines",
"project": "front-end-vue",
"path": ".junie/guidelines.md"
}
],
"sync_targets": {
"front-end-vue": {
"source_root": ".",
"destination": "C:\\Users\\2jepp\\WebstormProjects\\pleno-vue",
"supported_metadata_dirs": [
".codex",
".aiassistant",
".junie",
".github",
".ai-workflow",
"scripts"
]
}
},
"unsupported": {
"mcp": [
"front-end-vue/.ai/mcp/mcp.json"
]
}
}
+74
View File
@@ -0,0 +1,74 @@
<!-- AUTOGENERATED SNAPSHOT for front-end-vue: refresh from the canonical workspace with `node scripts/sync-ai-workflow.mjs --write`. -->
# Developer AI Workflow
This directory is the canonical source of truth for the repository's developer-facing AI workflow.
## Goals
- Keep Codex, AI Assistant, Junie, and Copilot aligned from one maintained source.
- Make assistant metadata deterministic so the generated files can be rewritten safely and checked in CI.
- Preserve the current mirror-repo workflow for `backend-php` and `front-end-vue`.
- Keep all changes in this workflow scoped to developer tooling and documentation. Runtime OpenAI features stay out of scope.
## Supported Assistants
- `Codex`: local environments and actions under `.codex/environments`.
- `AI Assistant`: generated guidance under `.aiassistant/rules`.
- `Junie`: generated project guidance under `.junie/guidelines.md`.
- `Copilot`: standardized issue-dispatch workflow content under `.github/workflows/copilot.yml`.
## Global Rules
1. Change only `.ai-workflow/workflow.md` and `.ai-workflow/manifest.json` when updating the developer AI workflow.
2. Regenerate all derived files with `node scripts/sync-ai-workflow.mjs --write`.
3. Validate drift with `node scripts/sync-ai-workflow.mjs --check`.
4. Generated assistant files are not hand-edited.
5. Unsupported surfaces stay unsupported until they have a real owner and a real config.
6. `front-end-vue/.ai/mcp/mcp.json` is intentionally unsupported and should not be recreated until there is an actual MCP integration to maintain.
## Command Matrix
### `backend-php`
- Setup: use the existing setup scripts in `backend-php/scripts`.
- Run: start the API stack with `traefik`, `redis`, `mysql-debug`, `php1`, and `caddy`.
- Debug: tail `php1` logs.
- PHP verification: always run backend validation in the `php1` container.
- Unit tests: `docker compose exec -T php1 sh -lc "cd /var/www/html && composer test:unit"`.
- Integration tests: `docker compose exec -T php1 sh -lc "cd /var/www/html && composer test:integration"`.
- API tests: `docker compose exec -T php1 sh -lc "cd /var/www/html && composer test:api"`.
### `front-end-vue`
- Setup: `npm ci` and `npx playwright install`.
- Run: `npm run dev`.
- Unit tests: `npm run test:unit` with Vitest.
- Browser smoke: `npm run test:e2e:smoke`.
- Browser full suite: `npm run test:e2e:ci`.
- Debug: `npx playwright test --debug`.
- Browser validation is Playwright-first. WebdriverIO and Appium are not part of the supported workflow.
### `automation`
- Setup: `npm ci` and `npx playwright install`.
- Run: `npx playwright test --ui`.
- Browser tests: `npx playwright test`.
- Debug: `npx playwright test --debug`.
## Mirror Repos And Sync
- `backend-php` mirrors to `C:\Users\2jepp\PhpstormProjects\api`.
- `front-end-vue` mirrors to `C:\Users\2jepp\WebstormProjects\pleno-vue`.
- Supported metadata directories that must stay mirrored are `.codex`, `.aiassistant`, `.junie`, `.github`, generated `.ai-workflow`, and `scripts/sync-ai-workflow.mjs`.
- Cache and build directories remain excluded from the watcher.
- The mirror repos receive generated snapshots of `.ai-workflow` and `scripts/sync-ai-workflow.mjs` so their local CI can run `--check` without depending on the combined workspace root.
## Generated Outputs
- Root Codex environment for combined backend and frontend entrypoints.
- Backend and frontend Codex environments.
- Backend and frontend AI Assistant guidance.
- Backend and frontend Junie guidance.
- Backend Copilot dispatcher workflow.
- Backend and frontend snapshot copies of `.ai-workflow` plus `scripts/sync-ai-workflow.mjs` for mirrored repositories.
@@ -2,256 +2,20 @@
apply: always
---
# Guidelines for Creating and Maintaining Tests
<!-- AUTOGENERATED: Run `node scripts/sync-ai-workflow.mjs --write`. -->
## When to Create Tests
# Frontend Testing Rules
Tests **MUST** be created when any of the following criteria are met:
These rules apply to Vue changes, browser flows, and frontend test updates.
1. **New Feature Development**: Any new feature, component, or functionality requires accompanying tests
2. **Bug Fixes**: When fixing a bug, write a test that reproduces the issue first, then verify the fix
3. **API Changes**: Changes to APIs, services, or data-fetching logic require updated or new tests
4. **Critical User Flows**: Any modification to authentication, payments, bookings, or POS flows
5. **Component Behavior Changes**: When modifying Vue component logic, props, or events
6. **i18n/Localization Changes**: When adding or modifying translation keys
1. Add or update tests for every new feature, bug fix, API integration change, or auth, booking, POS, or i18n workflow change.
2. Browser validation runs through Playwright. Legacy browser-driver tooling is outside the supported workflow.
3. Use `npm run test:unit` for Vitest coverage of isolated logic and component behavior.
4. Use `npm run test:e2e:smoke` for the narrow Playwright browser pass on changed user flows.
5. Use `npm run test:e2e:ci` when a change affects shared navigation, authentication, release-critical flows, or multiple browser projects.
6. Use `npx playwright test --debug` for interactive debugging and trace capture.
7. Prefer stable selectors, explicit fixtures, and assertions about user-visible behavior.
8. Avoid manual sleeps. Let Playwright waiting and expectations drive synchronization.
9. If a bug fix needs a browser regression test, prove the failure first and then keep the test with the fix.
## Running Tests When Adding Tests
When creating or modifying tests, you **MUST**:
1. **Run the specific test file** to verify it passes:
```bash
npx playwright test tests/your-test-file.spec.ts
```
2. **Run the full test suite** before committing to ensure no regressions:
```bash
npx playwright test
```
3. **Verify test failures before fixes**: If writing a test for a bug, confirm the test fails before applying the fix
## Testing Framework
This project uses **Playwright** as the primary testing framework. Playwright provides reliable end-to-end testing with auto-waiting, powerful selectors, and built-in assertions.
## Running Tests
```bash
# Run full test suite
npx playwright test
# Run tests in headed mode (visible browser)
npx playwright test --headed
# Run tests in a specific browser
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit
# Run tests with UI mode for debugging
npx playwright test --ui
# Generate HTML report
npx playwright show-report
```
## Test File Location and Naming
- **Location**: All test files should be placed in `tests/` or `tests/specs/`
- **Naming Convention**: Use kebab-case with `.spec.ts` suffix
- ✅ `i18n-settings-wheel.spec.ts`
- ✅ `user-authentication.spec.ts`
- ❌ `UserAuth.test.js`
## Test Structure
### Basic Template
```typescript
import { test, expect } from '@playwright/test';
/**
* Brief description of what this test suite covers
*/
test.describe('Feature or Component Name', () => {
test('should describe expected behavior', async ({ page }) => {
await page.goto('/');
// Test implementation
await expect(page).toHaveTitle(/Expected Text/);
});
});
```
### Key Conventions
1. **Use descriptive test names**: Start with `should` to describe expected behavior
2. **Always use `async/await`**: All Playwright interactions are asynchronous
3. **Include JSDoc comments**: Add a comment block at the top describing the test purpose
4. **Group related tests**: Use `test.describe` blocks to organize related test cases
## Writing Tests
### Navigation
```typescript
// Navigate to a page
await page.goto('/');
await page.goto('/dashboard');
// Wait for navigation
await page.goto('/dashboard', { waitUntil: 'networkidle' });
```
### Page Interactions
```typescript
// Fill input fields
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
// Click elements
await page.click('button[id="login-button"]');
// Wait for elements
await page.waitForSelector('input[name="2fa_code"]');
// Get element text
const text = await page.locator('.message').textContent();
// Get page title
const title = await page.title();
// Execute JavaScript in browser context
const result = await page.evaluate(() => {
return document.querySelector('.data')?.textContent;
});
```
### Locators (Recommended)
```typescript
// Use locators for better reliability
const submitButton = page.locator('button[type="submit"]');
await submitButton.click();
// Locator with text
const loginButton = page.locator('button:has-text("Login")');
// Get by role (accessibility-friendly)
const heading = page.getByRole('heading', { name: 'Welcome' });
// Get by test ID
const element = page.getByTestId('submit-button');
```
### Assertions
```typescript
// Use Playwright's built-in expect assertions
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveTitle(/Dashboard/);
await expect(page.locator('.message')).toBeVisible();
await expect(page.locator('.message')).toHaveText('Success');
await expect(page.locator('input')).toHaveValue('expected value');
await expect(page.locator('.items')).toHaveCount(5);
```
## Authentication Helpers
Create reusable authentication helpers in `tests/fixtures/`:
```typescript
import { Page, expect } from '@playwright/test';
/**
* Login as a customer user
* @param page - Playwright page object
* @param credentials - User credentials
*/
export async function loginAsUser(
page: Page,
credentials: { customerNumber: string; password: string }
) {
await page.goto('/login');
await page.fill('input[name="customer_number"]', credentials.customerNumber);
await page.fill('input[name="password"]', credentials.password);
await page.click('button[id="login-button"]');
await expect(page).toHaveURL('/user');
}
```
## Best Practices
### DO:
- ✅ Test one behavior per `test` block
- ✅ Use meaningful variable names
- ✅ Use Playwright's auto-waiting (avoid manual waits when possible)
- ✅ Use locators for element interactions
- ✅ Use `expect` assertions with auto-retry
- ✅ Clean up test state when necessary
- ✅ Use test fixtures for setup/teardown
### DON'T:
- ❌ Write tests that depend on execution order
- ❌ Hardcode sensitive data (use environment variables)
- ❌ Use `page.waitForTimeout()` for synchronization (use proper waits)
- ❌ Leave commented-out code in test files
## Testing i18n/Translations
When testing internationalization:
1. Define expected translation keys as an array
2. Fetch locale files via page context
3. Verify all required keys exist in each locale
4. Check parity between locales (same keys in all languages)
```typescript
import { test, expect } from '@playwright/test';
const requiredKeys = ['key1', 'key2', 'key3'];
test('should have all translations in English', async ({ page }) => {
await page.goto('/');
const result = await page.evaluate(async (keys) => {
const response = await fetch('/src/i18n/locales/en.json');
const data = await response.json();
const missingKeys = keys.filter(key => !(key in data));
return { success: missingKeys.length === 0, missingKeys };
}, requiredKeys);
expect(result.success).toBe(true);
});
```
## Mobile Testing
Playwright supports mobile emulation out of the box:
```typescript
import { test, devices } from '@playwright/test';
// Use device presets
test.use({ ...devices['iPhone 13'] });
test('should work on mobile', async ({ page }) => {
await page.goto('/');
// Test mobile-specific behavior
});
```
Configure mobile projects in `playwright.config.ts`:
```typescript
projects: [
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
]
```
## Maintaining Tests
1. **Keep tests updated**: When modifying features, update corresponding tests
2. **Review failing tests**: Investigate failures before marking as skipped
3. **Remove obsolete tests**: Delete tests for removed features
4. **Refactor shared logic**: Extract common test utilities to helper files in `tests/fixtures/`
Canonical workflow reference: `.ai-workflow/workflow.md`.
+32 -5
View File
@@ -11,14 +11,41 @@ npx playwright install
[[actions]]
name = "Run dev"
icon = "run"
command = "npm run dev"
command = '''
npm run dev
'''
[[actions]]
name = "Run all tests"
name = "Vitest unit tests"
icon = "test"
command = "npm run test:all"
command = '''
npm run test:unit
'''
[[actions]]
name = "Run debug"
name = "Playwright smoke"
icon = "test"
command = '''
npm run test:e2e:smoke
'''
[[actions]]
name = "Playwright full suite"
icon = "test"
command = '''
npm run test:e2e:ci
'''
[[actions]]
name = "Playwright debug"
icon = "debug"
command = "npx playwright test --debug"
command = '''
npx playwright test --debug
'''
[[actions]]
name = "AI workflow check"
icon = "debug"
command = '''
node scripts/sync-ai-workflow.mjs --check
'''
+21 -21
View File
@@ -1,28 +1,28 @@
name: Qodana Configuration Upload
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main ]
workflow_dispatch:
push:
branches: [main, dev]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
upload-qodana-config:
runs-on: ubuntu-latest
upload-qodana-config:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
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
- 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
+21 -21
View File
@@ -1,28 +1,28 @@
name: Qodana Configuration Upload
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main ]
workflow_dispatch:
push:
branches: [main, dev]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
upload-qodana-config:
runs-on: ubuntu-latest
upload-qodana-config:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
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
- 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
+72 -20
View File
@@ -3,20 +3,49 @@ name: Automated Tests
on:
pull_request:
push:
branches: [main, dev]
workflow_dispatch:
schedule:
- cron: "0 2 * * *"
permissions:
contents: read
concurrency:
group: frontend-tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-unit:
runs-on: ubuntu-latest
format-tests:
# Match the labels exposed by the Coolify-managed GitHub runner.
runs-on: [self-hosted, Linux, X64, default]
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
- name: Check AI workflow sync
run: node scripts/sync-ai-workflow.mjs --check
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Check frontend test formatting
run: npm run format:tests:check
build-and-unit:
needs: format-tests
runs-on: [self-hosted, Linux, X64, default]
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
@@ -29,21 +58,23 @@ jobs:
- name: Unit tests
run: npm run test:unit
env:
VITEST_BATCH_SIZE: 5
e2e-smoke:
if: github.event_name != 'schedule'
needs: build-and-unit
runs-on: ubuntu-latest
runs-on: [self-hosted, Linux, X64, default]
strategy:
fail-fast: false
matrix:
project: [chromium-desktop, chromium-mobile]
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
@@ -58,7 +89,7 @@ jobs:
run: npx playwright test --grep @smoke --project="${{ matrix.project }}"
- name: Upload Playwright report
if: always()
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-smoke-${{ matrix.project }}
@@ -69,15 +100,35 @@ jobs:
retention-days: 7
e2e-full:
if: github.event_name == 'schedule' || github.ref == 'refs/heads/main'
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch
needs: build-and-unit
runs-on: ubuntu-latest
name: E2E-full-${{ matrix.role }}-${{ matrix.browser_label }}-${{ matrix.device }}
runs-on: [self-hosted, Linux, X64, default]
strategy:
fail-fast: false
matrix:
role: [customer, subuser, admin, superuser]
browser: [chromium, firefox, webkit]
device: [mobile, tablet, desktop]
include:
- browser: chromium
browser_label: Chromium
browser_install: chromium
- browser: firefox
browser_label: Firefox
browser_install: firefox
- browser: webkit
browser_label: WebKit
browser_install: webkit
env:
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}
PLAYWRIGHT_REPORTER_MODE: line-html
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
@@ -86,18 +137,19 @@ jobs:
run: npm ci --legacy-peer-deps
- name: Install Playwright browsers
run: npx playwright install --with-deps
run: npx playwright install --with-deps ${{ matrix.browser_install }}
- name: Run full Playwright suite
run: npm run test:e2e:ci
- name: Run full Playwright slice
run: npm run test:e2e:full:slice -- --role="${{ matrix.role }}" --project="${{ matrix.browser }}-${{ matrix.device }}"
- name: Upload Playwright report
if: always()
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-full
name: playwright-report-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}
path: |
output/playwright/report
output/playwright/test-results
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/report
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
+59287
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
cd -- "$(dirname -- "$0")/.."
npm run format:tests:commit
+18 -68
View File
@@ -1,77 +1,27 @@
# Project Guidelines
<!-- AUTOGENERATED: Run `node scripts/sync-ai-workflow.mjs --write`. -->
## Project Overview
# Truckwash Frontend Development Guidelines
This repository contains a Vue 3 single-page application built with Vite. It powers dashboard-style pages and modules such as bookings and POS flows. The UI is primarily styled with Bulma. The app leverages libraries like FullCalendar and Chart.js for scheduling and analytics.
This file is generated from the canonical AI workflow and is the supported Junie-facing reference for the frontend repository.
- Tech stack: Vue 3, Vite 5, Vue Router, Vuex, Bulma, FullCalendar, Chart.js
- Notable integrations: toast notifications, QR/Barcode utilities, and PWA/devtools plugins
- Target: modern browsers and mobile WebView contexts
## Build And Run
Local development
- Requirements: Node.js 18+ (recommended to match Vite 5), npm 9+
- Install dependencies: `npm install`
- Start dev server: `npm run dev` (default at http://localhost:5173)
- Setup: `npm ci` and `npx playwright install`.
- Start development server: `npm run dev`.
- Production build sanity check: `npm run build`.
Build and preview
- Production build: `npm run build` (output in `dist/`)
- Preview build locally: `npm run preview`
## Testing
Testing
- Framework: WebdriverIO (wdio) with Vite service; Appium available for mobile testing
- Common commands:
- `npm run wdio` — run test suite per `wdio.conf.js`
- `npm run test:mobile` — run tests excluding Safari
- `npm run test:android` — run Android-targeted specs
- Prerequisites for mobile tests may include Android SDK/emulator or iOS tooling and Appium drivers
- Vitest unit tests: `npm run test:unit`.
- Playwright smoke: `npm run test:e2e:smoke`.
- Playwright full suite: `npm run test:e2e:ci`.
- Playwright debug: `npx playwright test --debug`.
- Browser validation is Playwright-first. Legacy browser-driver tooling is not part of the supported workflow.
Environment configuration
- Place environment variables in `.env.*` files (Vite loads `VITE_`-prefixed vars into the client)
## Workflow Notes
See the sections below for code style and detailed project structure.
- Generated assistant metadata is checked with `node scripts/sync-ai-workflow.mjs --check`.
- Keep frontend browser verification aligned with the existing Playwright projects and runner scripts in `package.json`.
- Use the narrowest relevant test command first, then expand to the broader suite when the change risk requires it.
## Code Style and Conventions
### Vue Components
- Use PascalCase for component names (e.g., `UserProfile.vue`)
- Implement Single File Components (SFC) pattern
- Keep components focused and single-responsibility
- Use composition API for new components
- Document component props and events
### JavaScript/TypeScript
- Use ES6+ features
- Maintain consistent naming conventions (camelCase for variables/methods)
- Document complex logic with clear comments
- Use TypeScript for type safety when possible
## Project Structure
### Root Directory Structure
- `/src` - Source code files
- `/assets` - Static assets (images, fonts, etc.)
- `/components` - Reusable Vue components
- `/views` - Page components
- `/router` - Vue Router configuration
- `/store` - Vuex store modules
- `/services` - API and external service integrations
- `/utils` - Utility functions and helpers
- `/styles` - Global styles and variables
### Configuration Files
- `vite.config.js` - Vite configuration
- `.env.*` - Environment variables
- `package.json` - Project dependencies and scripts
- `jsconfig.json` - JavaScript/TypeScript tooling and path aliases
### File Naming Conventions
- Components: PascalCase (e.g., `UserProfile.vue`)
- Utilities: camelCase (e.g., `formatDate.js`)
- Store modules: camelCase (e.g., `userStore.js`)
- Style files: kebab-case (e.g., `main-styles.scss`)
- Test files: `*.spec.js` or `*.test.js`
Canonical workflow reference: `.ai-workflow/workflow.md`.
+6
View File
@@ -0,0 +1,6 @@
{
"endOfLine": "lf",
"tabWidth": 2,
"printWidth": 120,
"semi": true
}
+81
View File
@@ -28,6 +28,87 @@ npm run dev
npm run build
```
## Playwright Batched Chromium Runs
Run default e2e tests in deterministic 25-test shards across `chromium-desktop` and `chromium-mobile`:
```sh
npm run test:e2e:batched:chromium
```
Run the same flow and automatically re-run failed shards with `PLAYWRIGHT_WORKERS=1`:
```sh
npm run test:e2e:batched:chromium:rerun-failed
```
Optional overrides:
```sh
PLAYWRIGHT_BATCH_SIZE=25
PLAYWRIGHT_BATCH_WORKERS=2
PLAYWRIGHT_BATCH_DEV_PORT=5193
```
You can also forward Playwright args:
```sh
npm run test:e2e:batched:chromium -- --grep @smoke
```
Artifacts and summaries:
- `output/playwright/batched-chromium/last-run.json`
- `output/playwright/batched-chromium/failed-shards.json`
- `output/playwright/batched-chromium/report-index.html`
- `output/playwright/batched-chromium-shard-<i>-of-<n>/report/index.html`
- `output/playwright/batched-chromium-rerun-shard-<i>-of-<n>/report/index.html`
## Playwright Full E2E
Run the permanent grouped full-suite entrypoint with a hard max of 5 total workers across:
- `chromium-(desktop|tablet|mobile)`
- `firefox-(desktop|tablet|mobile)`
- `webkit-(desktop|tablet|mobile)`
```sh
npm run test:e2e:ci
```
Run a single full-suite slice for one role and one Playwright project:
```sh
npm run test:e2e:full:slice -- --role=admin --project=webkit-tablet
```
Default worker allocation:
```sh
PLAYWRIGHT_PARALLEL_WORKERS_CHROMIUM=2
PLAYWRIGHT_PARALLEL_WORKERS_FIREFOX=1
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=2
```
Optional overrides:
```sh
PLAYWRIGHT_PARALLEL_BASE_PORT=5191
PLAYWRIGHT_PARALLEL_WORKERS_CHROMIUM=2
PLAYWRIGHT_PARALLEL_WORKERS_FIREFOX=1
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=2
```
The runner fails fast if the combined worker count exceeds 5.
Artifacts and summaries:
- `output/playwright/ci-parallel-report/index.html`
- `output/playwright/ci-parallel-chromium/report/index.html`
- `output/playwright/ci-parallel-firefox/report/index.html`
- `output/playwright/ci-parallel-webkit/report/index.html`
- `output/playwright/test-lists/<role>-<project>.txt`
## Bubblewrap (TWA) Build and Install
To build and install the Trusted Web Activity (TWA) using Bubblewrap, use the following commands:
File diff suppressed because one or more lines are too long
-1
View File
@@ -8,7 +8,6 @@
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<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">
View File
@@ -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
});
@@ -0,0 +1,60 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var exports_exports = {};
__export(exports_exports, {
BrowserBackend: () => import_browserBackend.BrowserBackend,
Tab: () => import_tab.Tab,
browserTools: () => import_tools.browserTools,
createClientInfo: () => import_registry.createClientInfo,
createConnection: () => import_mcp.createConnection,
filteredTools: () => import_tools.filteredTools,
logUnhandledError: () => import_log.logUnhandledError,
parseResponse: () => import_response.parseResponse,
setupExitWatchdog: () => import_watchdog.setupExitWatchdog,
start: () => import_server.start,
startCliDaemonServer: () => import_daemon.startCliDaemonServer,
toMcpTool: () => import_tool.toMcpTool
});
module.exports = __toCommonJS(exports_exports);
var import_registry = require("./cli-client/registry");
var import_daemon = require("./cli-daemon/daemon");
var import_log = require("./mcp/log");
var import_watchdog = require("./mcp/watchdog");
var import_tool = require("./utils/mcp/tool");
var import_browserBackend = require("./backend/browserBackend");
var import_response = require("./backend/response");
var import_tab = require("./backend/tab");
var import_tools = require("./backend/tools");
var import_server = require("./utils/mcp/server");
var import_mcp = require("./mcp/index");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BrowserBackend,
Tab,
browserTools,
createClientInfo,
createConnection,
filteredTools,
logUnhandledError,
parseResponse,
setupExitWatchdog,
start,
startCliDaemonServer,
toMcpTool
});
@@ -0,0 +1,233 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var browserFactory_exports = {};
__export(browserFactory_exports, {
createBrowser: () => createBrowser,
createBrowserWithInfo: () => createBrowserWithInfo,
isProfileLocked: () => isProfileLocked
});
module.exports = __toCommonJS(browserFactory_exports);
var import_crypto = __toESM(require("crypto"));
var import_fs = __toESM(require("fs"));
var import_net = __toESM(require("net"));
var import_path = __toESM(require("path"));
var playwright = __toESM(require("../../.."));
var import_registry = require("../../server/registry/index");
var import_log = require("./log");
var import_context = require("../backend/context");
var import_extensionContextFactory = require("./extensionContextFactory");
var import_connect = require("../utils/connect");
var import_serverRegistry = require("../../serverRegistry");
var import_connect2 = require("../../client/connect");
async function createBrowser(config, clientInfo) {
const { browser } = await createBrowserWithInfo(config, clientInfo);
return browser;
}
async function createBrowserWithInfo(config, clientInfo) {
if (config.browser.remoteEndpoint)
return await createRemoteBrowser(config);
let browser;
if (config.browser.cdpEndpoint)
browser = await createCDPBrowser(config, clientInfo);
else if (config.browser.isolated)
browser = await createIsolatedBrowser(config, clientInfo);
else if (config.extension)
browser = await (0, import_extensionContextFactory.createExtensionBrowser)(config, clientInfo);
else
browser = await createPersistentBrowser(config, clientInfo);
return { browser, browserInfo: browserInfo(browser, config) };
}
function browserInfo(browser, config) {
return {
// eslint-disable-next-line no-restricted-syntax
guid: browser._guid,
browserName: config.browser.browserName,
launchOptions: config.browser.launchOptions,
userDataDir: config.browser.userDataDir
};
}
async function createIsolatedBrowser(config, clientInfo) {
(0, import_log.testDebug)("create browser (isolated)");
await injectCdpPort(config.browser);
const browserType = playwright[config.browser.browserName];
const tracesDir = await computeTracesDir(config, clientInfo);
const browser = await browserType.launch({
tracesDir,
...config.browser.launchOptions,
handleSIGINT: false,
handleSIGTERM: false
}).catch((error) => {
if (error.message.includes("Executable doesn't exist"))
throwBrowserIsNotInstalledError(config);
throw error;
});
await startServer(browser, clientInfo);
return browser;
}
async function createCDPBrowser(config, clientInfo) {
(0, import_log.testDebug)("create browser (cdp)");
const browser = await playwright.chromium.connectOverCDP(config.browser.cdpEndpoint, {
headers: config.browser.cdpHeaders,
timeout: config.browser.cdpTimeout
});
await startServer(browser, clientInfo);
return browser;
}
async function createRemoteBrowser(config) {
(0, import_log.testDebug)("create browser (remote)");
const descriptor = await import_serverRegistry.serverRegistry.find(config.browser.remoteEndpoint);
if (descriptor) {
const browser2 = await (0, import_connect.connectToBrowserAcrossVersions)(descriptor);
return {
browser: browser2,
browserInfo: {
guid: descriptor.browser.guid,
browserName: descriptor.browser.browserName,
launchOptions: descriptor.browser.launchOptions,
userDataDir: descriptor.browser.userDataDir
}
};
}
const endpoint = config.browser.remoteEndpoint;
const playwrightObject = playwright;
const browser = await (0, import_connect2.connectToBrowser)(playwrightObject, { endpoint });
browser._connectToBrowserType(playwrightObject[browser._browserName], {}, void 0);
return { browser, browserInfo: browserInfo(browser, config) };
}
async function createPersistentBrowser(config, clientInfo) {
(0, import_log.testDebug)("create browser (persistent)");
await injectCdpPort(config.browser);
const userDataDir = config.browser.userDataDir ?? await createUserDataDir(config, clientInfo);
const tracesDir = await computeTracesDir(config, clientInfo);
if (await isProfileLocked5Times(userDataDir))
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
const browserType = playwright[config.browser.browserName];
const launchOptions = {
tracesDir,
...config.browser.launchOptions,
...config.browser.contextOptions,
handleSIGINT: false,
handleSIGTERM: false,
ignoreDefaultArgs: [
"--disable-extensions"
]
};
try {
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
const browser = browserContext.browser();
await startServer(browser, clientInfo);
return browser;
} catch (error) {
if (error.message.includes("Executable doesn't exist"))
throwBrowserIsNotInstalledError(config);
if (error.message.includes("cannot open shared object file: No such file or directory")) {
const browserName = launchOptions.channel ?? config.browser.browserName;
throw new Error(`Missing system dependencies required to run browser ${browserName}. Install them with: sudo npx playwright install-deps ${browserName}`);
}
if (error.message.includes("ProcessSingleton") || error.message.includes("exitCode=21"))
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
throw error;
}
}
async function createUserDataDir(config, clientInfo) {
const dir = process.env.PWMCP_PROFILES_DIR_FOR_TEST ?? import_registry.registryDirectory;
const browserToken = config.browser.launchOptions?.channel ?? config.browser?.browserName;
const rootPathToken = createHash(clientInfo.cwd);
const result = import_path.default.join(dir, `mcp-${browserToken}-${rootPathToken}`);
await import_fs.default.promises.mkdir(result, { recursive: true });
return result;
}
async function injectCdpPort(browserConfig) {
if (browserConfig.browserName === "chromium")
browserConfig.launchOptions.cdpPort = await findFreePort();
}
async function findFreePort() {
return new Promise((resolve, reject) => {
const server = import_net.default.createServer();
server.listen(0, "127.0.0.1", () => {
const { port } = server.address();
server.close(() => resolve(port));
});
server.on("error", reject);
});
}
function createHash(data) {
return import_crypto.default.createHash("sha256").update(data).digest("hex").slice(0, 7);
}
async function computeTracesDir(config, clientInfo) {
return import_path.default.resolve((0, import_context.outputDir)({ config, cwd: clientInfo.cwd }), "traces");
}
async function isProfileLocked5Times(userDataDir) {
for (let i = 0; i < 5; i++) {
if (!isProfileLocked(userDataDir))
return false;
await new Promise((f) => setTimeout(f, 1e3));
}
return true;
}
function isProfileLocked(userDataDir) {
const lockFile = process.platform === "win32" ? "lockfile" : "SingletonLock";
const lockPath = import_path.default.join(userDataDir, lockFile);
if (process.platform === "win32") {
try {
const fd = import_fs.default.openSync(lockPath, "r+");
import_fs.default.closeSync(fd);
return false;
} catch (e) {
return e.code !== "ENOENT";
}
}
try {
const target = import_fs.default.readlinkSync(lockPath);
const pid = parseInt(target.split("-").pop() || "", 10);
if (isNaN(pid))
return false;
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function throwBrowserIsNotInstalledError(config) {
const channel = config.browser.launchOptions?.channel ?? config.browser.browserName;
if (config.skillMode)
throw new Error(`Browser "${channel}" is not installed. Run \`playwright-cli install-browser ${channel}\` to install`);
else
throw new Error(`Browser "${channel}" is not installed. Run \`npx @playwright/mcp install-browser ${channel}\` to install`);
}
async function startServer(browser, clientInfo) {
if (clientInfo.sessionName)
await browser.bind(clientInfo.sessionName, { workspaceDir: clientInfo.workspaceDir });
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createBrowser,
createBrowserWithInfo,
isProfileLocked
});
@@ -0,0 +1,352 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var cdpRelay_exports = {};
__export(cdpRelay_exports, {
CDPRelayServer: () => CDPRelayServer
});
module.exports = __toCommonJS(cdpRelay_exports);
var import_child_process = require("child_process");
var import_os = __toESM(require("os"));
var import_utilsBundle = require("../../utilsBundle");
var import_registry = require("../../server/registry/index");
var import_manualPromise = require("../../utils/isomorphic/manualPromise");
var import_http2 = require("../utils/mcp/http");
var import_log = require("./log");
var protocol = __toESM(require("./protocol"));
const debugLogger = (0, import_utilsBundle.debug)("pw:mcp:relay");
class CDPRelayServer {
constructor(server, browserChannel, userDataDir, executablePath) {
this._playwrightConnection = null;
this._extensionConnection = null;
this._nextSessionId = 1;
this._wsHost = (0, import_http2.addressToString)(server.address(), { protocol: "ws" });
this._browserChannel = browserChannel;
this._userDataDir = userDataDir;
this._executablePath = executablePath;
const uuid = crypto.randomUUID();
this._cdpPath = `/cdp/${uuid}`;
this._extensionPath = `/extension/${uuid}`;
this._resetExtensionConnection();
this._wss = new import_utilsBundle.wsServer({ server });
this._wss.on("connection", this._onConnection.bind(this));
}
cdpEndpoint() {
return `${this._wsHost}${this._cdpPath}`;
}
extensionEndpoint() {
return `${this._wsHost}${this._extensionPath}`;
}
async ensureExtensionConnectionForMCPContext(clientInfo) {
debugLogger("Ensuring extension connection for MCP context");
if (this._extensionConnection)
return;
this._connectBrowser(clientInfo);
debugLogger("Waiting for incoming extension connection");
await Promise.race([
this._extensionConnectionPromise,
new Promise((_, reject) => setTimeout(() => {
reject(new Error(`Extension connection timeout. Make sure the "Playwright MCP Bridge" extension is installed. See https://github.com/microsoft/playwright-mcp/blob/main/packages/extension/README.md for installation instructions.`));
}, process.env.PWMCP_TEST_CONNECTION_TIMEOUT ? parseInt(process.env.PWMCP_TEST_CONNECTION_TIMEOUT, 10) : 5e3))
]);
debugLogger("Extension connection established");
}
_connectBrowser(clientInfo) {
const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
const url = new URL("chrome-extension://mmlmfjhmonkocbjadbfplnigmagldckm/connect.html");
url.searchParams.set("mcpRelayUrl", mcpRelayEndpoint);
const client = {
name: "Playwright Agent",
version: require("../../../package.json").version
};
url.searchParams.set("client", JSON.stringify(client));
url.searchParams.set("protocolVersion", process.env.PWMCP_TEST_PROTOCOL_VERSION ?? protocol.VERSION.toString());
const token = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
if (token)
url.searchParams.set("token", token);
const href = url.toString();
const channel = import_registry.registry.isChromiumAlias(this._browserChannel) ? "chromium" : this._browserChannel;
let executablePath = this._executablePath;
if (!executablePath) {
const executableInfo = import_registry.registry.findExecutable(channel);
if (!executableInfo)
throw new Error(`Unsupported channel: "${this._browserChannel}"`);
executablePath = executableInfo.executablePath();
if (!executablePath)
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
}
const args = [];
if (this._userDataDir)
args.push(`--user-data-dir=${this._userDataDir}`);
if (import_os.default.platform() === "linux" && channel === "chromium")
args.push("--no-sandbox");
args.push(href);
(0, import_child_process.spawn)(executablePath, args, {
windowsHide: true,
detached: true,
shell: false,
stdio: "ignore"
});
}
stop() {
this.closeConnections("Server stopped");
this._wss.close();
}
closeConnections(reason) {
this._closePlaywrightConnection(reason);
this._closeExtensionConnection(reason);
}
_onConnection(ws2, request) {
const url = new URL(`http://localhost${request.url}`);
debugLogger(`New connection to ${url.pathname}`);
if (url.pathname === this._cdpPath) {
this._handlePlaywrightConnection(ws2);
} else if (url.pathname === this._extensionPath) {
this._handleExtensionConnection(ws2);
} else {
debugLogger(`Invalid path: ${url.pathname}`);
ws2.close(4004, "Invalid path");
}
}
_handlePlaywrightConnection(ws2) {
if (this._playwrightConnection) {
debugLogger("Rejecting second Playwright connection");
ws2.close(1e3, "Another CDP client already connected");
return;
}
this._playwrightConnection = ws2;
ws2.on("message", async (data) => {
try {
const message = JSON.parse(data.toString());
await this._handlePlaywrightMessage(message);
} catch (error) {
debugLogger(`Error while handling Playwright message
${data.toString()}
`, error);
}
});
ws2.on("close", () => {
if (this._playwrightConnection !== ws2)
return;
this._playwrightConnection = null;
this._closeExtensionConnection("Playwright client disconnected");
debugLogger("Playwright WebSocket closed");
});
ws2.on("error", (error) => {
debugLogger("Playwright WebSocket error:", error);
});
debugLogger("Playwright MCP connected");
}
_closeExtensionConnection(reason) {
this._extensionConnection?.close(reason);
this._extensionConnectionPromise.reject(new Error(reason));
this._resetExtensionConnection();
}
_resetExtensionConnection() {
this._connectedTabInfo = void 0;
this._extensionConnection = null;
this._extensionConnectionPromise = new import_manualPromise.ManualPromise();
void this._extensionConnectionPromise.catch(import_log.logUnhandledError);
}
_closePlaywrightConnection(reason) {
if (this._playwrightConnection?.readyState === import_utilsBundle.ws.OPEN)
this._playwrightConnection.close(1e3, reason);
this._playwrightConnection = null;
}
_handleExtensionConnection(ws2) {
if (this._extensionConnection) {
ws2.close(1e3, "Another extension connection already established");
return;
}
this._extensionConnection = new ExtensionConnection(ws2);
this._extensionConnection.onclose = (c, reason) => {
debugLogger("Extension WebSocket closed:", reason, c === this._extensionConnection);
if (this._extensionConnection !== c)
return;
this._resetExtensionConnection();
this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
};
this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
this._extensionConnectionPromise.resolve();
}
_handleExtensionMessage(method, params) {
switch (method) {
case "forwardCDPEvent":
const sessionId = params.sessionId || this._connectedTabInfo?.sessionId;
this._sendToPlaywright({
sessionId,
method: params.method,
params: params.params
});
break;
}
}
async _handlePlaywrightMessage(message) {
debugLogger("\u2190 Playwright:", `${message.method} (id=${message.id})`);
const { id, sessionId, method, params } = message;
try {
const result = await this._handleCDPCommand(method, params, sessionId);
this._sendToPlaywright({ id, sessionId, result });
} catch (e) {
debugLogger("Error in the extension:", e);
this._sendToPlaywright({
id,
sessionId,
error: { message: e.message }
});
}
}
async _handleCDPCommand(method, params, sessionId) {
switch (method) {
case "Browser.getVersion": {
return {
protocolVersion: "1.3",
product: "Chrome/Extension-Bridge",
userAgent: "CDP-Bridge-Server/1.0.0"
};
}
case "Browser.setDownloadBehavior": {
return {};
}
case "Target.setAutoAttach": {
if (sessionId)
break;
const { targetInfo } = await this._extensionConnection.send("attachToTab", {});
this._connectedTabInfo = {
targetInfo,
sessionId: `pw-tab-${this._nextSessionId++}`
};
debugLogger("Simulating auto-attach");
this._sendToPlaywright({
method: "Target.attachedToTarget",
params: {
sessionId: this._connectedTabInfo.sessionId,
targetInfo: {
...this._connectedTabInfo.targetInfo,
attached: true
},
waitingForDebugger: false
}
});
return {};
}
case "Target.getTargetInfo": {
return this._connectedTabInfo?.targetInfo;
}
}
return await this._forwardToExtension(method, params, sessionId);
}
async _forwardToExtension(method, params, sessionId) {
if (!this._extensionConnection)
throw new Error("Extension not connected");
if (this._connectedTabInfo?.sessionId === sessionId)
sessionId = void 0;
return await this._extensionConnection.send("forwardCDPCommand", { sessionId, method, params });
}
_sendToPlaywright(message) {
debugLogger("\u2192 Playwright:", `${message.method ?? `response(id=${message.id})`}`);
this._playwrightConnection?.send(JSON.stringify(message));
}
}
class ExtensionConnection {
constructor(ws2) {
this._callbacks = /* @__PURE__ */ new Map();
this._lastId = 0;
this._ws = ws2;
this._ws.on("message", this._onMessage.bind(this));
this._ws.on("close", this._onClose.bind(this));
this._ws.on("error", this._onError.bind(this));
}
async send(method, params) {
if (this._ws.readyState !== import_utilsBundle.ws.OPEN)
throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`);
const id = ++this._lastId;
this._ws.send(JSON.stringify({ id, method, params }));
const error = new Error(`Protocol error: ${method}`);
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject, error });
});
}
close(message) {
debugLogger("closing extension connection:", message);
if (this._ws.readyState === import_utilsBundle.ws.OPEN)
this._ws.close(1e3, message);
}
_onMessage(event) {
const eventData = event.toString();
let parsedJson;
try {
parsedJson = JSON.parse(eventData);
} catch (e) {
debugLogger(`<closing ws> Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`);
this._ws.close();
return;
}
try {
this._handleParsedMessage(parsedJson);
} catch (e) {
debugLogger(`<closing ws> Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`);
this._ws.close();
}
}
_handleParsedMessage(object) {
if (object.id && this._callbacks.has(object.id)) {
const callback = this._callbacks.get(object.id);
this._callbacks.delete(object.id);
if (object.error) {
const error = callback.error;
error.message = object.error;
callback.reject(error);
} else {
callback.resolve(object.result);
}
} else if (object.id) {
debugLogger("\u2190 Extension: unexpected response", object);
} else {
this.onmessage?.(object.method, object.params);
}
}
_onClose(event) {
debugLogger(`<ws closed> code=${event.code} reason=${event.reason}`);
this._dispose();
this.onclose?.(this, event.reason);
}
_onError(event) {
debugLogger(`<ws error> message=${event.message} type=${event.type} target=${event.target}`);
this._dispose();
}
_dispose() {
for (const callback of this._callbacks.values())
callback.reject(new Error("WebSocket closed"));
this._callbacks.clear();
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
CDPRelayServer
});
@@ -0,0 +1,7 @@
"use strict";
var import_utilsBundle = require("../../utilsBundle");
var import_program = require("./program");
const packageJSON = require("../../../package.json");
const p = import_utilsBundle.program.version("Version " + packageJSON.version).name("Playwright MCP");
(0, import_program.decorateMCPCommand)(p);
void import_utilsBundle.program.parseAsync(process.argv);
@@ -0,0 +1,16 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var config_d_exports = {};
module.exports = __toCommonJS(config_d_exports);
@@ -0,0 +1,446 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var config_exports = {};
__export(config_exports, {
commaSeparatedList: () => commaSeparatedList,
configFromEnv: () => configFromEnv,
dotenvFileLoader: () => dotenvFileLoader,
enumParser: () => enumParser,
headerParser: () => headerParser,
loadConfig: () => loadConfig,
numberParser: () => numberParser,
resolutionParser: () => resolutionParser,
resolveCLIConfigForCLI: () => resolveCLIConfigForCLI,
resolveCLIConfigForMCP: () => resolveCLIConfigForMCP,
resolveConfig: () => resolveConfig,
semicolonSeparatedList: () => semicolonSeparatedList
});
module.exports = __toCommonJS(config_exports);
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_os = __toESM(require("os"));
var import__ = require("../../..");
var import_utilsBundle = require("../../utilsBundle");
var import_configIni = require("./configIni");
async function fileExistsAsync(resolved) {
try {
return (await import_fs.default.promises.stat(resolved)).isFile();
} catch {
return false;
}
}
const defaultConfig = {
browser: {
launchOptions: {},
contextOptions: {}
},
timeouts: {
action: 5e3,
navigation: 6e4,
expect: 5e3
}
};
async function resolveConfig(config) {
const merged = mergeConfig(defaultConfig, config);
const browser = await validateBrowserConfig(merged.browser);
return { ...merged, browser };
}
async function resolveCLIConfigForMCP(cliOptions, env) {
const envOverrides = configFromEnv(env);
const cliOverrides = configFromCLIOptions(cliOptions);
const configFile = cliOverrides.configFile ?? envOverrides.configFile;
const configInFile = await loadConfig(configFile);
let result = defaultConfig;
result = mergeConfig(result, configInFile);
result = mergeConfig(result, envOverrides);
result = mergeConfig(result, cliOverrides);
const browser = await validateBrowserConfig(result.browser);
if (browser.launchOptions.headless === void 0)
browser.launchOptions.headless = import_os.default.platform() === "linux" && !process.env.DISPLAY;
return { ...result, browser, configFile };
}
async function resolveCLIConfigForCLI(daemonProfilesDir, sessionName, options, env) {
const config = options.config ? import_path.default.resolve(options.config) : void 0;
try {
const defaultConfigFile = import_path.default.resolve(".playwright", "cli.config.json");
if (!config && import_fs.default.existsSync(defaultConfigFile))
options.config = defaultConfigFile;
} catch {
}
const daemonOverrides = configFromCLIOptions({
endpoint: options.endpoint,
config: options.config,
browser: options.browser,
headless: options.headed ? false : void 0,
extension: options.extension,
userDataDir: options.profile,
snapshotMode: "full"
});
const envOverrides = configFromEnv(env);
const configFile = daemonOverrides.configFile ?? envOverrides.configFile;
const configInFile = await loadConfig(configFile);
const globalConfigPath = import_path.default.join((env ?? process.env)["PWTEST_CLI_GLOBAL_CONFIG"] ?? import_os.default.homedir(), ".playwright", "cli.config.json");
const globalConfigInFile = await loadConfig(import_fs.default.existsSync(globalConfigPath) ? globalConfigPath : void 0);
let result = defaultConfig;
result = mergeConfig(result, globalConfigInFile);
result = mergeConfig(result, configInFile);
result = mergeConfig(result, envOverrides);
result = mergeConfig(result, daemonOverrides);
if (result.browser.isolated === void 0)
result.browser.isolated = !options.profile && !options.persistent && !result.browser.userDataDir && !result.browser.remoteEndpoint && !result.extension;
if (!result.extension && !result.browser.isolated && !result.browser.userDataDir && !result.browser.remoteEndpoint) {
const browserToken = result.browser.launchOptions?.channel ?? result.browser?.browserName;
const userDataDir = import_path.default.resolve(daemonProfilesDir, `ud-${sessionName}-${browserToken}`);
result.browser.userDataDir = userDataDir;
}
if (result.browser.launchOptions.headless === void 0)
result.browser.launchOptions.headless = true;
const browser = await validateBrowserConfig(result.browser);
return { ...result, browser, configFile, skillMode: true };
}
async function validateBrowserConfig(browser) {
let browserName = browser.browserName;
if (!browserName) {
browserName = "chromium";
if (browser.launchOptions.channel === void 0)
browser.launchOptions.channel = "chrome";
}
if (browser.browserName === "chromium" && browser.launchOptions.chromiumSandbox === void 0) {
if (process.platform === "linux")
browser.launchOptions.chromiumSandbox = browser.launchOptions.channel !== "chromium" && browser.launchOptions.channel !== "chrome-for-testing";
else
browser.launchOptions.chromiumSandbox = true;
}
if (browser.isolated && browser.userDataDir)
throw new Error("Browser userDataDir is not supported in isolated mode.");
if (browser.initScript) {
for (const script of browser.initScript) {
if (!await fileExistsAsync(script))
throw new Error(`Init script file does not exist: ${script}`);
}
}
if (browser.initPage) {
for (const page of browser.initPage) {
if (!await fileExistsAsync(page))
throw new Error(`Init page file does not exist: ${page}`);
}
}
if (browser.contextOptions.viewport === void 0) {
if (browser.launchOptions.headless)
browser.contextOptions.viewport = { width: 1280, height: 720 };
else
browser.contextOptions.viewport = null;
}
return { ...browser, browserName };
}
function configFromCLIOptions(cliOptions) {
let browserName;
let channel;
switch (cliOptions.browser) {
case "chrome":
case "chrome-beta":
case "chrome-canary":
case "chrome-dev":
case "msedge":
case "msedge-beta":
case "msedge-canary":
case "msedge-dev":
browserName = "chromium";
channel = cliOptions.browser;
break;
case "chromium":
browserName = "chromium";
channel = "chrome-for-testing";
break;
case "firefox":
browserName = "firefox";
break;
case "webkit":
browserName = "webkit";
break;
}
const launchOptions = {
channel,
executablePath: cliOptions.executablePath,
headless: cliOptions.headless
};
if (cliOptions.sandbox !== void 0)
launchOptions.chromiumSandbox = cliOptions.sandbox;
if (cliOptions.proxyServer) {
launchOptions.proxy = {
server: cliOptions.proxyServer
};
if (cliOptions.proxyBypass)
launchOptions.proxy.bypass = cliOptions.proxyBypass;
}
if (cliOptions.device && cliOptions.cdpEndpoint)
throw new Error("Device emulation is not supported with cdpEndpoint.");
const contextOptions = cliOptions.device ? import__.devices[cliOptions.device] : {};
if (cliOptions.storageState)
contextOptions.storageState = cliOptions.storageState;
if (cliOptions.userAgent)
contextOptions.userAgent = cliOptions.userAgent;
if (cliOptions.viewportSize)
contextOptions.viewport = cliOptions.viewportSize;
if (cliOptions.ignoreHttpsErrors)
contextOptions.ignoreHTTPSErrors = true;
if (cliOptions.blockServiceWorkers)
contextOptions.serviceWorkers = "block";
if (cliOptions.grantPermissions)
contextOptions.permissions = cliOptions.grantPermissions;
const config = {
browser: {
browserName,
isolated: cliOptions.isolated,
userDataDir: cliOptions.userDataDir,
launchOptions,
contextOptions,
cdpEndpoint: cliOptions.cdpEndpoint,
cdpHeaders: cliOptions.cdpHeader,
cdpTimeout: cliOptions.cdpTimeout,
initPage: cliOptions.initPage,
initScript: cliOptions.initScript,
remoteEndpoint: cliOptions.endpoint
},
extension: cliOptions.extension,
server: {
port: cliOptions.port,
host: cliOptions.host,
allowedHosts: cliOptions.allowedHosts
},
capabilities: cliOptions.caps,
console: {
level: cliOptions.consoleLevel
},
network: {
allowedOrigins: cliOptions.allowedOrigins,
blockedOrigins: cliOptions.blockedOrigins
},
allowUnrestrictedFileAccess: cliOptions.allowUnrestrictedFileAccess,
codegen: cliOptions.codegen,
saveSession: cliOptions.saveSession,
secrets: cliOptions.secrets,
sharedBrowserContext: cliOptions.sharedBrowserContext,
snapshot: cliOptions.snapshotMode ? { mode: cliOptions.snapshotMode } : void 0,
outputDir: cliOptions.outputDir,
imageResponses: cliOptions.imageResponses,
testIdAttribute: cliOptions.testIdAttribute,
timeouts: {
action: cliOptions.timeoutAction,
navigation: cliOptions.timeoutNavigation
}
};
return { ...config, configFile: cliOptions.config };
}
function configFromEnv(env) {
const e = env ?? process.env;
const options = {};
options.allowedHosts = commaSeparatedList(e.PLAYWRIGHT_MCP_ALLOWED_HOSTS);
options.allowedOrigins = semicolonSeparatedList(e.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
options.allowUnrestrictedFileAccess = envToBoolean(e.PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS);
options.blockedOrigins = semicolonSeparatedList(e.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
options.blockServiceWorkers = envToBoolean(e.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
options.browser = envToString(e.PLAYWRIGHT_MCP_BROWSER);
options.caps = commaSeparatedList(e.PLAYWRIGHT_MCP_CAPS);
options.cdpEndpoint = envToString(e.PLAYWRIGHT_MCP_CDP_ENDPOINT);
options.cdpHeader = headerParser(envToString(e.PLAYWRIGHT_MCP_CDP_HEADERS));
options.cdpTimeout = numberParser(e.PLAYWRIGHT_MCP_CDP_TIMEOUT);
options.config = envToString(e.PLAYWRIGHT_MCP_CONFIG);
if (e.PLAYWRIGHT_MCP_CONSOLE_LEVEL)
options.consoleLevel = enumParser("--console-level", ["error", "warning", "info", "debug"], e.PLAYWRIGHT_MCP_CONSOLE_LEVEL);
options.device = envToString(e.PLAYWRIGHT_MCP_DEVICE);
options.executablePath = envToString(e.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
options.extension = envToBoolean(e.PLAYWRIGHT_MCP_EXTENSION);
options.grantPermissions = commaSeparatedList(e.PLAYWRIGHT_MCP_GRANT_PERMISSIONS);
options.headless = envToBoolean(e.PLAYWRIGHT_MCP_HEADLESS);
options.host = envToString(e.PLAYWRIGHT_MCP_HOST);
options.ignoreHttpsErrors = envToBoolean(e.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
const initPage = envToString(e.PLAYWRIGHT_MCP_INIT_PAGE);
if (initPage)
options.initPage = [initPage];
const initScript = envToString(e.PLAYWRIGHT_MCP_INIT_SCRIPT);
if (initScript)
options.initScript = [initScript];
options.isolated = envToBoolean(e.PLAYWRIGHT_MCP_ISOLATED);
if (e.PLAYWRIGHT_MCP_IMAGE_RESPONSES)
options.imageResponses = enumParser("--image-responses", ["allow", "omit"], e.PLAYWRIGHT_MCP_IMAGE_RESPONSES);
options.sandbox = envToBoolean(e.PLAYWRIGHT_MCP_SANDBOX);
options.outputDir = envToString(e.PLAYWRIGHT_MCP_OUTPUT_DIR);
options.port = numberParser(e.PLAYWRIGHT_MCP_PORT);
options.proxyBypass = envToString(e.PLAYWRIGHT_MCP_PROXY_BYPASS);
options.proxyServer = envToString(e.PLAYWRIGHT_MCP_PROXY_SERVER);
options.secrets = dotenvFileLoader(e.PLAYWRIGHT_MCP_SECRETS_FILE);
options.storageState = envToString(e.PLAYWRIGHT_MCP_STORAGE_STATE);
options.testIdAttribute = envToString(e.PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE);
options.timeoutAction = numberParser(e.PLAYWRIGHT_MCP_TIMEOUT_ACTION);
options.timeoutNavigation = numberParser(e.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION);
options.userAgent = envToString(e.PLAYWRIGHT_MCP_USER_AGENT);
options.userDataDir = envToString(e.PLAYWRIGHT_MCP_USER_DATA_DIR);
options.viewportSize = resolutionParser("--viewport-size", e.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
return configFromCLIOptions(options);
}
async function loadConfig(configFile) {
if (!configFile)
return {};
if (configFile.endsWith(".ini"))
return (0, import_configIni.configFromIniFile)(configFile);
try {
const data = await import_fs.default.promises.readFile(configFile, "utf8");
return JSON.parse(data.charCodeAt(0) === 65279 ? data.slice(1) : data);
} catch {
return (0, import_configIni.configFromIniFile)(configFile);
}
}
function pickDefined(obj) {
return Object.fromEntries(
Object.entries(obj ?? {}).filter(([_, v]) => v !== void 0)
);
}
function mergeConfig(base, overrides) {
const browser = {
...pickDefined(base.browser),
...pickDefined(overrides.browser),
browserName: overrides.browser?.browserName ?? base.browser?.browserName,
isolated: overrides.browser?.isolated ?? base.browser?.isolated,
launchOptions: {
...pickDefined(base.browser?.launchOptions),
...pickDefined(overrides.browser?.launchOptions),
// Assistant mode is not a part of the public API.
...{ assistantMode: true }
},
contextOptions: {
...pickDefined(base.browser?.contextOptions),
...pickDefined(overrides.browser?.contextOptions)
}
};
if (browser.browserName !== "chromium" && browser.launchOptions)
delete browser.launchOptions.channel;
return {
...pickDefined(base),
...pickDefined(overrides),
browser,
console: {
...pickDefined(base.console),
...pickDefined(overrides.console)
},
network: {
...pickDefined(base.network),
...pickDefined(overrides.network)
},
server: {
...pickDefined(base.server),
...pickDefined(overrides.server)
},
snapshot: {
...pickDefined(base.snapshot),
...pickDefined(overrides.snapshot)
},
timeouts: {
...pickDefined(base.timeouts),
...pickDefined(overrides.timeouts)
}
};
}
function semicolonSeparatedList(value) {
if (!value)
return void 0;
return value.split(";").map((v) => v.trim());
}
function commaSeparatedList(value) {
if (!value)
return void 0;
return value.split(",").map((v) => v.trim());
}
function dotenvFileLoader(value) {
if (!value)
return void 0;
return import_utilsBundle.dotenv.parse(import_fs.default.readFileSync(value, "utf8"));
}
function numberParser(value) {
if (!value)
return void 0;
return +value;
}
function resolutionParser(name, value) {
if (!value)
return void 0;
if (value.includes("x")) {
const [width, height] = value.split("x").map((v) => +v);
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
return { width, height };
}
if (value.includes(",")) {
const [width, height] = value.split(",").map((v) => +v);
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0)
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
return { width, height };
}
throw new Error(`Invalid resolution format: use ${name}="800x600"`);
}
function headerParser(arg, previous) {
if (!arg)
return previous;
const result = { ...previous ?? {} };
const colonIndex = arg.indexOf(":");
const name = colonIndex === -1 ? arg.trim() : arg.substring(0, colonIndex).trim();
const value = colonIndex === -1 ? "" : arg.substring(colonIndex + 1).trim();
result[name] = value;
return result;
}
function enumParser(name, options, value) {
if (!options.includes(value))
throw new Error(`Invalid ${name}: ${value}. Valid values are: ${options.join(", ")}`);
return value;
}
function envToBoolean(value) {
if (value === "true" || value === "1")
return true;
if (value === "false" || value === "0")
return false;
return void 0;
}
function envToString(value) {
return value ? value.trim() : void 0;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
commaSeparatedList,
configFromEnv,
dotenvFileLoader,
enumParser,
headerParser,
loadConfig,
numberParser,
resolutionParser,
resolveCLIConfigForCLI,
resolveCLIConfigForMCP,
resolveConfig,
semicolonSeparatedList
});
@@ -0,0 +1,189 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var configIni_exports = {};
__export(configIni_exports, {
configFromIniFile: () => configFromIniFile,
configsFromIniFile: () => configsFromIniFile
});
module.exports = __toCommonJS(configIni_exports);
var import_fs = __toESM(require("fs"));
var import_utilsBundle = require("../../utilsBundle");
function configFromIniFile(filePath) {
const content = import_fs.default.readFileSync(filePath, "utf8");
const parsed = import_utilsBundle.ini.parse(content);
return iniEntriesToConfig(parsed);
}
function configsFromIniFile(filePath) {
const content = import_fs.default.readFileSync(filePath, "utf8");
const parsed = import_utilsBundle.ini.parse(content);
const result = /* @__PURE__ */ new Map();
for (const [sectionName, sectionData] of Object.entries(parsed)) {
if (typeof sectionData !== "object" || sectionData === null)
continue;
result.set(sectionName, iniEntriesToConfig(sectionData));
}
return result;
}
function iniEntriesToConfig(entries) {
const config = {};
for (const [targetPath, rawValue] of Object.entries(entries)) {
const type = longhandTypes[targetPath];
const value = type ? coerceToType(rawValue, type) : coerceIniValue(rawValue);
setNestedValue(config, targetPath, value);
}
return config;
}
function coerceToType(value, type) {
switch (type) {
case "string":
return String(value);
case "number":
return Number(value);
case "boolean":
if (typeof value === "boolean")
return value;
return value === "true" || value === "1";
case "string[]":
if (Array.isArray(value))
return value.map(String);
return [String(value)];
case "size": {
if (typeof value === "string" && value.includes("x")) {
const [w, h] = value.split("x").map(Number);
if (!isNaN(w) && !isNaN(h) && w > 0 && h > 0)
return { width: w, height: h };
}
return void 0;
}
}
}
function coerceIniValue(value) {
if (typeof value !== "string")
return value;
const trimmed = value.trim();
if (trimmed === "")
return trimmed;
const num = Number(trimmed);
if (!isNaN(num))
return num;
return value;
}
function setNestedValue(obj, dotPath, value) {
const parts = dotPath.split(".");
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!(part in current) || typeof current[part] !== "object" || current[part] === null)
current[part] = {};
current = current[part];
}
current[parts[parts.length - 1]] = value;
}
const longhandTypes = {
// browser direct
"browser.browserName": "string",
"browser.isolated": "boolean",
"browser.userDataDir": "string",
"browser.cdpEndpoint": "string",
"browser.cdpTimeout": "number",
"browser.remoteEndpoint": "string",
"browser.initPage": "string[]",
"browser.initScript": "string[]",
// browser.launchOptions
"browser.launchOptions.channel": "string",
"browser.launchOptions.headless": "boolean",
"browser.launchOptions.executablePath": "string",
"browser.launchOptions.chromiumSandbox": "boolean",
"browser.launchOptions.args": "string[]",
"browser.launchOptions.downloadsPath": "string",
"browser.launchOptions.handleSIGHUP": "boolean",
"browser.launchOptions.handleSIGINT": "boolean",
"browser.launchOptions.handleSIGTERM": "boolean",
"browser.launchOptions.slowMo": "number",
"browser.launchOptions.timeout": "number",
"browser.launchOptions.tracesDir": "string",
"browser.launchOptions.proxy.server": "string",
"browser.launchOptions.proxy.bypass": "string",
"browser.launchOptions.proxy.username": "string",
"browser.launchOptions.proxy.password": "string",
// browser.contextOptions
"browser.contextOptions.acceptDownloads": "boolean",
"browser.contextOptions.baseURL": "string",
"browser.contextOptions.bypassCSP": "boolean",
"browser.contextOptions.colorScheme": "string",
"browser.contextOptions.contrast": "string",
"browser.contextOptions.deviceScaleFactor": "number",
"browser.contextOptions.forcedColors": "string",
"browser.contextOptions.hasTouch": "boolean",
"browser.contextOptions.ignoreHTTPSErrors": "boolean",
"browser.contextOptions.isMobile": "boolean",
"browser.contextOptions.javaScriptEnabled": "boolean",
"browser.contextOptions.locale": "string",
"browser.contextOptions.offline": "boolean",
"browser.contextOptions.permissions": "string[]",
"browser.contextOptions.reducedMotion": "string",
"browser.contextOptions.screen": "size",
"browser.contextOptions.serviceWorkers": "string",
"browser.contextOptions.storageState": "string",
"browser.contextOptions.strictSelectors": "boolean",
"browser.contextOptions.timezoneId": "string",
"browser.contextOptions.userAgent": "string",
"browser.contextOptions.viewport": "size",
// top-level
"extension": "boolean",
"capabilities": "string[]",
"saveSession": "boolean",
"saveTrace": "boolean",
"saveVideo": "size",
"sharedBrowserContext": "boolean",
"outputDir": "string",
"imageResponses": "string",
"allowUnrestrictedFileAccess": "boolean",
"codegen": "string",
"testIdAttribute": "string",
// server
"server.port": "number",
"server.host": "string",
"server.allowedHosts": "string[]",
// console
"console.level": "string",
// network
"network.allowedOrigins": "string[]",
"network.blockedOrigins": "string[]",
// timeouts
"timeouts.action": "number",
"timeouts.navigation": "number",
// snapshot
"snapshot.mode": "string"
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
configFromIniFile,
configsFromIniFile
});
@@ -0,0 +1,55 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var extensionContextFactory_exports = {};
__export(extensionContextFactory_exports, {
createExtensionBrowser: () => createExtensionBrowser
});
module.exports = __toCommonJS(extensionContextFactory_exports);
var playwright = __toESM(require("../../.."));
var import_utilsBundle = require("../../utilsBundle");
var import_network = require("../../server/utils/network");
var import_cdpRelay = require("./cdpRelay");
const debugLogger = (0, import_utilsBundle.debug)("pw:mcp:relay");
async function createExtensionBrowser(config, clientInfo) {
const httpServer = (0, import_network.createHttpServer)();
await (0, import_network.startHttpServer)(httpServer, {});
const relay = new import_cdpRelay.CDPRelayServer(
httpServer,
config.browser.launchOptions.channel || "chrome",
config.browser.userDataDir,
config.browser.launchOptions.executablePath
);
debugLogger(`CDP relay server started, extension endpoint: ${relay.extensionEndpoint()}.`);
await relay.ensureExtensionConnectionForMCPContext(clientInfo);
return await playwright.chromium.connectOverCDP(relay.cdpEndpoint(), { isLocal: true });
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createExtensionBrowser
});
@@ -0,0 +1,62 @@
"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 mcp_exports = {};
__export(mcp_exports, {
createConnection: () => createConnection
});
module.exports = __toCommonJS(mcp_exports);
var import_config = require("./config");
var import_tools = require("../backend/tools");
var import_browserFactory = require("./browserFactory");
var import_browserBackend = require("../backend/browserBackend");
var import_server = require("../utils/mcp/server");
const packageJSON = require("../../../package.json");
async function createConnection(userConfig = {}, contextGetter) {
const config = await (0, import_config.resolveConfig)(userConfig);
const tools = (0, import_tools.filteredTools)(config);
const backendFactory = {
name: "api",
nameInConfig: "api",
version: packageJSON.version,
toolSchemas: tools.map((tool) => tool.schema),
create: async (clientInfo) => {
const browser = contextGetter ? new SimpleBrowser(await contextGetter()) : await (0, import_browserFactory.createBrowser)(config, clientInfo);
const context = config.browser.isolated ? await browser.newContext(config.browser.contextOptions) : browser.contexts()[0];
return new import_browserBackend.BrowserBackend(config, context, tools);
},
disposed: async () => {
}
};
return (0, import_server.createServer)("api", packageJSON.version, backendFactory, false);
}
class SimpleBrowser {
constructor(context) {
this._context = context;
}
contexts() {
return [this._context];
}
async newContext() {
throw new Error("Creating a new context is not supported in SimpleBrowserContextFactory.");
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createConnection
});
@@ -0,0 +1,35 @@
"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 log_exports = {};
__export(log_exports, {
logUnhandledError: () => logUnhandledError,
testDebug: () => testDebug
});
module.exports = __toCommonJS(log_exports);
var import_utilsBundle = require("../../utilsBundle");
const errorDebug = (0, import_utilsBundle.debug)("pw:mcp:error");
function logUnhandledError(error) {
errorDebug(error);
}
const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
logUnhandledError,
testDebug
});

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