├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── integration.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── CI └── beta-release-script.sh ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── doc └── assets │ ├── ic_baseband.svg │ ├── ic_battery.svg │ ├── ic_biometrics.svg │ ├── ic_camera.svg │ ├── ic_clipboard.svg │ ├── ic_diskIO.svg │ ├── ic_fullscreen.svg │ ├── ic_gamepad.svg │ ├── ic_hq.svg │ ├── ic_id.svg │ ├── ic_installation.svg │ ├── ic_keymapping.svg │ ├── ic_keymapping_desactivate.svg │ ├── ic_location.svg │ ├── ic_nav_android_back.svg │ ├── ic_nav_android_home.svg │ ├── ic_nav_android_multiapp.svg │ ├── ic_network.svg │ ├── ic_power.svg │ ├── ic_resolution.svg │ ├── ic_rotation.svg │ ├── ic_screencast.svg │ ├── ic_sound_down.svg │ ├── ic_sound_up.svg │ ├── ic_text_and_call.svg │ └── screenshot.png ├── example ├── geny-window.css ├── geny-window.html ├── geny-window.js └── logo.png ├── gulp ├── graspify-squery.js └── gulp-template-collector.js ├── gulpfile.js ├── index.d.ts ├── package.json ├── src ├── APIManager.js ├── DeviceRenderer.js ├── DeviceRendererFactory.js ├── assets │ ├── fonts │ │ ├── lato-regular.css │ │ └── lato_regular.ttf │ └── images │ │ ├── ic-close-popup-default.svg │ │ ├── ic-close-popup-hover.svg │ │ ├── ic_alert.svg │ │ ├── ic_baseband.svg │ │ ├── ic_battery.svg │ │ ├── ic_battery_charging.svg │ │ ├── ic_battery_empty.svg │ │ ├── ic_battery_not_charging.svg │ │ ├── ic_biometrics.svg │ │ ├── ic_biometrics_auto_validation.svg │ │ ├── ic_biometrics_auto_validation_black.svg │ │ ├── ic_camera.svg │ │ ├── ic_camera_mic.svg │ │ ├── ic_check.svg │ │ ├── ic_click_to_display_default.svg │ │ ├── ic_click_to_display_hover.svg │ │ ├── ic_clipboard.svg │ │ ├── ic_close.svg │ │ ├── ic_cloud_upload.svg │ │ ├── ic_cloud_upload_congralutation.svg │ │ ├── ic_cloud_upload_failed.svg │ │ ├── ic_copy.svg │ │ ├── ic_diskIO.svg │ │ ├── ic_disk_IO.svg │ │ ├── ic_error_default.svg │ │ ├── ic_feedback.svg │ │ ├── ic_feedback_hover.svg │ │ ├── ic_fingerprint_active.svg │ │ ├── ic_fingerprint_dirty.svg │ │ ├── ic_fingerprint_inactive.svg │ │ ├── ic_fingerprint_insufficient.svg │ │ ├── ic_fingerprint_partial.svg │ │ ├── ic_fingerprint_recognized.svg │ │ ├── ic_fingerprint_toofast.svg │ │ ├── ic_fingerprint_unrecognized.svg │ │ ├── ic_fullscreen.svg │ │ ├── ic_fullscreen_active.svg │ │ ├── ic_gamepad.svg │ │ ├── ic_generate.svg │ │ ├── ic_hq.svg │ │ ├── ic_icon_mute.svg │ │ ├── ic_id.svg │ │ ├── ic_installation.svg │ │ ├── ic_keymapping.svg │ │ ├── ic_keymapping_desactivate.svg │ │ ├── ic_location.svg │ │ ├── ic_nav_android_back.svg │ │ ├── ic_nav_android_home.svg │ │ ├── ic_nav_android_multiapp.svg │ │ ├── ic_network-type-2G-edge.svg │ │ ├── ic_network-type-2G-gprs.svg │ │ ├── ic_network-type-2G-gsm.svg │ │ ├── ic_network-type-3G.svg │ │ ├── ic_network-type-4G.svg │ │ ├── ic_network-type-5G.svg │ │ ├── ic_network-type-unlimited.svg │ │ ├── ic_network.svg │ │ ├── ic_pixelperfect_active.svg │ │ ├── ic_pixelperfect_active_black.svg │ │ ├── ic_pixelperfect_inactive.svg │ │ ├── ic_power.svg │ │ ├── ic_resolution.svg │ │ ├── ic_rotation.svg │ │ ├── ic_screencast.svg │ │ ├── ic_screenshot.svg │ │ ├── ic_sound_active_black.svg │ │ ├── ic_sound_down.svg │ │ ├── ic_sound_inactive.svg │ │ ├── ic_sound_up.svg │ │ ├── ic_spinner-material.svg │ │ ├── ic_text_and_call.svg │ │ ├── ic_textandcall.svg │ │ ├── ic_warning.svg │ │ ├── ic_world.svg │ │ └── loader.svg ├── index.js ├── plugins │ ├── BasebandRIL.js │ ├── Battery.js │ ├── ButtonsEvents.js │ ├── Camera.js │ ├── Clipboard.js │ ├── CoordinateUtils.js │ ├── FileUpload.js │ ├── FingerPrint.js │ ├── Fullscreen.js │ ├── GPS.js │ ├── Gamepad.js │ ├── GamepadManager.js │ ├── IOThrottling.js │ ├── Identifiers.js │ ├── KeyboardEvents.js │ ├── KeyboardMapping.js │ ├── MediaManager.js │ ├── MouseEvents.js │ ├── MultiTouchEvents.js │ ├── Network.js │ ├── PeerConnectionStats.js │ ├── Phone.js │ ├── Screencast.js │ ├── StreamBitrate.js │ ├── StreamResolution.js │ └── util │ │ ├── OverlayPlugin.js │ │ ├── ToolBarManager.js │ │ ├── components.js │ │ ├── iothrottling-profiles.js │ │ ├── mobile-signal-strength.js │ │ └── network-mobile-profiles.js ├── scss │ ├── base │ │ ├── _animations.scss │ │ ├── _genymotion.scss │ │ └── _variables.scss │ ├── components │ │ ├── _baseband.scss │ │ ├── _battery.scss │ │ ├── _chipTag.scss │ │ ├── _clipboard.scss │ │ ├── _dropdown.scss │ │ ├── _fingerprints.scss │ │ ├── _fullscreen.scss │ │ ├── _gps.scss │ │ ├── _imei.scss │ │ ├── _iothrottling.scss │ │ ├── _network.scss │ │ ├── _phone.scss │ │ ├── _screencast.scss │ │ ├── _slider.scss │ │ ├── _switchButton.scss │ │ ├── _textArea.scss │ │ ├── _textInput.scss │ │ ├── _toolbar.scss │ │ ├── _turn.scss │ │ ├── _upload.scss │ │ ├── _uploader.scss │ │ └── _widgetwindow.scss │ └── main.scss ├── store │ └── index.js ├── utils │ └── helpers.js └── worker │ └── FileUploaderWorker.js ├── tests ├── config.js ├── mocks │ └── DeviceRenderer.js └── unit │ ├── apiManager.test.js │ ├── baseband.test.js │ ├── battery.test.js │ ├── buttonsevent.test.js │ ├── camera.test.js │ ├── clipboard.test.js │ ├── fileupload.test.js │ ├── fingerprint.test.js │ ├── fullscreen.test.js │ ├── gps.test.js │ ├── identifiers.test.js │ ├── iothrottling.test.js │ ├── mediamanager.test.js │ ├── network.test.js │ ├── phone.test.js │ └── streambitrate.test.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | test/reports/* 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 3 | "plugins": ["prettier"], 4 | "env": { 5 | "browser": true, 6 | "es6": true, 7 | "node": true, 8 | "jest": true 9 | }, 10 | "parserOptions": { 11 | "ecmaVersion": 2020 12 | }, 13 | "rules": { 14 | "prettier/prettier": [ 15 | "error", 16 | { 17 | "insertPragma": true, 18 | "requirePragma": true, 19 | "nestedBinaryExpressions": false, 20 | "enforceForArrowConditionals": false 21 | } 22 | ], 23 | "comma-spacing": ["error", {"before": false, "after": true}], 24 | "no-console": "warn", 25 | "no-extra-parens": "off", 26 | "no-loss-of-precision": "error", 27 | "no-promise-executor-return": "off", 28 | "no-template-curly-in-string": "error", 29 | "no-unreachable-loop": "error", 30 | "require-atomic-updates": "error", 31 | "no-useless-backreference": "error", 32 | // Best practices 33 | "array-callback-return": ["error", {"checkForEach": true}], 34 | "block-scoped-var": "error", 35 | "consistent-return": "error", 36 | "curly": "error", 37 | "default-case": "error", 38 | "default-case-last": "error", 39 | "default-param-last": "error", 40 | "dot-notation": "error", 41 | "eqeqeq": "error", 42 | "no-constructor-return": "error", 43 | "no-else-return": "error", 44 | "no-extra-bind": "error", 45 | "no-multi-spaces": "error", 46 | "no-underscore-dangle": "error", 47 | "no-sequences": "error", 48 | "no-unused-expressions": "warn", 49 | "prefer-regex-literals": "warn", 50 | "valid-jsdoc": ["error", {"requireReturn": false}], 51 | // Strict mode 52 | "strict": "error", 53 | // Variables 54 | "no-shadow": "error", 55 | "no-undefined": "error", 56 | "no-use-before-define": "error", 57 | // Stylistic issues 58 | "array-bracket-newline": ["error", "consistent"], 59 | "array-bracket-spacing": ["error", "never"], 60 | "array-element-newline": ["error", "consistent"], 61 | "block-spacing": "error", 62 | "brace-style": "error", 63 | "camelcase": ["error", {"ignoreDestructuring": true, "ignoreGlobals": true}], 64 | "comma-dangle": ["error", "only-multiline"], 65 | "computed-property-spacing": ["error", "never", {"enforceForClassMembers": true}], 66 | "consistent-this": ["error", "self"], 67 | "eol-last": ["error", "always"], 68 | "func-call-spacing": ["error", "never"], 69 | "func-style": ["error", "declaration", {"allowArrowFunctions": true}], 70 | "indent": ["error", 4, {"SwitchCase": 1}], 71 | "keyword-spacing": "error", 72 | "linebreak-style": ["error", "unix"], 73 | "max-len": ["error", 120, {"ignoreComments": true}], 74 | "multiline-comment-style": ["error", "starred-block"], 75 | "new-cap": ["error", {"properties": false}], 76 | "new-parens": "error", 77 | "newline-per-chained-call": "off", 78 | "no-multi-assign": "error", 79 | "no-multiple-empty-lines": ["error", {"max": 1}], 80 | "no-trailing-spaces": "error", 81 | "object-curly-spacing": ["error", "never"], 82 | "padded-blocks": ["error", "never"], 83 | "quotes": ["error", "single", {"avoidEscape": true}], 84 | "semi": ["error", "always"], 85 | "semi-style": ["error", "last"], 86 | "space-before-function-paren": [ 87 | "error", 88 | { 89 | "anonymous": "always", 90 | "named": "never", 91 | "asyncArrow": "never" 92 | } 93 | ], 94 | "space-in-parens": ["error", "never"], 95 | "spaced-comment": ["error", "always"], 96 | // ECMAScript 6 97 | "arrow-parens": ["error", "always"], 98 | "arrow-spacing": "error", 99 | "no-confusing-arrow": "error", 100 | "no-var": "error", 101 | "prefer-const": "error", 102 | "prefer-spread": "error" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | - [ ] I've read & comply with the [contributing guidelines](https://github.com/Genymobile/genymotion-device-web-player/blob/main/CONTRIBUTING.md) 4 | - [ ] I've searched open issues if my demand is already reported. 5 | 6 | ## Description 7 | 8 | Description of the bug or feature / support request. 9 | 10 | ## Steps to Reproduce 11 | 12 | 1. [First Step] 13 | 2. [Second Step] 14 | 3. [and so on...] 15 | 16 | **Expected behavior:** 17 | 18 | What you expected to happen. 19 | 20 | **Actual behavior:** 21 | 22 | What actually happened. 23 | 24 | ## Versions / environment 25 | 26 | If you are reporting a bug, please describe your environement and the versions you are using (you can get this information from executing `npm version`). 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed if any. Please also include relevant motivation and context (a screenshot is appreciated). 4 | 5 | Fixes # 6 | 7 | ## Type of change 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | 13 | ## Checklist 14 | 15 | - [ ] I've read & comply with the [contributing guidelines](https://github.com/Genymobile/genymotion-device-web-player/blob/main/CONTRIBUTING.md) 16 | - [ ] I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers. 17 | - [ ] I have made corresponding changes to the documentation (README.md). 18 | - [ ] I've checked my modifications for any breaking changes. 19 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Integration 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [20.x] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'yarn' 27 | - run: yarn install 28 | - run: yarn validate 29 | - run: yarn test 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run lints & tests then create a GitHub release and publish a package to NPM 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Release 5 | 6 | on: 7 | push: 8 | tags: [v*] 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | registry-url: https://registry.npmjs.org/ 19 | cache: 'yarn' 20 | - name: Build project 21 | run: | 22 | yarn install 23 | yarn validate 24 | yarn test 25 | yarn build 26 | - name: Create GitHub Release 27 | uses: actions/create-release@v1 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | with: 31 | tag_name: ${{ github.ref }} 32 | release_name: Release ${{ github.ref }} 33 | draft: false 34 | prerelease: false 35 | 36 | - name: NPM Release 37 | run: yarn publish --access public 38 | env: 39 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,jetbrains,linux,osx,virtualenv 3 | 4 | # Build artifact 5 | /dist 6 | 7 | ### Node ### 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # test report directory 25 | tests/reports 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules 41 | jspm_packages 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional REPL history 47 | .node_repl_history 48 | 49 | ### Microsoft ### 50 | # Visual Studio Code 51 | .vscode/ 52 | 53 | ### JetBrains ### 54 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 55 | .idea/* 56 | *.iml 57 | 58 | ## File-based project format: 59 | *.iws 60 | 61 | ## Plugin-specific files: 62 | 63 | # IntelliJ 64 | /out/ 65 | 66 | # mpeltonen/sbt-idea plugin 67 | .idea_modules/ 68 | 69 | # JIRA plugin 70 | atlassian-ide-plugin.xml 71 | 72 | # Crashlytics plugin (for Android Studio and IntelliJ) 73 | com_crashlytics_export_strings.xml 74 | crashlytics.properties 75 | crashlytics-build.properties 76 | fabric.properties 77 | 78 | ### JetBrains Patch ### 79 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 80 | 81 | # *.iml 82 | # modules.xml 83 | 84 | 85 | ### Linux ### 86 | *~ 87 | 88 | # temporary files which can be created if a process still has a handle open of a deleted file 89 | .fuse_hidden* 90 | 91 | # KDE directory preferences 92 | .directory 93 | 94 | # Linux trash folder which might appear on any partition or disk 95 | .Trash-* 96 | 97 | 98 | ### OSX ### 99 | *.DS_Store 100 | .AppleDouble 101 | .LSOverride 102 | 103 | # Icon must end with two \r 104 | Icon 105 | 106 | 107 | # Thumbnails 108 | ._* 109 | 110 | # Files that might appear in the root of a volume 111 | .DocumentRevisions-V100 112 | .fseventsd 113 | .Spotlight-V100 114 | .TemporaryItems 115 | .Trashes 116 | .VolumeIcon.icns 117 | .com.apple.timemachine.donotpresent 118 | 119 | # Directories potentially created on remote AFP share 120 | .AppleDB 121 | .AppleDesktop 122 | Network Trash Folder 123 | Temporary Items 124 | .apdisk 125 | 126 | 127 | ### VirtualEnv ### 128 | # Virtualenv 129 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 130 | .Python 131 | [Bb]in 132 | [Ii]nclude 133 | [Ll]ib 134 | [Ll]ib64 135 | [Ll]ocal 136 | [Ss]cripts 137 | pyvenv.cfg 138 | .venv 139 | pip-selfcheck.json 140 | 141 | 142 | ### Other ### 143 | # Swap files 144 | *.swp 145 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "consistent", 8 | "jsxSingleQuote": true, 9 | "trailingComma": "all", 10 | "bracketSpacing": false, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "always", 13 | "proseWrap": "preserve", 14 | "htmlWhitespaceSensitivity": "css", 15 | "endOfLine": "lf", 16 | "embeddedLanguageFormatting": "auto" 17 | } 18 | -------------------------------------------------------------------------------- /CI/beta-release-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | SCRIPT_PATH="/tmp/version.py" 4 | 5 | cat < $SCRIPT_PATH 6 | 7 | import json 8 | import sys 9 | 10 | with open("./package.json", "r") as package: 11 | package_info = json.loads(package.read()) 12 | version = package_info["version"] 13 | 14 | print(version) 15 | 16 | EOF 17 | 18 | VERSION=$(python3 $SCRIPT_PATH) 19 | sed -r -i "s/\"version\": \"[^\"]+\"/\"version\": \"$VERSION\"/g" bower.json package.json 20 | sed -r -i "s/device-web-player\@.+\/dist/device-web-player@$VERSION\/dist/g" README.md 21 | 22 | FILENAME="beta-device-web-player-$VERSION.tar.gz" 23 | BRANCH_NAME="beta-release-$GIT_BRANCH-$VERSION" 24 | TAG="beta-release-v$VERSION" 25 | 26 | mkdir beta-release 27 | yarn install 28 | yarn build 29 | yarn pack --filename $FILENAME 30 | mv $FILENAME beta-release 31 | 32 | git add --force beta-release/$FILENAME 33 | git add --force dist 34 | 35 | git stash push dist/ beta-release/$FILENAME 36 | 37 | git checkout -b $BRANCH_NAME 38 | git rm -rf . 39 | git clean -fdx 40 | 41 | git stash pop 42 | git add --force beta-release/$FILENAME 43 | git add --force dist 44 | git checkout $GIT_BRANCH LICENSE 45 | git checkout $GIT_BRANCH package.json 46 | 47 | git commit -m "[DONOTMERGE] add yarn tar.gz" 48 | git push origin $BRANCH_NAME --force 49 | 50 | REPO_URL=$(git config --get remote.origin.url | sed 's|git@\(.*\):|https://\1/|') 51 | COMMIT=$(git rev-parse HEAD) 52 | 53 | echo base branch : $GIT_BRANCH 54 | echo package URL : git+$REPO_URL#$COMMIT 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to Genymotion device web player 2 | 3 | Welcome and thank you for considering contributing to Genymotion device web player! 4 | 5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this 6 | open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and 7 | helping you finalize your pull requests. 8 | 9 | ## Ground Rules 10 | 11 | - Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. See our [Code of Conduct](https://github.com/Genymobile/genymotion-device-web-player/blob/main/CODE_OF_CONDUCT.md). 12 | - Each pull request should implement **ONE** feature or bugfix. If you want to add or fix more than one thing, submit more than one pull request. 13 | - Do not commit changes to files that are irrelevant to your feature or bugfix. 14 | - Do not add unnecessary dependencies. 15 | 16 | ## Getting started 17 | 18 | ### Discuss about ideas 19 | 20 | If you want to add a feature, it's often best to talk about it before starting to work on it and submitting a pull 21 | request. It's not mandatory at all, but feel free to open an issue to present your idea. 22 | 23 | ### Developement 24 | 25 | To start working on the Genymotion device web player, all you need if an HTML page to instanciate a player from your 26 | local copy of this repository. 27 | Build the player in dev mode: 28 | 29 | ```sh 30 | yarn build:dev 31 | ``` 32 | 33 | And import your local file directly 34 | 35 | ```html 36 | 37 | 38 | ``` 39 | 40 | Don't forget to re-run the build command each time you make a modification to the code. 41 | 42 | #### Running tests 43 | 44 | ```sh 45 | # run tests 46 | yarn test 47 | 48 | # validate coding style 49 | yarn lint 50 | yarn checkstyle 51 | ``` 52 | 53 | ### How to submit a contribution 54 | 55 | The general process to submit a contribution is as follow: 56 | 57 | 1. Create your own fork of the code 58 | 2. Do the changes in your fork 59 | 3. Open a pull request. Make sure to fill the [pull request description](https://github.com/Genymobile/genymotion-device-web-player/blob/main/.github/PULL_REQUEST_TEMPLATE.md) properly. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Genymobile 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "genymotion-webrtc-client", 3 | "main": ["dist/js/device-renderer.min.js", "dist/css/device-renderer.min.css"], 4 | "moduleType": "globals", 5 | "version": "4.1.5" 6 | } 7 | -------------------------------------------------------------------------------- /doc/assets/ic_baseband.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_battery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_biometrics.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_diskIO.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_gamepad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_hq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_id.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_installation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_keymapping.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_keymapping_desactivate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_location.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_nav_android_back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_nav_android_home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_nav_android_multiapp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_network.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_power.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_resolution.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_rotation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_screencast.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_sound_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_sound_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/ic_text_and_call.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Genymobile/genymotion-device-web-player/affe1a7e28a3f2825690a5ce7bff2d43f5af21ab/doc/assets/screenshot.png -------------------------------------------------------------------------------- /example/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Genymobile/genymotion-device-web-player/affe1a7e28a3f2825690a5ce7bff2d43f5af21ab/example/logo.png -------------------------------------------------------------------------------- /gulp/graspify-squery.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const transformTools = require('browserify-transform-tools'); 4 | 5 | const options = { 6 | jsFilesOnly: true, 7 | }; 8 | 9 | /** 10 | * String transform is called per-module. 11 | * In that, as many `require` calls you have, that many times it is called. 12 | * If you replace `require` call, transform for that module won't be called. 13 | * 14 | * Transform can be called globally as `browserify -g graspify`. 15 | * In that case transform is called for each nested module as well, 16 | * getting global config, defined in package.json. 17 | * 18 | * `opts.config` is taken from package.json, can be whether object or array. 19 | * `opts.opts` is taken as browserify transform param. 20 | * 21 | * Module taken from https://www.npmjs.com/package/graspify 22 | */ 23 | module.exports = transformTools.makeStringTransform('graspify', options, (content, opts, done) => { 24 | try { 25 | // Normalize plain replacements 26 | opts.opts = opts.opts ? (Array.isArray(opts.opts) ? opts.opts : [opts.opts]) : []; 27 | opts.config = opts.config ? (Array.isArray(opts.config) ? opts.config : [opts.config]) : []; 28 | 29 | // Merge opts & config for the full list of replacements an loop over 30 | [].concat(opts.opts, opts.config).forEach((args) => { 31 | // args is not an array but an object with numeric key (0,1) so args[0] works 32 | const selector = args[0]; 33 | const replacement = args[1]; 34 | 35 | content = content.replace(selector.substring(1, selector.length), replacement); 36 | }); 37 | 38 | done(null, content); 39 | } catch (e) { 40 | done(e); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /gulp/gulp-template-collector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * gulp/gulp-template-collector.js 5 | * Gulp plugin to collect all template resources 6 | */ 7 | 8 | const through = require('through2'); 9 | const File = require('vinyl'); 10 | const path = require('path'); 11 | 12 | /** 13 | * templateCollector 14 | * Collect / combine all resources for an individual template 15 | * 16 | * @param {Object} options {Object} 17 | * @param {string} options.file concat all templates into single file 18 | * @return {Object} Gulp plugin 19 | */ 20 | module.exports = function templateCollector(options) { 21 | const templates = {}; 22 | options = options || {}; 23 | options.file = options.file || 'templates.js'; 24 | 25 | return through.obj( 26 | function transform(file, enc, done) { 27 | // Ensure file, not directory 28 | if (file.isNull()) { 29 | return done(); 30 | } 31 | 32 | // Break up filename 33 | const pieces = file.relative.split(path.sep); 34 | const template = pieces[0]; 35 | const fileName = pieces[1]; 36 | 37 | // Capture contents 38 | if (!templates[template]) { 39 | templates[template] = {}; 40 | } 41 | templates[template][fileName.split('.')[1]] = file.contents.toString(); 42 | 43 | return done(); 44 | }, 45 | function flush(done) { 46 | const templateBlob = JSON.stringify(templates); 47 | this.push( 48 | new File({ 49 | path: options.file, 50 | contents: Buffer.from(templateBlob), 51 | }), 52 | ); 53 | done(); 54 | }, 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | type KeyEffectDistance = { 2 | distanceX: number; 3 | distanceY: number; 4 | }; 5 | 6 | interface KeyEffect { 7 | initialX: number; 8 | initialY: number; 9 | name?: string; 10 | description?: string; 11 | } 12 | 13 | interface KeyList { 14 | keys: Key[]; 15 | name?: string; 16 | description?: string; 17 | } 18 | 19 | interface Key { 20 | key: string; 21 | effect: E; 22 | name?: string; 23 | description?: string; 24 | } 25 | 26 | interface KeyMappingConfig { 27 | dpad?: KeyList[]; 28 | tap?: Key[]; 29 | swipe?: Key[]; 30 | } 31 | 32 | type DeviceRendererKeyMapping = { 33 | enable(isEnable: boolean): void; 34 | setConfig(config: KeyMappingConfig): void; 35 | activeKeyMappingDebug(isTraceActivate?: boolean, isGridActivate?: boolean): void; 36 | }; 37 | 38 | type VmEvent = 39 | | 'baseband' 40 | | 'battery' 41 | | 'beforeunload' 42 | | 'diskio' 43 | | 'fingerprint' 44 | | 'framework' 45 | | 'gps' 46 | | 'network_profile' 47 | | 'settings' 48 | | 'systempatcher' 49 | | 'vinput' 50 | | string; // This list is not exhaustive and should be completed 51 | 52 | type VmCommunication = { 53 | disconnect(): void; 54 | addEventListener(event: VmEvent, callback: (msg: string) => void): void; 55 | sendData(data: {channel: string; messages: string[]}): void; 56 | }; 57 | 58 | type RegisteredFunctionDoc = { 59 | apiName: string; 60 | apiDescription: string; 61 | }; 62 | 63 | type Utils = { 64 | getRegisteredFunctions(): RegisteredFunctionDoc[]; 65 | }; 66 | 67 | type Media = { 68 | mute(): void; 69 | unmute(): void; 70 | }; 71 | 72 | type Video = { 73 | fullscreen: () => void; 74 | }; 75 | 76 | type Template = 77 | | 'bootstrap' 78 | | 'fullscreen' 79 | | 'fullwindow' 80 | | 'renderer' 81 | | 'renderer_minimal' 82 | | 'renderer_no_toolbar' 83 | | 'renderer_partial'; 84 | 85 | interface RendererSetupOptions { 86 | baseband?: boolean; // Default: false 87 | battery?: boolean; // Default: true 88 | biometrics?: boolean; // Default: true 89 | camera?: boolean; // Default: true 90 | capture?: boolean; // Default: true 91 | clipboard?: boolean; // Default: true 92 | connectionFailedURL?: string; 93 | diskIO?: boolean; // Default: true 94 | fileUpload?: boolean; // Default: true 95 | fileUploadUrl?: string; 96 | fullscreen?: boolean; // Default: true 97 | gamepad?: boolean; // Default: true 98 | giveFeedbackLink?: string; 99 | gps?: boolean; // Default: true 100 | gpsSpeedSupport?: boolean; // Default: false 101 | i18n?: Record; 102 | identifiers?: boolean; // Default: true 103 | keyboard?: boolean; // Default: true 104 | keyboardMapping?: boolean; // Default: true 105 | microphone?: boolean; // Default: false 106 | mobilethrottling?: boolean; // Default: false 107 | mouse?: boolean; // Default: true 108 | navbar?: boolean; // Default: true 109 | network?: boolean; // Default: true 110 | phone?: boolean; // Default: true 111 | power?: boolean; // Default: true 112 | rotation?: boolean; // Default: true 113 | streamBitrate?: boolean; // Default: false 114 | streamResolution?: boolean; // Default: true 115 | stun?: {urls?: string[]}; 116 | template?: Template; // Default: 'renderer' 117 | token?: string; 118 | touch?: boolean; // Default: true 119 | turn?: { 120 | urls?: string[]; 121 | username?: string; 122 | credential?: string; 123 | default?: boolean; // Default: false 124 | }; 125 | volume?: boolean; // Default: true 126 | } 127 | 128 | type DefaultTrue = B extends void 129 | ? T // Key is not present 130 | : B extends true | undefined 131 | ? T // Key is true or undefined 132 | : B extends false 133 | ? undefined // Key is false 134 | : T | undefined; // Key is true, false or undefined (we cannot infer anything) 135 | 136 | type ExtractKey = O extends {[P in K]: infer T} ? T : void; 137 | 138 | type DeviceRendererApi = { 139 | keyMapping: DefaultTrue, DeviceRendererKeyMapping>; 140 | media: Media; 141 | utils: Utils; 142 | video?: Video; // Available if any plugin (e.g. fullscreen) using it is enabled. 143 | VM_communication: VmCommunication; 144 | }; 145 | 146 | declare class DeviceRendererFactory { 147 | constructor(); 148 | setupRenderer( 149 | targetElement: HTMLDivElement, 150 | webrtcAddress: string, 151 | options?: O, 152 | ): DeviceRendererApi; 153 | } 154 | 155 | export {DeviceRendererApi, DeviceRendererFactory, RendererSetupOptions, KeyMappingConfig}; 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@genymotion/device-web-player", 3 | "version": "4.1.5", 4 | "description": "Genymotion Virtual Device Web Renderer", 5 | "main": "dist/js/device-renderer.min.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "types": "dist/index.d.ts", 10 | "engines": { 11 | "node": ">=20" 12 | }, 13 | "scripts": { 14 | "test": "jest --config tests/config.js", 15 | "lint": "eslint src tests gulp", 16 | "checkstyle": "eslint -f checkstyle -o ./tests/reports/lint/report.xml src tests gulp", 17 | "validate": "yarn lint && yarn checkstyle", 18 | "build:dev": "gulp build", 19 | "build": "gulp build --production", 20 | "start": "gulp serve" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/Genymobile/genymotion-device-web-player.git" 25 | }, 26 | "author": "Genymotion team (https://www.genymotion.com)", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/Genymobile/genymotion-device-web-player/issues" 30 | }, 31 | "homepage": "https://github.com/Genymobile/genymotion-device-web-player#readme", 32 | "dependencies": { 33 | "gulp-header": "^2.0.9", 34 | "gulp-replace": "^1.1.4", 35 | "lodash": "^4.17.21", 36 | "loglevel": "^1.8.0" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.18.6", 40 | "@babel/preset-env": "^7.18.6", 41 | "@babel/register": "^7.18.6", 42 | "babel-eslint": "^10.1.0", 43 | "browser-sync": "^2.27.10", 44 | "browserify": "^17.0.0", 45 | "browserify-transform-tools": "^1.7.0", 46 | "del": "^6.1.1", 47 | "eslint": "^7.32.0", 48 | "eslint-config-prettier": "^9.1.0", 49 | "eslint-plugin-jest": "^24.7.0", 50 | "eslint-plugin-prettier": "^5.1.3", 51 | "estraverse-fb": "^1.3.2", 52 | "grasp": "^0.6.0", 53 | "gulp": "^4.0.2", 54 | "gulp-autoprefixer": "^7.0.1", 55 | "gulp-babel": "^8.0.0", 56 | "gulp-clean-css": "^4.3.0", 57 | "gulp-concat": "^2.6.1", 58 | "gulp-connect": "^5.7.0", 59 | "gulp-css-base64": "^2.0.0", 60 | "gulp-html-to-js": "0.0.7", 61 | "gulp-htmlmin": "^5.0.1", 62 | "gulp-if": "^3.0.0", 63 | "gulp-inject": "^5.0.5", 64 | "gulp-inline-fonts": "^1.2.1", 65 | "gulp-sass": "^5.1.0", 66 | "gulp-streamify": "^1.0.2", 67 | "gulp-tap": "^2.0.0", 68 | "gulp-uglify-es": "^2.0.0", 69 | "gulp-using": "^0.1.1", 70 | "gulp-util": "^3.0.8", 71 | "jest": "^26.6.3", 72 | "jest-junit": "^12.3.0", 73 | "merge2": "^1.4.1", 74 | "prettier": "^3.3.2", 75 | "sass": "^1.53.0", 76 | "through2": "^4.0.2", 77 | "typescript": "^5.5.4", 78 | "vinyl": "^2.2.1", 79 | "vinyl-source-stream": "^2.0.0" 80 | }, 81 | "jest-junit": { 82 | "suiteName": "jest tests", 83 | "outputDirectory": "tests/reports", 84 | "outputName": "junit.xml" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/assets/fonts/lato_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Genymobile/genymotion-device-web-player/affe1a7e28a3f2825690a5ce7bff2d43f5af21ab/src/assets/fonts/lato_regular.ttf -------------------------------------------------------------------------------- /src/assets/images/ic-close-popup-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/images/ic-close-popup-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/images/ic_alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/ic_baseband.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_battery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_battery_charging.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_battery_empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_battery_not_charging.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_biometrics.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_camera_mic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_cloud_upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | Group 5 9 | Created with Sketch. 10 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/images/ic_cloud_upload_congralutation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | Group 5 9 | Created with Sketch. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/assets/images/ic_cloud_upload_failed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/images/ic_copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_diskIO.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_disk_IO.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_feedback.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/images/ic_feedback_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/images/ic_fingerprint_active.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/images/ic_fingerprint_dirty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | 14 | 16 | 19 | 21 | 27 | 29 | 31 | 34 | 40 | 46 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/assets/images/ic_fingerprint_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/images/ic_fingerprint_partial.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 13 | 16 | 19 | 20 | 22 | 26 | 28 | 30 | 34 | 36 | 37 | 39 | 40 | -------------------------------------------------------------------------------- /src/assets/images/ic_fingerprint_recognized.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | 13 | 15 | 17 | 19 | 22 | 24 | 27 | 30 | 33 | 36 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/assets/images/ic_fingerprint_unrecognized.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | 12 | 15 | 17 | 21 | 24 | 27 | 29 | 32 | 34 | 43 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/assets/images/ic_fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_fullscreen_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_gamepad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_generate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_hq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_icon_mute.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/images/ic_id.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_installation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_keymapping.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_keymapping_desactivate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_location.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_nav_android_back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_nav_android_home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_nav_android_multiapp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_network-type-2G-edge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_network-type-2G-gprs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_network-type-2G-gsm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/ic_network-type-3G.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_network-type-4G.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_network-type-5G.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_network-type-unlimited.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_network.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_pixelperfect_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ic_pixelperfect_active 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/images/ic_pixelperfect_active_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ic_pixelperfect_active_black 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/images/ic_pixelperfect_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ic_pixelperfect_inactive 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/images/ic_power.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_resolution.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_rotation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_screencast.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_screenshot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_sound_active_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ic_sound_active_black 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/images/ic_sound_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_sound_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ic_sound_inactive 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/images/ic_sound_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_spinner-material.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/ic_text_and_call.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_textandcall.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic_warning.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/images/ic_world.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DeviceRenderer = require('./DeviceRenderer'); 4 | const DeviceRendererFactory = require('./DeviceRendererFactory'); 5 | const CoordinateUtils = require('./plugins/CoordinateUtils'); 6 | const KeyboardEvents = require('./plugins/KeyboardEvents'); 7 | const MouseEvents = require('./plugins/MouseEvents'); 8 | const MediaManager = require('./plugins/MediaManager'); 9 | const GamepadManager = require('./plugins/GamepadManager'); 10 | 11 | module.exports = { 12 | DeviceRenderer, 13 | DeviceRendererFactory, 14 | CoordinateUtils, 15 | KeyboardEvents, 16 | MouseEvents, 17 | MediaManager, 18 | GamepadManager, 19 | }; 20 | -------------------------------------------------------------------------------- /src/plugins/Camera.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const OverlayPlugin = require('./util/OverlayPlugin'); 4 | 5 | const log = require('loglevel'); 6 | log.setDefaultLevel('debug'); 7 | 8 | /** 9 | * Instance camera plugin. 10 | * Provides client webcam and camera control. 11 | */ 12 | module.exports = class Camera extends OverlayPlugin { 13 | static get name() { 14 | return 'Camera'; 15 | } 16 | /** 17 | * Plugin initialization. 18 | * 19 | * @param {Object} instance Associated instance. 20 | * @param {Object} i18n Translations keys for the UI. 21 | */ 22 | constructor(instance, i18n) { 23 | super(instance); 24 | 25 | // Reference instance 26 | this.instance = instance; 27 | this.i18n = i18n || {}; 28 | 29 | // Register plugin 30 | this.instance.camera = this; 31 | 32 | // Display widget 33 | this.registerToolbarButton(); 34 | } 35 | 36 | /** 37 | * Add the button to the renderer toolbar. 38 | */ 39 | registerToolbarButton() { 40 | this.toolbarBtn = this.instance.toolbarManager.registerButton({ 41 | id: this.constructor.name, 42 | iconClass: this.instance.options.microphone ? 'gm-camera-mic-button' : 'gm-camera-button', 43 | title: this.instance.options.microphone 44 | ? this.i18n.CAMERA_MIC_TITLE || 'Camera and microphone injection' 45 | : this.i18n.CAMERA_TITLE || 'Camera injection', 46 | onClick: () => this.instance.mediaManager.toggleVideoStreaming(), 47 | }); 48 | } 49 | 50 | enable() { 51 | const videoCapabilities = RTCRtpSender.getCapabilities('video'); 52 | if (videoCapabilities.codecs.some((codec) => codec.mimeType === 'video/H264')) { 53 | super.enable(); 54 | } else { 55 | this.toolbarBtn.disable(); 56 | } 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/plugins/Fullscreen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Display fullscreen plugin. 5 | * Gives the ability to display the renderer on the whole screen. 6 | */ 7 | module.exports = class Fullscreen { 8 | static get name() { 9 | return 'Fullscreen'; 10 | } 11 | /** 12 | * Plugin initialization. 13 | * 14 | * @param {Object} instance Associated instance. 15 | * @param {Object} i18n Translations keys for the UI. 16 | */ 17 | constructor(instance, i18n) { 18 | // Reference instance 19 | this.instance = instance; 20 | this.i18n = i18n || {}; 21 | 22 | // we register for fullscreen event changes 23 | this.instance.addListener(document, 'webkitfullscreenchange', this.onFullscreenEvent.bind(this), false); 24 | this.instance.addListener(document, 'fullscreenchange', this.onFullscreenEvent.bind(this), false); 25 | 26 | this.instance.apiManager.registerFunction({ 27 | name: 'fullscreen', 28 | category: 'video', 29 | fn: () => { 30 | if (this.fullscreenEnabled()) { 31 | this.exitFullscreen(); 32 | } else { 33 | this.goFullscreen(this.instance.root); 34 | } 35 | }, 36 | description: 37 | // eslint-disable-next-line max-len 38 | 'Toggle fullscreen mode for the video player. If the player is currently in fullscreen, it will exit fullscreen; otherwise, it will enter fullscreen mode.', 39 | }); 40 | // Display widget 41 | this.registerToolbarButton(); 42 | } 43 | 44 | /** 45 | * Add the button to the renderer toolbar. 46 | */ 47 | registerToolbarButton() { 48 | this.toolbarBtn = this.instance.toolbarManager.registerButton({ 49 | id: this.constructor.name, 50 | iconClass: 'gm-fullscreen-button', 51 | title: this.i18n.FULLSCREEN || 'Fullscreen', 52 | onClick: () => { 53 | if (this.fullscreenEnabled()) { 54 | this.exitFullscreen(); 55 | } else { 56 | this.goFullscreen(this.instance.root); 57 | } 58 | }, 59 | }); 60 | } 61 | 62 | /** 63 | * Enter fullscreen mode. 64 | * 65 | * @param {HTMLElement} element DOM element to set fullscreen on. 66 | */ 67 | goFullscreen(element) { 68 | this.instance.wrapper.classList.add('gm-fullscreen'); 69 | this.toolbarBtn.setActive(); 70 | if (element.requestFullscreen) { 71 | element.requestFullscreen(); 72 | } else if (element.webkitRequestFullscreen) { 73 | element.webkitRequestFullscreen(); 74 | } 75 | } 76 | 77 | /** 78 | * Determine whether fullscreen mode is active or not. 79 | * 80 | * @return {boolean} Whether fullscreen mode is active or not. 81 | */ 82 | fullscreenEnabled() { 83 | return document.fullscreenElement || document.webkitFullscreenElement; 84 | } 85 | 86 | /** 87 | * Exit fullscreen mode. 88 | */ 89 | exitFullscreen() { 90 | this.instance.wrapper.classList.remove('gm-fullscreen'); 91 | this.toolbarBtn.setActive(false); 92 | if (!this.fullscreenEnabled()) { 93 | return; // do not try to remove fulllscreen if it is not active 94 | } 95 | if (document.exitFullscreen) { 96 | document.exitFullscreen(); 97 | } else if (document.webkitExitFullscreen) { 98 | document.webkitExitFullscreen(); 99 | } 100 | } 101 | 102 | /** 103 | * Fullscreen event listener. 104 | */ 105 | onFullscreenEvent() { 106 | // if we lose fullscreen, we have to make sure that it has correctly exited 107 | if (!this.fullscreenEnabled()) { 108 | this.exitFullscreen(); 109 | } 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /src/plugins/MultiTouchEvents.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Instance multi touch plugin. 5 | * Forward multi touch events to instance. 6 | */ 7 | module.exports = class MultiTouchEvents { 8 | static get name() { 9 | return 'MultiTouchEvents'; 10 | } 11 | /** 12 | * Plugin initialization. 13 | * 14 | * @param {Object} instance Associated instance. 15 | */ 16 | constructor(instance) { 17 | // Reference instance 18 | this.instance = instance; 19 | 20 | // Register plugin 21 | this.instance.touchEvents = this; 22 | this.instance.touchEventsEnabled = true; 23 | } 24 | 25 | /** 26 | * Called when the user starts touching the screen 27 | * 28 | * @param {Event} event Event. 29 | */ 30 | onScreenTouchStart(event) { 31 | event.preventDefault(); 32 | event.stopPropagation(); 33 | 34 | const json = {type: 'MULTI_TOUCH', nb: event.targetTouches.length, mode: 0, points: []}; 35 | for (let i = 0; i < event.targetTouches.length; i++) { 36 | const touch = event.targetTouches[i]; 37 | 38 | this.instance.x = this.instance.coordinateUtils.getXCoordinate(touch); 39 | this.instance.y = this.instance.coordinateUtils.getYCoordinate(touch); 40 | 41 | json.points.push({x: this.instance.x, y: this.instance.y}); 42 | } 43 | this.instance.sendEvent(json); 44 | } 45 | 46 | /** 47 | * Called when the user is moving on the screen. 48 | * 49 | * @param {Event} event Event. 50 | */ 51 | onScreenTouchMove(event) { 52 | event.preventDefault(); 53 | event.stopPropagation(); 54 | 55 | const json = {type: 'MULTI_TOUCH', nb: event.targetTouches.length, mode: 2, points: []}; 56 | for (let i = 0; i < event.targetTouches.length; i++) { 57 | const touch = event.targetTouches[i]; 58 | 59 | this.instance.x = this.instance.coordinateUtils.getXCoordinate(touch); 60 | this.instance.y = this.instance.coordinateUtils.getYCoordinate(touch); 61 | 62 | json.points.push({x: this.instance.x, y: this.instance.y}); 63 | } 64 | this.instance.sendEvent(json); 65 | } 66 | 67 | /** 68 | * Called when the user stops touching the screen. 69 | * 70 | * @param {Event} event Event. 71 | */ 72 | onScreenTouchEnd(event) { 73 | event.preventDefault(); 74 | event.stopPropagation(); 75 | 76 | const json = {type: 'MULTI_TOUCH', nb: event.targetTouches.length, mode: 1, points: []}; 77 | for (let i = 0; i < event.targetTouches.length; i++) { 78 | const touch = event.targetTouches[i]; 79 | 80 | this.instance.x = this.instance.coordinateUtils.getXCoordinate(touch); 81 | this.instance.y = this.instance.coordinateUtils.getYCoordinate(touch); 82 | 83 | json.points.push({x: this.instance.x, y: this.instance.y}); 84 | } 85 | this.instance.sendEvent(json); 86 | } 87 | 88 | /** 89 | * Bind all event handlers to the video wrapper. 90 | */ 91 | addTouchCallbacks() { 92 | this.instance.addListener(this.instance.videoWrapper, 'touchstart', this.onScreenTouchStart.bind(this), false); 93 | this.instance.addListener(this.instance.videoWrapper, 'touchend', this.onScreenTouchEnd.bind(this), false); 94 | this.instance.addListener(this.instance.videoWrapper, 'touchmove', this.onScreenTouchMove.bind(this), false); 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /src/plugins/StreamBitrate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Stream bitrate plugin. 5 | * Provides video stream quality (audio and video bitrates) control. 6 | */ 7 | module.exports = class StreamBitrate { 8 | static get name() { 9 | return 'StreamBitrate'; 10 | } 11 | /** 12 | * Plugin initialization. 13 | * 14 | * @param {Object} instance Associated instance. 15 | * @param {Object} i18n Translations keys for the UI. 16 | */ 17 | constructor(instance, i18n) { 18 | // Reference instance 19 | this.instance = instance; 20 | 21 | // Register plugin 22 | this.instance.StreamBitrate = this; 23 | this.i18n = i18n || {}; 24 | 25 | this.highQuality = false; 26 | 27 | // Display widget 28 | this.registerToolbarButton(); 29 | } 30 | 31 | /** 32 | * Add the button to the renderer toolbar. 33 | */ 34 | registerToolbarButton() { 35 | this.toolbarBtn = this.instance.toolbarManager.registerButton({ 36 | id: this.constructor.name, 37 | iconClass: 'gm-streamrate-chooser', 38 | title: this.i18n.STREAMRATE_TITLE || 'High quality', 39 | onClick: () => { 40 | this.highQuality = !this.highQuality; 41 | if (this.highQuality) { 42 | this.toolbarBtn.setActive(); 43 | 44 | const json = {type: 'BITRATE', videoBitrate: 5000, audioBitrate: 192000}; 45 | this.instance.sendEvent(json); 46 | this.instance.renegotiateWebRTCConnection(); 47 | } else { 48 | this.toolbarBtn.setActive(false); 49 | 50 | // if we pass 0 here, we just use WebRTC default value 51 | const json = {type: 'BITRATE', videoBitrate: 0, audioBitrate: 0}; 52 | this.instance.sendEvent(json); 53 | this.instance.renegotiateWebRTCConnection(); 54 | } 55 | }, 56 | }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/plugins/StreamResolution.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RESOLUTIONS = [ 4 | {text: '-', value: '0'}, 5 | {text: '240p', value: '240'}, 6 | {text: '360p', value: '360'}, 7 | {text: '480p', value: '480'}, 8 | {text: '720p', value: '720'}, 9 | {text: '1080p', value: '1080'}, 10 | ]; 11 | 12 | /** 13 | * Stream resolution plugin. 14 | * Provides video stream resolution control. 15 | */ 16 | module.exports = class StreamResolution { 17 | static get name() { 18 | return 'StreamResolution'; 19 | } 20 | /** 21 | * Plugin initialization. 22 | * 23 | * @param {Object} instance Associated instance. 24 | * @param {Object} i18n Translations keys for the UI. 25 | */ 26 | constructor(instance, i18n) { 27 | // Reference instance 28 | this.instance = instance; 29 | 30 | // Register plugin 31 | this.instance.StreamResolution = this; 32 | this.i18n = i18n || {}; 33 | 34 | // Display widget 35 | this.registerToolbarButton(); 36 | } 37 | 38 | /** 39 | * Add the button to the renderer toolbar. 40 | */ 41 | registerToolbarButton() { 42 | this.instance.toolbarManager.registerButton({ 43 | id: this.constructor.name, 44 | iconClass: 'gm-streamres-button', 45 | title: this.i18n.STREAMRES_TITLE || 'Quality', 46 | onClick: (event) => { 47 | if (event.target.tagName === 'OPTION') { 48 | return; 49 | } 50 | let select = event.target.querySelector('select'); 51 | if (!select) { 52 | // draw the select 53 | select = document.createElement('select'); 54 | select.className = 'gm-icon-button gm-streamres-button'; 55 | select.title = this.i18n.STREAMRES_TITLE || 'Quality'; 56 | RESOLUTIONS.forEach((resolution) => { 57 | select.add(new Option(resolution.text, resolution.value)); 58 | }); 59 | select.onchange = () => { 60 | const json = {type: 'SIZE', width: Number(select.value)}; 61 | this.instance.sendEvent(json); 62 | select.blur(); 63 | }; 64 | 65 | event.target.appendChild(select); 66 | } 67 | }, 68 | }); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/plugins/util/iothrottling-profiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * List of available IO throttling profiles to send to VM 5 | * 6 | * readByteRate: The total rate in MegaBytes of read per second 7 | * writeByteRate: The total rate in MegaBytes of write per second 8 | * readIoRate: The number of read operation allowed per second 9 | * writeIoRate: The number of write operation allowed per second 10 | * 11 | */ 12 | module.exports = [ 13 | { 14 | name: 'None', 15 | label: '(No disk performance alteration)', 16 | readByteRate: 0, 17 | }, 18 | { 19 | name: 'High-end device', 20 | label: '(200 MiB per second)', 21 | readByteRate: 200, 22 | }, 23 | { 24 | name: 'Mid-range device', 25 | label: '(100 MiB per second)', 26 | readByteRate: 100, 27 | }, 28 | { 29 | name: 'Low-end device', 30 | label: '(50 MiB per second)', 31 | readByteRate: 50, 32 | }, 33 | { 34 | name: 'Custom', 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /src/plugins/util/mobile-signal-strength.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * List of possible network signal strengths to send to the VM. 5 | * Used only for android versions greater or equal to 8. 6 | */ 7 | module.exports = [ 8 | { 9 | id: 4, 10 | name: 'great', 11 | label: 'Great', 12 | }, 13 | { 14 | id: 3, 15 | name: 'good', 16 | label: 'Good', 17 | }, 18 | { 19 | id: 2, 20 | name: 'moderate', 21 | label: 'Moderate', 22 | }, 23 | { 24 | id: 1, 25 | name: 'poor', 26 | label: 'Poor', 27 | }, 28 | { 29 | id: 0, 30 | name: 'none', 31 | label: 'None', 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /src/plugins/util/network-mobile-profiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * List of available network profiles to send to the VM 5 | * for android versions greater or equal to 8 6 | */ 7 | module.exports = [ 8 | { 9 | id: 7, 10 | name: '5g', 11 | label: '5G', 12 | icon: 'five-G', 13 | }, 14 | { 15 | id: 6, 16 | name: 'lte', 17 | label: '4G LTE', 18 | icon: 'four-G', 19 | }, 20 | { 21 | id: 5, 22 | name: 'hsdpa', 23 | label: '3G HSDPA', 24 | icon: 'three-G', 25 | }, 26 | { 27 | id: 4, 28 | name: 'umts', 29 | label: '3G UMTS', 30 | icon: 'three-G', 31 | }, 32 | { 33 | id: 3, 34 | name: 'edge', 35 | label: '2G EDGE', 36 | icon: 'two-G-edge', 37 | }, 38 | { 39 | id: 2, 40 | name: 'gprs', 41 | label: '2G GPRS', 42 | icon: 'two-G-gprs', 43 | }, 44 | { 45 | id: 1, 46 | name: 'gsm', 47 | label: '2G GSM', 48 | icon: 'two-G-gsm', 49 | }, 50 | { 51 | id: 0, 52 | name: 'none', 53 | label: 'Unlimited', 54 | icon: 'unlimited', 55 | }, 56 | ]; 57 | -------------------------------------------------------------------------------- /src/scss/base/_animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes blink { 2 | 0% { 3 | opacity: 1; 4 | } 5 | 30% { 6 | opacity: 0.3; 7 | } 8 | 100% { 9 | opacity: 1; 10 | } 11 | } 12 | 13 | @keyframes blink-alert { 14 | 0% { 15 | opacity: 1; 16 | } 17 | 10% { 18 | opacity: 0.3; 19 | } 20 | 20% { 21 | opacity: 1; 22 | } 23 | 30% { 24 | opacity: 0.3; 25 | } 26 | 40% { 27 | opacity: 1; 28 | } 29 | 50% { 30 | opacity: 0.3; 31 | } 32 | 100% { 33 | opacity: 1; 34 | } 35 | } 36 | 37 | @keyframes notification-pulse { 38 | 0% { 39 | transform: scale(0.2); 40 | opacity: 1; 41 | } 42 | 50% { 43 | transform: scale(1); 44 | opacity: 0.5; 45 | } 46 | 80% { 47 | transform: scale(0.2); 48 | opacity: 1; 49 | } 50 | 100% { 51 | transform: scale(0.2); 52 | opacity: 1; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/scss/base/_variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS variables 3 | */ 4 | :root { 5 | --gm-primary-color: rgba(230, 25, 94, 1); 6 | --gm-on-primary-color: rgba(255, 255, 255, 1); 7 | --gm-primary-variant-color: rgba(230, 25, 94, 0.2); 8 | --gm-on-primary-variant-color: rgba(230, 25, 94, 1); 9 | --gm-secondary-color: #292929; 10 | --gm-on-secondary-color: #ffffff; 11 | --gm-secondary-variant-color: #212121; 12 | --gm-on-secondary-variant-color: #ffffff; 13 | --gm-tertiary-color: #c4c4c44d; 14 | --gm-on-tertiary-color: #ffffff; 15 | --gm-tertiary-variant-color: #c4c4c4; 16 | --gm-on-tertiary-variant-color: #1a1a1a; 17 | --gm-background-color: #1a1a1a; 18 | --gm-on-background-color: #ffffff; 19 | --gm-surface-color: #292929; 20 | --gm-on-surface-color: #ffffff; 21 | --gm-error-color: rgba(255, 17, 17, 1); 22 | --gm-background-error-color: rgba(255, 17, 17, 0.1); 23 | --gm-success-color: rgba(17, 185, 32, 1); 24 | --gm-background-success-color: rgba(17, 185, 32, 0.1); 25 | --gm-warning-color: rgba(255, 204, 0, 1); 26 | --gm-background-warning-color: rgba(255, 204, 0, 0.1); 27 | --gm-gradiant-1: linear-gradient(142.33deg, #e6195e 4.58%, #2b41ea 122.31%); 28 | --gm-on-gradiant-1: rgba(255, 255, 255, 1); 29 | --gm-gradiant-2: linear-gradient(275.69deg, #e6195e 10.31%, #2b41ea 136.09%); 30 | --gm-on-gradiant-2: rgba(255, 255, 255, 1); 31 | 32 | /* Keep for compatibility while all plugin aren't refacto*/ 33 | /** Button **/ 34 | --gm-btn-text-color: var(--gm-text-color); 35 | --gm-btn-bg-color: var(--gm-primary-color); 36 | --gm-btn-bg-color-hover: var(--gm-primary-color); 37 | --gm-btn-bg-color-disabled: rgba(179, 179, 179, 0.24); // TODO 38 | --gm-btn-bg-color-disabled-hover: #828282; 39 | } 40 | 41 | /** 42 | * SASS variables 43 | */ 44 | 45 | // Responsive breakpoint 46 | $breakpoint-mobile: 650px; 47 | 48 | // Spacing 49 | $spacing-xxs: 2px; 50 | $spacing-xs: 5px; 51 | $spacing-s: 8px; 52 | $spacing-m: 13px; 53 | $spacing-l: 18px; 54 | $spacing-xl: 28px; 55 | 56 | // Modal 57 | $modal-x-padding: 30px; 58 | $modal-section-mb: $spacing-xl; 59 | $modal-label-mb: $spacing-m; 60 | -------------------------------------------------------------------------------- /src/scss/components/_baseband.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Baseband plugin styles 3 | */ 4 | .device-renderer-instance .gm-baseband-plugin { 5 | .gm-baseband-saved { 6 | .gm-actions { 7 | .gm-tag-success { 8 | visibility: visible; 9 | } 10 | } 11 | } 12 | 13 | .gm-network-operator-section-line, 14 | .gm-sim-operator-section-line { 15 | display: flex; 16 | gap: $spacing-xl; 17 | /*margin-bottom: $spacing-m;*/ 18 | } 19 | .gm-network-operator-section-title { 20 | color: var(--gm-on-secondary-color); 21 | display: flex; 22 | align-items: center; 23 | font-weight: bolder; 24 | margin-bottom: $spacing-m; 25 | &::before { 26 | content: ' '; 27 | width: 30px; 28 | height: 30px; 29 | background-color: var(--gm-on-secondary-color); 30 | -webkit-mask-size: contain; 31 | mask-size: contain; 32 | -webkit-mask-repeat: no-repeat; 33 | mask-repeat: no-repeat; 34 | -webkit-mask-position: center; 35 | mask-position: center; 36 | mask-image: url('../assets/images/ic_world.svg'); 37 | -webkit-mask-image: url('../assets/images/ic_world.svg'); 38 | } 39 | } 40 | 41 | .gm-sim-operator-section-title { 42 | color: var(--gm-on-secondary-color); 43 | display: flex; 44 | align-items: center; 45 | font-weight: bolder; 46 | margin-bottom: $spacing-m; 47 | 48 | &::before { 49 | content: ' '; 50 | width: 30px; 51 | height: 30px; 52 | background-color: var(--gm-on-secondary-color); 53 | -webkit-mask-size: contain; 54 | mask-size: contain; 55 | -webkit-mask-repeat: no-repeat; 56 | mask-repeat: no-repeat; 57 | -webkit-mask-position: center; 58 | mask-position: center; 59 | mask-image: url('../assets/images/ic_baseband.svg'); 60 | -webkit-mask-image: url('../assets/images/ic_baseband.svg'); 61 | } 62 | } 63 | .gm-actions { 64 | display: flex; 65 | justify-content: space-between; 66 | align-items: center; 67 | height: 60px; 68 | .gm-tag-success { 69 | visibility: hidden; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/scss/components/_battery.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Battery plugin styles 3 | */ 4 | .device-renderer-instance .gm-battery-plugin { 5 | .gm { 6 | &-charging-group { 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | 11 | > div:nth-child(2) { 12 | flex-grow: 1; 13 | } 14 | 15 | .gm-charging-image { 16 | width: 38px; 17 | height: 38px; 18 | margin-left: -9px; 19 | mask-image: url('../assets/images/ic_battery_not_charging.svg'); 20 | -webkit-mask-image: url('../assets/images/ic_battery_not_charging.svg'); 21 | -webkit-mask-size: contain; 22 | mask-size: contain; 23 | -webkit-mask-repeat: no-repeat; 24 | mask-repeat: no-repeat; 25 | -webkit-mask-position: center; 26 | mask-position: center; 27 | background-color: var(--gm-tertiary-variant-color); 28 | 29 | &.charging { 30 | mask-image: url('../assets/images/ic_battery_charging.svg'); 31 | -webkit-mask-image: url('../assets/images/ic_battery_charging.svg'); 32 | background-color: var(--gm-on-secondary-color); 33 | } 34 | } 35 | 36 | .gm-charging-status { 37 | height: 32px; 38 | line-height: 32px; 39 | font-weight: bold; 40 | margin-left: 10px; 41 | } 42 | } 43 | 44 | &-charge-level-group { 45 | display: flex; 46 | justify-content: space-around; 47 | align-items: center; 48 | margin-bottom: $modal-section-mb; 49 | 50 | > div:nth-child(2) { 51 | flex-grow: 1; 52 | padding: 0 30px; 53 | } 54 | 55 | &.low { 56 | .gm-charge-image,.gm-charge-image-overlay { 57 | background-color: var(--gm-error-color) !important; 58 | 59 | } 60 | .text-input-container { 61 | color: var(--gm-error-color); 62 | border-color: var(--gm-error-color); 63 | input { 64 | color: var(--gm-error-color); 65 | } 66 | } 67 | } 68 | &.medium { 69 | .gm-charge-image,.gm-charge-image-overlay { 70 | background-color: var(--gm-warning-color) !important; 71 | } 72 | .text-input-container { 73 | color: var(--gm-warning-color); 74 | border-color: var(--gm-warning-color); 75 | input { 76 | color: var(--gm-warning-color); 77 | } 78 | } 79 | } 80 | 81 | .gm-charge-image-group { 82 | position: relative; 83 | 84 | .gm-charge-image-overlay-container { 85 | position: absolute; 86 | left: 29%; 87 | bottom: 13.2%; 88 | width: 40%; 89 | height: 63.5%; 90 | display: flex; 91 | margin-left: -4px; 92 | 93 | .gm-charge-image-overlay { 94 | height: 0; 95 | width: 100%; 96 | align-self: flex-end; 97 | background-color: var(--gm-on-secondary-color); 98 | } 99 | } 100 | 101 | .gm-charge-image { 102 | position: relative; 103 | display: block; 104 | width: 38px; 105 | height: 38px; 106 | margin-left: -9px; 107 | mask-image: url('../assets/images/ic_battery_empty.svg'); 108 | -webkit-mask-image: url('../assets/images/ic_battery_empty.svg'); 109 | -webkit-mask-size: contain; 110 | mask-size: contain; 111 | -webkit-mask-repeat: no-repeat; 112 | mask-repeat: no-repeat; 113 | -webkit-mask-position: center; 114 | mask-position: center; 115 | background-color: var(--gm-on-secondary-color); 116 | } 117 | } 118 | 119 | .gm-charge-input { 120 | width: 39px; 121 | margin-top: -30px; 122 | input { 123 | text-align: right; 124 | } 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/scss/components/_chipTag.scss: -------------------------------------------------------------------------------- 1 | 2 | .gm-tag{ 3 | &-success{ 4 | .gm-tag-container{ 5 | display: inline-flex; 6 | align-items: center; 7 | border-radius: 8px; 8 | padding: 2px 6px; 9 | color: var(--gm-success-color); 10 | background-color: var(--gm-background-success-color); 11 | 12 | &::after{ 13 | content: ' '; 14 | background-color: var(--gm-success-color); 15 | mask-image: url('../assets/images/ic_check.svg'); 16 | -webkit-mask-image: url('../assets/images/ic_check.svg'); 17 | -webkit-mask-size: contain; 18 | mask-size: contain; 19 | -webkit-mask-repeat: no-repeat; 20 | mask-repeat: no-repeat; 21 | -webkit-mask-position: center; 22 | mask-position: center; 23 | width: 22px; 24 | height: 22px; 25 | display: block; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/scss/components/_clipboard.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Clipboard plugin styles 3 | */ 4 | .device-renderer-instance .gm-clipboard-plugin { 5 | .gm { 6 | &-modal-body { 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | &-clipboard-saved { 12 | .gm-actions { 13 | .gm-tag-success { 14 | visibility: visible; 15 | } 16 | } 17 | } 18 | 19 | &-clipboard-input { 20 | margin: $spacing-m 0; 21 | } 22 | &-actions { 23 | display: flex; 24 | justify-content: space-between; 25 | align-items: center; 26 | .gm-clipboard-apply { 27 | display: flex; 28 | align-items: center; 29 | } 30 | .gm-clipboard-apply::before { 31 | content: ''; 32 | mask-image: url('../assets/images/ic_copy.svg'); 33 | -webkit-mask-image: url('../assets/images/ic_copy.svg'); 34 | -webkit-mask-size: contain; 35 | mask-size: contain; 36 | -webkit-mask-repeat: no-repeat; 37 | mask-repeat: no-repeat; 38 | -webkit-mask-position: center; 39 | mask-position: center; 40 | background: currentColor; 41 | width: 32px; 42 | height: 32px; 43 | } 44 | .gm-tag-success { 45 | visibility: hidden; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/scss/components/_dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | position: relative; 3 | font-family: Arial, sans-serif; 4 | &.open { 5 | .dropdown-menu { 6 | display: block; 7 | } 8 | .dropdown-selected { 9 | border-color: var(--gm-primary-color); 10 | border-width: 2px; 11 | &::after { 12 | transform: rotate(-45deg); 13 | } 14 | } 15 | } 16 | 17 | &.disabled { 18 | .dropdown-selected { 19 | pointer-events: none; 20 | border-color: var(--gm-tertiary-color); 21 | color: var(--gm-tertiary-color); 22 | &::after { 23 | border-color: var(--gm-tertiary-color); 24 | } 25 | } 26 | } 27 | 28 | .dropdown-selected { 29 | display: flex; 30 | justify-content: space-between; 31 | align-items: center; 32 | padding: 10px 0px 15px 0px; 33 | cursor: pointer; 34 | 35 | transition: transform 0.2s ease; 36 | transition: border-color 0.2s ease; 37 | border-bottom: 1px solid var(--gm-tertiary-variant-color); 38 | 39 | &::after { 40 | content: ''; 41 | width: 8px; 42 | height: 8px; 43 | margin-right: 8px; 44 | margin-left: auto; 45 | border: solid var(--gm-on-secondary-color); 46 | border-width: 2px 2px 0 0; 47 | transform: rotate(135deg); 48 | transform-origin: center; 49 | transition: transform 0.2s ease; 50 | } 51 | } 52 | 53 | .dropdown-menu { 54 | display: none; 55 | position: absolute; 56 | top: 100%; 57 | left: 0; 58 | z-index: 10; 59 | margin-top: 5px; 60 | 61 | padding: 10px 0; 62 | gap: 6px; 63 | border-radius: 4px; 64 | background-color: var(--gm-secondary-variant-color); 65 | overflow-y: auto; 66 | 67 | scrollbar-width: thin; 68 | scrollbar-color: rgba(0, 0, 0, 0.3) transparent; 69 | 70 | &::-webkit-scrollbar { 71 | width: 6px; 72 | height: 6px; 73 | } 74 | 75 | &::-webkit-scrollbar-track { 76 | background: transparent; 77 | border-radius: 10px; 78 | } 79 | 80 | &::-webkit-scrollbar-thumb { 81 | background: rgba(0, 0, 0, 0.3); 82 | border-radius: 10px; 83 | } 84 | 85 | &::-webkit-scrollbar-thumb:hover { 86 | background: rgba(0, 0, 0, 0.5); 87 | } 88 | 89 | .dropdown-item { 90 | padding: 16px 20px; 91 | cursor: pointer; 92 | display: flex; 93 | justify-content: space-between; 94 | align-items: center; 95 | .dropdown-checkmark { 96 | background-color: var(--gm-on-secondary-color); 97 | mask-image: url('../assets/images/ic_check.svg'); 98 | -webkit-mask-image: url('../assets/images/ic_check.svg'); 99 | -webkit-mask-size: contain; 100 | mask-size: contain; 101 | -webkit-mask-repeat: no-repeat; 102 | mask-repeat: no-repeat; 103 | -webkit-mask-position: center; 104 | mask-position: center; 105 | width: 22px; 106 | height: 22px; 107 | } 108 | } 109 | 110 | .dropdown-item:hover { 111 | background: var(--gm-background-color); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/scss/components/_fullscreen.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Fullscreen plugin styles 3 | */ 4 | .device-renderer-instance .gm-fullscreen { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | width: 100%; 9 | height: 100%; 10 | padding: 0; 11 | 12 | .gm { 13 | &-video { 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | width: calc(100% - 60px); 18 | height: 100%; 19 | padding: 0; 20 | } 21 | 22 | &-toolbars { 23 | position: absolute; 24 | top: 0; 25 | right: 0; 26 | width: 48px; 27 | height: 100%; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/scss/components/_gps.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * GPS plugin styles 3 | */ 4 | .device-renderer-instance .gm-gps { 5 | &-controls { 6 | label { 7 | margin-top: 10px; 8 | } 9 | 10 | input { 11 | &.gm-error { 12 | border-bottom: 1px solid var(--gm-btn-bg-color); 13 | } 14 | } 15 | } 16 | 17 | &-submit { 18 | margin-top: 10px; 19 | } 20 | 21 | &-geoloc { 22 | margin-top: 10px; 23 | } 24 | 25 | &-mapview { 26 | height: 460px; 27 | 28 | .gm-mapview { 29 | position: absolute; 30 | bottom: 76px; 31 | left: 26px; 32 | right: 26px; 33 | top: 26px; 34 | background-color: #fff; 35 | } 36 | 37 | .gm-gps-mapview-capture { 38 | position: absolute; 39 | bottom: 20px; 40 | left: calc(50% + 80px); 41 | } 42 | 43 | .gm-gps-mapview-cancel { 44 | position: absolute; 45 | bottom: 20px; 46 | right: calc(50% + 80px); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/scss/components/_imei.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Identifiers (IMEI) plugin styles 3 | */ 4 | .device-renderer-instance .gm-identifiers-plugin { 5 | .gm-modal-body { 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | .gm-identifiers-saved { 11 | .gm-actions{ 12 | .gm-tag-success { 13 | visibility: visible; 14 | } 15 | } 16 | } 17 | 18 | .gm-inputs{ 19 | flex: 1; 20 | .gm-identifier-android{ 21 | margin-bottom: $modal-section-mb; 22 | } 23 | .gm-identifier-android, 24 | .gm-identifier-device{ 25 | display: flex; 26 | justify-content: space-between; 27 | gap: calc($modal-x-padding / 2); 28 | align-items: center; 29 | .gm-identifiers-device-input, 30 | .gm-identifiers-android-input { 31 | flex: 1; 32 | } 33 | .gm-identifiers-android-generate, 34 | .gm-identifiers-device-generate { 35 | mask-image: url('../assets/images/ic_generate.svg'); 36 | -webkit-mask-image: url('../assets/images/ic_generate.svg'); 37 | background-color: var(--gm-on-secondary-color); 38 | cursor: pointer; 39 | } 40 | } 41 | } 42 | .gm-actions{ 43 | display: flex; 44 | justify-content: space-between; 45 | align-items: center; 46 | height: 60px; 47 | .gm-tag-success { 48 | visibility: hidden; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/scss/components/_iothrottling.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * IO Throttling plugin styles 3 | */ 4 | .device-renderer-instance .gm-iothrottling-plugin { 5 | .gm-noThrottling, 6 | .gm-customThrottling { 7 | display: none; 8 | } 9 | .gm-iothrottling-none { 10 | .gm-units, 11 | .gm-iothrottling-readbyterate { 12 | display: none; 13 | } 14 | .gm-noThrottling { 15 | display: block; 16 | } 17 | } 18 | .gm-iothrottling-custom { 19 | .gm-customThrottling { 20 | display: block; 21 | //text italic 22 | font-style: italic; 23 | margin-top: $spacing-m; 24 | } 25 | } 26 | 27 | .gm-iothrottling-cache-cleared { 28 | .gm-iothrottling-clearcache { 29 | label { 30 | animation: blink-alert 1.5s ease-in-out; 31 | } 32 | } 33 | } 34 | 35 | .gm-tag-success { 36 | display: none; 37 | } 38 | .gm-iothrottling-notapplied-text { 39 | display: block; 40 | } 41 | .gm-iothrottling-saved { 42 | .gm-tag-success { 43 | display: block; 44 | } 45 | .gm-iothrottling-notapplied-text { 46 | display: none; 47 | } 48 | } 49 | 50 | .gm-modal-body { 51 | display: flex; 52 | flex-direction: column; 53 | } 54 | 55 | .gm-fields { 56 | margin-top: $spacing-m; 57 | min-height: 40px; 58 | flex: 1; 59 | font-size: 13px; 60 | 61 | .gm-fields-container { 62 | display: flex; 63 | align-items: center; 64 | gap: 10px; 65 | .gm-iothrottling-readbyterate { 66 | width: 49px; 67 | input { 68 | text-align: center; 69 | } 70 | } 71 | .gm-units { 72 | margin-left: 20px; 73 | } 74 | } 75 | } 76 | 77 | .gm-iothrottling-clearcache { 78 | label { 79 | margin-bottom: 0; 80 | } 81 | } 82 | .gm-iothrottling-clearcache, 83 | .gm-iothrottling-apply { 84 | display: flex; 85 | justify-content: space-between; 86 | align-items: center; 87 | height: 60px; 88 | .gm-iothrottling-status { 89 | display: flex; 90 | align-items: center; 91 | gap: 5px; 92 | .gm-iothrottling-notapplied-text { 93 | font-style: italic; 94 | color: var(--gm-tertiary-variant-color); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/scss/components/_network.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Network plugin styles 3 | */ 4 | .device-renderer-instance .gm-network-plugin { 5 | .gm-network-wifi-section { 6 | display: flex; 7 | justify-content: space-between; 8 | margin-bottom: $spacing-m; 9 | } 10 | .gm-mobile-data-switch { 11 | display: flex; 12 | justify-content: space-between; 13 | margin-top: $spacing-m; 14 | margin-bottom: $spacing-m * 2; 15 | } 16 | 17 | .gm-network-type-dropdown { 18 | margin-bottom: $spacing-m * 2; 19 | .dropdown-menu { 20 | .dropdown-item { 21 | display: flex; 22 | align-items: center; 23 | .gm-network-profile-icon { 24 | width: 30px; 25 | height: 30px; 26 | -webkit-mask-size: contain; 27 | mask-size: contain; 28 | -webkit-mask-repeat: no-repeat; 29 | mask-repeat: no-repeat; 30 | -webkit-mask-position: center; 31 | mask-position: center; 32 | background-color: var(--gm-on-secondary-color); 33 | 34 | &.unlimited { 35 | mask-image: url('../assets/images/ic_network-type-unlimited.svg'); 36 | -webkit-mask-image: url('../assets/images/ic_network-type-unlimited.svg'); 37 | } 38 | &.five-G { 39 | mask-image: url('../assets/images/ic_network-type-5g.svg'); 40 | -webkit-mask-image: url('../assets/images/ic_network-type-5G.svg'); 41 | } 42 | &.four-G { 43 | mask-image: url('../assets/images/ic_network-type-4g.svg'); 44 | -webkit-mask-image: url('../assets/images/ic_network-type-4G.svg'); 45 | } 46 | &.three-G { 47 | mask-image: url('../assets/images/ic_network-type-3g.svg'); 48 | -webkit-mask-image: url('../assets/images/ic_network-type-3G.svg'); 49 | } 50 | &.two-G-edge { 51 | mask-image: url('../assets/images/ic_network-type-edge.svg'); 52 | -webkit-mask-image: url('../assets/images/ic_network-type-2G-edge.svg'); 53 | } 54 | &.two-G-gprs { 55 | mask-image: url('../assets/images/ic_network-type-gprs.svg'); 56 | -webkit-mask-image: url('../assets/images/ic_network-type-2G-gprs.svg'); 57 | } 58 | &.two-G-gsm { 59 | mask-image: url('../assets/images/ic_network-type-gprs.svg'); 60 | -webkit-mask-image: url('../assets/images/ic_network-type-2G-gsm.svg'); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | .gm-network-mobile-section { 68 | .gm-signal-strength-dropdown { 69 | margin-bottom: $spacing-m * 2; 70 | } 71 | &.disabled { 72 | label, 73 | section { 74 | color: var(--gm-tertiary-color); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/scss/components/_phone.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Phone plugin styles 3 | */ 4 | .device-renderer-instance .gm-phone-plugin { 5 | .gm-phone-group { 6 | display: flex; 7 | flex-direction: column; 8 | margin-bottom: $modal-section-mb; 9 | .gm-phone-call, 10 | .gm-phone-send { 11 | margin-top: $spacing-m; 12 | align-self: flex-end; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/scss/components/_screencast.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Screencast plugin styles 3 | */ 4 | .device-renderer-instance { 5 | .gm-screencast-plugin { 6 | // TODO delete this line in the PR which will refacto this plugin, keep for css compatibility 7 | label{ 8 | color: var(--gm-text-color); 9 | font-weight: normal; 10 | } 11 | width: 400px; 12 | left: calc(50% - 200px); 13 | 14 | /* Extra Small Devices, Tablet or Phones */ 15 | @media only screen and (max-width: $breakpoint-mobile) { 16 | max-width: 400px; 17 | } 18 | 19 | label { 20 | height: 32px; 21 | line-height: 35px; 22 | margin-bottom: 0; 23 | margin-left: 20px; 24 | cursor: pointer; 25 | } 26 | 27 | .disabled { 28 | opacity: 0.4; 29 | pointer-events: none; 30 | } 31 | 32 | .gm-inputs { 33 | display: flex; 34 | } 35 | 36 | .gm-screenshot, 37 | .gm-screencast { 38 | display: inline-block; 39 | width: auto; 40 | margin-right: 50px; 41 | display: flex; 42 | flex-direction: row-reverse; 43 | 44 | .gm-action { 45 | width: 32px; 46 | min-height: 32px; 47 | cursor: pointer; 48 | display: block; 49 | -webkit-mask-size: contain; 50 | mask-size: contain; 51 | -webkit-mask-repeat: no-repeat; 52 | mask-repeat: no-repeat; 53 | -webkit-mask-position: center; 54 | mask-position: center; 55 | background-color: var(--gm-on-secondary-color); 56 | } 57 | } 58 | 59 | .gm-screenshot .gm-action { 60 | mask-image: url('../assets/images/ic_screenshot.svg'); 61 | --webkit-mask-image: url('../assets/images/ic_screenshot.svg'); 62 | } 63 | 64 | .gm-screencast .gm-action { 65 | mask-image: url('../assets/images/ic_screencast.svg'); 66 | --webkit-mask-image: url('../assets/images/ic_screencast.svg'); 67 | 68 | .disabled { 69 | color: var(--gm-btn-bg-color-disabled); 70 | } 71 | } 72 | } 73 | 74 | .gm-screencast-button:hover { 75 | + .gm-screencast-timer { 76 | display: block; 77 | } 78 | } 79 | .gm-screencast-timer { 80 | position: fixed; 81 | right: 57px; 82 | color: #c6225a; 83 | font-size: 20px; 84 | font-family: Helvetica, sans-serif; 85 | pointer-events: none; 86 | padding: 11px 8px 8px 8px; 87 | border-radius: 6px; 88 | width: 80px; 89 | background-color: #303339; 90 | margin-top: -37px; 91 | display: none; 92 | 93 | &::after { 94 | content: ' '; 95 | position: absolute; 96 | right: -10px; 97 | top: 16px; 98 | border-width: 5px; 99 | border-style: solid; 100 | border-color: transparent transparent transparent #303339; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/scss/components/_slider.scss: -------------------------------------------------------------------------------- 1 | .slider { 2 | position: relative; 3 | display: inline-block; 4 | width: 100%; 5 | height: 15px; 6 | 7 | .slider-progress-bar { 8 | position: absolute; 9 | height: 3px; 10 | width: 0; 11 | top: calc(50% - 1.5px); 12 | background-color: var(--gm-primary-color); 13 | border-radius: 10px; 14 | } 15 | 16 | .slider-progress-bar-remaining { 17 | position: absolute; 18 | height: 3px; 19 | width: 100%; 20 | top: calc(50% - 1.5px); 21 | background-color: var(--gm-tertiary-variant-color); 22 | opacity: 0.2; 23 | border-radius: 10px; 24 | } 25 | 26 | .slider-cursor { 27 | position: absolute; 28 | height: 16px; 29 | width: 16px; 30 | background-color: var(--gm-primary-color); 31 | border-radius: 50%; 32 | transform: translate(-8px, -50%); 33 | top: 50%; 34 | z-index: 2; 35 | &:hover { 36 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.2); 37 | } 38 | } 39 | 40 | .slider-input { 41 | position: absolute; 42 | left: 0; 43 | width: 100%; 44 | height: 100%; 45 | opacity: 0; 46 | margin: 0; 47 | cursor: pointer; 48 | z-index: 3; 49 | padding: 0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/scss/components/_switchButton.scss: -------------------------------------------------------------------------------- 1 | .switch { 2 | position: relative; 3 | display: inline-block; 4 | width: 34px; 5 | height: 14px; 6 | } 7 | 8 | .switch input { 9 | opacity: 0; 10 | width: 0; 11 | height: 0; 12 | } 13 | 14 | .switch-slider { 15 | position: absolute; 16 | cursor: pointer; 17 | top: 0; 18 | left: 0; 19 | right: 0; 20 | bottom: 0; 21 | background-color: var(--gm-tertiary-color); 22 | border-radius: 15px; 23 | transition: background-color 0.4s; 24 | } 25 | 26 | .switch-slider::before { 27 | position: absolute; 28 | content: ''; 29 | height:20px; 30 | width: 20px; 31 | bottom: -3px; 32 | background-color: var(--gm-tertiary-variant-color); 33 | border-radius: 50%; 34 | transition: 35 | transform 0.4s, 36 | background-color 0.4s; 37 | } 38 | 39 | /* Active state */ 40 | .switch input:checked + .switch-slider { 41 | background-color: var(--gm-primary-variant-color); 42 | 43 | } 44 | 45 | .switch input:checked + .switch-slider::before { 46 | background-color: var(--gm-primary-color); 47 | transform: translateX(17px); 48 | } 49 | -------------------------------------------------------------------------------- /src/scss/components/_textArea.scss: -------------------------------------------------------------------------------- 1 | textarea { 2 | height: 249px; 3 | resize: none; 4 | border: none; 5 | padding: $spacing-m 0 0 $spacing-m; 6 | 7 | color: var(--gm-on-secondary-color); 8 | background-color: var(--gm-secondary-variant-color); 9 | font-size: 14px; 10 | 11 | &::placeholder { 12 | color: var(--gm-tertiary-color); 13 | } 14 | &:focus { 15 | outline: none; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/scss/components/_textInput.scss: -------------------------------------------------------------------------------- 1 | .text-input { 2 | .text-input-container { 3 | border-bottom: 1px solid var(--gm-tertiary-variant-color); 4 | color: var(--gm-on-secondary-color); 5 | display: flex; 6 | align-items: center; 7 | height: 34px; 8 | 9 | &:has(.text-input:read-only) { 10 | border-bottom: 1px solid transparent; 11 | } 12 | 13 | .input { 14 | text-align: left; 15 | padding-left: 0; 16 | padding-right: 0; 17 | border: none; 18 | background-color: transparent; 19 | color: var(--gm-on-secondary-color); 20 | /*** This is the line fix bad alignment of the text in the input in Battery, for i.e. ***/ 21 | font-size: inherit; 22 | 23 | &::placeholder { 24 | color: var(--gm-tertiary-color); 25 | } 26 | &:focus { 27 | outline: none; 28 | } 29 | 30 | &:read-only { 31 | background: var(--gm-tertiary-color); 32 | border: none; 33 | color: var(--gm-tertiary-variant-color); 34 | } 35 | } 36 | } 37 | &:has(.text-input-message.error) .text-input-container { 38 | border-bottom: 1px solid var(--gm-error-color); 39 | } 40 | .text-input-message { 41 | display: flex; 42 | justify-content: space-between; 43 | align-items: center; 44 | &::after { 45 | content: ' '; 46 | -webkit-mask-size: contain; 47 | mask-size: contain; 48 | -webkit-mask-repeat: no-repeat; 49 | mask-repeat: no-repeat; 50 | -webkit-mask-position: center; 51 | mask-position: center; 52 | width: 20px; 53 | height: 20px; 54 | } 55 | &.error { 56 | &::before { 57 | content: attr(data-error); 58 | color: var(--gm-error-color); 59 | } 60 | &::after { 61 | content: ' '; 62 | mask-image: url('../assets/images/ic_alert.svg'); 63 | -webkit-mask-image: url('../assets/images/ic_alert.svg'); 64 | background-color: var(--gm-error-color); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/scss/components/_turn.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Turn server plugin styles 3 | */ 4 | 5 | .device-renderer-instance { 6 | .gm-default-turn-used { 7 | 8 | position: absolute; 9 | left: -305px; 10 | color: var(--gm-text-color); 11 | font-size: 1em; 12 | font-family: Helvetica, sans-serif; 13 | pointer-events: none; 14 | z-index: 5; 15 | border-radius: 6px; 16 | width: 290px; 17 | background-color: var(--gm-modal-bg-color); 18 | vertical-align: middle; 19 | padding: 11px 8px 20px; 20 | margin-top: -83px; 21 | 22 | &.gm-hidden { 23 | display: none; 24 | } 25 | 26 | &::after { 27 | content: ' '; 28 | position: absolute; 29 | bottom: 0; 30 | right: -17px; 31 | margin-top: -10px; 32 | border-width: 10px; 33 | border-style: solid; 34 | border-color: transparent transparent transparent var(--gm-modal-bg-color); 35 | } 36 | 37 | h1 { 38 | font-size: 1.1em; 39 | color: var(--gm-primary-color); 40 | padding-bottom: 5px; 41 | 42 | span { 43 | font-weight: bolder; 44 | font-size: 1.2em; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/scss/components/_uploader.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * File upload plugin styles 3 | */ 4 | .device-renderer-instance .gm-uploader-content { 5 | .gm { 6 | &-upload-icon { 7 | display: inline-block; 8 | height: 39px; 9 | width: 58px; 10 | background-repeat: no-repeat; 11 | background-position: center center; 12 | } 13 | 14 | &-upload-main-img { 15 | background-image: url('../assets/images/ic_cloud_upload.svg'); 16 | } 17 | 18 | &-upload-success-img { 19 | background-image: url('../assets/images/ic_cloud_upload_congralutation.svg'); 20 | } 21 | 22 | &-upload-error-img { 23 | background-image: url('../assets/images/ic_cloud_upload_failed.svg'); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Main stylesheet. 3 | * Only imports other styles. 4 | */ 5 | 6 | @import 'base/_variables'; 7 | @import 'base/_genymotion'; 8 | @import 'base/_animations'; 9 | 10 | @import 'components/_widgetwindow'; 11 | @import 'components/_fullscreen'; 12 | @import 'components/_toolbar'; 13 | @import 'components/_upload'; 14 | @import 'components/_battery'; 15 | @import 'components/_gps'; 16 | @import 'components/_screencast'; 17 | @import 'components/_imei'; 18 | @import 'components/_network'; 19 | @import 'components/_iothrottling'; 20 | @import 'components/_phone'; 21 | @import 'components/_baseband'; 22 | @import 'components/_uploader'; 23 | @import 'components/_clipboard'; 24 | @import 'components/_turn'; 25 | @import 'components/_fingerprints'; 26 | @import 'components/_slider'; 27 | @import 'components/_switchButton'; 28 | @import 'components/_textInput'; 29 | @import 'components/_dropdown'; 30 | @import 'components/_chipTag'; 31 | @import 'components/_textArea'; 32 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function generateUID() { 4 | const date = Date.now().toString(36); 5 | const part1 = date.substring(date.length - 5); 6 | const part2 = Math.random().toString(36).substring(2, 7); 7 | return part1 + part2; 8 | } 9 | 10 | module.exports = { 11 | generateUID, 12 | }; 13 | -------------------------------------------------------------------------------- /tests/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | clearMocks: true, 5 | reporters: ['default', 'jest-junit'], 6 | testMatch: ['**/?(*.)+(test).js'], 7 | }; 8 | -------------------------------------------------------------------------------- /tests/mocks/DeviceRenderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Original = require('../../src/DeviceRenderer'); 4 | const OriginalToolbarManager = require('../../src/plugins/util/ToolBarManager'); 5 | const OriginalApiManager = require('../../src/APIManager'); 6 | 7 | const store = require('../../src/store/index'); 8 | 9 | class ToolBarManager extends OriginalToolbarManager { 10 | constructor() { 11 | super(); 12 | } 13 | registerButton(args) { 14 | const button = super.registerButton(args); 15 | /** 16 | * Tricks, render button after register 17 | * cause it's the deviceRenderFactory script which launch the renderButton 18 | * and this script is not available in test 19 | */ 20 | this.renderButton(args.id); 21 | 22 | return button; 23 | } 24 | } 25 | module.exports = class DeviceRenderer extends Original { 26 | constructor(options) { 27 | document.body.innerHTML = ` 28 |
29 |
30 |
31 | 34 |
35 |
36 |
37 |
    38 |
39 |
40 |
41 |
42 |
`; 43 | 44 | navigator.mediaDevices = { 45 | getUserMedia: jest.fn().mockReturnValue(Promise.resolve(null)), 46 | }; 47 | 48 | super(document.body, options || {}); 49 | this.outgoingMessages = []; 50 | this.apiManager = new OriginalApiManager(this); 51 | this.toolbarManager = new ToolBarManager(); 52 | // Load store since it's deviceRendererFactory which load it 53 | store(this); 54 | } 55 | 56 | /** 57 | * Send event to the instance through the Websocket connection. 58 | * 59 | * @param {Object} event Event to send. 60 | */ 61 | sendEvent(event) { 62 | this.outgoingMessages.push(event); 63 | } 64 | 65 | /** 66 | * Add a local stream and send it through SDP renegotiation. 67 | */ 68 | addLocalStream() {} 69 | 70 | /** 71 | * Remove a local stream and stop sending it through SDP renegotiation. 72 | */ 73 | removeLocalStream() {} 74 | 75 | /** 76 | * Reconfigure & setup the peer-to-peer connection (SDP). 77 | * Can be used anytime to renegotiate the SDP if necessary. 78 | */ 79 | renegotiateWebRTCConnection() {} 80 | }; 81 | -------------------------------------------------------------------------------- /tests/unit/apiManager.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Instance = require('../mocks/DeviceRenderer'); 4 | const Clipboard = require('../../src/plugins/Clipboard'); 5 | 6 | let instance; 7 | let exposedApiFunctions; 8 | 9 | describe('APIManager', () => { 10 | beforeEach(() => { 11 | instance = new Instance({}); 12 | exposedApiFunctions = instance.apiManager.getExposedApiFunctions(); 13 | 14 | new Clipboard(instance, { 15 | CLIPBOARD_TITLE: 'TEST CLIPBOARD PLUGIN TITLE', 16 | }); 17 | }); 18 | 19 | describe('has exposed api', () => { 20 | test('getRegisteredFunctions', () => { 21 | const registeredFunctions = exposedApiFunctions.utils.getRegisteredFunctions(); 22 | expect(Object.keys(registeredFunctions)).toEqual( 23 | expect.arrayContaining([ 24 | 'sendData', 25 | 'getRegisteredFunctions', 26 | 'addEventListener', 27 | 'disconnect', 28 | 'enableTrackEvents', 29 | 'trackEvents', 30 | ]), 31 | ); 32 | }); 33 | 34 | test('sendTrackEvent', () => { 35 | let events = []; 36 | 37 | exposedApiFunctions.analytics.enableTrackEvents(true); 38 | 39 | // attach callback to get events 40 | exposedApiFunctions.analytics.trackEvents((evts) => { 41 | events = evts; 42 | }); 43 | 44 | const button = document.getElementsByClassName('gm-clipboard-button')[0]; 45 | expect(button).toBeTruthy(); 46 | button.click(); 47 | 48 | // expect object to be exactly the same 49 | expect(events).toEqual([{category: 'widget', action: 'open', name: 'Clipboard'}]); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/unit/camera.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.mock('loglevel'); 4 | 5 | const MediaManager = require('../../src/plugins/MediaManager'); 6 | const Camera = require('../../src/plugins/Camera'); 7 | const Instance = require('../mocks/DeviceRenderer'); 8 | 9 | // eslint-disable-next-line no-unused-vars 10 | let mediaManager; 11 | let instance; 12 | 13 | describe('Camera Plugin', () => { 14 | describe('api', () => { 15 | test('exposes a high level constructor', () => { 16 | expect(typeof Camera).toBe('function'); 17 | }); 18 | }); 19 | 20 | describe('UI', () => { 21 | beforeEach(() => { 22 | instance = new Instance(); 23 | mediaManager = new MediaManager(instance); // needed for button initialisation 24 | new Camera(instance, { 25 | CAMERA_TITLE: 'TEST CAMERA PLUGIN BUTTON TITLE', 26 | }); 27 | }); 28 | 29 | test('is initialized properly at construct', () => { 30 | // Toolbar button 31 | expect(document.getElementsByClassName('gm-camera-button')).toHaveLength(1); 32 | }); 33 | 34 | test('has translations', () => { 35 | const plugin = document.body; 36 | expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST CAMERA PLUGIN BUTTON TITLE')); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/unit/clipboard.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.mock('loglevel'); 4 | 5 | const Clipboard = require('../../src/plugins/Clipboard'); 6 | const Instance = require('../mocks/DeviceRenderer'); 7 | 8 | let clipboard; 9 | let instance; 10 | let plugin; 11 | 12 | describe('Clipboard Plugin', () => { 13 | beforeEach(() => { 14 | instance = new Instance(); 15 | clipboard = new Clipboard(instance, {}); 16 | plugin = document.getElementsByClassName('gm-clipboard-plugin')[0]; 17 | }); 18 | 19 | describe('api', () => { 20 | test('exposes a high level constructor', () => { 21 | expect(typeof Clipboard).toBe('function'); 22 | }); 23 | }); 24 | 25 | describe('UI', () => { 26 | beforeEach(() => { 27 | instance = new Instance(); 28 | clipboard = new Clipboard(instance, { 29 | CLIPBOARD_TITLE: 'TEST CLIPBOARD PLUGIN TITLE', 30 | }); 31 | plugin = document.getElementsByClassName('gm-clipboard-plugin')[0]; 32 | }); 33 | 34 | test('is initialized properly at construct', () => { 35 | // Widget 36 | expect(document.getElementsByClassName('gm-clipboard-plugin')).toHaveLength(1); 37 | // Toolbar button 38 | expect(document.getElementsByClassName('gm-clipboard-button')).toHaveLength(1); 39 | }); 40 | 41 | test('has default values', () => { 42 | expect(clipboard.clipboardInput.value).toBe(''); 43 | }); 44 | 45 | test('has translations', () => { 46 | expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST CLIPBOARD PLUGIN TITLE')); 47 | }); 48 | 49 | test('reflects internal clipboard state when displayed', () => { 50 | const button = document.getElementsByClassName('gm-clipboard-button')[0]; 51 | 52 | clipboard.clipboard = 'test value'; 53 | button.click(); 54 | expect(clipboard.clipboardInput.value).toBe('test value'); 55 | }); 56 | }); 57 | 58 | describe('incoming events', () => { 59 | test('framework', () => { 60 | instance.emit('framework', 'clipboard too many arguments'); 61 | expect(clipboard.clipboard).toBe(''); 62 | 63 | instance.emit('framework', 'clipboard missingparam'); 64 | expect(clipboard.clipboard).toBe(''); 65 | 66 | instance.emit('framework', 'unrelevant channel'); 67 | expect(clipboard.clipboard).toBe(''); 68 | 69 | instance.emit('framework', 'clipboard from_android badvalue'); // Bad value 70 | expect(clipboard.clipboard).toBe(''); 71 | 72 | instance.emit('framework', 'clipboard from_android dGVzdCB2YWx1ZQ=='); 73 | expect(clipboard.clipboard).toBe('test value'); 74 | }); 75 | }); 76 | 77 | describe('outgoing events', () => { 78 | let sendEventSpy; 79 | 80 | beforeEach(() => { 81 | sendEventSpy = jest.spyOn(instance, 'sendEvent'); 82 | }); 83 | 84 | afterEach(() => { 85 | sendEventSpy.mockRestore(); 86 | }); 87 | 88 | test('clipboard send value', () => { 89 | expect(clipboard.clipboard).toBe(''); 90 | 91 | clipboard.clipboardInput.value = 'test value'; 92 | const event = new Event('input', {bubbles: true}); 93 | clipboard.clipboardInput.dispatchEvent(event); 94 | 95 | expect(clipboard.clipboard).toBe(''); 96 | 97 | clipboard.submitBtn.click(); 98 | instance.emit('framework', 'clipboard from_android dGVzdCB2YWx1ZQ=='); 99 | 100 | expect(sendEventSpy).toHaveBeenCalledTimes(1); 101 | expect(instance.outgoingMessages[0]).toEqual({ 102 | channel: 'framework', 103 | messages: ['set_device_clipboard dGVzdCB2YWx1ZQ=='], 104 | }); 105 | expect(clipboard.clipboard).toBe('test value'); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /tests/unit/fullscreen.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FullScreen = require('../../src/plugins/Fullscreen'); 4 | const Instance = require('../mocks/DeviceRenderer'); 5 | 6 | describe('FullScreen Plugin', () => { 7 | beforeEach(() => { 8 | const instance = new Instance(); 9 | new FullScreen(instance); 10 | }); 11 | 12 | describe('api', () => { 13 | test('exposes a high level constructor', () => { 14 | expect(typeof FullScreen).toBe('function'); 15 | }); 16 | }); 17 | 18 | describe('UI', () => { 19 | test('is initialized properly at construct', () => { 20 | // Toolbar button 21 | expect(document.getElementsByClassName('gm-fullscreen-button')).toHaveLength(1); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/unit/phone.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Phone = require('../../src/plugins/Phone'); 4 | const Instance = require('../mocks/DeviceRenderer'); 5 | 6 | let phone; 7 | let instance; 8 | let plugin; 9 | 10 | describe('Phone Plugin', () => { 11 | beforeEach(() => { 12 | instance = new Instance(); 13 | phone = new Phone(instance, {}); 14 | plugin = document.getElementsByClassName('gm-phone-plugin')[0]; 15 | }); 16 | 17 | describe('api', () => { 18 | test('exposes a high level constructor', () => { 19 | expect(typeof Phone).toBe('function'); 20 | }); 21 | }); 22 | 23 | describe('UI', () => { 24 | beforeEach(() => { 25 | instance = new Instance(); 26 | new Phone(instance, { 27 | PHONE_TITLE: 'TEST PHONE PLUGIN TITLE', 28 | PHONE_CALL_PLACEHOLDER: 'TEST PHONE PLUGIN CALL PLACEHOLDER', 29 | PHONE_CALL: 'TEST PHONE PLUGIN CALL', 30 | PHONE_INCOMING: 'TEST PHONE PLUGIN INCOMING', 31 | PHONE_MESSAGE_PLACEHOLDER: 'TEST PHONE PLUGIN MESSAGE PLACEHOLDER', 32 | PHONE_MESSAGE: 'TEST PHONE PLUGIN MESSAGE', 33 | PHONE_MESSAGE_VALUE: 'TEST PHONE PLUGIN MESSAGE_VALUE', 34 | }); 35 | plugin = document.getElementsByClassName('gm-phone-plugin')[0]; 36 | }); 37 | 38 | test('is initialized properly at construct', () => { 39 | // Widget 40 | expect(document.getElementsByClassName('gm-phone-plugin')).toHaveLength(1); 41 | // Toolbar button 42 | expect(document.getElementsByClassName('gm-phone-button')).toHaveLength(1); 43 | }); 44 | 45 | test('has translations', () => { 46 | expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST PHONE PLUGIN TITLE')); 47 | expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST PHONE PLUGIN CALL PLACEHOLDER')); 48 | expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST PHONE PLUGIN CALL')); 49 | expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST PHONE PLUGIN INCOMING')); 50 | expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST PHONE PLUGIN MESSAGE PLACEHOLDER')); 51 | expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST PHONE PLUGIN MESSAGE')); 52 | expect(plugin.innerHTML).toEqual(expect.stringContaining('TEST PHONE PLUGIN MESSAGE_VALUE')); 53 | }); 54 | }); 55 | 56 | describe('outgoing events', () => { 57 | test('gsm call', () => { 58 | const sendEventSpy = jest.spyOn(instance, 'sendEvent'); 59 | 60 | ['jean-michel', ''].forEach((invalidValue) => { 61 | phone.phoneInput.setValue(invalidValue, true); 62 | phone.phoneBtn.click(); 63 | expect(sendEventSpy).toHaveBeenCalledTimes(0); 64 | }); 65 | 66 | phone.phoneInput.setValue('0123456789', true); 67 | phone.phoneBtn.click(); 68 | expect(sendEventSpy).toHaveBeenCalledTimes(1); 69 | 70 | expect(instance.outgoingMessages[0]).toEqual({channel: 'baseband', messages: ['gsm call 0123456789']}); 71 | }); 72 | 73 | test('sms send', () => { 74 | const sendEventSpy = jest.spyOn(instance, 'sendEvent'); 75 | phone.phoneInput.setValue('0123456789', true); 76 | 77 | const event = new KeyboardEvent('keyup', {key: ''}); 78 | phone.textInput.dispatchEvent(event); 79 | 80 | phone.textBtn.click(); 81 | expect(sendEventSpy).toHaveBeenCalledTimes(0); 82 | 83 | phone.textInput.value = 'Hello world'; 84 | const eventt = new KeyboardEvent('keyup', {key: 'Hello world'}); 85 | phone.textInput.dispatchEvent(eventt); 86 | 87 | phone.textBtn.click(); 88 | expect(sendEventSpy).toHaveBeenCalledTimes(1); 89 | 90 | expect(instance.outgoingMessages[0]).toEqual({ 91 | channel: 'baseband', 92 | messages: ['sms send 0123456789 Hello world'], 93 | }); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /tests/unit/streambitrate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const StreamBitrate = require('../../src/plugins/StreamBitrate'); 4 | const Instance = require('../mocks/DeviceRenderer'); 5 | 6 | let chooser; 7 | let instance; 8 | 9 | describe.only('StreamBitrate Plugin', () => { 10 | beforeEach(() => { 11 | instance = new Instance(); 12 | chooser = new StreamBitrate(instance, {}); 13 | }); 14 | 15 | describe('api', () => { 16 | test('exposes a high level constructor', () => { 17 | expect(typeof StreamBitrate).toBe('function'); 18 | }); 19 | }); 20 | 21 | describe('UI', () => { 22 | beforeEach(() => { 23 | instance = new Instance(); 24 | new StreamBitrate(instance, { 25 | STREAMRATE_TITLE: 'TEST QUALITY PLUGIN TITLE', 26 | }); 27 | }); 28 | 29 | test('is initialized properly at construct', () => { 30 | // Toolbar button 31 | document.querySelector('.gm-streamrate-button'); 32 | expect(document.getElementsByClassName('gm-streamrate-chooser')).toHaveLength(1); 33 | }); 34 | }); 35 | 36 | describe('outgoing events', () => { 37 | test('hq', () => { 38 | const sendEventSpy = jest.spyOn(instance, 'sendEvent'); 39 | 40 | chooser.highQuality = false; 41 | document.querySelector('.gm-streamrate-chooser').click(); 42 | expect(sendEventSpy).toHaveBeenCalledTimes(1); 43 | expect(instance.outgoingMessages[0]).toEqual({type: 'BITRATE', videoBitrate: 5000, audioBitrate: 192000}); 44 | 45 | document.querySelector('.gm-streamrate-chooser').click(); 46 | expect(sendEventSpy).toHaveBeenCalledTimes(2); 47 | expect(instance.outgoingMessages[1]).toEqual({type: 'BITRATE', videoBitrate: 0, audioBitrate: 0}); 48 | }); 49 | 50 | test('default', () => { 51 | const sendEventSpy = jest.spyOn(instance, 'sendEvent'); 52 | 53 | chooser.highQuality = true; 54 | document.querySelector('.gm-streamrate-chooser').click(); 55 | expect(sendEventSpy).toHaveBeenCalledTimes(1); 56 | expect(instance.outgoingMessages[0]).toEqual({type: 'BITRATE', videoBitrate: 0, audioBitrate: 0}); 57 | 58 | document.querySelector('.gm-streamrate-chooser').click(); 59 | expect(sendEventSpy).toHaveBeenCalledTimes(2); 60 | expect(instance.outgoingMessages[1]).toEqual({type: 'BITRATE', videoBitrate: 5000, audioBitrate: 192000}); 61 | }); 62 | }); 63 | }); 64 | --------------------------------------------------------------------------------