├── .gitignore ├── .readme ├── demo.gif └── icon.png ├── .travis.yml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ThirdPartyNotices.txt ├── gulpfile.js ├── package.json ├── src ├── iosDebug.ts ├── iosDebugAdapter.ts ├── iosDebugAdapterInterfaces.d.ts ├── localtunnel.d.ts └── utilities.ts ├── test ├── iosDebugAdapter.test.ts └── utilities.test.ts ├── tsconfig.json └── typings.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | out/ 4 | lib/ 5 | typings/ 6 | *.vsix 7 | -------------------------------------------------------------------------------- /.readme/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-ios-web-debug/561503a8a4d1f23f2b7c8479b6a03b2d371466df/.readme/demo.gif -------------------------------------------------------------------------------- /.readme/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-ios-web-debug/561503a8a4d1f23f2b7c8479b6a03b2d371466df/.readme/icon.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "node" 5 | 6 | # https://docs.travis-ci.com/user/languages/javascript-with-nodejs#Node.js-v4-(or-io.js-v3)-compiler-requirementsdf 7 | env: 8 | - CXX=g++-4.8 9 | 10 | addons: 11 | apt: 12 | sources: 13 | - ubuntu-toolchain-r-test 14 | packages: 15 | - g++-4.8 16 | 17 | before_script: 18 | - npm install -g gulp 19 | - npm install -g typings 20 | - typings install 21 | 22 | script: 23 | - gulp build-test 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "launch as server", 7 | "type": "node", 8 | "program": "${workspaceRoot}/out/src/iosDebug.js", 9 | "runtimeArgs": ["--harmony"], 10 | "stopOnEntry": false, 11 | "args": [ "--server=4712" ], 12 | "sourceMaps": true, 13 | "outDir": "${workspaceRoot}/out" 14 | }, 15 | { 16 | "name": "test", 17 | "type": "node", 18 | "program": "${workspaceRoot}/node_modules/gulp/bin/gulp.js", 19 | "stopOnEntry": false, 20 | "args": [ "test" ], 21 | "sourceMaps": true, 22 | "outDir": "${workspaceRoot}/out" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version 10 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "gulp", 4 | "isShellCommand": true, 5 | "tasks": [ 6 | { 7 | "taskName": "watch", 8 | "args": [], 9 | "isBuildCommand": true, 10 | "isWatching": true, 11 | "problemMatcher": [ 12 | "$tsc" 13 | ] 14 | }, 15 | { 16 | "taskName": "watch-build-test", 17 | "isWatching": true, 18 | "isTestCommand": true 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/*.ts 2 | **/*.map 3 | .vscode/** 4 | lib/** 5 | typings/** 6 | test/** 7 | src/** 8 | *.vsix 9 | .gitignore 10 | gulpfile.js 11 | tsconfig.json 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Development setup 2 | We welcome any quality bugfixes or contributions! 3 | 4 | To avoid a conflict, delete the installed extension at `~/.vscode/extensions/msjsdiag.debugger-for-ios-web`. 5 | 6 | ### Windows 7 | * In `C:/Users//.vscode/extensions/`, `git clone` this repository 8 | 9 | ### OS X/Linux 10 | * `git clone` this repository 11 | * Run `ln -s ~/.vscode/extensions/vscode-ios-web-debug` 12 | * You could clone it to the extensions directory if you want, but working with hidden folders in OS X can be a pain. 13 | 14 | ### Then... 15 | * `cd` to the folder you just cloned 16 | * Run `npm install -g gulp` and `npm install` 17 | * You may see an error if `bufferutil` or `utf-8-validate` fail to build. These native modules required by `vscode-chrome-debug-core` are optional and the adapter should work fine without them. 18 | * Run `gulp build` 19 | 20 | ### Getting the proxy 21 | * You'll need to also follow the readme instructions for getting the safari -> chrome proxy installed and running before debugging 22 | 23 | ## Debugging 24 | In VS Code, run the `launch as server` launch config - it will start the adapter as a server listening on port 4712. In your test app launch.json, include this flag at the top level: `"debugServer": "4712"`. Then you'll be able to debug the adapter in the first instance of VS Code, in its original TypeScript, using sourcemaps. 25 | 26 | ## Testing 27 | There is a set of mocha tests which can be run with `gulp test` or with the `test` launch config. Also run `gulp tslint` to check your code against our tslint rules. 28 | 29 | See the project under testapp/ for a bunch of test scenarios crammed onto one page. 30 | 31 | ## Naming 32 | Client: VS Code 33 | Target: The debuggee, which implements the Chrome Debug Protocol 34 | Server-mode: In the normal use-case, the extension does not run in server-mode. For debugging, you can run it as a debug server - see the 'Debugging' section above. 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Microsoft 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 |
4 | logo 5 |
6 | VS Code - Debugger for iOS Web 7 |
8 |
9 |

10 | 11 |

Debug your JavaScript code running in Safari on iOS devices from VS Code.

12 | 13 |

14 | Travis 15 | Release 16 |

17 | 18 | ### Deprecation update 19 | The iOS Web debugger has been deprecated, and we recommend you using [remotedebug-ios-webkit-adaptor](https://github.com/RemoteDebug/remotedebug-ios-webkit-adapter) together with VS Code. A guide can be read here https://medium.com/@auchenberg/hello-remotedebug-ios-webkit-adapter-debug-safari-and-ios-webviews-from-anywhere-2a8553df7465. 20 | 21 | ### Introduction 22 | The VS Code iOS Web Debugger allows to debug your JavaScript code running in Safari on iOS devices (and iOS Simulators) from VS Code both on **Windows and Mac** without addtional tools. 23 | 24 | ![](https://cdn.rawgit.com/Microsoft/vscode-ios-web-debug/master/.readme/demo.gif) 25 | 26 | **Supported features** 27 | * Setting breakpoints, including in source files when source maps are enabled 28 | * Stepping 29 | * Stack traces 30 | * Locals 31 | * Debugging eval scripts, script tags, and scripts that are added dynamically 32 | * Watches 33 | * Console 34 | * Virtual port forwarding via HTTP tunnel from local computer. 35 | 36 | **Unsupported scenarios** 37 | * Debugging web workers 38 | * Any features that aren't script debugging. 39 | 40 | ## Getting Started 41 | 42 | Before you use the debugger you need to make sure you have the [latest version of iTunes](http://www.apple.com/itunes/download/) installed, as we need a few libraries provided by iTunes to talk to the iOS devices. 43 | 44 | #### Windows 45 | Nothing to do as there is a proxy included with the extension from the `vs-libimobile` npm package 46 | 47 | #### OSX/Mac 48 | Make sure you have Homebrew installed, and run the following command to install [ios-webkit-debug-proxy](https://github.com/google/ios-webkit-debug-proxy) 49 | 50 | ``` 51 | brew install ios-webkit-debug-proxy 52 | ``` 53 | 54 | #### iOS Device 55 | On your iOS device then go to `Settings > Safari > Advanced`, and enable the `Web Inspector` option. 56 | 57 | ## Using the debugger 58 | 59 | When your launch config is set up, you can debug your project! Pick a launch config from the dropdown on the Debug pane in Code. Press the play button or F5 to start. 60 | 61 | ### Configuration 62 | 63 | The extension operates in two modes - it can `launch` a URL in Safari on the device, or it can `attach` to a running tab inside Safari. Just like when using the Node debugger, you configure these modes with a `.vscode/launch.json` file in the root directory of your project. You can create this file manually, or Code will create one for you if you try to run your project, and it doesn't exist yet. 64 | 65 | To use this extension, you must first open the folder containing the project you want to work on. 66 | 67 | #### Launch 68 | Two example `launch.json` configs. You must specify either `file` or `url` to launch Chrome against a local file or a url. If you use a url, set `webRoot` to the directory that files are served from. This can be either an absolute path or a path relative to the workspace (the folder open in Code). It's used to resolve urls (like "http://localhost/app.js") to a file on disk (like "/users/me/project/app.js"), so be careful that it's set correctly. 69 | 70 | ```json 71 | { 72 | "version": "0.1.0", 73 | "configurations": [ 74 | { 75 | "name": "iOS - Launch localhost with sourcemaps", 76 | "type": "ios", 77 | "request": "launch", 78 | "port": 9222, 79 | "url": "http://dev.domain.com/", 80 | "webRoot": "${workspaceRoot}", 81 | "deviceName": "*", 82 | "sourceMaps": true 83 | }, 84 | { 85 | "name": "iOS - Launch localhost with sourcemaps via Tunnel", 86 | "type": "ios", 87 | "request": "launch", 88 | "port": 9222, 89 | "webRoot": "${workspaceRoot}", 90 | "deviceName": "*", 91 | "sourceMaps": true, 92 | "tunnelPort": 8080 93 | } 94 | ] 95 | } 96 | ``` 97 | 98 | #### Attach 99 | 100 | Attach to an already running browser tab in Safari by using the `url` to match the correct tab 101 | 102 | An example `launch.json` config. 103 | ```json 104 | { 105 | "version": "0.1.0", 106 | "configurations": [ 107 | { 108 | "name": "iOS - Attach", 109 | "type": "ios", 110 | "request": "attach", 111 | "port": 9222, 112 | "sourceMaps": true, 113 | "url": "http://dev.domain.com/", 114 | "webRoot": "${workspaceRoot}", 115 | "deviceName": "*" 116 | } 117 | ] 118 | } 119 | ``` 120 | 121 | #### Other optional launch config fields 122 | * `diagnosticLogging`: When true, the adapter logs its own diagnostic info to the console 123 | * `deviceName`: The name of the devices, if multiple devices are connected. `*` matches any device. 124 | * `tunnelPort`: Port to be tunnel via local HTTP port. Usually the `port` your developer server is running on. 125 | 126 | ## Troubleshooting 127 | Please have a look at [vscode-chrome-debug](https://github.com/Microsoft/vscode-chrome-debug/) for additional troubleshooting and options. 128 | 129 | === 130 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 131 | -------------------------------------------------------------------------------- /ThirdPartyNotices.txt: -------------------------------------------------------------------------------- 1 | This project may use or incorporate third party material from the projects listed below. The original copyright notice and the license under which Microsoft received such third party material are set forth below. Microsoft reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. 2 | 3 | websockets-ws 4 | 5 | (The MIT License) 6 | 7 | Copyright (c) 2011 Einar Otto Stangvik 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | const gulp = require('gulp'); 6 | const path = require('path'); 7 | const ts = require('gulp-typescript'); 8 | const log = require('gulp-util').log; 9 | const typescript = require('typescript'); 10 | const sourcemaps = require('gulp-sourcemaps'); 11 | const mocha = require('gulp-mocha'); 12 | const tslint = require('gulp-tslint'); 13 | 14 | var sources = [ 15 | 'src', 16 | 'test', 17 | 'typings/main' 18 | ].map(function(tsFolder) { return tsFolder + '/**/*.ts'; }); 19 | 20 | var lintSources = [ 21 | 'src', 22 | 'test' 23 | ].map(function(tsFolder) { return tsFolder + '/**/*.ts'; }); 24 | 25 | var projectConfig = { 26 | noImplicitAny: false, 27 | target: 'ES5', 28 | module: 'commonjs', 29 | declaration: true, 30 | typescript: typescript, 31 | moduleResolution: "node" 32 | }; 33 | 34 | gulp.task('build', function () { 35 | return gulp.src(sources, { base: '.' }) 36 | .pipe(sourcemaps.init()) 37 | .pipe(ts(projectConfig)).js 38 | .pipe(sourcemaps.write('.', { includeContent: false, sourceRoot: 'file:///' + __dirname })) 39 | .pipe(gulp.dest('out')); 40 | }); 41 | 42 | gulp.task('watch', ['build'], function(cb) { 43 | log('Watching build sources...'); 44 | return gulp.watch(sources, ['build']); 45 | }); 46 | 47 | gulp.task('default', ['build']); 48 | 49 | gulp.task('tslint', function() { 50 | return gulp.src(lintSources, { base: '.' }) 51 | .pipe(tslint()) 52 | .pipe(tslint.report('verbose')); 53 | }); 54 | 55 | function test() { 56 | return gulp.src('out/test/**/*.test.js', { read: false }) 57 | .pipe(mocha({ ui: 'tdd' })) 58 | .on('error', function(e) { 59 | log(e ? e.toString() : 'error in test task!'); 60 | this.emit('end'); 61 | }); 62 | } 63 | 64 | gulp.task('build-test', ['build'], test); 65 | gulp.task('test', test); 66 | 67 | gulp.task('watch-build-test', ['build', 'build-test'], function() { 68 | return gulp.watch(sources, ['build', 'build-test']); 69 | }); 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "debugger-for-ios-web", 3 | "displayName": "Debugger for iOS Web", 4 | "version": "0.1.2", 5 | "icon": ".readme/icon.png", 6 | "description": "Debug your JavaScript code running in Safari on iOS devices from VS Code.", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Microsoft/vscode-ios-web-debug" 10 | }, 11 | "publisher": "msjsdiag", 12 | "bugs": "https://github.com/Microsoft/vscode-ios-web-debug/issues", 13 | "engines": { 14 | "vscode": "*" 15 | }, 16 | "categories": [ 17 | "Debuggers" 18 | ], 19 | "license": "SEE LICENSE IN LICENSE.txt", 20 | "dependencies": { 21 | "vscode-chrome-debug-core": "0.1.16", 22 | "localtunnel": "1.8.1" 23 | }, 24 | "optionalDependencies": { 25 | "vs-libimobile": "^0.0.3" 26 | }, 27 | "devDependencies": { 28 | "gulp": "^3.9.1", 29 | "gulp-mocha": "^2.1.3", 30 | "gulp-sourcemaps": "^1.5.2", 31 | "gulp-tslint": "^3.3.1", 32 | "gulp-typescript": "^2.12.1", 33 | "gulp-util": "^3.0.5", 34 | "mocha": "^2.3.3", 35 | "mockery": "^1.4.0", 36 | "sinon": "^1.17.2", 37 | "tslint": "^2.5.1", 38 | "typescript": "^1.8.9", 39 | "typings": "^0.7.12" 40 | }, 41 | "scripts": { 42 | "test": "node ./node_modules/mocha/bin/mocha --recursive -u tdd ./out/test/" 43 | }, 44 | "contributes": { 45 | "debuggers": [ 46 | { 47 | "type": "ios", 48 | "label": "iOS", 49 | "enableBreakpointsFor": { 50 | "languageIds": [ 51 | "javascript", 52 | "typescriptreact" 53 | ] 54 | }, 55 | "program": "./out/src/iosDebug.js", 56 | "runtime": "node", 57 | "initialConfigurations": [ 58 | { 59 | "name": "iOS - launch localhost", 60 | "type": "ios", 61 | "request": "launch", 62 | "port": 9222, 63 | "url": "http://localhost:8080", 64 | "webRoot": "${workspaceRoot}", 65 | "deviceName": "*", 66 | "tunnelPort": 8080 67 | }, 68 | { 69 | "name": "iOS - launch mysite.com", 70 | "type": "ios", 71 | "request": "launch", 72 | "port": 9222, 73 | "url": "http://mysite.com/index.html", 74 | "webRoot": "${workspaceRoot}", 75 | "deviceName": "*" 76 | }, 77 | { 78 | "name": "iOS - attach to dev.domain.com", 79 | "type": "ios", 80 | "request": "attach", 81 | "port": 9222, 82 | "url": "http://dev.domain.com/", 83 | "webRoot": "${workspaceRoot}", 84 | "deviceName": "*" 85 | } 86 | ], 87 | "configurationAttributes": { 88 | "launch": { 89 | "required": [ 90 | "port" 91 | ], 92 | "properties": { 93 | "port": { 94 | "type": "number", 95 | "description": "Port to use for Chrome remote debugging.", 96 | "default": 9222 97 | }, 98 | "sourceMaps": { 99 | "type": "boolean", 100 | "description": "Use JavaScript source maps (if they exist).", 101 | "default": true 102 | }, 103 | "diagnosticLogging": { 104 | "type": "boolean", 105 | "description": "When true, the adapter logs its own diagnostic info to the console", 106 | "default": false 107 | }, 108 | "webRoot": { 109 | "type": "string", 110 | "description": "This specifies the workspace absolute path to the webserver root.", 111 | "default": "${workspaceRoot}" 112 | }, 113 | "url": { 114 | "type": "string", 115 | "description": "A url to launch in the attached browser. If you are using the tunnelPort or startLocalServer settings, you can use a relative url here.", 116 | "default": "http://mysite.com/index.html" 117 | }, 118 | "deviceName": { 119 | "type": "string", 120 | "description": "The name of the device to attach to (can be found at the proxy json endpoint) or '*' for any", 121 | "default": "*" 122 | }, 123 | "proxyExecutable": { 124 | "type": [ 125 | "string", 126 | "null" 127 | ], 128 | "description": "Workspace absolute path to the proxy executable to be used when attaching to a 'device'. If not specified, ios_webkit_debug_proxy.exe will be used from the default install location.", 129 | "default": null 130 | }, 131 | "proxyArgs": { 132 | "type": "array", 133 | "description": "Optional arguments passed to the proxy executable.", 134 | "items": { 135 | "type": "string" 136 | }, 137 | "default": [] 138 | }, 139 | "tunnelPort": { 140 | "type": "number", 141 | "description": "The local port to expose from the local machine to the internet via a tunnel proxy (or '0' to disable). This allows the connected iOS device to access a webserver running on the local machine", 142 | "default": 0 143 | } 144 | } 145 | }, 146 | "attach": { 147 | "required": [ 148 | "port" 149 | ], 150 | "properties": { 151 | "port": { 152 | "type": "number", 153 | "description": "Port to use for Chrome remote debugging.", 154 | "default": 9222 155 | }, 156 | "sourceMaps": { 157 | "type": "boolean", 158 | "description": "Use JavaScript source maps (if they exist).", 159 | "default": true 160 | }, 161 | "diagnosticLogging": { 162 | "type": "boolean", 163 | "description": "When true, the adapter logs its own diagnostic info to the console", 164 | "default": false 165 | }, 166 | "webRoot": { 167 | "type": "string", 168 | "description": "This specifies the workspace absolute path to the webserver root.", 169 | "default": "${workspaceRoot}" 170 | }, 171 | "url": { 172 | "type": "string", 173 | "description": "A url to find in the attached browser", 174 | "default": "http://mysite.com/index.html" 175 | }, 176 | "deviceName": { 177 | "type": "string", 178 | "description": "The name of the device to attach to (can be found at the proxy json endpoint) or '*' for any", 179 | "default": "*" 180 | }, 181 | "proxyExecutable": { 182 | "type": [ 183 | "string", 184 | "null" 185 | ], 186 | "description": "Workspace absolute path to the proxy executable to be used when attaching to a 'device'. If not specified, ios_webkit_debug_proxy.exe will be used from the default install location.", 187 | "default": null 188 | }, 189 | "proxyArgs": { 190 | "type": "array", 191 | "description": "Optional arguments passed to the proxy executable.", 192 | "items": { 193 | "type": "string" 194 | }, 195 | "default": [] 196 | } 197 | } 198 | } 199 | } 200 | } 201 | ] 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/iosDebug.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import * as path from 'path'; 6 | import {ChromeDebugSession, ChromeConnection, chromeTargetDiscoveryStrategy, logger} from 'vscode-chrome-debug-core'; 7 | import {IOSDebugAdapter} from './iosDebugAdapter'; 8 | 9 | const targetFilter = target => target && (!target.type || target.type === 'page'); 10 | const connection = new ChromeConnection(chromeTargetDiscoveryStrategy.getChromeTargetWebSocketURL, targetFilter); 11 | 12 | ChromeDebugSession.run(ChromeDebugSession.getSession({ 13 | logFileDirectory: path.resolve(__dirname, '../../'), 14 | targetFilter: targetFilter, 15 | adapter: new IOSDebugAdapter(connection) 16 | })); 17 | 18 | logger.log('debugger-for-ios-web: ' + require('../../package.json').version); -------------------------------------------------------------------------------- /src/iosDebugAdapter.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import {spawn, ChildProcess} from 'child_process'; 6 | import * as Url from 'url'; 7 | import * as localtunnel from 'localtunnel'; 8 | 9 | import {ChromeDebugAdapter, utils, logger} from 'vscode-chrome-debug-core'; 10 | import * as iosUtils from './utilities'; 11 | import {IProxySettings} from './iosDebugAdapterInterfaces'; 12 | 13 | export class IOSDebugAdapter extends ChromeDebugAdapter { 14 | private _proxyProc: ChildProcess; 15 | private _localTunnel: ILocalTunnelInfoObject; 16 | 17 | constructor(chromeConnection) { 18 | super(chromeConnection) 19 | } 20 | 21 | public launch(args: any): Promise { 22 | if (args.port == null) { 23 | return utils.errP('The "port" field is required in the launch config.'); 24 | } 25 | 26 | super.setupLogging(args); 27 | 28 | // Process the launch arguments 29 | const settings = this._getProxySettings(args); 30 | if (typeof settings == "string") { 31 | return utils.errP('Passed settings object is a string'); 32 | } 33 | 34 | let tunnelPort = args.tunnelPort || 0; 35 | let launchPromise: Promise = null; 36 | 37 | if (tunnelPort) { 38 | launchPromise = new Promise((resolve, reject) => { 39 | logger.log('Launching localtunnel against port ' + tunnelPort); 40 | localtunnel(tunnelPort, (err: Error, tunnel: ILocalTunnelInfoObject) => { 41 | // Navigate the to the given tunneled url 42 | if (err) { 43 | logger.error('Failed to launch localtunnel.'); 44 | return utils.errP(err); 45 | } 46 | 47 | logger.log('Success.' + tunnelPort); 48 | 49 | // Set the store member and listen for any errors 50 | this._localTunnel = tunnel; 51 | this._localTunnel.on('error', (err) => { 52 | logger.log('Tunneling proxy error: ' + err); 53 | this.terminateSession(); 54 | }); 55 | 56 | // Navigate the attached instance to the tunneled url 57 | let pathname = ""; 58 | if (args.url) { 59 | let url = Url.parse(args.url); 60 | pathname = url.pathname; 61 | } 62 | 63 | let navigateTo = Url.resolve(this._localTunnel.url, pathname); 64 | resolve(navigateTo); 65 | }); 66 | }); 67 | } else { 68 | launchPromise = Promise.resolve(args.url); 69 | } 70 | 71 | return this._spawnProxy(settings).then(() => { 72 | return launchPromise; 73 | }).then(url => { 74 | logger.log('Navigating to ' + url); 75 | (this)._chromeConnection.sendMessage("Page.navigate", {url: url}); 76 | }); 77 | } 78 | 79 | public attach(args: any): Promise { 80 | if (args.port == null) { 81 | return utils.errP('The "port" field is required in the attach config.'); 82 | } 83 | 84 | super.setupLogging(args); 85 | 86 | // Process the attach arguments 87 | const settings = this._getProxySettings(args); 88 | if (typeof settings == "string") { 89 | return utils.errP('Passed settings object is a string'); 90 | } 91 | 92 | return this._spawnProxy(settings); 93 | } 94 | 95 | public clearEverything(): void { 96 | if (this._localTunnel) { 97 | this._localTunnel.close(); 98 | this._localTunnel = null; 99 | } 100 | 101 | if (this._proxyProc) { 102 | this._proxyProc.kill('SIGINT'); 103 | this._proxyProc = null; 104 | } 105 | 106 | super.clearEverything(); 107 | } 108 | 109 | private _getProxySettings(args: any): IProxySettings | string { 110 | var settings: IProxySettings = null; 111 | var errorMessage: string = null; 112 | 113 | // Check that the proxy exists 114 | const proxyPath = args.proxyExecutable || iosUtils.getProxyPath(); 115 | if (!proxyPath) { 116 | if (utils.getPlatform() != utils.Platform.Windows) { 117 | errorMessage = `No iOS proxy was found. Install an iOS proxy (https://github.com/google/ios-webkit-debug-proxy) and specify a valid 'proxyExecutable' path`; 118 | } else { 119 | errorMessage = `No iOS proxy was found. Run 'npm install -g vs-libimobile' and specify a valid 'proxyExecutable' path`; 120 | } 121 | } else { 122 | // Grab the specified device name, or default to * (which means first) 123 | const optionalDeviceName = args.deviceName || "*"; 124 | 125 | // Start with remote debugging enabled 126 | const proxyPort = args.port || 9222; 127 | const proxyArgs = []; 128 | 129 | // Use default parameters for the ios_webkit_debug_proxy executable 130 | if (!args.proxyExecutable) { 131 | proxyArgs.push('--no-frontend'); 132 | 133 | // Set the ports available for devices 134 | proxyArgs.push('--config=null:' + proxyPort + ',:' + (proxyPort + 1) + '-' + (proxyPort + 101)); 135 | } 136 | 137 | if (args.proxyArgs) { 138 | // Add additional parameters 139 | proxyArgs.push(...args.proxyArgs); 140 | } 141 | 142 | settings = { 143 | proxyPath: proxyPath, 144 | optionalDeviceName: optionalDeviceName, 145 | proxyPort: proxyPort, 146 | proxyArgs: proxyArgs, 147 | originalArgs: args 148 | }; 149 | } 150 | 151 | return errorMessage || settings; 152 | } 153 | 154 | private _spawnProxy(settings: IProxySettings): Promise { 155 | // Spawn the proxy with the specified settings 156 | logger.log(`spawn('${settings.proxyPath}', ${JSON.stringify(settings.proxyArgs) })`); 157 | this._proxyProc = spawn(settings.proxyPath, settings.proxyArgs, { 158 | detached: true, 159 | stdio: ['ignore'] 160 | }); 161 | (this._proxyProc).unref(); 162 | this._proxyProc.on('error', (err) => { 163 | logger.log('device proxy error: ' + err); 164 | logger.log('Do you have the iTunes drivers installed?'); 165 | this.terminateSession(); 166 | }); 167 | 168 | // Now attach to the device 169 | return this._attachToDevice(settings.proxyPort, settings.optionalDeviceName).then((devicePort: number) => { 170 | let attachArgs = settings.originalArgs; 171 | attachArgs["port"] = devicePort; 172 | attachArgs["cwd"] = ""; 173 | return super.attach(attachArgs); 174 | }); 175 | } 176 | 177 | private _attachToDevice(proxyPort: number, deviceName: string): Promise { 178 | // Attach to a device over the proxy 179 | return utils.getURL(`http://localhost:${proxyPort}/json`).then(jsonResponse => { 180 | let devicePort = proxyPort; 181 | 182 | try { 183 | const responseArray = JSON.parse(jsonResponse); 184 | if (Array.isArray(responseArray)) { 185 | let devices = responseArray.filter(deviceInfo => deviceInfo && deviceInfo.url && deviceInfo.deviceName); 186 | 187 | // If a device name was specified find the matching one 188 | if (deviceName !== "*") { 189 | const matchingDevices = devices.filter(deviceInfo => deviceInfo.deviceName && deviceInfo.deviceName.toLowerCase() === deviceName.toLowerCase()); 190 | if (!matchingDevices.length) { 191 | logger.log(`Warning: Can't find a device with deviceName: ${deviceName}. Available devices: ${JSON.stringify(devices.map(d => d.deviceName))}`); 192 | } else { 193 | devices = matchingDevices; 194 | } 195 | } 196 | 197 | if (devices.length) { 198 | if (devices.length > 1 && deviceName !== "*") { 199 | logger.log(`Warning: Found more than one valid target device. Attaching to the first one. Available devices: ${JSON.stringify(devices.map(d => d.deviceName))}`); 200 | } 201 | 202 | // Get the port for the actual device endpoint 203 | const deviceUrl: string = devices[0].url; 204 | if (deviceUrl) { 205 | const portIndex = deviceUrl.indexOf(':'); 206 | if (portIndex > -1) { 207 | devicePort = parseInt(deviceUrl.substr(portIndex + 1), 10); 208 | } 209 | } 210 | } 211 | } 212 | } 213 | catch (e) { 214 | // JSON.parse can throw 215 | } 216 | 217 | return devicePort; 218 | }, 219 | e => { 220 | return utils.errP('Cannot connect to the proxy: ' + e.message); 221 | }); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/iosDebugAdapterInterfaces.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | export interface IProxySettings { 6 | proxyPath: string; 7 | optionalDeviceName: string; 8 | proxyPort: number, 9 | proxyArgs: string[], 10 | originalArgs: any 11 | } -------------------------------------------------------------------------------- /src/localtunnel.d.ts: -------------------------------------------------------------------------------- 1 | // Typings for localtunnel 2 | declare interface ILocalTunnelInfoObject { 3 | url: string; 4 | close: () => void; 5 | on(event: string, listener: Function): this; 6 | } 7 | 8 | declare module 'localtunnel' { 9 | function temp(port: number, callback: (err: Error, tunnel: ILocalTunnelInfoObject) => void): void; 10 | module temp {} 11 | export = temp; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import { utils } from 'vscode-chrome-debug-core'; 6 | import * as path from 'path'; 7 | 8 | export function getProxyPath(): string { 9 | const platform = utils.getPlatform(); 10 | if (platform === utils.Platform.Windows) { 11 | var proxy = path.resolve(__dirname, "../../node_modules/vs-libimobile/lib/ios_webkit_debug_proxy.exe"); 12 | if (utils.existsSync(proxy)) { 13 | return proxy; 14 | } 15 | } else if (platform === utils.Platform.OSX) { 16 | let path = '/usr/local/bin/ios_webkit_debug_proxy' 17 | if (utils.existsSync(path)) { 18 | return path; 19 | } 20 | } 21 | 22 | return null; 23 | } -------------------------------------------------------------------------------- /test/iosDebugAdapter.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import * as mockery from 'mockery'; 6 | import * as assert from 'assert'; 7 | import * as sinon from 'sinon'; 8 | import {utils, ChromeDebugAdapter, ChromeConnection, chromeTargetDiscoveryStrategy, logger} from 'vscode-chrome-debug-core'; 9 | 10 | /** Not mocked - use for type only */ 11 | import {IOSDebugAdapter as _IOSDebugAdapter} from '../src/iosDebugAdapter'; 12 | 13 | const MODULE_UNDER_TEST = '../src/iosDebugAdapter'; 14 | suite('IOSDebugAdapter', () => { 15 | 16 | function createAdapter(): _IOSDebugAdapter { 17 | const IOSDebugAdapter: typeof _IOSDebugAdapter = require(MODULE_UNDER_TEST).IOSDebugAdapter; 18 | const targetFilter = target => target && (!target.type || target.type === 'page'); 19 | const connection = new ChromeConnection(chromeTargetDiscoveryStrategy.getChromeTargetWebSocketURL, targetFilter); 20 | 21 | return new IOSDebugAdapter(connection); 22 | }; 23 | 24 | setup(() => { 25 | mockery.enable({ useCleanCache: true, warnOnReplace: false }); 26 | mockery.registerAllowables([MODULE_UNDER_TEST, './utilities', 'path', 'child_process']); 27 | mockery.warnOnUnregistered(false); // The npm packages pull in too many modules to list all as allowable 28 | 29 | // Stub wrapMethod to create a function if it doesn's already exist 30 | let originalWrap = (sinon).wrapMethod; 31 | sinon.stub(sinon, 'wrapMethod', function(...args) { 32 | if (!args[0][args[1]]) { 33 | args[0][args[1]] = () => { }; 34 | } 35 | return originalWrap.apply(this, args); 36 | }); 37 | }); 38 | 39 | teardown(() => { 40 | (sinon).wrapMethod.restore(); 41 | 42 | mockery.deregisterAll(); 43 | mockery.disable(); 44 | }); 45 | 46 | suite('launch()', () => { 47 | suite('no port', () => { 48 | test('if no port, rejects the launch promise', done => { 49 | mockery.registerMock('vscode-chrome-debug-core', { 50 | ChromeDebugAdapter: () => { }, 51 | utils: utils, 52 | logger: logger 53 | }); 54 | 55 | const adapter = createAdapter(); 56 | return adapter.launch({}).then( 57 | () => assert.fail('Expecting promise to be rejected'), 58 | e => done() 59 | ); 60 | }); 61 | }); 62 | 63 | suite('start server', () => { 64 | let adapterMock; 65 | let chromeConnectionMock; 66 | let utilitiesMock; 67 | let cpMock; 68 | let MockUtilities; 69 | let MockChromeConnection; 70 | setup(() => { 71 | let deviceInfo = [ 72 | { 73 | url: 'localhost:' + 8080, 74 | deviceName: 'iphone1' 75 | }, 76 | { 77 | url: 'localhost:' + (8080 + 1), 78 | deviceName: 'iphone2' 79 | } 80 | ]; 81 | MockChromeConnection = { }; 82 | class MockAdapter { 83 | public _chromeConnection = MockChromeConnection; 84 | }; 85 | class MockChildProcess { }; 86 | MockUtilities = { 87 | Platform: { Windows: 0, OSX: 1, Linux: 2 } 88 | }; 89 | 90 | mockery.registerMock('vscode-chrome-debug-core', { 91 | ChromeDebugAdapter: MockAdapter, 92 | utils: MockUtilities, 93 | logger: logger 94 | }); 95 | mockery.registerMock('child_process', MockChildProcess); 96 | 97 | adapterMock = sinon.mock(MockAdapter.prototype); 98 | adapterMock.expects('setupLogging').once(); 99 | adapterMock.expects('attach').returns(Promise.resolve('')); 100 | 101 | chromeConnectionMock = sinon.mock(MockChromeConnection); 102 | chromeConnectionMock.expects('sendMessage').withArgs("Page.navigate"); 103 | 104 | utilitiesMock = sinon.mock(MockUtilities); 105 | utilitiesMock.expects('getURL').returns(Promise.resolve(JSON.stringify(deviceInfo))); 106 | sinon.stub(MockUtilities, 'errP', () => Promise.reject('')); 107 | 108 | cpMock = sinon.mock(MockChildProcess); 109 | cpMock.expects('spawn').once().returns({ unref: () => { }, on: () => { } }); 110 | }); 111 | 112 | teardown(() => { 113 | chromeConnectionMock.verify(); 114 | adapterMock.verify(); 115 | utilitiesMock.verify(); 116 | cpMock.verify(); 117 | }); 118 | 119 | test('no settings should skip tunnel', done => { 120 | let isTunnelCreated = false; 121 | var MockServer = {}; 122 | var MockTunnel = () => { isTunnelCreated = true; }; 123 | mockery.registerMock('localtunnel', MockTunnel); 124 | 125 | const adapter = createAdapter(); 126 | return adapter.launch({ port: 1234, proxyExecutable: 'test.exe' }).then( 127 | () => { 128 | assert.equal(isTunnelCreated, false, "Should not create tunnel"); 129 | return done(); 130 | }, 131 | e => assert.fail('Expecting promise to succeed') 132 | ); 133 | }); 134 | 135 | suite('with settings', () => { 136 | let isTunnelCreated: boolean; 137 | let expectedWebRoot: string; 138 | let expectedPort: number; 139 | let instanceMock; 140 | let MockServer = {}; 141 | let MockTunnelInstance = {}; 142 | setup(() => { 143 | isTunnelCreated = false; 144 | expectedWebRoot = "root"; 145 | expectedPort = 8080; 146 | var MockTunnel = (a, f) => { isTunnelCreated = true; f(null, MockTunnelInstance); }; 147 | MockTunnelInstance = { url: "index.html" }; 148 | 149 | mockery.registerMock('localtunnel', MockTunnel); 150 | instanceMock = sinon.mock(MockTunnelInstance); 151 | instanceMock.expects("on").once(); 152 | 153 | }); 154 | teardown(() => { 155 | assert.equal(isTunnelCreated, true, "Should create tunnel"); 156 | instanceMock.verify(); 157 | }); 158 | 159 | test('tunnelPort alone should start the localtunnel', done => { 160 | const adapter = createAdapter(); 161 | return adapter.launch({ port: 1234, proxyExecutable: 'test.exe', tunnelPort: 9283 }).then( 162 | () => done(), 163 | e => assert.fail('Expecting promise to succeed') 164 | ); 165 | }); 166 | 167 | test('tunnelPort should use tunnel url', done => { 168 | let expectedUrl = "http://localtunnel.me/path/"; 169 | MockTunnelInstance = { url: expectedUrl }; 170 | 171 | instanceMock = sinon.mock(MockTunnelInstance); 172 | instanceMock.expects("on").once(); 173 | 174 | chromeConnectionMock.restore(); 175 | chromeConnectionMock = sinon.mock(MockChromeConnection); 176 | chromeConnectionMock.expects('sendMessage').withArgs("Page.navigate", {url: expectedUrl}); 177 | 178 | const adapter = createAdapter(); 179 | return adapter.launch({ port: 1234, proxyExecutable: 'test.exe', tunnelPort: 9283 }).then( 180 | () => done(), 181 | e => assert.fail('Expecting promise to succeed') 182 | ); 183 | }); 184 | 185 | test('tunnelPort should merge url', done => { 186 | let tunnelUrl = "http://localtunnel.me/"; 187 | let argsUrl = "http://website.com/index.html"; 188 | let expectedUrl = tunnelUrl + "index.html"; 189 | MockTunnelInstance = { url: tunnelUrl }; 190 | 191 | instanceMock = sinon.mock(MockTunnelInstance); 192 | instanceMock.expects("on").once(); 193 | 194 | chromeConnectionMock.restore(); 195 | chromeConnectionMock = sinon.mock(MockChromeConnection); 196 | chromeConnectionMock.expects('sendMessage').withArgs("Page.navigate", {url: expectedUrl}); 197 | 198 | const adapter = createAdapter(); 199 | return adapter.launch({ port: 1234, proxyExecutable: 'test.exe', tunnelPort: 9283, url: argsUrl }).then( 200 | () => done(), 201 | e => assert.fail('Expecting promise to succeed') 202 | ); 203 | }); 204 | }); 205 | }); 206 | }); 207 | 208 | suite('attach()', () => { 209 | suite('no port', () => { 210 | test('if no port, rejects the attach promise', done => { 211 | mockery.registerMock('vscode-chrome-debug-core', { 212 | ChromeDebugAdapter: () => { }, 213 | utils: utils, 214 | logger: logger 215 | }); 216 | 217 | const adapter = createAdapter(); 218 | return adapter.attach({}).then( 219 | () => assert.fail('Expecting promise to be rejected'), 220 | e => done() 221 | ); 222 | }); 223 | }); 224 | 225 | suite('valid port', () => { 226 | let adapterMock; 227 | let utilitiesMock; 228 | let cpMock; 229 | let MockUtilities; 230 | setup(() => { 231 | class MockAdapter { }; 232 | class MockChildProcess { }; 233 | MockUtilities = { 234 | Platform: { Windows: 0, OSX: 1, Linux: 2 } 235 | }; 236 | 237 | mockery.registerMock('vscode-chrome-debug-core', { 238 | ChromeDebugAdapter: MockAdapter, 239 | utils: MockUtilities, 240 | logger: logger 241 | }); 242 | mockery.registerMock('child_process', MockChildProcess); 243 | 244 | adapterMock = sinon.mock(MockAdapter.prototype); 245 | adapterMock.expects('setupLogging').once(); 246 | 247 | utilitiesMock = sinon.mock(MockUtilities); 248 | sinon.stub(MockUtilities, 'errP', () => Promise.reject('')); 249 | 250 | cpMock = sinon.mock(MockChildProcess); 251 | }); 252 | 253 | teardown(() => { 254 | adapterMock.verify(); 255 | utilitiesMock.verify(); 256 | cpMock.verify(); 257 | }); 258 | 259 | test('if no proxy, returns error on osx', done => { 260 | sinon.stub(MockUtilities, 'getPlatform', () => MockUtilities.Platform.OSX); 261 | 262 | const adapter = createAdapter(); 263 | return adapter.attach({ port: 1234 }).then( 264 | () => assert.fail('Expecting promise to be rejected'), 265 | e => { 266 | adapterMock.verify(); 267 | return done(); 268 | } 269 | ); 270 | }); 271 | 272 | test('if no proxy, returns error on linux', done => { 273 | sinon.stub(MockUtilities, 'getPlatform', () => MockUtilities.Platform.Linux); 274 | 275 | const adapter = createAdapter(); 276 | return adapter.attach({ port: 1234 }).then( 277 | () => assert.fail('Expecting promise to be rejected'), 278 | e => { 279 | adapterMock.verify(); 280 | return done(); 281 | } 282 | ); 283 | }); 284 | 285 | test('if no proxy, returns error on windows', done => { 286 | sinon.stub(MockUtilities, 'getPlatform', () => MockUtilities.Platform.Windows); 287 | sinon.stub(MockUtilities, 'existsSync', () => false); 288 | 289 | const adapter = createAdapter(); 290 | return adapter.attach({ port: 1234 }).then( 291 | () => assert.fail('Expecting promise to be rejected'), 292 | e => { 293 | adapterMock.verify(); 294 | return done(); 295 | } 296 | ); 297 | }); 298 | 299 | test('if valid port and proxy path, spawns the proxy', done => { 300 | sinon.stub(MockUtilities, 'getPlatform', () => MockUtilities.Platform.Windows); 301 | sinon.stub(MockUtilities, 'existsSync', () => true); 302 | utilitiesMock.expects('getURL').returns(Promise.reject('')); 303 | 304 | cpMock.expects('spawn').once().returns({ unref: () => { }, on: () => { } }); 305 | 306 | const adapter = createAdapter(); 307 | return adapter.attach({ port: 1234 }).then( 308 | () => assert.fail('Expecting promise to be rejected'), 309 | e => { 310 | adapterMock.verify(); 311 | utilitiesMock.verify(); 312 | cpMock.verify(); 313 | return done(); 314 | } 315 | ); 316 | }); 317 | }); 318 | 319 | suite('device', () => { 320 | let adapterMock; 321 | let utilitiesMock; 322 | let cpMock; 323 | setup(() => { 324 | class MockAdapter { }; 325 | class MockChildProcess { }; 326 | var MockUtilities = { 327 | Platform: { Windows: 0, OSX: 1, Linux: 2 }, 328 | Logger: { log: () => { } } 329 | }; 330 | 331 | mockery.registerMock('vscode-chrome-debug-core', { 332 | ChromeDebugAdapter: MockAdapter, 333 | utils: MockUtilities, 334 | logger: logger 335 | }); 336 | mockery.registerMock('child_process', MockChildProcess); 337 | 338 | adapterMock = sinon.mock(MockAdapter.prototype); 339 | adapterMock.expects('setupLogging').once(); 340 | 341 | utilitiesMock = sinon.mock(MockUtilities); 342 | 343 | cpMock = sinon.mock(MockChildProcess); 344 | cpMock.expects('spawn').once().returns({ unref: () => { }, on: () => { } }); 345 | }); 346 | 347 | teardown(() => { 348 | adapterMock.verify(); 349 | utilitiesMock.verify(); 350 | cpMock.verify(); 351 | }); 352 | 353 | test('if no proxy data, returns the proxy port', done => { 354 | let proxyPort = 1234; 355 | let deviceInfo = []; 356 | utilitiesMock.expects('getURL').returns(Promise.resolve(JSON.stringify(deviceInfo))); 357 | 358 | adapterMock.expects('attach').withArgs(sinon.match({ 359 | port: proxyPort, 360 | cwd: '' 361 | })).returns(Promise.resolve('')); 362 | 363 | const adapter = createAdapter(); 364 | return adapter.attach({ port: proxyPort, proxyExecutable: 'test.exe' }).then( 365 | done(), 366 | e => assert.fail('Expecting promise to succeed') 367 | ); 368 | }); 369 | 370 | test('if valid proxy data, returns the first device port', done => { 371 | let proxyPort = 1234; 372 | let devicePort = 9999; 373 | let deviceInfo = [ 374 | { 375 | url: 'localhost:' + devicePort, 376 | deviceName: 'iphone1' 377 | }, 378 | { 379 | url: 'localhost:' + (devicePort + 1), 380 | deviceName: 'iphone2' 381 | } 382 | ]; 383 | utilitiesMock.expects('getURL').returns(Promise.resolve(JSON.stringify(deviceInfo))); 384 | 385 | adapterMock.expects('attach').withArgs(sinon.match({ 386 | port: devicePort, 387 | cwd: '' 388 | })).returns(Promise.resolve('')); 389 | 390 | const adapter = createAdapter(); 391 | return adapter.attach({ port: proxyPort, proxyExecutable: 'test.exe' }).then( 392 | done(), 393 | e => assert.fail('Expecting promise to succeed') 394 | ); 395 | }); 396 | 397 | test('if valid proxy data and unknown deviceName, returns the first device port', done => { 398 | let proxyPort = 1234; 399 | let devicePort = 9999; 400 | let deviceInfo = [ 401 | { 402 | url: 'localhost:' + devicePort, 403 | deviceName: 'iphone1' 404 | }, 405 | { 406 | url: 'localhost:' + (devicePort + 1), 407 | deviceName: 'iphone2' 408 | } 409 | ]; 410 | utilitiesMock.expects('getURL').returns(Promise.resolve(JSON.stringify(deviceInfo))); 411 | 412 | adapterMock.expects('attach').withArgs(sinon.match({ 413 | port: devicePort, 414 | cwd: '' 415 | })).returns(Promise.resolve('')); 416 | 417 | const adapter = createAdapter(); 418 | return adapter.attach({ port: proxyPort, proxyExecutable: 'test.exe', deviceName: 'nophone' }).then( 419 | done(), 420 | e => assert.fail('Expecting promise to succeed') 421 | ); 422 | }); 423 | 424 | test('if valid proxy data and * deviceName, returns the first device port', done => { 425 | let proxyPort = 1234; 426 | let devicePort = 9999; 427 | let deviceInfo = [ 428 | { 429 | url: 'localhost:' + devicePort, 430 | deviceName: 'iphone1' 431 | }, 432 | { 433 | url: 'localhost:' + (devicePort + 1), 434 | deviceName: 'iphone2' 435 | } 436 | ]; 437 | utilitiesMock.expects('getURL').returns(Promise.resolve(JSON.stringify(deviceInfo))); 438 | 439 | adapterMock.expects('attach').withArgs(sinon.match({ 440 | port: devicePort, 441 | cwd: '' 442 | })).returns(Promise.resolve('')); 443 | 444 | const adapter = createAdapter(); 445 | return adapter.attach({ port: proxyPort, proxyExecutable: 'test.exe', deviceName: '*' }).then( 446 | done(), 447 | e => assert.fail('Expecting promise to succeed') 448 | ); 449 | }); 450 | 451 | test('if valid proxy data and valid deviceName, returns the matching device port', done => { 452 | let proxyPort = 1234; 453 | let devicePort = 9999; 454 | let deviceInfo = [ 455 | { 456 | url: 'localhost:' + devicePort, 457 | deviceName: 'iphone1' 458 | }, 459 | { 460 | url: 'localhost:' + (devicePort + 1), 461 | deviceName: 'iphone2' 462 | } 463 | ]; 464 | utilitiesMock.expects('getURL').returns(Promise.resolve(JSON.stringify(deviceInfo))); 465 | 466 | adapterMock.expects('attach').withArgs(sinon.match({ 467 | port: devicePort + 1, 468 | cwd: '' 469 | })).returns(Promise.resolve('')); 470 | 471 | const adapter = createAdapter(); 472 | return adapter.attach({ port: proxyPort, proxyExecutable: 'test.exe', deviceName: 'IPHonE2' }).then( 473 | done(), 474 | e => assert.fail('Expecting promise to succeed') 475 | ); 476 | }); 477 | 478 | test('passes on sourceMaps argument', done => { 479 | let proxyPort = 1234; 480 | let deviceInfo = []; 481 | utilitiesMock.expects('getURL').returns(Promise.resolve(JSON.stringify(deviceInfo))); 482 | 483 | adapterMock.expects('attach').withArgs(sinon.match({ 484 | port: proxyPort, 485 | cwd: '', 486 | sourceMaps: true 487 | })).returns(Promise.resolve('')); 488 | 489 | const adapter = createAdapter(); 490 | return adapter.attach({ port: proxyPort, proxyExecutable: 'test.exe', sourceMaps: true }).then( 491 | done(), 492 | e => assert.fail('Expecting promise to succeed') 493 | ); 494 | }); 495 | }); 496 | }); 497 | }); 498 | -------------------------------------------------------------------------------- /test/utilities.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 5 | import * as mockery from 'mockery'; 6 | import * as assert from 'assert'; 7 | import { utils } from 'vscode-chrome-debug-core'; 8 | 9 | /** Not mocked - use for type only */ 10 | import * as _Utilities from '../src/utilities'; 11 | 12 | const MODULE_UNDER_TEST = '../src/utilities'; 13 | suite('Utilities', () => { 14 | function getUtilities(): typeof _Utilities { 15 | return require(MODULE_UNDER_TEST); 16 | } 17 | 18 | setup(() => { 19 | mockery.enable({ useCleanCache: true, warnOnReplace: false }); 20 | mockery.registerAllowables([MODULE_UNDER_TEST]); 21 | }); 22 | 23 | teardown(() => { 24 | mockery.deregisterAll(); 25 | mockery.disable(); 26 | }); 27 | 28 | suite('getProxyPath()', () => { 29 | test('if windows and path exists, returns correct path', () => { 30 | mockery.registerMock('vscode-chrome-debug-core', { 31 | utils: { 32 | Platform: { Windows: 0, OSX: 1, Linux: 2 }, 33 | getPlatform: () => utils.Platform.Windows, 34 | existsSync: () => true 35 | } 36 | }); 37 | 38 | mockery.registerMock('path', { 39 | resolve: (a, b) => b 40 | }); 41 | 42 | const _Utilities = getUtilities(); 43 | assert.equal(_Utilities.getProxyPath(), "../../node_modules/vs-libimobile/lib/ios_webkit_debug_proxy.exe"); 44 | }); 45 | 46 | test('if windows and path not found, returns null', () => { 47 | mockery.registerMock('vscode-chrome-debug-core', { 48 | utils: { 49 | Platform: { Windows: 0, OSX: 1, Linux: 2 }, 50 | getPlatform: () => utils.Platform.Windows, 51 | existsSync: () => false 52 | } 53 | }); 54 | 55 | mockery.registerMock('path', { 56 | resolve: (a, b) => b 57 | }); 58 | 59 | const _Utilities = getUtilities(); 60 | assert.equal(_Utilities.getProxyPath(), null); 61 | }); 62 | 63 | test('if osx, returns null', () => { 64 | mockery.registerMock('vscode-chrome-debug-core', { 65 | utils: { 66 | Platform: { Windows: 0, OSX: 1, Linux: 2 }, 67 | getPlatform: () => utils.Platform.OSX 68 | } 69 | }); 70 | mockery.registerMock('path', {}); 71 | 72 | const _Utilities = getUtilities(); 73 | assert.equal(_Utilities.getProxyPath(), null); 74 | }); 75 | 76 | test('if linux, returns null', () => { 77 | mockery.registerMock('vscode-chrome-debug-core', { 78 | utils: { 79 | Platform: { Windows: 0, OSX: 1, Linux: 2 }, 80 | getPlatform: () => utils.Platform.Linux 81 | } 82 | }); 83 | mockery.registerMock('path', {}); 84 | 85 | const _Utilities = getUtilities(); 86 | assert.equal(_Utilities.getProxyPath(), null); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": false, 5 | "removeComments": false, 6 | "target": "es5", 7 | "sourceMap": true, 8 | "outDir": "out" 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "typings/browser" 13 | ] 14 | } -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDependencies": { 3 | "es6-collections": "registry:dt/es6-collections#0.5.1+20160316155526", 4 | "es6-promise": "registry:dt/es6-promise#0.0.0+20160317120654", 5 | "mocha": "registry:dt/mocha#2.2.5+20160317120654", 6 | "mockery": "registry:dt/mockery#1.4.0+20160316155526", 7 | "node": "registry:dt/node#4.0.0+20160412142033", 8 | "sinon": "registry:dt/sinon#1.16.0+20160317120654" 9 | } 10 | } 11 | --------------------------------------------------------------------------------