├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .github
├── semantic.yml
└── workflows
│ ├── ci.yml
│ └── docs-build.yml
├── .gitignore
├── .vscode
└── launch.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── assets
├── .eslintrc.js
├── components
│ ├── app-info.jsx
│ ├── app-info.less
│ ├── index.jsx
│ ├── info.jsx
│ ├── info.less
│ ├── screen.jsx
│ ├── screen.less
│ ├── tree.jsx
│ └── tree.less
├── index.jsx
├── index.less
└── libs
│ ├── bounds.js
│ ├── utils.js
│ └── xpath.js
├── bin
└── app-inspector.js
├── css
├── cayman.css
└── normalize.css
├── docs
├── .vuepress
│ ├── config.js
│ ├── override.styl
│ └── public
│ │ └── assets
│ │ ├── 6d308bd9gy1fg7cnt9hf6j20t70h7782.jpg
│ │ └── 7dfcf2f7gw1f77ev6csw5g20s50iwe81.gif
├── README.md
├── guide
│ ├── get-device-id.md
│ ├── install.md
│ └── quick-start.md
└── zh
│ ├── README.md
│ └── guide
│ ├── get-device-id.md
│ ├── install.md
│ └── quick-start.md
├── index.js
├── issue_template.md
├── lib
├── android.js
├── app-inspector.js
├── common
│ ├── helper.js
│ └── logger.js
├── config.js
├── ios.js
├── middlewares.js
├── render.js
├── router.js
└── server.js
├── package.json
├── public
└── 3rdparty
│ ├── react-dom.min.js
│ └── react.min.js
├── test
├── bin
│ ├── app-inspector.test.js
│ └── utils.js
├── lib
│ └── common
│ │ └── helper.test.js
└── mocha.opts
├── views
└── index.html
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/.*
2 | **/node_modules
3 | **/test
4 | **/logs
5 | **/coverage/
6 | **/assets/
7 | **/dist/
8 | **/docs_dist/
9 | **/public
10 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | root: true,
5 | extends: 'eslint-config-egg',
6 | parserOptions: {
7 | ecmaVersion: 2020,
8 | },
9 | plugins: [],
10 | rules: {
11 | 'valid-jsdoc': 0,
12 | 'no-script-url': 0,
13 | 'no-multi-spaces': 0,
14 | 'default-case': 0,
15 | 'no-case-declarations': 0,
16 | 'one-var-declaration-per-line': 0,
17 | 'no-restricted-syntax': 0,
18 | 'jsdoc/require-param': 0,
19 | 'jsdoc/check-param-names': 0,
20 | 'jsdoc/require-param-description': 0,
21 | 'arrow-parens': 0,
22 | 'prefer-promise-reject-errors': 0,
23 | 'no-control-regex': 0,
24 | 'no-use-before-define': 0,
25 | 'array-callback-return': 0,
26 | 'no-bitwise': 0,
27 | 'no-self-compare': 0,
28 | 'one-var': 0,
29 | 'no-trailing-spaces': [ 'warn', { skipBlankLines: true }],
30 | 'no-return-await': 0,
31 | },
32 | globals: {
33 | window: true,
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/.github/semantic.yml:
--------------------------------------------------------------------------------
1 | titleOnly: true
2 | types:
3 | - feat
4 | - fix
5 | - docs
6 | - dx
7 | - refactor
8 | - perf
9 | - test
10 | - workflow
11 | - build
12 | - ci
13 | - chore
14 | - types
15 | - wip
16 | - release
17 | - deps
18 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | # allows to manually run the job at any time
5 | workflow_dispatch:
6 |
7 | push:
8 | branches:
9 | - '**'
10 |
11 | jobs:
12 | test:
13 | name: 'Run Unit Test: node-16, macos-latest'
14 | timeout-minutes: 10
15 | runs-on: macos-latest
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v3
19 | with:
20 | fetch-depth: 0
21 |
22 | - name: Set node version to 16
23 | uses: actions/setup-node@v3
24 | with:
25 | node-version: '16'
26 |
27 | - name: Install deps
28 | # https://github.com/webpack-contrib/terser-webpack-plugin/issues/66
29 | run: |
30 | npm i terser@3.14.1
31 | npm install
32 |
33 | - name: Run ci
34 | run: |
35 | npm run lint
36 | npm run build
37 | npm run test
38 |
39 | - name: Codecov
40 | uses: codecov/codecov-action@v3.0.0
41 | with:
42 | token: ${{ secrets.CODECOV_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/docs-build.yml:
--------------------------------------------------------------------------------
1 | name: Docs Build
2 |
3 | on:
4 | # allows to manually run the job at any time
5 | workflow_dispatch:
6 |
7 | # run on every push on the master branch
8 | push:
9 | branches:
10 | - master
11 |
12 | jobs:
13 | docs-build:
14 | timeout-minutes: 10
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v3
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: Set node version to 16
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: '16'
27 |
28 | - name: Install deps
29 | run: npm install vuepress macaca-ecosystem -D
30 |
31 | - name: Build docs
32 | run: npm run docs:build
33 |
34 | - name: Deploy to GitHub Pages
35 | if: success()
36 | uses: peaceiris/actions-gh-pages@v3
37 | with:
38 | github_token: ${{ secrets.GITHUB_TOKEN }}
39 | publish_dir: ./docs_dist
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.sw*
3 | *.un~
4 | coverage/
5 | docs_dist/
6 | .nyc_output
7 | .temp
8 | public/dist
9 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Launch",
6 | "type": "node",
7 | "request": "launch",
8 | "program": "${workspaceRoot}/bin/app-inspector",
9 | "stopOnEntry": false,
10 | "args": [ "-u", "41D38B64-877A-4020-A212-67DA5F248235", "--verbose" ],
11 | "cwd": "${workspaceRoot}",
12 | "preLaunchTask": null,
13 | "runtimeExecutable": null,
14 | "runtimeArgs": [
15 | "--nolazy"
16 | ],
17 | "env": {
18 | "NODE_ENV": "development",
19 | "APP_INSPECTOR": "dev"
20 | },
21 | "console": "internalConsole",
22 | "sourceMaps": false,
23 | "outFiles": []
24 | },
25 | {
26 | "name": "Attach",
27 | "type": "node",
28 | "request": "attach",
29 | "port": 5858,
30 | "address": "localhost",
31 | "restart": false,
32 | "sourceMaps": false,
33 | "outFiles": [],
34 | "localRoot": "${workspaceRoot}",
35 | "remoteRoot": null
36 | },
37 | {
38 | "name": "Attach to Process",
39 | "type": "node",
40 | "request": "attach",
41 | "processId": "${command.PickProcess}",
42 | "port": 5858,
43 | "sourceMaps": false,
44 | "outFiles": []
45 | }
46 | ]
47 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Macaca App Inspector
2 |
3 | We love pull requests from everyone.
4 |
5 | ## Link Global To Local
6 |
7 | ```bash
8 | $ cd path/to/app-inspector
9 | $ npm link
10 | # now project app-inspector is linked to your global
11 | $ app-inspector -v
12 | # start dev
13 | $ npm run dev
14 | ```
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT LICENSE
2 |
3 | Copyright (c) 2017 Alibaba Group Holding Limited and other contributors.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # app-inspector
2 |
3 | ---
4 |
5 | [![NPM version][npm-image]][npm-url]
6 | [![Package quality][quality-image]][quality-url]
7 | [![CI][CI-image]][CI-url]
8 | [![Test coverage][coveralls-image]][coveralls-url]
9 | [![node version][node-image]][node-url]
10 | [![npm download][download-image]][download-url]
11 |
12 | [npm-image]: https://img.shields.io/npm/v/app-inspector.svg
13 | [npm-url]: https://npmjs.org/package/app-inspector
14 | [quality-image]: https://packagequality.com/shield/app-inspector.svg
15 | [quality-url]: https://packagequality.com/#?package=app-inspector
16 | [CI-image]: https://github.com/macacajs/app-inspector/actions/workflows/ci.yml/badge.svg
17 | [CI-url]: https://github.com/macacajs/app-inspector/actions/workflows/ci.yml
18 | [coveralls-image]: https://img.shields.io/coveralls/macacajs/app-inspector.svg
19 | [coveralls-url]: https://coveralls.io/r/macacajs/app-inspector?branch=master
20 | [node-image]: https://img.shields.io/badge/node.js-%3E=_8-green.svg
21 | [node-url]: http://nodejs.org/download/
22 | [download-image]: https://img.shields.io/npm/dm/app-inspector.svg
23 | [download-url]: https://npmjs.org/package/app-inspector
24 |
25 | [App-inspector](//macacajs.github.io/app-inspector/) is a mobile UI viewer in browser.
26 |
27 |
28 |
29 | ## Contributors
30 |
31 | |[
xudafeng](https://github.com/xudafeng)
|[
meowtec](https://github.com/meowtec)
|[
paradite](https://github.com/paradite)
|[
zivyangll](https://github.com/zivyangll)
|[
ziczhu](https://github.com/ziczhu)
|[
CodeToSurvive1](https://github.com/CodeToSurvive1)
|
32 | | :---: | :---: | :---: | :---: | :---: | :---: |
33 | [
qichuan](https://github.com/qichuan)
|[
snapre](https://github.com/snapre)
|[
risinek](https://github.com/risinek)
|[
mahalo777](https://github.com/mahalo777)
|[
zhuyali](https://github.com/zhuyali)
34 |
35 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Thu Apr 21 2022 00:04:00 GMT+0800`.
36 |
37 |
38 |
39 | 
40 |
41 | ## Installation
42 |
43 | App-inspector is distibuted through npm. To install it, run the following command line:
44 |
45 | ```bash
46 | $ npm i app-inspector -g
47 | ```
48 |
49 | Note: If you are going to use app-inspector on real iOS device, see [iOS Real Device section](#ios-real-device)
50 |
51 | ## Usage
52 |
53 | ```bash
54 | $ app-inspector -u xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
55 | ```
56 |
57 | ## Home Page
58 |
59 | Visit https://macacajs.github.io/app-inspector/ for more information.
60 |
61 | ### iOS Real Device
62 |
63 | First, find the Development Team ID as shown on image below.
64 |
65 | 
66 |
67 | Run this command where TEAM_ID is your ID from the first step.
68 |
69 | ```bash
70 | $ DEVELOPMENT_TEAM_ID=TEAM_ID npm i app-inspector -g
71 | ```
72 |
73 | ## License
74 |
75 | The MIT License (MIT)
76 |
--------------------------------------------------------------------------------
/assets/.eslintrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | extends: 'eslint-config-airbnb',
5 | parser: '@babel/eslint-parser',
6 | env: {
7 | browser: true,
8 | es6: true,
9 | },
10 | parserOptions: {
11 | ecmaVersion: 6,
12 | sourceType: 'module',
13 | requireConfigFile: false,
14 | ecmaFeatures: {
15 | jsx: true,
16 | },
17 | babelOptions: {
18 | presets: ['@babel/preset-react'],
19 | },
20 | },
21 | plugins: ['babel', 'react'],
22 | rules: {
23 | camelcase: 0,
24 | 'no-restricted-syntax': 0,
25 | 'no-plusplus': 0,
26 | 'no-underscore-dangle': 0,
27 | 'no-useless-escape': 0,
28 | 'no-prototype-builtins': 0,
29 | 'max-len': 0,
30 | 'class-methods-use-this': 0,
31 | 'function-paren-newline': 0,
32 | 'react/no-array-index-key': 0,
33 | 'react/no-danger': 0,
34 | 'react/jsx-no-undef': [2, { allowGlobals: true }],
35 | 'react/no-find-dom-node': 0,
36 | 'react/no-did-mount-set-state': 0,
37 | 'react/jsx-no-target-blank': 0,
38 | 'react/no-did-update-set-state': 0,
39 | 'react/prop-types': 0,
40 | 'react/jsx-filename-extension': 0,
41 | 'react/sort-comp': 0,
42 | 'react/require-default-props': 0,
43 | 'react/no-typos': 0,
44 | 'react/forbid-prop-types': 0,
45 | 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'],
46 | 'react/jsx-one-expression-per-line': 0,
47 | 'react/destructuring-assignment': 0,
48 | 'react/no-unused-prop-types': ['warn'],
49 | 'react/self-closing-comp': 0,
50 | 'react/no-unescaped-entities': 0,
51 | 'react/jsx-props-no-spreading': 0,
52 | 'react/no-unused-state': 0,
53 | 'react/no-deprecated': 0,
54 | 'react/state-in-constructor': 0,
55 | 'react/no-access-state-in-setstate': 0,
56 | 'react/no-unstable-nested-components': 0,
57 | 'react/no-unused-class-component-methods': 0,
58 | 'react/jsx-no-bind': 0,
59 | 'jsx-a11y/no-static-element-interactions': 0,
60 | 'jsx-a11y/no-noninteractive-element-interactions': 0,
61 | 'jsx-a11y/anchor-has-content': 0,
62 | 'jsx-a11y/click-events-have-key-events': 0,
63 | 'jsx-a11y/anchor-is-valid': 0,
64 | 'jsx-a11y/alt-text': 0,
65 | 'jsx-a11y/mouse-events-have-key-events': 0,
66 | 'jsx-a11y/iframe-has-title': 0,
67 | 'jsx-a11y/media-has-caption': 0,
68 | 'jsx-a11y/tabindex-no-positive': 0,
69 | 'jsx-a11y/aria-role': 0,
70 | 'jsx-a11y/no-noninteractive-tabindex': 0,
71 | 'jsx-a11y/label-has-for': 0,
72 | 'jsx-a11y/accessible-emoji': 0,
73 | 'jsx-a11y/label-has-associated-control': 0,
74 | 'jsx-a11y/control-has-associated-label': 0,
75 | 'no-await-in-loop': 0,
76 | 'comma-dangle': [
77 | 'error',
78 | {
79 | arrays: 'always-multiline',
80 | objects: 'always-multiline',
81 | imports: 'always-multiline',
82 | exports: 'always-multiline',
83 | functions: 'ignore',
84 | },
85 | ],
86 | 'no-param-reassign': 0,
87 | 'consistent-return': 0,
88 | 'object-curly-newline': ['error', { consistent: true, minProperties: 6 }],
89 | 'arrow-parens': 0,
90 | 'arrow-body-style': ['error', 'always'],
91 | 'prefer-destructuring': [
92 | 'error',
93 | {
94 | VariableDeclarator: {
95 | array: false,
96 | object: true,
97 | },
98 | AssignmentExpression: {
99 | array: false,
100 | object: false,
101 | },
102 | },
103 | ],
104 | 'import/named': 0,
105 | 'import/no-extraneous-dependencies': 0,
106 | 'no-mixed-operators': 0,
107 | 'no-shadow': 0,
108 | 'no-useless-constructor': 0,
109 | 'no-return-assign': 0,
110 | 'prefer-rest-params': 0,
111 | 'no-restricted-globals': [0, 'location'],
112 | 'prefer-promise-reject-errors': 0,
113 | 'no-bitwise': 0,
114 | 'no-return-await': 0,
115 | 'import/prefer-default-export': 0,
116 | 'func-names': 0,
117 | 'no-use-before-define': 0,
118 | 'global-require': 0,
119 | 'no-else-return': 0,
120 | 'no-lonely-if': 0,
121 | 'guard-for-in': 0,
122 | 'one-var': 0,
123 | 'one-var-declaration-per-line': 0,
124 | 'no-console': 0,
125 | 'no-unused-expressions': 0,
126 | 'no-unused-vars': 0,
127 | 'no-continue': 0,
128 | 'no-eval': 0,
129 | 'default-param-last': 0,
130 | 'default-case': 0,
131 | 'semi-style': 0,
132 | 'import/no-import-module-exports': 0,
133 | 'import/extensions': 0,
134 | },
135 | };
136 |
--------------------------------------------------------------------------------
/assets/components/app-info.jsx:
--------------------------------------------------------------------------------
1 | import pkg from '../../package.json';
2 | import './app-info.less';
3 |
4 | export default () => (
5 |
16 | );
17 |
--------------------------------------------------------------------------------
/assets/components/app-info.less:
--------------------------------------------------------------------------------
1 | .app-info {
2 | color: #666;
3 | font-size: 12px;
4 | border-top: 1px solid #eee;
5 | padding: 5px;
6 | text-align: center;
7 |
8 | a {
9 | text-decoration: none;
10 | color: #666;
11 |
12 | &:hover {
13 | text-decoration: underline;
14 | }
15 | }
16 |
17 | span, strong {
18 | margin: 0 5px;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/assets/components/index.jsx:
--------------------------------------------------------------------------------
1 | import ReactGA from 'react-ga';
2 | import React, { Component } from 'react';
3 | import GitHubButton from 'react-github-button';
4 |
5 | import 'react-github-button/assets/style.css';
6 |
7 | import Tree from './tree';
8 | import Info from './info';
9 | import Screen from './screen';
10 | import AppInfo from './app-info';
11 | import {
12 | getXPath,
13 | getXPathLite
14 | } from '../libs/xpath';
15 | import { getNodePathByXY } from '../libs/bounds';
16 |
17 | const { appData } = window;
18 | const { isIOS, serverStarted, dumpFailed } = appData;
19 |
20 | window.addEventListener('load', () => {
21 | ReactGA.initialize('UA-49226133-2');
22 | ReactGA.pageview(window.location.pathname + window.location.search);
23 | process.env.traceFragment;
24 | });
25 |
26 | class App extends Component {
27 |
28 | constructor() {
29 | super();
30 |
31 | this.state = {
32 | node: null,
33 | tree: null,
34 | xpath_lite: null,
35 | xpath: null,
36 | focusBounds: null,
37 | treeViewPortWidth: null,
38 | timer: 0,
39 | };
40 |
41 | window.addEventListener('resize', () => this.resizeTreeViewport());
42 | }
43 |
44 | componentDidMount() {
45 | if (serverStarted) {
46 | fetch(isIOS ? './ios.json' : './android.json')
47 | .then(res => res.json())
48 | .then(tree => {
49 | this.setState({ tree });
50 | });
51 | } else {
52 | setTimeout(() => location.reload(), 3000);
53 | }
54 | }
55 |
56 | componentWillUnmount() {
57 | clearTimeout(this.timer);
58 | }
59 |
60 | handleTreeSelect(node, nodePath) {
61 | const { tree } = this.state;
62 |
63 | this.setState({
64 | node,
65 | focusBounds: node.bounds,
66 | xpath_lite: getXPathLite(tree, nodePath),
67 | xpath: getXPath(tree, nodePath)
68 | });
69 | this.resizeTreeViewport();
70 | }
71 |
72 | handleMouseEnter(node) {
73 | this.setState({
74 | focusBounds: node.bounds
75 | });
76 | }
77 |
78 | handleMouseLeave(node) {
79 | this.setState({
80 | focusBounds: null
81 | });
82 | }
83 |
84 | handleCanvasClick(x, y) {
85 | const nodePath = getNodePathByXY(this.state.tree, isIOS, x, y);
86 | if (!nodePath) return;
87 | this.refs.tree.focus(nodePath);
88 | this.resizeTreeViewport();
89 | this.updateTreeScrollerStyle();
90 | }
91 |
92 | resizeTreeViewport() {
93 | setTimeout(() => {
94 | this.refs.treeScroller && this.setState({
95 | treeViewPortWidth: this.refs.treeScroller.scrollWidth
96 | });
97 | });
98 | }
99 |
100 | updateTreeScrollerStyle() {
101 | clearTimeout(this.timer);
102 | this.timer = setTimeout(() => {
103 | const scrollTop = document.querySelectorAll('.tree-selected')[0].offsetTop;
104 | const scrollLeft = document.querySelectorAll('.tree-selected .tree-indent')[0].offsetWidth;
105 | this.refs.treeScroller.scrollTo({
106 | left: scrollLeft - 80,
107 | top: scrollTop - 100,
108 | behavior: "smooth",
109 | });
110 | })
111 | }
112 |
113 | renderLoading() {
114 | return dumpFailed ? (
115 |
116 | Get UI Failed, location.reload()} style={{ color: '#1672f3' }}>Retry...
117 |
118 | ) : (
119 | Waiting Device start...
120 | );
121 | }
122 |
123 | render() {
124 | return (
125 |
126 |
137 | {
138 | this.state.tree ? (
139 |
140 |
141 |
147 |
148 |
149 |
157 |
158 | {
159 | this.state.node ? (
160 |
161 |
166 |
167 | ) : null
168 | }
169 |
170 | ) : this.renderLoading()
171 | }
172 |
173 |
174 | );
175 | }
176 | }
177 |
178 | module.exports = App;
179 |
--------------------------------------------------------------------------------
/assets/components/info.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import CopyToClipboard from 'react-copy-to-clipboard';
3 |
4 | import './info.less';
5 |
6 | const blackList = [
7 | 'index',
8 | 'nodes',
9 | 'selected',
10 | 'open',
11 | 'state',
12 | 'nodeId',
13 | 'parentId',
14 | 'rect'
15 | ];
16 |
17 | export default class App extends PureComponent {
18 |
19 | constructor() {
20 | super();
21 | this.state = {
22 | copied: false
23 | };
24 | }
25 |
26 | filter(node) {
27 | const array = Object.keys(node)
28 | .filter(key => blackList.indexOf(key) < 0 && !/^\$/.test(key))
29 | .map(key => ({
30 | key, text: String(node[key])
31 | }));
32 |
33 | array.push({
34 | key: 'xpath_lite',
35 | text: this.props.xpath_lite
36 | }, {
37 | key: 'xpath',
38 | text: this.props.xpath
39 | });
40 |
41 | return array;
42 | }
43 |
44 | onCopy() {
45 | this.setState({
46 | copied: true
47 | });
48 | setTimeout(() => {
49 | this.setState({
50 | copied: false
51 | });
52 | }, 1000)
53 | }
54 |
55 | render() {
56 | const node = this.props.node;
57 | const blackList = [];
58 | return (
59 |
60 | {
61 | this.filter(node).map(item => (
62 | -
63 |
68 |
70 |
{ item.text }
71 |
72 |
73 | ))
74 | }
75 | - copied to clipboard
76 |
77 | );
78 | }
79 |
80 | };
81 |
--------------------------------------------------------------------------------
/assets/components/info.less:
--------------------------------------------------------------------------------
1 | .info {
2 | word-break: break-all;
3 | padding: 0 15px;
4 | margin-top: 23px;
5 | min-width: 300px;
6 |
7 | li {
8 | display: flex;
9 | font-size: 14px;
10 | border-top: 1px solid #eee;
11 | padding: 2px 0;
12 |
13 | > label {
14 | width: 100px;
15 | position: relative;
16 | }
17 |
18 | a {
19 | color: #333;
20 | text-decoration: none;
21 | }
22 | ._xpath:before {
23 | display: block;
24 | content: '?';
25 | position: absolute;
26 | top: 0;
27 | left: 40px;
28 | color: rgb(255, 102, 0);
29 | font-weight: 600;
30 | cursor: pointer;
31 | }
32 |
33 | > div {
34 | flex: 1;
35 | color: #678;
36 | cursor: pointer;
37 | }
38 |
39 | }
40 |
41 | li:last-child {
42 | color: #666;
43 | opacity: 0;
44 | transition: opacity 0.8s ease;
45 | }
46 |
47 | li:nth-last-child(2) {
48 | border-bottom: 1px solid #eee;
49 | }
50 |
51 | .fadeIn {
52 | opacity: 1 !important;
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/assets/components/screen.jsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | PureComponent
3 | } from 'react';
4 |
5 | import './screen.less';
6 |
7 | // TODO: should by device name.
8 | function getIOSDprByScreenWidth(width) {
9 | return width > 1000 ? 3 : 2;
10 | }
11 |
12 | export default class Screen extends PureComponent {
13 |
14 | constructor() {
15 | super();
16 |
17 | this.state = {
18 | width: 0,
19 | height: 0,
20 | frameBounds: null
21 | };
22 | }
23 |
24 | get rate() {
25 | /**
26 | * iOS: bounds-width = 320 but image-width = 640
27 | * Android: bounds-width = 720 and image-width = 720
28 | */
29 | return this.props.isIOS ? getIOSDprByScreenWidth(this.state.width) : 1;
30 | }
31 |
32 | handleImageLoad() {
33 | this.setState({
34 | width: this.refs.image.naturalWidth,
35 | height: this.refs.image.naturalHeight,
36 | }, () => {
37 | console.info('iOS', this.props.isIOS);
38 | console.info('image-width-height', this.state.width, this.state.height);
39 | console.info('dpr', this.rate);
40 | });
41 | this.initCanvas();
42 | }
43 |
44 | displayWidth() {
45 | return 320; // css client width
46 | }
47 |
48 | paintFrame(frameBounds, style) {
49 | console.info('frameBounds', frameBounds);
50 | const rate = this.rate;
51 | const cxt = this.cxt;
52 | cxt.clearRect(0, 0, this.state.width, this.state.height);
53 | if (!frameBounds) return;
54 |
55 | cxt.fillStyle = 'red';
56 | cxt.globalAlpha = 0.5;
57 |
58 | cxt.fillRect.apply(cxt, frameBounds.map(x => x * rate));
59 | }
60 |
61 | handleClick(e) {
62 | const rate = this.rate;
63 | const scale = this.state.width / this.displayWidth();
64 |
65 | this.props.onClick(
66 | (e.clientX - this.canvas.offsetLeft) * scale / rate,
67 | (e.clientY - this.canvas.offsetTop) * scale / rate
68 | );
69 | }
70 |
71 | initCanvas() {
72 | const canvas = this.refs.canvas;
73 | this.cxt = canvas.getContext('2d');
74 | this.canvas = canvas;
75 | }
76 |
77 | shouldComponentUpdate(nextProps, nextState) {
78 | if (nextProps.frame !== this.props.frame && this.cxt) {
79 | this.paintFrame(nextProps.frame);
80 | }
81 | return this.state !== nextState;
82 | }
83 |
84 | render() {
85 | return (
86 |
87 |
99 | );
100 | }
101 | };
102 |
103 | Screen.defaultProps = {
104 | onClick() {},
105 | frame: null,
106 | isIOS: true
107 | };
108 |
--------------------------------------------------------------------------------
/assets/components/screen.less:
--------------------------------------------------------------------------------
1 | .screen {
2 | font-size: 0;
3 |
4 | canvas {
5 | position: absolute;
6 | }
7 |
8 | img, canvas {
9 | width: 320px;
10 | }
11 | }
--------------------------------------------------------------------------------
/assets/components/tree.jsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | PureComponent,
3 | } from 'react';
4 |
5 | import SyntaxHighlighter from 'react-syntax-highlighter';
6 | import { docco } from 'react-syntax-highlighter/styles/hljs';
7 |
8 | import {
9 | className,
10 | pick,
11 | } from '../libs/utils';
12 |
13 | import './tree.less';
14 |
15 | const TreeNodeList = (props) => (
16 |
17 | {
18 | props.nodes.map((node, index) => (
19 | -
20 |
26 |
27 | ))
28 | }
29 |
30 | );
31 |
32 | class TreeNode extends PureComponent {
33 |
34 | updateData(updater) {
35 | this.props.onUpdate(this.props.path, updater);
36 | }
37 |
38 | handleTagClick(e) {
39 | e.stopPropagation();
40 | const data = this.props.data;
41 |
42 | this.updateData({
43 | $open: !data.$open
44 | });
45 | }
46 |
47 | handleSelect() {
48 | const data = this.props.data;
49 |
50 | this.updateData({
51 | $selected: !data.$selected
52 | });
53 | }
54 |
55 | handleMouseEnter() {
56 | this.props.onMouseEnter(this.props.data);
57 | }
58 |
59 | handleMouseLeave() {
60 | this.props.onMouseLeave(this.props.data);
61 | }
62 |
63 | tagName() {
64 | if (this.props.data.class) {
65 | return this.props.data.class.replace('android.widget.', '');
66 | }
67 | return 'unknown_class';
68 | }
69 |
70 | openTag() {
71 | return `<${this.tagName()}>`;
72 | }
73 |
74 | closeTag() {
75 | return `${this.tagName()}>`;
76 | }
77 |
78 | render() {
79 | const props = this.props;
80 | const data = this.props.data;
81 | const childrenCount = data.nodes && data.nodes.length || 0;
82 |
83 | return (
84 |
89 |
94 |
97 |
98 |
99 | { this.openTag(data) }
100 |
101 | {
102 | !data.$open && childrenCount ? '...' : null
103 | }
104 | {
105 | !data.$open || !childrenCount ? (
106 |
107 | { this.closeTag(data) }
108 |
109 | ) : null
110 | }
111 |
112 | {
113 | childrenCount && data.$open ? (
114 | [
115 |
,
123 |
129 |
132 |
133 | { this.closeTag(data) }
134 |
135 |
136 | ]
137 | ) : null
138 | }
139 |
140 | );
141 | }
142 |
143 | }
144 |
145 | export default class Tree extends PureComponent {
146 |
147 | constructor(props) {
148 | super();
149 | this.state = {
150 | data: this.expandShallow(props.initialData, 5),
151 | isTreeView: true,
152 | };
153 | window.tree = this;
154 | }
155 |
156 | expandShallow(data, depth) {
157 | data.$open = true;
158 | const nodes = data.nodes;
159 |
160 | if (nodes && depth > 1) {
161 | for (let i = 0; i < nodes.length; i++) {
162 | this.expandShallow(nodes[i], depth - 1);
163 | }
164 | }
165 |
166 | return data;
167 | }
168 |
169 | handleNodeUpdate(nodePath, updater) {
170 | let root = this.state.data;
171 | let updateResult = { root };
172 |
173 | /**
174 | * if a node will be selected,
175 | * the previous selected node (is exist) must be unselected.
176 | */
177 | if (updater.$selected) {
178 | if (this.selectedPath) {
179 | updateResult = this.updateDate(updateResult.root, this.selectedPath, {
180 | $selected: false
181 | });
182 | }
183 | this.selectedPath = nodePath;
184 | }
185 |
186 | /**
187 | * if a node will expand
188 | * it's all ancestors will expand too.
189 | */
190 | const bubble = updater.$open ? {
191 | $open: true
192 | } : null;
193 | updateResult = this.updateDate(updateResult.root, nodePath, updater, bubble);
194 |
195 | this.setState({
196 | data: updateResult.root
197 | });
198 |
199 | if (updater.$selected) {
200 | this.props.onSelect(updateResult.node, nodePath);
201 | }
202 | }
203 |
204 | focus(nodePath) {
205 | this.handleNodeUpdate(nodePath, {
206 | $open: true,
207 | $selected: true
208 | });
209 | }
210 |
211 | /**
212 | * root is an immutable data. each time it update, should return a diffrent one.
213 | * it will be clone along the nodePath.
214 | */
215 | updateDate(root, nodePath, updater, bubble) {
216 | let len = nodePath.length;
217 | let node = root;
218 | let nodes;
219 | let index;
220 |
221 | let bubbleUpdater;
222 |
223 | if (bubble === true) {
224 | bubbleUpdater = updater;
225 | } else if (bubble) {
226 | bubbleUpdater = pick(updater, bubble);
227 | }
228 |
229 | for (let i = 0; i < len + 1; i++) {
230 | if (i === len) {
231 | node = Object.assign({}, node, updater);
232 | } else {
233 | node = Object.assign({}, node, bubbleUpdater);
234 | }
235 |
236 | if (nodes) {
237 | nodes[index] = node;
238 | } else {
239 | root = node;
240 | }
241 |
242 | if (i < len) {
243 | nodes = node.nodes = node.nodes.concat();
244 | index = nodePath[i];
245 | node = nodes[index];
246 | }
247 | }
248 | return {
249 | root, node
250 | };
251 | }
252 |
253 | onViewButtonClick() {
254 | this.setState({
255 | isTreeView: !this.state.isTreeView
256 | })
257 | }
258 |
259 | render() {
260 | return (
261 |
264 |
{ this.state.isTreeView ? 'View Source' : 'View Tree' }
265 | {
266 | this.state.isTreeView ?
267 |
:
274 |
279 | { JSON.stringify(this.state.data, null, 2) }
280 |
281 | }
282 |
283 | );
284 | }
285 | };
286 |
287 | Tree.defaultProps = {
288 | onSelect() {},
289 | onHover() {},
290 | onNodeMouseEnter() {},
291 | onNodeMouseLeave() {},
292 | width: null
293 | };
294 |
--------------------------------------------------------------------------------
/assets/components/tree.less:
--------------------------------------------------------------------------------
1 | .tree-list {
2 | list-style: none;
3 | margin: 0;
4 | cursor: default;
5 | }
6 |
7 | .view-source {
8 | cursor: pointer;
9 | text-align: left;
10 | margin: 0;
11 | padding-left: 5px;
12 | padding-bottom: 5px;
13 | border-bottom: 1px solid #eee;
14 | font-weight: 600;
15 | }
16 |
17 | .list-view {
18 | pre {
19 | margin: 0;
20 | }
21 | }
22 |
23 | .tree-tag-open,
24 | .tree-tag-close {
25 | height: 16px;
26 | line-height: 16px;
27 | vertical-align: top;
28 | }
29 |
30 | .tree-line {
31 | position: relative;
32 | font-size: 12px;
33 |
34 | &:hover {
35 | background: #eee;
36 | }
37 |
38 | white-space: nowrap;
39 | }
40 |
41 | .tree-indent {
42 | display: inline-block;
43 | height: 16px;
44 | padding-left: 30px;
45 | vertical-align: top;
46 | }
47 |
48 | .tree-trigger {
49 | position: absolute;
50 | visibility: hidden;
51 | display: inline-block;
52 | width: 0;
53 | height: 0;
54 | top: 50%;
55 | margin-top: -6px;
56 | margin-left: -15px;
57 | border: 5px solid transparent;
58 | border-left: 8px solid #ddd;
59 |
60 | &:hover {
61 | border-left-color: #666;
62 | }
63 | }
64 |
65 | .tree-has-child > div > .tree-trigger {
66 | visibility: visible;
67 | }
68 |
69 | .tree-open > div > .tree-trigger {
70 | transform: rotate(90deg);
71 | transform-origin: 3px 5px;
72 | }
73 |
74 | .tree-selected {
75 | > .tree-line {
76 | background: #666!important;
77 | color: #fff;
78 | }
79 |
80 | .tree-list {
81 | background: #f4f4f4;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/assets/index.jsx:
--------------------------------------------------------------------------------
1 | import 'es6-promise';
2 | import 'whatwg-fetch';
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 |
6 | import './index.less';
7 | import App from './components/index';
8 |
9 | ReactDOM.render(, document.getElementById('app'));
10 |
--------------------------------------------------------------------------------
/assets/index.less:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font: 14px/1.5 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;
4 | }
5 |
6 | ul {
7 | list-style: none;
8 | padding: 0;
9 | }
10 |
11 | .fn-left {
12 | float: right;
13 | }
14 |
15 | .fn-right {
16 | float: right;
17 | }
18 |
19 | .container {
20 | display: flex;
21 | flex-direction: column;
22 | position: absolute;
23 | box-sizing: border-box;
24 | top: 0;
25 | left: 0;
26 | right: 0;
27 | bottom: 0;
28 | }
29 |
30 | .main {
31 | display: flex;
32 | flex: 1;
33 | overflow: scroll;
34 | }
35 |
36 | .header {
37 | border-bottom: 1px solid #eee;
38 | padding: 10px;
39 | }
40 |
41 | .header a {
42 | text-decoration: none;
43 | }
44 |
45 | .header img {
46 | width: 50px;
47 | margin: 5px 20px;
48 | vertical-align: bottom;
49 | }
50 |
51 | .header h1 {
52 | display: inline;
53 | text-align: center;
54 | line-height: 50px;
55 | }
56 |
57 | .header .github-btn {
58 | float: right;
59 | margin-top: 15px;
60 | margin-right: 8px;
61 | }
62 |
63 | .loading {
64 | flex: 1;
65 | text-align: center;
66 | font-size: 20px;
67 | color: #aaa;
68 | padding: 100px 0;
69 | }
70 |
71 | .screen {
72 | flex: 0;
73 | padding: 0 20px;
74 |
75 | img {
76 | box-shadow: 0 0 20px rgba(0,0,0,.2);
77 | }
78 | }
79 |
80 | .flex-col {
81 | flex: 1;
82 | overflow: auto;
83 | font-size: 12px;
84 | padding: 20px 0;
85 | border-left: 1px solid #eee;
86 |
87 | &:first-child {
88 | flex: none;
89 | border-left: 0;
90 | }
91 |
92 | }
93 |
94 | h1 {
95 | color: #333;
96 | }
97 |
98 |
--------------------------------------------------------------------------------
/assets/libs/bounds.js:
--------------------------------------------------------------------------------
1 | export function boundsSize(bounds) {
2 | const [
3 | x,
4 | y,
5 | width,
6 | height
7 | ] = bounds;
8 | return width * height;
9 | };
10 |
11 | export function compareBoundsSize(rectA, rectB) {
12 | return boundsSize(rectA) > boundsSize(rectB);
13 | };
14 |
15 | export function isInRect(x, y, bounds) {
16 | const [
17 | _x,
18 | _y,
19 | width,
20 | height
21 | ] = bounds;
22 |
23 | return x >= _x
24 | && x <= _x + width
25 | && y >= _y
26 | && y <= _y + height;
27 | };
28 |
29 | export function getNodePathByXY(tree, isIOS, x, y) {
30 | let bestBounds = null;
31 | let bestPath = null;
32 |
33 | function walk(node, path) {
34 | let bounds = node.bounds;
35 | let inRect = isInRect(x, y, bounds);
36 |
37 | if (inRect) {
38 | if (!bestBounds || compareBoundsSize(bestBounds, bounds)) {
39 | bestBounds = bounds;
40 | bestPath = path;
41 | }
42 |
43 | if (node.nodes) {
44 | node.nodes.forEach((child, index) => {
45 | walk(child, path.concat([index]));
46 | });
47 | }
48 | }
49 | }
50 |
51 | walk(tree, []);
52 |
53 | return bestPath;
54 | };
55 |
--------------------------------------------------------------------------------
/assets/libs/utils.js:
--------------------------------------------------------------------------------
1 | function isObject(obj) {
2 | return Object.prototype.toString.call(obj) === '[object Object]';
3 | }
4 |
5 | function objectClassName(obj) {
6 | return Object.keys(obj).reduce((prev, key) => {
7 | return obj[key] ? (prev + ' ' + key) : prev;
8 | }, '');
9 | }
10 |
11 | function arrayClassName(arr) {
12 | return arr.map(value => {
13 | return isObject(value) ? objectClassName(value) : value;
14 | }).join(' ');
15 | }
16 |
17 | /**
18 | * className for human
19 | * @example
20 | * className('button', 'primary', { disabled: true })
21 | */
22 | export function className(...args) {
23 | return arrayClassName(args);
24 | };
25 |
26 | /**
27 | * clone part of object.
28 | * @example
29 | * pick({ a: 3, b: 4}, { a: true, _c: true })
30 | * -> { a: 3}
31 | */
32 | export function pick(dict, term) {
33 | const newer = {};
34 | for (let key in term) {
35 | if (term[key] && dict.hasOwnProperty(key)) {
36 | newer[key] = dict[key];
37 | }
38 | }
39 | return newer;
40 | };
41 |
--------------------------------------------------------------------------------
/assets/libs/xpath.js:
--------------------------------------------------------------------------------
1 | var arrKeyAttrs = [
2 | 'resource-id', // Android
3 | 'rawIndentifier', // iOS
4 | 'name', // Android & iOS
5 | 'text', // Android
6 | 'value' // iOS
7 | ];
8 |
9 | var mapIdCount = {};
10 | var mapRawIndentifierCount = {};
11 | var mapTextCount = {};
12 | var mapNameCount = {};
13 | var mapValueCount = {};
14 | var isScan = false;
15 |
16 | const androidRootName = 'MacacaAppInspectorRoot';
17 |
18 | function getChildIndex(node, nodes) {
19 | let index = 0;
20 |
21 | for (var i = 0; i < nodes.length; i++) {
22 | var item = nodes[i];
23 |
24 | if (item.class === node.class) {
25 | index++;
26 | }
27 |
28 | if (node === item) {
29 | break;
30 | }
31 | }
32 |
33 | return index;
34 | }
35 |
36 | function scanNode(nodes) {
37 |
38 | if (!isScan) {
39 |
40 | if (!nodes) {
41 | return;
42 | }
43 |
44 | for (let i = 0; i < nodes.length; i++) {
45 | let current = nodes[i];
46 | arrKeyAttrs.forEach(attr => {
47 | let value = current[attr];
48 |
49 | if (value) {
50 | switch (attr) {
51 | case 'resource-id':
52 | mapIdCount[value] = mapIdCount[value] && mapIdCount[value] + 1 || 1;
53 | break;
54 | case 'rawIndentifier':
55 | mapRawIndentifierCount[value] = mapRawIndentifierCount[value] && mapRawIndentifierCount[value] + 1 || 1;
56 | break;
57 | case 'name':
58 | mapNameCount[value] = mapNameCount[value] && mapNameCount[value] + 1 || 1;
59 | break;
60 | case 'text':
61 | mapTextCount[value] = mapTextCount[value] && mapTextCount[value] + 1 || 1;
62 | break;
63 | case 'value':
64 | mapValueCount[value] = mapValueCount[value] && mapValueCount[value] + 1 || 1;
65 | break;
66 | }
67 | }
68 | });
69 | scanNode(current.nodes);
70 | }
71 | }
72 | }
73 |
74 | export function getXPathLite(tree, nodePath) {
75 |
76 | scanNode([tree]);
77 | isScan = true;
78 |
79 | const array = [];
80 | let nodes = [tree];
81 | const paths = [0, ...nodePath];
82 |
83 | let XPath = '';
84 |
85 | for (let i = 0; i < paths.length; i++) {
86 | let current = nodes[paths[i]];
87 | let name = current['name'];
88 | let resourceId = current['resource-id'];
89 | let text = current['text'];
90 | let value = current['value'];
91 | let rawIndentifier = current['rawIndentifier'];
92 |
93 | let index = getChildIndex(current, nodes);
94 |
95 | if (resourceId && mapIdCount[resourceId] === 1 && resourceId.trim()) {
96 | XPath = `/*[@resource-id="${resourceId.trim()}"]`;
97 | } else if (rawIndentifier && mapRawIndentifierCount[rawIndentifier] === 1 && rawIndentifier.trim()) {
98 | XPath = `/*[@name="${rawIndentifier.trim()}"]`;
99 | } else if (name && mapNameCount[name] === 1 && name.trim()) {
100 | XPath = `/*[@name="${name.trim()}"]`;
101 | } else if (text && mapTextCount[text] === 1 && text.trim()) {
102 | XPath = `/*[@text="${text.trim()}"]`;
103 | } else {
104 | if (current.class !== androidRootName) {
105 | XPath = `${XPath}/${current.class}[${index}]`;
106 | }
107 | }
108 | nodes = current.nodes;
109 | }
110 | return `/${XPath}`;
111 | };
112 |
113 | export function getXPath(tree, nodePath) {
114 | const array = [];
115 | let nodes = [tree];
116 | const paths = [0, ...nodePath];
117 |
118 | for (let i = 0; i < paths.length; i++) {
119 | let current = nodes[paths[i]];
120 | let index = getChildIndex(current, nodes);
121 |
122 | const tagName = current.class;
123 |
124 | if (current.class !== androidRootName) {
125 | array.push(`${tagName}[${index}]`);
126 | }
127 | nodes = current.nodes;
128 | }
129 |
130 | return `//${array.join('/')}`;
131 | };
132 |
--------------------------------------------------------------------------------
/bin/app-inspector.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict';
4 |
5 | const co = require('co');
6 | const program = require('commander');
7 | const update = require('npm-update');
8 |
9 | const pkg = require('../package.json');
10 | const _ = require('../lib/common/helper');
11 |
12 | const {
13 | chalk,
14 | } = _;
15 |
16 | const Inspector = require('..');
17 |
18 | const options = {
19 | port: 5678,
20 | verbose: true,
21 | };
22 | const EOL = require('os').EOL;
23 |
24 | program
25 | .option('-p, --port ', 'port to use (5678 default)')
26 | .option('-u, --udid ', 'udid of device')
27 | .option('-s, --silent', 'start without opening browser')
28 | .option('--verbose', 'show more debugging information')
29 | .option('-v, --versions', 'output version infomation')
30 | .usage('');
31 |
32 | program.parse(process.argv);
33 |
34 | const printInfo = function(lines) {
35 | let maxLength = 0;
36 | lines.forEach(line => {
37 | maxLength = line.length > maxLength ? line.length : maxLength;
38 | });
39 |
40 | const res = [ new Array(maxLength + 7).join('*') ];
41 |
42 | lines.forEach(line => {
43 | res.push(`* ${line + new Array(maxLength - line.length + 1).join(' ')} *`);
44 | });
45 |
46 | res.push(new Array(maxLength + 7).join('*'));
47 | console.log(chalk.white(`${EOL}${res.join(EOL)}${EOL}`));
48 | };
49 |
50 | // eslint-disable-next-line handle-callback-err
51 | function init(error, data) {
52 | if (data && data.version && pkg.version !== data.version) {
53 | printInfo([ `version ${pkg.version} is outdate`, `run: npm i -g ${pkg.name}@${data.version}` ]);
54 | }
55 |
56 | if (program.versions) {
57 | console.info(`${EOL} ${chalk.grey(pkg.version)} ${EOL}`);
58 | process.exit(0);
59 | }
60 |
61 | if (!program.udid) {
62 | program.help();
63 | process.exit(0);
64 | }
65 |
66 | _.merge(options, _.getConfig(program));
67 |
68 | co(Inspector, options).catch(e => {
69 | console.log(e);
70 | });
71 | }
72 |
73 | co(update, {
74 | pkg,
75 | callback: init,
76 | });
77 |
--------------------------------------------------------------------------------
/css/cayman.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box; }
3 |
4 | body {
5 | padding: 0;
6 | margin: 0;
7 | /* http://zenozeng.github.io/fonts.css/ */
8 | font-family: "Open Sans", "Helvetica Neue", Helvetica, "Nimbus Sans L", Arial, "Liberation Sans", "PingFang SC", "Hiragino Sans GB", "Source Han Sans CN", "Source Han Sans SC", "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti", SimHei, "WenQuanYi Zen Hei Sharp", sans-serif;
9 | font-size: 16px;
10 | line-height: 1.5;
11 | color: #606c71; }
12 |
13 | a {
14 | color: #1ea8e8;
15 | text-decoration: none; }
16 | a:hover {
17 | text-decoration: underline; }
18 |
19 | .btn {
20 | display: inline-block;
21 | margin-bottom: 1rem;
22 | color: rgba(255, 255, 255, 0.7);
23 | background-color: rgba(255, 255, 255, 0.08);
24 | border-color: rgba(255, 255, 255, 0.2);
25 | border-style: solid;
26 | border-width: 1px;
27 | border-radius: 0.3rem;
28 | transition: color 0.2s, background-color 0.2s, border-color 0.2s; }
29 | .btn:hover {
30 | color: rgba(255, 255, 255, 0.9);
31 | text-decoration: none;
32 | background-color: rgba(255, 255, 255, 0.2);
33 | border-color: rgba(255, 255, 255, 0.5); }
34 | .btn + .btn {
35 | margin-left: 1rem; }
36 | @media screen and (min-width: 64em) {
37 | .btn {
38 | padding: 0.75rem 1rem; } }
39 | @media screen and (min-width: 42em) and (max-width: 64em) {
40 | .btn {
41 | padding: 0.6rem 0.9rem;
42 | font-size: 0.9rem; } }
43 | @media screen and (max-width: 42em) {
44 | .btn {
45 | display: block;
46 | width: 100%;
47 | padding: 0.75rem;
48 | font-size: 0.9rem; }
49 | .btn + .btn {
50 | margin-top: 1rem;
51 | margin-left: 0; } }
52 |
53 | .page-header {
54 | color: #fff;
55 | text-align: center;
56 | background-color: #28ded6;
57 | background-image: linear-gradient(120deg, #1ea8e8, #1ed1da);}
58 |
59 | @media screen and (min-width: 64em) {
60 | .page-header {
61 | padding: 5rem 6rem; } }
62 | @media screen and (min-width: 42em) and (max-width: 64em) {
63 | .page-header {
64 | padding: 3rem 4rem; } }
65 | @media screen and (max-width: 42em) {
66 | .page-header {
67 | padding: 2rem 1rem; } }
68 |
69 | .project-name {
70 | margin-top: 0;
71 | margin-bottom: 0.1rem; }
72 | @media screen and (min-width: 64em) {
73 | .project-name {
74 | font-size: 3.25rem; } }
75 | @media screen and (min-width: 42em) and (max-width: 64em) {
76 | .project-name {
77 | font-size: 2.25rem; } }
78 | @media screen and (max-width: 42em) {
79 | .project-name {
80 | font-size: 1.75rem; } }
81 |
82 | .project-tagline {
83 | margin-bottom: 2rem;
84 | font-weight: normal;
85 | opacity: 0.9; }
86 | @media screen and (min-width: 64em) {
87 | .project-tagline {
88 | font-size: 1.25rem; } }
89 | @media screen and (min-width: 42em) and (max-width: 64em) {
90 | .project-tagline {
91 | font-size: 1.15rem; } }
92 | @media screen and (max-width: 42em) {
93 | .project-tagline {
94 | font-size: 1rem; } }
95 |
96 | .main-content {
97 | word-wrap: break-word; }
98 | .main-content :first-child {
99 | margin-top: 0; }
100 | @media screen and (min-width: 64em) {
101 | .main-content {
102 | max-width: 64rem;
103 | padding: 2rem 6rem;
104 | margin: 0 auto;
105 | font-size: 1.1rem; } }
106 | @media screen and (min-width: 42em) and (max-width: 64em) {
107 | .main-content {
108 | padding: 2rem 4rem;
109 | font-size: 1.1rem; } }
110 | @media screen and (max-width: 42em) {
111 | .main-content {
112 | padding: 2rem 1rem;
113 | font-size: 1rem; } }
114 | .main-content img {
115 | max-width: 100%; }
116 | .main-content h1,
117 | .main-content h2,
118 | .main-content h3,
119 | .main-content h4,
120 | .main-content h5,
121 | .main-content h6 {
122 | margin-top: 2rem;
123 | margin-bottom: 1rem;
124 | font-weight: normal;
125 | color: #59bcc1; }
126 | .main-content p {
127 | margin-bottom: 1em; }
128 | .main-content code {
129 | padding: 2px 4px;
130 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
131 | font-size: 0.9rem;
132 | color: #567482;
133 | background-color: #f3f6fa;
134 | border-radius: 0.3rem; }
135 | .main-content pre {
136 | padding: 0.8rem;
137 | margin-top: 0;
138 | margin-bottom: 1rem;
139 | font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace;
140 | color: #567482;
141 | word-wrap: normal;
142 | background-color: #f3f6fa;
143 | border: solid 1px #dce6f0;
144 | border-radius: 0.3rem; }
145 | .main-content pre > code {
146 | padding: 0;
147 | margin: 0;
148 | font-size: 0.9rem;
149 | color: #567482;
150 | word-break: normal;
151 | white-space: pre;
152 | background: transparent;
153 | border: 0; }
154 | .main-content .highlight {
155 | margin-bottom: 1rem; }
156 | .main-content .highlight pre {
157 | margin-bottom: 0;
158 | word-break: normal; }
159 | .main-content .highlight pre,
160 | .main-content pre {
161 | padding: 0.8rem;
162 | overflow: auto;
163 | font-size: 0.9rem;
164 | line-height: 1.45;
165 | border-radius: 0.3rem;
166 | -webkit-overflow-scrolling: touch; }
167 | .main-content pre code,
168 | .main-content pre tt {
169 | display: inline;
170 | max-width: initial;
171 | padding: 0;
172 | margin: 0;
173 | overflow: initial;
174 | line-height: inherit;
175 | word-wrap: normal;
176 | background-color: transparent;
177 | border: 0; }
178 | .main-content pre code:before, .main-content pre code:after,
179 | .main-content pre tt:before,
180 | .main-content pre tt:after {
181 | content: normal; }
182 | .main-content ul,
183 | .main-content ol {
184 | margin-top: 0; }
185 | .main-content blockquote {
186 | padding: 0 1rem;
187 | margin-left: 0;
188 | color: #819198;
189 | border-left: 0.3rem solid #dce6f0; }
190 | .main-content blockquote > :first-child {
191 | margin-top: 0; }
192 | .main-content blockquote > :last-child {
193 | margin-bottom: 0; }
194 | .main-content table {
195 | display: block;
196 | width: 100%;
197 | overflow: auto;
198 | word-break: normal;
199 | word-break: keep-all;
200 | -webkit-overflow-scrolling: touch; }
201 | .main-content table th {
202 | font-weight: bold; }
203 | .main-content table th,
204 | .main-content table td {
205 | padding: 0.5rem 1rem;
206 | border: 1px solid #e9ebec; }
207 | .main-content dl {
208 | padding: 0; }
209 | .main-content dl dt {
210 | padding: 0;
211 | margin-top: 1rem;
212 | font-size: 1rem;
213 | font-weight: bold; }
214 | .main-content dl dd {
215 | padding: 0;
216 | margin-bottom: 1rem; }
217 | .main-content hr {
218 | height: 2px;
219 | padding: 0;
220 | margin: 1rem 0;
221 | background-color: #eff0f1;
222 | border: 0; }
223 |
224 | .site-footer {
225 | text-align: center;
226 | padding-top: 2rem;
227 | padding-bottom: 1rem;
228 | margin-top: 2rem;
229 | border-top: solid 1px #eff0f1; }
230 | @media screen and (min-width: 64em) {
231 | .site-footer {
232 | font-size: 1rem; } }
233 | @media screen and (min-width: 42em) and (max-width: 64em) {
234 | .site-footer {
235 | font-size: 1rem; } }
236 | @media screen and (max-width: 42em) {
237 | .site-footer {
238 | font-size: 0.9rem; } }
239 |
240 | .site-footer-owner {
241 | display: block;
242 | font-weight: bold; }
243 |
244 | .site-footer-credits {
245 | color: #819198; }
246 |
--------------------------------------------------------------------------------
/css/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 |
3 | /**
4 | * 1. Set default font family to sans-serif.
5 | * 2. Prevent iOS text size adjust after orientation change, without disabling
6 | * user zoom.
7 | */
8 |
9 | html {
10 | font-family: sans-serif; /* 1 */
11 | -ms-text-size-adjust: 100%; /* 2 */
12 | -webkit-text-size-adjust: 100%; /* 2 */
13 | }
14 |
15 | /**
16 | * Remove default margin.
17 | */
18 |
19 | body {
20 | margin: 0;
21 | }
22 |
23 | /* HTML5 display definitions
24 | ========================================================================== */
25 |
26 | /**
27 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
29 | * and Firefox.
30 | * Correct `block` display not defined for `main` in IE 11.
31 | */
32 |
33 | article,
34 | aside,
35 | details,
36 | figcaption,
37 | figure,
38 | footer,
39 | header,
40 | hgroup,
41 | main,
42 | menu,
43 | nav,
44 | section,
45 | summary {
46 | display: block;
47 | }
48 |
49 | /**
50 | * 1. Correct `inline-block` display not defined in IE 8/9.
51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
52 | */
53 |
54 | audio,
55 | canvas,
56 | progress,
57 | video {
58 | display: inline-block; /* 1 */
59 | vertical-align: baseline; /* 2 */
60 | }
61 |
62 | /**
63 | * Prevent modern browsers from displaying `audio` without controls.
64 | * Remove excess height in iOS 5 devices.
65 | */
66 |
67 | audio:not([controls]) {
68 | display: none;
69 | height: 0;
70 | }
71 |
72 | /**
73 | * Address `[hidden]` styling not present in IE 8/9/10.
74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
75 | */
76 |
77 | [hidden],
78 | template {
79 | display: none;
80 | }
81 |
82 | /* Links
83 | ========================================================================== */
84 |
85 | /**
86 | * Remove the gray background color from active links in IE 10.
87 | */
88 |
89 | a {
90 | background-color: transparent;
91 | }
92 |
93 | /**
94 | * Improve readability when focused and also mouse hovered in all browsers.
95 | */
96 |
97 | a:active,
98 | a:hover {
99 | outline: 0;
100 | }
101 |
102 | /* Text-level semantics
103 | ========================================================================== */
104 |
105 | /**
106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
107 | */
108 |
109 | abbr[title] {
110 | border-bottom: 1px dotted;
111 | }
112 |
113 | /**
114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
115 | */
116 |
117 | b,
118 | strong {
119 | font-weight: bold;
120 | }
121 |
122 | /**
123 | * Address styling not present in Safari and Chrome.
124 | */
125 |
126 | dfn {
127 | font-style: italic;
128 | }
129 |
130 | /**
131 | * Address variable `h1` font-size and margin within `section` and `article`
132 | * contexts in Firefox 4+, Safari, and Chrome.
133 | */
134 |
135 | h1 {
136 | font-size: 2em;
137 | margin: 0.67em 0;
138 | }
139 |
140 | /**
141 | * Address styling not present in IE 8/9.
142 | */
143 |
144 | mark {
145 | background: #ff0;
146 | color: #000;
147 | }
148 |
149 | /**
150 | * Address inconsistent and variable font size in all browsers.
151 | */
152 |
153 | small {
154 | font-size: 80%;
155 | }
156 |
157 | /**
158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
159 | */
160 |
161 | sub,
162 | sup {
163 | font-size: 75%;
164 | line-height: 0;
165 | position: relative;
166 | vertical-align: baseline;
167 | }
168 |
169 | sup {
170 | top: -0.5em;
171 | }
172 |
173 | sub {
174 | bottom: -0.25em;
175 | }
176 |
177 | /* Embedded content
178 | ========================================================================== */
179 |
180 | /**
181 | * Remove border when inside `a` element in IE 8/9/10.
182 | */
183 |
184 | img {
185 | border: 0;
186 | }
187 |
188 | /**
189 | * Correct overflow not hidden in IE 9/10/11.
190 | */
191 |
192 | svg:not(:root) {
193 | overflow: hidden;
194 | }
195 |
196 | /* Grouping content
197 | ========================================================================== */
198 |
199 | /**
200 | * Address margin not present in IE 8/9 and Safari.
201 | */
202 |
203 | figure {
204 | margin: 1em 40px;
205 | }
206 |
207 | /**
208 | * Address differences between Firefox and other browsers.
209 | */
210 |
211 | hr {
212 | box-sizing: content-box;
213 | height: 0;
214 | }
215 |
216 | /**
217 | * Contain overflow in all browsers.
218 | */
219 |
220 | pre {
221 | overflow: auto;
222 | }
223 |
224 | /**
225 | * Address odd `em`-unit font size rendering in all browsers.
226 | */
227 |
228 | code,
229 | kbd,
230 | pre,
231 | samp {
232 | font-family: monospace, monospace;
233 | font-size: 1em;
234 | }
235 |
236 | /* Forms
237 | ========================================================================== */
238 |
239 | /**
240 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
241 | * styling of `select`, unless a `border` property is set.
242 | */
243 |
244 | /**
245 | * 1. Correct color not being inherited.
246 | * Known issue: affects color of disabled elements.
247 | * 2. Correct font properties not being inherited.
248 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
249 | */
250 |
251 | button,
252 | input,
253 | optgroup,
254 | select,
255 | textarea {
256 | color: inherit; /* 1 */
257 | font: inherit; /* 2 */
258 | margin: 0; /* 3 */
259 | }
260 |
261 | /**
262 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
263 | */
264 |
265 | button {
266 | overflow: visible;
267 | }
268 |
269 | /**
270 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
271 | * All other form control elements do not inherit `text-transform` values.
272 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
273 | * Correct `select` style inheritance in Firefox.
274 | */
275 |
276 | button,
277 | select {
278 | text-transform: none;
279 | }
280 |
281 | /**
282 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
283 | * and `video` controls.
284 | * 2. Correct inability to style clickable `input` types in iOS.
285 | * 3. Improve usability and consistency of cursor style between image-type
286 | * `input` and others.
287 | */
288 |
289 | button,
290 | html input[type="button"], /* 1 */
291 | input[type="reset"],
292 | input[type="submit"] {
293 | -webkit-appearance: button; /* 2 */
294 | cursor: pointer; /* 3 */
295 | }
296 |
297 | /**
298 | * Re-set default cursor for disabled elements.
299 | */
300 |
301 | button[disabled],
302 | html input[disabled] {
303 | cursor: default;
304 | }
305 |
306 | /**
307 | * Remove inner padding and border in Firefox 4+.
308 | */
309 |
310 | button::-moz-focus-inner,
311 | input::-moz-focus-inner {
312 | border: 0;
313 | padding: 0;
314 | }
315 |
316 | /**
317 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
318 | * the UA stylesheet.
319 | */
320 |
321 | input {
322 | line-height: normal;
323 | }
324 |
325 | /**
326 | * It's recommended that you don't attempt to style these elements.
327 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
328 | *
329 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
330 | * 2. Remove excess padding in IE 8/9/10.
331 | */
332 |
333 | input[type="checkbox"],
334 | input[type="radio"] {
335 | box-sizing: border-box; /* 1 */
336 | padding: 0; /* 2 */
337 | }
338 |
339 | /**
340 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
341 | * `font-size` values of the `input`, it causes the cursor style of the
342 | * decrement button to change from `default` to `text`.
343 | */
344 |
345 | input[type="number"]::-webkit-inner-spin-button,
346 | input[type="number"]::-webkit-outer-spin-button {
347 | height: auto;
348 | }
349 |
350 | /**
351 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
352 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
353 | * (include `-moz` to future-proof).
354 | */
355 |
356 | input[type="search"] {
357 | -webkit-appearance: textfield; /* 1 */ /* 2 */
358 | box-sizing: content-box;
359 | }
360 |
361 | /**
362 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
363 | * Safari (but not Chrome) clips the cancel button when the search input has
364 | * padding (and `textfield` appearance).
365 | */
366 |
367 | input[type="search"]::-webkit-search-cancel-button,
368 | input[type="search"]::-webkit-search-decoration {
369 | -webkit-appearance: none;
370 | }
371 |
372 | /**
373 | * Define consistent border, margin, and padding.
374 | */
375 |
376 | fieldset {
377 | border: 1px solid #c0c0c0;
378 | margin: 0 2px;
379 | padding: 0.35em 0.625em 0.75em;
380 | }
381 |
382 | /**
383 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
384 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
385 | */
386 |
387 | legend {
388 | border: 0; /* 1 */
389 | padding: 0; /* 2 */
390 | }
391 |
392 | /**
393 | * Remove default vertical scrollbar in IE 8/9/10/11.
394 | */
395 |
396 | textarea {
397 | overflow: auto;
398 | }
399 |
400 | /**
401 | * Don't inherit the `font-weight` (applied by a rule above).
402 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
403 | */
404 |
405 | optgroup {
406 | font-weight: bold;
407 | }
408 |
409 | /* Tables
410 | ========================================================================== */
411 |
412 | /**
413 | * Remove most spacing between table cells.
414 | */
415 |
416 | table {
417 | border-collapse: collapse;
418 | border-spacing: 0;
419 | }
420 |
421 | td,
422 | th {
423 | padding: 0;
424 | }
425 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const macacaEcosystem = require('macaca-ecosystem');
4 | const traceFragment = require('macaca-ecosystem/lib/trace-fragment');
5 |
6 | const name = 'app-inspector';
7 |
8 | module.exports = {
9 | dest: 'docs_dist',
10 | base: `/${name}/`,
11 |
12 | locales: {
13 | '/': {
14 | lang: 'en-US',
15 | title: 'App Inspector',
16 | description: 'Mobile UI viewer in browser, view the UI in a tree view, and generate XPath automatically',
17 | },
18 | '/zh/': {
19 | lang: 'zh-CN',
20 | title: 'App Inspector',
21 | description: '浏览器端的移动设备 UI 查看器,使用树状态结构查看 UI 布局,自动生成 XPath',
22 | },
23 | },
24 | head: [
25 | ['script', {
26 | async: true,
27 | src: 'https://www.googletagmanager.com/gtag/js?id=UA-49226133-2',
28 | }, ''],
29 | ['script', {}, `
30 | window.dataLayer = window.dataLayer || [];
31 | function gtag(){dataLayer.push(arguments);}
32 | gtag('js', new Date());
33 | gtag('config', 'UA-49226133-2');
34 | `],
35 | ['script', {}, traceFragment],
36 | ],
37 | serviceWorker: true,
38 | themeConfig: {
39 | repo: `macacajs/${name}`,
40 | editLinks: true,
41 | docsDir: 'docs',
42 | locales: {
43 | '/': {
44 | label: 'English',
45 | selectText: 'Languages',
46 | editLinkText: 'Edit this page on GitHub',
47 | lastUpdated: 'Last Updated',
48 | serviceWorker: {
49 | updatePopup: {
50 | message: 'New content is available.',
51 | buttonText: 'Refresh',
52 | },
53 | },
54 | nav: [
55 | {
56 | text: 'Guide',
57 | link: '/guide/install.html'
58 | },
59 | macacaEcosystem.en,
60 | ],
61 | sidebar: {
62 | '/guide/': genSidebarConfig('Guide')
63 | }
64 | },
65 | '/zh/': {
66 | label: '简体中文',
67 | selectText: '选择语言',
68 | editLinkText: '在 GitHub 上编辑此页',
69 | lastUpdated: '上次更新',
70 | serviceWorker: {
71 | updatePopup: {
72 | message: '发现新内容可用',
73 | buttonText: '刷新',
74 | },
75 | },
76 | nav: [
77 | {
78 | text: '指南',
79 | link: '/zh/guide/install.html'
80 | },
81 | macacaEcosystem.zh,
82 | ],
83 | sidebar: {
84 | '/zh/guide/': genSidebarConfig('指南')
85 | }
86 | },
87 | },
88 | },
89 | };
90 |
91 | function genSidebarConfig(title) {
92 | return [
93 | {
94 | title,
95 | collapsable: false,
96 | children: [
97 | 'install',
98 | 'quick-start',
99 | 'get-device-id'
100 | ],
101 | },
102 | ];
103 | }
104 |
--------------------------------------------------------------------------------
/docs/.vuepress/override.styl:
--------------------------------------------------------------------------------
1 | $textColor = #2c3e50
2 | $borderColor = #eaecef
3 | $accentColor = #ee6723
4 | $codeBgColor = #282c34
--------------------------------------------------------------------------------
/docs/.vuepress/public/assets/6d308bd9gy1fg7cnt9hf6j20t70h7782.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macacajs/app-inspector/bb0df518f903c6c17a6d5fe3473b4f90489aa03f/docs/.vuepress/public/assets/6d308bd9gy1fg7cnt9hf6j20t70h7782.jpg
--------------------------------------------------------------------------------
/docs/.vuepress/public/assets/7dfcf2f7gw1f77ev6csw5g20s50iwe81.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macacajs/app-inspector/bb0df518f903c6c17a6d5fe3473b4f90489aa03f/docs/.vuepress/public/assets/7dfcf2f7gw1f77ev6csw5g20s50iwe81.gif
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | home: true
4 | heroImage: https://macacajs.github.io/logo/macaca.svg
5 | actionText: Try it Out →
6 | actionLink: /guide/install.html
7 | features:
8 | - title: Cross Platform
9 | details: Support iOS and Android platforms
10 | footer: MIT Licensed | Copyright © 2015-present Macaca
11 |
12 | ---
13 |
14 | ## Fast start
15 |
16 | ```bash
17 | # install
18 | $ npm i app-inspector -g
19 | ```
20 |
--------------------------------------------------------------------------------
/docs/guide/get-device-id.md:
--------------------------------------------------------------------------------
1 | # Get the Device ID
2 |
3 | ## iOS
4 |
5 | ### From command line
6 |
7 | ```bash
8 | $ xcrun simctl list
9 | ```
10 |
11 | The command above will list all your iOS simulator devices infomation. Your can find the UDID like XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.
12 |
13 | ### From Xcode
14 |
15 | Open your simulator, choose **Hardware - devices - manage devices**. You will find the identifier in device information.
16 |
17 | ## Android
18 |
19 | ### From command line
20 |
21 | Launch your device firstly, then use adb to list all your devices.
22 |
23 | ```bash
24 | $ adb devices
25 |
26 | 123ABCDEFG device
27 | 192.168.0.100:5555 device
28 | ```
29 |
30 | ## iOS Real Device
31 |
32 | ```bash
33 | $ DEVELOPMENT_TEAM_ID=TEAM_ID npm i app-inspector -g
34 | ```
35 |
36 | 
--------------------------------------------------------------------------------
/docs/guide/install.md:
--------------------------------------------------------------------------------
1 | # Install
2 |
3 | ## Requirements
4 |
5 | To install app-inspector, [Node.js](https://nodejs.org) environment is required.
6 |
7 | [macaca-cli](https://macacajs.github.io/guide/environment-setup.html) is recommended to install.
8 |
9 | ```base
10 | $ npm install macaca-cli -g
11 | ```
12 |
13 | Your environment needs to be setup for the particular mobile platforms that you want to view.
14 |
15 | Refer to [Macaca Environment Setup doc](https://macacajs.github.io/guide/environment-setup.html) to install Android SDK for Android, Xcode for iOS.
16 |
17 | Verify the environment with macaca-cli.
18 |
19 | ```bash
20 | $ macaca doctor
21 | ```
22 |
23 | If you saw some green log information, it means your platform environment is ready. Then you can install app-inspector and use it.
24 |
25 | ## Installation
26 |
27 | ```base
28 | $ npm install app-inspector -g
29 | ```
30 |
31 |
--------------------------------------------------------------------------------
/docs/guide/quick-start.md:
--------------------------------------------------------------------------------
1 | # Quick Start
2 |
3 | ## Launch from cli
4 |
5 | ```bash
6 | $ app-inspector -u YOUR-DEVICE-ID
7 | ```
8 |
9 | See Get the Device ID to lean how to get the device ID.
10 |
11 | ## Open Interface
12 |
13 | You will see the log information like below:
14 |
15 | > inspector start at: http://192.168.10.100:5678
16 |
17 | Then open the link http://192.168.10.100:5678 in your browser.
18 |
19 | 
--------------------------------------------------------------------------------
/docs/zh/README.md:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | home: true
4 | heroImage: https://macacajs.github.io/logo/macaca.svg
5 | actionText: 快速上手
6 | actionLink: /zh/guide/install.html
7 | features:
8 | - title: 多端支持
9 | details: 支持iOS 以及安卓平台
10 | footer: MIT Licensed | Copyright © 2015-present Macaca
11 |
12 | ---
13 |
14 | ## 准备起航
15 |
16 | ```bash
17 | # 安装
18 | $ npm i app-inspector -g
19 | ```
20 |
--------------------------------------------------------------------------------
/docs/zh/guide/get-device-id.md:
--------------------------------------------------------------------------------
1 | # 获取设备 ID
2 |
3 | ## iOS
4 |
5 | ### 命令行方式
6 |
7 | ```bash
8 | $ xcrun simctl list
9 | ```
10 |
11 | 这行命令会列出你的所以模拟器信息,里面有类似 XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 的代码,就是模拟器 UDID。
12 |
13 | ### 从 Xcode 获取
14 |
15 | 打开模拟器,从菜单中打开 **Hardware - devices - manage devices**。 然后你会看到模拟器信息界面,里面有个 identifier,就是 UDID。
16 |
17 | ## Android
18 |
19 | ### 从命令行
20 |
21 | 先启动你的设备,然后使用 adb 命令查看设备信息:
22 |
23 | ```bash
24 | $ adb devices
25 |
26 | 123ABCDEFG device
27 | 192.168.0.100:5555 device
28 | ```
29 |
30 | ## iOS 真机问题
31 |
32 | ```bash
33 | $ DEVELOPMENT_TEAM_ID=TEAM_ID npm i app-inspector -g
34 | ```
35 |
36 | 
37 |
--------------------------------------------------------------------------------
/docs/zh/guide/install.md:
--------------------------------------------------------------------------------
1 | # 安装
2 |
3 | ## 环境需要
4 |
5 | 要安装 app-inspector, 你需要首先安装 [Node.js](https://nodejs.org)。 国内用户可以安装 [cnpm](https://npm.taobao.org/) 加快 NPM 模块安装速度。
6 |
7 | 另外,推荐安装 [macaca-cli](https://macacajs.github.io/zh/guide/environment-setup.html).
8 |
9 | ```base
10 | $ npm install macaca-cli -g
11 | ```
12 |
13 | 你需要准备好你需要进行查看的移动平台的环境。
14 |
15 | 请参考[Macaca 环境配置文档](https://macacajs.github.io/zh/guide/environment-setup.html)。Android 请安装 Android SDK,iOS 安装 Xcode。
16 |
17 | 然后使用 macaca 命令行工具检测环境是否准备好。
18 |
19 | ```bash
20 | $ macaca doctor
21 | ```
22 |
23 | 如果你看到一堆绿色的文字输出了,说明你的这个环境是 OK 的。然后你就可以安装使用 app-inspector。
24 |
25 | ## 安装
26 |
27 | ```base
28 | $ npm install app-inspector -g
29 | ```
30 |
--------------------------------------------------------------------------------
/docs/zh/guide/quick-start.md:
--------------------------------------------------------------------------------
1 | # 快速开始
2 |
3 | ## 从命令行启动
4 |
5 | ```bash
6 | $ app-inspector -u YOUR-DEVICE-ID
7 | ```
8 |
9 | 关于如何获取设备 ID,请查看 获取设备 ID 部分。
10 |
11 | ## 打开界面
12 |
13 | 你的命令行将输出如下的文字:
14 |
15 | > inspector start at: http://192.168.10.100:5678
16 |
17 | 然后在浏览器里面打开输出的链接:http://192.168.10.100:5678。推荐用 Chrome 浏览器。
18 |
19 | 
20 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = require('./lib/app-inspector');
4 |
--------------------------------------------------------------------------------
/issue_template.md:
--------------------------------------------------------------------------------
1 | Environment check:
2 |
3 | ```bash
4 | $ npm i macaca-cli -g && macaca doctor
5 | ```
6 |
7 | ---
8 |
9 | - app inspector version / 版本号:
10 |
11 |
12 | - os version / 系统:
13 |
14 |
15 | - device information / 设备版本:
16 |
17 |
18 | - terminal log / 终端输出信息:
19 |
20 | > try `app-inspector -u xxxx --verbose` for more log detail.
21 |
22 |
--------------------------------------------------------------------------------
/lib/android.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const ADB = require('macaca-adb');
6 | const xml2map = require('xml2map2');
7 | const UIAutomatorWD = require('uiautomatorwd');
8 |
9 | const _ = require('./common/helper');
10 | const logger = require('./common/logger');
11 |
12 | let adb;
13 | let uiautomator;
14 |
15 | exports.dumpXMLAndScreenShot = function* () {
16 | yield adb.shell(`touch ${ADB.ANDROID_TMP_DIR}/macaca-dump.xml`);
17 | const result = yield uiautomator.sendCommand('/wd/hub/session/:sessionId/source', 'get', null);
18 | let xml = result.value;
19 | xml = xml.replace(/content-desc=\"\"/g, 'content-desc="null"');
20 | const tempDir = path.join(__dirname, '..', '.temp');
21 | _.mkdir(tempDir);
22 | const xmlFilePath = path.join(tempDir, 'android.json');
23 | const {
24 | hierarchy,
25 | } = xml2map.tojson(xml);
26 |
27 | const adaptor = function(node) {
28 | if (node.bounds) {
29 | const bounds = node.bounds.match(/[\d\.]+/g);
30 |
31 | // [ x, y, width, height]
32 | node.bounds = [
33 | ~~bounds[0],
34 | ~~bounds[1],
35 | bounds[2] - bounds[0],
36 | bounds[3] - bounds[1],
37 | ];
38 | }
39 |
40 | if (node.node) {
41 | node.nodes = node.node.length ? node.node : [ node.node ];
42 | node.nodes.forEach(adaptor);
43 | delete node.node;
44 | }
45 | return node;
46 | };
47 |
48 | const matchedNode = _.findLast(hierarchy.node, i => {
49 | return (
50 | i !== null &&
51 | typeof i === 'object' &&
52 | i.package !== 'com.android.systemui'
53 | );
54 | });
55 |
56 | let data;
57 |
58 | try {
59 | data = adaptor(matchedNode);
60 | } finally {
61 | !data && _.reportNodesError(xml);
62 | }
63 |
64 | fs.writeFileSync(xmlFilePath, JSON.stringify(data), 'utf8');
65 | logger.info(`Dump Android XML success, save to ${xmlFilePath}`);
66 |
67 | const remoteFile = `${ADB.ANDROID_TMP_DIR}/screenshot.png`;
68 | const cmd = `/system/bin/rm ${remoteFile}; /system/bin/screencap -p ${remoteFile}`;
69 | yield adb.shell(cmd);
70 | const localPath = path.join(tempDir, 'android-screenshot.png');
71 | yield adb.pull(remoteFile, localPath);
72 | };
73 |
74 | exports.initDevice = function* (udid) {
75 | adb = new ADB();
76 | adb.setDeviceId(udid);
77 | uiautomator = new UIAutomatorWD();
78 | yield uiautomator.init(adb);
79 | logger.info(`Android device started: ${udid}`);
80 | };
81 |
--------------------------------------------------------------------------------
/lib/app-inspector.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Server = require('./server');
4 | const _ = require('./common/helper');
5 | const logger = require('./common/logger');
6 |
7 | const {
8 | chalk,
9 | detectPort,
10 | } = _;
11 |
12 | function* parseOptions(options) {
13 | const port = yield detectPort(options.port);
14 |
15 | if (port !== parseInt(options.port, 10)) {
16 | logger.info('port: %d was occupied, changed port: %d', options.port, port);
17 | options.port = port;
18 | }
19 | }
20 |
21 | function* initDevice(options) {
22 | const udid = options.udid;
23 | const isIOS = _.getDeviceInfo(udid).isIOS;
24 |
25 | if (isIOS) {
26 | yield require('./ios').initDevice(udid);
27 | } else {
28 | yield require('./android').initDevice(udid);
29 | }
30 | }
31 |
32 | function* openBrowser(url) {
33 | const platform = process.platform;
34 | const linuxShell = platform === 'linux' ? 'xdg-open' : 'open';
35 | const openShell = platform === 'win32' ? 'start' : linuxShell;
36 | yield _.exec(`${openShell} ${url}`);
37 | }
38 |
39 | module.exports = function* (options) {
40 | try {
41 | yield parseOptions(options);
42 | const server = new Server(options);
43 | yield server.start();
44 | const url = `http://${_.ipv4}:${options.port}`;
45 | logger.debug(`server start at: ${url}`);
46 | yield initDevice(options);
47 | global.serverStarted = true;
48 | logger.info(`inspector start at: ${chalk.white(url)}`);
49 |
50 | if (!options.silent) {
51 | yield openBrowser(url);
52 | }
53 | } catch (e) {
54 | console.log(e);
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/lib/common/helper.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const utils = require('xutil');
4 | const request = require('request');
5 | const childProcess = require('child_process');
6 |
7 | const logger = require('./logger');
8 |
9 | const _ = utils.merge({}, utils);
10 |
11 | _.exec = function(cmd, opts) {
12 | return new Promise(function(resolve, reject) {
13 | childProcess.exec(cmd, _.merge({
14 | maxBuffer: 1024 * 512,
15 | wrapArgs: false,
16 | }, opts || {}), function(err, stdout) {
17 | if (err) {
18 | return reject(err);
19 | }
20 | resolve(_.trim(stdout));
21 | });
22 | });
23 | };
24 |
25 | _.spawn = function() {
26 | const args = Array.prototype.slice.call(arguments);
27 | return new Promise((resolve, reject) => {
28 | let stdout = '';
29 | let stderr = '';
30 | const child = childProcess.spawn.apply(childProcess, args);
31 |
32 | child.stdout.setEncoding('utf8');
33 | child.stderr.setEncoding('utf8');
34 |
35 | child.on('error', error => {
36 | reject(error);
37 | });
38 |
39 | child.stdout.on('data', data => {
40 | stdout += data;
41 | });
42 |
43 | child.stderr.on('data', data => {
44 | stderr += data;
45 | });
46 |
47 | child.on('close', code => {
48 | let error;
49 | if (code) {
50 | error = new Error(stderr);
51 | error.code = code;
52 | return reject(error);
53 | }
54 | resolve([ stdout, stderr ]);
55 | });
56 | });
57 | };
58 |
59 | _.sleep = function(ms) {
60 | return new Promise((resolve) => {
61 | setTimeout(resolve, ms);
62 | });
63 | };
64 |
65 | _.retry = function(func, interval, num) {
66 | return new Promise((resolve, reject) => {
67 | func().then(resolve, err => {
68 | if (num > 0 || typeof num === 'undefined') {
69 | _.sleep(interval).then(() => {
70 | resolve(_.retry(func, interval, num - 1));
71 | });
72 | } else {
73 | reject(err);
74 | }
75 | });
76 | });
77 | };
78 |
79 | _.request = function(url, method) {
80 | return new Promise((resolve, reject) => {
81 | method = method.toUpperCase();
82 |
83 | const reqOpts = {
84 | url,
85 | method,
86 | headers: {
87 | 'Content-type': 'application/json;charset=UTF=8',
88 | },
89 | resolveWithFullResponse: true,
90 | };
91 |
92 | request(reqOpts, (error, res, body) => {
93 | if (error) {
94 | logger.debug(`xctest client proxy error with: ${error}`);
95 | return reject(error);
96 | }
97 |
98 | resolve(body);
99 | });
100 | });
101 | };
102 |
103 | _.getDeviceInfo = function(udid) {
104 | return {
105 | isIOS: !!~[ 25, 36, 40 ].indexOf(udid.length),
106 | // iPhone XR (12.1) [00008020-001D4D38XXXXXXXX]
107 | isRealIOS: /^\w{40}|(\d{8}-\w{16})$/.test(udid),
108 | };
109 | };
110 |
111 | /**
112 | * print error information for wrong node
113 | * @param {string} source
114 | */
115 | _.reportNodesError = function(source) {
116 | console.error(_.chalk.red(
117 | `The source may be wrong, please check your app or report with below message at:
118 | ${_.chalk.blue('https://github.com/macacajs/app-inspector/issues/new')}
119 | ****** SOURCE START *******
120 | ${source}
121 | '****** SOURCE END *******`
122 | ));
123 | };
124 |
125 | module.exports = _;
126 |
--------------------------------------------------------------------------------
/lib/common/logger.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const logger = require('xlogger');
4 |
5 | const options = {
6 | logToStd: true,
7 | closeFile: true,
8 | };
9 |
10 | module.exports = logger.Logger(options);
11 | module.exports.middleware = logger.middleware(options);
12 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.isDev = process.env.APP_INSPECTOR === 'dev';
4 |
--------------------------------------------------------------------------------
/lib/ios.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const XCTestWD = require('xctestwd');
6 | const Simulator = require('ios-simulator');
7 |
8 | const _ = require('./common/helper');
9 | const logger = require('./common/logger');
10 |
11 | const adaptor = function(node) {
12 | node.class = `XCUIElementType${node.type}`;
13 |
14 | const rect = node.rect;
15 | node.bounds = [
16 | rect.x,
17 | rect.y,
18 | rect.width,
19 | rect.height,
20 | ];
21 |
22 | if (node.children) {
23 | const children = node.children.length ? node.children : [ node.children ];
24 |
25 | const nodes = [];
26 | children.forEach(child => {
27 | if (child.isVisible || child.type !== 'Window') {
28 | nodes.push(adaptor(child));
29 | }
30 | });
31 |
32 | node.nodes = nodes;
33 | delete node.children;
34 | }
35 | return node;
36 | };
37 |
38 | let xctest;
39 |
40 | exports.dumpXMLAndScreenShot = function* () {
41 | const source = yield _.request(`http://${xctest.proxyHost}:${xctest.proxyPort}/wd/hub/source`, 'get', {});
42 | const tree = JSON.parse(source).value;
43 | const tempDir = path.join(__dirname, '..', '.temp');
44 | _.mkdir(tempDir);
45 | const xmlFilePath = path.join(tempDir, 'ios.json');
46 |
47 | let compatibleTree;
48 | try {
49 | compatibleTree = adaptor(tree);
50 | } finally {
51 | // use finally instead of catch + throw to keep the error stack clean
52 | !compatibleTree && _.reportNodesError(source);
53 | }
54 |
55 | fs.writeFileSync(xmlFilePath, JSON.stringify(compatibleTree), 'utf8');
56 | logger.debug(`Dump iOS XML success, save to ${xmlFilePath}`);
57 |
58 | const screenshot = yield _.request(`http://${xctest.proxyHost}:${xctest.proxyPort}/wd/hub/screenshot`, 'get', {});
59 | const base64Data = JSON.parse(screenshot).value;
60 | const imgFilePath = path.join(tempDir, 'ios-screenshot.png');
61 | fs.writeFileSync(imgFilePath, base64Data, 'base64');
62 | };
63 |
64 | exports.initDevice = function* (udid) {
65 | const isRealIOS = _.getDeviceInfo(udid).isRealIOS;
66 |
67 | let device;
68 |
69 | if (isRealIOS) {
70 | device = {
71 | deviceId: udid,
72 | };
73 | } else {
74 | device = new Simulator({
75 | deviceId: udid,
76 | });
77 | }
78 |
79 | xctest = new XCTestWD({
80 | proxyPort: 8001,
81 | device,
82 | });
83 |
84 | try {
85 | yield xctest.start({
86 | desiredCapabilities: {},
87 | });
88 | } catch (e) {
89 | console.log(e);
90 | }
91 |
92 | logger.info(`iOS device started: ${udid}`);
93 | };
94 |
--------------------------------------------------------------------------------
/lib/middlewares.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const os = require('os');
4 | const path = require('path');
5 | const serve = require('koa-static');
6 | const router = require('koa-router');
7 |
8 | const pkg = require('../package');
9 | const logger = require('./common/logger');
10 |
11 | module.exports = app => {
12 | const string = `${pkg.name}/${pkg.version} node/${process.version}(${os.platform()})`;
13 |
14 | app.use(function* powerby(next) {
15 | yield next;
16 | this.set('X-Powered-By', string);
17 | });
18 |
19 | const p = path.join(__dirname, '..', 'public');
20 | app.use(serve(p));
21 | app.use(serve(path.join(p, '3rdparty')));
22 | app.use(serve(path.join(path.resolve(p, '..', '.temp'))));
23 | router(app);
24 |
25 | app.use(logger.middleware);
26 | };
27 |
--------------------------------------------------------------------------------
/lib/render.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const nunjucks = require('nunjucks');
5 |
6 | const config = require('./config');
7 |
8 | const env = new nunjucks.Environment(
9 | new nunjucks.FileSystemLoader(path.resolve(__dirname, '../views'), {
10 | noCache: config.isDev,
11 | })
12 | );
13 |
14 | env.addFilter('jstring', str =>
15 | str
16 | .replace(/["'\/]/g, char => '\\' + char)
17 | .replace(/\n/g, () => '\\\n')
18 | );
19 |
20 | module.exports = function render(path, data) {
21 | return new Promise((resolve, reject) => {
22 | env.render(path, data, (err, res) => {
23 | if (err) {
24 | reject(err);
25 | } else {
26 | resolve(res);
27 | }
28 | });
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/lib/router.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Router = require('koa-router');
4 |
5 | const pgk = require('../package');
6 | const render = require('./render');
7 | const _ = require('./common/helper');
8 |
9 | const rootRouter = new Router();
10 |
11 | module.exports = function(app) {
12 | rootRouter
13 | .get('/', function* () {
14 | const isIOS = _.getDeviceInfo(this._options.udid).isIOS;
15 |
16 | let dumpFailed = false;
17 | try {
18 | if (global.serverStarted) {
19 | if (isIOS) {
20 | yield require('./ios').dumpXMLAndScreenShot();
21 | } else {
22 | yield require('./android').dumpXMLAndScreenShot();
23 | }
24 | }
25 | } catch (e) {
26 | dumpFailed = true;
27 | console.error(e);
28 | }
29 | this.body = yield render('index.html', {
30 | data: {
31 | title: pgk.name,
32 | version: pgk.version,
33 | isIOS,
34 | serverStarted: global.serverStarted,
35 | dumpFailed,
36 | },
37 | });
38 | });
39 |
40 | app
41 | .use(rootRouter.routes());
42 | };
43 |
--------------------------------------------------------------------------------
/lib/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const os = require('os');
4 | const koa = require('koa');
5 | const bodyParser = require('koa-bodyparser');
6 |
7 | const router = require('./router');
8 | const _ = require('./common/helper');
9 | const logger = require('./common/logger');
10 | const middlewares = require('./middlewares');
11 |
12 | const startServer = (options) => {
13 |
14 | return new Promise((resolve, reject) => {
15 | logger.debug('server start with config:\n %j', options);
16 |
17 | try {
18 | const app = koa();
19 |
20 | app.use(bodyParser());
21 |
22 | app.use(function* (next) {
23 | this._options = options;
24 | yield next;
25 | });
26 |
27 | middlewares(app);
28 |
29 | router(app);
30 |
31 | app.listen(options.port, resolve);
32 | } catch (e) {
33 | logger.debug(`server failed to start: ${e.stack}`);
34 | reject(e);
35 | }
36 | });
37 | };
38 |
39 | const defaultOpt = {};
40 |
41 | function Server(options) {
42 | this.options = _.merge(defaultOpt, options || {});
43 | this.init();
44 | }
45 |
46 | Server.prototype.init = function() {
47 | this.options.ip = _.ipv4;
48 | this.options.host = os.hostname();
49 | this.options.loaded_time = _.moment(Date.now()).format('YYYY-MM-DD HH:mm:ss');
50 | };
51 |
52 | Server.prototype.start = function() {
53 | return startServer(this.options);
54 | };
55 |
56 | module.exports = Server;
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app-inspector",
3 | "version": "2.2.0",
4 | "description": "app inspector",
5 | "keywords": [
6 | "inspector",
7 | "macaca"
8 | ],
9 | "bin": {
10 | "inspector": "./bin/app-inspector.js",
11 | "app-inspector": "./bin/app-inspector.js"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git://github.com/macacajs/app-inspector.git"
16 | },
17 | "files": [
18 | "lib/**/*.js",
19 | "public/**/*.js",
20 | "public/**/*.css",
21 | "views/index.html",
22 | "index.js"
23 | ],
24 | "dependencies": {
25 | "co": "^4.6.0",
26 | "co-request": "^1.0.0",
27 | "commander": "^2.9.0",
28 | "detect-port": "1",
29 | "koa": "^1.2.1",
30 | "koa-bodyparser": "^2.2.0",
31 | "koa-router": "^5.4.0",
32 | "koa-static": "^2.0.0",
33 | "npm-update": "^1.0.2",
34 | "nunjucks": "^2.4.2",
35 | "react-syntax-highlighter": "^7.0.2",
36 | "request": "^2.74.0",
37 | "xlogger": "^1.0.4",
38 | "xml2map2": "^1.0.2",
39 | "xutil": "1"
40 | },
41 | "devDependencies": {
42 | "babel-core": "^6.13.2",
43 | "babel-loader": "^7.1.5",
44 | "babel-preset-es2015": "^6.13.2",
45 | "babel-preset-react": "^6.11.1",
46 | "command-line-test": "^1.0.5",
47 | "concurrently": "^7.1.0",
48 | "cross-env": "^7.0.3",
49 | "css-loader": "^6.7.1",
50 | "es6-promise": "^3.2.1",
51 | "eslint": "8",
52 | "eslint-config-airbnb": "^19.0.4",
53 | "eslint-config-egg": "^11.0.1",
54 | "eslint-config-prettier": "^6.9.0",
55 | "eslint-plugin-babel": "^5.3.1",
56 | "eslint-plugin-mocha": "^4.11.0",
57 | "eslint-plugin-react": "^7.2.1",
58 | "git-contributor": "1",
59 | "husky": "^1.3.1",
60 | "json-loader": "^0.5.4",
61 | "koa-webpack-dev-middleware": "^1.2.2",
62 | "less": "^3.11.3",
63 | "less-loader": "^6.2.0",
64 | "macaca-ecosystem": "*",
65 | "mini-css-extract-plugin": "^2.6.0",
66 | "mocha": "6",
67 | "nyc": "^11.8.0",
68 | "react": "^15.2.1",
69 | "react-copy-to-clipboard": "^5.0.0",
70 | "react-dom": "^15.2.1",
71 | "react-ga": "^2.7.0",
72 | "react-github-button": "0.1.11",
73 | "style-loader": "^3.3.1",
74 | "terser": "^3.14.1",
75 | "vuepress": "^0.14.8",
76 | "webpack": "^5.69.1",
77 | "webpack-cli": "^4.9.0",
78 | "webpack-dev-server": "^4.8.1",
79 | "whatwg-fetch": "^1.0.0"
80 | },
81 | "scripts": {
82 | "dev": "concurrently \"npm run watch\" \"npm run start\"",
83 | "test": "nyc --reporter=text mocha",
84 | "lint": "eslint --fix .",
85 | "start": "APP_INSPECTOR=dev ./bin/app-inspector.js -u emulator-5554 --verbose",
86 | "build": "cross-env NODE_ENV=production webpack",
87 | "watch": "cross-env NODE_ENV=development webpack serve",
88 | "prepublish": "npm run build",
89 | "contributor": "git-contributor",
90 | "docs:dev": "vuepress dev docs",
91 | "docs:build": "vuepress build docs",
92 | "prepublishOnly": "npm run build"
93 | },
94 | "husky": {
95 | "hooks": {
96 | "pre-commit": "npm run lint"
97 | }
98 | },
99 | "homepage": "https://github.com/macacajs/app-inspector",
100 | "bugs": "https://github.com/macacajs/app-inspector/issues/new",
101 | "license": "MIT",
102 | "optionalDependencies": {
103 | "ios-simulator": "*",
104 | "macaca-adb": "*",
105 | "uiautomatorwd": "*",
106 | "xctestwd": "*"
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/public/3rdparty/react-dom.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ReactDOM v15.3.1
3 | *
4 | * Copyright 2013-present, Facebook, Inc.
5 | * All rights reserved.
6 | *
7 | * This source code is licensed under the BSD-style license found in the
8 | * LICENSE file in the root directory of this source tree. An additional grant
9 | * of patent rights can be found in the PATENTS file in the same directory.
10 | *
11 | */
12 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e(require("react"));else if("function"==typeof define&&define.amd)define(["react"],e);else{var f;f="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,f.ReactDOM=e(f.React)}}(function(e){return e.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED});
--------------------------------------------------------------------------------
/test/bin/app-inspector.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {
4 | EOL,
5 | } = require('os');
6 | const path = require('path');
7 | const assert = require('assert');
8 | const CliTest = require('command-line-test');
9 |
10 | const utils = require('./utils');
11 | const pkg = require('../../package');
12 |
13 | const binFile = path.resolve(pkg.bin[pkg.name]);
14 |
15 | const startString = 'inspector start at:';
16 |
17 | describe('test/bin/app-inspector.test.js', function() {
18 | this.timeout(60 * 10 * 1000);
19 |
20 | it('`app-inspector -v` should be ok', function *() {
21 | var cliTest = new CliTest();
22 | var res = yield cliTest.execFile(binFile, ['-v'], {});
23 | assert.ok(res.stdout.includes(pkg.version));
24 | });
25 |
26 | it('`app-inspector -h` should be ok', function *() {
27 | var cliTest = new CliTest();
28 | var res = yield cliTest.execFile(binFile, ['-h'], {});
29 | var lines = res.stdout.trim().split(EOL);
30 | assert.ok(lines[0].includes(pkg.name));
31 | });
32 |
33 | it('app-inspector start should be ok', function *() {
34 | var device = yield utils.getDevices();
35 | var res = yield utils.getOutPut(device.udid);
36 | assert.ok(res.includes(startString));
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/test/bin/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const ADB = require('macaca-adb');
5 | const Simulator = require('ios-simulator');
6 | const child_process = require('child_process');
7 |
8 | const pkg = require('../../package');
9 |
10 | const binFile = path.resolve(pkg.bin[pkg.name]);
11 |
12 | const isIOS = process.env.PLATFORM === 'ios';
13 | const startString = 'inspector start at:';
14 |
15 | exports.getDevices = function *() {
16 | var matchedDevice = null;
17 | var devices;
18 |
19 | if (isIOS) {
20 | devices = yield Simulator.getDevices();
21 | devices.forEach(function(device) {
22 | if (device.name === 'iPhone 6' && device.available) {
23 | matchedDevice = device;
24 | }
25 | });
26 | } else {
27 | devices = yield ADB.getDevices();
28 | matchedDevice = devices[0];
29 | }
30 | return matchedDevice;
31 | };
32 |
33 | exports.getOutPut = function(udid) {
34 | console.log(`get simulator id: ${udid}`);
35 | return new Promise((resolve, reject) => {
36 | let res = '';
37 | const child = child_process.spawn(binFile, ['-u', udid, '-s']);
38 |
39 | child.stdout.setEncoding('utf8');
40 | child.stderr.setEncoding('utf8');
41 | child.stdout.on('data', data => {
42 | res += data;
43 | console.log(res);
44 | if (!!~res.indexOf(startString)) {
45 | resolve(res);
46 | }
47 | });
48 | });
49 | };
50 |
51 |
--------------------------------------------------------------------------------
/test/lib/common/helper.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('assert');
4 |
5 | const _ = require('../../../lib/common/helper');
6 |
7 | describe('lib/common/helper.test.js', () => {
8 | let res;
9 | it('getDeviceInfo()', () => {
10 | res = _.getDeviceInfo('00008020-001D4D38XXXXXXXX');
11 | assert.equal(res.isIOS, true);
12 | assert.equal(res.isRealIOS, true);
13 | res = _.getDeviceInfo('B8B6B1F5-30E0-401B-B1CE-70E4F39937A5');
14 | assert.equal(res.isIOS, true);
15 | assert.equal(res.isRealIOS, false);
16 | });
17 | });
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter spec
2 | --recursive
3 |
--------------------------------------------------------------------------------
/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Macaca App inspector
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const webpack = require('webpack');
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 | const traceFragment = require('macaca-ecosystem/lib/trace-fragment');
7 |
8 | const pkg = require('./package');
9 |
10 | module.exports = {
11 | entry: {
12 | index: path.resolve(__dirname, './assets/index'),
13 | },
14 | output: {
15 | path: path.resolve(__dirname, 'public/dist'),
16 | publicPath: '/dist',
17 | filename: '[name].js',
18 | },
19 | resolve: {
20 | extensions: [ '.jsx', '.js' ],
21 | },
22 | externals: [
23 | {
24 | react: 'React',
25 | 'react-dom': 'ReactDOM',
26 | },
27 | ],
28 | module: {
29 | rules: [
30 | {
31 | test: /\.jsx?/,
32 | loader: 'babel-loader',
33 | exclude: /node_modules/,
34 | }, {
35 | test: /\.json$/,
36 | use: 'json-loader',
37 | type: 'javascript/auto',
38 | exclude: /node_modules/,
39 | },
40 | {
41 | test: /\.less$/,
42 | use: [
43 | {
44 | loader: 'style-loader',
45 | },
46 | {
47 | loader: 'css-loader',
48 | },
49 | {
50 | loader: 'less-loader',
51 | },
52 | ],
53 | },
54 | {
55 | test: /\.css$/,
56 | use: [
57 | MiniCssExtractPlugin.loader,
58 | 'css-loader',
59 | ],
60 | },
61 | ],
62 | },
63 | plugins: [
64 | new MiniCssExtractPlugin({
65 | filename: '[name].css',
66 | chunkFilename: '[name].css',
67 | }),
68 | new webpack.DefinePlugin({
69 | 'process.env.VERSION': JSON.stringify(pkg.version),
70 | 'process.env.traceFragment': traceFragment,
71 | }),
72 | ],
73 | devServer: {
74 | hot: true,
75 | static: {
76 | directory: __dirname,
77 | },
78 | },
79 | };
80 |
--------------------------------------------------------------------------------