├── .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: '
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 |
2 |
3 |
4 |
5 | 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 |
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 | } />
34 |
35 |
36 | );
37 | };
38 |
39 | export default FileUploader;
40 |
--------------------------------------------------------------------------------
/app/common/renderer/components/Inspector/HighlighterRectForBounds.jsx:
--------------------------------------------------------------------------------
1 | import InspectorCSS from './Inspector.module.css';
2 |
3 | /**
4 | * Single absolute positioned div that overlays the app screenshot and highlights the bounding
5 | * box of the element found through element search
6 | */
7 | const HighlighterRectForBounds = ({elSize, elLocation, scaleRatio, xOffset}) => (
8 |
21 | );
22 |
23 | export default HighlighterRectForBounds;
24 |
--------------------------------------------------------------------------------
/app/common/renderer/components/Inspector/HighlighterRectForElem.jsx:
--------------------------------------------------------------------------------
1 | import InspectorCSS from './Inspector.module.css';
2 |
3 | /**
4 | * Absolute positioned divs that overlay the app screenshot and highlight the bounding
5 | * boxes of the elements in the app
6 | */
7 | const HighlighterRectForElem = (props) => {
8 | const {
9 | hoveredElement = {},
10 | selectHoveredElement,
11 | unselectHoveredElement,
12 | selectedElement = {},
13 | selectElement,
14 | unselectElement,
15 | dimensions,
16 | element,
17 | } = props;
18 |
19 | const {width, height, left, top} = dimensions;
20 | const key = element.path;
21 | let highlighterClasses = [InspectorCSS['highlighter-box']];
22 |
23 | // Add class + special classes to hovered and selected elements
24 | if (hoveredElement.path === element.path) {
25 | highlighterClasses.push(InspectorCSS['hovered-element-box']);
26 | }
27 | if (selectedElement.path === element.path) {
28 | highlighterClasses.push(InspectorCSS['inspected-element-box']);
29 | }
30 |
31 | return (
32 | selectHoveredElement(key)}
35 | onMouseOut={unselectHoveredElement}
36 | onClick={() => (key === selectedElement.path ? unselectElement() : selectElement(key))}
37 | key={key}
38 | style={{left: left || 0, top: top || 0, width: width || 0, height: height || 0}}
39 | >
40 |
41 |
42 | );
43 | };
44 |
45 | export default HighlighterRectForElem;
46 |
--------------------------------------------------------------------------------
/app/common/renderer/components/Inspector/LocatorTestModal.jsx:
--------------------------------------------------------------------------------
1 | import {Button, Modal} from 'antd';
2 |
3 | import ElementLocator from './ElementLocator.jsx';
4 | import LocatedElements from './LocatedElements.jsx';
5 |
6 | const LocatorTestModal = (props) => {
7 | const {
8 | isLocatorTestModalVisible,
9 | isSearchingForElements,
10 | clearSearchResults,
11 | locatedElements,
12 | t,
13 | } = props;
14 |
15 | const onCancel = () => {
16 | const {hideLocatorTestModal} = props;
17 | hideLocatorTestModal();
18 | clearSearchResults();
19 | };
20 |
21 | const onSubmit = () => {
22 | const {locatorTestStrategy, locatorTestValue, searchForElement} = props;
23 | if (locatedElements) {
24 | onCancel();
25 | } else {
26 | searchForElement(locatorTestStrategy, locatorTestValue);
27 | }
28 | };
29 |
30 | // Footer displays all the buttons at the bottom of the Modal
31 | return (
32 |
38 | {locatedElements && (
39 | e.preventDefault() || clearSearchResults()}>{t('Back')}
40 | )}
41 |
42 | {locatedElements ? t('Done') : t('Search')}
43 |
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 | } onClick={() => copyToClipboard(code())} />
31 |
32 | ({
38 | value: fwId,
39 | label: fwClass.readableName,
40 | }))}
41 | />
42 |
43 | );
44 |
45 | return (
46 |
49 | {t('Start this Kind of Session with Code')}
50 |
51 | }
52 | extra={actionBar()}
53 | >
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default SessionCodeBox;
62 |
--------------------------------------------------------------------------------
/app/common/renderer/components/Inspector/SiriCommandModal.jsx:
--------------------------------------------------------------------------------
1 | import {Button, Input, Modal, Row} from 'antd';
2 |
3 | const SiriCommandModal = (props) => {
4 | const {siriCommandValue, setSiriCommandValue, isSiriCommandModalVisible, t} = props;
5 |
6 | const onSubmit = () => {
7 | const {applyClientMethod} = props;
8 | applyClientMethod({
9 | methodName: 'executeScript',
10 | args: ['mobile:siriCommand', [{text: siriCommandValue}]],
11 | });
12 | onCancel();
13 | };
14 |
15 | const onCancel = () => {
16 | const {hideSiriCommandModal} = props;
17 | hideSiriCommandModal();
18 | };
19 |
20 | // Footer displays all the buttons at the bottom of the Modal
21 | return (
22 |
28 | {t('Execute Command')}
29 |
30 | }
31 | >
32 |
33 | {t('Command')}
34 | setSiriCommandValue(e.target.value)}
36 | value={siriCommandValue}
37 | />
38 |
39 |
40 | );
41 | };
42 |
43 | export default SiriCommandModal;
44 |
--------------------------------------------------------------------------------
/app/common/renderer/components/Notification.jsx:
--------------------------------------------------------------------------------
1 | import {App} from 'antd';
2 | import {useEffect} from 'react';
3 |
4 | import {NOTIFICATION_EVENT} from '../utils/notification';
5 |
6 | export default function Notification() {
7 | const {notification} = App.useApp();
8 |
9 | useEffect(() => {
10 | const handleMessage = (event) => {
11 | const {args, type} = event.detail;
12 | notification[type](args);
13 | };
14 |
15 | document.addEventListener(NOTIFICATION_EVENT, handleMessage);
16 |
17 | return () => {
18 | document.removeEventListener(NOTIFICATION_EVENT, handleMessage);
19 | };
20 | }, [notification]);
21 |
22 | return null;
23 | }
24 |
--------------------------------------------------------------------------------
/app/common/renderer/components/Session/AdvancedServerParams.jsx:
--------------------------------------------------------------------------------
1 | import {Checkbox, Col, Collapse, Form, Input, Row} from 'antd';
2 |
3 | import {SERVER_TYPES} from '../../constants/session-builder';
4 | import styles from './Session.module.css';
5 |
6 | const AdvancedServerParams = ({server, setServerParam, serverType, t}) => (
7 |
8 |
9 |
10 |
16 | {serverType !== 'lambdatest' && (
17 |
18 |
19 |
22 | setServerParam(
23 | 'allowUnauthorized',
24 | e.target.checked,
25 | SERVER_TYPES.ADVANCED,
26 | )
27 | }
28 | >
29 | {t('allowUnauthorizedCerts')}
30 |
31 |
32 |
33 | )}
34 |
35 |
36 |
39 | setServerParam('useProxy', e.target.checked, SERVER_TYPES.ADVANCED)
40 | }
41 | >
42 | {t('Use Proxy')}
43 |
44 |
45 |
46 |
47 |
48 |
51 | setServerParam('proxy', e.target.value, SERVER_TYPES.ADVANCED)
52 | }
53 | placeholder={t('Proxy URL')}
54 | value={server.advanced.proxy}
55 | />
56 |
57 |
58 |
59 | ),
60 | },
61 | ]}
62 | />
63 |
64 |
65 |
66 | );
67 |
68 | export default AdvancedServerParams;
69 |
--------------------------------------------------------------------------------
/app/common/renderer/components/Session/AttachToSession.jsx:
--------------------------------------------------------------------------------
1 | import {ReloadOutlined} from '@ant-design/icons';
2 | import {Button, Card, Col, Form, Row, Select, Tooltip} from 'antd';
3 |
4 | import {getSessionInfo} from '../../utils/attaching-to-session';
5 | import SessionStyles from './Session.module.css';
6 |
7 | const AttachToSession = ({
8 | serverType,
9 | attachSessId,
10 | setAttachSessId,
11 | runningAppiumSessions,
12 | getRunningSessions,
13 | t,
14 | }) => (
15 |
17 |
18 |
19 | {t('connectToExistingSessionInstructions')}
20 |
21 | {t('selectSessionIDInDropdown')}
22 |
23 |
24 |
25 |
26 |
27 |
28 | setAttachSessId(value)}
34 | >
35 | {runningAppiumSessions
36 | .slice()
37 | .reverse()
38 | .map((session) => (
39 | // list is reversed in order to place the most recent sessions at the top
40 | // slice() is added because reverse() mutates the original array
41 |
42 | {getSessionInfo(session, serverType)}
43 |
44 | ))}
45 |
46 |
47 |
48 |
49 | }
53 | />
54 |
55 |
56 |
57 |
58 |
59 | );
60 |
61 | export default AttachToSession;
62 |
--------------------------------------------------------------------------------
/app/common/renderer/components/Session/CapabilityControl.jsx:
--------------------------------------------------------------------------------
1 | import {Input, Switch} from 'antd';
2 |
3 | import {INPUT} from '../../constants/antd-types';
4 | import {CAPABILITY_TYPES} from '../../constants/session-builder';
5 | import SessionStyles from './Session.module.css';
6 |
7 | const CapabilityControl = ({
8 | cap,
9 | onSetCapabilityParam,
10 | onPressEnter,
11 | isEditingDesiredCaps,
12 | id,
13 | t,
14 | }) => {
15 | switch (cap.type) {
16 | case CAPABILITY_TYPES.TEXT:
17 | case CAPABILITY_TYPES.FILE:
18 | return (
19 | onSetCapabilityParam(e.target.value)}
25 | onPressEnter={onPressEnter}
26 | className={SessionStyles.capsBoxFont}
27 | />
28 | );
29 | case CAPABILITY_TYPES.BOOL:
30 | return (
31 | onSetCapabilityParam(value)}
39 | />
40 | );
41 | case CAPABILITY_TYPES.NUM:
42 | return (
43 |
49 | !isNaN(parseInt(e.target.value, 10))
50 | ? onSetCapabilityParam(parseInt(e.target.value, 10))
51 | : onSetCapabilityParam(undefined)
52 | }
53 | onPressEnter={onPressEnter}
54 | className={SessionStyles.capsBoxFont}
55 | />
56 | );
57 | case CAPABILITY_TYPES.OBJECT:
58 | case CAPABILITY_TYPES.JSON_OBJECT:
59 | return (
60 | onSetCapabilityParam(e.target.value)}
68 | className={SessionStyles.capsBoxFont}
69 | />
70 | );
71 | default:
72 | throw new Error(t('invalidCapType', {type: cap.type}));
73 | }
74 | };
75 |
76 | export default CapabilityControl;
77 |
--------------------------------------------------------------------------------
/app/common/renderer/components/Session/CloudProviderSelector.jsx:
--------------------------------------------------------------------------------
1 | import {Button, Col, Modal, Row} from 'antd';
2 | import _ from 'lodash';
3 |
4 | import {BUTTON} from '../../constants/antd-types';
5 | import CloudProviders from './CloudProviders.jsx';
6 | import SessionStyles from './Session.module.css';
7 |
8 | const CloudProviderSelector = (props) => {
9 | const {visibleProviders = [], isAddingCloudProvider, stopAddCloudProvider, t} = props;
10 |
11 | const footer = (
12 |
13 | {t('Done')}
14 |
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 | toggleVisibleProvider(providerName)}
54 | >
55 | {provider.logo}
56 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 | t.key === preferredTheme)?.icon || }
44 | />
45 |
46 |
47 |
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 |
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 | 
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 |
37 |
38 | {% for toc_item in toc_item.children %}
39 | {% include "partials/toc-item.html" %}
40 | {% endfor %}
41 |
42 |
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
41 |
42 | ## Advanced Settings
43 |
44 | The _Advanced Settings_ options allow further configuration of the Appium server connection:
45 |
46 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
28 |
29 | Changing the language in this state also changes the already-recorded code to the new language.
30 |
31 | 
32 |
33 | There are also a few management buttons shown next to the language dropdown:
34 |
35 | 
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 | 
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 | 
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 | 
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 | [](https://npmjs.org/package/appium-inspector-plugin)
4 | [](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 |
--------------------------------------------------------------------------------