├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── labeler.yml ├── release.yml └── workflows │ ├── auto-labeler.yml │ ├── build-docs.yml │ ├── crowdin-sync-translations.yml │ ├── crowdin-update-resources.yml │ ├── main-commit-validation.yml │ ├── package.yml │ ├── pr-title.yml │ ├── pr-validation-lint.yml │ ├── pr-validation.yml │ └── publish-docs.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── app ├── common │ ├── index.html │ ├── public │ │ └── locales │ │ │ ├── ar │ │ │ └── translation.json │ │ │ ├── da │ │ │ └── translation.json │ │ │ ├── de │ │ │ └── translation.json │ │ │ ├── en │ │ │ └── translation.json │ │ │ ├── es-ES │ │ │ └── translation.json │ │ │ ├── fa │ │ │ └── translation.json │ │ │ ├── fi │ │ │ └── translation.json │ │ │ ├── fr │ │ │ └── translation.json │ │ │ ├── hi │ │ │ └── translation.json │ │ │ ├── hu │ │ │ └── translation.json │ │ │ ├── it │ │ │ └── translation.json │ │ │ ├── ja │ │ │ └── translation.json │ │ │ ├── kn │ │ │ └── translation.json │ │ │ ├── ko │ │ │ └── translation.json │ │ │ ├── lt │ │ │ └── translation.json │ │ │ ├── ml-IN │ │ │ └── translation.json │ │ │ ├── nl │ │ │ └── translation.json │ │ │ ├── no │ │ │ └── translation.json │ │ │ ├── pa-IN │ │ │ └── translation.json │ │ │ ├── pl │ │ │ └── translation.json │ │ │ ├── pt-BR │ │ │ └── translation.json │ │ │ ├── pt-PT │ │ │ └── translation.json │ │ │ ├── ru │ │ │ └── translation.json │ │ │ ├── sv-SE │ │ │ └── translation.json │ │ │ ├── te │ │ │ └── translation.json │ │ │ ├── tr │ │ │ └── translation.json │ │ │ ├── uk │ │ │ └── translation.json │ │ │ ├── zh-CN │ │ │ └── translation.json │ │ │ └── zh-TW │ │ │ └── translation.json │ ├── renderer │ │ ├── Root.jsx │ │ ├── actions │ │ │ ├── Inspector.js │ │ │ ├── Session.js │ │ │ └── index.js │ │ ├── assets │ │ │ ├── images │ │ │ │ ├── bitbar_logo.svg │ │ │ │ ├── browserstack_logo.svg │ │ │ │ ├── browserstack_logo_dark.svg │ │ │ │ ├── experitest_logo.svg │ │ │ │ ├── headspin_logo.svg │ │ │ │ ├── icon.png │ │ │ │ ├── kobiton_logo.svg │ │ │ │ ├── kobiton_logo_dark.svg │ │ │ │ ├── lambdatest_logo.svg │ │ │ │ ├── loader.svg │ │ │ │ ├── mobitru_logo.svg │ │ │ │ ├── pcloudy_logo.svg │ │ │ │ ├── pcloudy_logo_dark.svg │ │ │ │ ├── perfecto_logo.svg │ │ │ │ ├── remotetestkit_logo.svg │ │ │ │ ├── robotqa_logo.svg │ │ │ │ ├── robotqa_logo_dark.svg │ │ │ │ ├── sauce_icon.svg │ │ │ │ ├── sauce_logo.svg │ │ │ │ ├── sauce_logo_dark.svg │ │ │ │ ├── testcribe_logo.svg │ │ │ │ ├── testcribe_logo_dark.svg │ │ │ │ ├── testingbot_logo.svg │ │ │ │ ├── tvlabs_logo.svg │ │ │ │ └── tvlabs_logo_dark.svg │ │ │ └── stylesheets │ │ │ │ ├── main.css │ │ │ │ └── splash.css │ │ ├── components │ │ │ ├── ErrorBoundary │ │ │ │ ├── ErrorBoundary.jsx │ │ │ │ ├── ErrorMessage.jsx │ │ │ │ └── ErrorMessage.module.css │ │ │ ├── Inspector │ │ │ │ ├── Commands.jsx │ │ │ │ ├── ElementLocator.jsx │ │ │ │ ├── FileUploader.jsx │ │ │ │ ├── GestureEditor.jsx │ │ │ │ ├── HeaderButtons.jsx │ │ │ │ ├── HighlighterCentroid.jsx │ │ │ │ ├── HighlighterRectForBounds.jsx │ │ │ │ ├── HighlighterRectForElem.jsx │ │ │ │ ├── HighlighterRects.jsx │ │ │ │ ├── Inspector.jsx │ │ │ │ ├── Inspector.module.css │ │ │ │ ├── LocatedElements.jsx │ │ │ │ ├── LocatorTestModal.jsx │ │ │ │ ├── Recorder.jsx │ │ │ │ ├── SavedGestures.jsx │ │ │ │ ├── Screenshot.jsx │ │ │ │ ├── SelectedElement.jsx │ │ │ │ ├── SessionCodeBox.jsx │ │ │ │ ├── SessionInfo.jsx │ │ │ │ ├── SiriCommandModal.jsx │ │ │ │ └── Source.jsx │ │ │ ├── Notification.jsx │ │ │ ├── Session │ │ │ │ ├── AdvancedServerParams.jsx │ │ │ │ ├── AttachToSession.jsx │ │ │ │ ├── CapabilityControl.jsx │ │ │ │ ├── CapabilityEditor.jsx │ │ │ │ ├── CloudProviderSelector.jsx │ │ │ │ ├── CloudProviders.jsx │ │ │ │ ├── FormattedCaps.jsx │ │ │ │ ├── SavedSessions.jsx │ │ │ │ ├── ServerTabBitbar.jsx │ │ │ │ ├── ServerTabBrowserstack.jsx │ │ │ │ ├── ServerTabCustom.jsx │ │ │ │ ├── ServerTabExperitest.jsx │ │ │ │ ├── ServerTabHeadspin.jsx │ │ │ │ ├── ServerTabKobiton.jsx │ │ │ │ ├── ServerTabLambdatest.jsx │ │ │ │ ├── ServerTabMobitru.jsx │ │ │ │ ├── ServerTabPcloudy.jsx │ │ │ │ ├── ServerTabPerfecto.jsx │ │ │ │ ├── ServerTabRemoteTestKit.jsx │ │ │ │ ├── ServerTabRobotQA.jsx │ │ │ │ ├── ServerTabSauce.jsx │ │ │ │ ├── ServerTabTVLabs.jsx │ │ │ │ ├── ServerTabTestcribe.jsx │ │ │ │ ├── ServerTabTestingbot.jsx │ │ │ │ ├── Session.jsx │ │ │ │ ├── Session.module.css │ │ │ │ └── ToggleTheme.jsx │ │ │ └── Spinner │ │ │ │ ├── Spinner.jsx │ │ │ │ └── Spinner.module.css │ │ ├── constants │ │ │ ├── antd-types.js │ │ │ ├── commands.js │ │ │ ├── common.js │ │ │ ├── gestures.js │ │ │ ├── screenshot.js │ │ │ ├── session-builder.js │ │ │ ├── session-inspector.js │ │ │ ├── source.js │ │ │ └── webdriver.js │ │ ├── containers │ │ │ ├── InspectorPage.js │ │ │ └── SessionPage.js │ │ ├── hooks │ │ │ └── use-theme.jsx │ │ ├── i18next.js │ │ ├── index.jsx │ │ ├── lib │ │ │ ├── appium │ │ │ │ ├── inspector-driver.js │ │ │ │ ├── session-driver.js │ │ │ │ ├── session-element.js │ │ │ │ └── session-starter.js │ │ │ ├── client-frameworks │ │ │ │ ├── common.js │ │ │ │ ├── dotnet-nunit.js │ │ │ │ ├── java-common.js │ │ │ │ ├── java-junit4.js │ │ │ │ ├── java-junit5.js │ │ │ │ ├── js-oxygen.js │ │ │ │ ├── js-wdio.js │ │ │ │ ├── map.js │ │ │ │ ├── python.js │ │ │ │ ├── robot.js │ │ │ │ └── ruby.js │ │ │ └── vendor │ │ │ │ ├── base.js │ │ │ │ ├── bitbar.js │ │ │ │ ├── browserstack.js │ │ │ │ ├── experitest.js │ │ │ │ ├── headspin.js │ │ │ │ ├── kobiton.js │ │ │ │ ├── lambdatest.js │ │ │ │ ├── local.js │ │ │ │ ├── map.js │ │ │ │ ├── mobitru.js │ │ │ │ ├── pcloudy.js │ │ │ │ ├── perfecto.js │ │ │ │ ├── remote.js │ │ │ │ ├── remotetestkit.js │ │ │ │ ├── robotqa.js │ │ │ │ ├── saucelabs.js │ │ │ │ ├── testcribe.js │ │ │ │ ├── testingbot.js │ │ │ │ └── tvlabs.js │ │ ├── polyfills.js │ │ ├── providers │ │ │ └── ThemeProvider.jsx │ │ ├── reducers │ │ │ ├── Inspector.js │ │ │ ├── Session.js │ │ │ └── index.js │ │ ├── store.js │ │ └── utils │ │ │ ├── attaching-to-session.js │ │ │ ├── file-handling.js │ │ │ ├── highlight-theme.js │ │ │ ├── locator-generation.js │ │ │ ├── logger.js │ │ │ ├── notification.js │ │ │ ├── other.js │ │ │ ├── source-parsing.js │ │ │ └── webview.js │ ├── shared │ │ ├── i18next.config.js │ │ └── setting-defs.js │ └── splash.html ├── electron │ ├── main │ │ ├── debug.js │ │ ├── helpers.js │ │ ├── i18next.js │ │ ├── main.js │ │ ├── menus.js │ │ ├── updater.js │ │ └── windows.js │ ├── preload │ │ └── preload.mjs │ └── renderer │ │ └── polyfills.js └── web │ └── polyfills.js ├── build ├── entitlements.mac.plist ├── icon.icns └── icon.ico ├── docs ├── .prettierrc.json ├── assets │ ├── images │ │ ├── icon.png │ │ ├── menu-bar-macos.png │ │ ├── session-builder.png │ │ └── session-inspector.png │ └── stylesheets │ │ └── extra.css ├── contributing.md ├── index.md ├── menu-bar.md ├── overrides │ └── partials │ │ └── toc-item.html ├── overview.md ├── quickstart │ ├── assets │ │ └── images │ │ │ ├── mac-ctrl-click.png │ │ │ ├── open-warning-macos.png │ │ │ ├── open-warning-sequoia.png │ │ │ └── open-warning-windows.png │ ├── index.md │ ├── installation.md │ ├── requirements.md │ └── starting-a-session.md ├── session-builder │ ├── app-settings.md │ ├── assets │ │ └── images │ │ │ ├── attach-to-session │ │ │ ├── attach-to-session.png │ │ │ └── found-sessions.png │ │ │ ├── capability-builder │ │ │ ├── capability-builder-footer.png │ │ │ ├── capability-builder.png │ │ │ ├── capability-fields.png │ │ │ ├── capability-json-editor.png │ │ │ └── capability-json.png │ │ │ ├── empty-session-builder.png │ │ │ ├── saved-capability-sets │ │ │ ├── saved-caps-name-editor.png │ │ │ ├── saved-caps-set-list.png │ │ │ └── saved-caps-sets.png │ │ │ ├── server-details │ │ │ ├── advanced-settings.png │ │ │ ├── cloud-providers.png │ │ │ ├── default-server-details.png │ │ │ ├── lambdatest-details.png │ │ │ └── server-configuration.png │ │ │ └── theme-selector.png │ ├── attach-to-session.md │ ├── capability-builder.md │ ├── index.md │ ├── saved-capability-sets.md │ └── server-details.md ├── session-inspector │ ├── assets │ │ └── images │ │ │ ├── commands │ │ │ ├── command-params.png │ │ │ ├── command-result.png │ │ │ ├── commands-tab.png │ │ │ └── opened-category.png │ │ │ ├── gestures │ │ │ ├── gesture-editor-actions.png │ │ │ ├── gesture-editor-header.png │ │ │ ├── gesture-editor-pointers.png │ │ │ ├── gesture-timeline-empty.png │ │ │ ├── gesture-timeline-full.png │ │ │ ├── gestures-tab.png │ │ │ ├── move-action.png │ │ │ ├── new-gesture-builder.png │ │ │ ├── pause-action.png │ │ │ ├── pointer-action-visualization.png │ │ │ ├── pointer-down-action.png │ │ │ └── two-pointers.png │ │ │ ├── header │ │ │ ├── app-header.png │ │ │ ├── context-group.png │ │ │ ├── multiple-contexts.png │ │ │ ├── no-additional-contexts.png │ │ │ ├── quit-button.png │ │ │ ├── record-start-button.png │ │ │ ├── record-stop-button.png │ │ │ ├── refresh-button.png │ │ │ ├── refresh-source-pause.png │ │ │ ├── refresh-source-resume.png │ │ │ ├── search-button.png │ │ │ ├── search-inputs.png │ │ │ ├── search-results.png │ │ │ ├── search-reveal-element.png │ │ │ ├── search-send-clear-element-text.png │ │ │ ├── search-tap-element.png │ │ │ ├── system-buttons-android.png │ │ │ └── system-buttons-ios.png │ │ │ ├── recorder │ │ │ ├── recorder-tab-buttons.png │ │ │ ├── recorder-tab-empty.png │ │ │ ├── recorder-tab-filled.png │ │ │ └── recorder-tab-language.png │ │ │ ├── screenshot │ │ │ ├── app-screenshot-highlighters.png │ │ │ ├── app-screenshot-landscape.png │ │ │ ├── app-screenshot.png │ │ │ ├── download-screenshot-button.png │ │ │ ├── expanded-group-handle.png │ │ │ ├── interaction-mode-buttons.png │ │ │ └── toggle-element-handles-button.png │ │ │ ├── session-info │ │ │ ├── sesion-overall-info.png │ │ │ ├── session-boilerplate.png │ │ │ └── session-info-tab.png │ │ │ └── source │ │ │ ├── app-source-expanded.png │ │ │ ├── app-source.png │ │ │ ├── copy-attributes.png │ │ │ ├── copy-xml-button.png │ │ │ ├── download-elem-screenshot.png │ │ │ ├── download-xml-button.png │ │ │ ├── get-timings.png │ │ │ ├── selected-element.png │ │ │ ├── source-tab.png │ │ │ ├── timing-values.png │ │ │ └── toggle-attributes-button.png │ ├── commands.md │ ├── gestures.md │ ├── header.md │ ├── index.md │ ├── recorder.md │ ├── screenshot.md │ ├── session-info.md │ └── source.md └── troubleshooting.md ├── electron-builder.json ├── electron.vite.config.mjs ├── eslint.config.mjs ├── mkdocs.yml ├── package-lock.json ├── package.json ├── plugins ├── README.md ├── index.mjs └── package.json ├── renovate.json ├── sample-session-files ├── corrupted.appiumsession ├── fake-app.xml ├── fake.appiumsession └── sample.appiumsession ├── scripts ├── crowdin-common.mjs ├── crowdin-sync-translations.mjs ├── crowdin-update-resources.mjs └── sync-plugin.mjs ├── test ├── e2e │ ├── inspector-e2e.test.js │ ├── pages │ │ ├── base-page-object.js │ │ ├── inspector-page-object.js │ │ └── utils.js │ └── pre-e2e.test.js ├── integration │ └── inspectordriver-actions.test.js └── unit │ ├── mocks │ ├── appium.page.original.html │ └── appium.page.parsed.html │ ├── utils-attaching-to-session.spec.js │ ├── utils-file-handling.spec.js │ ├── utils-locator-generation.spec.js │ ├── utils-other.spec.js │ ├── utils-source-parsing.spec.js │ └── utils-webview.spec.js ├── tsconfig.json └── vite.config.mjs /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Create a new bug report 3 | title: 'bug: ' 4 | labels: [bug] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Is this an issue specifically with Appium Inspector? 9 | description: Appium Inspector is a wrapper around Appium. If you are having trouble running tests, it is much more likely that your problem is caused by Appium, not Appium Inspector, and should be reported in [the Appium repository](https://github.com/appium/appium/issues) instead. 10 | options: 11 | - label: I have verified that my issue does _not_ occur with Appium, and should be investigated as an Appium Inspector issue 12 | required: true 13 | - type: checkboxes 14 | attributes: 15 | label: Is there an existing issue for this? 16 | description: 'Please [search :mag: the issues](https://github.com/appium/appium-inspector/issues) to check if this bug has already been reported.' 17 | options: 18 | - label: I have searched the existing issues 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Current Behavior 23 | description: Describe the problem you are experiencing. Screenshots are welcome. 24 | validations: 25 | required: true 26 | - type: textarea 27 | attributes: 28 | label: Expected Behavior 29 | description: Describe what you expect to happen instead. 30 | validations: 31 | required: true 32 | - type: dropdown 33 | attributes: 34 | label: Operating System 35 | description: What operating system are you using? 36 | options: 37 | - Mac 38 | - Windows 39 | - Linux 40 | validations: 41 | required: true 42 | - type: input 43 | attributes: 44 | label: Appium Inspector Version 45 | description: What version of Appium Inspector are you using? 46 | validations: 47 | required: true 48 | - type: input 49 | attributes: 50 | label: Appium Version 51 | description: | 52 | What version of Appium are you using? 53 | Note that this may not be relevant, depending on your issue. 54 | placeholder: Output of `appium --version` 55 | validations: 56 | required: false 57 | - type: textarea 58 | attributes: 59 | label: Further Information 60 | description: | 61 | Add Appium logs, links, references, or anything else that will give us more context about the issue you are encountering! 62 | _Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in._ 63 | validations: 64 | required: false 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: discuss.appium.io 4 | url: https://discuss.appium.io/ 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature Request 2 | description: Suggest an idea for this project 3 | title: 'feature request: <title>' 4 | labels: [enhancement] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Current Behavior 9 | description: Describe the problem you are experiencing. Screenshots are welcome. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Suggested Solution 15 | description: Describe what you would like to happen instead. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Additional Information 21 | description: Add any other useful context or information. 22 | validations: 23 | required: false 24 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | version: v1 2 | 3 | labels: 4 | - label: 'enhancement' 5 | matcher: 6 | title: '^feat.*?:' 7 | 8 | - label: 'fix' 9 | matcher: 10 | title: '^fix.*?:' 11 | 12 | - label: 'documentation' 13 | matcher: 14 | title: '^docs.*?:' 15 | files: ['README.md', 'docs/**'] 16 | 17 | - label: 'chore' 18 | matcher: 19 | title: '^chore.*?:' 20 | 21 | - label: 'dependencies' 22 | matcher: 23 | files: ['package-lock.json'] 24 | 25 | - label: 'i18n' 26 | matcher: 27 | files: ['app/common/public/locales/**'] 28 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # Configuration for automatically generated release notes 2 | 3 | changelog: 4 | exclude: 5 | labels: 6 | - dependencies 7 | categories: 8 | - title: 🚀 New Features 9 | labels: 10 | - enhancement 11 | - title: 🛠 Fixes 12 | labels: 13 | - fix 14 | - title: 📖 Documentation 15 | labels: 16 | - documentation 17 | - title: 🌐 Localization 18 | labels: 19 | - i18n 20 | - title: 🔍 Other Changes 21 | labels: 22 | - '*' 23 | -------------------------------------------------------------------------------- /.github/workflows/auto-labeler.yml: -------------------------------------------------------------------------------- 1 | name: Labeler 2 | on: 3 | pull_request_target: 4 | types: [opened, synchronize] 5 | branches: [main] 6 | 7 | permissions: read-all 8 | 9 | jobs: 10 | labeler: 11 | permissions: 12 | pull-requests: write 13 | name: Auto-Label PRs 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: fuxingloh/multi-labeler@v4 17 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | # Builds the Appium Inspector MkDocs documentation 2 | # Executed on pull request if documentation-related files are changed 3 | 4 | name: Build Docs 5 | 6 | on: 7 | pull_request: 8 | branches: [main] 9 | paths: 10 | - 'docs/**' 11 | - 'mkdocs.yml' 12 | - 'tsconfig.json' 13 | - 'package*.json' 14 | - '.github/workflows/build-docs.yml' # this file 15 | 16 | jobs: 17 | docs: 18 | name: Build Docs 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js LTS 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | cache: 'npm' 27 | - name: Install dependencies (Node.js) 28 | run: npm install 29 | - name: Install dependencies (Python) 30 | run: npm run install-docs-deps 31 | - name: Build Docs 32 | run: npm run build:docs 33 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-sync-translations.yml: -------------------------------------------------------------------------------- 1 | # Retrieves non-English translations from Crowdin and creates a PR with new changes 2 | 3 | name: Sync Crowdin Translations 4 | 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: 0 0 * * 0 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js LTS 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: lts/* 20 | cache: 'npm' 21 | - name: Install Dependencies 22 | run: npm ci 23 | - name: Crowdin Sync 24 | run: npm run crowdin-sync 25 | env: 26 | # appium-desktop 27 | CROWDIN_PROJECT_ID: 346705 28 | CROWDIN_TOKEN: ${{ secrets.CROWDIN_TOKEN }} 29 | - name: Create Pull Request 30 | uses: peter-evans/create-pull-request@v7.0.8 31 | with: 32 | token: ${{ github.token }} 33 | commit-message: 'chore: Update translations' 34 | title: 'chore: Update translations' 35 | labels: i18n 36 | branch: crowdin-sync-${{ github.run_id }} 37 | body: 'Update Crowdin Translations: https://crowdin.com/project/appium-desktop' 38 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-update-resources.yml: -------------------------------------------------------------------------------- 1 | # Updates Crowdin with any changes in the English translation file (/app/common/public/locales/en/translation.json) 2 | 3 | name: Update Crowdin English Resources 4 | 5 | on: 6 | push: 7 | branches: [main] 8 | paths: 9 | - 'app/common/public/locales/en/translation.json' 10 | - '.github/workflows/crowdin-update-resources.yml' # this file 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js LTS 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: lts/* 22 | cache: 'npm' 23 | - name: Install Dependencies 24 | run: npm ci 25 | - name: Crowdin Update 26 | run: npm run crowdin-update 27 | env: 28 | # appium-desktop 29 | CROWDIN_PROJECT_ID: 346705 30 | CROWDIN_TOKEN: ${{ secrets.CROWDIN_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/main-commit-validation.yml: -------------------------------------------------------------------------------- 1 | # Code validation job, executed on new commit on main branch 2 | # Builds the app, and runs lint, unit and integration tests 3 | 4 | name: Commit Validation 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | paths-ignore: 10 | - '.github/ISSUE_TEMPLATE/**' 11 | - 'app/common/public/locales/**' 12 | - 'docs/**' 13 | - 'sample-session-files/**' 14 | - '.gitignore' 15 | - 'LICENSE' 16 | - 'README.md' 17 | - 'renovate.json' 18 | 19 | jobs: 20 | build: 21 | name: Build & Test 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Use Node.js LTS 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: lts/* 30 | cache: 'npm' 31 | - name: Install Dependencies 32 | run: npm ci 33 | - name: Build 34 | run: npm run build --if-present 35 | - name: Test 36 | run: npm test 37 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: Create packages 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | issues: write 13 | 14 | jobs: 15 | electron: 16 | strategy: 17 | matrix: 18 | image: [ubuntu-latest, macos-latest, windows-latest] 19 | runs-on: ${{ matrix.image }} 20 | 21 | env: 22 | CSC_IDENTITY_AUTO_DISCOVERY: true 23 | CSC_LINK: ${{ secrets.CSC_LINK }} 24 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} 25 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Use Node.js LTS 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: lts/* 33 | cache: 'npm' 34 | - name: Install dependencies (Node.js) 35 | run: npm ci 36 | - name: Build electron app 37 | run: npm run build:electron 38 | - name: build package 39 | run: npx electron-builder build --x64 --arm64 --publish always 40 | - name: Upload built packages 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: artifact-${{ matrix.image }} 44 | path: release/ 45 | 46 | plugin: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - name: Use Node.js LTS 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version: lts/* 54 | cache: 'npm' 55 | - name: Authenticate with npm registry 56 | run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc 57 | env: 58 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | - name: Install dependencies (Node.js) 60 | run: npm ci 61 | - name: generate contents 62 | run: npm run build:plugin 63 | - name: publish 64 | run: npm publish 65 | working-directory: plugins 66 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | # Checks that the PR title follows conventional commit standards 2 | 3 | name: Conventional Commits 4 | on: 5 | pull_request: 6 | types: [opened, edited, synchronize, reopened] 7 | 8 | jobs: 9 | conventional: 10 | name: PR Title 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: beemojs/conventional-pr-action@v3 14 | with: 15 | config-preset: angular 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/pr-validation-lint.yml: -------------------------------------------------------------------------------- 1 | # Code validation job, executed on pull request 2 | # Runs linting and formatting tests 3 | 4 | name: PR Validation 5 | 6 | on: 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | lint: 12 | name: Lint & Format 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js LTS 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: lts/* 21 | cache: 'npm' 22 | - name: Install Dependencies 23 | run: npm ci 24 | - name: Lint 25 | run: npm run test:lint 26 | - name: Format 27 | run: npm run test:format 28 | -------------------------------------------------------------------------------- /.github/workflows/pr-validation.yml: -------------------------------------------------------------------------------- 1 | # Code validation job, executed on pull request 2 | # Builds the app, and runs unit and integration tests 3 | 4 | name: PR Validation 5 | 6 | on: 7 | pull_request: 8 | branches: [main] 9 | paths-ignore: 10 | - '.github/ISSUE_TEMPLATE/**' 11 | - 'app/common/public/locales/**' 12 | - 'docs/**' 13 | - 'sample-session-files/**' 14 | - '.gitignore' 15 | - 'LICENSE' 16 | - 'README.md' 17 | - 'renovate.json' 18 | 19 | jobs: 20 | build: 21 | name: Build & Test 22 | strategy: 23 | matrix: 24 | image: [ubuntu-latest, macos-latest, windows-latest] 25 | runs-on: ${{ matrix.image }} 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Use Node.js LTS 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: lts/* 33 | cache: 'npm' 34 | - name: Install Dependencies 35 | run: npm ci 36 | - name: Build 37 | run: npm run build --if-present 38 | - name: Test 39 | run: npm run test:unit && npm run test:integration 40 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | # Builds and publishes the Appium Inspector MkDocs documentation 2 | 3 | name: Publish Docs 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use GH Actions credentials 14 | run: | 15 | git config user.name github-actions 16 | git config user.email github-actions@github.com 17 | - name: Use Node.js LTS 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: lts/* 21 | cache: 'npm' 22 | - name: Install dependencies (Node.js) 23 | run: npm install 24 | - name: Install dependencies (Python) 25 | run: npm run install-docs-deps 26 | - name: Build and deploy docs 27 | run: npm run publish:docs 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | dist 4 | dist-browser 5 | release 6 | site 7 | main.map 8 | 9 | *.log 10 | .DS_Store 11 | 12 | Certificates.p12 13 | .cache/ 14 | .idea/ 15 | .settings/ 16 | .project 17 | .history 18 | junit-test-results.xml 19 | junit-testresults.xml 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Prettier ignore extends .gitignore 2 | **/*.log 3 | **/*.xml 4 | **/*.html 5 | !app/**/*.html 6 | -------------------------------------------------------------------------------- /app/common/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <title>Appium Inspector 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/common/renderer/Root.jsx: -------------------------------------------------------------------------------- 1 | import {Suspense} from 'react'; 2 | import {Provider} from 'react-redux'; 3 | import {MemoryRouter, Route, Routes} from 'react-router'; 4 | 5 | import Spinner from './components/Spinner/Spinner.jsx'; 6 | import InspectorPage from './containers/InspectorPage'; 7 | import SessionPage from './containers/SessionPage'; 8 | import i18n from './i18next'; 9 | import {ipcRenderer} from './polyfills'; 10 | import {ThemeProvider} from './providers/ThemeProvider'; 11 | 12 | ipcRenderer.on('appium-language-changed', (event, message) => { 13 | if (i18n.language !== message.language) { 14 | i18n.changeLanguage(message.language); 15 | } 16 | }); 17 | 18 | const Root = ({store}) => ( 19 | 20 | 21 | 22 | }> 23 | 24 | } /> 25 | } /> 26 | } /> 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | 34 | export default Root; 35 | -------------------------------------------------------------------------------- /app/common/renderer/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as inspectorActions from './Inspector'; 2 | import * as sessionActions from './Session'; 3 | 4 | export default { 5 | ...inspectorActions, 6 | ...sessionActions, 7 | }; 8 | -------------------------------------------------------------------------------- /app/common/renderer/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/app/common/renderer/assets/images/icon.png -------------------------------------------------------------------------------- /app/common/renderer/assets/images/sauce_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/common/renderer/assets/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | @import 'antd/dist/reset.css'; 2 | 3 | body.dark { 4 | color-scheme: dark; 5 | } 6 | 7 | body { 8 | min-height: 610px; 9 | min-width: 870px; 10 | color: #222 !important; 11 | box-sizing: border-box; 12 | } 13 | 14 | body::-webkit-scrollbar { 15 | width: 0px; 16 | background: transparent; 17 | } 18 | 19 | body::-webkit-scrollbar-corner { 20 | background: transparent; 21 | } 22 | 23 | #root, 24 | .ant-app, 25 | .ant-layout { 26 | height: 100%; 27 | } 28 | 29 | #root .ant-spin-nested-loading > div > .ant-spin { 30 | max-height: initial; 31 | } 32 | 33 | .window { 34 | background-color: #cde4f5 !important; 35 | width: 100%; 36 | height: 100%; 37 | } 38 | 39 | .list-group-item.active { 40 | background-color: #662d91 !important; 41 | } 42 | 43 | .ant-spin-nested-loading, 44 | .ant-spin-container { 45 | height: 100%; 46 | } 47 | 48 | .ant-input-group .ant-select { 49 | height: 100%; 50 | } 51 | 52 | .ant-input-group .ant-select-selection { 53 | border-top-left-radius: 0; 54 | border-bottom-left-radius: 0; 55 | height: 100%; 56 | padding-top: 0; 57 | } 58 | 59 | .ant-input-group .ant-select-selection .ant-select-selection-selected-value { 60 | padding-top: 2px; 61 | } 62 | 63 | .ant-input-group .ant-input-group-addon { 64 | padding-left: 8px; 65 | } 66 | 67 | .ant-input-group .select-container { 68 | height: 32px; 69 | } 70 | 71 | .ant-btn-icon { 72 | font-size: 14px; 73 | display: flex; 74 | justify-content: center; 75 | } 76 | 77 | .anticon { 78 | font-size: 14px !important; 79 | } 80 | 81 | .ant-switch .anticon { 82 | font-size: 12px !important; 83 | } 84 | 85 | .ant-steps-icon .anticon { 86 | font-size: 20px !important; 87 | } 88 | 89 | .ant-switch-inner-checked, 90 | .ant-switch-inner-unchecked { 91 | font-size: 12px !important; 92 | } 93 | -------------------------------------------------------------------------------- /app/common/renderer/assets/stylesheets/splash.css: -------------------------------------------------------------------------------- 1 | #root { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | } 7 | 8 | #splashImage { 9 | height: 150px; 10 | width: 150px; 11 | margin-top: 35px; 12 | } 13 | 14 | #loader { 15 | height: 50px; 16 | width: 50px; 17 | margin-top: 20px; 18 | } 19 | -------------------------------------------------------------------------------- /app/common/renderer/components/ErrorBoundary/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import {Component} from 'react'; 2 | 3 | import {copyToClipboard} from '../../polyfills'; 4 | import ErrorMessage from './ErrorMessage.jsx'; 5 | 6 | const copyTrace = (trace) => { 7 | copyToClipboard(trace); 8 | }; 9 | 10 | export default class ErrorBoundary extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | error: null, 15 | }; 16 | } 17 | 18 | static getDerivedStateFromError(error) { 19 | // Update state so the next render will show the fallback UI. 20 | return {error}; 21 | } 22 | 23 | render() { 24 | const {error} = this.state; 25 | if (error) { 26 | return ; 27 | } 28 | return this.props.children; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/common/renderer/components/ErrorBoundary/ErrorMessage.jsx: -------------------------------------------------------------------------------- 1 | import {CopyOutlined} from '@ant-design/icons'; 2 | import {Alert, Button, Tooltip} from 'antd'; 3 | 4 | import {ALERT} from '../../constants/antd-types'; 5 | import {LINKS} from '../../constants/common'; 6 | import {withTranslation} from '../../i18next'; 7 | import {openLink} from '../../polyfills'; 8 | import styles from './ErrorMessage.module.css'; 9 | 10 | const ErrorMessage = ({error, copyTrace, t}) => ( 11 |
12 | 15 | {t('Unexpected Error:')} {error.message} 16 | 17 | } 18 | type={ALERT.ERROR} 19 | showIcon 20 | description={ 21 | <> 22 | {t('Please report this issue at:')}  23 | e.preventDefault() || openLink(LINKS.CREATE_ISSUE)}> 24 | {LINKS.CREATE_ISSUE} 25 | 26 |
27 | {t('Full error trace:')} 28 | 29 |
41 | ); 42 | 43 | export default withTranslation(ErrorMessage); 44 | -------------------------------------------------------------------------------- /app/common/renderer/components/ErrorBoundary/ErrorMessage.module.css: -------------------------------------------------------------------------------- 1 | .errorMessage { 2 | padding: 3em; 3 | } 4 | 5 | .copyTraceBtn { 6 | position: absolute; 7 | left: 24px; 8 | } 9 | -------------------------------------------------------------------------------- /app/common/renderer/components/Inspector/ElementLocator.jsx: -------------------------------------------------------------------------------- 1 | import {Alert, Input, Radio, Row, Space} from 'antd'; 2 | 3 | import {ALERT} from '../../constants/antd-types'; 4 | import {LOCATOR_STRATEGY_MAP as STRAT} from '../../constants/session-inspector'; 5 | import InspectorStyles from './Inspector.module.css'; 6 | 7 | const locatorStrategies = (automationName) => { 8 | let strategies = [STRAT.ID, STRAT.XPATH, STRAT.NAME, STRAT.CLASS_NAME, STRAT.ACCESSIBILITY_ID]; 9 | switch (automationName) { 10 | case 'xcuitest': 11 | case 'mac2': 12 | strategies.push(STRAT.PREDICATE, STRAT.CLASS_CHAIN); 13 | break; 14 | case 'espresso': 15 | strategies.push(STRAT.DATAMATCHER, STRAT.VIEWTAG); 16 | break; 17 | case 'uiautomator2': 18 | strategies.push(STRAT.UIAUTOMATOR); 19 | break; 20 | } 21 | return strategies; 22 | }; 23 | 24 | const ElementLocator = (props) => { 25 | const { 26 | setLocatorTestValue, 27 | locatorTestValue, 28 | setLocatorTestStrategy, 29 | locatorTestStrategy, 30 | automationName, 31 | t, 32 | } = props; 33 | 34 | return ( 35 | 36 | {t('locatorStrategy')} 37 | 38 | setLocatorTestStrategy(e.target.value)} 41 | defaultValue={locatorTestStrategy} 42 | > 43 | 44 | {locatorStrategies(automationName).map(([strategyValue, strategyName]) => ( 45 | 50 | {strategyName} 51 | 52 | ))} 53 | 54 | 55 | 56 | {!automationName && ( 57 | 58 | )} 59 | {t('selector')} 60 | setLocatorTestValue(e.target.value)} 63 | value={locatorTestValue} 64 | allowClear={true} 65 | rows={3} 66 | /> 67 | 68 | ); 69 | }; 70 | 71 | export default ElementLocator; 72 | -------------------------------------------------------------------------------- /app/common/renderer/components/Inspector/FileUploader.jsx: -------------------------------------------------------------------------------- 1 | import {UploadOutlined} from '@ant-design/icons'; 2 | import {Button, Tooltip, Upload} from 'antd'; 3 | import {useEffect, useState} from 'react'; 4 | 5 | const FileUploader = (props) => { 6 | const {multiple, onUpload, type, icon, tooltipTitle} = props; 7 | 8 | const [fileList, setFileList] = useState([]); 9 | 10 | useEffect(() => { 11 | if (fileList.length > 0) { 12 | onUpload(fileList); 13 | setFileList([]); 14 | } 15 | }, [fileList]); 16 | 17 | const handleFileUpload = (_file, list) => { 18 | if (fileList.length !== list.length) { 19 | setFileList(list); 20 | } 21 | return false; 22 | }; 23 | 24 | return ( 25 | 32 | 33 | 40 | )} 41 | 44 | 45 | } 46 | > 47 | {!locatedElements && } 48 | {locatedElements && } 49 | 50 | ); 51 | }; 52 | 53 | export default LocatorTestModal; 54 | -------------------------------------------------------------------------------- /app/common/renderer/components/Inspector/SessionCodeBox.jsx: -------------------------------------------------------------------------------- 1 | import {CodeOutlined, CopyOutlined} from '@ant-design/icons'; 2 | import {Button, Card, Select, Space, Tooltip} from 'antd'; 3 | import hljs from 'highlight.js'; 4 | import _ from 'lodash'; 5 | 6 | import {CLIENT_FRAMEWORK_MAP} from '../../lib/client-frameworks/map'; 7 | import {copyToClipboard} from '../../polyfills'; 8 | import InspectorStyles from './Inspector.module.css'; 9 | 10 | const SessionCodeBox = (props) => { 11 | const {clientFramework, setClientFramework, t} = props; 12 | 13 | const code = (raw = true) => { 14 | const {serverDetails, sessionCaps} = props; 15 | const {serverUrl, serverUrlParts} = serverDetails; 16 | 17 | const ClientFrameworkClass = CLIENT_FRAMEWORK_MAP[clientFramework]; 18 | const framework = new ClientFrameworkClass(serverUrl, serverUrlParts, sessionCaps); 19 | const rawCode = framework.getCodeString(true); 20 | if (raw) { 21 | return rawCode; 22 | } 23 | 24 | return hljs.highlight(rawCode, {language: ClientFrameworkClass.highlightLang}).value; 25 | }; 26 | 27 | const actionBar = () => ( 28 | 29 | 30 | 15 | ); 16 | const providersGrid = _.chunk(_.keys(CloudProviders), 2); // Converts list of providers into list of pairs of providers 17 | 18 | const toggleVisibleProvider = (providerName) => { 19 | const {addVisibleProvider, removeVisibleProvider} = props; 20 | if (visibleProviders.includes(providerName)) { 21 | removeVisibleProvider(providerName); 22 | } else { 23 | addVisibleProvider(providerName); 24 | } 25 | }; 26 | 27 | return ( 28 | 36 | {[ 37 | ..._.map(providersGrid, (row, key) => ( 38 | 39 | {[ 40 | ..._(row).map((providerName) => { 41 | const providerIsVisible = visibleProviders.includes(providerName); 42 | const style = {}; 43 | if (providerIsVisible) { 44 | style.borderColor = '#40a9ff'; 45 | } 46 | const provider = CloudProviders[providerName]; 47 | return ( 48 | provider && ( 49 | 50 | 57 | 58 | ) 59 | ); 60 | }), 61 | ]} 62 | 63 | )), 64 | ]} 65 | 66 | ); 67 | }; 68 | 69 | export default CloudProviderSelector; 70 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabBitbar.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {INPUT} from '../../constants/antd-types'; 4 | 5 | const bitbarApiKeyPlaceholder = (t) => { 6 | if (process.env.BITBAR_API_KEY) { 7 | return t('usingDataFoundIn', {environmentVariable: 'BITBAR_API_KEY'}); 8 | } 9 | return t('yourApiKey'); 10 | }; 11 | 12 | const ServerTabBitbar = ({server, setServerParam, t}) => ( 13 |
14 | 15 | 16 | 17 | setServerParam('apiKey', e.target.value)} 24 | /> 25 | 26 | 27 | 28 |
29 | ); 30 | 31 | export default ServerTabBitbar; 32 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabBrowserstack.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {INPUT} from '../../constants/antd-types'; 4 | 5 | const browserstackUsernamePlaceholder = (t) => { 6 | if (process.env.BROWSERSTACK_USERNAME) { 7 | return t('usingDataFoundIn', {environmentVariable: 'BROWSERSTACK_USERNAME'}); 8 | } 9 | return t('yourUsername'); 10 | }; 11 | 12 | const browserstackAccessKeyPlaceholder = (t) => { 13 | if (process.env.BROWSERSTACK_ACCESS_KEY) { 14 | return t('usingDataFoundIn', {environmentVariable: 'BROWSERSTACK_ACCESS_KEY'}); 15 | } 16 | return t('yourAccessKey'); 17 | }; 18 | 19 | const ServerTabBrowserstack = ({server, setServerParam, t}) => ( 20 |
21 | 22 | 23 | 24 | setServerParam('username', e.target.value)} 30 | /> 31 | 32 | 33 | 34 | 35 | setServerParam('accessKey', e.target.value)} 42 | /> 43 | 44 | 45 | 46 |
47 | ); 48 | 49 | export default ServerTabBrowserstack; 50 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabCustom.jsx: -------------------------------------------------------------------------------- 1 | import {Checkbox, Col, Form, Input, Row} from 'antd'; 2 | 3 | import {DEFAULT_SERVER_PROPS} from '../../constants/webdriver.js'; 4 | 5 | const ServerTabCustom = ({server, setServerParam, t}) => ( 6 |
7 | 8 | 9 | 10 | setServerParam('hostname', e.target.value)} 16 | /> 17 | 18 | 19 | 20 | 21 | setServerParam('port', e.target.value)} 27 | /> 28 | 29 | 30 | 31 | 32 | setServerParam('path', e.target.value)} 38 | /> 39 | 40 | 41 | 42 | 43 | setServerParam('ssl', e.target.checked)} 48 | > 49 | {t('SSL')} 50 | 51 | 52 | 53 | 54 |
55 | ); 56 | 57 | export default ServerTabCustom; 58 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabExperitest.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {PROVIDER_VALUES} from '../../constants/session-builder'; 4 | import SessionStyles from './Session.module.css'; 5 | 6 | const ServerTabExperitest = ({server, setServerParam, t}) => ( 7 |
8 | 9 | 10 | 11 | setServerParam('url', evt.target.value)} 18 | /> 19 | 20 | 21 | 22 | 23 | setServerParam('accessKey', evt.target.value)} 30 | /> 31 | 32 | 33 | 34 |
35 | ); 36 | 37 | export default ServerTabExperitest; 38 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabHeadspin.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {PROVIDER_VALUES} from '../../constants/session-builder'; 4 | import SessionStyles from './Session.module.css'; 5 | 6 | const ServerTabHeadspin = ({server, setServerParam, t}) => ( 7 |
8 | 9 | 10 | 11 | setServerParam('webDriverUrl', e.target.value)} 18 | /> 19 |

{t('sessionHeadspinWebDriverURLDescription')}

20 |
21 | 22 |
23 |
24 | ); 25 | 26 | export default ServerTabHeadspin; 27 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabKobiton.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {INPUT} from '../../constants/antd-types'; 4 | 5 | const kobitonUsernamePlaceholder = (t) => { 6 | if (process.env.KOBITON_USERNAME) { 7 | return t('usingDataFoundIn', {environmentVariable: 'KOBITON_USERNAME'}); 8 | } 9 | return t('yourUsername'); 10 | }; 11 | 12 | const kobitonAccessKeyPlaceholder = (t) => { 13 | if (process.env.KOBITON_ACCESS_KEY) { 14 | return t('usingDataFoundIn', {environmentVariable: 'KOBITON_ACCESS_KEY'}); 15 | } 16 | return t('yourAccessKey'); 17 | }; 18 | 19 | const ServerTabKobiton = ({server, setServerParam, t}) => ( 20 |
21 | 22 | 23 | 24 | setServerParam('username', e.target.value)} 30 | /> 31 | 32 | 33 | 34 | 35 | setServerParam('accessKey', e.target.value)} 42 | /> 43 | 44 | 45 | 46 |
47 | ); 48 | 49 | export default ServerTabKobiton; 50 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabLambdatest.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {INPUT} from '../../constants/antd-types'; 4 | 5 | const lambdatestUsernamePlaceholder = (t) => { 6 | if (process.env.LAMBDATEST_USERNAME) { 7 | return t('usingDataFoundIn', {environmentVariable: 'LAMBDATEST_USERNAME'}); 8 | } 9 | return t('yourUsername'); 10 | }; 11 | 12 | const lambdatestAccessKeyPlaceholder = (t) => { 13 | if (process.env.LAMBDATEST_ACCESS_KEY) { 14 | return t('usingDataFoundIn', {environmentVariable: 'LAMBDATEST_ACCESS_KEY'}); 15 | } 16 | return t('yourAccessKey'); 17 | }; 18 | 19 | const ServerTabLambdatest = ({server, setServerParam, t}) => ( 20 |
21 | 22 | 23 | 24 | setServerParam('username', e.target.value)} 30 | /> 31 | 32 | 33 | 34 | 35 | setServerParam('accessKey', e.target.value)} 42 | /> 43 | 44 | 45 | 46 |
47 | ); 48 | 49 | export default ServerTabLambdatest; 50 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabMobitru.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {INPUT} from '../../constants/antd-types'; 4 | 5 | const mobitrWebDriverUrlPlaceholder = (t) => { 6 | if (process.env.MOBITRU_WEBDRIVER_URL) { 7 | return t('usingDataFoundIn', {environmentVariable: 'MOBITRU_WEBDRIVER_URL'}); 8 | } 9 | return 'https://app.mobitru.com/wd/hub'; 10 | }; 11 | 12 | const mobitruBillingUnitPlaceholder = (t) => { 13 | if (process.env.MOBITRU_BILLING_UNIT) { 14 | return t('usingDataFoundIn', {environmentVariable: 'MOBITRU_BILLING_UNIT'}); 15 | } 16 | return 'personal'; 17 | }; 18 | 19 | const mobitruAccessKeyPlaceholder = (t) => { 20 | if (process.env.MOBITRU_ACCESS_KEY) { 21 | return t('usingDataFoundIn', {environmentVariable: 'MOBITRU_ACCESS_KEY'}); 22 | } 23 | return t('yourAccessKey'); 24 | }; 25 | 26 | const ServerTabMobitru = ({server, setServerParam, t}) => ( 27 |
28 | 29 | 30 | 31 | setServerParam('webDriverUrl', e.target.value)} 37 | /> 38 | 39 | 40 | 41 | 42 | 43 | 44 | setServerParam('username', e.target.value)} 50 | /> 51 | 52 | 53 | 54 | 55 | setServerParam('accessKey', e.target.value)} 62 | /> 63 | 64 | 65 | 66 |
67 | ); 68 | 69 | export default ServerTabMobitru; 70 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabPcloudy.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {INPUT} from '../../constants/antd-types'; 4 | import {PROVIDER_VALUES} from '../../constants/session-builder'; 5 | import SessionStyles from './Session.module.css'; 6 | 7 | const ServerTabPcloudy = ({server, setServerParam, t}) => ( 8 |
9 | 10 | 11 | 12 | setServerParam('hostname', e.target.value)} 19 | /> 20 | 21 | 22 | 23 | 24 | setServerParam('username', e.target.value)} 31 | /> 32 | 33 | 34 | 35 | 36 | setServerParam('accessKey', e.target.value)} 43 | /> 44 | 45 | 46 | 47 |
48 | ); 49 | 50 | export default ServerTabPcloudy; 51 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabPerfecto.jsx: -------------------------------------------------------------------------------- 1 | import {Checkbox, Col, Form, Input, Row} from 'antd'; 2 | 3 | import {PROVIDER_VALUES} from '../../constants/session-builder'; 4 | import SessionStyles from './Session.module.css'; 5 | 6 | const portPlaceholder = (server) => (server.perfecto.ssl ? '443' : '80'); 7 | 8 | const perfectoTokenPlaceholder = (t) => { 9 | if (process.env.PERFECTO_TOKEN) { 10 | return t('usingDataFoundIn', {environmentVariable: 'PERFECTO_TOKEN'}); 11 | } 12 | return t('Add your token'); 13 | }; 14 | 15 | const ServerTabPerfecto = ({server, setServerParam, t}) => ( 16 |
17 | 18 | 19 | 20 | setServerParam('hostname', e.target.value)} 27 | /> 28 | 29 | 30 | 31 | 32 | setServerParam('port', e.target.value)} 38 | /> 39 | 40 | 41 | 42 | 43 | setServerParam('token', e.target.value)} 49 | /> 50 | 51 | 52 | 53 | 54 | setServerParam('ssl', e.target.checked)} 57 | > 58 | {t('SSL')} 59 | 60 | 61 | 62 | 63 |
64 | ); 65 | 66 | export default ServerTabPerfecto; 67 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabRemoteTestKit.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {INPUT} from '../../constants/antd-types'; 4 | 5 | const ServerTabRemoteTestkit = ({server, setServerParam, t}) => ( 6 |
7 | 8 | 9 | 10 | setServerParam('token', e.target.value)} 16 | /> 17 | 18 | 19 | 20 |
21 | ); 22 | 23 | export default ServerTabRemoteTestkit; 24 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabRobotQA.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | const robotQATokenPlaceholder = (t) => { 4 | if (process.env.ROBOTQA_TOKEN) { 5 | return t('usingDataFoundIn', {environmentVariable: 'ROBOTQA_TOKEN'}); 6 | } 7 | return t('Add your token'); 8 | }; 9 | 10 | const ServerTabRobotQA = ({server, setServerParam, t}) => ( 11 |
12 | 13 | 14 | 15 | setServerParam('token', e.target.value)} 21 | /> 22 | 23 | 24 | 25 |
26 | ); 27 | 28 | export default ServerTabRobotQA; 29 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabTVLabs.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {INPUT} from '../../constants/antd-types'; 4 | 5 | const tvlabsApiKeyPlaceholder = (t) => { 6 | if (process.env.TVLABS_API_KEY) { 7 | return t('usingDataFoundIn', {environmentVariable: 'TVLABS_API_KEY'}); 8 | } 9 | return t('yourApiKey'); 10 | }; 11 | 12 | const ServerTabTVLabs = ({server, setServerParam, t}) => ( 13 |
14 | 15 | 16 | 17 | setServerParam('apiKey', e.target.value)} 24 | /> 25 | 26 | 27 | 28 |
29 | ); 30 | 31 | export default ServerTabTVLabs; 32 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabTestcribe.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {PROVIDER_VALUES} from '../../constants/session-builder'; 4 | import SessionStyles from './Session.module.css'; 5 | 6 | const ServerTabTestcribe = ({server, setServerParam, t}) => ( 7 |
8 | 9 | 10 | 11 | setServerParam('apiKey', e.target.value)} 18 | /> 19 |

{t('sessionTestcribeApiKeyDescription')}

20 |
21 | 22 |
23 |
24 | ); 25 | 26 | export default ServerTabTestcribe; 27 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ServerTabTestingbot.jsx: -------------------------------------------------------------------------------- 1 | import {Col, Form, Input, Row} from 'antd'; 2 | 3 | import {INPUT} from '../../constants/antd-types'; 4 | 5 | const testingbotUsernamePlaceholder = (t) => { 6 | if (process.env.TB_KEY) { 7 | return t('usingDataFoundIn', {environmentVariable: 'TB_KEY'}); 8 | } 9 | return t('yourUsername'); 10 | }; 11 | 12 | const testingbotAccessKeyPlaceholder = (t) => { 13 | if (process.env.TB_SECRET) { 14 | return t('usingDataFoundIn', {environmentVariable: 'TB_SECRET'}); 15 | } 16 | return t('yourAccessKey'); 17 | }; 18 | 19 | const ServerTabTestingbot = ({server, setServerParam, t}) => ( 20 |
21 | 22 | 23 | 24 | setServerParam('username', e.target.value)} 30 | /> 31 | 32 | 33 | 34 | 35 | setServerParam('accessKey', e.target.value)} 42 | /> 43 | 44 | 45 | 46 |
47 | ); 48 | 49 | export default ServerTabTestingbot; 50 | -------------------------------------------------------------------------------- /app/common/renderer/components/Session/ToggleTheme.jsx: -------------------------------------------------------------------------------- 1 | import {BgColorsOutlined, MoonOutlined, SunOutlined} from '@ant-design/icons'; 2 | import {Button, Dropdown, Tooltip} from 'antd'; 3 | 4 | import {useTheme} from '../../hooks/use-theme'; 5 | import SessionStyles from './Session.module.css'; 6 | 7 | const ToggleTheme = ({t}) => { 8 | const {preferredTheme, updateTheme} = useTheme(); 9 | 10 | const themes = [ 11 | { 12 | key: 'light', 13 | label: t('Light Theme'), 14 | icon: , 15 | }, 16 | { 17 | key: 'dark', 18 | label: t('Dark Theme'), 19 | icon: , 20 | }, 21 | { 22 | key: 'system', 23 | label: t('System Theme'), 24 | icon: , 25 | }, 26 | ]; 27 | 28 | return ( 29 |
30 | 31 | { 37 | updateTheme(key); 38 | }, 39 | }} 40 | trigger={['click']} 41 | > 42 |
48 | ); 49 | }; 50 | 51 | export default ToggleTheme; 52 | -------------------------------------------------------------------------------- /app/common/renderer/components/Spinner/Spinner.jsx: -------------------------------------------------------------------------------- 1 | import styles from './Spinner.module.css'; 2 | 3 | const Spinner = () => ( 4 |
5 |
6 |
7 | ); 8 | 9 | export default Spinner; 10 | -------------------------------------------------------------------------------- /app/common/renderer/components/Spinner/Spinner.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .loader, 8 | .loader:before, 9 | .loader:after { 10 | background: #555; 11 | -webkit-animation: load1 1s infinite ease-in-out; 12 | animation: load1 1s infinite ease-in-out; 13 | width: 1em; 14 | height: 4em; 15 | } 16 | .loader { 17 | color: #555; 18 | text-indent: -9999em; 19 | top: 45%; 20 | margin: auto; 21 | position: relative; 22 | font-size: 11px; 23 | -webkit-transform: translateZ(0); 24 | -ms-transform: translateZ(0); 25 | transform: translateZ(0); 26 | -webkit-animation-delay: -0.16s; 27 | animation-delay: -0.16s; 28 | } 29 | .loader:before, 30 | .loader:after { 31 | position: absolute; 32 | top: 0; 33 | content: ''; 34 | } 35 | .loader:before { 36 | left: -1.5em; 37 | -webkit-animation-delay: -0.32s; 38 | animation-delay: -0.32s; 39 | } 40 | .loader:after { 41 | left: 1.5em; 42 | } 43 | @-webkit-keyframes load1 { 44 | 0%, 45 | 80%, 46 | 100% { 47 | box-shadow: 0 0; 48 | height: 4em; 49 | } 50 | 40% { 51 | box-shadow: 0 -2em; 52 | height: 5em; 53 | } 54 | } 55 | @keyframes load1 { 56 | 0%, 57 | 80%, 58 | 100% { 59 | box-shadow: 0 0; 60 | height: 4em; 61 | } 62 | 40% { 63 | box-shadow: 0 -2em; 64 | height: 5em; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/common/renderer/constants/antd-types.js: -------------------------------------------------------------------------------- 1 | // Constants used as antd element property values 2 | 3 | export const ALERT = { 4 | ERROR: 'error', 5 | WARNING: 'warning', 6 | INFO: 'info', 7 | }; 8 | 9 | export const BUTTON = { 10 | DEFAULT: 'default', 11 | PRIMARY: 'primary', 12 | DASHED: 'dashed', 13 | TEXT: 'text', 14 | LINK: 'link', 15 | }; 16 | 17 | export const INPUT = { 18 | NUMBER: 'number', 19 | TEXT: 'text', 20 | TEXTAREA: 'textarea', 21 | PASSWORD: 'password', 22 | SUBMIT: 'submit', 23 | }; 24 | 25 | export const NOTIF = {ERROR: 'error', SUCCESS: 'success'}; 26 | 27 | export const ROW = {FLEX: 'flex'}; 28 | 29 | export const TABLE_TAB = {ADD: 'add', REMOVE: 'remove'}; 30 | -------------------------------------------------------------------------------- /app/common/renderer/constants/common.js: -------------------------------------------------------------------------------- 1 | export const WINDOW_DIMENSIONS = { 2 | MIN_WIDTH: 870, 3 | MIN_HEIGHT: 610, 4 | MAX_IMAGE_WIDTH_FRACTION: 0.4, 5 | }; 6 | 7 | export const LINKS = { 8 | CREATE_ISSUE: 'https://github.com/appium/appium-inspector/issues/new/choose', 9 | CAPS_DOCS: 'https://appium.io/docs/en/latest/guides/caps/', 10 | HYBRID_MODE_DOCS: 'https://appium.github.io/appium.io/docs/en/writing-running-appium/web/hybrid/', 11 | CLASS_CHAIN_DOCS: 12 | 'https://github.com/facebookarchive/WebDriverAgent/wiki/Class-Chain-Queries-Construction-Rules', 13 | PREDICATE_DOCS: 14 | 'https://github.com/facebookarchive/WebDriverAgent/wiki/Predicate-Queries-Construction-Rules', 15 | UIAUTOMATOR_DOCS: 16 | 'https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/uiautomator-uiselector.md', 17 | }; 18 | 19 | export const DRIVERS = { 20 | UIAUTOMATOR2: 'uiautomator2', 21 | ESPRESSO: 'espresso', 22 | XCUITEST: 'xcuitest', 23 | FLUTTER: 'flutter', 24 | MAC2: 'mac2', 25 | WINDOWS: 'windows', 26 | CHROMIUM: 'chromium', 27 | SAFARI: 'safari', 28 | GECKO: 'gecko', 29 | }; 30 | -------------------------------------------------------------------------------- /app/common/renderer/constants/gestures.js: -------------------------------------------------------------------------------- 1 | // Columns in the saved gestures table 2 | export const SAVED_GESTURE_PROPS = { 3 | NAME: 'Name', 4 | DESCRIPTION: 'Description', 5 | CREATED: 'Created', 6 | ACTIONS: 'Actions', 7 | }; 8 | 9 | export const POINTER_TYPES = { 10 | POINTER_UP: 'pointerUp', 11 | POINTER_DOWN: 'pointerDown', 12 | PAUSE: 'pause', 13 | POINTER_MOVE: 'pointerMove', 14 | }; 15 | 16 | export const POINTER_TYPES_MAP = { 17 | [POINTER_TYPES.POINTER_UP]: 'Pointer Up', 18 | [POINTER_TYPES.POINTER_DOWN]: 'Pointer Down', 19 | [POINTER_TYPES.PAUSE]: 'Pause', 20 | [POINTER_TYPES.POINTER_MOVE]: 'Move', 21 | }; 22 | 23 | // Colors used to distinguish multiple pointers in the same gesture 24 | export const POINTER_COLORS = ['#FF3333', '#FF8F00', '#B65FF4', '#6CFF00', '#00FFDC']; 25 | 26 | // Default pointer shown upon creating a new gesture 27 | export const DEFAULT_POINTER = [ 28 | { 29 | name: 'pointer1', 30 | ticks: [{id: '1.1'}], 31 | color: POINTER_COLORS[0], 32 | id: '1', 33 | }, 34 | ]; 35 | 36 | // HTML cursor style (used when hovering over pointer title) 37 | export const CURSOR = {POINTER: 'pointer', TEXT: 'text'}; 38 | 39 | // Properties for a single tick included in a pointer 40 | export const TICK_PROPS = { 41 | POINTER_TYPE: 'pointerType', 42 | DURATION: 'duration', 43 | BUTTON: 'button', 44 | X: 'x', 45 | Y: 'y', 46 | }; 47 | 48 | // Default duration for a pointer move action, in milliseconds 49 | export const POINTER_MOVE_DEFAULT_DURATION = 2500; 50 | 51 | export const POINTER_MOVE_COORDS_TYPE = { 52 | PERCENTAGES: 'percentages', 53 | PIXELS: 'pixels', 54 | }; 55 | 56 | export const POINTER_DOWN_BTNS = { 57 | LEFT: 0, 58 | RIGHT: 1, 59 | }; 60 | 61 | // Details for 'filler' ticks used to ensure timelines for all pointers have consistent length 62 | export const FILLER_TICK = {TYPE: 'filler', WAIT: 'wait', FINISH: 'finish', COLOR: '#FFFFFF'}; 63 | 64 | // Style for dots and lines drawn over the app screenshot 65 | export const GESTURE_ITEM_STYLES = { 66 | FILLED: 'filled', 67 | NEW_DASHED: 'newDashed', 68 | WHOLE: 'whole', 69 | DASHED: 'dashed', 70 | }; 71 | -------------------------------------------------------------------------------- /app/common/renderer/constants/screenshot.js: -------------------------------------------------------------------------------- 1 | // Screenshot interaction modes 2 | // TAP_SWIPE refers to both TAP and SWIPE 3 | // GESTURE refers to playback via gesture editor 4 | export const SCREENSHOT_INTERACTION_MODE = { 5 | SELECT: 'select', 6 | SWIPE: 'swipe', 7 | TAP: 'tap', 8 | TAP_SWIPE: 'tap_swipe', 9 | GESTURE: 'gesture', 10 | }; 11 | 12 | // Default parameters when executing coordinate-based swipe over app screenshot 13 | export const DEFAULT_SWIPE = { 14 | POINTER_NAME: 'finger1', 15 | DURATION_1: 0, 16 | DURATION_2: 750, 17 | BUTTON: 0, 18 | ORIGIN: 'viewport', 19 | }; 20 | 21 | // Default parameters when executing coordinate-based tap over app screenshot 22 | export const DEFAULT_TAP = { 23 | POINTER_NAME: 'finger1', 24 | DURATION_1: 0, 25 | DURATION_2: 100, 26 | BUTTON: 0, 27 | }; 28 | 29 | // 3 Types of Centroids: 30 | // CENTROID is the circle/square displayed on the screen 31 | // EXPAND is the +/- circle displayed on the screen 32 | // OVERLAP is the same as CENTROID but is only visible when clicked on +/- circle 33 | export const RENDER_CENTROID_AS = { 34 | CENTROID: 'centroid', 35 | EXPAND: 'expand', 36 | OVERLAP: 'overlap', 37 | }; 38 | 39 | export const CENTROID_STYLES = { 40 | VISIBLE: 'visible', 41 | HIDDEN: 'hidden', 42 | CONTAINER: '50%', 43 | NON_CONTAINER: '0%', 44 | }; 45 | -------------------------------------------------------------------------------- /app/common/renderer/constants/session-builder.js: -------------------------------------------------------------------------------- 1 | export const SESSION_BUILDER_TABS = { 2 | CAPS_BUILDER: 'new', 3 | SAVED_CAPS: 'saved', 4 | ATTACH_TO_SESSION: 'attach', 5 | }; 6 | 7 | export const SERVER_TYPES = { 8 | LOCAL: 'local', 9 | REMOTE: 'remote', 10 | ADVANCED: 'advanced', 11 | SAUCE: 'sauce', 12 | HEADSPIN: 'headspin', 13 | BROWSERSTACK: 'browserstack', 14 | LAMBDATEST: 'lambdatest', 15 | TESTINGBOT: 'testingbot', 16 | EXPERITEST: 'experitest', 17 | ROBOTQA: 'roboticmobi', 18 | REMOTETESTKIT: 'remotetestkit', 19 | BITBAR: 'bitbar', 20 | KOBITON: 'kobiton', 21 | PERFECTO: 'perfecto', 22 | PCLOUDY: 'pcloudy', 23 | MOBITRU: 'mobitru', 24 | TVLABS: 'tvlabs', 25 | TESTCRIBE: 'testcribe', 26 | }; 27 | 28 | export const SAVED_SESSIONS_TABLE_VALUES = { 29 | DATE_COLUMN_WIDTH: '25%', 30 | ACTIONS_COLUMN_WIDTH: '106px', 31 | }; 32 | 33 | // Placeholder values for specific cloud provider input fields 34 | export const PROVIDER_VALUES = { 35 | EXPERITEST_ACCESS_KEY: 'accessKey', 36 | EXPERITEST_URL: 'https://example.experitest.com', 37 | HEADSPIN_URL: 'https://xxxx.headspin.io:4723/v0/your-api-token/wd/hub', 38 | PCLOUDY_USERNAME: 'username@pcloudy.com', 39 | PCLOUDY_HOST: 'cloud.pcloudy.com', 40 | PCLOUDY_ACCESS_KEY: 'kjdgtdwn65fdasd78uy6y', 41 | PERFECTO_URL: 'cloud.Perfectomobile.com', 42 | TESTCRIBE_API_KEY: 'your-api-key', 43 | }; 44 | 45 | export const ADD_CLOUD_PROVIDER_TAB_KEY = 'addCloudProvider'; 46 | 47 | export const CAPABILITY_TYPES = { 48 | TEXT: 'text', 49 | BOOL: 'boolean', 50 | NUM: 'number', 51 | OBJECT: 'object', 52 | // historical 53 | FILE: 'file', 54 | JSON_OBJECT: 'json_object', 55 | }; 56 | 57 | export const STANDARD_W3C_CAPS = [ 58 | 'platformName', 59 | 'browserName', 60 | 'browserVersion', 61 | 'acceptInsecureCerts', 62 | 'pageLoadStrategy', 63 | 'proxy', 64 | 'setWindowRect', 65 | 'timeouts', 66 | 'strictFileInteractability', 67 | 'unhandledPromptBehavior', 68 | 'userAgent', 69 | 'webSocketUrl', // WebDriver BiDi 70 | ]; 71 | -------------------------------------------------------------------------------- /app/common/renderer/constants/session-inspector.js: -------------------------------------------------------------------------------- 1 | export const MJPEG_STREAM_CHECK_INTERVAL = 1000; 2 | export const SESSION_EXPIRY_PROMPT_TIMEOUT = 60 * 60 * 1000; // Give user 1 hour to reply 3 | export const REFRESH_DELAY_MILLIS = 500; 4 | 5 | export const APP_MODE = { 6 | NATIVE: 'native', 7 | WEB_HYBRID: 'web_hybrid', 8 | }; 9 | 10 | export const NATIVE_APP = 'NATIVE_APP'; 11 | 12 | export const LOCATOR_STRATEGIES = { 13 | ID: 'id', 14 | XPATH: 'xpath', 15 | NAME: 'name', 16 | CLASS_NAME: 'class name', 17 | ACCESSIBILITY_ID: 'accessibility id', 18 | PREDICATE: '-ios predicate string', 19 | CLASS_CHAIN: '-ios class chain', 20 | UIAUTOMATOR: '-android uiautomator', 21 | DATAMATCHER: '-android datamatcher', 22 | VIEWTAG: '-android viewtag', 23 | }; 24 | 25 | // Used for a cleaner presentation in locator search modal 26 | export const LOCATOR_STRATEGY_MAP = { 27 | ID: [LOCATOR_STRATEGIES.ID, 'Id'], 28 | XPATH: [LOCATOR_STRATEGIES.XPATH, 'XPath'], 29 | NAME: [LOCATOR_STRATEGIES.NAME, 'Name'], 30 | CLASS_NAME: [LOCATOR_STRATEGIES.CLASS_NAME, 'Class Name'], 31 | ACCESSIBILITY_ID: [LOCATOR_STRATEGIES.ACCESSIBILITY_ID, 'Accessibility ID'], 32 | PREDICATE: [LOCATOR_STRATEGIES.PREDICATE, 'Predicate String'], 33 | CLASS_CHAIN: [LOCATOR_STRATEGIES.CLASS_CHAIN, 'Class Chain'], 34 | UIAUTOMATOR: [LOCATOR_STRATEGIES.UIAUTOMATOR, 'UIAutomator'], 35 | DATAMATCHER: [LOCATOR_STRATEGIES.DATAMATCHER, 'DataMatcher'], 36 | VIEWTAG: [LOCATOR_STRATEGIES.VIEWTAG, 'View Tag'], 37 | }; 38 | 39 | export const INSPECTOR_TABS = { 40 | SOURCE: 'source', 41 | COMMANDS: 'commands', 42 | GESTURES: 'gestures', 43 | RECORDER: 'recorder', 44 | SESSION_INFO: 'sessionInfo', 45 | }; 46 | 47 | export const CLIENT_FRAMEWORKS = { 48 | DOTNET_NUNIT: 'dotNetNUnit', 49 | JS_WDIO: 'jsWdIo', 50 | JS_OXYGEN: 'jsOxygen', 51 | JAVA_JUNIT4: 'java', // historical 52 | JAVA_JUNIT5: 'javaJUnit5', 53 | PYTHON: 'python', 54 | ROBOT: 'robot', 55 | RUBY: 'ruby', 56 | }; 57 | -------------------------------------------------------------------------------- /app/common/renderer/constants/source.js: -------------------------------------------------------------------------------- 1 | // Attributes which are listed in the source by default 2 | export const IMPORTANT_SOURCE_ATTRS = [ 3 | 'name', 4 | 'content-desc', 5 | 'resource-id', 6 | 'AXDescription', 7 | 'AXIdentifier', 8 | 'text', 9 | 'label', 10 | 'value', 11 | 'id', 12 | ]; 13 | -------------------------------------------------------------------------------- /app/common/renderer/constants/webdriver.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SERVER_PROPS = { 2 | protocol: 'http', 3 | hostname: '127.0.0.1', 4 | port: 4723, 5 | path: '/', 6 | logLevel: process.env.NODE_ENV === 'development' ? 'info' : 'warn', 7 | }; 8 | 9 | // All properties defined on the WDIO browser object 10 | // https://webdriver.io/docs/api/browser 11 | export const BROWSER_PROPERTIES = [ 12 | 'capabilities', 13 | 'requestedCapabilities', 14 | 'sessionId', 15 | 'options', 16 | 'commandList', 17 | 'isW3C', 18 | 'isChrome', 19 | 'isFirefox', 20 | 'isBidi', 21 | 'isSauce', 22 | 'isMacApp', 23 | 'isWindowsApp', 24 | 'isMobile', 25 | 'isIOS', 26 | 'isAndroid', 27 | 'isNativeContext', 28 | 'mobileContext', 29 | ]; 30 | 31 | // Various protocol commands that should not be added to WDSessionDriver 32 | export const AVOID_CMDS = [ 33 | 'newSession', 34 | 'findElement', 35 | 'findElements', 36 | 'findElementFromElement', 37 | 'findElementsFromElement', 38 | 'executeScript', 39 | 'executeAsyncScript', 40 | ]; 41 | 42 | // All commands defined in the webdriver protocol that are specific to a single element 43 | // https://webdriver.io/docs/api/webdriver 44 | export const ELEMENT_CMDS = [ 45 | // 'findElementFromElement', // defined as 'findElement' in WDSessionElement 46 | // 'findElementsFromElement', // defined as 'findElements' in WDSessionElement 47 | 'getElementShadowRoot', 48 | 'isElementSelected', 49 | 'isElementDisplayed', 50 | 'getElementAttribute', 51 | 'getElementProperty', 52 | 'getElementCSSValue', 53 | 'getElementText', 54 | 'getElementTagName', 55 | 'getElementRect', 56 | 'isElementEnabled', 57 | 'elementClick', 58 | 'elementClear', 59 | 'elementSendKeys', 60 | 'takeElementScreenshot', 61 | 'getElementComputedRole', 62 | 'getElementComputedLabel', 63 | ]; 64 | -------------------------------------------------------------------------------- /app/common/renderer/containers/InspectorPage.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | 3 | import * as InspectorActions from '../actions/Inspector'; 4 | import InspectorPage from '../components/Inspector/Inspector.jsx'; 5 | import {withTranslation} from '../i18next'; 6 | 7 | function mapStateToProps(state) { 8 | return state.inspector; 9 | } 10 | 11 | export default withTranslation(InspectorPage, connect(mapStateToProps, InspectorActions)); 12 | -------------------------------------------------------------------------------- /app/common/renderer/containers/SessionPage.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | 3 | import * as SessionActions from '../actions/Session'; 4 | import Session from '../components/Session/Session.jsx'; 5 | import {withTranslation} from '../i18next'; 6 | 7 | function mapStateToProps(state) { 8 | return state.session; 9 | } 10 | 11 | export default withTranslation(Session, connect(mapStateToProps, SessionActions)); 12 | -------------------------------------------------------------------------------- /app/common/renderer/hooks/use-theme.jsx: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | 3 | import {ThemeContext} from '../providers/ThemeProvider'; 4 | 5 | export const useTheme = () => useContext(ThemeContext); 6 | -------------------------------------------------------------------------------- /app/common/renderer/i18next.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import _ from 'lodash'; 3 | import {initReactI18next, withTranslation as wt} from 'react-i18next'; 4 | 5 | import {commonI18NextOptions} from '../shared/i18next.config'; 6 | import {PREFERRED_LANGUAGE} from '../shared/setting-defs'; 7 | import {getSetting, i18NextBackend, i18NextBackendOptions} from './polyfills'; 8 | 9 | const i18nextOptions = { 10 | ...commonI18NextOptions, 11 | backend: i18NextBackendOptions, 12 | lng: await getSetting(PREFERRED_LANGUAGE), 13 | }; 14 | 15 | const namespace = 'translation'; 16 | 17 | if (!i18n.isInitialized) { 18 | i18n.use(initReactI18next).use(i18NextBackend).init(i18nextOptions); 19 | } 20 | 21 | export function withTranslation(componentCls, ...hocs) { 22 | return _.flow(...hocs, wt(namespace))(componentCls); 23 | } 24 | 25 | export default i18n; 26 | -------------------------------------------------------------------------------- /app/common/renderer/index.jsx: -------------------------------------------------------------------------------- 1 | import {createRoot} from 'react-dom/client'; 2 | 3 | import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary.jsx'; 4 | import Root from './Root.jsx'; 5 | import store from './store.js'; 6 | 7 | const container = document.getElementById('root'); 8 | const root = createRoot(container); 9 | 10 | root.render( 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /app/common/renderer/lib/appium/session-element.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 HeadSpin, Inc. 3 | * Modifications copyright OpenJS Foundation and other contributors, 4 | * https://openjsf.org/ 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import {ELEMENT_CMDS} from '../../constants/webdriver.js'; 20 | 21 | const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf'; 22 | const JWP_ELEMENT_KEY = 'ELEMENT'; 23 | 24 | /** 25 | * Class used as a wrapper for a webdriver element 26 | * in order to allow calling element-related methods on it directly, 27 | * instead of needing to use WDSessionDriver 28 | */ 29 | class WDSessionElement { 30 | constructor(elementKey, findRes, parent) { 31 | this.elementKey = elementKey; 32 | this.elementId = this[elementKey] = findRes[elementKey]; 33 | this.parent = parent; 34 | this.session = parent.session || parent; 35 | } 36 | 37 | get executeObj() { 38 | return {[this.elementKey]: this.elementId}; 39 | } 40 | 41 | async findElement(using, value) { 42 | const res = await this.session.cmd('findElementFromElement', this.elementId, using, value); 43 | return getElementFromResponse(res, this); 44 | } 45 | 46 | async findElements(using, value) { 47 | const ress = await this.session.cmd('findElementsFromElement', this.elementId, using, value); 48 | return ress.map((res) => getElementFromResponse(res, this)); 49 | } 50 | } 51 | 52 | export function getElementFromResponse(res, parent) { 53 | const elementKey = res[W3C_ELEMENT_KEY] ? W3C_ELEMENT_KEY : JWP_ELEMENT_KEY; 54 | 55 | if (!res[elementKey]) { 56 | throw new Error( 57 | `Bad findElement response; did not have element key. ` + 58 | `Response was: ${JSON.stringify(res)}`, 59 | ); 60 | } 61 | 62 | return new WDSessionElement(elementKey, res, parent); 63 | } 64 | 65 | // Walk through all webdriver protocol element methods and add them to WDSessionElement 66 | // (except for edge cases) 67 | for (const cmdName of ELEMENT_CMDS) { 68 | WDSessionElement.prototype[cmdName] = async function (...args) { 69 | return await this.session.cmd(cmdName, this.elementId, ...args); 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /app/common/renderer/lib/appium/session-starter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2024 HeadSpin, Inc. 3 | * Modifications copyright OpenJS Foundation and other contributors, 4 | * https://openjsf.org/ 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import webdriver from 'webdriver'; 20 | 21 | import {DEFAULT_SERVER_PROPS} from '../../constants/webdriver.js'; 22 | import WDSessionDriver from './session-driver.js'; 23 | 24 | /** 25 | * Class used to retrieve a webdriver session, 26 | * either by creating a new one, or finding an existing one, 27 | * with additional safeguards for session parameters 28 | */ 29 | export default class WDSessionStarter { 30 | static async newSession(serverOpts, capabilities = {}) { 31 | const safeServerOpts = {...DEFAULT_SERVER_PROPS, ...serverOpts, capabilities}; 32 | const sessionClient = await webdriver.newSession(safeServerOpts); 33 | return new WDSessionDriver(sessionClient); 34 | } 35 | 36 | static attachToSession(sessionId, serverOpts, capabilities = {}) { 37 | if (!sessionId) { 38 | throw new Error("Can't attach to a session without a session id"); 39 | } 40 | const isW3C = true; 41 | const safeServerOpts = {sessionId, isW3C, ...DEFAULT_SERVER_PROPS, ...serverOpts, capabilities}; 42 | const sessionClient = webdriver.attachToSession(safeServerOpts); 43 | return new WDSessionDriver(sessionClient); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/common/renderer/lib/client-frameworks/java-junit4.js: -------------------------------------------------------------------------------- 1 | import JavaFramework from './java-common.js'; 2 | 3 | export default class JavaJUnit4Framework extends JavaFramework { 4 | static readableName = 'Java - JUnit4'; 5 | 6 | wrapWithBoilerplate(code) { 7 | const [pkg, cls, capStr] = this.getBoilerplateParams(); 8 | // Import everything from Selenium in order to use WebElement, Point and other classes. 9 | return `// This sample code supports Appium Java client >=9 10 | // https://github.com/appium/java-client 11 | import io.appium.java_client.remote.options.BaseOptions; 12 | import io.appium.java_client.AppiumBy; 13 | import io.appium.java_client.${pkg}.${cls}; 14 | import java.net.URL; 15 | import java.net.MalformedURLException; 16 | import java.time.Duration; 17 | import java.util.Arrays; 18 | import java.util.Base64; 19 | import org.junit.After; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | import org.openqa.selenium.*; 23 | 24 | public class SampleTest { 25 | 26 | private ${cls} driver; 27 | 28 | @Before 29 | public void setUp() { 30 | Capabilities options = new BaseOptions() 31 | ${capStr}; 32 | 33 | driver = new ${cls}(this.getUrl(), options); 34 | } 35 | 36 | @Test 37 | public void sampleTest() { 38 | ${this.indent(code, 4)} 39 | } 40 | 41 | @After 42 | public void tearDown() { 43 | driver.quit(); 44 | } 45 | 46 | private URL getUrl() { 47 | try { 48 | return new URL("${this.serverUrl}"); 49 | } catch (MalformedURLException e) { 50 | e.printStackTrace(); 51 | } 52 | } 53 | } 54 | `; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/common/renderer/lib/client-frameworks/java-junit5.js: -------------------------------------------------------------------------------- 1 | import JavaFramework from './java-common.js'; 2 | 3 | export default class JavaJUnit5Framework extends JavaFramework { 4 | static readableName = 'Java - JUnit5'; 5 | 6 | wrapWithBoilerplate(code) { 7 | const [pkg, cls, capStr] = this.getBoilerplateParams(); 8 | // Import everything from Selenium in order to use WebElement, Point and other classes. 9 | return `// This sample code supports Appium Java client >=9 10 | // https://github.com/appium/java-client 11 | import io.appium.java_client.remote.options.BaseOptions; 12 | import io.appium.java_client.AppiumBy; 13 | import io.appium.java_client.${pkg}.${cls}; 14 | import java.net.URL; 15 | import java.net.MalformedURLException; 16 | import java.time.Duration; 17 | import java.util.Arrays; 18 | import java.util.Base64; 19 | import org.junit.jupiter.api.AfterEach; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | import org.openqa.selenium.*; 23 | 24 | public class SampleTest { 25 | 26 | private ${cls} driver; 27 | 28 | @BeforeEach 29 | public void setUp() { 30 | Capabilities options = new BaseOptions() 31 | ${capStr}; 32 | 33 | driver = new ${cls}(this.getUrl(), options); 34 | } 35 | 36 | @Test 37 | public void sampleTest() { 38 | ${this.indent(code, 4)} 39 | } 40 | 41 | @AfterEach 42 | public void tearDown() { 43 | driver.quit(); 44 | } 45 | 46 | private URL getUrl() { 47 | try { 48 | return new URL("${this.serverUrl}"); 49 | } catch (MalformedURLException e) { 50 | e.printStackTrace(); 51 | } 52 | } 53 | } 54 | `; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/common/renderer/lib/client-frameworks/map.js: -------------------------------------------------------------------------------- 1 | import {CLIENT_FRAMEWORKS} from '../../constants/session-inspector.js'; 2 | import DotNetNUnitFramework from './dotnet-nunit.js'; 3 | import JavaJUnit4Framework from './java-junit4.js'; 4 | import JavaJUnit5Framework from './java-junit5.js'; 5 | import JsOxygenFramework from './js-oxygen.js'; 6 | import JsWdIoFramework from './js-wdio.js'; 7 | import PythonFramework from './python.js'; 8 | import RobotFramework from './robot.js'; 9 | import RubyFramework from './ruby.js'; 10 | 11 | export const CLIENT_FRAMEWORK_MAP = { 12 | [CLIENT_FRAMEWORKS.DOTNET_NUNIT]: DotNetNUnitFramework, 13 | [CLIENT_FRAMEWORKS.JS_WDIO]: JsWdIoFramework, 14 | [CLIENT_FRAMEWORKS.JS_OXYGEN]: JsOxygenFramework, 15 | [CLIENT_FRAMEWORKS.JAVA_JUNIT4]: JavaJUnit4Framework, 16 | [CLIENT_FRAMEWORKS.JAVA_JUNIT5]: JavaJUnit5Framework, 17 | [CLIENT_FRAMEWORKS.PYTHON]: PythonFramework, 18 | [CLIENT_FRAMEWORKS.ROBOT]: RobotFramework, 19 | [CLIENT_FRAMEWORKS.RUBY]: RubyFramework, 20 | }; 21 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/bitbar.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class BitbarVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const bitbar = this._server.bitbar; 9 | const vendorName = 'BitBar'; 10 | 11 | const apiKey = bitbar.apiKey || process.env.BITBAR_API_KEY; 12 | this._checkInputPropertyPresence(vendorName, [{name: 'API Key', val: apiKey}]); 13 | 14 | const host = process.env.BITBAR_HOST || 'appium.bitbar.com'; 15 | const port = 443; 16 | const path = '/wd/hub'; 17 | const https = true; 18 | this._saveProperties(bitbar, {host, path, port, https, accessKey: apiKey}); 19 | 20 | this._updateSessionCap( 21 | 'bitbar:options', 22 | { 23 | source: 'appiumdesktop', 24 | apiKey, 25 | }, 26 | false, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/browserstack.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class BrowserstackVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const browserstack = this._server.browserstack; 9 | const vendorName = 'BrowserStack'; 10 | 11 | const username = browserstack.username || process.env.BROWSERSTACK_USERNAME; 12 | const accessKey = browserstack.accessKey || process.env.BROWSERSTACK_ACCESS_KEY; 13 | this._checkInputPropertyPresence(vendorName, [ 14 | {name: 'Username', val: username}, 15 | {name: 'Access Key', val: accessKey}, 16 | ]); 17 | 18 | const host = process.env.BROWSERSTACK_HOST || 'hub-cloud.browserstack.com'; 19 | const port = process.env.BROWSERSTACK_PORT || 443; 20 | const path = '/wd/hub'; 21 | const https = parseInt(port, 10) === 443; 22 | this._saveProperties(browserstack, {host, path, port, https, username, accessKey}); 23 | 24 | this._updateSessionCap('bstack:options', { 25 | source: 'appiumdesktop', 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/experitest.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class ExperitestVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const experitest = this._server.experitest; 9 | const vendorName = 'Experitest'; 10 | 11 | const url = experitest.url; 12 | const accessKey = experitest.accessKey; 13 | this._checkInputPropertyPresence(vendorName, [ 14 | {name: 'URL', val: url}, 15 | {name: 'Access Key', val: accessKey}, 16 | ]); 17 | const experitestUrl = this._validateUrl(url); 18 | 19 | const host = experitestUrl.hostname; 20 | const path = '/wd/hub'; 21 | const https = experitestUrl.protocol === 'https:'; 22 | const port = experitestUrl.port === '' ? (https ? 443 : 80) : experitestUrl.port; 23 | this._saveProperties(experitest, {host, path, port, https}); 24 | 25 | this._updateSessionCap('experitest:accessKey', accessKey); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/headspin.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class HeadspinVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const headspin = this._server.headspin; 9 | const vendorName = 'HeadSpin'; 10 | 11 | const url = headspin.webDriverUrl; 12 | this._checkInputPropertyPresence(vendorName, [{name: 'WebDriver URL', val: url}]); 13 | const headspinUrl = this._validateUrl(headspin.webDriverUrl); 14 | 15 | const host = headspinUrl.hostname; 16 | const path = headspinUrl.pathname; 17 | const https = headspinUrl.protocol === 'https:'; 18 | // new URL() does not have the port of 443 when `https` and 80 when `http` 19 | const port = headspinUrl.port === '' ? (https ? 443 : 80) : headspinUrl.port; 20 | this._saveProperties(headspin, {host, path, port, https}); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/kobiton.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class KobitonVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const kobiton = this._server.kobiton; 9 | const vendorName = 'Kobiton'; 10 | 11 | const username = kobiton.username || process.env.KOBITON_USERNAME; 12 | const accessKey = kobiton.accessKey || process.env.KOBITON_ACCESS_KEY; 13 | this._checkInputPropertyPresence(vendorName, [ 14 | {name: 'Username', val: username}, 15 | {name: 'API Key', val: accessKey}, 16 | ]); 17 | 18 | const host = process.env.KOBITON_HOST || 'api.kobiton.com'; 19 | const port = 443; 20 | const path = '/wd/hub'; 21 | const https = true; 22 | this._saveProperties(kobiton, {host, path, port, https, username, accessKey}); 23 | 24 | this._updateSessionCap('kobiton:options', { 25 | source: 'appiumdesktop', 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/lambdatest.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {BaseVendor} from './base.js'; 4 | 5 | export class LambdatestVendor extends BaseVendor { 6 | /** 7 | * @override 8 | */ 9 | async configureProperties() { 10 | const lambdatest = this._server.lambdatest; 11 | const advanced = this._server.advanced; 12 | const vendorName = 'LambdaTest'; 13 | 14 | const username = lambdatest.username || process.env.LAMBDATEST_USERNAME; 15 | const accessKey = lambdatest.accessKey || process.env.LAMBDATEST_ACCESS_KEY; 16 | this._checkInputPropertyPresence(vendorName, [ 17 | {name: 'Username', val: username}, 18 | {name: 'Access Key', val: accessKey}, 19 | ]); 20 | 21 | const host = process.env.LAMBDATEST_HOST || 'mobile-hub.lambdatest.com'; 22 | const port = process.env.LAMBDATEST_PORT || 443; 23 | const path = '/wd/hub'; 24 | const https = parseInt(port, 10) === 443; 25 | this._saveProperties(lambdatest, {host, path, port, https, username, accessKey}); 26 | 27 | if (_.has(this._sessionCaps, 'lt:options')) { 28 | const options = { 29 | source: 'appiumdesktop', 30 | isRealMobile: true, 31 | }; 32 | if (advanced.useProxy) { 33 | options.proxyUrl = _.isUndefined(advanced.proxy) ? '' : advanced.proxy; 34 | } 35 | this._updateSessionCap('lt:options', options); 36 | } else { 37 | this._updateSessionCap('lambdatest:source', 'appiumdesktop'); 38 | this._updateSessionCap('lambdatest:isRealMobile', true); 39 | if (advanced.useProxy) { 40 | this._updateSessionCap( 41 | 'lambdatest:proxyUrl', 42 | _.isUndefined(advanced.proxy) ? '' : advanced.proxy, 43 | ); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/local.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class LocalVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const local = this._server.local; 9 | 10 | // if we're on windows, we won't be able to connect directly to '0.0.0.0' 11 | // so just connect to localhost; if we're listening on all interfaces, 12 | // that will of course include 127.0.0.1 on all platforms 13 | const host = local.host === '0.0.0.0' ? 'localhost' : local.hostname; 14 | const port = local.port; 15 | this._saveProperties(local, {host, port}); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/map.js: -------------------------------------------------------------------------------- 1 | import {SERVER_TYPES} from '../../constants/session-builder'; 2 | import {BitbarVendor} from './bitbar.js'; 3 | import {BrowserstackVendor} from './browserstack.js'; 4 | import {ExperitestVendor} from './experitest.js'; 5 | import {HeadspinVendor} from './headspin.js'; 6 | import {KobitonVendor} from './kobiton.js'; 7 | import {LambdatestVendor} from './lambdatest.js'; 8 | import {LocalVendor} from './local.js'; 9 | import {MobitruVendor} from './mobitru.js'; 10 | import {PcloudyVendor} from './pcloudy.js'; 11 | import {PerfectoVendor} from './perfecto.js'; 12 | import {RemoteVendor} from './remote.js'; 13 | import {RemotetestkitVendor} from './remotetestkit.js'; 14 | import {RobotqaVendor} from './robotqa.js'; 15 | import {SaucelabsVendor} from './saucelabs.js'; 16 | import {TestcribeVendor} from './testcribe.js'; 17 | import {TestingbotVendor} from './testingbot.js'; 18 | import {TvlabsVendor} from './tvlabs.js'; 19 | 20 | export const VENDOR_MAP = { 21 | [SERVER_TYPES.LOCAL]: LocalVendor, 22 | [SERVER_TYPES.REMOTE]: RemoteVendor, 23 | [SERVER_TYPES.SAUCE]: SaucelabsVendor, 24 | [SERVER_TYPES.HEADSPIN]: HeadspinVendor, 25 | [SERVER_TYPES.PERFECTO]: PerfectoVendor, 26 | [SERVER_TYPES.BROWSERSTACK]: BrowserstackVendor, 27 | [SERVER_TYPES.LAMBDATEST]: LambdatestVendor, 28 | [SERVER_TYPES.BITBAR]: BitbarVendor, 29 | [SERVER_TYPES.KOBITON]: KobitonVendor, 30 | [SERVER_TYPES.PCLOUDY]: PcloudyVendor, 31 | [SERVER_TYPES.TESTINGBOT]: TestingbotVendor, 32 | [SERVER_TYPES.EXPERITEST]: ExperitestVendor, 33 | [SERVER_TYPES.ROBOTQA]: RobotqaVendor, 34 | [SERVER_TYPES.REMOTETESTKIT]: RemotetestkitVendor, 35 | [SERVER_TYPES.MOBITRU]: MobitruVendor, 36 | [SERVER_TYPES.TVLABS]: TvlabsVendor, 37 | [SERVER_TYPES.TESTCRIBE]: TestcribeVendor, 38 | }; 39 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/mobitru.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class MobitruVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const mobitru = this._server.mobitru; 9 | const vendorName = 'Mobitru'; 10 | 11 | const username = mobitru.username || process.env.MOBITRU_BILLING_UNIT || 'personal'; 12 | const accessKey = mobitru.accessKey || process.env.MOBITRU_ACCESS_KEY; 13 | const url = 14 | mobitru.webDriverUrl || process.env.MOBITRU_WEBDRIVER_URL || 'https://app.mobitru.com/wd/hub'; 15 | this._checkInputPropertyPresence(vendorName, [{name: 'Access Key', val: accessKey}]); 16 | const mobitruUrl = this._validateUrl(url); 17 | 18 | const host = mobitruUrl.hostname; 19 | const path = mobitruUrl.pathname; 20 | const https = mobitruUrl.protocol === 'https:'; 21 | const port = mobitruUrl.port === '' ? (https ? 443 : 80) : mobitruUrl.port; 22 | this._saveProperties(mobitru, {host, path, port, https, username, accessKey}); 23 | 24 | this._updateSessionCap('mobitru:options', { 25 | source: 'appium-inspector', 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/pcloudy.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class PcloudyVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const pcloudy = this._server.pcloudy; 9 | const vendorName = 'pCloudy'; 10 | 11 | const host = pcloudy.hostname; 12 | const username = pcloudy.username || process.env.PCLOUDY_USERNAME; 13 | const accessKey = pcloudy.accessKey || process.env.PCLOUDY_ACCESS_KEY; 14 | this._checkInputPropertyPresence(vendorName, [ 15 | {name: 'Host', val: host}, 16 | {name: 'Username', val: username}, 17 | {name: 'API Key', val: accessKey}, 18 | ]); 19 | 20 | const port = 443; 21 | const path = '/objectspy/wd/hub'; 22 | const https = true; 23 | this._saveProperties(pcloudy, {host, path, port, https, username, accessKey}); 24 | 25 | this._updateSessionCap( 26 | 'pcloudy:options', 27 | { 28 | source: 'appiumdesktop', 29 | pCloudy_Username: username, 30 | pCloudy_ApiKey: accessKey, 31 | }, 32 | false, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/perfecto.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class PerfectoVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const perfecto = this._server.perfecto; 9 | const vendorName = 'Perfecto'; 10 | 11 | const host = perfecto.hostname; 12 | const securityToken = perfecto.token || process.env.PERFECTO_TOKEN; 13 | this._checkInputPropertyPresence(vendorName, [ 14 | {name: 'Host', val: host}, 15 | {name: 'SecurityToken', val: securityToken}, 16 | ]); 17 | 18 | const port = perfecto.port || (perfecto.ssl ? 443 : 80); 19 | const https = perfecto.ssl; 20 | const path = '/nexperience/perfectomobile/wd/hub'; 21 | this._saveProperties(perfecto, {host, path, port, https, accessKey: securityToken}); 22 | 23 | this._updateSessionCap('perfecto:options', {securityToken}, false); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/remote.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class RemoteVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const remote = this._server.remote; 9 | 10 | const host = remote.hostname; 11 | const port = remote.port; 12 | const path = remote.path; 13 | const https = remote.ssl; 14 | this._saveProperties(remote, {host, path, port, https}); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/remotetestkit.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class RemotetestkitVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const remotetestkit = this._server.remotetestkit; 9 | const vendorName = 'RemoteTestKit'; 10 | 11 | const accessToken = remotetestkit.token; 12 | this._checkInputPropertyPresence(vendorName, [{name: 'AccessToken', val: accessToken}]); 13 | 14 | const host = 'gwjp.appkitbox.com'; 15 | const path = '/wd/hub'; 16 | const port = 443; 17 | const https = true; 18 | this._saveProperties(remotetestkit, {host, path, port, https, accessKey: accessToken}); 19 | 20 | this._updateSessionCap('remotetestkit:options', {accessToken}); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/robotqa.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class RobotqaVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const robotqa = this._server.roboticmobi; 9 | const vendorName = 'RobotQA'; 10 | 11 | const token = robotqa.token || process.env.ROBOTQA_TOKEN; 12 | this._checkInputPropertyPresence(vendorName, [{name: 'Token', val: token}]); 13 | 14 | const host = 'remote.robotqa.com'; 15 | const path = '/'; 16 | const port = 443; 17 | const https = true; 18 | this._saveProperties(robotqa, {host, path, port, https, accessKey: token}); 19 | 20 | this._updateSessionCap('robotqa:options', { 21 | robotqa_token: token, 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/saucelabs.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import {BaseVendor} from './base.js'; 4 | 5 | const SAUCE_OPTIONS_CAP = 'sauce:options'; 6 | 7 | export class SaucelabsVendor extends BaseVendor { 8 | /** 9 | * @override 10 | */ 11 | async configureProperties() { 12 | const sauce = this._server.sauce; 13 | const vendorName = 'Sauce Labs'; 14 | 15 | const username = sauce.username || process.env.SAUCE_USERNAME; 16 | const accessKey = sauce.accessKey || process.env.SAUCE_ACCESS_KEY; 17 | this._checkInputPropertyPresence(vendorName, [ 18 | {name: 'Username', val: username}, 19 | {name: 'Access Key', val: accessKey}, 20 | ]); 21 | 22 | let host = `ondemand.${sauce.dataCenter}.saucelabs.com`; 23 | let port = 80; 24 | if (sauce.useSCProxy) { 25 | host = sauce.scHost || 'localhost'; 26 | port = parseInt(sauce.scPort, 10) || 4445; 27 | } 28 | const path = '/wd/hub'; 29 | const https = false; 30 | this._saveProperties(sauce, {host, path, port, https, username, accessKey}); 31 | 32 | if (!this._sessionCaps[SAUCE_OPTIONS_CAP]?.name) { 33 | const dateTime = moment().format('lll'); 34 | this._updateSessionCap(SAUCE_OPTIONS_CAP, { 35 | name: `Appium Desktop Session -- ${dateTime}`, 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/testcribe.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class TestcribeVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const testcribe = this._server.testcribe; 9 | const vendorName = 'Testcribe'; 10 | 11 | const apiKey = testcribe.apiKey || process.env.TESTCRIBE_API_KEY; 12 | this._checkInputPropertyPresence(vendorName, [{name: 'API Key', val: apiKey}]); 13 | 14 | const host = process.env.TESTCRIBE_WEBDRIVER_URL || 'app.testcribe.com'; 15 | const port = 443; 16 | const https = true; 17 | const path = '/gw'; 18 | this._saveProperties(testcribe, {host, path, port, https, accessKey: apiKey}); 19 | 20 | this._updateSessionCap('testcribe:options', {apikey: apiKey}); 21 | this._updateSessionCap('appium:apiKey', apiKey); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/testingbot.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class TestingbotVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const testingbot = this._server.testingbot; 9 | const vendorName = 'TestingBot'; 10 | 11 | const key = testingbot.username || process.env.TB_KEY; 12 | const secret = testingbot.accessKey || process.env.TB_SECRET; 13 | this._checkInputPropertyPresence(vendorName, [ 14 | {name: 'Key', val: key}, 15 | {name: 'Secret', val: secret}, 16 | ]); 17 | 18 | const host = process.env.TB_HOST || 'hub.testingbot.com'; 19 | const port = 443; 20 | const path = '/wd/hub'; 21 | const https = true; 22 | this._saveProperties(testingbot, {host, path, port, https, username: key, accessKey: secret}); 23 | 24 | this._updateSessionCap('tb:options', { 25 | key, 26 | secret, 27 | source: 'appiumdesktop', 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/common/renderer/lib/vendor/tvlabs.js: -------------------------------------------------------------------------------- 1 | import {BaseVendor} from './base.js'; 2 | 3 | export class TvlabsVendor extends BaseVendor { 4 | /** 5 | * @override 6 | */ 7 | async configureProperties() { 8 | const tvlabs = this._server.tvlabs; 9 | const vendorName = 'TV Labs'; 10 | 11 | const apiKey = tvlabs.apiKey || process.env.TVLABS_API_KEY; 12 | this._checkInputPropertyPresence(vendorName, [{name: 'API Key', val: apiKey}]); 13 | const headers = {Authorization: `Bearer ${apiKey}`}; 14 | 15 | const host = process.env.TVLABS_WEBDRIVER_URL || tvlabs.host || 'appium.tvlabs.ai'; 16 | const path = tvlabs.path || '/'; 17 | const port = tvlabs.port || 4723; 18 | const https = tvlabs.ssl || host === 'appium.tvlabs.ai'; 19 | this._saveProperties(tvlabs, {host, path, port, https, accessKey: apiKey, headers}); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/common/renderer/polyfills.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The '#local-polyfills' alias is defined in both Vite config files. 3 | * Since both files define different resolution paths, 4 | * they cannot be added to tsconfig and eslint configurations 5 | */ 6 | 7 | import {settings} from '#local-polyfills'; // eslint-disable-line import/no-unresolved 8 | 9 | import {DEFAULT_SETTINGS} from '../shared/setting-defs'; 10 | 11 | export async function getSetting(setting) { 12 | if (await settings.has(setting)) { 13 | return await settings.get(setting); 14 | } 15 | return DEFAULT_SETTINGS[setting]; 16 | } 17 | 18 | export async function setSetting(setting, value) { 19 | await settings.set(setting, value); 20 | } 21 | 22 | export { 23 | copyToClipboard, 24 | i18NextBackend, 25 | i18NextBackendOptions, 26 | ipcRenderer, 27 | openLink, 28 | setTheme, 29 | } from '#local-polyfills'; // eslint-disable-line import/no-unresolved 30 | -------------------------------------------------------------------------------- /app/common/renderer/providers/ThemeProvider.jsx: -------------------------------------------------------------------------------- 1 | import {App, ConfigProvider, Layout, theme} from 'antd'; 2 | import {createContext, useEffect, useState} from 'react'; 3 | 4 | import {PREFERRED_THEME} from '../../shared/setting-defs'; 5 | import Notification from '../components/Notification'; 6 | import {getSetting, setSetting, setTheme} from '../polyfills'; 7 | import {loadHighlightTheme} from '../utils/highlight-theme'; 8 | 9 | const systemPrefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; 10 | 11 | export const ThemeContext = createContext(null); 12 | 13 | export const ThemeProvider = ({children}) => { 14 | const [preferredTheme, setPreferredTheme] = useState('system'); 15 | 16 | const isDarkTheme = 17 | preferredTheme === 'dark' || (preferredTheme === 'system' && systemPrefersDarkTheme); 18 | 19 | loadHighlightTheme(isDarkTheme); 20 | 21 | useEffect(() => { 22 | initTheme(); 23 | }, []); 24 | 25 | const handleDarkClass = () => { 26 | if (isDarkTheme) { 27 | document.body.classList.add('dark'); 28 | } else { 29 | document.body.classList.remove('dark'); 30 | } 31 | }; 32 | 33 | handleDarkClass(); 34 | 35 | const initTheme = async () => { 36 | const savedTheme = await getSetting(PREFERRED_THEME); 37 | setTheme(savedTheme); 38 | setPreferredTheme(savedTheme); 39 | }; 40 | 41 | const updateTheme = async (theme) => { 42 | setTheme(theme); 43 | setPreferredTheme(theme); 44 | await setSetting(PREFERRED_THEME, theme); 45 | }; 46 | 47 | const themeConfig = { 48 | algorithm: isDarkTheme ? theme.darkAlgorithm : theme.defaultAlgorithm, 49 | token: { 50 | colorBgLayout: isDarkTheme ? '#191919' : '#f5f5f5', 51 | fontSize: 12, 52 | }, 53 | components: { 54 | Badge: { 55 | colorError: '#1677ff', 56 | indicatorHeight: 20, 57 | textFontSize: 12, 58 | }, 59 | Switch: { 60 | handleSize: 18, 61 | trackHeight: 22, 62 | trackMinWidth: 44, 63 | }, 64 | Tabs: { 65 | titleFontSize: 14, 66 | }, 67 | }, 68 | }; 69 | 70 | return ( 71 | 72 | 73 | 74 | {children} 75 | 76 | 77 | 78 | 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /app/common/renderer/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from '@reduxjs/toolkit'; 2 | 3 | import inspector from './Inspector'; 4 | import session from './Session'; 5 | 6 | // create our root reducer 7 | export default function createRootReducer() { 8 | return combineReducers({ 9 | session, 10 | inspector, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /app/common/renderer/store.js: -------------------------------------------------------------------------------- 1 | import {configureStore} from '@reduxjs/toolkit'; 2 | 3 | import actions from './actions'; 4 | import createRootReducer from './reducers'; 5 | 6 | const store = configureStore({ 7 | reducer: createRootReducer(), 8 | middleware: (getDefaultMiddleware) => 9 | getDefaultMiddleware({ 10 | serializableCheck: false, 11 | }), 12 | devTools: 13 | process.env.NODE_ENV !== 'development' 14 | ? false 15 | : { 16 | actionCreators: {...actions}, 17 | }, 18 | }); 19 | 20 | export default store; 21 | -------------------------------------------------------------------------------- /app/common/renderer/utils/file-handling.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {CAPABILITY_TYPES, SERVER_TYPES} from '../constants/session-builder'; 4 | 5 | export function downloadFile(href, filename) { 6 | let element = document.createElement('a'); 7 | element.setAttribute('href', href); 8 | element.setAttribute('download', filename); 9 | element.style.display = 'none'; 10 | 11 | document.body.appendChild(element); 12 | element.click(); 13 | 14 | document.body.removeChild(element); 15 | } 16 | 17 | export async function readTextFromUploadedFiles(fileList) { 18 | const fileReaderPromise = fileList.map((file) => { 19 | const reader = new FileReader(); 20 | return new Promise((resolve) => { 21 | reader.onload = (event) => 22 | resolve({ 23 | fileName: file.name, 24 | content: event.target.result, 25 | }); 26 | reader.onerror = (error) => { 27 | resolve({ 28 | name: file.name, 29 | error: error.message, 30 | }); 31 | }; 32 | reader.readAsText(file); 33 | }); 34 | }); 35 | return await Promise.all(fileReaderPromise); 36 | } 37 | 38 | export function parseSessionFileContents(sessionFileString) { 39 | try { 40 | const sessionJSON = JSON.parse(sessionFileString); 41 | for (const sessionProp of ['version', 'caps', 'serverType']) { 42 | if (!(sessionProp in sessionJSON)) { 43 | return null; 44 | } 45 | } 46 | if (!_.values(SERVER_TYPES).includes(sessionJSON.serverType)) { 47 | return null; 48 | } else if (!_.isArray(sessionJSON.caps)) { 49 | return null; 50 | } else { 51 | for (const cap of sessionJSON.caps) { 52 | for (const capProp of ['type', 'name', 'value']) { 53 | if (!(capProp in cap)) { 54 | return null; 55 | } 56 | } 57 | if (!_.values(CAPABILITY_TYPES).includes(cap.type)) { 58 | return null; 59 | } 60 | } 61 | } 62 | return sessionJSON; 63 | } catch { 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/common/renderer/utils/highlight-theme.js: -------------------------------------------------------------------------------- 1 | import darkTheme from 'highlight.js/styles/atom-one-dark.css?url'; 2 | import lightTheme from 'highlight.js/styles/intellij-light.css?url'; 3 | 4 | export const loadHighlightTheme = (isDarkTheme) => { 5 | const linkId = 'highlight-theme'; 6 | let link = document.getElementById(linkId); 7 | 8 | if (!link) { 9 | link = document.createElement('link'); 10 | link.id = linkId; 11 | link.rel = 'stylesheet'; 12 | link.type = 'text/css'; 13 | document.head.appendChild(link); 14 | } 15 | 16 | if (isDarkTheme) { 17 | link.href = darkTheme; 18 | } else { 19 | link.href = lightTheme; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /app/common/renderer/utils/logger.js: -------------------------------------------------------------------------------- 1 | class Logger { 2 | info(...args) { 3 | console.info(...args); // eslint-disable-line no-console 4 | } 5 | 6 | warn(...args) { 7 | console.warn(...args); // eslint-disable-line no-console 8 | } 9 | 10 | error(...args) { 11 | console.error(...args); // eslint-disable-line no-console 12 | } 13 | } 14 | 15 | export const log = new Logger(); 16 | -------------------------------------------------------------------------------- /app/common/renderer/utils/notification.js: -------------------------------------------------------------------------------- 1 | export const NOTIFICATION_EVENT = 'notificationEvent'; 2 | 3 | function dispatchNotificationEvent(type, args) { 4 | document.dispatchEvent( 5 | new CustomEvent(NOTIFICATION_EVENT, { 6 | detail: { 7 | type, 8 | args, 9 | }, 10 | }), 11 | ); 12 | } 13 | 14 | export const notification = { 15 | success: (args) => { 16 | dispatchNotificationEvent('success', args); 17 | }, 18 | error: (args) => { 19 | dispatchNotificationEvent('error', args); 20 | }, 21 | info: (args) => { 22 | dispatchNotificationEvent('info', args); 23 | }, 24 | warning: (args) => { 25 | dispatchNotificationEvent('warning', args); 26 | }, 27 | open: (args) => { 28 | dispatchNotificationEvent('open', args); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /app/common/renderer/utils/other.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {STANDARD_W3C_CAPS} from '../constants/session-builder'; 4 | 5 | export function addVendorPrefixes(caps) { 6 | return caps.map((cap) => { 7 | // if we don't have a valid unprefixed cap or a cap with an existing prefix, update it 8 | if ( 9 | !_.isUndefined(cap.name) && 10 | !STANDARD_W3C_CAPS.includes(cap.name) && 11 | !_.includes(cap.name, ':') 12 | ) { 13 | cap.name = `appium:${cap.name}`; 14 | } 15 | return cap; 16 | }); 17 | } 18 | 19 | export function pixelsToPercentage(px, maxPixels) { 20 | if (!isNaN(px)) { 21 | return parseFloat(((px / maxPixels) * 100).toFixed(1), 10); 22 | } 23 | } 24 | 25 | export function percentageToPixels(pct, maxPixels) { 26 | if (!isNaN(pct)) { 27 | return Math.round(maxPixels * (pct / 100)); 28 | } 29 | } 30 | 31 | // Extracts element coordinates from its properties. 32 | // Depending on the platform, this is contained either in the 'bounds' property, 33 | // or the 'x'/'y'/'width'/'height' properties 34 | export function parseCoordinates(element) { 35 | const {bounds, x, y, width, height} = element.attributes || {}; 36 | 37 | if (bounds) { 38 | const boundsArray = bounds.split(/\[|\]|,/).filter((str) => str !== ''); 39 | const [x1, y1, x2, y2] = boundsArray.map((val) => parseInt(val, 10)); 40 | return {x1, y1, x2, y2}; 41 | } else if (x) { 42 | const originsArray = [x, y, width, height]; 43 | const [xInt, yInt, widthInt, heightInt] = originsArray.map((val) => parseInt(val, 10)); 44 | return {x1: xInt, y1: yInt, x2: xInt + widthInt, y2: yInt + heightInt}; 45 | } else { 46 | return {}; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/common/shared/i18next.config.js: -------------------------------------------------------------------------------- 1 | export const languageList = [ 2 | {name: 'Arabic', code: 'ar', original: 'العربية'}, 3 | {name: 'Chinese Simplified', code: 'zh-CN', original: '中文简体'}, 4 | {name: 'Chinese Traditional', code: 'zh-TW', original: '中文繁體'}, 5 | {name: 'English', code: 'en', original: 'English'}, 6 | {name: 'French', code: 'fr', original: 'Française'}, 7 | {name: 'German', code: 'de', original: 'Deutsch'}, 8 | {name: 'Hindi', code: 'hi', original: 'हिंदी'}, 9 | {name: 'Hungarian', code: 'hu', original: 'Magyar'}, 10 | {name: 'Italian', code: 'it', original: 'Italiano'}, 11 | {name: 'Japanese', code: 'ja', original: '日本語'}, 12 | {name: 'Kannada', code: 'kn', original: 'ಕನ್ನಡ'}, 13 | {name: 'Korean', code: 'ko', original: '한국어'}, 14 | {name: 'Malayalam', code: 'ml-IN', original: 'മലയാളം'}, 15 | {name: 'Persian', code: 'fa', original: 'فارسی'}, 16 | {name: 'Polish', code: 'pl', original: 'Polski'}, 17 | {name: 'Portuguese', code: 'pt-PT', original: 'Português'}, 18 | {name: 'Portuguese (Brazil)', code: 'pt-BR', original: 'Português (Brasil)'}, 19 | {name: 'Russian', code: 'ru', original: 'Русский'}, 20 | {name: 'Spanish', code: 'es-ES', original: 'Español'}, 21 | {name: 'Telugu', code: 'te', original: 'తెలుగు'}, 22 | {name: 'Turkish', code: 'tr', original: 'Türk'}, 23 | {name: 'Ukrainian', code: 'uk', original: 'Українська'}, 24 | ]; 25 | 26 | export const fallbackLng = 'en'; 27 | 28 | export const commonI18NextOptions = { 29 | // debug: true, 30 | // saveMissing: true, 31 | interpolation: { 32 | escapeValue: false, 33 | }, 34 | load: 'currentOnly', 35 | fallbackLng, 36 | supportedLngs: languageList.map((language) => language.code), 37 | }; 38 | -------------------------------------------------------------------------------- /app/common/shared/setting-defs.js: -------------------------------------------------------------------------------- 1 | // Definitions for all the persistent settings used in the app 2 | 3 | import {fallbackLng} from './i18next.config'; 4 | 5 | export const PREFERRED_LANGUAGE = 'PREFERRED_LANGUAGE'; 6 | export const PREFERRED_THEME = 'PREFERRED_THEME'; 7 | export const SAVED_SESSIONS = 'SAVED_SESSIONS'; 8 | export const SET_SAVED_GESTURES = 'SET_SAVED_GESTURES'; 9 | export const SERVER_ARGS = 'SERVER_ARGS'; 10 | export const SESSION_SERVER_PARAMS = 'SESSION_SERVER_PARAMS'; 11 | export const SESSION_SERVER_TYPE = 'SESSION_SERVER_TYPE'; 12 | export const SAVED_CLIENT_FRAMEWORK = 'SAVED_FRAMEWORK'; 13 | export const VISIBLE_PROVIDERS = 'VISIBLE_PROVIDERS'; 14 | 15 | export const DEFAULT_SETTINGS = { 16 | [PREFERRED_LANGUAGE]: fallbackLng, 17 | [PREFERRED_THEME]: 'system', 18 | [SAVED_SESSIONS]: [], 19 | [SET_SAVED_GESTURES]: [], 20 | [SERVER_ARGS]: null, 21 | [SESSION_SERVER_PARAMS]: null, 22 | [SESSION_SERVER_TYPE]: null, 23 | [SAVED_CLIENT_FRAMEWORK]: 'java', 24 | [VISIBLE_PROVIDERS]: null, 25 | }; 26 | -------------------------------------------------------------------------------- /app/common/splash.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Appium Inspector 6 | 7 | 8 | 9 |
10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /app/electron/main/debug.js: -------------------------------------------------------------------------------- 1 | import {installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS} from 'electron-devtools-installer'; 2 | 3 | export async function installExtensions() { 4 | const opts = { 5 | forceDownload: !!process.env.UPGRADE_EXTENSIONS, 6 | }; 7 | try { 8 | await installExtension([REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS], opts); 9 | } catch (e) { 10 | console.warn(`Error installing extension: ${e}`); // eslint-disable-line no-console 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/electron/main/helpers.js: -------------------------------------------------------------------------------- 1 | import {clipboard, ipcMain, nativeTheme, shell} from 'electron'; 2 | import settings from 'electron-settings'; 3 | import fs from 'fs'; 4 | 5 | import i18n from './i18next'; 6 | 7 | export const isDev = process.env.NODE_ENV === 'development'; 8 | 9 | export function setupIPCListeners() { 10 | ipcMain.handle('settings:has', async (_evt, key) => await settings.has(key)); 11 | ipcMain.handle('settings:set', async (_evt, key, value) => await settings.set(key, value)); 12 | ipcMain.handle('settings:get', async (_evt, key) => await settings.get(key)); 13 | ipcMain.on('electron:openLink', (_evt, link) => shell.openExternal(link)); 14 | ipcMain.on('electron:copyToClipboard', (_evt, text) => clipboard.writeText(text)); 15 | ipcMain.on('electron:setTheme', (_evt, theme) => (nativeTheme.themeSource = theme)); 16 | ipcMain.handle('sessionfile:open', async (_evt, filePath) => openSessionFile(filePath)); 17 | } 18 | 19 | // Open an .appiumsession file from the specified path and return its contents 20 | export const openSessionFile = (filePath) => fs.readFileSync(filePath, 'utf8'); 21 | 22 | export const t = (string, params = null) => i18n.t(string, params); 23 | 24 | export const APPIUM_SESSION_EXTENSION = 'appiumsession'; 25 | -------------------------------------------------------------------------------- /app/electron/main/i18next.js: -------------------------------------------------------------------------------- 1 | import settings from 'electron-settings'; 2 | import i18n from 'i18next'; 3 | import i18NextBackend from 'i18next-fs-backend'; 4 | import {join} from 'path'; 5 | 6 | import {commonI18NextOptions, fallbackLng} from '../../common/shared/i18next.config'; 7 | import {PREFERRED_LANGUAGE} from '../../common/shared/setting-defs'; 8 | 9 | const localesPath = 10 | process.env.NODE_ENV === 'development' 11 | ? join('app', 'common', 'public', 'locales') // from project root 12 | : join(__dirname, '..', 'renderer', 'locales'); // from 'main' in package.json 13 | const translationFilePath = join(localesPath, '{{lng}}', '{{ns}}.json'); 14 | 15 | const i18NextBackendOptions = { 16 | loadPath: translationFilePath, 17 | addPath: translationFilePath, 18 | jsonIndent: 2, 19 | }; 20 | 21 | const i18nextOptions = { 22 | ...commonI18NextOptions, 23 | backend: i18NextBackendOptions, 24 | lng: settings.getSync(PREFERRED_LANGUAGE) || fallbackLng, 25 | }; 26 | 27 | if (!i18n.isInitialized) { 28 | i18n.use(i18NextBackend).init(i18nextOptions); 29 | } 30 | 31 | export default i18n; 32 | -------------------------------------------------------------------------------- /app/electron/main/main.js: -------------------------------------------------------------------------------- 1 | import {app} from 'electron'; 2 | import debug from 'electron-debug'; 3 | 4 | import {installExtensions} from './debug'; 5 | import {isDev, setupIPCListeners} from './helpers'; 6 | import {setupMainWindow} from './windows'; 7 | 8 | // Used when opening Inspector through an .appiumsession file (Windows/Linux). 9 | // This value is not set in dev mode, since accessing argv[1] there throws an error, 10 | // and this flow only makes sense for the installed Inspector app anyway 11 | export let openFilePath = process.platform === 'darwin' || isDev ? null : process.argv[1]; 12 | 13 | // Used when opening Inspector through an .appiumsession file (macOS) 14 | app.on('open-file', (event, filePath) => { 15 | event.preventDefault(); 16 | openFilePath = filePath; 17 | }); 18 | 19 | app.on('window-all-closed', () => { 20 | app.quit(); 21 | }); 22 | 23 | app.on('ready', async () => { 24 | if (isDev) { 25 | debug(); 26 | await installExtensions(); 27 | } 28 | 29 | setupIPCListeners(); 30 | await setupMainWindow(); 31 | }); 32 | -------------------------------------------------------------------------------- /app/electron/main/updater.js: -------------------------------------------------------------------------------- 1 | import {dialog} from 'electron'; 2 | import pkg from 'electron-updater'; 3 | // eslint-disable-next-line import/no-named-as-default-member -- module is CJS 4 | const {autoUpdater} = pkg; 5 | 6 | import {t} from './helpers'; 7 | 8 | const RELEASES_LINK = 'https://github.com/appium/appium-inspector/releases'; 9 | 10 | autoUpdater.autoDownload = false; 11 | autoUpdater.autoInstallOnAppQuit = false; 12 | 13 | autoUpdater.on('error', (error) => { 14 | dialog.showErrorBox(t('Could not download update'), t('updateDownloadFailed', {message: error})); 15 | }); 16 | 17 | autoUpdater.on('update-not-available', () => { 18 | dialog.showMessageBox({ 19 | type: 'info', 20 | buttons: [t('OK')], 21 | message: t('No update available'), 22 | detail: t('Appium Inspector is up-to-date'), 23 | }); 24 | }); 25 | 26 | autoUpdater.on('update-available', async ({version, releaseDate}) => { 27 | const pubDate = new Date(releaseDate).toDateString(); 28 | const {response} = await dialog.showMessageBox({ 29 | type: 'info', 30 | message: t('appiumIsAvailable', {name: version}), 31 | buttons: [t('Install Now'), t('Install Later')], 32 | detail: t('updateDetails', {pubDate, notes: RELEASES_LINK}), 33 | }); 34 | if (response === 0) { 35 | // download is started without waiting for the dialog box to be dismissed 36 | dialog.showMessageBox({ 37 | type: 'info', 38 | buttons: [t('OK')], 39 | message: t('updateIsBeingDownloaded'), 40 | }); 41 | autoUpdater.downloadUpdate(); 42 | } 43 | }); 44 | 45 | autoUpdater.on('update-downloaded', async ({releaseName}) => { 46 | const {response} = await dialog.showMessageBox({ 47 | type: 'info', 48 | buttons: [t('Restart Now'), t('Later')], 49 | message: t('Update Downloaded'), 50 | detail: t('updateIsDownloaded', {releaseName}), 51 | }); 52 | if (response === 0) { 53 | autoUpdater.quitAndInstall(); 54 | } 55 | }); 56 | 57 | export function checkForUpdates() { 58 | autoUpdater.checkForUpdates(); 59 | } 60 | -------------------------------------------------------------------------------- /app/electron/preload/preload.mjs: -------------------------------------------------------------------------------- 1 | // Required by Vite 2 | -------------------------------------------------------------------------------- /app/electron/renderer/polyfills.js: -------------------------------------------------------------------------------- 1 | import {ipcRenderer} from 'electron'; 2 | import i18NextBackend from 'i18next-fs-backend'; 3 | import {join} from 'path'; 4 | 5 | const localesPath = 6 | process.env.NODE_ENV === 'development' 7 | ? join('app', 'common', 'public', 'locales') // from project root 8 | : join(__dirname, '..', 'renderer', 'locales'); // from 'main' in package.json 9 | const translationFilePath = join(localesPath, '{{lng}}', '{{ns}}.json'); 10 | 11 | const i18NextBackendOptions = { 12 | loadPath: translationFilePath, 13 | addPath: translationFilePath, 14 | jsonIndent: 2, 15 | }; 16 | 17 | const electronUtils = { 18 | copyToClipboard: (text) => ipcRenderer.send('electron:copyToClipboard', text), 19 | openLink: (link) => ipcRenderer.send('electron:openLink', link), 20 | setTheme: (theme) => ipcRenderer.send('electron:setTheme', theme), 21 | }; 22 | 23 | class ElectronSettings { 24 | async has(key) { 25 | return await ipcRenderer.invoke('settings:has', key); 26 | } 27 | 28 | async set(key, val) { 29 | return await ipcRenderer.invoke('settings:set', key, val); 30 | } 31 | 32 | async get(key) { 33 | return await ipcRenderer.invoke('settings:get', key); 34 | } 35 | } 36 | 37 | const settings = new ElectronSettings(); 38 | const {copyToClipboard, openLink, setTheme} = electronUtils; 39 | 40 | export { 41 | copyToClipboard, 42 | i18NextBackend, 43 | i18NextBackendOptions, 44 | ipcRenderer, 45 | openLink, 46 | setTheme, 47 | settings, 48 | }; 49 | -------------------------------------------------------------------------------- /app/web/polyfills.js: -------------------------------------------------------------------------------- 1 | import i18NextBackend from 'i18next-chained-backend'; 2 | import HttpApi from 'i18next-http-backend'; 3 | import LocalStorageBackend from 'i18next-localstorage-backend'; 4 | import _ from 'lodash'; 5 | 6 | // Adjust locales path depending on Vite base (web vs plugin) 7 | const viteBase = import.meta.env.BASE_URL; 8 | const vitePath = `${_.trimEnd(viteBase, '/')}/`; 9 | 10 | const localesPath = 11 | process.env.NODE_ENV === 'development' 12 | ? '/locales' // 'public' folder contents are served at '/' 13 | : `..${vitePath}locales`; // from 'dist-browser/assets/' 14 | 15 | const i18NextBackendOptions = { 16 | backends: [LocalStorageBackend, HttpApi], 17 | backendOptions: [ 18 | {}, 19 | { 20 | loadPath: `${localesPath}/{{lng}}/{{ns}}.json`, 21 | }, 22 | ], 23 | }; 24 | 25 | const browserUtils = { 26 | copyToClipboard: (text) => navigator.clipboard.writeText(text), 27 | openLink: (url) => window.open(url, ''), 28 | setTheme: () => {}, 29 | ipcRenderer: { 30 | on: (evt) => { 31 | console.warn(`Cannot listen for IPC event ${evt} in browser context`); // eslint-disable-line no-console 32 | }, 33 | }, 34 | }; 35 | 36 | class BrowserSettings { 37 | has(key) { 38 | return this.get(key) !== null; 39 | } 40 | 41 | set(key, val) { 42 | return localStorage.setItem(key, JSON.stringify(val)); 43 | } 44 | 45 | get(key) { 46 | return JSON.parse(localStorage.getItem(key)); 47 | } 48 | } 49 | 50 | const settings = new BrowserSettings(); 51 | const {copyToClipboard, openLink, setTheme, ipcRenderer} = browserUtils; 52 | 53 | export { 54 | copyToClipboard, 55 | i18NextBackend, 56 | i18NextBackendOptions, 57 | ipcRenderer, 58 | openLink, 59 | setTheme, 60 | settings, 61 | }; 62 | -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | 8 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/build/icon.ico -------------------------------------------------------------------------------- /docs/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /docs/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/assets/images/icon.png -------------------------------------------------------------------------------- /docs/assets/images/menu-bar-macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/assets/images/menu-bar-macos.png -------------------------------------------------------------------------------- /docs/assets/images/session-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/assets/images/session-builder.png -------------------------------------------------------------------------------- /docs/assets/images/session-inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/assets/images/session-inspector.png -------------------------------------------------------------------------------- /docs/assets/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | .md-source__fact--version { 2 | display: none; 3 | } 4 | .md-source__fact:nth-child(1n + 2):before { 5 | margin-left: 0 !important; 6 | } 7 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | - toc 5 | 6 | title: Welcome 7 | --- 8 | 9 | 14 | 15 |
16 | 17 |
18 | 19 | Welcome to the Appium Inspector documentation! The Inspector is a GUI assistant tool for Appium, 20 | providing visual inspection of the application under test (screenshots and page sources), with 21 | features such as interacting with the app screenshot, searching for and interacting with elements, 22 | executing driver actions, recording user actions, and more! 23 | 24 | Appium Inspector is part of the Appium ecosystem. For information on Appium itself, please visit 25 | [the Appium documentation](https://appium.io). 26 | 27 | ## Explore the Documentation 28 | 29 |
30 | 31 | - Check out the [**Overview**](./overview.md) to learn the basics of the Inspector 32 | - Go through the [**Quickstart**](./quickstart/index.md) steps to get set up and start inspecting your app 33 | - The [**Menu Bar**](./menu-bar.md) section acts as a reference for the application menu bar 34 | - The [**Session Builder**](./session-builder/index.md) section acts as a reference for the default landing screen 35 | - The [**Session Inspector**](./session-inspector/index.md) section acts as a reference for the inspector screen 36 | - Refer to the [**Troubleshooting**](./troubleshooting.md) page for a list of potential issues 37 | - For contributions to the Inspector, refer to the [**Contributing**](./contributing.md) page 38 | 39 |
40 | -------------------------------------------------------------------------------- /docs/menu-bar.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | 5 | title: Menu Bar 6 | --- 7 | 8 | The **Menu Bar** is the always shown either at the top of the application window (Windows) or in the 9 | system menu bar (macOS). 10 | 11 | ![macOS Menu Bar](assets/images/menu-bar-macos.png) 12 | 13 | !!! note 14 | 15 | The menu bar is not available in the [web app version](./overview.md#formats) of the Inspector. 16 | 17 | Several standard menu bar options are included, mainly related to window and text management. 18 | However, there are a few specific options as well: 19 | 20 | ## Update Checker 21 | 22 | The update checker is available under the _File_ menu (Windows/Linux) or the application menu 23 | (macOS). It can be used to check if there is a newer version of the Inspector available, and if so, 24 | it is possible to automatically download and install the latest version. 25 | 26 | Updating is supported for the following application formats: 27 | 28 | - macOS: `.dmg` 29 | - Windows: `.exe` installer 30 | - Linux: `.AppImage` 31 | 32 | ## Open/Save Session 33 | 34 | The _Open Session File_ / _Save As_ options in the _File_ menu provides the ability to import and 35 | export session details. Only one set of session details can be imported/exported at a time. 36 | 37 | ### Exporting Sessions 38 | 39 | Selecting the _Save As_ option will package the currently specified server and session details into 40 | a downloadable `.appiumsession` file, which can then be shared to other computers. 41 | 42 | ### Importing Sessions 43 | 44 | Selecting the _Open Session File_ option will load the server and session details in the 45 | [Session Builder](./session-builder/index.md). The loaded information can then be modified and/or 46 | saved inside the Inspector. 47 | 48 | ## Change Language 49 | 50 | The _Language_ option in the _View_ menu allows to change the entire application language. Currently 51 | there are over 20 available languages with community-provided translations! 52 | 53 | !!! note 54 | 55 | Most languages only include partial translations. You can help by providing your translations on 56 | [Crowdin](https://crowdin.com/project/appium-desktop)! 57 | -------------------------------------------------------------------------------- /docs/overrides/partials/toc-item.html: -------------------------------------------------------------------------------- 1 | 22 | 23 | 25 | 26 | 27 |
  • 28 | 29 | 30 | {{ toc_item.title }} 31 | 32 | 33 | 34 | 35 | {% if toc_item.children and toc_item.level < 3 %} 36 | 43 | {% endif %} 44 |
  • 45 | -------------------------------------------------------------------------------- /docs/quickstart/assets/images/mac-ctrl-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/quickstart/assets/images/mac-ctrl-click.png -------------------------------------------------------------------------------- /docs/quickstart/assets/images/open-warning-macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/quickstart/assets/images/open-warning-macos.png -------------------------------------------------------------------------------- /docs/quickstart/assets/images/open-warning-sequoia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/quickstart/assets/images/open-warning-sequoia.png -------------------------------------------------------------------------------- /docs/quickstart/assets/images/open-warning-windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/quickstart/assets/images/open-warning-windows.png -------------------------------------------------------------------------------- /docs/quickstart/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | 5 | title: Quickstart Intro 6 | --- 7 | 8 | Let's get started with Appium Inspector! Make sure you have checked out the 9 | [Overview](../overview.md) to understand the Inspector basics. 10 | 11 | This quickstart will cover the following: 12 | 13 | 1. [Appium Inspector install requirements](./requirements.md) 14 | 2. [Installing the Inspector](./installation.md) 15 | 3. [Configure Appium session details](./starting-a-session.md) 16 | 4. [Start an Inspector session](./starting-a-session.md#launching-the-session) 17 | 18 | Continue with the [system requirements](./requirements.md)! 19 | -------------------------------------------------------------------------------- /docs/quickstart/requirements.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | 5 | title: System Requirements 6 | --- 7 | 8 | Since the Inspector is released in [3 versions](../overview.md#formats), the requirements for these 9 | will differ: 10 | 11 | - Desktop app 12 | - Works on Windows 10+, macOS 11+, Ubuntu 18.04+, Debian 10+, openSUSE 15.5+, or Fedora Linux 39+ 13 | - [These requirements are taken from Chrome](https://support.google.com/chrome/a/answer/7100626), 14 | as the Inspector is built using Electron (which uses Chromium) 15 | - Up to around **600MB** of free space is required 16 | - The minimum application window size is **890 x 710** pixels 17 | - Web app/Appium server plugin 18 | - Works in Chrome/Edge/Firefox/Safari, released in 2022 or later 19 | - The plugin version requires around **9MB** of free space 20 | - Viewport size of at least **870 x 610** pixels is recommended 21 | 22 | ### Appium Server Requirements 23 | 24 | The Inspector cannot do much without an **Appium server** to connect to. Unless you only want to 25 | connect to existing Appium servers, you will need to install and set up a server of your own, 26 | which can be hosted either locally or remotely. For instructions on how to do this, please refer 27 | to the [Appium documentation](https://appium.io/docs/en/latest/quickstart/install/). 28 | 29 | If setting up your own server, make sure to also install the **Appium driver(s)** for your target 30 | platform(s). You can find links to all known drivers in the [Appium documentation's Ecosystem page](https://appium.io/docs/en/latest/ecosystem/drivers/). 31 | Refer to each driver's documentation for its specific requirements and setup instructions. 32 | 33 | The following driver versions are recommended for best compatibility: 34 | 35 | - [Espresso](https://github.com/appium/appium-espresso-driver): `2.23.0` or later 36 | - [UiAutomator2](https://github.com/appium/appium-uiautomator2-driver): `2.21.0` or later 37 | - [XCUITest](https://appium.github.io/appium-xcuitest-driver/latest/): `3.38.0` or later 38 | 39 | Continue with the [Installation](./installation.md) steps, or jump directly to 40 | [Starting a Session](./starting-a-session.md)! 41 | -------------------------------------------------------------------------------- /docs/session-builder/app-settings.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | 5 | title: Application Settings 6 | --- 7 | 8 | The application settings can be accessed using the button in the top-right of the Session Builder screen. 9 | 10 | ![Theme Selector Button](assets/images/theme-selector.png) 11 | 12 | The only currently available option is the ability to change the application theme. By default, the 13 | Inspector will match the system theme, but it is also possible to explicitly switch to a light or 14 | dark theme. 15 | -------------------------------------------------------------------------------- /docs/session-builder/assets/images/attach-to-session/attach-to-session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/attach-to-session/attach-to-session.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/attach-to-session/found-sessions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/attach-to-session/found-sessions.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/capability-builder/capability-builder-footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/capability-builder/capability-builder-footer.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/capability-builder/capability-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/capability-builder/capability-builder.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/capability-builder/capability-fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/capability-builder/capability-fields.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/capability-builder/capability-json-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/capability-builder/capability-json-editor.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/capability-builder/capability-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/capability-builder/capability-json.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/empty-session-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/empty-session-builder.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/saved-capability-sets/saved-caps-name-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/saved-capability-sets/saved-caps-name-editor.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/saved-capability-sets/saved-caps-set-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/saved-capability-sets/saved-caps-set-list.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/saved-capability-sets/saved-caps-sets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/saved-capability-sets/saved-caps-sets.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/server-details/advanced-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/server-details/advanced-settings.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/server-details/cloud-providers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/server-details/cloud-providers.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/server-details/default-server-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/server-details/default-server-details.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/server-details/lambdatest-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/server-details/lambdatest-details.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/server-details/server-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/server-details/server-configuration.png -------------------------------------------------------------------------------- /docs/session-builder/assets/images/theme-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-builder/assets/images/theme-selector.png -------------------------------------------------------------------------------- /docs/session-builder/attach-to-session.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | 5 | title: Attach to Session Tab 6 | --- 7 | 8 | The Attach to Session tab of the Session Builder provides the ability to connect to an existing 9 | Appium session using the Inspector. 10 | 11 | ![Attach to Session](assets/images/attach-to-session/attach-to-session.png) 12 | 13 | The Inspector will automatically try to discover existing sessions when the Attach to Session tab is 14 | opened. The dropdown can then be opened to list all the discovered sessions and their details: 15 | 16 | ![Found Sessions](assets/images/attach-to-session/found-sessions.png) 17 | 18 | The most recently created sessions will be shown at the top of the list. If exactly one session is 19 | discovered, the dropdown will also auto-populate with the details of that session. 20 | 21 | Additionally, a refresh button is available to retry the session discovery process. 22 | 23 | !!! note 24 | 25 | The session discovery process uses the current [server details](./server-details.md). Make sure 26 | to select the correct server tab and enter the expected server details before selecting the 27 | Attach to Server tab or pressing the refresh button. 28 | 29 | The footer of this screen contains a link the Appium documentation, and a single button for 30 | connecting to the selected session. 31 | -------------------------------------------------------------------------------- /docs/session-builder/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | 5 | title: Session Builder Overview 6 | --- 7 | 8 | The **Session Builder** is the default screen shown upon opening the Inspector. 9 | 10 | ![Empty Session Builder](../session-builder/assets/images/empty-session-builder.png) 11 | 12 | The user interface here can be divided into several groups: 13 | 14 | - [Application Settings](./app-settings.md) 15 | - [Configuration of Server Details](./server-details.md) 16 | - [Capability Builder tab](./capability-builder.md) 17 | - [Saved Capability Sets tab](./saved-capability-sets.md) 18 | - [Attaching to Existing Session tab](./attach-to-session.md) 19 | -------------------------------------------------------------------------------- /docs/session-builder/saved-capability-sets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Saved Capability Sets Tab 3 | --- 4 | 5 | The Saved Capability Sets tab of the Session Builder is used for listing and configuring any saved 6 | capability sets, which can be created using [the button in the footer of the Capability Builder tab](./capability-builder.md#footer). 7 | Parts of this tab are similar to the Capability Builder tab. 8 | 9 | ![Saved Capability Sets](assets/images/saved-capability-sets/saved-caps-sets.png) 10 | 11 | ## List of Saved Capability Sets 12 | 13 | The left side of this screen contains a list of all saved capability sets. The number of saved sets 14 | is also shown in the title of the Saved Capability Sets tab. 15 | 16 | ![Saved Caps Set List](assets/images/saved-capability-sets/saved-caps-set-list.png) 17 | 18 | Selecting any set populates the JSON structure on the right side with the contents of the set. There 19 | are also 2 buttons: one for opening the set in the Capability Builder tab, and one for deleting the set. 20 | 21 | ## Saved Capability Set JSON Structure 22 | 23 | The JSON structure on the right side shows the capabilities of the saved set in JSON format, exactly 24 | like [in the Capability Builder tab](./capability-builder.md#capability-json-structure). One 25 | additional functionality here is the ability to rename a saved set: 26 | 27 | ![Saved Caps Name Editor](assets/images/saved-capability-sets/saved-caps-name-editor.png) 28 | 29 | ## Footer 30 | 31 | The footer is largely similar to [that in the Capability Builder tab](./capability-builder.md#footer), 32 | with one additional button: 33 | 34 | - The _Save_ button is shown upon selecting any saved capability set, and is enabled after making 35 | any changes in its capabilities. Pressing it overwrites the capabilities in the saved set with the 36 | new changes. 37 | -------------------------------------------------------------------------------- /docs/session-builder/server-details.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Server Details 3 | --- 4 | 5 | The top of the Session Builder screen is used to configure server details - that is, how the 6 | Inspector should connect to the target Appium server. 7 | 8 | ![Server Details](./assets/images/server-details/server-configuration.png) 9 | 10 | By default, the _Appium Server_ tab is selected, which is used for connecting to a standalone local 11 | or remote Appium server. However, it is also possible to connect to a server provided by a cloud 12 | service. [See the section below for more details.](#cloud-providers) 13 | 14 | ## Default Server Detail Fields 15 | 16 | The default server details have 4 fields: 17 | 18 | ![Default Server Details](./assets/images/server-details/default-server-details.png) 19 | 20 | - **Remote Host**: the host URL of the server (default: `127.0.0.1`) 21 | - **Remote Port**: the port on which the server is running (default: `4723`) 22 | - **Remote Path**: the base path used to access the server (default: `/`) 23 | - **SSL**: whether HTTPS should be used when connecting to the server (default: `false`) 24 | 25 | If using the placeholder details, the Inspector will try to connect to `http://127.0.0.1:4723/`. 26 | If you have a locally-running Appium server that was launched with default parameters, it should 27 | also be using this address, in which case you can leave the fields unchanged. 28 | 29 | ## Cloud Providers 30 | 31 | Clicking the _Select Cloud Providers_ button opens a screen with various cloud providers that 32 | support integration through Appium Inspector: 33 | 34 | ![Cloud Providers](./assets/images/server-details/cloud-providers.png) 35 | 36 | Selecting any provider then adds a new tab next to the default _Appium Server_ tab, and switching to 37 | the provider's tab changes the available server detail fields. Different providers will have 38 | different fields - for example, LambdaTest only requires the _username_ and _access key_: 39 | 40 | ![LambdaTest Server Details](./assets/images/server-details/lambdatest-details.png) 41 | 42 | ## Advanced Settings 43 | 44 | The _Advanced Settings_ options allow further configuration of the Appium server connection: 45 | 46 | ![Advanced Settings](./assets/images/server-details/advanced-settings.png) 47 | -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/commands/command-params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/commands/command-params.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/commands/command-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/commands/command-result.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/commands/commands-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/commands/commands-tab.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/commands/opened-category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/commands/opened-category.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/gesture-editor-actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/gesture-editor-actions.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/gesture-editor-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/gesture-editor-header.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/gesture-editor-pointers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/gesture-editor-pointers.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/gesture-timeline-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/gesture-timeline-empty.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/gesture-timeline-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/gesture-timeline-full.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/gestures-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/gestures-tab.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/move-action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/move-action.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/new-gesture-builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/new-gesture-builder.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/pause-action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/pause-action.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/pointer-action-visualization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/pointer-action-visualization.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/pointer-down-action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/pointer-down-action.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/gestures/two-pointers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/gestures/two-pointers.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/app-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/app-header.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/context-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/context-group.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/multiple-contexts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/multiple-contexts.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/no-additional-contexts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/no-additional-contexts.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/quit-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/quit-button.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/record-start-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/record-start-button.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/record-stop-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/record-stop-button.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/refresh-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/refresh-button.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/refresh-source-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/refresh-source-pause.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/refresh-source-resume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/refresh-source-resume.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/search-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/search-button.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/search-inputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/search-inputs.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/search-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/search-results.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/search-reveal-element.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/search-reveal-element.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/search-send-clear-element-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/search-send-clear-element-text.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/search-tap-element.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/search-tap-element.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/system-buttons-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/system-buttons-android.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/header/system-buttons-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/header/system-buttons-ios.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/recorder/recorder-tab-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/recorder/recorder-tab-buttons.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/recorder/recorder-tab-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/recorder/recorder-tab-empty.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/recorder/recorder-tab-filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/recorder/recorder-tab-filled.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/recorder/recorder-tab-language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/recorder/recorder-tab-language.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/screenshot/app-screenshot-highlighters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/screenshot/app-screenshot-highlighters.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/screenshot/app-screenshot-landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/screenshot/app-screenshot-landscape.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/screenshot/app-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/screenshot/app-screenshot.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/screenshot/download-screenshot-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/screenshot/download-screenshot-button.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/screenshot/expanded-group-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/screenshot/expanded-group-handle.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/screenshot/interaction-mode-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/screenshot/interaction-mode-buttons.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/screenshot/toggle-element-handles-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/screenshot/toggle-element-handles-button.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/session-info/sesion-overall-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/session-info/sesion-overall-info.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/session-info/session-boilerplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/session-info/session-boilerplate.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/session-info/session-info-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/session-info/session-info-tab.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/app-source-expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/app-source-expanded.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/app-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/app-source.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/copy-attributes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/copy-attributes.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/copy-xml-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/copy-xml-button.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/download-elem-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/download-elem-screenshot.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/download-xml-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/download-xml-button.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/get-timings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/get-timings.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/selected-element.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/selected-element.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/source-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/source-tab.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/timing-values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/timing-values.png -------------------------------------------------------------------------------- /docs/session-inspector/assets/images/source/toggle-attributes-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium/appium-inspector/b6e4d379496679cc12aca27fe9bc71e6f042c8e4/docs/session-inspector/assets/images/source/toggle-attributes-button.png -------------------------------------------------------------------------------- /docs/session-inspector/commands.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | 5 | title: Commands Tab 6 | --- 7 | 8 | The Commands tab provides a way to execute various Appium driver commands through the Inspector GUI. 9 | 10 | ![Commands Tab](./assets/images/commands/commands-tab.png) 11 | 12 | All commands are grouped into various categories. Opening any category shows several buttons, each 13 | of which corresponds to an Appium driver command. 14 | 15 | ![Opened Commands Category](./assets/images/commands/opened-category.png) 16 | 17 | !!! note 18 | 19 | Commands may be driver-specific, in which case their buttons may not be visible when using 20 | other drivers. 21 | 22 | The available buttons may correspond to commands without parameters, and commands with parameters: 23 | 24 | - For a command without parameters, clicking its button will execute the command 25 | - For a command with parameters, clicking its button will open the parameter popup: 26 | 27 | ![Command Parameters](./assets/images/commands/command-params.png) 28 | 29 | Some commands may require special conditions (e.g. they are only supported in simulators). This 30 | additional information, if present, is shown as follows: 31 | 32 | - For a command without parameters, this is shown by hovering over the button 33 | - For a command with parameters, this is listed inside the parameter popup 34 | 35 | Regardless of the command type, once it is run and its execution finishes, a new popup will show the 36 | result returned by the command. 37 | 38 | ![Command Result](./assets/images/commands/command-result.png) 39 | 40 | Depending on the command, it may also trigger a refresh for the application screenshot and source. 41 | -------------------------------------------------------------------------------- /docs/session-inspector/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | 5 | title: Session Inspector Overview 6 | --- 7 | 8 | The **Session Inspector** is the screen shown when connected to a session, and provides the majority 9 | of the Inspector's functionality. 10 | 11 | ![Session Inspector](../assets/images/session-inspector.png) 12 | 13 | The user interface here can be divided into several groups: 14 | 15 | - [Header (buttons and more)](./header.md) 16 | - [The Screenshot panel](./screenshot.md) 17 | - [Source tab](./source.md) 18 | - [Commands tab](./commands.md) 19 | - [Gestures tab](./gestures.md) 20 | - [Recorder tab](./recorder.md) 21 | - [Session Information tab](./session-info.md) 22 | -------------------------------------------------------------------------------- /docs/session-inspector/recorder.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | 5 | title: Recorder Tab 6 | --- 7 | 8 | The Recorder tab is used to record various Inspector interactions into executable code, for use with 9 | various [Appium clients](https://appium.io/docs/en/latest/ecosystem/clients/). 10 | 11 | ![Empty Recorder Tab](assets/images/recorder/recorder-tab-empty.png) 12 | 13 | By default, the tab contents are empty, since recording must be manually enabled in the 14 | [Inspector header](./header.md#toggle-recorder). However, the dropdown in the top-right corner can 15 | be used in advance to select the target language for the recorded code. 16 | 17 | Refer to the [Toggle Recorder button documentation](./header.md#toggle-recorder) for a list of 18 | interactions that can be recorded. 19 | 20 | !!! tip 21 | 22 | The recording of Inspector actions does not require the Recorder tab to remain opened. 23 | 24 | Once recording is enabled and a few actions are recorded, the tab contents are populated with the 25 | generated code. 26 | 27 | ![Filled Recorder Tab](assets/images/recorder/recorder-tab-filled.png) 28 | 29 | Changing the language in this state also changes the already-recorded code to the new language. 30 | 31 | ![Recorder Tab Language Change](assets/images/recorder/recorder-tab-language.png) 32 | 33 | There are also a few management buttons shown next to the language dropdown: 34 | 35 | ![Recorder Tab Management Buttons](assets/images/recorder/recorder-tab-buttons.png) 36 | 37 | - The boilerplate toggle button allows showing or hiding additional boilerplate code. This code is 38 | also shown in the [Session Information tab](./session-info.md#session-boilerplate). 39 | - The copy button copies the currently recorded code to the clipboard. If enabled, boilerplate code 40 | is copied as well. 41 | - The clear button deletes all the currently recorded code. Note that the recording state is not changed. 42 | -------------------------------------------------------------------------------- /docs/session-inspector/session-info.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Session Information Tab 3 | --- 4 | 5 | The Session Information tab can be used as a reference for the current state of the session. 6 | 7 | ![Session Information Tab](assets/images/session-info/session-info-tab.png) 8 | 9 | It can be divided into two parts: the informational tables at the top, and the session boilerplate 10 | at the bottom. 11 | 12 | ## Informational Tables 13 | 14 | ![Session Information Tables](assets/images/session-info/sesion-overall-info.png) 15 | 16 | These tables provide general information about the session, such as its ID, URL, server details, 17 | capabilities, and so on. The _Server Details_ and _Session Details_ sub-tables can be scrolled for 18 | further information. 19 | 20 | ## Session Boilerplate 21 | 22 | ![Session Boilerplate](assets/images/session-info/session-boilerplate.png) 23 | 24 | This codeblock provides example boilerplate code that can be used to create a session with the 25 | currently used server and session details. It includes all the necessary imports, setup, and 26 | teardown for creating a session in a single, self-contained file. 27 | 28 | The copy button in the top-right corner can be used to copy the code to the clipboard, whereas the 29 | dropdown can be used to change the target language. 30 | -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "Appium Inspector", 3 | "appId": "io.appium.inspector", 4 | "asar": true, 5 | "directories": { 6 | "output": "release" 7 | }, 8 | "files": ["dist/"], 9 | "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", 10 | "fileAssociations": [ 11 | { 12 | "ext": "appiumsession", 13 | "name": "Appium Session", 14 | "role": "Editor" 15 | } 16 | ], 17 | "mac": { 18 | "category": "public.app-category.developer-tools" 19 | }, 20 | "dmg": { 21 | "contents": [ 22 | { 23 | "x": 410, 24 | "y": 150, 25 | "type": "link", 26 | "path": "/Applications" 27 | }, 28 | { 29 | "x": 130, 30 | "y": 150, 31 | "type": "file" 32 | } 33 | ] 34 | }, 35 | "win": { 36 | "target": ["nsis", "zip"] 37 | }, 38 | "nsis": { 39 | "oneClick": false 40 | }, 41 | "linux": { 42 | "target": ["AppImage", "tar.gz"], 43 | "category": "Development" 44 | }, 45 | "publish": { 46 | "provider": "github", 47 | "owner": "appium", 48 | "vPrefixedTagName": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /electron.vite.config.mjs: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import {defineConfig, externalizeDepsPlugin} from 'electron-vite'; 3 | import {join} from 'path'; 4 | import renderer from 'vite-plugin-electron-renderer'; 5 | 6 | export default defineConfig({ 7 | main: { 8 | build: { 9 | outDir: join(__dirname, 'dist', 'main'), 10 | lib: { 11 | entry: join(__dirname, 'app', 'electron', 'main', 'main.js'), 12 | }, 13 | }, 14 | // main process has a few imports from common, so this is needed 15 | resolve: { 16 | alias: { 17 | '#local-polyfills': join(__dirname, 'app', 'electron', 'renderer', 'polyfills'), 18 | }, 19 | }, 20 | plugins: [externalizeDepsPlugin()], 21 | }, 22 | preload: { 23 | build: { 24 | outDir: join(__dirname, 'dist', 'preload'), 25 | lib: { 26 | entry: join(__dirname, 'app', 'electron', 'preload', 'preload.mjs'), 27 | }, 28 | }, 29 | plugins: [externalizeDepsPlugin()], 30 | }, 31 | renderer: { 32 | build: { 33 | outDir: join(__dirname, 'dist', 'renderer'), 34 | rollupOptions: { 35 | input: { 36 | main: join(__dirname, 'app', 'common', 'index.html'), 37 | splash: join(__dirname, 'app', 'common', 'splash.html'), 38 | }, 39 | }, 40 | }, 41 | optimizeDeps: { 42 | include: ['i18next-fs-backend'], 43 | esbuildOptions: { 44 | supported: { 45 | 'top-level-await': true, 46 | }, 47 | }, 48 | }, 49 | plugins: [react(), renderer()], 50 | resolve: { 51 | alias: { 52 | '#local-polyfills': join(__dirname, 'app', 'electron', 'renderer', 'polyfills'), 53 | }, 54 | }, 55 | root: join(__dirname, 'app', 'common'), 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import appiumConfig from '@appium/eslint-config-appium-ts'; 2 | import reactPlugin from 'eslint-plugin-react'; 3 | import simpleImportSortPlugin from 'eslint-plugin-simple-import-sort'; 4 | import globals from 'globals'; 5 | 6 | export default [ 7 | ...appiumConfig, 8 | { 9 | name: 'React Plugin', 10 | files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], 11 | ...reactPlugin.configs.flat.recommended, 12 | ...reactPlugin.configs.flat['jsx-runtime'], 13 | }, 14 | { 15 | name: 'JS/TS Files', 16 | files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], 17 | languageOptions: { 18 | globals: { 19 | ...globals.browser, 20 | ...globals.node, 21 | document: 'readonly', 22 | }, 23 | }, 24 | plugins: { 25 | 'simple-import-sort': simpleImportSortPlugin, 26 | }, 27 | settings: { 28 | react: { 29 | version: 'detect', 30 | }, 31 | }, 32 | rules: { 33 | 'react/prop-types': 'off', 34 | 'simple-import-sort/imports': 'error', 35 | 'simple-import-sort/exports': 'error', 36 | }, 37 | }, 38 | { 39 | name: 'Ignores', 40 | ignores: ['**/*.xml', '**/*.html'], 41 | }, 42 | ]; 43 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | INHERIT: ./node_modules/@appium/docutils/base-mkdocs.yml 2 | site_name: Appium Inspector 3 | repo_url: https://github.com/appium/appium-inspector 4 | repo_name: appium/appium-inspector 5 | copyright: Copyright © 2012 OpenJS Foundation - Change cookie settings 6 | site_url: https://appium.github.io/appium-inspector 7 | edit_uri: edit/main/docs 8 | site_description: Appium Inspector Documentation 9 | docs_dir: docs 10 | site_dir: site 11 | 12 | theme: 13 | logo: assets/images/icon.png 14 | favicon: assets/images/icon.png 15 | custom_dir: docs/overrides 16 | extra_css: 17 | - assets/stylesheets/extra.css 18 | 19 | extra: 20 | homepage: /appium-inspector 21 | version: 22 | provider: mike 23 | social: 24 | - icon: fontawesome/brands/twitter 25 | link: https://twitter.com/AppiumDevs 26 | 27 | nav: 28 | - index.md 29 | - overview.md 30 | - Quickstart: 31 | - quickstart/index.md 32 | - quickstart/requirements.md 33 | - quickstart/installation.md 34 | - quickstart/starting-a-session.md 35 | - menu-bar.md 36 | - Session Builder: 37 | - session-builder/index.md 38 | - session-builder/app-settings.md 39 | - session-builder/server-details.md 40 | - session-builder/capability-builder.md 41 | - session-builder/saved-capability-sets.md 42 | - session-builder/attach-to-session.md 43 | - Session Inspector: 44 | - session-inspector/index.md 45 | - session-inspector/header.md 46 | - session-inspector/screenshot.md 47 | - session-inspector/source.md 48 | - session-inspector/commands.md 49 | - session-inspector/gestures.md 50 | - session-inspector/recorder.md 51 | - session-inspector/session-info.md 52 | - troubleshooting.md 53 | - contributing.md 54 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Appium Inspector Plugin 2 | 3 | [![npm version](http://img.shields.io/npm/v/appium-inspector-plugin.svg)](https://npmjs.org/package/appium-inspector-plugin) 4 | [![Downloads](http://img.shields.io/npm/dm/appium-inspector-plugin.svg)](https://npmjs.org/package/appium-inspector-plugin) 5 | 6 | A plugin that integrates the [Appium Inspector](https://github.com/appium/appium-inspector) directly 7 | into your Appium server installation, providing a web-based interface for inspecting and interacting 8 | with your application under test. 9 | 10 | ## Features 11 | 12 | - Web-based Appium Inspector interface, accessible via the Appium server's `/inspector` endpoint 13 | - Full feature parity with standalone Appium Inspector 14 | 15 | ## Installation 16 | 17 | Refer to the [Plugin Installation documentation](https://appium.github.io/appium-inspector/latest/quickstart/installation/#appium-plugin). 18 | 19 | ## Development 20 | 21 | Refer to the [Contributing documentation](https://appium.github.io/appium-inspector/latest/contributing/). 22 | 23 | ## License 24 | 25 | [Apache-2.0](https://github.com/appium/appium-inspector/blob/main/LICENSE) 26 | -------------------------------------------------------------------------------- /plugins/index.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import {fileURLToPath} from 'node:url'; 3 | 4 | import {BasePlugin} from '@appium/base-plugin'; 5 | const PLUGIN_ROOT_PATH = '/inspector'; 6 | const INDEX_HTML = 'index.html'; 7 | const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'dist-browser'); 8 | 9 | /** 10 | * Appium Inspector Plugin class 11 | * @extends {BasePlugin} 12 | */ 13 | export class AppiumInspectorPlugin extends BasePlugin { 14 | /** 15 | * Creates an instance of AppiumInspectorPlugin 16 | * @param {string} name - The name of the plugin 17 | * @param {Record} cliArgs - Command line arguments 18 | */ 19 | constructor(name, cliArgs) { 20 | super(name, cliArgs); 21 | } 22 | 23 | /** 24 | * Handles inspector page requests 25 | * @param {import('express').Request} req - Express request object 26 | * @param {import('express').Response} res - Express response object 27 | * @returns {Promise} 28 | */ 29 | static async openInspector(req, res) { 30 | const reqPath = 31 | req.path === PLUGIN_ROOT_PATH ? INDEX_HTML : req.path.substring(PLUGIN_ROOT_PATH.length); 32 | res.sendFile(reqPath, {root: ROOT_DIR}); 33 | } 34 | 35 | /** 36 | * Updates the Express server configuration 37 | * @param {import('express').Application} expressApp - Express application instance 38 | * @returns {Promise} 39 | */ 40 | static async updateServer(expressApp) { 41 | // Handle both /inspector and /inspector/* paths 42 | expressApp.all(['/inspector', '/inspector/*'], AppiumInspectorPlugin.openInspector); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appium-inspector-plugin", 3 | "version": "2025.7.1", 4 | "description": "An app inspector for use with an Appium server", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/appium/appium-inspector.git" 8 | }, 9 | "author": { 10 | "name": "Appium Developers", 11 | "url": "https://github.com/appium" 12 | }, 13 | "license": "Apache-2.0", 14 | "bugs": { 15 | "url": "https://github.com/appium/appium-inspector/issues" 16 | }, 17 | "keywords": [ 18 | "appium" 19 | ], 20 | "homepage": "https://github.com/appium/appium-inspector", 21 | "main": "index.mjs", 22 | "type": "module", 23 | "exports": { 24 | ".": { 25 | "import": "./index.mjs" 26 | }, 27 | "./package.json": "./package.json" 28 | }, 29 | "peerDependencies": { 30 | "appium": "^2.0.0 || ^3.0.0-beta.0" 31 | }, 32 | "files": [ 33 | "index.mjs", 34 | "package.json", 35 | "dist-browser", 36 | "README.md" 37 | ], 38 | "dependencies": { 39 | "@appium/base-plugin": "2.3.7" 40 | }, 41 | "devDependencies": {}, 42 | "engines": { 43 | "node": ">=20.x", 44 | "npm": ">=10.x" 45 | }, 46 | "appium": { 47 | "pluginName": "inspector", 48 | "mainClass": "AppiumInspectorPlugin" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:recommended", 4 | ":semanticCommitTypeAll(chore)", 5 | ":pinAllExceptPeerDependencies" 6 | ], 7 | "labels": ["dependencies"], 8 | "packageRules": [ 9 | { 10 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 11 | "matchCurrentVersion": "!/^0/", 12 | "automerge": true 13 | }, 14 | { 15 | "matchPackageNames": ["@types/react", "@types/react-dom", "react", "react-dom"], 16 | "matchUpdateTypes": ["major"], 17 | "enabled": false 18 | }, 19 | { 20 | "matchPackageNames": ["webdriver", "@wdio/protocols"], 21 | "groupName": "WebdriverIO-related packages", 22 | "groupSlug": "webdriverio" 23 | }, 24 | { 25 | "matchPackageNames": ["appium", "@appium/**", "!@appium/base-plugin"], 26 | "groupName": "Appium-related packages", 27 | "groupSlug": "appium" 28 | }, 29 | { 30 | "matchPackageNames": ["@appium/base-plugin"], 31 | "updateLockFiles": false 32 | } 33 | ], 34 | "baseBranches": ["main"], 35 | "semanticCommits": "enabled", 36 | "schedule": ["after 10pm", "before 5:00am"], 37 | "timezone": "America/Vancouver" 38 | } 39 | -------------------------------------------------------------------------------- /sample-session-files/corrupted.appiumsession: -------------------------------------------------------------------------------- 1 | This is not valid JSON! 2 | { 3 | 4 | asdfasdfafsd 5 | } 6 | { 7 | 1121212121 8 | -------------------------------------------------------------------------------- /sample-session-files/fake-app.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Test Webview 10 | 11 | 12 |

    Hello

    13 | 23 | 24 | 25 |
    26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
    39 | -------------------------------------------------------------------------------- /sample-session-files/fake.appiumsession: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "caps": [ 4 | { 5 | "type": "text", 6 | "name": "appium:deviceName", 7 | "value": "fake" 8 | }, 9 | { 10 | "type": "text", 11 | "name": "appium:platformVersion", 12 | "value": "1.2" 13 | }, 14 | { 15 | "type": "text", 16 | "name": "appium:automationName", 17 | "value": "fake" 18 | }, 19 | { 20 | "type": "text", 21 | "name": "appium:app", 22 | "value": "/appium-inspector/test/sample-session-files/fake-app.xml" 23 | }, 24 | { 25 | "type": "text", 26 | "name": "platformName", 27 | "value": "fake" 28 | }, 29 | { 30 | "type": "text", 31 | "name": "fakeCapability", 32 | "value": "Fake 1:21" 33 | } 34 | ], 35 | "server": { 36 | "local": {}, 37 | "remote": {}, 38 | "sauce": { 39 | "dataCenter": "us-west-1" 40 | }, 41 | "headspin": {}, 42 | "browserstack": {}, 43 | "lambdatest": {}, 44 | "advanced": {}, 45 | "bitbar": {}, 46 | "kobiton": {}, 47 | "perfecto": {}, 48 | "pcloudy": {}, 49 | "testingbot": {}, 50 | "experitest": {}, 51 | "roboticmobi": {} 52 | }, 53 | "serverType": "remote", 54 | "visibleProviders": [ 55 | "remote" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /sample-session-files/sample.appiumsession: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "caps": [ 4 | { 5 | "type": "text", 6 | "name": "fake-capability", 7 | "value": "Fake capability to prove that loading extension worked" 8 | }, 9 | { 10 | "type": "text", 11 | "name": "appium:app", 12 | "value": "storage:filename=SauceLabs-Demo-App.ipa" 13 | }, 14 | { 15 | "type": "text", 16 | "name": "platformName", 17 | "value": "iOS" 18 | }, 19 | { 20 | "type": "text", 21 | "name": "appium:deviceName", 22 | "value": "iPhone.*" 23 | }, 24 | { 25 | "type": "text", 26 | "name": "appium:platformVersion", 27 | "value": "15.2" 28 | } 29 | ], 30 | "server": { 31 | "local": {}, 32 | "remote": {}, 33 | "sauce": { 34 | "dataCenter": "us-west-1" 35 | }, 36 | "headspin": {}, 37 | "browserstack": {}, 38 | "lambdatest": {}, 39 | "advanced": {}, 40 | "bitbar": {}, 41 | "kobiton": {}, 42 | "perfecto": {}, 43 | "pcloudy": {}, 44 | "testingbot": {}, 45 | "experitest": {}, 46 | "roboticmobi": {} 47 | }, 48 | "serverType": "sauce", 49 | "visibleProviders": [ 50 | "sauce" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /scripts/crowdin-common.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import {logger} from '@appium/support'; 4 | import axios from 'axios'; 5 | import _ from 'lodash'; 6 | 7 | export const log = logger.getLogger('CROWDIN'); 8 | 9 | // https://developer.crowdin.com/api/v2/ 10 | const PROJECT_ID = process.env.CROWDIN_PROJECT_ID; 11 | const API_TOKEN = process.env.CROWDIN_TOKEN; 12 | if (!PROJECT_ID || !API_TOKEN) { 13 | throw new Error(`Both CROWDIN_PROJECT_ID and CROWDIN_TOKEN environment variables must be set`); 14 | } 15 | export const RESOURCES_ROOT = path.resolve('app', 'common', 'public', 'locales'); 16 | export const ORIGINAL_LANGUAGE = 'en'; 17 | const USER_AGENT = 'Appium Inspector CI'; 18 | const API_ROOT = 'https://api.crowdin.com/api/v2'; 19 | 20 | export async function performApiRequest(suffix = '', opts = {}) { 21 | const {method = 'GET', payload, headers, isProjectSpecific = true} = opts; 22 | const url = isProjectSpecific 23 | ? `${API_ROOT}/projects/${PROJECT_ID}${suffix}` 24 | : `${API_ROOT}${suffix}`; 25 | log.debug(`Sending ${method} request to ${url}`); 26 | if (_.isPlainObject(payload)) { 27 | log.debug(`Request payload: ${JSON.stringify(payload)}`); 28 | } 29 | return ( 30 | await axios({ 31 | method, 32 | headers: { 33 | Authorization: `Bearer ${API_TOKEN}`, 34 | 'Content-Type': 'application/json', 35 | 'User-Agent': USER_AGENT, 36 | ...(headers || {}), 37 | }, 38 | url, 39 | data: payload, 40 | }) 41 | ).data; 42 | } 43 | -------------------------------------------------------------------------------- /scripts/crowdin-update-resources.mjs: -------------------------------------------------------------------------------- 1 | import {createReadStream} from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | import {log, ORIGINAL_LANGUAGE, performApiRequest, RESOURCES_ROOT} from './crowdin-common.mjs'; 5 | 6 | const RESOURCE_NAME = 'translation.json'; 7 | const RESOURCE_PATH = path.resolve(RESOURCES_ROOT, ORIGINAL_LANGUAGE, RESOURCE_NAME); 8 | 9 | async function uploadToStorage() { 10 | log.info(`Uploading '${RESOURCE_PATH}' to Crowdin`); 11 | const {data: storageData} = await performApiRequest('/storages', { 12 | method: 'POST', 13 | headers: { 14 | 'Crowdin-API-FileName': encodeURIComponent(RESOURCE_NAME), 15 | }, 16 | payload: createReadStream(RESOURCE_PATH), 17 | isProjectSpecific: false, 18 | }); 19 | log.info(`'${RESOURCE_NAME}' has been successfully uploaded to Crowdin`); 20 | return storageData.id; 21 | } 22 | 23 | async function getFileId() { 24 | const {data: filesData} = await performApiRequest('/files'); 25 | const mainFile = filesData.map(({data}) => data).find(({name}) => name === RESOURCE_NAME); 26 | if (!mainFile) { 27 | log.debug(JSON.stringify(filesData)); 28 | throw new Error(`Cannot determine the Crowdin identifier of the '${RESOURCE_NAME}' resource`); 29 | } 30 | return mainFile.id; 31 | } 32 | 33 | async function updateFile(fileId, storageId) { 34 | log.info(`Updating the project with the newly uploaded '${RESOURCE_NAME}' instance`); 35 | await performApiRequest(`/files/${fileId}`, { 36 | method: 'PUT', 37 | payload: { 38 | storageId, 39 | }, 40 | }); 41 | } 42 | 43 | async function main() { 44 | const [storageId, fileId] = await Promise.all([uploadToStorage(), getFileId()]); 45 | await updateFile(fileId, storageId); 46 | log.info('All done'); 47 | } 48 | 49 | (async () => await main())(); 50 | -------------------------------------------------------------------------------- /scripts/sync-plugin.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import {fileURLToPath} from 'node:url'; 4 | 5 | const PROJECT_ROOT = path.dirname(fileURLToPath(import.meta.url)); 6 | const ROOT_PKG_JSON_PATH = path.resolve(PROJECT_ROOT, '..', 'package.json'); 7 | const PLUGIN_PKG_JSON_PATH = path.resolve(PROJECT_ROOT, '..', 'plugins', 'package.json'); 8 | 9 | const SYNC_PACKAGE_KEYS = [ 10 | // To update ever version release 11 | 'version', 12 | 13 | // These basic information should be the same with the top package.json 14 | 'engines', 15 | 'license', 16 | 'repository', 17 | 'author', 18 | 'bugs', 19 | 'homepage', 20 | ]; 21 | 22 | /** 23 | * Return JSON parsed contents from the given path. 24 | * @param {string} path 25 | * @returns {object} 26 | */ 27 | async function readJsonContent(jsonPath) { 28 | return JSON.parse(await fs.readFile(jsonPath, 'utf8')); 29 | } 30 | 31 | async function main() { 32 | const [rootJsonContent, pluginJsonContent] = await Promise.all( 33 | [ROOT_PKG_JSON_PATH, PLUGIN_PKG_JSON_PATH].map(readJsonContent), 34 | ); 35 | 36 | for (const key of SYNC_PACKAGE_KEYS) { 37 | pluginJsonContent[key] = rootJsonContent[key]; 38 | } 39 | 40 | // The new line in the last is to avoid prettier error. 41 | await fs.writeFile( 42 | PLUGIN_PKG_JSON_PATH, 43 | `${JSON.stringify(pluginJsonContent, null, 2)}\n`, 44 | 'utf8', 45 | ); 46 | } 47 | 48 | (async () => await main())(); 49 | -------------------------------------------------------------------------------- /test/e2e/pages/base-page-object.js: -------------------------------------------------------------------------------- 1 | export default class BasePage { 2 | constructor(client) { 3 | this.client = client; 4 | } 5 | 6 | async open(path) { 7 | const url = await this.client.getUrl(); 8 | this.originalUrl = url; 9 | await this.client.navigateTo(`${this.originalUrl}${path}`); 10 | } 11 | 12 | async goHome() { 13 | if (this.originalUrl) { 14 | await this.client.navigateTo(this.originalUrl); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/e2e/pages/utils.js: -------------------------------------------------------------------------------- 1 | export function setValueReact(selector, value) { 2 | return ` 3 | const element = document.querySelector("${selector}"); 4 | const value = "${value}"; 5 | const { set: valueSetter } = Object.getOwnPropertyDescriptor(element, 'value') || {}; 6 | const prototype = Object.getPrototypeOf(element); 7 | const { set: prototypeValueSetter } = Object.getOwnPropertyDescriptor(prototype, 'value') || {}; 8 | 9 | if (prototypeValueSetter && valueSetter !== prototypeValueSetter) { 10 | prototypeValueSetter.call(element, value); 11 | } else if (valueSetter) { 12 | valueSetter.call(element, value); 13 | } else { 14 | throw new Error('The given element does not have a value setter'); 15 | } 16 | element.dispatchEvent(new Event('input', { bubbles: true })); 17 | `; 18 | } 19 | -------------------------------------------------------------------------------- /test/e2e/pre-e2e.test.js: -------------------------------------------------------------------------------- 1 | import {fs, logger} from '@appium/support'; 2 | import {retryInterval} from 'asyncbox'; 3 | import os from 'os'; 4 | import {join} from 'path'; 5 | import {expect} from 'vitest'; 6 | 7 | const platform = os.platform(); 8 | const appName = 'inspector'; 9 | const log = logger.getLogger('E2E Test'); 10 | 11 | describe('E2E tests', function () { 12 | before(async function () { 13 | let appPath; 14 | // let args = []; 15 | if (process.env.SPECTRON_TEST_PROD_BINARIES) { 16 | if (platform === 'linux') { 17 | appPath = join( 18 | __dirname, 19 | '..', 20 | '..', 21 | appName, 22 | 'release', 23 | 'linux-unpacked', 24 | 'appium-desktop', 25 | ); 26 | } else if (platform === 'darwin') { 27 | appPath = join( 28 | __dirname, 29 | '..', 30 | '..', 31 | appName, 32 | 'release', 33 | 'mac', 34 | 'Appium.app', 35 | 'Contents', 36 | 'MacOS', 37 | 'Appium', 38 | ); 39 | } else if (platform === 'win32') { 40 | appPath = join( 41 | __dirname, 42 | '..', 43 | '..', 44 | appName, 45 | 'release', 46 | 'win-ia32-unpacked', 47 | 'Appium.exe', 48 | ); 49 | } 50 | } else { 51 | appPath = require(join(__dirname, '..', '..', 'node_modules', 'electron')); 52 | // args = [join(__dirname, '..', '..')]; 53 | } 54 | 55 | this.timeout(process.env.E2E_TIMEOUT || 60 * 1000); 56 | log.info(`Running Appium from: ${appPath}`); 57 | log.info(`Checking that "${appPath}" exists`); 58 | const applicationExists = await fs.exists(appPath); 59 | if (!applicationExists) { 60 | log.error(`Could not run tests. "${appPath}" does not exist.`); 61 | process.exit(1); 62 | } 63 | log.info(`App exists. Creating application instance`); 64 | // this.app = new Application({ 65 | // path: appPath, 66 | // env: { 67 | // FORCE_NO_WRONG_FOLDER: true, 68 | // }, 69 | // args, 70 | // }); 71 | log.info(`Application instance created. Starting app`); 72 | await this.app.start(); 73 | const client = this.app.client; 74 | log.info(`App started; waiting for splash page to go away`); 75 | await retryInterval(20, 1000, async function () { 76 | const handles = await client.getWindowHandles(); 77 | await client.switchToWindow(handles[0]); 78 | expect(await client.getUrl()).toContain('index.html'); 79 | }); 80 | log.info(`App ready for automation`); 81 | }); 82 | 83 | after(function () { 84 | if (this.app && this.app.isRunning()) { 85 | return this.app.stop(); 86 | } 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/unit/utils-other.spec.js: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from 'vitest'; 2 | 3 | import {addVendorPrefixes} from '../../app/common/renderer/utils/other'; 4 | 5 | describe('utils/other.js', function () { 6 | describe('#addVendorPrefixes', function () { 7 | it('should convert unprefixed non-standard caps to use appium prefix', function () { 8 | const caps = [{name: 'udid'}, {name: 'deviceName'}]; 9 | expect(addVendorPrefixes(caps)).toEqual([{name: 'appium:udid'}, {name: 'appium:deviceName'}]); 10 | }); 11 | 12 | it('should not convert already-prefixed or standard caps', function () { 13 | const caps = [{name: 'udid'}, {name: 'browserName'}, {name: 'goog:chromeOptions'}]; 14 | expect(addVendorPrefixes(caps)).toEqual([ 15 | {name: 'appium:udid'}, 16 | {name: 'browserName'}, 17 | {name: 'goog:chromeOptions'}, 18 | ]); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/utils-webview.spec.js: -------------------------------------------------------------------------------- 1 | import {promises as fs} from 'fs'; 2 | import {join} from 'path'; 3 | import {describe, expect, it} from 'vitest'; 4 | 5 | import {parseHtmlSource} from '../../app/common/renderer/utils/webview'; 6 | 7 | describe('webview-helpers.js', function () { 8 | describe('#parseHtmlSource', function () { 9 | it('should parse html to proper xml', async function () { 10 | const original = await fs.readFile( 11 | join(__dirname, 'mocks', 'appium.page.original.html'), 12 | 'utf8', 13 | ); 14 | const parsed = await fs.readFile(join(__dirname, 'mocks', 'appium.page.parsed.html'), 'utf8'); 15 | expect(parseHtmlSource(original)).toEqual(parsed); 16 | }); 17 | 18 | it('should do nothing if the source is already xml', function () { 19 | const basicXmlSource = ` 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | `; 31 | expect(parseHtmlSource(basicXmlSource)).toEqual(basicXmlSource); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@appium/tsconfig/tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "dist" 6 | }, 7 | "include": ["app", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.mjs: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import {join} from 'path'; 3 | import {defineConfig} from 'vite'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig(({command}) => { 7 | const commonConfig = { 8 | build: { 9 | outDir: join(__dirname, 'dist-browser'), 10 | emptyOutDir: true, 11 | minify: false, 12 | reportCompressedSize: false, 13 | target: 'es2022', 14 | }, 15 | define: { 16 | // add empty polyfills for some Node.js primitives 17 | 'process.argv': [], 18 | 'process.env': {}, 19 | }, 20 | plugins: [react()], 21 | resolve: { 22 | alias: { 23 | '#local-polyfills': join(__dirname, 'app', 'web', 'polyfills'), 24 | }, 25 | }, 26 | root: join(__dirname, 'app', 'common'), 27 | test: { 28 | restoreMocks: true, 29 | root: join(__dirname, 'test'), 30 | }, 31 | }; 32 | // workaround to prevent webdriver from bundling various Node.js imports 33 | if (command === 'build') { 34 | commonConfig.define = {...commonConfig.define, 'globalThis.window': true}; 35 | } 36 | return commonConfig; 37 | }); 38 | --------------------------------------------------------------------------------