├── .npmrc ├── .eslintignore ├── env ├── .env-dev.js ├── .env-nsis.js └── .env.js ├── docs ├── images │ ├── screen-logs.png │ ├── screen-inspector.png │ ├── screen-new-session.png │ ├── screen-start-simple.png │ ├── screen-recorder-empty.png │ ├── screen-start-advanced.png │ ├── screen-start-presets.png │ ├── screen-recorder-detail.png │ ├── screen-inspector-and-logs.png │ └── screen-recorder-boilerplate.png └── style.md ├── app ├── renderer │ ├── images │ │ ├── appium_logo.png │ │ ├── appium_icon_big.png │ │ ├── appium_small_light.png │ │ ├── appium_small_magenta.png │ │ ├── sauce_icon.svg │ │ └── headspin_logo.svg │ ├── components │ │ ├── StartServer │ │ │ ├── SimpleTab.css │ │ │ ├── StartButton.css │ │ │ ├── DeletePresetButton.js │ │ │ ├── SavePresetButton.js │ │ │ ├── StartServer.css │ │ │ ├── SimpleTab.js │ │ │ ├── StartButton.js │ │ │ ├── shared.js │ │ │ ├── AdvancedTab.css │ │ │ ├── StartServer.js │ │ │ ├── PresetsTab.css │ │ │ └── PresetsTab.js │ │ ├── Config │ │ │ ├── Config.css │ │ │ └── Config.js │ │ ├── Stub.js │ │ ├── WrongFolder │ │ │ ├── WrongFolderStyles.css │ │ │ └── WrongFolder.js │ │ ├── Session │ │ │ ├── ServerTabAutomatic.js │ │ │ ├── ServerTabBitbar.js │ │ │ ├── ServerTabBrowserstack.js │ │ │ ├── AdvancedServerParams.js │ │ │ ├── ServerTabHeadspin.js │ │ │ ├── ServerTabTestobject.js │ │ │ ├── ServerTabCustom.js │ │ │ ├── AttachToSession.js │ │ │ ├── FormattedCaps.js │ │ │ ├── ServerTabSauce.js │ │ │ ├── Session.css │ │ │ ├── SavedSessions.js │ │ │ ├── Session.js │ │ │ └── NewSessionForm.js │ │ ├── Inspector │ │ │ ├── LocatorTestModal.js │ │ │ ├── shared.js │ │ │ ├── ElementLocator.js │ │ │ ├── HighlighterRect.js │ │ │ ├── SourceScrollButtons.js │ │ │ ├── Source.js │ │ │ ├── LocatedElements.js │ │ │ ├── RecordedActions.js │ │ │ ├── SelectedElement.js │ │ │ ├── HighlighterRects.js │ │ │ └── Screenshot.js │ │ └── ServerMonitor │ │ │ └── ServerMonitor.css │ ├── .eslintrc │ ├── actions │ │ ├── Updater.js │ │ ├── Stub.js │ │ ├── Config.js │ │ ├── shared.js │ │ ├── ServerMonitor.js │ │ └── StartServer.js │ ├── store │ │ ├── configureStore.js │ │ ├── configureStore.production.js │ │ └── configureStore.development.js │ ├── containers │ │ ├── App.js │ │ ├── ConfigPage.js │ │ ├── SessionPage.js │ │ ├── InspectorPage.js │ │ ├── StartServerPage.js │ │ └── ServerMonitorPage.js │ ├── reducers │ │ ├── Updater.js │ │ ├── index.js │ │ ├── Stub.js │ │ ├── Config.js │ │ ├── StartServer.js │ │ └── ServerMonitor.js │ ├── lib │ │ └── client-frameworks │ │ │ ├── index.js │ │ │ ├── ruby.js │ │ │ ├── js-wd.js │ │ │ ├── python.js │ │ │ ├── js-wdio.js │ │ │ ├── robot.js │ │ │ ├── framework.js │ │ │ └── java.js │ ├── routes.js │ ├── index.html │ ├── styles │ │ ├── github-gist-theme.global.css │ │ └── app.global.css │ ├── index.js │ └── util.js ├── env.js ├── shared │ └── cloud-providers │ │ ├── config.js │ │ └── index.js ├── main │ ├── auto-updater │ │ ├── update-checker.js │ │ ├── config.js │ │ └── index.js │ ├── helpers.js │ └── main.development.js └── settings.js ├── .gitignore ├── webpack.config.node.js ├── .vscode └── launch.json ├── appveyor.yml ├── test ├── e2e │ ├── pages │ │ ├── base-page-object.js │ │ ├── main-page-object.js │ │ └── inspector-page-object.js │ ├── main-e2e.test.js │ └── inspector-e2e.test.js ├── integration │ ├── update-checker.integration-test.js │ └── appium-method-handler.integration-test.js └── unit │ └── inspector-specs.js ├── .eslintrc ├── .babelrc ├── LICENSE ├── .github └── ISSUE_TEMPLATE.md ├── server.js ├── webpack.config.base.js ├── webpack.config.electron.js ├── webpack.config.development.js ├── webpack.config.production.js ├── .travis.yml ├── package.js └── CONDUCT.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | main.js 2 | -------------------------------------------------------------------------------- /env/.env-dev.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | }; -------------------------------------------------------------------------------- /docs/images/screen-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/docs/images/screen-logs.png -------------------------------------------------------------------------------- /docs/images/screen-inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/docs/images/screen-inspector.png -------------------------------------------------------------------------------- /app/renderer/images/appium_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/app/renderer/images/appium_logo.png -------------------------------------------------------------------------------- /docs/images/screen-new-session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/docs/images/screen-new-session.png -------------------------------------------------------------------------------- /docs/images/screen-start-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/docs/images/screen-start-simple.png -------------------------------------------------------------------------------- /docs/images/screen-recorder-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/docs/images/screen-recorder-empty.png -------------------------------------------------------------------------------- /docs/images/screen-start-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/docs/images/screen-start-advanced.png -------------------------------------------------------------------------------- /docs/images/screen-start-presets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/docs/images/screen-start-presets.png -------------------------------------------------------------------------------- /env/.env-nsis.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Env configuration for NSIS target 3 | */ 4 | module.exports = { 5 | NO_AUTO_UPDATE: true 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /app/renderer/images/appium_icon_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/app/renderer/images/appium_icon_big.png -------------------------------------------------------------------------------- /docs/images/screen-recorder-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/docs/images/screen-recorder-detail.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | dist 4 | release 5 | main.js 6 | main.js.map 7 | 8 | *.log 9 | .DS_Store 10 | 11 | Certificates.p12 12 | -------------------------------------------------------------------------------- /app/renderer/images/appium_small_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/app/renderer/images/appium_small_light.png -------------------------------------------------------------------------------- /docs/images/screen-inspector-and-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/docs/images/screen-inspector-and-logs.png -------------------------------------------------------------------------------- /env/.env.js: -------------------------------------------------------------------------------- 1 | let env = {}; 2 | 3 | if (process.env.TARGET) { 4 | env = require(`./.env-${process.env.TARGET}`); 5 | } 6 | 7 | export default env; -------------------------------------------------------------------------------- /app/renderer/images/appium_small_magenta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/app/renderer/images/appium_small_magenta.png -------------------------------------------------------------------------------- /docs/images/screen-recorder-boilerplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/appium-desktop/master/docs/images/screen-recorder-boilerplate.png -------------------------------------------------------------------------------- /app/renderer/components/StartServer/SimpleTab.css: -------------------------------------------------------------------------------- 1 | .form { 2 | width: 300px; 3 | margin: 0px auto; 4 | } 5 | 6 | .form :global(.ant-input-wrapper) { 7 | margin: 15px auto; 8 | } 9 | -------------------------------------------------------------------------------- /app/renderer/components/Config/Config.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | height: 100%; 4 | width: 100%; 5 | padding: 4em 2em 2em 2em; 6 | } 7 | 8 | .row { 9 | margin-bottom: 16px; 10 | } -------------------------------------------------------------------------------- /app/renderer/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-vars": ["error", {"varsIgnorePattern": "React"}], 4 | "react/jsx-uses-vars": 1 5 | }, 6 | "globals": { 7 | "document": "false" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/renderer/actions/Updater.js: -------------------------------------------------------------------------------- 1 | export const SET_UPDATE_STATE = 'SET_UPDATE_STATE'; 2 | 3 | export function setUpdateState (updateState) { 4 | return (dispatch) => { 5 | dispatch({type: SET_UPDATE_STATE, updateState}); 6 | }; 7 | } -------------------------------------------------------------------------------- /app/renderer/actions/Stub.js: -------------------------------------------------------------------------------- 1 | export const NAME_OF_ACTION_1 = 'NAME_OF_ACTION_1'; 2 | export const NAME_OF_ACTION_2 = 'NAME_OF_ACTION_2'; 3 | 4 | export function someAction () { 5 | return async (/*dispatch, getState*/) => { 6 | 7 | }; 8 | } -------------------------------------------------------------------------------- /app/renderer/components/Stub.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Stub extends Component { 4 | 5 | render () { 6 | const { someProp } = this.props; 7 | 8 | return
9 | Hello {someProp}! 10 |
; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/renderer/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.production'); // eslint-disable-line global-require 3 | } else { 4 | module.exports = require('./configureStore.development'); // eslint-disable-line global-require 5 | } 6 | -------------------------------------------------------------------------------- /app/renderer/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class App extends Component { 4 | static propTypes = { 5 | children: PropTypes.element.isRequired 6 | }; 7 | 8 | render () { 9 | return this.props.children; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/env.js: -------------------------------------------------------------------------------- 1 | let env = {}; 2 | 3 | // DefinePlugin doesn't work in dev so if _ENV_ is undefined, assume it's a development environment 4 | if (typeof(_ENV_) === "undefined") { 5 | env = require('../env/.env-dev'); 6 | } else { 7 | env = _ENV_; // eslint-disable-line no-undef 8 | } 9 | 10 | export default env; -------------------------------------------------------------------------------- /app/shared/cloud-providers/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | saucelabs: { 3 | label: 'Sauce Labs', 4 | }, 5 | testobject: { 6 | label: 'TestObject' 7 | }, 8 | browserstack: { 9 | label: 'BrowserStack', 10 | }, 11 | headspin: { 12 | label: 'HeadSpin', 13 | }, 14 | bitbar: { 15 | label: 'Bitbar' 16 | } 17 | }; -------------------------------------------------------------------------------- /webpack.config.node.js: -------------------------------------------------------------------------------- 1 | // for babel-plugin-webpack-loaders 2 | require('babel-register'); 3 | const devConfigs = require('./webpack.config.development'); 4 | 5 | module.exports = { 6 | output: { 7 | libraryTarget: 'commonjs2' 8 | }, 9 | module: { 10 | loaders: devConfigs.module.loaders.slice(1) // remove babel-loader 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /app/renderer/reducers/Updater.js: -------------------------------------------------------------------------------- 1 | import { SET_UPDATE_STATE } from '../actions/Updater'; 2 | 3 | const INITIAL_STATE = {}; 4 | 5 | export default function session (state = INITIAL_STATE, action) { 6 | switch (action.type) { 7 | case SET_UPDATE_STATE: 8 | return {...action.updateState}; 9 | default: 10 | return {...state}; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/main/auto-updater/update-checker.js: -------------------------------------------------------------------------------- 1 | import request from 'request-promise'; 2 | import { getFeedUrl } from './config'; 3 | 4 | export async function checkUpdate (currentVersion) { 5 | try { 6 | const res = await request.get(getFeedUrl(currentVersion)); 7 | if (res) { 8 | return JSON.parse(res); 9 | } 10 | } catch (ign) { } 11 | 12 | return false; 13 | } -------------------------------------------------------------------------------- /app/main/auto-updater/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | baseFeedUrl: `https://hazel-server-pxufsrwofl.now.sh`, 3 | getFeedUrl (version) { 4 | let platform = process.platform; 5 | if (platform.toLowerCase() === 'linux') { 6 | platform = 'AppImage'; 7 | } 8 | return `${config.baseFeedUrl}/update/${platform}/${version}`; 9 | } 10 | }; 11 | 12 | export default config; -------------------------------------------------------------------------------- /app/renderer/components/StartServer/StartButton.css: -------------------------------------------------------------------------------- 1 | .startButton { 2 | width: 100%; 3 | margin-top: 10px; 4 | padding: 10px !important; 5 | height: auto; 6 | font-size: 1.3em !important; 7 | } 8 | 9 | .configButton { 10 | width: 100%; 11 | margin-top: 10px; 12 | padding: 10px !important; 13 | height: auto; 14 | font-size: 1.3em !important; 15 | } 16 | -------------------------------------------------------------------------------- /app/renderer/components/WrongFolder/WrongFolderStyles.css: -------------------------------------------------------------------------------- 1 | .wrong-folder { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 1em; 5 | justify-content: center; 6 | height: 100%; 7 | } 8 | 9 | .wrong-folder div { 10 | flex-grow: 1; 11 | text-align: center; 12 | margin-bottom: 1em; 13 | font-size: 16px; 14 | color: red; 15 | } 16 | 17 | .wrong-folder div button { 18 | width: 75%; 19 | margin-bottom: 1em; 20 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "dev", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "npm", 10 | "env": { 11 | "NODE_ENV": "development" 12 | }, 13 | "runtimeArgs": [ 14 | "run-script", "dev" 15 | ], 16 | "port": 5858 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: "9" 3 | GH_TOKEN: 4 | secure: fyB6CRcrHbroxaBvzN6aPHUEhHGc3ljbbTPtEqruaDVe/iO8/dZn4LOVNS/fAlSX 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - npm install -g npm 8 | - npm ci 9 | 10 | test_script: 11 | - node --version 12 | - npm --version 13 | - npm run package-ci 14 | - npm run package-nsis 15 | 16 | build: off 17 | 18 | artifacts: 19 | - path: 'release\Appium*.exe' 20 | - path: 'release\*.yml' 21 | 22 | -------------------------------------------------------------------------------- /test/e2e/pages/base-page-object.js: -------------------------------------------------------------------------------- 1 | export default class BasePage { 2 | 3 | constructor (client) { 4 | this.client = client; 5 | this.originalUrl = client.url(); 6 | } 7 | 8 | async open (path) { 9 | const url = await this.client.url(); 10 | this.originalUrl = url.value; 11 | await this.client.url(`${this.originalUrl}${path}`); 12 | } 13 | 14 | async goHome () { 15 | if (this.originalUrl) { 16 | await this.client.url(this.originalUrl); 17 | } 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "appium", 4 | "plugins": [ 5 | "import", 6 | "react" 7 | ], 8 | "env": { 9 | "browser": true, 10 | "mocha": true, 11 | "node": true 12 | }, 13 | "ecmafeatures": { 14 | "jsx": true 15 | }, 16 | "settings": { 17 | "import/resolver": "webpack" 18 | }, 19 | "rules": { 20 | "import/no-unresolved": [2, {"ignore": ["electron"]}], 21 | "babel/arrow-parens": 0, 22 | "require-await": 2 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/renderer/containers/ConfigPage.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import * as ConfigActions from '../actions/Config'; 4 | import ConfigPage from '../components/Config/Config'; 5 | 6 | function mapStateToProps (state) { 7 | return state.config; 8 | } 9 | 10 | function mapDispatchToProps (dispatch) { 11 | return bindActionCreators(ConfigActions, dispatch); 12 | } 13 | 14 | export default connect(mapStateToProps, mapDispatchToProps)(ConfigPage); 15 | -------------------------------------------------------------------------------- /app/renderer/containers/SessionPage.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import * as SessionActions from '../actions/Session'; 4 | import Session from '../components/Session/Session'; 5 | 6 | function mapStateToProps (state) { 7 | return state.session; 8 | } 9 | 10 | function mapDispatchToProps (dispatch) { 11 | return bindActionCreators(SessionActions, dispatch); 12 | } 13 | 14 | export default connect(mapStateToProps, mapDispatchToProps)(Session); 15 | -------------------------------------------------------------------------------- /app/renderer/store/configureStore.production.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { hashHistory } from 'react-router'; 4 | import { routerMiddleware } from 'react-router-redux'; 5 | import rootReducer from '../reducers'; 6 | 7 | const router = routerMiddleware(hashHistory); 8 | 9 | const enhancer = applyMiddleware(thunk, router); 10 | 11 | export default function configureStore (initialState) { 12 | return createStore(rootReducer, initialState, enhancer); 13 | } 14 | -------------------------------------------------------------------------------- /app/renderer/containers/InspectorPage.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import * as InspectorActions from '../actions/Inspector'; 4 | import InspectorPage from '../components/Inspector/Inspector'; 5 | 6 | function mapStateToProps (state) { 7 | return state.inspector; 8 | } 9 | 10 | function mapDispatchToProps (dispatch) { 11 | return bindActionCreators(InspectorActions, dispatch); 12 | } 13 | 14 | export default connect(mapStateToProps, mapDispatchToProps)(InspectorPage); 15 | -------------------------------------------------------------------------------- /app/renderer/lib/client-frameworks/index.js: -------------------------------------------------------------------------------- 1 | import JsWdFramework from './js-wd'; 2 | import JsWdIoFramework from './js-wdio'; 3 | import JavaFramework from './java'; 4 | import PythonFramework from './python'; 5 | import RubyFramework from './ruby'; 6 | import RobotFramework from './robot'; 7 | 8 | const frameworks = { 9 | jsWd: JsWdFramework, 10 | jsWdIo: JsWdIoFramework, 11 | java: JavaFramework, 12 | python: PythonFramework, 13 | ruby: RubyFramework, 14 | robot: RobotFramework, 15 | }; 16 | 17 | export default frameworks; 18 | -------------------------------------------------------------------------------- /app/renderer/containers/StartServerPage.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import * as StartServerActions from '../actions/StartServer'; 4 | import StartServer from '../components/StartServer/StartServer'; 5 | 6 | function mapStateToProps (state) { 7 | return state.startServer; 8 | } 9 | 10 | function mapDispatchToProps (dispatch) { 11 | return bindActionCreators(StartServerActions, dispatch); 12 | } 13 | 14 | export default connect(mapStateToProps, mapDispatchToProps)(StartServer); 15 | -------------------------------------------------------------------------------- /app/renderer/containers/ServerMonitorPage.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import * as ServerActions from '../actions/ServerMonitor'; 4 | import ServerMonitor from '../components/ServerMonitor/ServerMonitor'; 5 | 6 | function mapStateToProps (state) { 7 | return state.serverMonitor; 8 | } 9 | 10 | function mapDispatchToProps (dispatch) { 11 | return bindActionCreators(ServerActions, dispatch); 12 | } 13 | 14 | export default connect(mapStateToProps, mapDispatchToProps)(ServerMonitor); 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["add-module-exports"], 4 | "env": { 5 | "production": { 6 | "presets": ["react-optimize"], 7 | "plugins": [ 8 | "babel-plugin-transform-remove-console", 9 | "babel-plugin-transform-remove-debugger", 10 | "babel-plugin-dev-expression" 11 | ] 12 | }, 13 | "development": { 14 | "presets": ["react-hmre"] 15 | }, 16 | "test": { 17 | "plugins": [ 18 | ["webpack-loaders", { "config": "webpack.config.node.js", "verbose": false }] 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/renderer/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer as routing } from 'react-router-redux'; 3 | import startServer from './StartServer'; 4 | import serverMonitor from './ServerMonitor'; 5 | import session from './Session'; 6 | import inspector from './Inspector'; 7 | import updater from './Updater'; 8 | import config from './Config'; 9 | 10 | // create our root reducer 11 | const rootReducer = combineReducers({ 12 | routing, 13 | startServer, 14 | serverMonitor, 15 | session, 16 | inspector, 17 | updater, 18 | config, 19 | }); 20 | 21 | export default rootReducer; 22 | -------------------------------------------------------------------------------- /app/renderer/reducers/Stub.js: -------------------------------------------------------------------------------- 1 | import { 2 | NAME_OF_ACTION_1, NAME_OF_ACTION_2 3 | } from '../actions/Stub'; 4 | 5 | 6 | const INITIAL_STATE = { 7 | someVariable: false, 8 | someOtherVariable: 'hello', 9 | }; 10 | 11 | export default function inspector (state=INITIAL_STATE, action) { 12 | switch (action.type) { 13 | case NAME_OF_ACTION_1: 14 | return { 15 | ...state, 16 | someVariable: true 17 | }; 18 | 19 | case NAME_OF_ACTION_2: 20 | return { 21 | ...state, 22 | someOtherVariable: 'world', 23 | }; 24 | 25 | default: 26 | return {...state}; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012-2016 JS Foundation and other contributors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /test/e2e/pages/main-page-object.js: -------------------------------------------------------------------------------- 1 | import BasePage from './base-page-object'; 2 | 3 | export default class MainPage extends BasePage { 4 | 5 | static selectors = { 6 | startServerButton: '#startServerBtn', 7 | startNewSessionButton: '#startNewSessionBtn', 8 | serverMonitorContainer: '#serverMonitorContainer', 9 | } 10 | 11 | constructor (client) { 12 | super(client); 13 | Object.assign(this, MainPage.selectors); 14 | } 15 | 16 | async startServer () { 17 | await this.client.click(this.startServerButton); 18 | } 19 | 20 | async startNewSession () { 21 | await this.client.click(this.startNewSessionButton); 22 | } 23 | } -------------------------------------------------------------------------------- /app/settings.js: -------------------------------------------------------------------------------- 1 | import settings from 'electron-settings'; 2 | import { PRESETS } from './renderer/actions/StartServer'; 3 | import { SAVED_SESSIONS, SERVER_ARGS, SESSION_SERVER_PARAMS, 4 | SESSION_SERVER_TYPE } from './renderer/actions/Session'; 5 | import { SAVED_FRAMEWORK } from './renderer/actions/Inspector'; 6 | 7 | // set default persistent settings 8 | // do it here because settings are kind of like state! 9 | settings.defaults({ 10 | [PRESETS]: {}, 11 | [SAVED_SESSIONS]: [], 12 | [SERVER_ARGS]: null, 13 | [SESSION_SERVER_PARAMS]: null, 14 | [SESSION_SERVER_TYPE]: null, 15 | [SAVED_FRAMEWORK]: 'java', 16 | }); 17 | 18 | export default settings; 19 | -------------------------------------------------------------------------------- /app/renderer/components/Session/ServerTabAutomatic.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Card } from 'antd'; 3 | import SessionStyles from './Session.css'; 4 | 5 | const FormItem = Form.Item; 6 | 7 | export default class ServerTabSauce extends Component { 8 | 9 | render () { 10 | 11 | const {server} = this.props; 12 | 13 | return
14 | 15 | 16 | {server.local.port &&

Will use currently-running Appium Desktop server at 17 | http://{server.local.hostname === "0.0.0.0" ? "localhost" : server.local.hostname}:{server.local.port} 18 |

} 19 |
20 |
21 |
; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /app/renderer/components/StartServer/DeletePresetButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Button } from 'antd'; 3 | 4 | import styles from './StartButton.css'; 5 | 6 | export default class DeletePresetButton extends Component { 7 | static propTypes = { 8 | presetDeleting: PropTypes.bool.isRequired, 9 | deletePreset: PropTypes.func.isRequired, 10 | } 11 | 12 | render () { 13 | const {presetDeleting, deletePreset} = this.props; 14 | 15 | return ( 16 |
17 | 21 |
22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/renderer/components/Session/ServerTabBitbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Input, Row, Col } from 'antd'; 3 | 4 | const FormItem = Form.Item; 5 | 6 | export default class ServerTabBitbar extends Component { 7 | 8 | render () { 9 | 10 | const { server, setServerParam } = this.props; 11 | 12 | return
13 | 14 | 15 | 16 | setServerParam('apiKey', e.target.value)} /> 17 | 18 | 19 | 20 |
; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/renderer/components/StartServer/SavePresetButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Button } from 'antd'; 3 | 4 | import styles from './StartButton.css'; 5 | 6 | export default class SavePresetButton extends Component { 7 | static propTypes = { 8 | presetSaving: PropTypes.bool.isRequired, 9 | savePreset: PropTypes.func.isRequired, 10 | } 11 | 12 | render () { 13 | const {presetSaving, savePreset} = this.props; 14 | 15 | return ( 16 |
17 | 21 | 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/renderer/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | import App from './containers/App'; 4 | import StartServerPage from './containers/StartServerPage'; 5 | import ServerMonitorPage from './containers/ServerMonitorPage'; 6 | import SessionPage from './containers/SessionPage'; 7 | import InspectorPage from './containers/InspectorPage'; 8 | import ConfigPage from './containers/ConfigPage'; 9 | 10 | export default ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /app/renderer/components/WrongFolder/WrongFolder.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'antd'; 3 | import { ipcRenderer } from 'electron'; 4 | import WrongFolderStyles from './WrongFolderStyles.css'; 5 | 6 | export default class WrongFolder extends Component { 7 | // This tells the main thread to move this to applications folder which will cause the app to close and restart 8 | moveToApplicationsFolder () { 9 | ipcRenderer.send('appium-move-to-applications-folder'); 10 | } 11 | 12 | render () { 13 | return
14 |
15 |
Appium Desktop should be run from the Applications folder
16 | 17 |
18 |
; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/renderer/reducers/Config.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_ENVIRONMENT_VARIABLE, SET_ENVIRONMENT_VARIABLES 3 | } from '../actions/Config'; 4 | 5 | 6 | const INITIAL_STATE = { 7 | environmentVariables: {}, 8 | defaultEnvironmentVariables: {}, 9 | }; 10 | 11 | export default function inspector (state=INITIAL_STATE, action) { 12 | switch (action.type) { 13 | case SET_ENVIRONMENT_VARIABLE: 14 | return { 15 | ...state, 16 | environmentVariables: { 17 | ...state.environmentVariables, 18 | [action.name]: action.value, 19 | } 20 | }; 21 | 22 | case SET_ENVIRONMENT_VARIABLES: 23 | return { 24 | ...state, 25 | environmentVariables: action.savedEnvironmentVariables || {}, 26 | defaultEnvironmentVariables: action.defaultEnvironmentVariables, 27 | }; 28 | 29 | default: 30 | return {...state}; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Appium or Appium Desktop? 2 | 3 | You are reporting an issue at the Appium Desktop repository. Appium Desktop is a wrapper around Appium. If you are having trouble running tests, it is much more likely that the problem you are encountering is not a problem with Appium Desktop but with Appium. For that reason we require you to have tried your testcase with Appium CLI before reporting issues. Check the checkbox below to confirm that you have proven your issue does _not_ reproduce on Appium itself: 4 | 5 | - [ ] I have verified that my issue does not occur with Appium and should be investigated as an Appium Desktop issue 6 | 7 | ## The problem 8 | 9 | _Replace this section with a description of your issue_ 10 | 11 | ## Environment 12 | 13 | - I am running Appium Desktop version __. 14 | - I am on (pick one): 15 | - [ ] Mac 16 | - [ ] Windows 17 | - [ ] Linux 18 | -------------------------------------------------------------------------------- /app/renderer/components/StartServer/StartServer.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | height: 100%; 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | } 9 | 10 | .tabs { 11 | width: 100%; 12 | border-bottom: 1px solid #662d91; 13 | margin-bottom: 20px; 14 | display: flex; 15 | justify-content: center; 16 | } 17 | 18 | .tabButtons { 19 | position: relative; 20 | top: 12px; 21 | } 22 | 23 | .formAndLogo { 24 | width: 100%; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: center; 28 | } 29 | 30 | .logo { 31 | width: 300px; 32 | margin-bottom: 15px; 33 | margin: 0px auto; 34 | } 35 | 36 | .failureMsg { 37 | width: 100%; 38 | padding: 5px; 39 | border: 2px solid #e47777; 40 | background-color: #d89494; 41 | color: #fff; 42 | font-weight: bold; 43 | margin: 10px 0px; 44 | } 45 | -------------------------------------------------------------------------------- /app/renderer/actions/Config.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | export const SET_ENVIRONMENT_VARIABLE = 'SET_ENVIRONMENT_VARIABLE'; 4 | export const SET_ENVIRONMENT_VARIABLES = 'SET_ENVIRONMENT_VARIABLES'; 5 | export const GET_ENVIRONMENT_VARIABLES = 'GET_ENVIRONMENT_VARIABLES'; 6 | export const SAVE_ENVIRONMENT_VARIABLES = 'SAVE_ENVIRONMENT_VARIABLES'; 7 | 8 | export function setEnvironmentVariable (name, value) { 9 | return (dispatch) => { 10 | dispatch({type: SET_ENVIRONMENT_VARIABLE, name, value}); 11 | }; 12 | } 13 | 14 | export function getEnvironmentVariables () { 15 | return (dispatch) => { 16 | dispatch({type: GET_ENVIRONMENT_VARIABLES}); 17 | ipcRenderer.send('appium-get-env'); 18 | ipcRenderer.once('appium-env', (evt, env) => { 19 | const {defaultEnvironmentVariables, savedEnvironmentVariables} = env; 20 | dispatch({type: SET_ENVIRONMENT_VARIABLES, defaultEnvironmentVariables, savedEnvironmentVariables}); 21 | }); 22 | }; 23 | } -------------------------------------------------------------------------------- /app/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Appium 6 | 7 | 8 | 18 | 19 | 20 |
21 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/renderer/store/configureStore.development.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import createLogger from 'redux-logger'; 4 | import { hashHistory } from 'react-router'; 5 | import { routerMiddleware } from 'react-router-redux'; 6 | import rootReducer from '../reducers'; 7 | 8 | const logger = createLogger({ 9 | level: 'info', 10 | collapsed: true, 11 | }); 12 | 13 | const router = routerMiddleware(hashHistory); 14 | 15 | const enhancer = compose( 16 | applyMiddleware(thunk, router, logger), 17 | window.devToolsExtension ? window.devToolsExtension() : (noop) => noop 18 | ); 19 | 20 | export default function configureStore (initialState) { 21 | const store = createStore(rootReducer, initialState, enhancer); 22 | 23 | if (module.hot) { 24 | module.hot.accept('../reducers', () => 25 | store.replaceReducer(require('../reducers')) // eslint-disable-line global-require 26 | ); 27 | } 28 | 29 | return store; 30 | } 31 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | /* eslint-disable promise/prefer-await-to-callbacks */ 3 | 4 | import express from 'express'; 5 | import webpack from 'webpack'; 6 | import webpackDevMiddleware from 'webpack-dev-middleware'; 7 | import webpackHotMiddleware from 'webpack-hot-middleware'; 8 | 9 | import config from './webpack.config.development'; 10 | 11 | const app = express(); 12 | const compiler = webpack(config); 13 | const PORT = 3000; 14 | 15 | const wdm = webpackDevMiddleware(compiler, { 16 | publicPath: config.output.publicPath, 17 | stats: { 18 | colors: true 19 | } 20 | }); 21 | 22 | app.use(wdm); 23 | 24 | app.use(webpackHotMiddleware(compiler)); 25 | 26 | const server = app.listen(PORT, 'localhost', (err) => { 27 | if (err) { 28 | console.error(err); 29 | return; 30 | } 31 | 32 | console.log(`Listening at http://localhost:${PORT}`); 33 | }); 34 | 35 | process.on('SIGTERM', () => { 36 | console.log('Stopping dev server'); 37 | wdm.close(); 38 | server.close(() => { 39 | process.exit(0); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | module: { 5 | loaders: [{ 6 | test: /\.jsx?$/, 7 | loaders: ['babel-loader'], 8 | exclude: /node_modules/ 9 | }, { 10 | test: /\.json$/, 11 | loader: 'json-loader' 12 | }, { 13 | test: /\.(jpe?g|png|gif|svg)$/i, 14 | loaders: [ 15 | 'file?hash=sha512&digest=hex&name=[hash].[ext]', 16 | 'image-webpack?bypassOnDebug&optimizationLevel=7&interlaced=false' 17 | ] 18 | }, { 19 | test: [/node_modules[\\\/](?:electron-settings|key-path-helpers)[\\\/]lib[\\\/](?:.+).js/], 20 | loaders: ['babel-loader'] 21 | }] 22 | }, 23 | output: { 24 | path: path.join(__dirname, 'dist'), 25 | filename: 'bundle.js', 26 | libraryTarget: 'commonjs2' 27 | }, 28 | resolve: { 29 | extensions: ['', '.js', '.jsx', '.json'], 30 | packageMains: ['webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main'] 31 | }, 32 | node: { 33 | __dirname: false, 34 | __filename: false 35 | }, 36 | plugins: [ 37 | 38 | ], 39 | externals: [ 40 | 41 | ] 42 | }; 43 | -------------------------------------------------------------------------------- /app/shared/cloud-providers/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import settings from 'electron-settings'; 3 | import config from './config'; 4 | 5 | class CloudProvider { 6 | 7 | constructor (providerName, providerData) { 8 | const {label} = providerData; 9 | this.name = providerName; 10 | this.label = label; 11 | } 12 | 13 | getSettingsKey (keyName) { 14 | return `Providers.${this.name}.${keyName}`; 15 | } 16 | 17 | async isVisible () { 18 | const visibilityKey = this.getSettingsKey('visible'); 19 | const isVisible = await settings.get(visibilityKey); 20 | if (!_.isBoolean(isVisible)) { 21 | await this.setVisible(true); 22 | return true; 23 | } 24 | return isVisible; 25 | } 26 | 27 | async setVisible (isVisible = true) { 28 | const visibilityKey = this.getSettingsKey('visible'); 29 | await settings.set(visibilityKey, isVisible); 30 | } 31 | 32 | } 33 | 34 | 35 | const providers = {}; 36 | for (let [providerName, providerData] of _.toPairs(config)) { 37 | providers[providerName] = new CloudProvider(providerName, providerData); 38 | } 39 | 40 | export default providers; -------------------------------------------------------------------------------- /app/renderer/images/sauce_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/renderer/components/StartServer/SimpleTab.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Input } from 'antd'; 3 | 4 | import { propTypes, updateArg } from './shared'; 5 | import StartButton from './StartButton'; 6 | import styles from './SimpleTab.css'; 7 | 8 | export default class SimpleTab extends Component { 9 | static propTypes = {...propTypes} 10 | 11 | render () { 12 | const {startServer, serverArgs, serverStarting, serverVersion} = this.props; 13 | 14 | return ( 15 |
16 |
17 | 22 | 26 |
27 | 28 |
29 |
30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/renderer/styles/github-gist-theme.global.css: -------------------------------------------------------------------------------- 1 | /** 2 | * GitHub Gist Theme 3 | * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro 4 | */ 5 | 6 | .hljs { 7 | display: block; 8 | background: white; 9 | padding: 0.5em; 10 | color: #333333; 11 | overflow-x: auto; 12 | } 13 | 14 | .hljs-comment, 15 | .hljs-meta { 16 | color: #969896; 17 | } 18 | 19 | .hljs-string, 20 | .hljs-variable, 21 | .hljs-template-variable, 22 | .hljs-strong, 23 | .hljs-emphasis, 24 | .hljs-quote { 25 | color: #df5000; 26 | } 27 | 28 | .hljs-keyword, 29 | .hljs-selector-tag, 30 | .hljs-type { 31 | color: #a71d5d; 32 | } 33 | 34 | .hljs-literal, 35 | .hljs-symbol, 36 | .hljs-bullet, 37 | .hljs-attribute { 38 | color: #0086b3; 39 | } 40 | 41 | .hljs-section, 42 | .hljs-name { 43 | color: #63a35c; 44 | } 45 | 46 | .hljs-tag { 47 | color: #333333; 48 | } 49 | 50 | .hljs-title, 51 | .hljs-attr, 52 | .hljs-selector-id, 53 | .hljs-selector-class, 54 | .hljs-selector-attr, 55 | .hljs-selector-pseudo { 56 | color: #795da3; 57 | } 58 | 59 | .hljs-addition { 60 | color: #55a532; 61 | background-color: #eaffea; 62 | } 63 | 64 | .hljs-deletion { 65 | color: #bd2c00; 66 | background-color: #ffecec; 67 | } 68 | 69 | .hljs-link { 70 | text-decoration: underline; 71 | } 72 | -------------------------------------------------------------------------------- /app/renderer/components/Session/ServerTabBrowserstack.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Input, Row, Col } from 'antd'; 3 | 4 | const FormItem = Form.Item; 5 | 6 | export default class ServerTabBrowserstack extends Component { 7 | 8 | render () { 9 | 10 | const {server, setServerParam} = this.props; 11 | 12 | return
13 | 14 | 15 | 16 | setServerParam('username', e.target.value)} /> 17 | 18 | 19 | 20 | 21 | 22 | 23 | setServerParam('accessKey', e.target.value)} /> 24 | 25 | 26 | 27 |
; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webpack.config.electron.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import baseConfig from './webpack.config.base'; 3 | import path from 'path'; 4 | 5 | export default { 6 | ...baseConfig, 7 | 8 | devtool: 'source-map', 9 | 10 | entry: ['babel-polyfill', './app/main/main.development'], 11 | 12 | output: { 13 | ...baseConfig.output, 14 | path: __dirname, 15 | filename: './main.js' 16 | }, 17 | 18 | plugins: [ 19 | new webpack.EnvironmentPlugin([ 20 | 'TARGET' 21 | ]), 22 | new webpack.BannerPlugin( 23 | 'require("source-map-support").install();', 24 | { raw: true, entryOnly: false } 25 | ), 26 | new webpack.DefinePlugin({ 27 | _ENV_: process.env.TARGET ? require(`./env/.env-${process.env.TARGET}`) : require('./env/.env') 28 | }) 29 | ], 30 | 31 | target: 'electron-main', 32 | 33 | node: { 34 | __dirname: false, 35 | __filename: false 36 | }, 37 | 38 | resolve: { 39 | packageAlias: 'main', 40 | alias: { 41 | env: path.resolve(__dirname, 'env', process.env.TARGET ? `.env-${process.env.TARGET}` : '.env'), 42 | }, 43 | }, 44 | 45 | externals: [ 46 | ...baseConfig.externals, 47 | 'font-awesome', 48 | 'source-map-support', 49 | 'appium', 50 | 'teen_process' 51 | ] 52 | }; 53 | -------------------------------------------------------------------------------- /app/renderer/components/Session/AdvancedServerParams.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Collapse, Form, Checkbox, Col, Input } from 'antd'; 3 | 4 | const {Panel} = Collapse; 5 | const FormItem = Form.Item; 6 | 7 | export default class AdvancedServerParams extends Component { 8 | 9 | 10 | render () { 11 | const {server, setServerParam} = this.props; 12 | 13 | return 14 | 15 | 16 | 17 | setServerParam('allowUnauthorized', e.target.checked, 'advanced')}>Allow Unauthorized Certificates 18 | 19 | 20 | 21 | 22 | setServerParam('useProxy', e.target.checked, 'advanced')}>Use Proxy 23 | 24 | 25 | 26 | 27 | setServerParam('proxy', e.target.value, 'advanced')} placeholder="Proxy URL" value={server.advanced.proxy} /> 28 | 29 | 30 | 31 | ; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/renderer/styles/app.global.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #222 !important; 3 | box-sizing: border-box; 4 | } 5 | 6 | #root { 7 | height: 100%; 8 | background: linear-gradient(180deg, #f9f9f9, #f2f2f2, 5%, #f2f2f2, 95%, #d9d9d9); 9 | } 10 | 11 | #root .ant-spin-nested-loading > div > .ant-spin { 12 | max-height: initial; 13 | } 14 | 15 | .window { 16 | background-color: #cde4f5 !important; 17 | width: 100%; 18 | height: 100%; 19 | } 20 | 21 | .list-group-item.active { 22 | background-color: #662d91 !important; 23 | } 24 | 25 | .ant-spin-nested-loading, .ant-spin-container { 26 | height: 100%; 27 | } 28 | 29 | .ant-notification { 30 | width: 80%; 31 | margin-right: 10%; 32 | } 33 | 34 | .inner-button { 35 | position: absolute; 36 | top: 0; 37 | right: 4px; 38 | z-index: 1000; 39 | } 40 | 41 | body::-webkit-scrollbar { 42 | width: 0px; 43 | background: transparent; 44 | } 45 | 46 | body::-webkit-scrollbar-corner { 47 | background: transparent; 48 | } 49 | 50 | .ant-input-group .ant-select { 51 | height: 100%; 52 | } 53 | 54 | .ant-input-group .ant-select-selection { 55 | border-top-left-radius: 0; 56 | border-bottom-left-radius: 0; 57 | height: 100%; 58 | padding-top: 0; 59 | } 60 | 61 | .ant-input-group .ant-select-selection .ant-select-selection-selected-value { 62 | padding-top: 2px; 63 | } 64 | 65 | .ant-input-group .select-container { 66 | height: 32px; 67 | } -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len: 0 */ 2 | import webpack from 'webpack'; 3 | import baseConfig from './webpack.config.base'; 4 | 5 | const config = { 6 | ...baseConfig, 7 | 8 | debug: true, 9 | 10 | devtool: 'cheap-module-eval-source-map', 11 | 12 | entry: [ 13 | 'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr', 14 | 'babel-polyfill', 15 | './app/renderer/index' 16 | ], 17 | 18 | output: { 19 | ...baseConfig.output, 20 | publicPath: 'http://localhost:3000/dist/' 21 | }, 22 | 23 | module: { 24 | ...baseConfig.module, 25 | loaders: [ 26 | ...baseConfig.module.loaders, 27 | 28 | { 29 | test: /\.global\.css$/, 30 | loaders: [ 31 | 'style-loader', 32 | 'css-loader?sourceMap' 33 | ] 34 | }, 35 | 36 | { 37 | test: /^((?!\.global).)*\.css$/, 38 | loaders: [ 39 | 'style-loader', 40 | 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' 41 | ] 42 | } 43 | ] 44 | }, 45 | 46 | plugins: [ 47 | ...baseConfig.plugins, 48 | new webpack.HotModuleReplacementPlugin(), 49 | new webpack.NoErrorsPlugin(), 50 | new webpack.DefinePlugin({ 51 | 'process.env.NODE_ENV': JSON.stringify('development') 52 | }) 53 | ], 54 | 55 | target: 'electron-renderer' 56 | }; 57 | 58 | export default config; 59 | -------------------------------------------------------------------------------- /app/renderer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { Router, hashHistory } from 'react-router'; 5 | import { syncHistoryWithStore } from 'react-router-redux'; 6 | import electron from 'electron'; 7 | import WrongFolder from './components/WrongFolder/WrongFolder'; 8 | import routes from './routes'; 9 | import configureStore from './store/configureStore'; 10 | import './styles/app.global.css'; 11 | import './styles/github-gist-theme.global.css'; 12 | 13 | const {app} = electron.remote; 14 | const isDev = process.env.NODE_ENV === 'development'; 15 | const store = configureStore(); 16 | const history = syncHistoryWithStore(hashHistory, store); 17 | 18 | function shouldShowWrongFolderComponent () { 19 | // If we set an ENV to require it to NOT be shown don't show it 20 | if (process.env.FORCE_NO_WRONG_FOLDER) { 21 | return false; 22 | } 23 | 24 | // Note: app.isInApplicationsFolder is undefined if it's not a Mac 25 | if (app.isInApplicationsFolder && !app.isInApplicationsFolder() && !isDev) { 26 | return true; 27 | } else if (isDev && process.env.WRONG_FOLDER) { 28 | return true; 29 | } 30 | } 31 | 32 | render( 33 | 34 | {shouldShowWrongFolderComponent() ? 35 | : 36 | 37 | } 38 | , 39 | document.getElementById('root') 40 | ); 41 | -------------------------------------------------------------------------------- /app/renderer/components/Inspector/LocatorTestModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Modal } from 'antd'; 4 | import LocatedElements from './LocatedElements'; 5 | import ElementLocator from './ElementLocator'; 6 | 7 | 8 | export default class LocatorTestModal extends Component { 9 | 10 | onSubmit () { 11 | const {locatedElements, locatorTestStrategy, locatorTestValue, searchForElement, clearSearchResults, hideLocatorTestModal} = this.props; 12 | if (locatedElements) { 13 | hideLocatorTestModal(); 14 | clearSearchResults(); 15 | } else { 16 | searchForElement(locatorTestStrategy, locatorTestValue); 17 | } 18 | } 19 | 20 | onCancel () { 21 | const {hideLocatorTestModal, clearSearchResults} = this.props; 22 | hideLocatorTestModal(); 23 | clearSearchResults(); 24 | } 25 | 26 | render () { 27 | const {isLocatorTestModalVisible, isSearchingForElements, locatedElements} = this.props; 28 | 29 | return 36 | {!locatedElements && } 37 | {locatedElements && } 38 | ; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/renderer/components/Session/ServerTabHeadspin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Row, Col, Input } from 'antd'; 3 | import SessionStyles from './Session.css'; 4 | 5 | const FormItem = Form.Item; 6 | 7 | export default class ServerTabHeadspin extends Component { 8 | 9 | render () { 10 | 11 | const {server, setServerParam} = this.props; 12 | 13 | return
14 | 15 | 16 | 17 | setServerParam('hostname', e.target.value)} size="large" /> 18 | 19 | 20 | 21 | 22 | setServerParam('port', e.target.value)} size="large" /> 23 | 24 | 25 | 26 | 27 | 28 | 29 | setServerParam('apiKey', e.target.value)} size="large" /> 30 | 31 | 32 | 33 |
; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import baseConfig from './webpack.config.base'; 4 | 5 | const config = { 6 | ...baseConfig, 7 | 8 | devtool: 'source-map', 9 | 10 | entry: ['babel-polyfill', './app/renderer/index'], 11 | 12 | output: { 13 | ...baseConfig.output, 14 | 15 | publicPath: '../dist/' 16 | }, 17 | 18 | module: { 19 | ...baseConfig.module, 20 | 21 | loaders: [ 22 | ...baseConfig.module.loaders, 23 | 24 | { 25 | test: /\.global\.css$/, 26 | loader: ExtractTextPlugin.extract( 27 | 'style-loader', 28 | 'css-loader' 29 | ) 30 | }, 31 | 32 | { 33 | test: /^((?!\.global).)*\.css$/, 34 | loader: ExtractTextPlugin.extract( 35 | 'style-loader', 36 | 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' 37 | ) 38 | } 39 | ] 40 | }, 41 | 42 | plugins: [ 43 | ...baseConfig.plugins, 44 | new webpack.optimize.OccurrenceOrderPlugin(), 45 | new webpack.DefinePlugin({ 46 | 'process.env.NODE_ENV': JSON.stringify('production') 47 | }), 48 | new webpack.optimize.UglifyJsPlugin({ 49 | compressor: { 50 | screw_ie8: true, 51 | warnings: false 52 | } 53 | }), 54 | new ExtractTextPlugin('style.css', { allChunks: true }) 55 | ], 56 | 57 | target: 'electron-renderer', 58 | }; 59 | 60 | export default config; 61 | -------------------------------------------------------------------------------- /app/renderer/components/Inspector/shared.js: -------------------------------------------------------------------------------- 1 | import { DOMParser } from 'xmldom'; 2 | import xpath from 'xpath'; 3 | 4 | export function parseCoordinates (element) { 5 | let {bounds, x, y, width, height} = element.attributes || {}; 6 | 7 | if (bounds) { 8 | let boundsArray = bounds.split(/\[|\]|,/).filter((str) => str !== ''); 9 | return {x1: boundsArray[0], y1: boundsArray[1], x2: boundsArray[2], y2: boundsArray[3]}; 10 | } else if (x) { 11 | x = parseInt(x, 10); 12 | y = parseInt(y, 10); 13 | width = parseInt(width, 10); 14 | height = parseInt(height, 10); 15 | return {x1: x, y1: y, x2: x + width, y2: y + height}; 16 | } else { 17 | return {}; 18 | } 19 | } 20 | 21 | export function isUnique (attrName, attrValue, sourceXML) { 22 | // If no sourceXML provided, assume it's unique 23 | if (!sourceXML) { 24 | return true; 25 | } 26 | const doc = new DOMParser().parseFromString(sourceXML); 27 | return xpath.select(`//*[@${attrName}="${attrValue}"]`, doc).length < 2; 28 | } 29 | 30 | // Map of the optimal strategies. 31 | const STRATEGY_MAPPINGS = [ 32 | ['name', 'accessibility id'], 33 | ['content-desc', 'accessibility id'], 34 | ['id', 'id'], 35 | ['resource-id', 'id'], 36 | ]; 37 | 38 | export function getLocators (attributes, sourceXML) { 39 | const res = {}; 40 | for (let [strategyAlias, strategy] of STRATEGY_MAPPINGS) { 41 | const value = attributes[strategyAlias]; 42 | if (value && isUnique(strategyAlias, value, sourceXML)) { 43 | res[strategy] = attributes[strategyAlias]; 44 | } 45 | } 46 | return res; 47 | } -------------------------------------------------------------------------------- /app/renderer/components/StartServer/StartButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Button, Icon } from 'antd'; 3 | import { ipcRenderer } from 'electron'; 4 | 5 | import styles from './StartButton.css'; 6 | 7 | export default class StartButton extends Component { 8 | static propTypes = { 9 | serverStarting: PropTypes.bool.isRequired, 10 | startServer: PropTypes.func.isRequired, 11 | disabledOverride: PropTypes.bool, 12 | } 13 | 14 | isEnabled () { 15 | return !(this.props.serverStarting || this.props.disabledOverride); 16 | } 17 | 18 | noop (evt) { 19 | evt.preventDefault(); 20 | } 21 | 22 | openConfig () { 23 | ipcRenderer.send('appium-open-config'); 24 | } 25 | 26 | render () { 27 | const {startServer, serverStarting, serverVersion} = this.props; 28 | const buttonProps = {}; 29 | if (!this.isEnabled()) { 30 | buttonProps.disabled = true; 31 | } 32 | 33 | return ( 34 |
35 | 42 | 43 | 48 |
49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/renderer/components/Session/ServerTabTestobject.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Input, Select, Row, Col, Checkbox } from 'antd'; 3 | 4 | const FormItem = Form.Item; 5 | 6 | export default class ServerTabTestobject extends Component { 7 | 8 | render () { 9 | 10 | const {server, setServerParam} = this.props; 11 | 12 | return
13 | 14 | 15 | 16 | setServerParam('apiKey', e.target.value)} /> 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
TestObject Data Center
25 |
26 | 30 |
31 |
32 |
33 | 34 |
35 |
; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/renderer/components/StartServer/shared.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | import { DEFAULT_ARGS } from '../../reducers/StartServer'; 3 | 4 | export const propTypes = { 5 | serverArgs: PropTypes.object.isRequired, 6 | serverStarting: PropTypes.bool.isRequired, 7 | startServer: PropTypes.func.isRequired, 8 | updateArgs: PropTypes.func.isRequired, 9 | savePreset: PropTypes.func.isRequired, 10 | presetSaving: PropTypes.bool.isRequired, 11 | deletePreset: PropTypes.func.isRequired, 12 | presetDeleting: PropTypes.bool.isRequired, 13 | }; 14 | 15 | export function updateArg (evt) { 16 | const {updateArgs} = this.props; 17 | let argName = evt.target.name; 18 | let newVal; 19 | switch (evt.target.type) { 20 | case "checkbox": 21 | newVal = evt.target.checked; 22 | break; 23 | default: 24 | newVal = evt.target.value; 25 | // if we have a string type, sometimes Appium's default value is actually 26 | // null, but our users can only make it an empty string, so convert it 27 | if (newVal === "" && DEFAULT_ARGS[argName] === null) { 28 | newVal = null; 29 | } 30 | // likewise if we have a string type, but Appium's defult value is 31 | // actually a number, convert it. For now assume that will be an integer 32 | // since Appium currently doesn't accept any non-integer numeric 33 | // arguments. 34 | if (typeof newVal === "string" && 35 | typeof DEFAULT_ARGS[argName] === "number") { 36 | newVal = parseInt(newVal, 10); 37 | } 38 | break; 39 | } 40 | updateArgs({[argName]: newVal}); 41 | } 42 | -------------------------------------------------------------------------------- /test/integration/update-checker.integration-test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import sinon from 'sinon'; 4 | import request from 'request-promise'; 5 | import { checkUpdate } from '../../app/main/auto-updater/update-checker'; 6 | 7 | chai.should(); 8 | chai.use(chaiAsPromised); 9 | 10 | describe('updateChecker', function () { 11 | let latestVersion; 12 | 13 | before(async function () { 14 | const latestReleaseUrl = `https://api.github.com/repos/appium/appium-desktop/releases/latest?access_token=${process.env.GITHUB_TOKEN}`; 15 | const res = JSON.parse(await request.get(latestReleaseUrl, {headers: {'user-agent': 'node.js'}})); 16 | latestVersion = res.name; 17 | }); 18 | 19 | describe('.checkUpdate', function () { 20 | it('not find anything if latest release is same as current release', async function () { 21 | await checkUpdate(latestVersion).should.eventually.equal(false); 22 | }); 23 | it('should find something if latest release is different from current release', async function () { 24 | const {name, notes, pub_date, url} = await checkUpdate('v0.0.0'); 25 | name.should.be.a.string; 26 | notes.should.be.a.string; 27 | pub_date.should.be.a.string; 28 | url.should.be.a.string; 29 | }); 30 | it('should return false if request for update throws error', async function () { 31 | let promiseStub = sinon.stub(request, 'get', () => { throw new Error(`Failed Request`); }); 32 | await checkUpdate('v0.0.0').should.eventually.be.false; 33 | promiseStub.restore(); 34 | }); 35 | }); 36 | }); -------------------------------------------------------------------------------- /app/renderer/actions/shared.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | import UUID from 'uuid'; 3 | import Promise from 'bluebird'; 4 | 5 | const clientMethodPromises = {}; 6 | 7 | export function bindClient () { 8 | /** 9 | * When we hear back from the main process, resolve the promise 10 | */ 11 | ipcRenderer.on('appium-client-command-response', (evt, resp) => { 12 | // Rename 'id' to 'elementId' 13 | resp.elementId = resp.id; 14 | let promise = clientMethodPromises[resp.uuid]; 15 | if (promise) { 16 | promise.resolve(resp); 17 | delete clientMethodPromises[resp.uuid]; 18 | } 19 | }); 20 | 21 | /** 22 | * If we hear back with an error, reject the promise 23 | */ 24 | ipcRenderer.on('appium-client-command-response-error', (evt, resp) => { 25 | const {e, uuid} = resp; 26 | let promise = clientMethodPromises[uuid]; 27 | if (promise) { 28 | promise.reject(new Error(e)); 29 | delete clientMethodPromises[uuid]; 30 | } 31 | }); 32 | } 33 | 34 | export function unbindClient () { 35 | ipcRenderer.removeAllListeners('appium-client-command-response'); 36 | ipcRenderer.removeAllListeners('appium-client-command-response-error'); 37 | } 38 | 39 | export function callClientMethod (params) { 40 | if (!ipcRenderer) { 41 | throw new Error('Cannot call ipcRenderer from main context'); 42 | } 43 | let uuid = UUID.v4(); 44 | let promise = new Promise((resolve, reject) => clientMethodPromises[uuid] = {resolve, reject}); 45 | ipcRenderer.send('appium-client-command-request', { 46 | ...params, 47 | uuid, 48 | }); 49 | return promise; 50 | } -------------------------------------------------------------------------------- /app/renderer/components/Session/ServerTabCustom.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Col, Input, Checkbox } from 'antd'; 3 | import SessionStyles from './Session.css'; 4 | 5 | const FormItem = Form.Item; 6 | 7 | export default class ServerTabCustom extends Component { 8 | 9 | render () { 10 | 11 | const {server, setServerParam} = this.props; 12 | 13 | return
14 | 15 | 16 | setServerParam('hostname', e.target.value)} size="large" /> 17 | 18 | 19 | 20 | 21 | setServerParam('port', e.target.value)} size="large" /> 22 | 23 | 24 | 25 | 26 | setServerParam('path', e.target.value)} size="large" /> 27 | 28 | 29 | 30 | 31 | setServerParam('ssl', e.target.checked)}>SSL 32 | 33 | 34 |
; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/renderer/components/Session/AttachToSession.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Card, Select, Button } from 'antd'; 3 | import SessionCSS from './Session.css'; 4 | 5 | const FormItem = Form.Item; 6 | 7 | function formatCaps (caps) { 8 | let importantCaps = [caps.app, caps.platformName, caps.deviceName]; 9 | if (caps.automationName) { 10 | importantCaps.push(caps.automationName); 11 | } 12 | return importantCaps.join(', ').trim(); 13 | } 14 | 15 | export default class AttachToSession extends Component { 16 | 17 | render () { 18 | let {attachSessId, setAttachSessId, runningAppiumSessions, getRunningSessions} = this.props; 19 | return (
20 | 21 | 22 |

If you have an already-running session of the above server type, you can attach an inspector to it directly.
Select the Session ID in the dropdown below.

23 |
24 |
25 | 26 | 36 |
37 |
39 |
40 |
); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/renderer/components/StartServer/AdvancedTab.css: -------------------------------------------------------------------------------- 1 | .advancedForm { 2 | width: 100%; 3 | } 4 | 5 | .inputSection { 6 | height: 360px; 7 | border-bottom: 1px solid #662d91; 8 | overflow: scroll; 9 | padding: 0px 10px; 10 | } 11 | 12 | .inputSection::-webkit-scrollbar { 13 | height: 0px; 14 | width: 0px; 15 | } 16 | 17 | .actions { 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | flex-direction: row; 22 | flex-wrap: wrap; 23 | flex-flow: row wrap; 24 | } 25 | 26 | .actions div { 27 | width: 220px; 28 | margin: 10px; 29 | } 30 | 31 | .input { 32 | width: 300px; 33 | margin: 5px 15px; 34 | margin-left: 0px; 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: center; 38 | } 39 | 40 | .advancedForm :global(.ant-checkbox-wrapper) { 41 | width: 300px; 42 | } 43 | 44 | .secTitle { 45 | font-size: 1.2em; 46 | color: #333; 47 | margin-bottom: 12px; 48 | width: 300px; 49 | } 50 | 51 | .secBody { 52 | display: flex; 53 | justify-content: flex-start; 54 | align-items: center; 55 | flex-direction: row; 56 | flex-wrap: wrap; 57 | flex-flow: row wrap; 58 | margin-bottom: 15px; 59 | } 60 | 61 | .secBody:after { 62 | width: 300px; 63 | margin-right: 15px; 64 | content: ""; 65 | } 66 | 67 | .presetActions { 68 | margin-top: 10px; 69 | display: flex; 70 | flex-direction: row; 71 | align-content: center; 72 | justify-content: center; 73 | } 74 | 75 | .presetActions button { 76 | margin: 5px; 77 | } 78 | 79 | .savePresetSuccess { 80 | width: 100%; 81 | font-size: 5em; 82 | font-style: italic; 83 | text-align: center; 84 | color: #999; 85 | } 86 | 87 | .comingSoon { 88 | font-style: italic; 89 | color: #666; 90 | } 91 | 92 | :global(.ant-modal) { 93 | width: auto; 94 | margin: auto; 95 | } 96 | -------------------------------------------------------------------------------- /app/renderer/components/Session/FormattedCaps.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import formatJSON from 'format-json'; 3 | import SessionCSS from './Session.css'; 4 | import { Card, Button, Alert, Tooltip } from 'antd'; 5 | import { getCapsObject } from '../../actions/Session.js'; 6 | 7 | export default class NewSessionForm extends Component { 8 | 9 | getFormattedJSON (caps) { 10 | return formatJSON.plain(getCapsObject(caps)); 11 | } 12 | 13 | render () { 14 | const {caps, title, isEditingDesiredCaps, startDesiredCapsEditor, abortDesiredCapsEditor, saveRawDesiredCaps, setRawDesiredCaps, rawDesiredCaps, 15 | isValidCapsJson, invalidCapsJsonReason} = this.props; 16 | return caps && 18 |
19 | {isEditingDesiredCaps && 20 |
29 | {isEditingDesiredCaps &&
30 |