├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── stale.yml ├── .gitignore ├── .prettierrc.js ├── .watchmanconfig ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── azure-pipelines.yml ├── babel.config.js ├── demo ├── .buckconfig ├── .bundle │ └── config ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── .ruby-version ├── .watchmanconfig ├── App.tsx ├── AppWithNavigation.tsx ├── Gemfile ├── __tests__ │ └── App-test.tsx ├── _node-version ├── android │ ├── app │ │ ├── _BUCK │ │ ├── build.gradle │ │ ├── build_defs.bzl │ │ ├── debug.keystore │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── demo │ │ │ │ └── ReactNativeFlipper.java │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── demo │ │ │ │ ├── MainActivity.java │ │ │ │ ├── MainApplication.java │ │ │ │ └── newarchitecture │ │ │ │ ├── MainApplicationReactNativeHost.java │ │ │ │ ├── components │ │ │ │ └── MainComponentsRegistry.java │ │ │ │ └── modules │ │ │ │ └── MainApplicationTurboModuleManagerDelegate.java │ │ │ ├── jni │ │ │ ├── CMakeLists.txt │ │ │ ├── MainApplicationModuleProvider.cpp │ │ │ ├── MainApplicationModuleProvider.h │ │ │ ├── MainApplicationTurboModuleManagerDelegate.cpp │ │ │ ├── MainApplicationTurboModuleManagerDelegate.h │ │ │ ├── MainComponentsRegistry.cpp │ │ │ ├── MainComponentsRegistry.h │ │ │ └── OnLoad.cpp │ │ │ └── res │ │ │ ├── drawable │ │ │ └── rn_edit_text_material.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── babel.config.js ├── index.js ├── indexSnapshot.js ├── ios │ ├── Podfile │ ├── Podfile.lock │ ├── _xcode.env │ ├── demo.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── demo.xcscheme │ ├── demo.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── demo │ │ ├── AppDelegate.h │ │ ├── AppDelegate.mm │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── LaunchScreen.storyboard │ │ └── main.m │ └── demoTests │ │ ├── Info.plist │ │ └── demoTests.m ├── metro.config.js ├── package.json ├── pixels-catcher.json ├── run_android_debug.sh ├── run_android_release.sh ├── run_android_test.sh ├── run_ios_debug.sh ├── run_ios_release.sh ├── run_ios_test.sh ├── snapshots │ ├── android │ │ ├── diffs │ │ │ ├── AppSnapshot.png │ │ │ ├── HomeScreen.png │ │ │ ├── WebViewTest.png │ │ │ ├── longContent.png │ │ │ └── someComponent.png │ │ ├── refImages │ │ │ ├── AppSnapshot.png │ │ │ ├── AppSnapshotWithWrongRefImg.png │ │ │ ├── HomeScreen.png │ │ │ ├── WebViewTest.png │ │ │ ├── longContent.png │ │ │ └── someComponent.png │ │ └── uploads │ │ │ ├── AppSnapshot.png │ │ │ ├── HomeScreen.png │ │ │ ├── WebViewTest.png │ │ │ ├── longContent.png │ │ │ └── someComponent.png │ ├── iPhone6 │ │ └── refImages │ │ │ ├── AppSnapshot.png │ │ │ ├── AppSnapshotWithWrongRefImg.png │ │ │ ├── WebViewTest.png │ │ │ ├── longContent.png │ │ │ └── someComponent.png │ └── ios │ │ ├── diffs │ │ ├── AppSnapshot.png │ │ ├── HomeScreen.png │ │ ├── WebViewTest.png │ │ ├── longContent.png │ │ └── someComponent.png │ │ ├── refImages │ │ ├── AppSnapshot.png │ │ ├── AppSnapshotWithWrongRefImg.png │ │ ├── HomeScreen.png │ │ ├── WebViewTest.png │ │ ├── longContent.png │ │ └── someComponent.png │ │ └── uploads │ │ ├── AppSnapshot.png │ │ ├── HomeScreen.png │ │ ├── WebViewTest.png │ │ ├── longContent.png │ │ └── someComponent.png └── tsconfig.json ├── metro.config.js ├── package.json ├── res ├── azureDevops.png └── testResults.png ├── scripts └── create_android_emulator.sh └── src ├── client ├── Snapshot.tsx ├── SnapshotsContainer.tsx ├── __tests__ │ ├── Snapshot.js │ ├── SnapshotsContainer.js │ ├── __snapshots__ │ │ ├── Snapshot.js.snap │ │ ├── SnapshotsContainer.js.snap │ │ └── index.js.snap │ ├── index.js │ └── snapshotsManager.js ├── index.ts ├── snapshotsManager.ts ├── tsconfig.json └── utils │ ├── __tests__ │ ├── __snapshots__ │ │ └── compareToReference.js.snap │ └── compareToReference.js │ ├── compareToReference.ts │ ├── log.ts │ └── network.ts └── runner ├── TestsRunner.ts ├── azure └── AzurePublisher.ts ├── cli.ts ├── server ├── compareImages.ts ├── dummy.png └── server.ts ├── tsconfig.json └── utils ├── Reporter.ts ├── __tests__ ├── __snapshots__ │ ├── log.js.snap │ └── readConfig.js.snap ├── isCommand.js ├── log.js └── readConfig.js ├── delay.ts ├── device ├── AndroidDevice.ts ├── AndroidEmulator.ts ├── AndroidEmulatorCmd.ts ├── DeviceInterface.ts ├── IosSimulator.ts ├── __tests__ │ ├── AndroidDevice.js │ ├── AndroidEmulator.js │ ├── AndroidEmulatorCmd.js │ ├── IosSimulator.js │ ├── __snapshots__ │ │ ├── AndroidEmulator.js.snap │ │ └── deviceProvider.js.snap │ └── deviceProvider.js └── deviceProvider.ts ├── exec.ts ├── isCommand.ts ├── log.ts ├── readConfig.ts └── timeToSec.ts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:14 6 | steps: 7 | - checkout 8 | - run: npm install 9 | - run: npm run lint 10 | - run: npm run check-types 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [**/{.js,.ts{x},package.json}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | demo/**/* 2 | lib/**/* 3 | preprocessor.js 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "es6": true, 6 | "jasmine": true 7 | }, 8 | "extends": [ 9 | "plugin:react/recommended", 10 | "airbnb" 11 | ], 12 | "globals": { 13 | "jest": false, 14 | "__DEV__": false 15 | }, 16 | "settings": { 17 | "import/resolver": { 18 | "node": { 19 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 20 | } 21 | } 22 | }, 23 | "parser": "@typescript-eslint/parser", 24 | "parserOptions": { 25 | "ecmaFeatures": { 26 | "jsx": true 27 | }, 28 | "ecmaVersion": 12, 29 | "sourceType": "module" 30 | }, 31 | "plugins": [ 32 | "react", 33 | "@typescript-eslint" 34 | ], 35 | "rules": { 36 | "import/no-extraneous-dependencies": 0, 37 | "react/destructuring-assignment": 0, 38 | "operator-linebreak": 0, 39 | "react/sort-comp": 0, 40 | "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 0 }], 41 | "no-underscore-dangle": 0, 42 | "class-methods-use-this": 0, 43 | "import/extensions": 0, 44 | "no-plusplus": 0, 45 | "no-await-in-loop": 0, 46 | "max-classes-per-file": 0, 47 | "global-require": 0, 48 | "react/jsx-filename-extension": 0 49 | }, 50 | "overrides": [ 51 | { 52 | "files": ["*.ts", "*.tsx"], 53 | "parser": "@typescript-eslint/parser", 54 | "rules": { 55 | "@typescript-eslint/no-unused-vars": "warn", 56 | "@typescript-eslint/explicit-function-return-type": "error", 57 | "@typescript-eslint/consistent-type-imports": [ 58 | 2, 59 | { "prefer": "type-imports" } 60 | ] 61 | } 62 | } 63 | ] 64 | 65 | } 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | 12 | - [ ] I checked the demo project and cannot reproduce the issue 13 | - [ ] I checked the demo project and the issue exists 14 | 15 | Steps to reproduce the behavior: 16 | 1. Render PDF with '...' 17 | 2. Other actions '...' 18 | 3. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Emulator information:** 27 | - Version 28 | - etc. 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 5 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | # Label to use when marking an issue as stale 9 | staleLabel: wontfix 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale because it has not had 13 | recent activity. It will be closed if no further activity occurs. Thank you 14 | for your contributions. 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .vscode 3 | 4 | # generated 5 | /lib/ 6 | demo/ios_logs.log 7 | demo/android_logs.log 8 | yarn.lock 9 | 10 | tmp/ 11 | coverage/ 12 | 13 | # OSX 14 | # 15 | .DS_Store 16 | 17 | # node.js 18 | # 19 | node_modules/ 20 | npm-debug.log 21 | yarn-error.log 22 | pixels-catcher*.tgz 23 | package*/ 24 | demo/junit.xml 25 | 26 | # Xcode 27 | # 28 | build/ 29 | *.pbxuser 30 | !default.pbxuser 31 | *.mode1v3 32 | !default.mode1v3 33 | *.mode2v3 34 | !default.mode2v3 35 | *.perspectivev3 36 | !default.perspectivev3 37 | xcuserdata 38 | *.xccheckout 39 | *.moved-aside 40 | DerivedData 41 | *.hmap 42 | *.ipa 43 | *.xcuserstate 44 | project.xcworkspace 45 | 46 | 47 | # Android/IntelliJ 48 | # 49 | build/ 50 | .idea 51 | .gradle 52 | local.properties 53 | *.iml 54 | 55 | # BUCK 56 | buck-out/ 57 | \.buckd/ 58 | *.keystore 59 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | arrowParens: 'avoid', 7 | }; 8 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to react-native-PixelsCatcher 2 | 3 | :+1::tada: Thanks for taking the time to contribute! :tada::+1: 4 | 5 | ## Styleguides 6 | 7 | ### Git Commit Messages 8 | 9 | * Use the present tense ("Add feature" not "Added feature") 10 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 11 | * Limit the first line to 72 characters or less 12 | * Reference issues and pull requests liberally after the first line 13 | * When only changing documentation, include `[ci skip]` in the commit title 14 | * Consider starting the commit message with an applicable emoji: 15 | * :art: `:art:` when improving the format/structure of the code 16 | * :racehorse: `:racehorse:` when improving performance 17 | * :non-potable_water: `:non-potable_water:` when plugging memory leaks 18 | * :memo: `:memo:` when writing docs 19 | * :lollipop: `:lollipop:` when fixing Android 20 | * :apple: `:apple:` when fixing iOS 21 | * :bug: `:bug:` when fixing a bug 22 | * :fire: `:fire:` when removing code or files 23 | * :green_heart: `:green_heart:` when fixing the CI build 24 | * :white_check_mark: `:white_check_mark:` when adding tests 25 | * :lock: `:lock:` when dealing with security 26 | * :arrow_up: `:arrow_up:` when upgrading dependencies 27 | * :arrow_down: `:arrow_down:` when downgrading dependencies 28 | * :shirt: `:shirt:` when removing linter warnings 29 | 30 | ### JavaScript Styleguide 31 | 32 | * All JavaScript must adhere to [JavaScript Standard Style](https://standardjs.com/). 33 | 34 | ### Java Styleguide 35 | 36 | ### Objective-C 37 | 38 | ### Documentation Styleguide 39 | 40 | * Use [Markdown](https://daringfireball.net/projects/markdown). 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 - present Maksym Rusynyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description of changes 2 | 3 | ### I did Exploratory testing: 4 | - [ ] android 5 | - [ ] ios 6 | 7 | ### Can it be published to NPM? 8 | - [ ] Breaking change 9 | - [ ] Documentation is updated 10 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | MAC_RUNNER_IMAGE: 'macos-12' 3 | NODE_VERSION: '19.x' 4 | XCODE_VERSION: '14.2' 5 | 6 | trigger: 7 | branches: 8 | include: 9 | - master 10 | 11 | stages: 12 | - stage: Tests 13 | jobs: 14 | - job: UiTestsIOS 15 | pool: 16 | vmImage: '$(MAC_RUNNER_IMAGE)' 17 | steps: 18 | - task: NodeTool@0 19 | displayName: 'Set Node version $(NODE_VERSION)' 20 | inputs: 21 | versionSpec: $(NODE_VERSION) 22 | - script: npm run preinstall 23 | displayName: 'NPM PreInstall' 24 | workingDirectory: '$(Build.SourcesDirectory)/demo' 25 | - script: npm install 26 | displayName: 'NPM Install' 27 | workingDirectory: '$(Build.SourcesDirectory)/demo' 28 | - script: sudo xcode-select --switch /Applications/Xcode_$(XCODE_VERSION).app/Contents/Developer 29 | displayName: 'Select xcode $(XCODE_VERSION)' 30 | - script: ./run_ios_debug.sh 31 | displayName: 'Run tests' 32 | workingDirectory: '$(Build.SourcesDirectory)/demo' 33 | - publish: $(Build.SourcesDirectory)/demo/snapshots/ios 34 | condition: failed() 35 | artifact: screenshots_ios 36 | displayName: 'Publish test results' 37 | - task: PublishTestResults@2 38 | condition: succeededOrFailed() 39 | displayName: 'Publish test report' 40 | inputs: 41 | testRunner: JUnit 42 | testResultsFiles: '$(Build.SourcesDirectory)/demo/junit.xml' 43 | - script: ./node_modules/.bin/pixels-catcher ios debug azureAttachments 44 | condition: failed() 45 | env: 46 | SYSTEM_ACCESSTOKEN: $(System.AccessToken) 47 | workingDirectory: '$(Build.SourcesDirectory)/demo' 48 | displayName: 'Upload screenshots' 49 | - task: PublishBuildArtifacts@1 50 | displayName: Publish Device Log Artifacts 51 | inputs: 52 | pathtoPublish: $(System.DefaultWorkingDirectory)/demo/ios_logs.log 53 | artifactName: ios_logs 54 | - job: UiTestsAndroid 55 | pool: 56 | vmImage: '$(MAC_RUNNER_IMAGE)' 57 | steps: 58 | - task: NodeTool@0 59 | displayName: 'Set Node version $(NODE_VERSION)' 60 | inputs: 61 | versionSpec: $(NODE_VERSION) 62 | - script: ./scripts/create_android_emulator.sh 63 | displayName: 'Create and start Android Emulator' 64 | - script: npm run preinstall 65 | displayName: 'NPM PreInstall' 66 | workingDirectory: '$(Build.SourcesDirectory)/demo' 67 | - script: npm install 68 | displayName: 'NPM Install' 69 | workingDirectory: '$(Build.SourcesDirectory)/demo' 70 | - task: JavaToolInstaller@0 71 | inputs: 72 | versionSpec: '11' 73 | jdkArchitectureOption: 'x64' 74 | jdkSourceOption: 'PreInstalled' 75 | - script: ./run_android_debug.sh 76 | displayName: 'Run tests' 77 | workingDirectory: '$(Build.SourcesDirectory)/demo' 78 | - publish: $(Build.SourcesDirectory)/demo/snapshots/android 79 | condition: failed() 80 | artifact: screenshots_android 81 | displayName: 'Publish test results' 82 | - task: PublishTestResults@2 83 | condition: succeededOrFailed() 84 | displayName: 'Publish test report' 85 | inputs: 86 | testRunner: JUnit 87 | testResultsFiles: '$(Build.SourcesDirectory)/demo/junit.xml' 88 | - script: ./node_modules/.bin/pixels-catcher android debug azureAttachments 89 | condition: failed() 90 | env: 91 | SYSTEM_ACCESSTOKEN: $(System.AccessToken) 92 | workingDirectory: '$(Build.SourcesDirectory)/demo' 93 | displayName: 'Upload screenshots' 94 | - task: PublishBuildArtifacts@1 95 | displayName: Publish Device Log Artifacts 96 | inputs: 97 | pathtoPublish: $(System.DefaultWorkingDirectory)/demo/android_logs.log 98 | artifactName: android_logs 99 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | 'module:metro-react-native-babel-preset', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /demo/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /demo/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /demo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | overrides: [ 7 | { 8 | files: ['*.ts', '*.tsx'], 9 | rules: { 10 | '@typescript-eslint/no-shadow': ['error'], 11 | 'no-shadow': 'off', 12 | 'no-undef': 'off', 13 | }, 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | ios/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | 35 | # node.js 36 | # 37 | node_modules/ 38 | npm-debug.log 39 | yarn-error.log 40 | 41 | # BUCK 42 | buck-out/ 43 | \.buckd/ 44 | *.keystore 45 | !debug.keystore 46 | 47 | # fastlane 48 | # 49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 50 | # screenshots whenever they are needed. 51 | # For more information about the recommended setup visit: 52 | # https://docs.fastlane.tools/best-practices/source-control/ 53 | 54 | **/fastlane/report.xml 55 | **/fastlane/Preview.html 56 | **/fastlane/screenshots 57 | **/fastlane/test_output 58 | 59 | # Bundle artifact 60 | *.jsbundle 61 | 62 | # Ruby / CocoaPods 63 | /ios/Pods/ 64 | /vendor/bundle/ 65 | -------------------------------------------------------------------------------- /demo/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: true, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /demo/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.5 2 | -------------------------------------------------------------------------------- /demo/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /demo/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | * 5 | * Generated with the TypeScript template 6 | * https://github.com/react-native-community/react-native-template-typescript 7 | * 8 | * @format 9 | */ 10 | 11 | import React, {type PropsWithChildren} from 'react'; 12 | import { 13 | SafeAreaView, 14 | ScrollView, 15 | StatusBar, 16 | StyleSheet, 17 | Text, 18 | useColorScheme, 19 | View, 20 | } from 'react-native'; 21 | 22 | import { 23 | Colors, 24 | DebugInstructions, 25 | Header, 26 | LearnMoreLinks, 27 | ReloadInstructions, 28 | } from 'react-native/Libraries/NewAppScreen'; 29 | 30 | const Section: React.FC< 31 | PropsWithChildren<{ 32 | title: string; 33 | }> 34 | > = ({children, title}) => { 35 | const isDarkMode = useColorScheme() === 'dark'; 36 | return ( 37 | 38 | 45 | {title} 46 | 47 | 54 | {children} 55 | 56 | 57 | ); 58 | }; 59 | 60 | const App = () => { 61 | const isDarkMode = useColorScheme() === 'dark'; 62 | 63 | const backgroundStyle = { 64 | backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, 65 | }; 66 | 67 | return ( 68 | 69 | 73 | 76 | 77 | 81 | 82 | Edit App.tsx to change this 83 | screen and then come back to see your edits. 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | Read the docs to discover what to do next: 93 | 94 | 95 | 96 | 97 | 98 | ); 99 | }; 100 | 101 | const styles = StyleSheet.create({ 102 | sectionContainer: { 103 | marginTop: 32, 104 | paddingHorizontal: 24, 105 | }, 106 | sectionTitle: { 107 | fontSize: 24, 108 | fontWeight: '600', 109 | }, 110 | sectionDescription: { 111 | marginTop: 8, 112 | fontSize: 18, 113 | fontWeight: '400', 114 | }, 115 | highlight: { 116 | fontWeight: '700', 117 | }, 118 | }); 119 | 120 | export default App; 121 | -------------------------------------------------------------------------------- /demo/AppWithNavigation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { View, Text } from 'react-native'; 3 | import { NavigationContainer, useNavigation } from '@react-navigation/native'; 4 | import { createStackNavigator } from '@react-navigation/stack'; 5 | import { Button } from 'react-native'; 6 | 7 | function NavToDetails() { 8 | const navigation = useNavigation(); 9 | return ( 10 | navigation.navigate('Details')} 13 | /> 14 | ); 15 | } 16 | 17 | export function HomeScreen() { 18 | return ( 19 | 20 | Home Screen 21 | 22 | 23 | ); 24 | } 25 | 26 | function DetailsScreen() { 27 | return ( 28 | 29 | Details Screen 30 | 31 | ); 32 | } 33 | 34 | const Stack = createStackNavigator(); 35 | 36 | function App() { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /demo/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby '2.7.5' 5 | 6 | gem 'cocoapods', '~> 1.11', '>= 1.11.2' 7 | -------------------------------------------------------------------------------- /demo/__tests__/App-test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /demo/_node-version: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /demo/android/app/_BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.demo", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.demo", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /demo/android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /demo/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/debug.keystore -------------------------------------------------------------------------------- /demo/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /demo/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/android/app/src/debug/java/com/demo/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.demo; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceEventListener; 23 | import com.facebook.react.ReactInstanceManager; 24 | import com.facebook.react.bridge.ReactContext; 25 | import com.facebook.react.modules.network.NetworkingModule; 26 | import okhttp3.OkHttpClient; 27 | 28 | public class ReactNativeFlipper { 29 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 30 | if (FlipperUtils.shouldEnableFlipper(context)) { 31 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 32 | 33 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 34 | client.addPlugin(new ReactFlipperPlugin()); 35 | client.addPlugin(new DatabasesFlipperPlugin(context)); 36 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 37 | client.addPlugin(CrashReporterPlugin.getInstance()); 38 | 39 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 40 | NetworkingModule.setCustomClientBuilder( 41 | new NetworkingModule.CustomClientBuilder() { 42 | @Override 43 | public void apply(OkHttpClient.Builder builder) { 44 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 45 | } 46 | }); 47 | client.addPlugin(networkFlipperPlugin); 48 | client.start(); 49 | 50 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 51 | // Hence we run if after all native modules have been initialized 52 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 53 | if (reactContext == null) { 54 | reactInstanceManager.addReactInstanceEventListener( 55 | new ReactInstanceEventListener() { 56 | @Override 57 | public void onReactContextInitialized(ReactContext reactContext) { 58 | reactInstanceManager.removeReactInstanceEventListener(this); 59 | reactContext.runOnNativeModulesQueueThread( 60 | new Runnable() { 61 | @Override 62 | public void run() { 63 | client.addPlugin(new FrescoFlipperPlugin()); 64 | } 65 | }); 66 | } 67 | }); 68 | } else { 69 | client.addPlugin(new FrescoFlipperPlugin()); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /demo/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.demo; 2 | 3 | import android.os.Bundle; 4 | import com.facebook.react.ReactActivity; 5 | import com.facebook.react.ReactActivityDelegate; 6 | import com.facebook.react.ReactRootView; 7 | 8 | public class MainActivity extends ReactActivity { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | @Override 15 | protected String getMainComponentName() { 16 | return "demo"; 17 | } 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(null); 22 | } 23 | 24 | /** 25 | * Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and 26 | * you can specify the renderer you wish to use - the new renderer (Fabric) or the old renderer 27 | * (Paper). 28 | */ 29 | @Override 30 | protected ReactActivityDelegate createReactActivityDelegate() { 31 | return new MainActivityDelegate(this, getMainComponentName()); 32 | } 33 | 34 | public static class MainActivityDelegate extends ReactActivityDelegate { 35 | public MainActivityDelegate(ReactActivity activity, String mainComponentName) { 36 | super(activity, mainComponentName); 37 | } 38 | 39 | @Override 40 | protected ReactRootView createRootView() { 41 | ReactRootView reactRootView = new ReactRootView(getContext()); 42 | // If you opted-in for the New Architecture, we enable the Fabric Renderer. 43 | reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED); 44 | return reactRootView; 45 | } 46 | 47 | @Override 48 | protected boolean isConcurrentRootEnabled() { 49 | // If you opted-in for the New Architecture, we enable Concurrent Root (i.e. React 18). 50 | // More on this on https://reactjs.org/blog/2022/03/29/react-v18.html 51 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.demo; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.react.config.ReactFeatureFlags; 11 | import com.facebook.soloader.SoLoader; 12 | import com.demo.newarchitecture.MainApplicationReactNativeHost; 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.util.List; 15 | 16 | public class MainApplication extends Application implements ReactApplication { 17 | 18 | private final ReactNativeHost mReactNativeHost = 19 | new ReactNativeHost(this) { 20 | @Override 21 | public boolean getUseDeveloperSupport() { 22 | return BuildConfig.DEBUG; 23 | } 24 | 25 | @Override 26 | protected List getPackages() { 27 | @SuppressWarnings("UnnecessaryLocalVariable") 28 | List packages = new PackageList(this).getPackages(); 29 | // Packages that cannot be autolinked yet can be added manually here, for example: 30 | // packages.add(new MyReactNativePackage()); 31 | return packages; 32 | } 33 | 34 | @Override 35 | protected String getJSMainModuleName() { 36 | return "index"; 37 | } 38 | }; 39 | 40 | private final ReactNativeHost mNewArchitectureNativeHost = 41 | new MainApplicationReactNativeHost(this); 42 | 43 | @Override 44 | public ReactNativeHost getReactNativeHost() { 45 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 46 | return mNewArchitectureNativeHost; 47 | } else { 48 | return mReactNativeHost; 49 | } 50 | } 51 | 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | // If you opted-in for the New Architecture, we enable the TurboModule system 56 | ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 57 | SoLoader.init(this, /* native exopackage */ false); 58 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 59 | } 60 | 61 | /** 62 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 63 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 64 | * 65 | * @param context 66 | * @param reactInstanceManager 67 | */ 68 | private static void initializeFlipper( 69 | Context context, ReactInstanceManager reactInstanceManager) { 70 | if (BuildConfig.DEBUG) { 71 | try { 72 | /* 73 | We use reflection here to pick up the class that initializes Flipper, 74 | since Flipper library is not available in release mode 75 | */ 76 | Class> aClass = Class.forName("com.demo.ReactNativeFlipper"); 77 | aClass 78 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 79 | .invoke(null, context, reactInstanceManager); 80 | } catch (ClassNotFoundException e) { 81 | e.printStackTrace(); 82 | } catch (NoSuchMethodException e) { 83 | e.printStackTrace(); 84 | } catch (IllegalAccessException e) { 85 | e.printStackTrace(); 86 | } catch (InvocationTargetException e) { 87 | e.printStackTrace(); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/newarchitecture/MainApplicationReactNativeHost.java: -------------------------------------------------------------------------------- 1 | package com.demo.newarchitecture; 2 | 3 | import android.app.Application; 4 | import androidx.annotation.NonNull; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactInstanceManager; 7 | import com.facebook.react.ReactNativeHost; 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate; 10 | import com.facebook.react.bridge.JSIModulePackage; 11 | import com.facebook.react.bridge.JSIModuleProvider; 12 | import com.facebook.react.bridge.JSIModuleSpec; 13 | import com.facebook.react.bridge.JSIModuleType; 14 | import com.facebook.react.bridge.JavaScriptContextHolder; 15 | import com.facebook.react.bridge.ReactApplicationContext; 16 | import com.facebook.react.bridge.UIManager; 17 | import com.facebook.react.fabric.ComponentFactory; 18 | import com.facebook.react.fabric.CoreComponentsRegistry; 19 | import com.facebook.react.fabric.FabricJSIModuleProvider; 20 | import com.facebook.react.fabric.ReactNativeConfig; 21 | import com.facebook.react.uimanager.ViewManagerRegistry; 22 | import com.demo.BuildConfig; 23 | import com.demo.newarchitecture.components.MainComponentsRegistry; 24 | import com.demo.newarchitecture.modules.MainApplicationTurboModuleManagerDelegate; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | /** 29 | * A {@link ReactNativeHost} that helps you load everything needed for the New Architecture, both 30 | * TurboModule delegates and the Fabric Renderer. 31 | * 32 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the 33 | * `newArchEnabled` property). Is ignored otherwise. 34 | */ 35 | public class MainApplicationReactNativeHost extends ReactNativeHost { 36 | public MainApplicationReactNativeHost(Application application) { 37 | super(application); 38 | } 39 | 40 | @Override 41 | public boolean getUseDeveloperSupport() { 42 | return BuildConfig.DEBUG; 43 | } 44 | 45 | @Override 46 | protected List getPackages() { 47 | List packages = new PackageList(this).getPackages(); 48 | // Packages that cannot be autolinked yet can be added manually here, for example: 49 | // packages.add(new MyReactNativePackage()); 50 | // TurboModules must also be loaded here providing a valid TurboReactPackage implementation: 51 | // packages.add(new TurboReactPackage() { ... }); 52 | // If you have custom Fabric Components, their ViewManagers should also be loaded here 53 | // inside a ReactPackage. 54 | return packages; 55 | } 56 | 57 | @Override 58 | protected String getJSMainModuleName() { 59 | return "index"; 60 | } 61 | 62 | @NonNull 63 | @Override 64 | protected ReactPackageTurboModuleManagerDelegate.Builder 65 | getReactPackageTurboModuleManagerDelegateBuilder() { 66 | // Here we provide the ReactPackageTurboModuleManagerDelegate Builder. This is necessary 67 | // for the new architecture and to use TurboModules correctly. 68 | return new MainApplicationTurboModuleManagerDelegate.Builder(); 69 | } 70 | 71 | @Override 72 | protected JSIModulePackage getJSIModulePackage() { 73 | return new JSIModulePackage() { 74 | @Override 75 | public List getJSIModules( 76 | final ReactApplicationContext reactApplicationContext, 77 | final JavaScriptContextHolder jsContext) { 78 | final List specs = new ArrayList<>(); 79 | 80 | // Here we provide a new JSIModuleSpec that will be responsible of providing the 81 | // custom Fabric Components. 82 | specs.add( 83 | new JSIModuleSpec() { 84 | @Override 85 | public JSIModuleType getJSIModuleType() { 86 | return JSIModuleType.UIManager; 87 | } 88 | 89 | @Override 90 | public JSIModuleProvider getJSIModuleProvider() { 91 | final ComponentFactory componentFactory = new ComponentFactory(); 92 | CoreComponentsRegistry.register(componentFactory); 93 | 94 | // Here we register a Components Registry. 95 | // The one that is generated with the template contains no components 96 | // and just provides you the one from React Native core. 97 | MainComponentsRegistry.register(componentFactory); 98 | 99 | final ReactInstanceManager reactInstanceManager = getReactInstanceManager(); 100 | 101 | ViewManagerRegistry viewManagerRegistry = 102 | new ViewManagerRegistry( 103 | reactInstanceManager.getOrCreateViewManagers(reactApplicationContext)); 104 | 105 | return new FabricJSIModuleProvider( 106 | reactApplicationContext, 107 | componentFactory, 108 | ReactNativeConfig.DEFAULT_CONFIG, 109 | viewManagerRegistry); 110 | } 111 | }); 112 | return specs; 113 | } 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/newarchitecture/components/MainComponentsRegistry.java: -------------------------------------------------------------------------------- 1 | package com.demo.newarchitecture.components; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.proguard.annotations.DoNotStrip; 5 | import com.facebook.react.fabric.ComponentFactory; 6 | import com.facebook.soloader.SoLoader; 7 | 8 | /** 9 | * Class responsible to load the custom Fabric Components. This class has native methods and needs a 10 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 11 | * folder for you). 12 | * 13 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the 14 | * `newArchEnabled` property). Is ignored otherwise. 15 | */ 16 | @DoNotStrip 17 | public class MainComponentsRegistry { 18 | static { 19 | SoLoader.loadLibrary("fabricjni"); 20 | } 21 | 22 | @DoNotStrip private final HybridData mHybridData; 23 | 24 | @DoNotStrip 25 | private native HybridData initHybrid(ComponentFactory componentFactory); 26 | 27 | @DoNotStrip 28 | private MainComponentsRegistry(ComponentFactory componentFactory) { 29 | mHybridData = initHybrid(componentFactory); 30 | } 31 | 32 | @DoNotStrip 33 | public static MainComponentsRegistry register(ComponentFactory componentFactory) { 34 | return new MainComponentsRegistry(componentFactory); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java: -------------------------------------------------------------------------------- 1 | package com.demo.newarchitecture.modules; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.soloader.SoLoader; 8 | import java.util.List; 9 | 10 | /** 11 | * Class responsible to load the TurboModules. This class has native methods and needs a 12 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 13 | * folder for you). 14 | * 15 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the 16 | * `newArchEnabled` property). Is ignored otherwise. 17 | */ 18 | public class MainApplicationTurboModuleManagerDelegate 19 | extends ReactPackageTurboModuleManagerDelegate { 20 | 21 | private static volatile boolean sIsSoLibraryLoaded; 22 | 23 | protected MainApplicationTurboModuleManagerDelegate( 24 | ReactApplicationContext reactApplicationContext, List packages) { 25 | super(reactApplicationContext, packages); 26 | } 27 | 28 | protected native HybridData initHybrid(); 29 | 30 | native boolean canCreateTurboModule(String moduleName); 31 | 32 | public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder { 33 | protected MainApplicationTurboModuleManagerDelegate build( 34 | ReactApplicationContext context, List packages) { 35 | return new MainApplicationTurboModuleManagerDelegate(context, packages); 36 | } 37 | } 38 | 39 | @Override 40 | protected synchronized void maybeLoadOtherSoLibraries() { 41 | if (!sIsSoLibraryLoaded) { 42 | // If you change the name of your application .so file in the Android.mk file, 43 | // make sure you update the name here as well. 44 | SoLoader.loadLibrary("demo_appmodules"); 45 | sIsSoLibraryLoaded = true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | # Define the library name here. 4 | project(demo_appmodules) 5 | 6 | # This file includes all the necessary to let you build your application with the New Architecture. 7 | include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake) 8 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationModuleProvider.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationModuleProvider.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace facebook { 7 | namespace react { 8 | 9 | std::shared_ptr MainApplicationModuleProvider( 10 | const std::string &moduleName, 11 | const JavaTurboModule::InitParams ¶ms) { 12 | // Here you can provide your own module provider for TurboModules coming from 13 | // either your application or from external libraries. The approach to follow 14 | // is similar to the following (for a library called `samplelibrary`: 15 | // 16 | // auto module = samplelibrary_ModuleProvider(moduleName, params); 17 | // if (module != nullptr) { 18 | // return module; 19 | // } 20 | // return rncore_ModuleProvider(moduleName, params); 21 | 22 | // Module providers autolinked by RN CLI 23 | auto rncli_module = rncli_ModuleProvider(moduleName, params); 24 | if (rncli_module != nullptr) { 25 | return rncli_module; 26 | } 27 | 28 | return rncore_ModuleProvider(moduleName, params); 29 | } 30 | 31 | } // namespace react 32 | } // namespace facebook 33 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationModuleProvider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | std::shared_ptr MainApplicationModuleProvider( 12 | const std::string &moduleName, 13 | const JavaTurboModule::InitParams ¶ms); 14 | 15 | } // namespace react 16 | } // namespace facebook 17 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationTurboModuleManagerDelegate.h" 2 | #include "MainApplicationModuleProvider.h" 3 | 4 | namespace facebook { 5 | namespace react { 6 | 7 | jni::local_ref 8 | MainApplicationTurboModuleManagerDelegate::initHybrid( 9 | jni::alias_ref) { 10 | return makeCxxInstance(); 11 | } 12 | 13 | void MainApplicationTurboModuleManagerDelegate::registerNatives() { 14 | registerHybrid({ 15 | makeNativeMethod( 16 | "initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid), 17 | makeNativeMethod( 18 | "canCreateTurboModule", 19 | MainApplicationTurboModuleManagerDelegate::canCreateTurboModule), 20 | }); 21 | } 22 | 23 | std::shared_ptr 24 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 25 | const std::string &name, 26 | const std::shared_ptr &jsInvoker) { 27 | // Not implemented yet: provide pure-C++ NativeModules here. 28 | return nullptr; 29 | } 30 | 31 | std::shared_ptr 32 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 33 | const std::string &name, 34 | const JavaTurboModule::InitParams ¶ms) { 35 | return MainApplicationModuleProvider(name, params); 36 | } 37 | 38 | bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule( 39 | const std::string &name) { 40 | return getTurboModule(name, nullptr) != nullptr || 41 | getTurboModule(name, {.moduleName = name}) != nullptr; 42 | } 43 | 44 | } // namespace react 45 | } // namespace facebook 46 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | namespace facebook { 8 | namespace react { 9 | 10 | class MainApplicationTurboModuleManagerDelegate 11 | : public jni::HybridClass< 12 | MainApplicationTurboModuleManagerDelegate, 13 | TurboModuleManagerDelegate> { 14 | public: 15 | // Adapt it to the package you used for your Java class. 16 | static constexpr auto kJavaDescriptor = 17 | "Lcom/demo/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;"; 18 | 19 | static jni::local_ref initHybrid(jni::alias_ref); 20 | 21 | static void registerNatives(); 22 | 23 | std::shared_ptr getTurboModule( 24 | const std::string &name, 25 | const std::shared_ptr &jsInvoker) override; 26 | std::shared_ptr getTurboModule( 27 | const std::string &name, 28 | const JavaTurboModule::InitParams ¶ms) override; 29 | 30 | /** 31 | * Test-only method. Allows user to verify whether a TurboModule can be 32 | * created by instances of this class. 33 | */ 34 | bool canCreateTurboModule(const std::string &name); 35 | }; 36 | 37 | } // namespace react 38 | } // namespace facebook 39 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainComponentsRegistry.cpp: -------------------------------------------------------------------------------- 1 | #include "MainComponentsRegistry.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace facebook { 10 | namespace react { 11 | 12 | MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {} 13 | 14 | std::shared_ptr 15 | MainComponentsRegistry::sharedProviderRegistry() { 16 | auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry(); 17 | 18 | // Autolinked providers registered by RN CLI 19 | rncli_registerProviders(providerRegistry); 20 | 21 | // Custom Fabric Components go here. You can register custom 22 | // components coming from your App or from 3rd party libraries here. 23 | // 24 | // providerRegistry->add(concreteComponentDescriptorProvider< 25 | // AocViewerComponentDescriptor>()); 26 | return providerRegistry; 27 | } 28 | 29 | jni::local_ref 30 | MainComponentsRegistry::initHybrid( 31 | jni::alias_ref, 32 | ComponentFactory *delegate) { 33 | auto instance = makeCxxInstance(delegate); 34 | 35 | auto buildRegistryFunction = 36 | [](EventDispatcher::Weak const &eventDispatcher, 37 | ContextContainer::Shared const &contextContainer) 38 | -> ComponentDescriptorRegistry::Shared { 39 | auto registry = MainComponentsRegistry::sharedProviderRegistry() 40 | ->createComponentDescriptorRegistry( 41 | {eventDispatcher, contextContainer}); 42 | 43 | auto mutableRegistry = 44 | std::const_pointer_cast(registry); 45 | 46 | mutableRegistry->setFallbackComponentDescriptor( 47 | std::make_shared( 48 | ComponentDescriptorParameters{ 49 | eventDispatcher, contextContainer, nullptr})); 50 | 51 | return registry; 52 | }; 53 | 54 | delegate->buildRegistryFunction = buildRegistryFunction; 55 | return instance; 56 | } 57 | 58 | void MainComponentsRegistry::registerNatives() { 59 | registerHybrid({ 60 | makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid), 61 | }); 62 | } 63 | 64 | } // namespace react 65 | } // namespace facebook 66 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainComponentsRegistry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | class MainComponentsRegistry 12 | : public facebook::jni::HybridClass { 13 | public: 14 | // Adapt it to the package you used for your Java class. 15 | constexpr static auto kJavaDescriptor = 16 | "Lcom/demo/newarchitecture/components/MainComponentsRegistry;"; 17 | 18 | static void registerNatives(); 19 | 20 | MainComponentsRegistry(ComponentFactory *delegate); 21 | 22 | private: 23 | static std::shared_ptr 24 | sharedProviderRegistry(); 25 | 26 | static jni::local_ref initHybrid( 27 | jni::alias_ref, 28 | ComponentFactory *delegate); 29 | }; 30 | 31 | } // namespace react 32 | } // namespace facebook 33 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/OnLoad.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "MainApplicationTurboModuleManagerDelegate.h" 3 | #include "MainComponentsRegistry.h" 4 | 5 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { 6 | return facebook::jni::initialize(vm, [] { 7 | facebook::react::MainApplicationTurboModuleManagerDelegate:: 8 | registerNatives(); 9 | facebook::react::MainComponentsRegistry::registerNatives(); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | demo 3 | 4 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "33.0.0" 6 | minSdkVersion = 21 7 | compileSdkVersion = 33 8 | targetSdkVersion = 33 9 | 10 | if (System.properties['os.arch'] == "aarch64") { 11 | // For M1 Users we need to use the NDK 24 which added support for aarch64 12 | ndkVersion = "24.0.8215888" 13 | } else { 14 | // Otherwise we default to the side-by-side NDK version from AGP. 15 | ndkVersion = "21.4.7075529" 16 | } 17 | } 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | dependencies { 23 | classpath("com.android.tools.build:gradle:7.2.1") 24 | classpath("com.facebook.react:react-native-gradle-plugin") 25 | classpath("de.undercouch:gradle-download-task:5.0.1") 26 | // NOTE: Do not place your application dependencies here; they belong 27 | // in the individual module build.gradle files 28 | } 29 | } 30 | 31 | allprojects { 32 | repositories { 33 | maven { 34 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 35 | url("$rootDir/../node_modules/react-native/android") 36 | } 37 | maven { 38 | // Android JSC is installed from npm 39 | url("$rootDir/../node_modules/jsc-android/dist") 40 | } 41 | mavenCentral { 42 | // We don't want to fetch react-native from Maven Central as there are 43 | // older versions over there. 44 | content { 45 | excludeGroup "com.facebook.react" 46 | } 47 | } 48 | google() 49 | maven { url 'https://www.jitpack.io' } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /demo/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.125.0 29 | 30 | # Use this property to specify which architecture you want to build. 31 | # You can also override it from the CLI using 32 | # ./gradlew -PreactNativeArchitectures=x86_64 33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 34 | 35 | # Use this property to enable support to the new architecture. 36 | # This will allow you to use TurboModules and the Fabric render in 37 | # your application. You should enable this flag either if you want 38 | # to write custom TurboModules/Fabric components OR use libraries that 39 | # are providing them. 40 | newArchEnabled=false 41 | -------------------------------------------------------------------------------- /demo/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /demo/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /demo/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/react-native-gradle-plugin') 5 | 6 | if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") { 7 | include(":ReactAndroid") 8 | project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid') 9 | include(":ReactAndroid:hermes-engine") 10 | project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine') 11 | } 12 | -------------------------------------------------------------------------------- /demo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "displayName": "demo" 4 | } -------------------------------------------------------------------------------- /demo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './App'; 7 | import AppWithNavigation from './AppWithNavigation'; 8 | import {name as appName} from './app.json'; 9 | 10 | const snapshots = true; 11 | const checkNavigation = false 12 | 13 | if (snapshots) { 14 | require('./indexSnapshot'); 15 | } else if (checkNavigation) { 16 | AppRegistry.registerComponent(appName, () => AppWithNavigation); 17 | } else { 18 | AppRegistry.registerComponent(appName, () => App); 19 | } 20 | -------------------------------------------------------------------------------- /demo/indexSnapshot.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, Text, View } from 'react-native'; 3 | import { WebView } from 'react-native-webview'; 4 | import { registerSnapshot, runSnapshots, Snapshot } from 'pixels-catcher'; 5 | import { NavigationContainer } from '@react-navigation/native'; 6 | import { createStackNavigator } from '@react-navigation/stack'; 7 | 8 | import App from './App'; 9 | import { HomeScreen } from './AppWithNavigation'; 10 | import { name as appName } from './app.json'; 11 | 12 | const baseUrl = Platform.select({ 13 | // Put real IP of your server to run on real device 14 | android: 'http://10.0.2.2:3000', 15 | ios: 'http://127.0.0.1:3000', 16 | }); 17 | const useFailedTest = false; 18 | 19 | const appSnapshot = false; 20 | 21 | if (appSnapshot) { 22 | registerSnapshot( 23 | class SnapshotClass extends Snapshot { 24 | static snapshotName = 'AppSnapshot'; 25 | 26 | componentDidMount() { 27 | setTimeout(() => { 28 | // delay for rendering images 29 | this.props.onReady(); 30 | }, 1000); 31 | } 32 | 33 | renderContent() { 34 | return ; 35 | } 36 | }, 37 | ); 38 | } 39 | 40 | registerSnapshot( 41 | class SnapshotClass extends Snapshot { 42 | static snapshotName = 'HomeScreen'; 43 | 44 | renderContent() { 45 | return ; 46 | } 47 | }, 48 | ); 49 | 50 | if (useFailedTest) { 51 | registerSnapshot( 52 | class SnapshotClass extends Snapshot { 53 | static snapshotName = 'AppSnapshotWithWrongRefImg'; 54 | 55 | renderContent() { 56 | return ; 57 | } 58 | }, 59 | ); 60 | } 61 | 62 | registerSnapshot( 63 | class SnapshotClass extends Snapshot { 64 | static snapshotName = 'someComponent'; 65 | 66 | renderContent() { 67 | return ( 68 | 69 | Some component 70 | 71 | ); 72 | } 73 | }, 74 | ); 75 | 76 | const useWebView = false; 77 | 78 | if (useWebView) { 79 | registerSnapshot( 80 | class SnapshotClass extends Snapshot { 81 | static snapshotName = 'WebViewTest'; 82 | 83 | componentDidMount() { 84 | // override default componentDidMount from Snapshot to delay it 85 | // until WebView is loaded. onLoad from WebView is used 86 | } 87 | 88 | renderContent() { 89 | return ( 90 | { 96 | setTimeout(() => { 97 | this.props.onReady(); 98 | }, 50); 99 | }} 100 | /> 101 | ); 102 | } 103 | }, 104 | ); 105 | } 106 | 107 | registerSnapshot( 108 | class SnapshotClass extends Snapshot { 109 | static snapshotName = 'longContent'; 110 | 111 | renderContent() { 112 | return ( 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | ); 121 | } 122 | }, 123 | ); 124 | 125 | const Stack = createStackNavigator(); 126 | 127 | function getRootElement(SnapshotsContainer) { 128 | const RootElement = ({ children }) => ( 129 | 130 | 131 | 139 | 140 | 141 | ); 142 | return RootElement; 143 | } 144 | 145 | runSnapshots(appName, { baseUrl, getRootElement }); 146 | -------------------------------------------------------------------------------- /demo/ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, '12.4' 5 | install! 'cocoapods', :deterministic_uuids => false 6 | 7 | target 'demo' do 8 | config = use_native_modules! 9 | 10 | # Flags change depending on the env values. 11 | flags = get_default_flags() 12 | 13 | use_react_native!( 14 | :path => config[:reactNativePath], 15 | # Hermes is now enabled by default. Disable by setting this flag to false. 16 | # Upcoming versions of React Native may rely on get_default_flags(), but 17 | # we make it explicit here to aid in the React Native upgrade process. 18 | :hermes_enabled => true, 19 | :fabric_enabled => flags[:fabric_enabled], 20 | # Enables Flipper. 21 | # 22 | # Note that if you have use_frameworks! enabled, Flipper will not work and 23 | # you should disable the next line. 24 | # :flipper_configuration => FlipperConfiguration.enabled, 25 | # An absolute path to your application root. 26 | :app_path => "#{Pod::Config.instance.installation_root}/.." 27 | ) 28 | 29 | target 'demoTests' do 30 | inherit! :complete 31 | # Pods for testing 32 | end 33 | 34 | post_install do |installer| 35 | react_native_post_install( 36 | installer, 37 | # Set `mac_catalyst_enabled` to `true` in order to apply patches 38 | # necessary for Mac Catalyst builds 39 | :mac_catalyst_enabled => false 40 | ) 41 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /demo/ios/_xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /demo/ios/demo.xcodeproj/xcshareddata/xcschemes/demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /demo/ios/demo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/ios/demo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/ios/demo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /demo/ios/demo/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | #import 8 | 9 | #if RCT_NEW_ARCH_ENABLED 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | 17 | #import 18 | 19 | static NSString *const kRNConcurrentRoot = @"concurrentRoot"; 20 | 21 | @interface AppDelegate () { 22 | RCTTurboModuleManager *_turboModuleManager; 23 | RCTSurfacePresenterBridgeAdapter *_bridgeAdapter; 24 | std::shared_ptr _reactNativeConfig; 25 | facebook::react::ContextContainer::Shared _contextContainer; 26 | } 27 | @end 28 | #endif 29 | 30 | @implementation AppDelegate 31 | 32 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 33 | { 34 | RCTAppSetupPrepareApp(application); 35 | 36 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 37 | 38 | #if RCT_NEW_ARCH_ENABLED 39 | _contextContainer = std::make_shared(); 40 | _reactNativeConfig = std::make_shared(); 41 | _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); 42 | _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; 43 | bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; 44 | #endif 45 | 46 | NSDictionary *initProps = [self prepareInitialProps]; 47 | UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"demo", initProps); 48 | 49 | if (@available(iOS 13.0, *)) { 50 | rootView.backgroundColor = [UIColor systemBackgroundColor]; 51 | } else { 52 | rootView.backgroundColor = [UIColor whiteColor]; 53 | } 54 | 55 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 56 | UIViewController *rootViewController = [UIViewController new]; 57 | rootViewController.view = rootView; 58 | self.window.rootViewController = rootViewController; 59 | [self.window makeKeyAndVisible]; 60 | return YES; 61 | } 62 | 63 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. 64 | /// 65 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html 66 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). 67 | /// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`. 68 | - (BOOL)concurrentRootEnabled 69 | { 70 | // Switch this bool to turn on and off the concurrent root 71 | return true; 72 | } 73 | 74 | - (NSDictionary *)prepareInitialProps 75 | { 76 | NSMutableDictionary *initProps = [NSMutableDictionary new]; 77 | 78 | #ifdef RCT_NEW_ARCH_ENABLED 79 | initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]); 80 | #endif 81 | 82 | return initProps; 83 | } 84 | 85 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 86 | { 87 | #if DEBUG 88 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 89 | #else 90 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 91 | #endif 92 | } 93 | 94 | #if RCT_NEW_ARCH_ENABLED 95 | 96 | #pragma mark - RCTCxxBridgeDelegate 97 | 98 | - (std::unique_ptr)jsExecutorFactoryForBridge:(RCTBridge *)bridge 99 | { 100 | _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge 101 | delegate:self 102 | jsInvoker:bridge.jsCallInvoker]; 103 | return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager); 104 | } 105 | 106 | #pragma mark RCTTurboModuleManagerDelegate 107 | 108 | - (Class)getModuleClassFromName:(const char *)name 109 | { 110 | return RCTCoreModulesClassProvider(name); 111 | } 112 | 113 | - (std::shared_ptr)getTurboModule:(const std::string &)name 114 | jsInvoker:(std::shared_ptr)jsInvoker 115 | { 116 | return nullptr; 117 | } 118 | 119 | - (std::shared_ptr)getTurboModule:(const std::string &)name 120 | initParams: 121 | (const facebook::react::ObjCTurboModule::InitParams &)params 122 | { 123 | return nullptr; 124 | } 125 | 126 | - (id)getModuleInstanceFromClass:(Class)moduleClass 127 | { 128 | return RCTAppSetupDefaultModuleFromClass(moduleClass); 129 | } 130 | 131 | #endif 132 | 133 | @end 134 | -------------------------------------------------------------------------------- /demo/ios/demo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /demo/ios/demo/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/ios/demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | demo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | localhost 32 | 33 | NSExceptionAllowsInsecureHTTPLoads 34 | 35 | 36 | 37 | 38 | NSLocationWhenInUseUsageDescription 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /demo/ios/demo/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/ios/demo/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/ios/demoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/ios/demoTests/demoTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface demoTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation demoTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /demo/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: true, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "cd ios/ && pod install", 7 | "android": "react-native run-android", 8 | "preinstall": "cd .. && rm -rf pixels-catcher-*.tgz && npm i && npm pack && mv pixels-catcher-*.tgz pixels-catcher.tgz", 9 | "ios": "react-native run-ios", 10 | "start": "react-native start", 11 | "test": "jest", 12 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx" 13 | }, 14 | "dependencies": { 15 | "@react-navigation/native": "^6.0.12", 16 | "@react-navigation/stack": "^6.2.3", 17 | "react": "18.1.0", 18 | "react-native": "0.70.0", 19 | "react-native-gesture-handler": "^2.6.0", 20 | "react-native-safe-area-context": "^4.3.3", 21 | "react-native-save-view": "^0.2.3", 22 | "react-native-screens": "^3.17.0", 23 | "react-native-webview": "^11.23.1" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.12.9", 27 | "@babel/runtime": "^7.12.5", 28 | "@react-native-community/eslint-config": "^2.0.0", 29 | "@tsconfig/react-native": "^2.0.2", 30 | "@types/jest": "^26.0.23", 31 | "@types/react-native": "^0.70.0", 32 | "@types/react-test-renderer": "^18.0.0", 33 | "@typescript-eslint/eslint-plugin": "^5.37.0", 34 | "@typescript-eslint/parser": "^5.37.0", 35 | "babel-jest": "^26.6.3", 36 | "eslint": "^7.32.0", 37 | "jest": "^26.6.3", 38 | "metro-react-native-babel-preset": "^0.72.1", 39 | "pixels-catcher": "../pixels-catcher.tgz", 40 | "react-test-renderer": "18.1.0", 41 | "typescript": "^4.8.3" 42 | }, 43 | "jest": { 44 | "preset": "react-native", 45 | "moduleFileExtensions": [ 46 | "ts", 47 | "tsx", 48 | "js", 49 | "jsx", 50 | "json", 51 | "node" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo/pixels-catcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "android": { 3 | "activityName": "com.demo.MainActivity", 4 | "deviceName": "Nexus_5X", 5 | "packageName": "com.demo", 6 | "snapshotsPath": "./snapshots/android", 7 | "dev": { 8 | "deviceParams": [ 9 | "-no-audio", 10 | "-no-snapshot" 11 | ] 12 | }, 13 | "debug": { 14 | "deviceParams": [ 15 | "-no-audio", 16 | "-no-snapshot" 17 | ], 18 | "canStopDevice": false, 19 | "appFile": "./android/app/build/outputs/apk/debug/app-debug.apk" 20 | }, 21 | "test": { 22 | "port": 3000, 23 | "canStopDevice": false, 24 | "deviceParams": [ 25 | "-no-audio", 26 | "-no-snapshot" 27 | ], 28 | "appFile": "./android/app/build/outputs/apk/debug/app-debug.apk" 29 | }, 30 | "release": { 31 | "deviceParams": [ 32 | "-no-audio", 33 | "-no-snapshot", 34 | "-no-window" 35 | ], 36 | "appFile": "./android/app/build/outputs/apk/release/app-release.apk" 37 | } 38 | }, 39 | "ios": { 40 | "deviceName": "iPhone 14 Plus", 41 | "packageName": "org.reactjs.native.example.demo", 42 | "snapshotsPath": "./snapshots/ios", 43 | "dev": {}, 44 | "debug": { 45 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app" 46 | }, 47 | "test": { 48 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app", 49 | "canStopDevice": false 50 | }, 51 | "testPort": {}, 52 | "testPort2": { 53 | "port": 3001 54 | }, 55 | "debugIphone6": { 56 | "deviceName": "iPhone 6", 57 | "snapshotsPath": "./snapshots/iPhone6", 58 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app" 59 | } 60 | }, 61 | "logLevel": "v", 62 | "timeout": 30000 63 | } 64 | -------------------------------------------------------------------------------- /demo/run_android_debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | export BUNDLE_IN_DEBUG="true" 7 | 8 | cd android 9 | rm -rf build .gradle/ app/build 10 | ./gradlew assembleDebug 11 | cd .. 12 | 13 | ./node_modules/.bin/pixels-catcher android debug 14 | -------------------------------------------------------------------------------- /demo/run_android_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | 7 | cd android 8 | rm -rf build .gradle/ app/build 9 | ./gradlew assembleRelease 10 | cd .. 11 | 12 | ./node_modules/.bin/pixels-catcher android release 13 | -------------------------------------------------------------------------------- /demo/run_android_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | export BUNDLE_IN_DEBUG="true" 7 | 8 | cd android 9 | rm -rf build .gradle/ app/build 10 | ./gradlew assembleDebug 11 | cd .. 12 | 13 | ../node_modules/.bin/flow-node ../src/runner/cli.js android test 14 | -------------------------------------------------------------------------------- /demo/run_ios_debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | BUILD_PATH="./build" 11 | rm -rf $BUILD_PATH 12 | 13 | xcrun xcodebuild \ 14 | -scheme demo \ 15 | -workspace demo.xcworkspace \ 16 | -configuration Debug \ 17 | -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.2' \ 18 | -derivedDataPath $BUILD_PATH \ 19 | ENTRY_FILE="indexSnapshot.js" \ 20 | build 21 | 22 | cd .. 23 | 24 | ./node_modules/.bin/pixels-catcher ios debug 25 | -------------------------------------------------------------------------------- /demo/run_ios_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | echo "ERROR: Not implemented. Requires https" 11 | -------------------------------------------------------------------------------- /demo/run_ios_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | BUILD_PATH="./build" 11 | rm -rf $BUILD_PATH 12 | 13 | xcrun xcodebuild \ 14 | -scheme demo \ 15 | -workspace demo.xcworkspace \ 16 | -configuration Debug \ 17 | -destination 'platform=iOS Simulator,name=iPhone 14 Plus,OS=13.3' \ 18 | -derivedDataPath $BUILD_PATH \ 19 | ENTRY_FILE="indexSnapshot.js" \ 20 | build 21 | 22 | cd .. 23 | 24 | ../node_modules/.bin/flow-node ../src/runner/cli.js ios test 25 | -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/someComponent.png -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | { 3 | "extends": "@tsconfig/react-native/tsconfig.json", /* Recommended React Native TSConfig base */ 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 6 | 7 | /* Completeness */ 8 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixels-catcher", 3 | "version": "0.13.1", 4 | "description": "UI snapshot testing for React Native", 5 | "main": "lib/client/index.js", 6 | "scripts": { 7 | "demo": "cd demo && ../node_modules/.bin/flow-node ../cli.js android debug", 8 | "lint": "eslint --ext .js --ext .ts --ext .tsx ./src/", 9 | "build": "./node_modules/.bin/tsc -p src/client && ./node_modules/.bin/tsc -p src/runner", 10 | "postbuild": "npm run assets", 11 | "prepare": "npm run build", 12 | "prepack": "npm run build", 13 | "check-types-client": "./node_modules/.bin/tsc --noEmit -p src/client", 14 | "check-types-runner": "./node_modules/.bin/tsc --noEmit -p src/runner", 15 | "check-types": "npm run check-types-client && npm run check-types-runner", 16 | "assets": "cp ./src/runner/server/dummy.png ./lib/runner/server/" 17 | }, 18 | "bin": "./lib/runner/cli.js", 19 | "files": [ 20 | "/lib", 21 | "/src/*.js", 22 | "/src/utils/*.js" 23 | ], 24 | "keywords": [ 25 | "react-native view android iOS UI screenshot snapshot testing" 26 | ], 27 | "author": "Maksym Rusynyk ", 28 | "license": "MIT", 29 | "engines": { 30 | "node": ">=14.*" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+ssh://git@github.com/rumax/react-native-PixelsCatcher.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/rumax/react-native-PixelsCatcher/issues" 38 | }, 39 | "homepage": "https://github.com/rumax/react-native-PixelsCatcher#readme", 40 | "dependencies": { 41 | "formidable": "^1.2.2", 42 | "pixelmatch": "^5.2.1", 43 | "pngjs": "^6.0.0", 44 | "react-native-save-view": "^0.2.3" 45 | }, 46 | "devDependencies": { 47 | "@types/formidable": "^1.2.2", 48 | "@types/node": "^18.7.16", 49 | "@types/pixelmatch": "^5.2.3", 50 | "@types/pngjs": "^6.0.0", 51 | "@types/react-native": "^0.64.6", 52 | "@typescript-eslint/eslint-plugin": "^4.25.0", 53 | "@typescript-eslint/parser": "^4.25.0", 54 | "eslint": "^7.27.0", 55 | "eslint-config-airbnb": "^18.2.1", 56 | "eslint-plugin-import": "^2.23.3", 57 | "eslint-plugin-jsx-a11y": "^6.4.1", 58 | "eslint-plugin-react": "^7.23.2", 59 | "eslint-plugin-react-hooks": "^4.2.0", 60 | "react": "17.0.1", 61 | "react-native": "0.64.1", 62 | "react-test-renderer": "17.0.1", 63 | "typescript": "^4.2.4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /res/azureDevops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/res/azureDevops.png -------------------------------------------------------------------------------- /res/testResults.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/res/testResults.png -------------------------------------------------------------------------------- /scripts/create_android_emulator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DEVICE="Nexus 5X" 4 | DEVICE_NAME="Nexus_5X" 5 | SDK="system-images;android-27;default;x86_64" 6 | 7 | # Install AVD files 8 | echo "y" | $ANDROID_HOME/tools/bin/sdkmanager \ 9 | --install $SDK 10 | 11 | # Create emulator 12 | echo "no" | $ANDROID_HOME/tools/bin/avdmanager \ 13 | create avd \ 14 | -n "$DEVICE_NAME" \ 15 | --device "$DEVICE" \ 16 | -k $SDK \ 17 | --force 18 | 19 | $ANDROID_HOME/emulator/emulator -list-avds 20 | 21 | echo "Starting emulator" 22 | 23 | # Start emulator in background 24 | nohup $ANDROID_HOME/emulator/emulator \ 25 | -avd $DEVICE_NAME \ 26 | -no-snapshot > /dev/null 2>&1 & 27 | $ANDROID_HOME/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82' 28 | 29 | $ANDROID_HOME/platform-tools/adb devices 30 | 31 | echo "Emulator started" 32 | -------------------------------------------------------------------------------- /src/client/Snapshot.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | // eslint-disable-next-line no-use-before-define 8 | import React, { Component } from 'react'; 9 | import { InteractionManager, ScrollView } from 'react-native'; 10 | 11 | import log from './utils/log'; 12 | 13 | type Props = { onReady: () => void }; 14 | 15 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOT'; 16 | const ERROR_NO_IMPLEMENTED = 17 | 'Not implemented. Should be implemented by actual snapshot'; 18 | 19 | export default class Snapshot extends Component { 20 | // Should be implemented by actual snapshot 21 | static snapshotName: string = ''; 22 | 23 | componentDidMount(): void { 24 | log.v(TAG, 'Awaiting interaction'); 25 | const startTime = new Date().getTime(); 26 | InteractionManager.runAfterInteractions(() => { 27 | const time = new Date().getTime() - startTime; 28 | log.v(TAG, `Interaction completed in ${time} milliseconds`); 29 | global.setTimeout(() => { 30 | this.props.onReady(); 31 | }, 50); 32 | }); 33 | } 34 | 35 | renderContent(): React.ReactNode { 36 | log.e(TAG, ERROR_NO_IMPLEMENTED); 37 | throw new Error(ERROR_NO_IMPLEMENTED); 38 | } 39 | 40 | render(): React.ReactNode { 41 | const content = this.renderContent(); 42 | return ( 43 | 44 | {content} 45 | 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/client/SnapshotsContainer.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | // eslint-disable-next-line no-use-before-define 8 | import React, { Component } from 'react'; 9 | import { View, Text } from 'react-native'; 10 | // @ts-ignore 11 | import SaveView from 'react-native-save-view'; 12 | 13 | import { getNextSnapshot } from './snapshotsManager'; 14 | import compareToReference from './utils/compareToReference'; 15 | import log from './utils/log'; 16 | import network from './utils/network'; 17 | 18 | import type Snapshot from './Snapshot'; 19 | 20 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOTS_CONTAINER'; 21 | 22 | type NoProps = Record; 23 | 24 | type State = { 25 | isReady: boolean, 26 | ActiveSnapshot: typeof Snapshot | null, 27 | }; 28 | 29 | export default class SnapshotsContainer extends Component { 30 | _viewRef: any; 31 | 32 | _testStartedAt: number = new Date().getTime(); 33 | 34 | _renderStartedAt: number = 0; 35 | 36 | constructor(props: Record) { 37 | super(props); 38 | 39 | this.state = { 40 | ActiveSnapshot: null, 41 | isReady: false, 42 | }; 43 | } 44 | 45 | shouldComponentUpdate( 46 | nextProps: NoProps, 47 | nextState: Readonly, 48 | ): boolean { 49 | return this.state.ActiveSnapshot !== nextState.ActiveSnapshot 50 | || this.state.isReady !== nextState.isReady; 51 | } 52 | 53 | componentDidMount(): void { 54 | requestAnimationFrame(() => { 55 | this._startTesting(); 56 | }); 57 | } 58 | 59 | render(): React.ReactNode { 60 | const { isReady, ActiveSnapshot } = this.state; 61 | 62 | if (!isReady) { 63 | return ( 64 | 65 | Initializing tests 66 | 67 | ); 68 | } 69 | 70 | if (!ActiveSnapshot) { 71 | log.i(TAG, 'No active snapshot'); 72 | return null; 73 | } 74 | 75 | log.i(TAG, `rendering snapshot [${ActiveSnapshot.snapshotName}]`); 76 | 77 | this._renderStartedAt = new Date().getTime(); 78 | 79 | return ; 80 | } 81 | 82 | _startTesting = async (): Promise => { 83 | await network.initTests(); 84 | const ActiveSnapshot = getNextSnapshot(); 85 | if (!ActiveSnapshot) { 86 | this._endOfTest(); 87 | log.e(TAG, 'No snapshots registered'); 88 | this._endOfTest(); 89 | return; 90 | } 91 | log.v(TAG, 'Start testing'); 92 | this.setState({ 93 | ActiveSnapshot, 94 | isReady: true, 95 | }); 96 | }; 97 | 98 | _onRef = (ref: any): void => { 99 | this._viewRef = ref; 100 | }; 101 | 102 | _onSnapshotReady = (): void => { 103 | const renderTime = new Date().getTime() - this._renderStartedAt; 104 | log.v(TAG, 'Snapshot ready'); 105 | 106 | setTimeout(async () => { 107 | const ref = this._viewRef; 108 | 109 | if (!ref) { 110 | const errorMessage = 'Something when wrong, no ref to the component'; 111 | log.e(TAG, errorMessage); 112 | throw new Error(errorMessage); 113 | } 114 | 115 | const { ActiveSnapshot } = this.state; 116 | const name = ActiveSnapshot?.snapshotName; 117 | 118 | log.v(TAG, `snapshotName: [${name || '-'}]`); 119 | 120 | if (!name) { 121 | const errorMessage = 'Snapshot should has a proper name'; 122 | 123 | log.w(TAG, errorMessage); 124 | network.reportTest({ 125 | name: '-', 126 | failure: errorMessage, 127 | time: this._getTestExecutionTime(), 128 | }); 129 | this._nextSnapshot(); 130 | 131 | return; 132 | } 133 | 134 | let failure: any; 135 | 136 | try { 137 | log.v(TAG, '++SaveView.save'); 138 | const base64 = await SaveView.saveToPNGBase64(ref); 139 | log.v(TAG, `--SaveView.save, size is ${base64.length}`); 140 | 141 | failure = await compareToReference(name, base64); 142 | if (failure) { 143 | log.e(TAG, `Snapshot ${name} failed: ${failure}`); 144 | } else { 145 | log.i(TAG, `Snapshot ${name} passed`); 146 | } 147 | } catch (err: unknown) { 148 | failure = `Failed to save view: ${ 149 | err instanceof Error ? err.message : 'Unknown error' 150 | }`; 151 | log.e(TAG, failure); 152 | } 153 | 154 | log.v(TAG, `Reporting [${name}], failure: [${failure}]`); 155 | try { 156 | await network.reportTest({ 157 | name, 158 | failure, 159 | time: this._getTestExecutionTime(), 160 | renderTime, 161 | }); 162 | } catch (err) { 163 | log.e(TAG, 'Failed to report test', err); 164 | } 165 | this._nextSnapshot(); 166 | }, 50); 167 | }; 168 | 169 | _getTestExecutionTime(): number { 170 | const time = new Date().getTime() - this._testStartedAt; 171 | log.v(TAG, `Execution time: ${time}`); 172 | return time; 173 | } 174 | 175 | _nextSnapshot(): void { 176 | log.v(TAG, 'Trying to get next snapshot'); 177 | const NextSnapshot = getNextSnapshot(); 178 | 179 | if (!NextSnapshot) { 180 | log.v('No more snapshots left, exit testing'); 181 | this._endOfTest(); 182 | return; 183 | } 184 | 185 | log.v(`Switching to next snapshot ${NextSnapshot.snapshotName}`); 186 | this._testStartedAt = new Date().getTime(); 187 | this.setState({ ActiveSnapshot: NextSnapshot }); 188 | } 189 | 190 | _endOfTest(): void { 191 | network.endOfTests({ message: 'All tests completed' }); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/client/__tests__/Snapshot.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { View } from 'react-native'; 5 | import renderer from 'react-test-renderer'; 6 | 7 | import Snapshot from '../Snapshot'; 8 | 9 | jest.mock('../utils/log', () => ({ e: () => {}, v: () => {} })); 10 | global.console.error = () => {}; 11 | 12 | describe('Snapshot component', () => { 13 | const onReadyMock = jest.fn(); 14 | 15 | it('throws exception if renderContent is not implemented', () => { 16 | let exception; 17 | 18 | try { 19 | renderer.create(); 20 | } catch (err) { 21 | exception = err; 22 | } 23 | 24 | expect(exception).toMatchSnapshot(); 25 | }); 26 | 27 | it('renders snapshot component', () => { 28 | class SnapshotClass extends Snapshot { 29 | static snapshotName = 'AppSnapshot'; 30 | 31 | renderContent() { 32 | return ( 33 | 34 | ); 35 | } 36 | } 37 | 38 | const tree = renderer.create(); 39 | 40 | expect(tree).toMatchSnapshot(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/client/__tests__/SnapshotsContainer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import SnapshotsContainer from '../SnapshotsContainer'; 7 | import network from '../utils/network'; 8 | import log from '../utils/log'; 9 | import { getNextSnapshot } from '../snapshotsManager'; 10 | 11 | jest.mock('../utils/log', () => ({ 12 | i: jest.fn(), 13 | v: jest.fn(), 14 | w: jest.fn(), 15 | e: jest.fn(), 16 | })); 17 | jest.mock('../utils/network', () => ({ endOfTests: jest.fn() })); 18 | jest.mock('../snapshotsManager', () => ({ getNextSnapshot: jest.fn() })); 19 | 20 | describe('SnapshotsContainer', () => { 21 | beforeEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | it('Renders empty view and reports it to the server if no snapshots are registered', () => { 26 | const tree = renderer.create(); 27 | 28 | expect(tree).toMatchSnapshot(); 29 | expect(network.endOfTests).toHaveBeenCalledTimes(1); 30 | expect(log.e).toMatchSnapshot('loggin error'); 31 | }); 32 | 33 | it('Renders registered snapshot', () => { 34 | getNextSnapshot.mockImplementationOnce(() => 'SomeSnapshot'); 35 | const tree = renderer.create(); 36 | 37 | expect(tree).toMatchSnapshot(); 38 | expect(network.endOfTests).toHaveBeenCalledTimes(0); 39 | expect(log.v).toMatchSnapshot('render snapshot reported'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/Snapshot.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot component renders snapshot component 1`] = ` 4 | 12 | 13 | 14 | 15 | 16 | `; 17 | 18 | exports[`Snapshot component renders snpashot component 1`] = ` 19 | 27 | 28 | 29 | 30 | 31 | `; 32 | 33 | exports[`Snapshot component throws exception if renderContent is not implemented 1`] = `[Error: Not implemented. Should be implemented by actual snapshot]`; 34 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/SnapshotsContainer.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SnapshotsContainer Renders empty view and reports it to the server if no snapshots are registered 1`] = `null`; 4 | 5 | exports[`SnapshotsContainer Renders empty view and reports it to the server if no snapshots are registered: loggin error 1`] = ` 6 | [MockFunction] { 7 | "calls": Array [ 8 | Array [ 9 | "PIXELS_CATCHER::APP::SNAPSHOTS_CONTAINER", 10 | "No snapshots registered", 11 | ], 12 | ], 13 | "results": Array [ 14 | Object { 15 | "type": "return", 16 | "value": undefined, 17 | }, 18 | ], 19 | } 20 | `; 21 | 22 | exports[`SnapshotsContainer Renders registered snapshot 1`] = ` 23 | 26 | `; 27 | 28 | exports[`SnapshotsContainer Renders registered snapshot: render snapshot reported 1`] = `[MockFunction]`; 29 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot component register component and start snapshots: registerComponent 1`] = ` 4 | Array [ 5 | Array [ 6 | "appName", 7 | [Function], 8 | ], 9 | ] 10 | `; 11 | 12 | exports[`Snapshot component register component with custom IP and start snapshots: registerComponent 1`] = ` 13 | Array [ 14 | Array [ 15 | "appName", 16 | [Function], 17 | ], 18 | ] 19 | `; 20 | -------------------------------------------------------------------------------- /src/client/__tests__/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { AppRegistry, View } from 'react-native'; 5 | 6 | import { runSnapshots, Snapshot, registerSnapshot } from '../index'; 7 | import network from '../utils/network'; 8 | 9 | jest.mock('react-native/Libraries/ReactNative/AppRegistry', () => ({ registerComponent: jest.fn() })); 10 | jest.mock('../utils/network', () => ({ setBaseUrl: jest.fn() })); 11 | jest.mock('../SnapshotsContainer', () => 'SnapshotsContainer'); 12 | jest.mock('../utils/log', () => ({ i: jest.fn() })); 13 | jest.mock('../Snapshot', () => ({ default: 'Snapshot' })); 14 | jest.mock('../snapshotsManager', () => ({ registerSnapshot: jest.fn() })); 15 | 16 | describe('Snapshot component', () => { 17 | const appName = 'appName'; 18 | 19 | afterEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | it('provides Snapshot component', () => { 24 | expect(Snapshot).toBe('Snapshot'); 25 | }); 26 | 27 | it('allows to register snapshot', () => { 28 | registerSnapshot(); 29 | expect(registerSnapshot).toHaveBeenCalledTimes(1); 30 | }); 31 | 32 | it('register component and start snapshots', () => { 33 | runSnapshots(appName); 34 | 35 | expect(AppRegistry.registerComponent).toHaveBeenCalledTimes(1); 36 | expect(AppRegistry.registerComponent.mock.calls) 37 | .toMatchSnapshot('registerComponent'); 38 | expect(AppRegistry.registerComponent.mock.calls[0][1]()) 39 | .toBe('SnapshotsContainer'); 40 | expect(network.setBaseUrl).toHaveBeenCalledTimes(0); 41 | }); 42 | 43 | it('register component with custom IP and start snapshots', () => { 44 | const baseUrl = 'baseUrl'; 45 | runSnapshots(appName, { baseUrl }); 46 | 47 | expect(AppRegistry.registerComponent).toHaveBeenCalledTimes(1); 48 | expect(AppRegistry.registerComponent.mock.calls) 49 | .toMatchSnapshot('registerComponent'); 50 | expect(AppRegistry.registerComponent.mock.calls[0][1]()) 51 | .toBe('SnapshotsContainer'); 52 | expect(network.setBaseUrl).toHaveBeenCalledTimes(1); 53 | expect(network.setBaseUrl).toHaveBeenCalledWith(baseUrl); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/client/__tests__/snapshotsManager.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { View, Text } from 'react-native'; 5 | 6 | import { registerSnapshot, getNextSnapshot } from '../snapshotsManager'; 7 | import Snapshot from '../Snapshot'; 8 | 9 | jest.mock('../utils/log', () => ({ i: jest.fn() })); 10 | 11 | describe('snapshotsManager', () => { 12 | it('Default snapshots list is empty', () => { 13 | const nextSnapshot = getNextSnapshot(); 14 | expect(nextSnapshot).toBe(undefined); 15 | }); 16 | 17 | it('register snapshot and get it', () => { 18 | class SnapshotClass extends Snapshot { 19 | static snapshotName = 'someComponent'; 20 | 21 | renderContent() { 22 | return ( 23 | Some component 24 | ); 25 | } 26 | } 27 | registerSnapshot(SnapshotClass); 28 | 29 | expect(getNextSnapshot()).toBe(SnapshotClass); 30 | expect(getNextSnapshot()).toBe(undefined); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type React from 'react'; 8 | import { AppRegistry } from 'react-native'; 9 | 10 | import log from './utils/log'; 11 | import network from './utils/network'; 12 | 13 | import SnapshotsContainer from './SnapshotsContainer'; 14 | 15 | export const Snapshot = require('./Snapshot').default; 16 | 17 | export const { registerSnapshot } = require('./snapshotsManager'); 18 | 19 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOT'; 20 | 21 | export type GetRootElementType = (element: React.ComponentType) => 22 | React.ComponentType 23 | 24 | interface ConfigType { 25 | baseUrl?: string; 26 | 27 | /** 28 | * Callback to override AppRegistry.registerComponent with custom 29 | * implementation. Can be used for projects with react-native-navigation 30 | * @param snapshot Current snapshot 31 | */ 32 | // eslint-disable-next-line no-unused-vars 33 | registerComponent?: (snapshot: typeof SnapshotsContainer) => void; 34 | 35 | /** 36 | * Root element. Allows to wrap the SnapshotsContainer, which could be 37 | * useful to implement some providers, for example for react navigation. 38 | * Example: 39 | * 40 | * import { NavigationContainer } from '@react-navigation/native'; 41 | * import { createStackNavigator } from '@react-navigation/stack'; 42 | * 43 | * const Stack = createStackNavigator(); 44 | * 45 | * function getRootElement(SnapshotsContainer) { 46 | * const RootElement = ({children}) => ( 47 | * 48 | * 49 | * 53 | * 54 | * 55 | * ) 56 | * return RootElement; 57 | * } 58 | * 59 | * runSnapshots(appName, { baseUrl, getRootElement }); 60 | */ 61 | getRootElement?: GetRootElementType; 62 | } 63 | 64 | export const runSnapshots = (appName: string, config: ConfigType = {}): void => { 65 | log.i(TAG, `Run snapshots for ${appName}`); 66 | log.i(TAG, `Config is:\n ${JSON.stringify(config, null, 2)}`); 67 | 68 | if (config.baseUrl) { 69 | network.setBaseUrl(config.baseUrl); 70 | } 71 | 72 | if (config.registerComponent) { 73 | config.registerComponent(SnapshotsContainer); 74 | return; 75 | } 76 | 77 | if (config.getRootElement) { 78 | const RootElement = config.getRootElement(SnapshotsContainer); 79 | AppRegistry.registerComponent(appName, () => RootElement); 80 | return; 81 | } 82 | 83 | AppRegistry.registerComponent(appName, () => SnapshotsContainer); 84 | }; 85 | -------------------------------------------------------------------------------- /src/client/snapshotsManager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type Snapshot from './Snapshot'; 8 | import log from './utils/log'; 9 | import network from './utils/network'; 10 | 11 | const snapshots: Array = []; 12 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOTS_MANAGER'; 13 | 14 | 15 | export function registerSnapshot(Component: typeof Snapshot): void { 16 | log.i(TAG, `Registering snapshot [${Component.snapshotName}]`); 17 | snapshots.push(Component); 18 | network.registerTest(Component.snapshotName); 19 | } 20 | 21 | 22 | export function getNextSnapshot(): typeof Snapshot | undefined { 23 | const NextSnapshot = snapshots.shift(); 24 | return NextSnapshot; 25 | } 26 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es6", 5 | "declaration": true, 6 | "outDir": "../../lib/client", 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true 11 | }, 12 | "include": ["./**/*"], 13 | "exclude": [ 14 | "../../node_modules", 15 | "**/__tests__/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/client/utils/__tests__/__snapshots__/compareToReference.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`compareToReference Returns failure if HTTP status is not 200 1`] = `"Invalid status 404"`; 4 | 5 | exports[`compareToReference Returns failure if result is not OK 1`] = `"Files mismatch with 1 pixels"`; 6 | -------------------------------------------------------------------------------- /src/client/utils/__tests__/compareToReference.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import network from '../network'; 3 | import compareToReference from '../compareToReference'; 4 | 5 | jest.mock('../network', () => ({ postBase64: jest.fn() })); 6 | 7 | describe('compareToReference', () => { 8 | it('Returns failure if HTTP status is not 200', async () => { 9 | const snapshotName = 'snapshotName'; 10 | const base64 = 'base64 data'; 11 | 12 | network.postBase64.mockImplementationOnce(() => ({ status: 404 })); 13 | 14 | const failure = await compareToReference(snapshotName, base64); 15 | expect(failure).toMatchSnapshot(); 16 | }); 17 | 18 | it('Returns failure if result is not OK', async () => { 19 | const snapshotName = 'snapshotName'; 20 | const base64 = 'base64 data'; 21 | 22 | network.postBase64.mockImplementationOnce(() => ({ 23 | status: 200, 24 | json: async () => ({ 25 | result: 'ERROR', 26 | info: 'Files mismatch with 1 pixels', 27 | }), 28 | })); 29 | 30 | const failure = await compareToReference(snapshotName, base64); 31 | expect(failure).toMatchSnapshot(); 32 | }); 33 | 34 | it('Returns nothing if image matches the reference', async () => { 35 | const snapshotName = 'snapshotName'; 36 | const base64 = 'base64 data'; 37 | 38 | network.postBase64.mockImplementationOnce(() => ({ 39 | status: 200, 40 | json: async () => ({ 41 | result: 'OK', 42 | info: { differentPixelsCount: 0 }, 43 | }), 44 | })); 45 | 46 | const result = await compareToReference(snapshotName, base64); 47 | expect(result).toBe(undefined); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/client/utils/compareToReference.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import network from './network'; 8 | 9 | const compareToReference = async (snapshotName: string, base64: string): Promise => { 10 | const response: any = await network.postBase64({ 11 | base64, 12 | fileName: `${snapshotName}.png`, 13 | }); 14 | 15 | if (response.status !== 200) { 16 | return `Invalid status ${response.status}`; 17 | } 18 | 19 | const responseJSON = await response.json(); 20 | 21 | if (responseJSON.result !== 'OK') { 22 | return responseJSON.info; 23 | } 24 | 25 | return undefined; 26 | }; 27 | 28 | export default compareToReference; 29 | -------------------------------------------------------------------------------- /src/client/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import network from './network'; 8 | 9 | const consoleLog = global.console && global.console.log 10 | ? global.console.log : (): void => {}; 11 | 12 | type LogLevelType = 'v' | 'd' | 'i' | 'w' | 'e'; 13 | 14 | const serverLog = async (logLevel: LogLevelType, tag: string, ...args: any): Promise => { 15 | try { 16 | network.serverLog({ 17 | logLevel, 18 | tag, 19 | args, 20 | }); 21 | } catch (err) { 22 | if (__DEV__) { 23 | // eslint-disable-next-line no-console 24 | console.warn('ERROR:serverLog: ', err instanceof Error ? err.message : 'Unknown error', err); 25 | } 26 | } 27 | }; 28 | 29 | const log = { 30 | v: (tag: string, ...args: any): void => { 31 | consoleLog(tag, ...args); 32 | serverLog('v', tag, ...args); 33 | }, 34 | 35 | d: (tag: string, ...args: any): void => { 36 | consoleLog(tag, ...args); 37 | serverLog('d', tag, ...args); 38 | }, 39 | 40 | i: (tag: string, ...args: any): void => { 41 | consoleLog(tag, ...args); 42 | serverLog('i', tag, ...args); 43 | }, 44 | 45 | w: (tag: string, ...args: any): void => { 46 | consoleLog(`${tag} WARNING:`, ...args); 47 | serverLog('w', `${tag} WARNING:`, ...args); 48 | }, 49 | 50 | e: (tag: string, ...args: any): void => { 51 | consoleLog(`${tag} ERROR:`, ...args); 52 | serverLog('e', `${tag} ERROR:`, ...args); 53 | }, 54 | }; 55 | 56 | export default log; 57 | -------------------------------------------------------------------------------- /src/client/utils/network.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import { Platform } from 'react-native'; 8 | 9 | let baseUrl = Platform.select({ 10 | android: 'http://10.0.2.2:3000', 11 | ios: 'http://127.0.0.1:3000', 12 | }); 13 | 14 | type TestCaseType = { 15 | name: string, 16 | failure?: string, 17 | isSkipped?: boolean, 18 | time: number, 19 | renderTime?: number, 20 | }; 21 | 22 | const fetchRequest = async (url: string, body: Object): Promise => { 23 | const response = await fetch(url, { 24 | method: 'POST', 25 | headers: { 26 | Accept: 'application/json', 27 | 'Content-Type': 'application/json', 28 | }, 29 | body: JSON.stringify(body), 30 | }); 31 | 32 | return response; 33 | }; 34 | 35 | 36 | export default { 37 | 38 | setBaseUrl(url: string): void { 39 | baseUrl = url; 40 | }, 41 | 42 | 43 | initTests: async (): Promise => { 44 | await fetchRequest(`${baseUrl}/initTests`, {}); 45 | }, 46 | 47 | 48 | registerTest: async (name: string): Promise => { 49 | await fetchRequest(`${baseUrl}/registerTest`, { name }); 50 | }, 51 | 52 | 53 | postBase64: async (body: Object): Promise => { 54 | const response = await fetchRequest(`${baseUrl}/base64`, body); 55 | return response; 56 | }, 57 | 58 | 59 | serverLog: async (body: Object): Promise => { 60 | await fetchRequest(`${baseUrl}/log`, body); 61 | }, 62 | 63 | 64 | reportTest: async (testCase: TestCaseType): Promise => { 65 | await fetchRequest(`${baseUrl}/reportTest`, testCase); 66 | }, 67 | 68 | 69 | endOfTests: async (body: Object): Promise => { 70 | await fetchRequest(`${baseUrl}/endOfTests`, body); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/runner/TestsRunner.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import log from './utils/log'; 5 | import Reporter from './utils/Reporter'; 6 | import server from './server/server'; 7 | 8 | const TAG = 'PIXELS_CATCHER'; 9 | 10 | type TestsRunnerParamsType = 11 | { 12 | activityName: string, 13 | appFile: string, 14 | device: any, 15 | deviceName: string, 16 | deviceParams: string, 17 | isDevMode: boolean, 18 | locale: string, 19 | packageName: string, 20 | platform: 'ios' | 'android', 21 | port: number, 22 | snapshotsPath: string, 23 | testRunName: string, 24 | timeout: number, 25 | }; 26 | 27 | class TestsRunner { 28 | _activityName: string; 29 | 30 | _appFile: string; 31 | 32 | _appFileFullPath: string | void; 33 | 34 | _device: any; 35 | 36 | _deviceName: string; 37 | 38 | _deviceParams: Object; 39 | 40 | _isDevMode: boolean; 41 | 42 | _locale: string; 43 | 44 | _packageName: string; 45 | 46 | _platform: 'ios' | 'android'; 47 | 48 | _port: number; 49 | 50 | _reporter: Reporter; 51 | 52 | _snapshotsPath: string; 53 | 54 | _stopByTimeoutID: ReturnType | void; 55 | 56 | _timeout: number; 57 | 58 | 59 | constructor(params: TestsRunnerParamsType) { 60 | this._activityName = params.activityName; 61 | this._appFile = params.appFile; 62 | this._device = params.device; 63 | this._deviceName = params.deviceName; 64 | this._deviceParams = params.deviceParams; 65 | this._isDevMode = params.isDevMode; 66 | this._locale = params.locale; 67 | this._packageName = params.packageName; 68 | this._platform = params.platform; 69 | this._port = params.port; 70 | this._snapshotsPath = params.snapshotsPath; 71 | this._timeout = params.timeout; 72 | 73 | if (!this._isDevMode) { 74 | if (!this._appFile) { 75 | log.e(TAG, 'Valid ap file is required, check config'); 76 | process.exit(-1); 77 | } 78 | 79 | this._appFileFullPath = path.isAbsolute(this._appFile) 80 | ? this._appFile : path.join(process.cwd(), this._appFile); 81 | 82 | if (!fs.existsSync(this._appFileFullPath)) { 83 | log.e(TAG, `Valid app file is required, cannot find [${this._appFile}] file`); 84 | process.exit(-1); 85 | } 86 | } 87 | 88 | this._reporter = new Reporter(params.testRunName, this._snapshotsPath); 89 | } 90 | 91 | 92 | _testingCompleted = async (isPassed: boolean = false): Promise => { 93 | if (this._stopByTimeoutID) { 94 | clearTimeout(this._stopByTimeoutID); 95 | } 96 | if (!this._isDevMode) { 97 | log.i(TAG, 'Stopping the server and emulator'); 98 | server.stop(); 99 | await this._device.stop(); 100 | log.i(TAG, 'Server and emulator are stopped'); 101 | 102 | if (!isPassed) { 103 | log.i(TAG, 'Some tests failed, exit with error'); 104 | process.exit(-1); 105 | } else { 106 | log.i(TAG, 'No errors found'); 107 | } 108 | } 109 | }; 110 | 111 | 112 | _onTestingCompleted = async (byTimeOut: boolean = false): Promise => { 113 | const jUnitFile = path.join(process.cwd(), 'junit.xml'); 114 | const deviceLogsFile = path.join( 115 | process.cwd(), 116 | `${this._platform}_logs.log`, 117 | ); 118 | await this._reporter.toLog(); 119 | this._reporter.tojUnit(jUnitFile); 120 | this._reporter.deviceLogsToFile(deviceLogsFile); 121 | this._testingCompleted(byTimeOut ? false : this._reporter.isPassed()); 122 | }; 123 | 124 | 125 | _onAppActivity = (): void => { 126 | this._stopByTimeout(); 127 | } 128 | 129 | 130 | _stopByTimeout = (): void => { 131 | if (this._stopByTimeoutID) { 132 | clearTimeout(this._stopByTimeoutID); 133 | } 134 | this._stopByTimeoutID = setTimeout(() => { 135 | log.e(TAG, 'Stop tests by timeout'); 136 | this._onTestingCompleted(true); 137 | }, this._timeout); 138 | }; 139 | 140 | 141 | async _startAndroid(): Promise { 142 | log.d(TAG, `Start emulator [${this._deviceName}]`); 143 | try { 144 | await this._device.start(this._deviceParams); 145 | } catch (err) { 146 | process.exit(-1); 147 | } 148 | log.d(TAG, 'Emulator started'); 149 | 150 | log.d(TAG, 'Installing APK'); 151 | await this._device.installApp(this._packageName, this._appFileFullPath); 152 | log.d(TAG, 'APK installed'); 153 | 154 | log.d(TAG, 'Starting application'); 155 | if (this._locale) { 156 | log.w(TAG, `[${this._locale} is ignored for android]`); 157 | } 158 | await this._device.startApp(this._packageName, this._activityName); 159 | log.d(TAG, 'Application started'); 160 | 161 | this._stopByTimeout(); 162 | } 163 | 164 | 165 | async _startIOS(): Promise { 166 | log.d(TAG, `Start emulator [${this._deviceName}]`); 167 | try { 168 | await this._device.start(this._deviceParams); 169 | } catch (err) { 170 | log.e(TAG, `Failed to start device: [${err instanceof Error ? err.message : 'Unknown error'}]`); 171 | process.exit(-1); 172 | } 173 | log.d(TAG, 'Emulator started'); 174 | 175 | log.d(TAG, 'Installing APP'); 176 | await this._device.installApp(this._packageName, this._appFileFullPath); 177 | log.d(TAG, 'APP installed'); 178 | 179 | log.d(TAG, 'Starting application'); 180 | await this._device.startApp(this._packageName, this._activityName, this._locale); 181 | log.d(TAG, 'Application started'); 182 | } 183 | 184 | 185 | async start(): Promise { 186 | log.d(TAG, 'Starting server'); 187 | server.start( 188 | this._reporter, 189 | this._onTestingCompleted, 190 | this._snapshotsPath, 191 | this._onAppActivity, 192 | this._port, 193 | ); 194 | 195 | if (this._isDevMode) { 196 | log.d(TAG, 'Only server is used in DEV mode. Waiting for tests'); 197 | return; 198 | } 199 | 200 | if (this._platform === 'ios') { 201 | this._startIOS(); 202 | } else { 203 | this._startAndroid(); 204 | } 205 | 206 | this._reporter.collectDeviceLogs(this._platform, this._packageName); 207 | } 208 | } 209 | 210 | 211 | export default TestsRunner; 212 | -------------------------------------------------------------------------------- /src/runner/azure/AzurePublisher.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as https from 'https'; 3 | import * as path from 'path'; 4 | 5 | import log from '../utils/log'; 6 | import type { TestcaseType } from '../utils/Reporter'; 7 | 8 | const TAG = 'PIXELS_CATCHER::AZURE_PUBLISHER'; 9 | 10 | const processEnv: any = process.env; 11 | const { 12 | BUILD_BUILDURI, 13 | SYSTEM_ACCESSTOKEN, 14 | SYSTEM_TEAMFOUNDATIONCOLLECTIONURI, 15 | SYSTEM_TEAMPROJECT, 16 | } = processEnv; 17 | 18 | const DEFAULT_OPTIONS = { 19 | hostname: 'dev.azure.com', 20 | port: 443, 21 | }; 22 | 23 | const DEFAULT_HEADERS = { 24 | 'Content-Type': 'application/json; charset=utf-8', 25 | 'X-TFS-FedAuthRedirect': 'Suppress', 26 | Accept: 'application/json', 27 | Authorization: `Basic ${Buffer.from(`:${SYSTEM_ACCESSTOKEN}`).toString('base64')}`, 28 | }; 29 | 30 | type ImageType = 'refImages' | 'uploads' | 'diffs'; 31 | 32 | const uploadImageSuffix = { 33 | diffs: 'Diff', 34 | refImages: 'Reference', 35 | uploads: 'Actual', 36 | }; 37 | 38 | const imageTypes = Object.keys(uploadImageSuffix); 39 | 40 | function base64Encode(file: string): string { 41 | if (!fs.existsSync(file)) { 42 | return ''; 43 | } 44 | return Buffer.from(fs.readFileSync(file)).toString('base64'); 45 | } 46 | 47 | class AzurePublisher { 48 | _workingDir: string; 49 | 50 | _testRunName: string; 51 | 52 | _urlBasePath: string; 53 | 54 | constructor(workingDir: string, testRunName: string) { 55 | this._workingDir = workingDir; 56 | this._testRunName = testRunName; 57 | const organization = SYSTEM_TEAMFOUNDATIONCOLLECTIONURI 58 | .split('/') 59 | .filter((str: string) => Boolean(str)) 60 | .reduce((acc: string, curr: string) => curr); 61 | this._urlBasePath = `/${organization}/${SYSTEM_TEAMPROJECT}/_apis/test`; 62 | } 63 | 64 | async publish(): Promise { 65 | try { 66 | const buildRunId = await this._getBuildRunId(BUILD_BUILDURI); 67 | log.i(TAG, `buildRunId [${buildRunId}]`); 68 | const failedTests = await this._getTestFailures(buildRunId); 69 | log.i(TAG, `failedTests count [${failedTests.length}]`); 70 | failedTests.forEach(async (test: any) => { 71 | log.v(TAG, `Uploading results for test [${test.testCaseTitle}] from [${test.automatedTestStorage}]`); 72 | let type: any; 73 | for (let ind = 0; ind < imageTypes.length; ++ind) { 74 | type = imageTypes[ind]; 75 | await this._uploadScreenshot( 76 | buildRunId, 77 | test.id, 78 | test.testCaseTitle, 79 | test.automatedTestStorage, 80 | type, 81 | ); 82 | } 83 | }); 84 | } catch (err) { 85 | log.e(TAG, `Failed to upload results: ${err instanceof Error ? err.message : 'Unknown error'}`); 86 | process.exit(-1); 87 | } 88 | } 89 | 90 | async _getBuildRunId(_buildUri: string): Promise { 91 | const data: any = await this._httpsRequest({ 92 | method: 'GET', 93 | path: `${this._urlBasePath}/runs?api-version=5.1&buildUri=${_buildUri}`, 94 | }); 95 | if (!data || !data.value || data.value.length === 0) { 96 | throw new Error('Failed to get build run, no data'); 97 | } 98 | let id; 99 | log.v(TAG, `Runs count: ${data.value.length}, searching for [${this._testRunName}]`); 100 | for (let ind = data.value.length - 1; ind >= 0; --ind) { 101 | log.v(TAG, `Name: [${data.value[ind].name}], id: ${data.value[ind].id}`); 102 | if (data.value[ind].name === this._testRunName) { 103 | log.v(TAG, `Id ${data.value[ind].id} found`); 104 | id = data.value[ind].id; 105 | break; 106 | } 107 | } 108 | 109 | if (id === undefined) { 110 | throw new Error(`Failed to get build run id for ${this._testRunName}`); 111 | } 112 | 113 | return id; 114 | } 115 | 116 | async _getTestFailures(runId: string): Promise> { 117 | const data: any = await this._httpsRequest({ 118 | method: 'GET', 119 | path: `${this._urlBasePath}/Runs/${runId}/results?outcomes=3&api-version=5.1&outcomes=3`, 120 | }); 121 | 122 | return data.value; 123 | } 124 | 125 | async _upload( 126 | buildRunId: string, 127 | id: string, 128 | fileToUpload: string, 129 | fileNameToShow: string, 130 | ): Promise { 131 | const postData = { 132 | stream: base64Encode(fileToUpload), 133 | fileName: fileNameToShow, 134 | comment: 'Diff uploaded by REST from pipeline', 135 | attachmentType: 'GeneralAttachment', 136 | }; 137 | 138 | const data: any = await this._httpsRequest({ 139 | method: 'POST', 140 | path: `${this._urlBasePath}/Runs/${buildRunId}/Results/${id}/attachments?api-version=5.1-preview.1`, 141 | }, postData); 142 | 143 | return data.value; 144 | } 145 | 146 | async _uploadScreenshot( 147 | buildRunId: string, 148 | id: string, 149 | testCaseTitle: string, 150 | className: string, 151 | type: ImageType, 152 | ): Promise { 153 | const suffix = uploadImageSuffix[type]; 154 | log.v(TAG, `Uploading ${suffix}`); 155 | await this._upload( 156 | buildRunId, 157 | id, 158 | path.join(this._workingDir, className, type, `${testCaseTitle}.png`), 159 | `${testCaseTitle}${suffix}.png`, 160 | ); 161 | log.v(TAG, `${suffix} uploaded`); 162 | } 163 | 164 | async _httpsRequest(options: any, postData: any = undefined): Promise { 165 | let _options = { 166 | ...DEFAULT_OPTIONS, 167 | ...options, 168 | headers: { 169 | ...DEFAULT_HEADERS, 170 | ...(options.headers ? options.headers : {}), 171 | }, 172 | }; 173 | const _postData = postData ? JSON.stringify(postData) : undefined; 174 | 175 | return new Promise((resolve: Function, reject: Function) => { 176 | if (_postData) { 177 | _options = { 178 | ..._options, 179 | 'Content-Length': Buffer.byteLength(_postData), 180 | }; 181 | } 182 | const req = https.request(_options, (resp: any) => { 183 | if (resp.statusCode >= 300) { 184 | log.e(TAG, `Failed to ${_options.method} [${_options.path}]`); 185 | reject(new Error(`Status code: ${resp.statusCode}, statusMessage: ${resp.statusMessage}`)); 186 | return; 187 | } 188 | 189 | let data = ''; 190 | 191 | resp.on('data', (chunk: string) => { 192 | data += chunk; 193 | }); 194 | 195 | resp.on('end', () => { 196 | resolve(JSON.parse(data)); 197 | }); 198 | }).on('error', (err: Error) => { 199 | reject(new Error(`Error: ${err.message}`)); 200 | }); 201 | 202 | if (_postData) { 203 | req.write(_postData); 204 | } 205 | 206 | req.end(); 207 | }); 208 | } 209 | } 210 | 211 | export default AzurePublisher; 212 | -------------------------------------------------------------------------------- /src/runner/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Maksym Rusynyk 2018 - present 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | import type { DeviceInterface } from './utils/device/DeviceInterface'; 9 | 10 | import log from './utils/log'; 11 | import readConfig from './utils/readConfig'; 12 | import getDevice from './utils/device/deviceProvider'; 13 | import AzurePublisher from './azure/AzurePublisher'; 14 | import TestsRunner from './TestsRunner'; 15 | 16 | const TAG = 'PIXELS_CATCHER'; 17 | const AZURE_PUBLISH_ACTION = 'azureAttachments'; 18 | const [,, platform, configuration, action] = process.argv; 19 | 20 | if (!platform || !(platform === 'ios' || platform === 'android')) { 21 | log.e(TAG, `Valid platform is required, specify "ios" or "android". Example: 22 | 23 | $ pixels-catcher android debug 24 | 25 | or 26 | 27 | $ pixels-catcher ios debug 28 | `); 29 | process.exit(-1); 30 | } 31 | 32 | if (!configuration) { 33 | log.e(TAG, `Configuration is required. Example: 34 | 35 | $ pixels-catcher android debug 36 | 37 | or 38 | 39 | $ pixels-catcher ios debug 40 | `); 41 | process.exit(-1); 42 | } 43 | 44 | if (action !== undefined && action !== AZURE_PUBLISH_ACTION) { 45 | log.e(TAG, `Only "${AZURE_PUBLISH_ACTION}" is available. Example: 46 | 47 | $ pixels-catcher android debug ${AZURE_PUBLISH_ACTION} 48 | 49 | or 50 | 51 | $ pixels-catcher ios debug ${AZURE_PUBLISH_ACTION} 52 | `); 53 | process.exit(-1); 54 | } 55 | 56 | 57 | const fullConfig = readConfig(); 58 | const config = fullConfig[platform]; 59 | 60 | if (!config) { 61 | log.e(TAG, `Cannot find configuration for plarform [${platform}] in ` 62 | + `config:\n ${JSON.stringify(fullConfig, null, 2)}`); 63 | process.exit(-1); 64 | } 65 | 66 | log.setLevel(fullConfig.logLevel); 67 | log.i(TAG, `Starting with [${configuration}] configuration for [${platform}]`); 68 | log.v(TAG, `Config\n${JSON.stringify(config, null, 2)}`); 69 | 70 | const getParamFromConfig = (paramName: string): any => { 71 | const value = (config[configuration] || {})[paramName]; 72 | return value !== undefined ? value : config[paramName]; 73 | }; 74 | 75 | const activityName = getParamFromConfig('activityName') || 'MainActivity'; 76 | const appFile = getParamFromConfig('appFile'); 77 | const canStopDevice = getParamFromConfig('canStopDevice'); 78 | const deviceName = getParamFromConfig('deviceName'); 79 | const deviceParams = getParamFromConfig('deviceParams'); 80 | const isPhysicalDevice = getParamFromConfig('physicalDevice'); 81 | const packageName = getParamFromConfig('packageName'); 82 | const snapshotsPath = getParamFromConfig('snapshotsPath'); 83 | const port = getParamFromConfig('port'); 84 | const locale = getParamFromConfig('locale'); 85 | const timeout = fullConfig.timeout || 25 * 1000; // 25 sec is default 86 | 87 | if (!deviceName) { 88 | log.e(TAG, 'Valid device name is required, check "PixelsCatcher.deviceName" ' 89 | + 'property in package.json'); 90 | process.exit(-1); 91 | } 92 | 93 | const device: DeviceInterface = getDevice( 94 | deviceName, 95 | platform, 96 | isPhysicalDevice, 97 | canStopDevice, 98 | ); 99 | 100 | log.i(TAG, `Starting with: 101 | - activityName: [${activityName}] 102 | - appFile: [${appFile}] 103 | - deviceName: [${deviceName}] 104 | - deviceParams: [${deviceParams}] 105 | - packageName: [${packageName}] 106 | - snapshotsPath: [${snapshotsPath}] 107 | - canStopDevice: [${canStopDevice}] 108 | - port: [${port}] 109 | - locale: [${locale}]`); 110 | 111 | if (!packageName) { 112 | log.e(TAG, 'Package name is required'); 113 | process.exit(-1); 114 | } 115 | 116 | const testRunName = `UI tests for ${platform}/${deviceName}`; 117 | 118 | if (action === AZURE_PUBLISH_ACTION) { 119 | const azurePublisher = new AzurePublisher(process.cwd(), testRunName); 120 | azurePublisher.publish(); 121 | } else { 122 | const isDevMode = !appFile; 123 | log.i(TAG, `Starting in ${isDevMode ? 'development' : 'ci'} mode`); 124 | const testsRunner = new TestsRunner({ 125 | testRunName, 126 | isDevMode, 127 | timeout, 128 | device, 129 | appFile, 130 | port, 131 | platform, 132 | deviceName, 133 | snapshotsPath, 134 | deviceParams, 135 | packageName, 136 | locale, 137 | activityName, 138 | }); 139 | testsRunner.start(); 140 | } 141 | -------------------------------------------------------------------------------- /src/runner/server/compareImages.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as fs from 'fs'; 8 | import { PNG } from 'pngjs'; 9 | import * as pixelmatch from 'pixelmatch'; 10 | 11 | export default (actual: any, expected: any, diffFile: any): number => { 12 | if (!actual || !fs.existsSync(actual)) { 13 | throw new Error(`Actual file is required, cannot get [${actual}] file`); 14 | } 15 | if (!expected || !fs.existsSync(expected)) { 16 | throw new Error(`Expected file is required, cannot get [${expected}] file`); 17 | } 18 | 19 | const imageActual = PNG.sync.read(fs.readFileSync(actual)); 20 | const imageExpected = PNG.sync.read(fs.readFileSync(expected)); 21 | 22 | if (imageActual.width !== imageExpected.width) { 23 | throw new Error(`Width mismatch: expected ${imageExpected.width}, actual: ${imageActual.width}`); 24 | } 25 | 26 | if (imageActual.height !== imageExpected.height) { 27 | throw new Error(`Height mismatch: expected ${imageExpected.height}, actual: ${imageActual.height}`); 28 | } 29 | 30 | const diff = new PNG({ width: imageExpected.width, height: imageExpected.height }); 31 | 32 | const differentPixelsCount = pixelmatch( 33 | imageActual.data, 34 | imageExpected.data, 35 | diff.data, 36 | imageActual.width, 37 | imageActual.height, 38 | { threshold: 0.1 }, 39 | ); 40 | 41 | if (diffFile) { 42 | diff.pack().pipe(fs.createWriteStream(diffFile)); 43 | } 44 | 45 | return differentPixelsCount; 46 | }; 47 | -------------------------------------------------------------------------------- /src/runner/server/dummy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/src/runner/server/dummy.png -------------------------------------------------------------------------------- /src/runner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es5", 5 | "declaration": true, 6 | "outDir": "../../lib/runner", 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true 11 | }, 12 | "include": ["./**/*"], 13 | "exclude": [ 14 | "../../node_modules", 15 | "**/__tests__/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/runner/utils/Reporter.ts: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as fs from 'fs'; 3 | 4 | import timeToSec from './timeToSec'; 5 | import exec from './exec'; 6 | import delay from './delay'; 7 | 8 | const { spawn } = require('child_process'); 9 | 10 | export type TestcaseType = { 11 | failure: string | void, 12 | isSkipped: boolean | void, 13 | name: string, 14 | renderTime?: number, 15 | time: number, 16 | }; 17 | 18 | const timeReducer = (time: number, testcase: TestcaseType): number => time + testcase.time; 19 | 20 | const filterSkipped = (testcase: TestcaseType): boolean => !testcase.isSkipped; 21 | 22 | const filterFailed = (testcase: TestcaseType): boolean => !testcase.failure; 23 | 24 | class TestReporter { 25 | _name: string; 26 | 27 | _className: string; 28 | 29 | _tests: Array = []; 30 | 31 | _deviceLogs: Array = []; 32 | 33 | _stopDeviceLogger: Function | undefined = undefined; 34 | 35 | _minRenderTime = { 36 | name: '-', 37 | time: Number.MAX_VALUE, 38 | }; 39 | 40 | _maxRenderTime = { 41 | name: '-', 42 | time: Number.MIN_VALUE, 43 | }; 44 | 45 | constructor(name: string, className: string) { 46 | this._name = name; 47 | this._className = className; 48 | } 49 | 50 | registerTest(name: string): void { 51 | this._tests.push({ 52 | failure: undefined, 53 | isSkipped: true, 54 | name, 55 | renderTime: 0, 56 | time: 0, 57 | }); 58 | } 59 | 60 | _updateTestResult(testCase: TestcaseType): void { 61 | const ind = this._tests.findIndex((test) => test.name === testCase.name); 62 | 63 | if (ind >= 0) { 64 | this._tests[ind] = testCase; 65 | } else { 66 | this._tests.push(testCase); 67 | } 68 | } 69 | 70 | reportTest(testCase: TestcaseType): void { 71 | this._updateTestResult(testCase); 72 | 73 | if (testCase.renderTime === undefined) { 74 | return; 75 | } 76 | if (testCase.renderTime < this._minRenderTime.time) { 77 | this._minRenderTime.time = testCase.renderTime; 78 | this._minRenderTime.name = testCase.name; 79 | } 80 | if (testCase.renderTime > this._maxRenderTime.time) { 81 | this._maxRenderTime.time = testCase.renderTime; 82 | this._maxRenderTime.name = testCase.name; 83 | } 84 | } 85 | 86 | isPassed(): boolean { 87 | return this._getFailedTests().length === 0; 88 | } 89 | 90 | async toLog(): Promise { 91 | global.console.log(''); 92 | global.console.log('==> All tests completed: <=='); 93 | 94 | const failedTests = this._getFailedTests(); 95 | const passedTests = this._getPassedTests(); 96 | const skippedTests = this._getSkippedTests(); 97 | const reportTable: any = []; 98 | 99 | this._tests.forEach((testcase: TestcaseType) => { 100 | let status = 'PASSED'; 101 | 102 | if (testcase.failure) { 103 | status = 'FAILED'; 104 | } else if (testcase.isSkipped) { 105 | status = 'SKIPPED'; 106 | } 107 | 108 | reportTable.push({ 109 | name: testcase.name, 110 | status, 111 | time: timeToSec(testcase.time), 112 | renderTime: testcase.renderTime !== undefined ? timeToSec(testcase.renderTime) : '-', 113 | failure: testcase.failure || '-', 114 | }); 115 | }); 116 | 117 | global.console.table(reportTable); 118 | 119 | global.console.log(''); 120 | global.console.log('==> Summary: <=='); 121 | 122 | global.console.table([ 123 | ['Total tests', this._tests.length], 124 | ['Passed tests', passedTests.length], 125 | ['Skipped tests', skippedTests.length], 126 | ['Failed tests', failedTests.length], 127 | ['Min render time', `${this._minRenderTime.time}ms (${this._minRenderTime.name})`], 128 | ['Max render time', `${this._maxRenderTime.time}ms (${this._maxRenderTime.name})`], 129 | ]); 130 | 131 | if (failedTests.length > 0) { 132 | global.console.log('==> Failed tests: <=='); 133 | global.console.table(failedTests.map((testCase: TestcaseType) => testCase.name)); 134 | } 135 | 136 | // on CI some logs are not available, adding a delay to fix it 137 | await delay(300); 138 | } 139 | 140 | tojUnit(jUnitFile: string): void { 141 | const xmlResult = ['']; 142 | xmlResult.push('`); 149 | xmlResult.push(' `); 156 | this._tests.forEach((testcase: TestcaseType) => { 157 | xmlResult.push(' `); 161 | if (testcase.failure) { 162 | xmlResult.push(` ${testcase.failure}`); 163 | } else if (testcase.isSkipped) { 164 | xmlResult.push(' '); 165 | } 166 | xmlResult.push(' '); 167 | }); 168 | xmlResult.push(' '); 169 | xmlResult.push(''); 170 | xmlResult.push(''); 171 | fs.writeFileSync(jUnitFile, xmlResult.join('\n')); 172 | } 173 | 174 | collectDeviceLogs(platform: 'ios' | 'android', packageName: string): void { 175 | let spawnProcess: any; 176 | if (platform === 'android') { 177 | exec('adb logcat -c'); 178 | spawnProcess = spawn('adb', [ 179 | 'logcat', `${packageName}:I`, '*:V', 180 | ]); 181 | } else if (platform === 'ios') { 182 | spawnProcess = spawn('xcrun', [ 183 | 'simctl', 'spawn', 'booted', 'log', 'stream', 184 | ]); 185 | } 186 | 187 | spawnProcess.stdout.on('data', (data: any): any => { 188 | const stringRepresentation = data.toString(); 189 | this._deviceLogs.push(stringRepresentation); 190 | }); 191 | 192 | this._stopDeviceLogger = (): void => { 193 | spawnProcess.stdin.pause(); 194 | spawnProcess.kill(); 195 | }; 196 | } 197 | 198 | deviceLogsToFile(fileName: string): void { 199 | if (this._stopDeviceLogger) { 200 | this._stopDeviceLogger(); 201 | this._stopDeviceLogger = undefined; 202 | } 203 | fs.writeFileSync(fileName, this._deviceLogs.join('')); 204 | } 205 | 206 | _getPassedTests(): Array { 207 | return this._tests 208 | .filter(filterSkipped) 209 | .filter(filterFailed); 210 | } 211 | 212 | _getSkippedTests(): Array { 213 | return this._tests 214 | .filter((testcase: TestcaseType): boolean => Boolean(testcase.isSkipped)); 215 | } 216 | 217 | _getFailedTests(): Array { 218 | return this._tests 219 | .filter(filterSkipped) 220 | .filter((test: TestcaseType) => Boolean(test.failure)); 221 | } 222 | 223 | _getTotalTime(): number { 224 | return this._tests 225 | .filter(filterSkipped) 226 | .reduce(timeReducer, 0); 227 | } 228 | } 229 | 230 | export default TestReporter; 231 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/__snapshots__/log.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`logging Allows to log including d 1`] = ` 4 | Array [ 5 | Array [ 6 | "i:", 7 | ], 8 | Array [ 9 | "w WARNING:", 10 | ], 11 | Array [ 12 | "e ERROR:", 13 | ], 14 | Array [ 15 | "v:", 16 | ], 17 | Array [ 18 | "d:", 19 | ], 20 | Array [ 21 | "i:", 22 | ], 23 | Array [ 24 | "w WARNING:", 25 | ], 26 | Array [ 27 | "e ERROR:", 28 | ], 29 | Array [ 30 | "d:", 31 | ], 32 | Array [ 33 | "i:", 34 | ], 35 | Array [ 36 | "w WARNING:", 37 | ], 38 | Array [ 39 | "e ERROR:", 40 | ], 41 | ] 42 | `; 43 | 44 | exports[`logging Allows to log including e 1`] = ` 45 | Array [ 46 | Array [ 47 | "i:", 48 | ], 49 | Array [ 50 | "w WARNING:", 51 | ], 52 | Array [ 53 | "e ERROR:", 54 | ], 55 | Array [ 56 | "v:", 57 | ], 58 | Array [ 59 | "d:", 60 | ], 61 | Array [ 62 | "i:", 63 | ], 64 | Array [ 65 | "w WARNING:", 66 | ], 67 | Array [ 68 | "e ERROR:", 69 | ], 70 | Array [ 71 | "d:", 72 | ], 73 | Array [ 74 | "i:", 75 | ], 76 | Array [ 77 | "w WARNING:", 78 | ], 79 | Array [ 80 | "e ERROR:", 81 | ], 82 | Array [ 83 | "i:", 84 | ], 85 | Array [ 86 | "w WARNING:", 87 | ], 88 | Array [ 89 | "e ERROR:", 90 | ], 91 | Array [ 92 | "w WARNING:", 93 | ], 94 | Array [ 95 | "e ERROR:", 96 | ], 97 | Array [ 98 | "e ERROR:", 99 | ], 100 | ] 101 | `; 102 | 103 | exports[`logging Allows to log including i 1`] = ` 104 | Array [ 105 | Array [ 106 | "i:", 107 | ], 108 | Array [ 109 | "w WARNING:", 110 | ], 111 | Array [ 112 | "e ERROR:", 113 | ], 114 | Array [ 115 | "v:", 116 | ], 117 | Array [ 118 | "d:", 119 | ], 120 | Array [ 121 | "i:", 122 | ], 123 | Array [ 124 | "w WARNING:", 125 | ], 126 | Array [ 127 | "e ERROR:", 128 | ], 129 | Array [ 130 | "d:", 131 | ], 132 | Array [ 133 | "i:", 134 | ], 135 | Array [ 136 | "w WARNING:", 137 | ], 138 | Array [ 139 | "e ERROR:", 140 | ], 141 | Array [ 142 | "i:", 143 | ], 144 | Array [ 145 | "w WARNING:", 146 | ], 147 | Array [ 148 | "e ERROR:", 149 | ], 150 | ] 151 | `; 152 | 153 | exports[`logging Allows to log including v 1`] = ` 154 | Array [ 155 | Array [ 156 | "i:", 157 | ], 158 | Array [ 159 | "w WARNING:", 160 | ], 161 | Array [ 162 | "e ERROR:", 163 | ], 164 | Array [ 165 | "v:", 166 | ], 167 | Array [ 168 | "d:", 169 | ], 170 | Array [ 171 | "i:", 172 | ], 173 | Array [ 174 | "w WARNING:", 175 | ], 176 | Array [ 177 | "e ERROR:", 178 | ], 179 | ] 180 | `; 181 | 182 | exports[`logging Allows to log including w 1`] = ` 183 | Array [ 184 | Array [ 185 | "i:", 186 | ], 187 | Array [ 188 | "w WARNING:", 189 | ], 190 | Array [ 191 | "e ERROR:", 192 | ], 193 | Array [ 194 | "v:", 195 | ], 196 | Array [ 197 | "d:", 198 | ], 199 | Array [ 200 | "i:", 201 | ], 202 | Array [ 203 | "w WARNING:", 204 | ], 205 | Array [ 206 | "e ERROR:", 207 | ], 208 | Array [ 209 | "d:", 210 | ], 211 | Array [ 212 | "i:", 213 | ], 214 | Array [ 215 | "w WARNING:", 216 | ], 217 | Array [ 218 | "e ERROR:", 219 | ], 220 | Array [ 221 | "i:", 222 | ], 223 | Array [ 224 | "w WARNING:", 225 | ], 226 | Array [ 227 | "e ERROR:", 228 | ], 229 | Array [ 230 | "w WARNING:", 231 | ], 232 | Array [ 233 | "e ERROR:", 234 | ], 235 | ] 236 | `; 237 | 238 | exports[`logging Default logs i, w and e 1`] = ` 239 | Array [ 240 | Array [ 241 | "i:", 242 | ], 243 | Array [ 244 | "w WARNING:", 245 | ], 246 | Array [ 247 | "e ERROR:", 248 | ], 249 | ] 250 | `; 251 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/__snapshots__/readConfig.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`readConfig read config from package.json 1`] = ` 4 | Object { 5 | "android": Object { 6 | "_content": "platform config content from package.json", 7 | }, 8 | } 9 | `; 10 | 11 | exports[`readConfig read config from pixels-catcher.json 1`] = ` 12 | Object { 13 | "ios": Object { 14 | "_content": "platform config content from pixels-catcher.json", 15 | }, 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/isCommand.js: -------------------------------------------------------------------------------- 1 | import isCommand from '../isCommand'; 2 | import exec from '../exec'; 3 | 4 | jest.mock('../exec', () => jest.fn()); 5 | 6 | describe('isCommand', () => { 7 | afterEach(() => { 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | it('Returns true if command exists', () => { 12 | exec.mockReturnValueOnce('/bin/ls'); 13 | 14 | const exists = isCommand('ls'); 15 | 16 | expect(exec).toHaveBeenCalledWith('whereis ls'); 17 | expect(exists).toBe(true); 18 | }); 19 | 20 | it('Returns false if command does not exists', () => { 21 | exec.mockReturnValueOnce(''); 22 | 23 | const exists = isCommand('sl'); 24 | 25 | expect(exec).toHaveBeenCalledWith('whereis sl'); 26 | expect(exists).toBe(false); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/log.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import log from '../log'; 3 | 4 | global.console.log = jest.fn(); 5 | 6 | describe('logging', () => { 7 | const consoleLog = global.console.log; 8 | const logAll = () => { 9 | log.v('v'); 10 | log.d('d'); 11 | log.i('i'); 12 | log.w('w'); 13 | log.e('e'); 14 | }; 15 | 16 | afterEach(() => { 17 | log.setLevel('v'); 18 | }); 19 | 20 | it('Default logs i, w and e', () => { 21 | logAll(); 22 | expect(consoleLog.mock.calls).toMatchSnapshot(); 23 | }); 24 | 25 | it('Allows to log including v', () => { 26 | log.setLevel('v'); 27 | logAll(); 28 | expect(consoleLog.mock.calls).toMatchSnapshot(); 29 | }); 30 | 31 | it('Allows to log including d', () => { 32 | log.setLevel('d'); 33 | logAll(); 34 | expect(consoleLog.mock.calls).toMatchSnapshot(); 35 | }); 36 | 37 | it('Allows to log including i', () => { 38 | log.setLevel('i'); 39 | logAll(); 40 | expect(consoleLog.mock.calls).toMatchSnapshot(); 41 | }); 42 | 43 | it('Allows to log including w', () => { 44 | log.setLevel('w'); 45 | logAll(); 46 | expect(consoleLog.mock.calls).toMatchSnapshot(); 47 | }); 48 | 49 | it('Allows to log including e', () => { 50 | log.setLevel('e'); 51 | logAll(); 52 | expect(consoleLog.mock.calls).toMatchSnapshot(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/readConfig.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import readConfig from '../readConfig'; 4 | 5 | jest.mock('fs', () => ({ 6 | existsSync: jest.fn(), 7 | readFileSync: jest.fn(), 8 | })); 9 | jest.mock('path', () => ({ 10 | join: jest.fn((...args) => args.join('/')), 11 | })); 12 | 13 | process.exit = jest.fn(); 14 | process.cwd = jest.fn(() => 'path_to_file'); 15 | 16 | describe('readConfig', () => { 17 | afterEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | it('read config from package.json', () => { 22 | const rawConfig = '{"PixelsCatcher":{"android":{"_content":"platform ' 23 | + 'config content from package.json"}}}'; 24 | 25 | fs.existsSync.mockImplementationOnce(() => true); 26 | fs.readFileSync.mockImplementationOnce(() => rawConfig); 27 | 28 | const config = readConfig(); 29 | 30 | expect(config).toMatchSnapshot(); 31 | }); 32 | 33 | it('read config from pixels-catcher.json', () => { 34 | // package.json mock 35 | fs.existsSync.mockImplementationOnce(() => true); 36 | fs.readFileSync.mockImplementationOnce(() => '{}'); 37 | // pixels-catcher.json mock 38 | const rawConfig = '{"ios":{"_content":"platform config content from ' 39 | + 'pixels-catcher.json"}}'; 40 | fs.existsSync.mockImplementationOnce(() => true); 41 | fs.readFileSync.mockImplementationOnce(() => rawConfig); 42 | 43 | const config = readConfig(); 44 | 45 | expect(config).toMatchSnapshot(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/runner/utils/delay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | function delay(milliseconds: number): Promise { 8 | return new Promise((resolve: Function) => { 9 | setTimeout(resolve, milliseconds); 10 | }); 11 | } 12 | 13 | export default delay; 14 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidDevice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2019 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type { DeviceInterface } from './DeviceInterface'; 8 | 9 | import exec from '../exec'; 10 | import delay from '../delay'; 11 | import log from '../log'; 12 | 13 | const TAG = 'PIXELS_CATCHER::ANDROID_DEVICE'; 14 | 15 | class AndroidDevice implements DeviceInterface { 16 | _name: string; 17 | 18 | constructor(name: string) { 19 | this._name = name; 20 | } 21 | 22 | 23 | _getDevices(): Array { 24 | const cmd = 'adb devices'; 25 | const devices = exec(cmd).split('\n').slice(1) 26 | .filter((line: string): boolean => Boolean(line)) 27 | .map((line: string): string => line.split('\t')[0]); 28 | 29 | return devices; 30 | } 31 | 32 | 33 | _isDeviceAvailable(name: string): boolean { 34 | const devices = this._getDevices(); 35 | let isAvailable = false; 36 | 37 | for (let ind = devices.length - 1; ind >= 0; --ind) { 38 | if (devices[ind].indexOf(name) >= 0) { 39 | isAvailable = true; 40 | break; 41 | } 42 | } 43 | 44 | return isAvailable; 45 | } 46 | 47 | 48 | async start(params: any = []): Promise { 49 | if (params.length !== 0) { 50 | log.e(TAG, 'There are currently no supported device parameters for physical devices, yet you tried to pass some im'); 51 | process.exit(-1); 52 | } 53 | if (!this._isDeviceAvailable(this._name)) { 54 | log.e(TAG, `Invalid name provided [${this._name}], check that the name is \ 55 | correct and device is available. Available devices: 56 | ${this._getDevices().map((device: any): any => ` - ${device}`).join('\n')}`); 57 | throw new Error(`Invalid emulator ${this._name}`); 58 | } 59 | } 60 | 61 | 62 | async stop(): Promise { 63 | log.v(TAG, 'Not stopping anything as it is assumed to be a physical device. Your responsibility!'); 64 | } 65 | 66 | 67 | isAppInstalled(packageName: string): boolean { 68 | const cmd = `adb -s ${this._name} shell pm list packages`; 69 | 70 | log.v(TAG, `Checking if [${packageName}] is installed`); 71 | 72 | const allPackages = exec(cmd); 73 | const isInstalled = allPackages.indexOf(packageName) >= 0; 74 | 75 | log.v(TAG, `Package [${packageName}] is ${isInstalled ? 'Installed' : 'Not installed'}`); 76 | 77 | return isInstalled; 78 | } 79 | 80 | 81 | async uninstallApp(name: string): Promise { 82 | log.v(TAG, `Uninstalling ${name}`); 83 | const isInstalled = await this.isAppInstalled(name); 84 | if (isInstalled) { 85 | const cmd = `adb -s ${this._name} uninstall ${name}`; 86 | exec(cmd); 87 | } 88 | log.v(TAG, 'Uninstalling completed'); 89 | } 90 | 91 | 92 | async installApp(name: string, apkFile: string): Promise { 93 | log.v(TAG, `Installing apk [${apkFile}]`); 94 | 95 | await this.uninstallApp(name); 96 | 97 | let tryCnt = 3; 98 | 99 | while (tryCnt >= 0) { 100 | const cmd = `adb -s ${this._name} install -r ${apkFile}`; 101 | const res = exec(cmd); 102 | log.v(TAG, 'Installed', res); 103 | const isOffline = res.indexOf('device offline') >= 0; 104 | if (isOffline) { 105 | await delay(1000); 106 | } else { 107 | const isSuccess = res.indexOf('Success') >= 0; 108 | if (isSuccess) { 109 | break; 110 | } else { 111 | log.e(TAG, `ERROR: Failed install apk [${apkFile}]`); 112 | process.exit(-1); 113 | } 114 | } 115 | tryCnt--; 116 | } 117 | } 118 | 119 | 120 | startApp(packageName: string, activityName: string): void { 121 | log.v(TAG, `Starting application [${packageName}]`); 122 | 123 | const cmd = `adb -s ${this._name} shell am start -n ${packageName}/${activityName}`; 124 | const result = exec(cmd); 125 | 126 | if (result.indexOf('does not exist') >= 0 || result.indexOf('Error') >= 0) { 127 | log.e(TAG, `Cannot start [${packageName}] with activity [${activityName}]`); 128 | process.exit(-1); 129 | } 130 | 131 | log.v(TAG, 'Application started'); 132 | } 133 | } 134 | 135 | export default AndroidDevice; 136 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidEmulator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import { spawn } from 'child_process'; 8 | 9 | import exec from '../exec'; 10 | import delay from '../delay'; 11 | import log from '../log'; 12 | import emulatorCmd from './AndroidEmulatorCmd'; 13 | 14 | import type { DeviceInterface } from './DeviceInterface'; 15 | 16 | const TAG = 'PIXELS_CATCHER::UTIL_EMULATOR'; 17 | 18 | const startupErrorsDataToIgnore = [ 19 | // Some data appears in stderr when running the emulator first time 20 | '.avd/snapshots/default_boot/ram.img', 21 | 'qemu: unsupported keyboard', 22 | 'WARNING', 23 | ]; 24 | 25 | const canIgnoreErrorData = (data: string): boolean => { 26 | for (let i = 0; i < startupErrorsDataToIgnore.length; ++i) { 27 | if (data.indexOf(startupErrorsDataToIgnore[i]) !== -1) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | }; 34 | 35 | class AndroidEmulator implements DeviceInterface { 36 | _name: string; 37 | 38 | _canStopDevice: boolean; 39 | 40 | constructor(name: string, canStopDevice?: boolean) { 41 | this._name = name; 42 | this._canStopDevice = Boolean(canStopDevice); 43 | } 44 | 45 | _getDevices(): Array { 46 | const cmd = 'emulator -avd -list-avds'; 47 | const devices = exec(cmd).split('\n') 48 | .filter((line: string): boolean => Boolean(line)); 49 | 50 | return devices; 51 | } 52 | 53 | _isDeviceAvailable(name: string): boolean { 54 | const devices = this._getDevices(); 55 | let isAvailable = false; 56 | 57 | for (let ind = devices.length - 1; ind >= 0; --ind) { 58 | if (devices[ind].indexOf(name) >= 0) { 59 | isAvailable = true; 60 | break; 61 | } 62 | } 63 | 64 | return isAvailable; 65 | } 66 | 67 | _getActiveDevice(): any { 68 | log.v(TAG, 'Get active device'); 69 | const device = exec('adb devices').split('\n') 70 | .filter((line: string): boolean => line.indexOf('emulator') === 0)[0]; 71 | 72 | if (!device) { 73 | log.v(TAG, 'No active devices'); 74 | return undefined; 75 | } 76 | const name = device.split('\t')[0]; 77 | 78 | log.v(TAG, 'Active device', name); 79 | return name; 80 | } 81 | 82 | async start(params: any = []): Promise { 83 | if (!this._isDeviceAvailable(this._name)) { 84 | log.e(TAG, `Invalid name provided [${this._name}], check that the name is \ 85 | correct and device is available. Available devices: 86 | ${this._getDevices().map((device: any): any => ` - ${device}`).join('\n')}`); 87 | throw new Error(`Invalid emulator ${this._name}`); 88 | } 89 | 90 | if (this._getActiveDevice()) { 91 | log.e(TAG, 'Other emulator already started'); 92 | if (this._canStopDevice) { 93 | log.e(TAG, 'Stopping emulator'); 94 | await this.stop(); 95 | } else { 96 | log.d(TAG, 'Using active emulator'); 97 | return; 98 | } 99 | } 100 | 101 | log.d(TAG, `Starting emulator [${this._name}]`); 102 | log.v(TAG, `cmd: ${emulatorCmd}`); 103 | log.v(TAG, `params: ${[ 104 | '-avd', this._name, 105 | ...params, 106 | ].filter((value: any): any => Boolean(value))}`); 107 | const result = spawn(emulatorCmd, [ 108 | '-avd', this._name, 109 | ...params, 110 | ].filter((value: any): any => Boolean(value))); 111 | 112 | let deviceBooted = false; 113 | 114 | result.stdout.on('data', (data: any): any => { 115 | log.d(TAG, `stdout: ${data}`); 116 | if (data.toString().toLowerCase().includes('boot completed')) { 117 | deviceBooted = true; 118 | } 119 | }); 120 | 121 | result.stderr.on('data', (data: any): any => { 122 | // Some data appears in stderr when running the emulator first time 123 | const stringRepresentation = data.toString(); 124 | if (canIgnoreErrorData(stringRepresentation)) { 125 | log.w(TAG, `Ignore: ${stringRepresentation}`); 126 | return; 127 | } 128 | log.e(TAG, `Failed to load emulator, stderr: ${data}`); 129 | process.exit(-1); 130 | }); 131 | 132 | result.on('close', (code: any): any => { 133 | log.v(TAG, `on close: child process exited with code ${code}`); 134 | }); 135 | 136 | let tryCnt = (60 * 2) / 5; // 2 minutes with 5000 delay 137 | 138 | while (--tryCnt >= 0 && !deviceBooted) { 139 | log.v(TAG, 'awaiting when device is booted'); 140 | await delay(5000); 141 | } 142 | 143 | if (!deviceBooted) { 144 | log.e(TAG, 'Failed to load emulator in 30 seconds. Check your emulator. Or try to run it with "-no-snapshot"'); 145 | throw new Error('Device is not loaded in 30 seconds'); 146 | } 147 | } 148 | 149 | async stop(): Promise { 150 | if (!this._canStopDevice) { 151 | log.v(TAG, 'Stopping device is restricted in config'); 152 | return; 153 | } 154 | log.v(TAG, 'Stopping active device'); 155 | try { 156 | exec(`adb -s ${this._getActiveDevice()} emu kill;`); 157 | } catch (err) { 158 | log.e(err instanceof Error ? err.message : 'Unknown error'); 159 | } 160 | await delay(5000); 161 | log.v(TAG, 'Active device stopped'); 162 | } 163 | 164 | isAppInstalled(packageName: string): boolean { 165 | const cmd = 'adb shell pm list packages'; 166 | 167 | log.v(TAG, `Checking if [${packageName}] is installed`); 168 | 169 | const allPackages = exec(cmd); 170 | const isInstalled = allPackages.indexOf(packageName) >= 0; 171 | 172 | log.v(TAG, `Package [${packageName}] is ${isInstalled ? 'Installed' : 'Not installed'}`); 173 | 174 | return isInstalled; 175 | } 176 | 177 | async uninstallApp(name: string): Promise { 178 | log.v(TAG, `Uninstalling ${name}`); 179 | const isInstalled = await this.isAppInstalled(name); 180 | if (isInstalled) { 181 | const cmd = `adb uninstall ${name}`; 182 | exec(cmd); 183 | } 184 | log.v(TAG, 'Uninstalling completed'); 185 | } 186 | 187 | async installApp(name: string, apkFile: string): Promise { 188 | let tryCnt = 3; 189 | 190 | log.v(TAG, `Installing apk [${apkFile}]`); 191 | 192 | await this.uninstallApp(name); 193 | 194 | while (tryCnt-- >= 0) { 195 | const cmd = `adb install -r ${apkFile}`; 196 | const res = exec(cmd); 197 | log.v(TAG, 'Installed', res); 198 | const isOffline = res.indexOf('device offline') >= 0; 199 | if (isOffline) { 200 | await delay(1000); 201 | } else { 202 | const isSuccess = res.indexOf('Success') >= 0; 203 | if (isSuccess) { 204 | break; 205 | } else { 206 | log.e(TAG, `ERROR: Failed install apk [${apkFile}]`); 207 | process.exit(-1); 208 | } 209 | } 210 | } 211 | } 212 | 213 | startApp(packageName: string, activityName: string): void { 214 | log.v(TAG, `Starting application [${packageName}]`); 215 | 216 | const cmd = `adb shell am start -n ${packageName}/${activityName}`; 217 | const result = exec(cmd); 218 | 219 | if (result.indexOf('does not exist') >= 0 || result.indexOf('Error') >= 0) { 220 | log.e(TAG, `Cannot start [${packageName}] with activity [${activityName}]`); 221 | process.exit(-1); 222 | } 223 | 224 | log.v(TAG, 'Application started'); 225 | } 226 | } 227 | 228 | export default AndroidEmulator; 229 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidEmulatorCmd.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import exec from '../exec'; 8 | import isCommand from '../isCommand'; 9 | 10 | export default process.env.ANDROID_EMULATOR 11 | || (isCommand('emulator') ? 'emulator' : undefined) 12 | || ( 13 | exec('uname -s').trim() === 'Darwin' 14 | ? `${process.env.HOME || ''}/Library/Android/sdk/emulator/emulator` 15 | : 'emulator'); 16 | -------------------------------------------------------------------------------- /src/runner/utils/device/DeviceInterface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /** 3 | * Copyright (c) Maksym Rusynyk 2018 - present 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | export type StartParamsType = Array; 10 | 11 | export interface DeviceInterface { 12 | // constructor(deviceName: string, canStopDevice?: boolean): void, 13 | 14 | start(params: StartParamsType): Promise, 15 | 16 | isAppInstalled(appName: string): boolean, 17 | 18 | installApp(appName: string, appFile: string): Promise, 19 | 20 | startApp(appName: string, activityName: string, locale?: string): void, 21 | 22 | uninstallApp(name: string): Promise, 23 | 24 | stop(): Promise, 25 | } 26 | -------------------------------------------------------------------------------- /src/runner/utils/device/IosSimulator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | /* @flow */ 8 | import type { DeviceInterface, StartParamsType } from './DeviceInterface'; 9 | 10 | import exec from '../exec'; 11 | import log from '../log'; 12 | import delay from '../delay'; 13 | 14 | const TAG = 'PIXELS_CATCHER::UTIL_SIMULATOR'; 15 | 16 | type DeviceType = { 17 | availability: string, 18 | state: string, 19 | isAvailable: boolean, 20 | name: string, 21 | udid: string, 22 | availabilityError: string, 23 | }; 24 | 25 | class IOSSimulator implements DeviceInterface { 26 | _name: string; 27 | 28 | _canStopDevice: boolean; 29 | 30 | constructor(name: string, canStopDevice?: boolean) { 31 | this._name = name; 32 | this._canStopDevice = Boolean(canStopDevice); 33 | } 34 | 35 | 36 | _getAvailableDevices(): Array { 37 | const cmd = 'xcrun simctl list --json'; 38 | const response = JSON.parse(exec(cmd)); 39 | const { devices } = response; 40 | const availableDevices: Array = []; 41 | 42 | Object.keys(devices).forEach((name: string) => { 43 | devices[name].forEach((device: DeviceType) => { 44 | if (device.isAvailable) { 45 | availableDevices.push(device); 46 | } 47 | }); 48 | }); 49 | 50 | return availableDevices; 51 | } 52 | 53 | 54 | _getDeviceByName(name: string): DeviceType | void { 55 | const devices = this._getAvailableDevices(); 56 | let device; 57 | 58 | for (let ind = devices.length - 1; ind >= 0; --ind) { 59 | if (devices[ind].name === name) { 60 | device = devices[ind]; 61 | break; 62 | } 63 | } 64 | 65 | return device; 66 | } 67 | 68 | 69 | _getDeviceByUid(uid: string): DeviceType | void { 70 | const devices = this._getAvailableDevices(); 71 | let device; 72 | 73 | for (let ind = devices.length - 1; ind >= 0; --ind) { 74 | if (devices[ind].udid === uid) { 75 | device = devices[ind]; 76 | break; 77 | } 78 | } 79 | 80 | return device; 81 | } 82 | 83 | 84 | _getDeviceWithStatus(status: string): DeviceType | void { 85 | const devices = this._getAvailableDevices(); 86 | let device; 87 | 88 | for (let ind = devices.length - 1; ind >= 0; --ind) { 89 | if (devices[ind].state === status) { 90 | device = devices[ind]; 91 | break; 92 | } 93 | } 94 | 95 | return device; 96 | } 97 | 98 | 99 | _getUid(name: string): string | void { 100 | const device = this._getDeviceByName(name); 101 | log.v(TAG, `Device ${name} is:`, device); 102 | return device ? device.udid : undefined; 103 | } 104 | 105 | 106 | async _boot(uid: string): Promise { 107 | const device = this._getDeviceByUid(uid); 108 | if (!device) { 109 | throw new Error(`Invalid device uid [${uid}], cannot find it`); 110 | } 111 | if (device.state === 'Booted') { 112 | log.i(TAG, `Device [${device.name}] already booted`); 113 | return; 114 | } 115 | const response = exec(`xcrun simctl boot ${uid}`); 116 | if (response) { 117 | log.v(TAG, 'boot response:', response); 118 | } 119 | } 120 | 121 | 122 | async _open(uid: string): Promise { 123 | const activeXcode = exec('xcode-select -p').trim(); 124 | log.v(TAG, `Active Xcode: ${activeXcode}`); 125 | const simulatorApp = `${activeXcode}/Applications/Simulator.app`; 126 | log.v(TAG, `starting ${simulatorApp}`); 127 | exec(`open -a ${simulatorApp} --args -CurrentDeviceUDID ${uid}`); 128 | log.v(TAG, 'started'); 129 | } 130 | 131 | 132 | async start(params: StartParamsType): Promise { 133 | log.v(TAG, 'Starting device with params:', params); 134 | 135 | this.stop(); 136 | 137 | const uid = this._getUid(this._name); 138 | log.i(TAG, `Uid of the device is [${uid || '-'}]`); 139 | 140 | if (!uid) { 141 | throw new Error(`Invalid simulator [${this._name}], cannot find uid`); 142 | } 143 | 144 | await this._boot(uid); 145 | await this._open(uid); 146 | 147 | log.v(TAG, 'Device started', this._getDeviceByUid(uid)); 148 | } 149 | 150 | 151 | isAppInstalled(appName: string): boolean { 152 | log.v(`isAppInstalled: appName [${appName}]`); 153 | return false; 154 | } 155 | 156 | 157 | async installApp(appName: string, appFile: string): Promise { 158 | this.uninstallApp(appName); 159 | log.v(TAG, `Installing application [${appName}], appFile [${appFile}]`); 160 | exec(`xcrun simctl install booted ${appFile}`); 161 | } 162 | 163 | 164 | startApp(appName: string, activityName: string, locale?: string): void { 165 | const withLocale = locale ? `-AppleLanguages "(${locale})"` : ''; 166 | log.v(TAG, `startApp: appName [${appName}], activityName [${activityName}], locale [${locale || '-'}]`); 167 | exec(`xcrun simctl launch booted ${appName} ${withLocale}`); 168 | } 169 | 170 | 171 | async uninstallApp(appName: string): Promise { 172 | log.v(TAG, `Uninstalling application [${appName}]`); 173 | exec(`xcrun simctl uninstall booted ${appName}`); 174 | } 175 | 176 | 177 | async stop(): Promise { 178 | if (!this._canStopDevice) { 179 | log.v(TAG, 'Stopping device is restricted in config'); 180 | return; 181 | } 182 | 183 | log.v(TAG, 'Stopping all devices'); 184 | 185 | exec('osascript -e \'tell application "iOS Simulator" to quit\''); 186 | exec('osascript -e \'tell application "Simulator" to quit\''); 187 | 188 | let device = this._getDeviceWithStatus('Shutting Down'); 189 | 190 | while (device) { 191 | log.v(TAG, `Awaiting for shutdown completed (Device ${device.name} has ` 192 | + `state ${device.state})`); 193 | await delay(1000); 194 | device = this._getDeviceWithStatus('Shutting Down'); 195 | } 196 | 197 | log.v(TAG, 'Devices stopped'); 198 | } 199 | } 200 | 201 | export default IOSSimulator; 202 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidDevice.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | describe('AndroidDevice', () => { 3 | it('initialise', () => {}); 4 | }); 5 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidEmulator.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | jest.mock('child_process', () => ({ spawn: jest.fn() })); 3 | jest.mock('../../exec', () => jest.fn(() => '')); 4 | jest.mock('../../delay', () => jest.fn()); 5 | jest.mock('../../log', () => ({ 6 | v: jest.fn(), 7 | d: jest.fn(), 8 | e: jest.fn(), 9 | })); 10 | jest.mock('../AndroidEmulatorCmd', () => 'emulator'); 11 | 12 | const { spawn } = require('child_process'); 13 | 14 | const AndroidEmulator = require('../AndroidEmulator').default; 15 | const exec = require('../../exec'); 16 | const delay = require('../../delay'); 17 | 18 | describe('AndroidEmulator', () => { 19 | const name = 'emulator_name'; 20 | 21 | beforeEach(() => { 22 | jest.resetAllMocks(); 23 | }); 24 | 25 | it('initialise emulator', () => { 26 | // $FlowFixMe: ignore for mock 27 | exec.mockImplementationOnce(() => 'avd devices'); 28 | 29 | const emularor = new AndroidEmulator(name); 30 | expect(emularor).toMatchSnapshot(); 31 | }); 32 | 33 | it('start emulator when it is not available should throw error', async () => { 34 | // $FlowFixMe: ignore for mocks 35 | exec.mockImplementation(() => 'avd devices'); 36 | 37 | const emularor = new AndroidEmulator(name); 38 | let exception; 39 | 40 | try { 41 | await emularor.start(); 42 | } catch (err) { 43 | exception = err; 44 | } 45 | 46 | expect(exception).toMatchSnapshot(); 47 | }); 48 | 49 | it('start emulator when it is available but not started throws error if not started', async () => { 50 | // $FlowFixMe: ignore for mock 51 | exec.mockImplementation(() => `avd devices including ${name}`); 52 | const spawnMock = { 53 | stdout: { on: jest.fn() }, 54 | stderr: { on: jest.fn() }, 55 | on: jest.fn(), 56 | }; 57 | spawn.mockImplementationOnce(() => spawnMock); 58 | 59 | const emularor = new AndroidEmulator(name); 60 | 61 | let exception; 62 | 63 | try { 64 | await emularor.start(); 65 | } catch (err) { 66 | exception = err; 67 | } 68 | 69 | expect(exception).toMatchSnapshot(); 70 | }); 71 | 72 | it('start emulator when it is available but not started', async () => { 73 | // $FlowFixMe: ignore for mock 74 | exec.mockImplementation(() => `avd devices including ${name}`); 75 | const spawnMock = { 76 | stdout: { on: jest.fn() }, 77 | stderr: { on: jest.fn() }, 78 | on: jest.fn(), 79 | }; 80 | spawn.mockImplementationOnce(() => spawnMock); 81 | 82 | const emularor = new AndroidEmulator(name); 83 | 84 | const startPromise = emularor.start(); 85 | 86 | const dataCallback = spawnMock.stdout.on.mock.calls[0][1]; 87 | dataCallback('boot completed'); 88 | 89 | await startPromise; 90 | }); 91 | 92 | it('start emulator when it is available and already started should stop it before starting', async () => { 93 | // $FlowFixMe: ignore for mock 94 | exec.mockImplementation((cmd) => { 95 | if (cmd === 'emulator -avd -list-avds') { 96 | return `avd devices including ${name}`; 97 | } 98 | return 'List of devices attached\nemulator-5554 device'; 99 | }); 100 | const spawnMock = { 101 | stdout: { on: jest.fn() }, 102 | stderr: { on: jest.fn() }, 103 | on: jest.fn(), 104 | }; 105 | 106 | spawn.mockImplementationOnce(() => spawnMock); 107 | // $FlowFixMe: ignore for mocks 108 | delay.mockImplementation(() => { 109 | const spawnMockCalls = spawnMock.stdout.on.mock.calls; 110 | if (spawnMockCalls && spawnMockCalls[0] && spawnMockCalls[0][1]) { 111 | const dataCallback = spawnMock.stdout.on.mock.calls[0][1]; 112 | dataCallback('boot completed'); 113 | } 114 | }); 115 | 116 | const emularor = new AndroidEmulator(name); 117 | 118 | const startPromise = emularor.start(); 119 | 120 | await startPromise; 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidEmulatorCmd.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | describe('AndroidEmulatorCmd', () => { 4 | beforeEach(() => { 5 | process.env.ANDROID_EMULATOR = ''; 6 | jest.resetModules(); 7 | }); 8 | 9 | it('returns command provided via ANDROID_EMULATOR', () => { 10 | process.env.ANDROID_EMULATOR = 'cmdFrom_ANDROID_EMULATOR'; 11 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 12 | 13 | expect(emulatorCmd).toBe(process.env.ANDROID_EMULATOR); 14 | }); 15 | 16 | it('returns command available in PATH', () => { 17 | process.env.ANDROID_EMULATOR = ''; 18 | jest.mock('../../isCommand', () => () => true); 19 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 20 | 21 | expect(emulatorCmd).toBe('emulator'); 22 | }); 23 | 24 | it('gets command from /Library/Android/sdk/emulator/emulator on mac', () => { 25 | process.env.ANDROID_EMULATOR = ''; 26 | jest.mock('../../isCommand', () => () => false); 27 | jest.mock('../../exec', () => () => 'Darwin'); 28 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 29 | 30 | expect(emulatorCmd 31 | .indexOf('Library/Android/sdk/emulator/emulator') > 0).toBe(true); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/IosSimulator.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | describe('IOSSimulator', () => { 3 | it('initialise', () => {}); 4 | }); 5 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/__snapshots__/AndroidEmulator.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AndroidEmulator initialise emulator 1`] = ` 4 | AndroidEmulator { 5 | "_canStopDevice": false, 6 | "_name": "emulator_name", 7 | } 8 | `; 9 | 10 | exports[`AndroidEmulator start emulator when it is available but not started throws error if not started 1`] = `[Error: Device is not loaded in 30 seconds]`; 11 | 12 | exports[`AndroidEmulator start emulator when it is not available should throw error 1`] = `[Error: Invalid emulator emulator_name]`; 13 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/__snapshots__/deviceProvider.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`deviceProvider provide Android Emulator 1`] = ` 4 | _class { 5 | "_name": "AndroidEmulator", 6 | "deviceName": "test", 7 | } 8 | `; 9 | 10 | exports[`deviceProvider provide AndroidDevice 1`] = ` 11 | _class2 { 12 | "_name": "AndroidDevice", 13 | "deviceName": "test", 14 | } 15 | `; 16 | 17 | exports[`deviceProvider provide iOS device throws error (not implemented) 1`] = `[Error: iOS devices are not supported yet]`; 18 | 19 | exports[`deviceProvider provide iOS simulator 1`] = ` 20 | _class3 { 21 | "_name": "IosSimulator", 22 | "deviceName": "test", 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/deviceProvider.js: -------------------------------------------------------------------------------- 1 | import getDevice from '../deviceProvider'; 2 | 3 | jest.mock('../AndroidEmulator', () => (class { 4 | constructor(name) { 5 | this._name = 'AndroidEmulator'; 6 | this.deviceName = name; 7 | } 8 | })); 9 | jest.mock('../AndroidDevice', () => (class { 10 | constructor(name) { 11 | this._name = 'AndroidDevice'; 12 | this.deviceName = name; 13 | } 14 | })); 15 | jest.mock('../IosSimulator', () => (class { 16 | constructor(name) { 17 | this._name = 'IosSimulator'; 18 | this.deviceName = name; 19 | } 20 | })); 21 | 22 | describe('deviceProvider', () => { 23 | it('provide Android Emulator', () => { 24 | const device = getDevice('test', 'android'); 25 | expect(device).toMatchSnapshot(); 26 | }); 27 | 28 | it('provide AndroidDevice', () => { 29 | const device = getDevice('test', 'android', true); 30 | expect(device).toMatchSnapshot(); 31 | }); 32 | 33 | it('provide iOS simulator', () => { 34 | const device = getDevice('test', 'ios'); 35 | expect(device).toMatchSnapshot(); 36 | }); 37 | 38 | it('provide iOS device throws error (not implemented)', () => { 39 | let error; 40 | let device; 41 | try { 42 | device = getDevice('test', 'ios', true); 43 | } catch (err) { 44 | error = err; 45 | } 46 | 47 | expect(device).toBe(undefined); 48 | expect(error).toMatchSnapshot(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/runner/utils/device/deviceProvider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2019 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type { DeviceInterface } from './DeviceInterface'; 8 | 9 | import log from '../log'; 10 | import AndroidEmulator from './AndroidEmulator'; 11 | import AndroidDevice from './AndroidDevice'; 12 | import IosSimulator from './IosSimulator'; 13 | 14 | const TAG = 'PIXELS_CATCHER::DEVICE_PROVIDER'; 15 | 16 | export default ( 17 | name: string, 18 | platform: string, 19 | isPhysicalDevice?: boolean, 20 | canStopDevice: boolean = true, 21 | ): DeviceInterface => { 22 | if (platform === 'android') { 23 | return isPhysicalDevice 24 | ? new AndroidDevice(name) 25 | : new AndroidEmulator(name, canStopDevice); 26 | } 27 | 28 | if (!isPhysicalDevice) { 29 | return new IosSimulator(name, canStopDevice); 30 | } 31 | 32 | log.e(TAG, 'iOS devices are not supported yet'); 33 | throw new Error('iOS devices are not supported yet'); 34 | }; 35 | -------------------------------------------------------------------------------- /src/runner/utils/exec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as childProcess from 'child_process'; 8 | 9 | import log from './log'; 10 | 11 | const TAG = 'PIXELS_CATCHER::UTIL_EXEC'; 12 | 13 | export default function exec(cmd: string): string { 14 | let result = ''; 15 | 16 | try { 17 | result = childProcess.execSync(cmd).toString(); 18 | } catch (err) { 19 | log.e(TAG, `Failed to execute [${cmd}], error: [${err instanceof Error ? err.message : 'Unknown error'}]`, err); 20 | } 21 | 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /src/runner/utils/isCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import exec from './exec'; 8 | 9 | function isCommand(cmd: string): boolean { 10 | const out = exec(`whereis ${cmd}`); 11 | 12 | return Boolean(out.trim()); 13 | } 14 | 15 | export default isCommand; 16 | -------------------------------------------------------------------------------- /src/runner/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | const TAG = 'PIXELS_CATCHER::UTIL_LOG'; 8 | const logLevels: { 9 | [key: string]: number 10 | } = { 11 | v: 4, 12 | d: 3, 13 | i: 2, 14 | w: 1, 15 | e: 0, 16 | }; 17 | let activeLevel = logLevels.i; 18 | 19 | const log: { 20 | [key: string]: Function 21 | } = { 22 | v(tag: string, ...args: any) { 23 | if (activeLevel >= logLevels.v) { 24 | global.console.log(`${tag}:`, ...args); 25 | } 26 | }, 27 | 28 | d: (tag: string, ...args: any) => { 29 | if (activeLevel >= logLevels.d) { 30 | global.console.log(`${tag}:`, ...args); 31 | } 32 | }, 33 | 34 | i: (tag: string, ...args: any) => { 35 | if (activeLevel >= logLevels.i) { 36 | global.console.log(`${tag}:`, ...args); 37 | } 38 | }, 39 | 40 | w: (tag: string, ...args: any) => { 41 | if (activeLevel >= logLevels.w) { 42 | global.console.log(`${tag} WARNING:`, ...args); 43 | } 44 | }, 45 | 46 | e: (tag: string, ...args: any) => { 47 | if (activeLevel >= logLevels.e) { 48 | global.console.log(`${tag} ERROR:`, ...args); 49 | } 50 | }, 51 | 52 | setLevel(level: string | undefined = 'i') { 53 | let nextLevel = logLevels[level]; 54 | if (nextLevel === undefined) { 55 | global.console.log(`${TAG} WARNING:`, `Invalid level [${level}]. Supported levels: ${Object.keys(logLevels).join(', ')}`); 56 | nextLevel = logLevels.i; 57 | } 58 | activeLevel = nextLevel; 59 | }, 60 | }; 61 | 62 | export default log; 63 | -------------------------------------------------------------------------------- /src/runner/utils/readConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as fs from 'fs'; 8 | import * as path from 'path'; 9 | 10 | import log from './log'; 11 | 12 | const TAG = 'PIXELS_CATCHER::UTIL_READ_CONFIG'; 13 | const CONFIG_FILE = 'pixels-catcher.json'; 14 | const PACKAGE_JSON_FILE = 'package.json'; 15 | 16 | const readConfigFromPackageJSON = (): any => { 17 | const projectPackageFile = path.join(process.cwd(), PACKAGE_JSON_FILE); 18 | 19 | if (!fs.existsSync(projectPackageFile)) { 20 | log.e(TAG, `Cannot find ${PACKAGE_JSON_FILE} file [${projectPackageFile}]. ` 21 | + 'Check that you started the script from the root of your application'); 22 | process.exit(-1); 23 | } 24 | 25 | const fileContent = fs.readFileSync(projectPackageFile, 'utf8'); 26 | 27 | return JSON.parse(fileContent).PixelsCatcher; 28 | }; 29 | 30 | const readConfigFromFile = (): any => { 31 | const configFile = path.join(process.cwd(), CONFIG_FILE); 32 | 33 | if (!fs.existsSync(configFile)) { 34 | log.w(TAG, `Cannot find [${configFile}] file`); 35 | return undefined; 36 | } 37 | 38 | const fileContent = fs.readFileSync(configFile, 'utf8'); 39 | 40 | return JSON.parse(fileContent); 41 | }; 42 | 43 | export default (): any => { 44 | const pixelsCatcherConfig = readConfigFromPackageJSON() 45 | || readConfigFromFile(); 46 | 47 | if (!pixelsCatcherConfig) { 48 | log.e(TAG, 'Cannot find "PixelsCatcher" in package.json or find ' 49 | + 'pixels-catcher.json file'); 50 | process.exit(-1); 51 | } 52 | 53 | return pixelsCatcherConfig; 54 | }; 55 | -------------------------------------------------------------------------------- /src/runner/utils/timeToSec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const timeToSec = (ms: number): number => { 9 | const sec = ms / 1000; 10 | return Math.round(sec * 1000) / 1000; 11 | }; 12 | 13 | export default timeToSec; 14 | --------------------------------------------------------------------------------
This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.demo; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceEventListener; 23 | import com.facebook.react.ReactInstanceManager; 24 | import com.facebook.react.bridge.ReactContext; 25 | import com.facebook.react.modules.network.NetworkingModule; 26 | import okhttp3.OkHttpClient; 27 | 28 | public class ReactNativeFlipper { 29 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 30 | if (FlipperUtils.shouldEnableFlipper(context)) { 31 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 32 | 33 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 34 | client.addPlugin(new ReactFlipperPlugin()); 35 | client.addPlugin(new DatabasesFlipperPlugin(context)); 36 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 37 | client.addPlugin(CrashReporterPlugin.getInstance()); 38 | 39 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 40 | NetworkingModule.setCustomClientBuilder( 41 | new NetworkingModule.CustomClientBuilder() { 42 | @Override 43 | public void apply(OkHttpClient.Builder builder) { 44 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 45 | } 46 | }); 47 | client.addPlugin(networkFlipperPlugin); 48 | client.start(); 49 | 50 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 51 | // Hence we run if after all native modules have been initialized 52 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 53 | if (reactContext == null) { 54 | reactInstanceManager.addReactInstanceEventListener( 55 | new ReactInstanceEventListener() { 56 | @Override 57 | public void onReactContextInitialized(ReactContext reactContext) { 58 | reactInstanceManager.removeReactInstanceEventListener(this); 59 | reactContext.runOnNativeModulesQueueThread( 60 | new Runnable() { 61 | @Override 62 | public void run() { 63 | client.addPlugin(new FrescoFlipperPlugin()); 64 | } 65 | }); 66 | } 67 | }); 68 | } else { 69 | client.addPlugin(new FrescoFlipperPlugin()); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /demo/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.demo; 2 | 3 | import android.os.Bundle; 4 | import com.facebook.react.ReactActivity; 5 | import com.facebook.react.ReactActivityDelegate; 6 | import com.facebook.react.ReactRootView; 7 | 8 | public class MainActivity extends ReactActivity { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | @Override 15 | protected String getMainComponentName() { 16 | return "demo"; 17 | } 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(null); 22 | } 23 | 24 | /** 25 | * Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and 26 | * you can specify the renderer you wish to use - the new renderer (Fabric) or the old renderer 27 | * (Paper). 28 | */ 29 | @Override 30 | protected ReactActivityDelegate createReactActivityDelegate() { 31 | return new MainActivityDelegate(this, getMainComponentName()); 32 | } 33 | 34 | public static class MainActivityDelegate extends ReactActivityDelegate { 35 | public MainActivityDelegate(ReactActivity activity, String mainComponentName) { 36 | super(activity, mainComponentName); 37 | } 38 | 39 | @Override 40 | protected ReactRootView createRootView() { 41 | ReactRootView reactRootView = new ReactRootView(getContext()); 42 | // If you opted-in for the New Architecture, we enable the Fabric Renderer. 43 | reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED); 44 | return reactRootView; 45 | } 46 | 47 | @Override 48 | protected boolean isConcurrentRootEnabled() { 49 | // If you opted-in for the New Architecture, we enable Concurrent Root (i.e. React 18). 50 | // More on this on https://reactjs.org/blog/2022/03/29/react-v18.html 51 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.demo; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.react.config.ReactFeatureFlags; 11 | import com.facebook.soloader.SoLoader; 12 | import com.demo.newarchitecture.MainApplicationReactNativeHost; 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.util.List; 15 | 16 | public class MainApplication extends Application implements ReactApplication { 17 | 18 | private final ReactNativeHost mReactNativeHost = 19 | new ReactNativeHost(this) { 20 | @Override 21 | public boolean getUseDeveloperSupport() { 22 | return BuildConfig.DEBUG; 23 | } 24 | 25 | @Override 26 | protected List getPackages() { 27 | @SuppressWarnings("UnnecessaryLocalVariable") 28 | List packages = new PackageList(this).getPackages(); 29 | // Packages that cannot be autolinked yet can be added manually here, for example: 30 | // packages.add(new MyReactNativePackage()); 31 | return packages; 32 | } 33 | 34 | @Override 35 | protected String getJSMainModuleName() { 36 | return "index"; 37 | } 38 | }; 39 | 40 | private final ReactNativeHost mNewArchitectureNativeHost = 41 | new MainApplicationReactNativeHost(this); 42 | 43 | @Override 44 | public ReactNativeHost getReactNativeHost() { 45 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 46 | return mNewArchitectureNativeHost; 47 | } else { 48 | return mReactNativeHost; 49 | } 50 | } 51 | 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | // If you opted-in for the New Architecture, we enable the TurboModule system 56 | ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 57 | SoLoader.init(this, /* native exopackage */ false); 58 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 59 | } 60 | 61 | /** 62 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 63 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 64 | * 65 | * @param context 66 | * @param reactInstanceManager 67 | */ 68 | private static void initializeFlipper( 69 | Context context, ReactInstanceManager reactInstanceManager) { 70 | if (BuildConfig.DEBUG) { 71 | try { 72 | /* 73 | We use reflection here to pick up the class that initializes Flipper, 74 | since Flipper library is not available in release mode 75 | */ 76 | Class> aClass = Class.forName("com.demo.ReactNativeFlipper"); 77 | aClass 78 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 79 | .invoke(null, context, reactInstanceManager); 80 | } catch (ClassNotFoundException e) { 81 | e.printStackTrace(); 82 | } catch (NoSuchMethodException e) { 83 | e.printStackTrace(); 84 | } catch (IllegalAccessException e) { 85 | e.printStackTrace(); 86 | } catch (InvocationTargetException e) { 87 | e.printStackTrace(); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/newarchitecture/MainApplicationReactNativeHost.java: -------------------------------------------------------------------------------- 1 | package com.demo.newarchitecture; 2 | 3 | import android.app.Application; 4 | import androidx.annotation.NonNull; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactInstanceManager; 7 | import com.facebook.react.ReactNativeHost; 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate; 10 | import com.facebook.react.bridge.JSIModulePackage; 11 | import com.facebook.react.bridge.JSIModuleProvider; 12 | import com.facebook.react.bridge.JSIModuleSpec; 13 | import com.facebook.react.bridge.JSIModuleType; 14 | import com.facebook.react.bridge.JavaScriptContextHolder; 15 | import com.facebook.react.bridge.ReactApplicationContext; 16 | import com.facebook.react.bridge.UIManager; 17 | import com.facebook.react.fabric.ComponentFactory; 18 | import com.facebook.react.fabric.CoreComponentsRegistry; 19 | import com.facebook.react.fabric.FabricJSIModuleProvider; 20 | import com.facebook.react.fabric.ReactNativeConfig; 21 | import com.facebook.react.uimanager.ViewManagerRegistry; 22 | import com.demo.BuildConfig; 23 | import com.demo.newarchitecture.components.MainComponentsRegistry; 24 | import com.demo.newarchitecture.modules.MainApplicationTurboModuleManagerDelegate; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | /** 29 | * A {@link ReactNativeHost} that helps you load everything needed for the New Architecture, both 30 | * TurboModule delegates and the Fabric Renderer. 31 | * 32 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the 33 | * `newArchEnabled` property). Is ignored otherwise. 34 | */ 35 | public class MainApplicationReactNativeHost extends ReactNativeHost { 36 | public MainApplicationReactNativeHost(Application application) { 37 | super(application); 38 | } 39 | 40 | @Override 41 | public boolean getUseDeveloperSupport() { 42 | return BuildConfig.DEBUG; 43 | } 44 | 45 | @Override 46 | protected List getPackages() { 47 | List packages = new PackageList(this).getPackages(); 48 | // Packages that cannot be autolinked yet can be added manually here, for example: 49 | // packages.add(new MyReactNativePackage()); 50 | // TurboModules must also be loaded here providing a valid TurboReactPackage implementation: 51 | // packages.add(new TurboReactPackage() { ... }); 52 | // If you have custom Fabric Components, their ViewManagers should also be loaded here 53 | // inside a ReactPackage. 54 | return packages; 55 | } 56 | 57 | @Override 58 | protected String getJSMainModuleName() { 59 | return "index"; 60 | } 61 | 62 | @NonNull 63 | @Override 64 | protected ReactPackageTurboModuleManagerDelegate.Builder 65 | getReactPackageTurboModuleManagerDelegateBuilder() { 66 | // Here we provide the ReactPackageTurboModuleManagerDelegate Builder. This is necessary 67 | // for the new architecture and to use TurboModules correctly. 68 | return new MainApplicationTurboModuleManagerDelegate.Builder(); 69 | } 70 | 71 | @Override 72 | protected JSIModulePackage getJSIModulePackage() { 73 | return new JSIModulePackage() { 74 | @Override 75 | public List getJSIModules( 76 | final ReactApplicationContext reactApplicationContext, 77 | final JavaScriptContextHolder jsContext) { 78 | final List specs = new ArrayList<>(); 79 | 80 | // Here we provide a new JSIModuleSpec that will be responsible of providing the 81 | // custom Fabric Components. 82 | specs.add( 83 | new JSIModuleSpec() { 84 | @Override 85 | public JSIModuleType getJSIModuleType() { 86 | return JSIModuleType.UIManager; 87 | } 88 | 89 | @Override 90 | public JSIModuleProvider getJSIModuleProvider() { 91 | final ComponentFactory componentFactory = new ComponentFactory(); 92 | CoreComponentsRegistry.register(componentFactory); 93 | 94 | // Here we register a Components Registry. 95 | // The one that is generated with the template contains no components 96 | // and just provides you the one from React Native core. 97 | MainComponentsRegistry.register(componentFactory); 98 | 99 | final ReactInstanceManager reactInstanceManager = getReactInstanceManager(); 100 | 101 | ViewManagerRegistry viewManagerRegistry = 102 | new ViewManagerRegistry( 103 | reactInstanceManager.getOrCreateViewManagers(reactApplicationContext)); 104 | 105 | return new FabricJSIModuleProvider( 106 | reactApplicationContext, 107 | componentFactory, 108 | ReactNativeConfig.DEFAULT_CONFIG, 109 | viewManagerRegistry); 110 | } 111 | }); 112 | return specs; 113 | } 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/newarchitecture/components/MainComponentsRegistry.java: -------------------------------------------------------------------------------- 1 | package com.demo.newarchitecture.components; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.proguard.annotations.DoNotStrip; 5 | import com.facebook.react.fabric.ComponentFactory; 6 | import com.facebook.soloader.SoLoader; 7 | 8 | /** 9 | * Class responsible to load the custom Fabric Components. This class has native methods and needs a 10 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 11 | * folder for you). 12 | * 13 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the 14 | * `newArchEnabled` property). Is ignored otherwise. 15 | */ 16 | @DoNotStrip 17 | public class MainComponentsRegistry { 18 | static { 19 | SoLoader.loadLibrary("fabricjni"); 20 | } 21 | 22 | @DoNotStrip private final HybridData mHybridData; 23 | 24 | @DoNotStrip 25 | private native HybridData initHybrid(ComponentFactory componentFactory); 26 | 27 | @DoNotStrip 28 | private MainComponentsRegistry(ComponentFactory componentFactory) { 29 | mHybridData = initHybrid(componentFactory); 30 | } 31 | 32 | @DoNotStrip 33 | public static MainComponentsRegistry register(ComponentFactory componentFactory) { 34 | return new MainComponentsRegistry(componentFactory); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java: -------------------------------------------------------------------------------- 1 | package com.demo.newarchitecture.modules; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.soloader.SoLoader; 8 | import java.util.List; 9 | 10 | /** 11 | * Class responsible to load the TurboModules. This class has native methods and needs a 12 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 13 | * folder for you). 14 | * 15 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the 16 | * `newArchEnabled` property). Is ignored otherwise. 17 | */ 18 | public class MainApplicationTurboModuleManagerDelegate 19 | extends ReactPackageTurboModuleManagerDelegate { 20 | 21 | private static volatile boolean sIsSoLibraryLoaded; 22 | 23 | protected MainApplicationTurboModuleManagerDelegate( 24 | ReactApplicationContext reactApplicationContext, List packages) { 25 | super(reactApplicationContext, packages); 26 | } 27 | 28 | protected native HybridData initHybrid(); 29 | 30 | native boolean canCreateTurboModule(String moduleName); 31 | 32 | public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder { 33 | protected MainApplicationTurboModuleManagerDelegate build( 34 | ReactApplicationContext context, List packages) { 35 | return new MainApplicationTurboModuleManagerDelegate(context, packages); 36 | } 37 | } 38 | 39 | @Override 40 | protected synchronized void maybeLoadOtherSoLibraries() { 41 | if (!sIsSoLibraryLoaded) { 42 | // If you change the name of your application .so file in the Android.mk file, 43 | // make sure you update the name here as well. 44 | SoLoader.loadLibrary("demo_appmodules"); 45 | sIsSoLibraryLoaded = true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | # Define the library name here. 4 | project(demo_appmodules) 5 | 6 | # This file includes all the necessary to let you build your application with the New Architecture. 7 | include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake) 8 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationModuleProvider.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationModuleProvider.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace facebook { 7 | namespace react { 8 | 9 | std::shared_ptr MainApplicationModuleProvider( 10 | const std::string &moduleName, 11 | const JavaTurboModule::InitParams ¶ms) { 12 | // Here you can provide your own module provider for TurboModules coming from 13 | // either your application or from external libraries. The approach to follow 14 | // is similar to the following (for a library called `samplelibrary`: 15 | // 16 | // auto module = samplelibrary_ModuleProvider(moduleName, params); 17 | // if (module != nullptr) { 18 | // return module; 19 | // } 20 | // return rncore_ModuleProvider(moduleName, params); 21 | 22 | // Module providers autolinked by RN CLI 23 | auto rncli_module = rncli_ModuleProvider(moduleName, params); 24 | if (rncli_module != nullptr) { 25 | return rncli_module; 26 | } 27 | 28 | return rncore_ModuleProvider(moduleName, params); 29 | } 30 | 31 | } // namespace react 32 | } // namespace facebook 33 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationModuleProvider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | std::shared_ptr MainApplicationModuleProvider( 12 | const std::string &moduleName, 13 | const JavaTurboModule::InitParams ¶ms); 14 | 15 | } // namespace react 16 | } // namespace facebook 17 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationTurboModuleManagerDelegate.h" 2 | #include "MainApplicationModuleProvider.h" 3 | 4 | namespace facebook { 5 | namespace react { 6 | 7 | jni::local_ref 8 | MainApplicationTurboModuleManagerDelegate::initHybrid( 9 | jni::alias_ref) { 10 | return makeCxxInstance(); 11 | } 12 | 13 | void MainApplicationTurboModuleManagerDelegate::registerNatives() { 14 | registerHybrid({ 15 | makeNativeMethod( 16 | "initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid), 17 | makeNativeMethod( 18 | "canCreateTurboModule", 19 | MainApplicationTurboModuleManagerDelegate::canCreateTurboModule), 20 | }); 21 | } 22 | 23 | std::shared_ptr 24 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 25 | const std::string &name, 26 | const std::shared_ptr &jsInvoker) { 27 | // Not implemented yet: provide pure-C++ NativeModules here. 28 | return nullptr; 29 | } 30 | 31 | std::shared_ptr 32 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 33 | const std::string &name, 34 | const JavaTurboModule::InitParams ¶ms) { 35 | return MainApplicationModuleProvider(name, params); 36 | } 37 | 38 | bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule( 39 | const std::string &name) { 40 | return getTurboModule(name, nullptr) != nullptr || 41 | getTurboModule(name, {.moduleName = name}) != nullptr; 42 | } 43 | 44 | } // namespace react 45 | } // namespace facebook 46 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | namespace facebook { 8 | namespace react { 9 | 10 | class MainApplicationTurboModuleManagerDelegate 11 | : public jni::HybridClass< 12 | MainApplicationTurboModuleManagerDelegate, 13 | TurboModuleManagerDelegate> { 14 | public: 15 | // Adapt it to the package you used for your Java class. 16 | static constexpr auto kJavaDescriptor = 17 | "Lcom/demo/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;"; 18 | 19 | static jni::local_ref initHybrid(jni::alias_ref); 20 | 21 | static void registerNatives(); 22 | 23 | std::shared_ptr getTurboModule( 24 | const std::string &name, 25 | const std::shared_ptr &jsInvoker) override; 26 | std::shared_ptr getTurboModule( 27 | const std::string &name, 28 | const JavaTurboModule::InitParams ¶ms) override; 29 | 30 | /** 31 | * Test-only method. Allows user to verify whether a TurboModule can be 32 | * created by instances of this class. 33 | */ 34 | bool canCreateTurboModule(const std::string &name); 35 | }; 36 | 37 | } // namespace react 38 | } // namespace facebook 39 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainComponentsRegistry.cpp: -------------------------------------------------------------------------------- 1 | #include "MainComponentsRegistry.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace facebook { 10 | namespace react { 11 | 12 | MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {} 13 | 14 | std::shared_ptr 15 | MainComponentsRegistry::sharedProviderRegistry() { 16 | auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry(); 17 | 18 | // Autolinked providers registered by RN CLI 19 | rncli_registerProviders(providerRegistry); 20 | 21 | // Custom Fabric Components go here. You can register custom 22 | // components coming from your App or from 3rd party libraries here. 23 | // 24 | // providerRegistry->add(concreteComponentDescriptorProvider< 25 | // AocViewerComponentDescriptor>()); 26 | return providerRegistry; 27 | } 28 | 29 | jni::local_ref 30 | MainComponentsRegistry::initHybrid( 31 | jni::alias_ref, 32 | ComponentFactory *delegate) { 33 | auto instance = makeCxxInstance(delegate); 34 | 35 | auto buildRegistryFunction = 36 | [](EventDispatcher::Weak const &eventDispatcher, 37 | ContextContainer::Shared const &contextContainer) 38 | -> ComponentDescriptorRegistry::Shared { 39 | auto registry = MainComponentsRegistry::sharedProviderRegistry() 40 | ->createComponentDescriptorRegistry( 41 | {eventDispatcher, contextContainer}); 42 | 43 | auto mutableRegistry = 44 | std::const_pointer_cast(registry); 45 | 46 | mutableRegistry->setFallbackComponentDescriptor( 47 | std::make_shared( 48 | ComponentDescriptorParameters{ 49 | eventDispatcher, contextContainer, nullptr})); 50 | 51 | return registry; 52 | }; 53 | 54 | delegate->buildRegistryFunction = buildRegistryFunction; 55 | return instance; 56 | } 57 | 58 | void MainComponentsRegistry::registerNatives() { 59 | registerHybrid({ 60 | makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid), 61 | }); 62 | } 63 | 64 | } // namespace react 65 | } // namespace facebook 66 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainComponentsRegistry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | class MainComponentsRegistry 12 | : public facebook::jni::HybridClass { 13 | public: 14 | // Adapt it to the package you used for your Java class. 15 | constexpr static auto kJavaDescriptor = 16 | "Lcom/demo/newarchitecture/components/MainComponentsRegistry;"; 17 | 18 | static void registerNatives(); 19 | 20 | MainComponentsRegistry(ComponentFactory *delegate); 21 | 22 | private: 23 | static std::shared_ptr 24 | sharedProviderRegistry(); 25 | 26 | static jni::local_ref initHybrid( 27 | jni::alias_ref, 28 | ComponentFactory *delegate); 29 | }; 30 | 31 | } // namespace react 32 | } // namespace facebook 33 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/OnLoad.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "MainApplicationTurboModuleManagerDelegate.h" 3 | #include "MainComponentsRegistry.h" 4 | 5 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { 6 | return facebook::jni::initialize(vm, [] { 7 | facebook::react::MainApplicationTurboModuleManagerDelegate:: 8 | registerNatives(); 9 | facebook::react::MainComponentsRegistry::registerNatives(); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | demo 3 | 4 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "33.0.0" 6 | minSdkVersion = 21 7 | compileSdkVersion = 33 8 | targetSdkVersion = 33 9 | 10 | if (System.properties['os.arch'] == "aarch64") { 11 | // For M1 Users we need to use the NDK 24 which added support for aarch64 12 | ndkVersion = "24.0.8215888" 13 | } else { 14 | // Otherwise we default to the side-by-side NDK version from AGP. 15 | ndkVersion = "21.4.7075529" 16 | } 17 | } 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | dependencies { 23 | classpath("com.android.tools.build:gradle:7.2.1") 24 | classpath("com.facebook.react:react-native-gradle-plugin") 25 | classpath("de.undercouch:gradle-download-task:5.0.1") 26 | // NOTE: Do not place your application dependencies here; they belong 27 | // in the individual module build.gradle files 28 | } 29 | } 30 | 31 | allprojects { 32 | repositories { 33 | maven { 34 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 35 | url("$rootDir/../node_modules/react-native/android") 36 | } 37 | maven { 38 | // Android JSC is installed from npm 39 | url("$rootDir/../node_modules/jsc-android/dist") 40 | } 41 | mavenCentral { 42 | // We don't want to fetch react-native from Maven Central as there are 43 | // older versions over there. 44 | content { 45 | excludeGroup "com.facebook.react" 46 | } 47 | } 48 | google() 49 | maven { url 'https://www.jitpack.io' } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /demo/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.125.0 29 | 30 | # Use this property to specify which architecture you want to build. 31 | # You can also override it from the CLI using 32 | # ./gradlew -PreactNativeArchitectures=x86_64 33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 34 | 35 | # Use this property to enable support to the new architecture. 36 | # This will allow you to use TurboModules and the Fabric render in 37 | # your application. You should enable this flag either if you want 38 | # to write custom TurboModules/Fabric components OR use libraries that 39 | # are providing them. 40 | newArchEnabled=false 41 | -------------------------------------------------------------------------------- /demo/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /demo/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /demo/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/react-native-gradle-plugin') 5 | 6 | if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") { 7 | include(":ReactAndroid") 8 | project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid') 9 | include(":ReactAndroid:hermes-engine") 10 | project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine') 11 | } 12 | -------------------------------------------------------------------------------- /demo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "displayName": "demo" 4 | } -------------------------------------------------------------------------------- /demo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './App'; 7 | import AppWithNavigation from './AppWithNavigation'; 8 | import {name as appName} from './app.json'; 9 | 10 | const snapshots = true; 11 | const checkNavigation = false 12 | 13 | if (snapshots) { 14 | require('./indexSnapshot'); 15 | } else if (checkNavigation) { 16 | AppRegistry.registerComponent(appName, () => AppWithNavigation); 17 | } else { 18 | AppRegistry.registerComponent(appName, () => App); 19 | } 20 | -------------------------------------------------------------------------------- /demo/indexSnapshot.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, Text, View } from 'react-native'; 3 | import { WebView } from 'react-native-webview'; 4 | import { registerSnapshot, runSnapshots, Snapshot } from 'pixels-catcher'; 5 | import { NavigationContainer } from '@react-navigation/native'; 6 | import { createStackNavigator } from '@react-navigation/stack'; 7 | 8 | import App from './App'; 9 | import { HomeScreen } from './AppWithNavigation'; 10 | import { name as appName } from './app.json'; 11 | 12 | const baseUrl = Platform.select({ 13 | // Put real IP of your server to run on real device 14 | android: 'http://10.0.2.2:3000', 15 | ios: 'http://127.0.0.1:3000', 16 | }); 17 | const useFailedTest = false; 18 | 19 | const appSnapshot = false; 20 | 21 | if (appSnapshot) { 22 | registerSnapshot( 23 | class SnapshotClass extends Snapshot { 24 | static snapshotName = 'AppSnapshot'; 25 | 26 | componentDidMount() { 27 | setTimeout(() => { 28 | // delay for rendering images 29 | this.props.onReady(); 30 | }, 1000); 31 | } 32 | 33 | renderContent() { 34 | return ; 35 | } 36 | }, 37 | ); 38 | } 39 | 40 | registerSnapshot( 41 | class SnapshotClass extends Snapshot { 42 | static snapshotName = 'HomeScreen'; 43 | 44 | renderContent() { 45 | return ; 46 | } 47 | }, 48 | ); 49 | 50 | if (useFailedTest) { 51 | registerSnapshot( 52 | class SnapshotClass extends Snapshot { 53 | static snapshotName = 'AppSnapshotWithWrongRefImg'; 54 | 55 | renderContent() { 56 | return ; 57 | } 58 | }, 59 | ); 60 | } 61 | 62 | registerSnapshot( 63 | class SnapshotClass extends Snapshot { 64 | static snapshotName = 'someComponent'; 65 | 66 | renderContent() { 67 | return ( 68 | 69 | Some component 70 | 71 | ); 72 | } 73 | }, 74 | ); 75 | 76 | const useWebView = false; 77 | 78 | if (useWebView) { 79 | registerSnapshot( 80 | class SnapshotClass extends Snapshot { 81 | static snapshotName = 'WebViewTest'; 82 | 83 | componentDidMount() { 84 | // override default componentDidMount from Snapshot to delay it 85 | // until WebView is loaded. onLoad from WebView is used 86 | } 87 | 88 | renderContent() { 89 | return ( 90 | { 96 | setTimeout(() => { 97 | this.props.onReady(); 98 | }, 50); 99 | }} 100 | /> 101 | ); 102 | } 103 | }, 104 | ); 105 | } 106 | 107 | registerSnapshot( 108 | class SnapshotClass extends Snapshot { 109 | static snapshotName = 'longContent'; 110 | 111 | renderContent() { 112 | return ( 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | ); 121 | } 122 | }, 123 | ); 124 | 125 | const Stack = createStackNavigator(); 126 | 127 | function getRootElement(SnapshotsContainer) { 128 | const RootElement = ({ children }) => ( 129 | 130 | 131 | 139 | 140 | 141 | ); 142 | return RootElement; 143 | } 144 | 145 | runSnapshots(appName, { baseUrl, getRootElement }); 146 | -------------------------------------------------------------------------------- /demo/ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, '12.4' 5 | install! 'cocoapods', :deterministic_uuids => false 6 | 7 | target 'demo' do 8 | config = use_native_modules! 9 | 10 | # Flags change depending on the env values. 11 | flags = get_default_flags() 12 | 13 | use_react_native!( 14 | :path => config[:reactNativePath], 15 | # Hermes is now enabled by default. Disable by setting this flag to false. 16 | # Upcoming versions of React Native may rely on get_default_flags(), but 17 | # we make it explicit here to aid in the React Native upgrade process. 18 | :hermes_enabled => true, 19 | :fabric_enabled => flags[:fabric_enabled], 20 | # Enables Flipper. 21 | # 22 | # Note that if you have use_frameworks! enabled, Flipper will not work and 23 | # you should disable the next line. 24 | # :flipper_configuration => FlipperConfiguration.enabled, 25 | # An absolute path to your application root. 26 | :app_path => "#{Pod::Config.instance.installation_root}/.." 27 | ) 28 | 29 | target 'demoTests' do 30 | inherit! :complete 31 | # Pods for testing 32 | end 33 | 34 | post_install do |installer| 35 | react_native_post_install( 36 | installer, 37 | # Set `mac_catalyst_enabled` to `true` in order to apply patches 38 | # necessary for Mac Catalyst builds 39 | :mac_catalyst_enabled => false 40 | ) 41 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /demo/ios/_xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /demo/ios/demo.xcodeproj/xcshareddata/xcschemes/demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /demo/ios/demo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/ios/demo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/ios/demo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /demo/ios/demo/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | #import 8 | 9 | #if RCT_NEW_ARCH_ENABLED 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | 17 | #import 18 | 19 | static NSString *const kRNConcurrentRoot = @"concurrentRoot"; 20 | 21 | @interface AppDelegate () { 22 | RCTTurboModuleManager *_turboModuleManager; 23 | RCTSurfacePresenterBridgeAdapter *_bridgeAdapter; 24 | std::shared_ptr _reactNativeConfig; 25 | facebook::react::ContextContainer::Shared _contextContainer; 26 | } 27 | @end 28 | #endif 29 | 30 | @implementation AppDelegate 31 | 32 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 33 | { 34 | RCTAppSetupPrepareApp(application); 35 | 36 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 37 | 38 | #if RCT_NEW_ARCH_ENABLED 39 | _contextContainer = std::make_shared(); 40 | _reactNativeConfig = std::make_shared(); 41 | _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); 42 | _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; 43 | bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; 44 | #endif 45 | 46 | NSDictionary *initProps = [self prepareInitialProps]; 47 | UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"demo", initProps); 48 | 49 | if (@available(iOS 13.0, *)) { 50 | rootView.backgroundColor = [UIColor systemBackgroundColor]; 51 | } else { 52 | rootView.backgroundColor = [UIColor whiteColor]; 53 | } 54 | 55 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 56 | UIViewController *rootViewController = [UIViewController new]; 57 | rootViewController.view = rootView; 58 | self.window.rootViewController = rootViewController; 59 | [self.window makeKeyAndVisible]; 60 | return YES; 61 | } 62 | 63 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. 64 | /// 65 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html 66 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). 67 | /// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`. 68 | - (BOOL)concurrentRootEnabled 69 | { 70 | // Switch this bool to turn on and off the concurrent root 71 | return true; 72 | } 73 | 74 | - (NSDictionary *)prepareInitialProps 75 | { 76 | NSMutableDictionary *initProps = [NSMutableDictionary new]; 77 | 78 | #ifdef RCT_NEW_ARCH_ENABLED 79 | initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]); 80 | #endif 81 | 82 | return initProps; 83 | } 84 | 85 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 86 | { 87 | #if DEBUG 88 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 89 | #else 90 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 91 | #endif 92 | } 93 | 94 | #if RCT_NEW_ARCH_ENABLED 95 | 96 | #pragma mark - RCTCxxBridgeDelegate 97 | 98 | - (std::unique_ptr)jsExecutorFactoryForBridge:(RCTBridge *)bridge 99 | { 100 | _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge 101 | delegate:self 102 | jsInvoker:bridge.jsCallInvoker]; 103 | return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager); 104 | } 105 | 106 | #pragma mark RCTTurboModuleManagerDelegate 107 | 108 | - (Class)getModuleClassFromName:(const char *)name 109 | { 110 | return RCTCoreModulesClassProvider(name); 111 | } 112 | 113 | - (std::shared_ptr)getTurboModule:(const std::string &)name 114 | jsInvoker:(std::shared_ptr)jsInvoker 115 | { 116 | return nullptr; 117 | } 118 | 119 | - (std::shared_ptr)getTurboModule:(const std::string &)name 120 | initParams: 121 | (const facebook::react::ObjCTurboModule::InitParams &)params 122 | { 123 | return nullptr; 124 | } 125 | 126 | - (id)getModuleInstanceFromClass:(Class)moduleClass 127 | { 128 | return RCTAppSetupDefaultModuleFromClass(moduleClass); 129 | } 130 | 131 | #endif 132 | 133 | @end 134 | -------------------------------------------------------------------------------- /demo/ios/demo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /demo/ios/demo/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/ios/demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | demo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | localhost 32 | 33 | NSExceptionAllowsInsecureHTTPLoads 34 | 35 | 36 | 37 | 38 | NSLocationWhenInUseUsageDescription 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /demo/ios/demo/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/ios/demo/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/ios/demoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/ios/demoTests/demoTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface demoTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation demoTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /demo/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: true, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "cd ios/ && pod install", 7 | "android": "react-native run-android", 8 | "preinstall": "cd .. && rm -rf pixels-catcher-*.tgz && npm i && npm pack && mv pixels-catcher-*.tgz pixels-catcher.tgz", 9 | "ios": "react-native run-ios", 10 | "start": "react-native start", 11 | "test": "jest", 12 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx" 13 | }, 14 | "dependencies": { 15 | "@react-navigation/native": "^6.0.12", 16 | "@react-navigation/stack": "^6.2.3", 17 | "react": "18.1.0", 18 | "react-native": "0.70.0", 19 | "react-native-gesture-handler": "^2.6.0", 20 | "react-native-safe-area-context": "^4.3.3", 21 | "react-native-save-view": "^0.2.3", 22 | "react-native-screens": "^3.17.0", 23 | "react-native-webview": "^11.23.1" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.12.9", 27 | "@babel/runtime": "^7.12.5", 28 | "@react-native-community/eslint-config": "^2.0.0", 29 | "@tsconfig/react-native": "^2.0.2", 30 | "@types/jest": "^26.0.23", 31 | "@types/react-native": "^0.70.0", 32 | "@types/react-test-renderer": "^18.0.0", 33 | "@typescript-eslint/eslint-plugin": "^5.37.0", 34 | "@typescript-eslint/parser": "^5.37.0", 35 | "babel-jest": "^26.6.3", 36 | "eslint": "^7.32.0", 37 | "jest": "^26.6.3", 38 | "metro-react-native-babel-preset": "^0.72.1", 39 | "pixels-catcher": "../pixels-catcher.tgz", 40 | "react-test-renderer": "18.1.0", 41 | "typescript": "^4.8.3" 42 | }, 43 | "jest": { 44 | "preset": "react-native", 45 | "moduleFileExtensions": [ 46 | "ts", 47 | "tsx", 48 | "js", 49 | "jsx", 50 | "json", 51 | "node" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo/pixels-catcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "android": { 3 | "activityName": "com.demo.MainActivity", 4 | "deviceName": "Nexus_5X", 5 | "packageName": "com.demo", 6 | "snapshotsPath": "./snapshots/android", 7 | "dev": { 8 | "deviceParams": [ 9 | "-no-audio", 10 | "-no-snapshot" 11 | ] 12 | }, 13 | "debug": { 14 | "deviceParams": [ 15 | "-no-audio", 16 | "-no-snapshot" 17 | ], 18 | "canStopDevice": false, 19 | "appFile": "./android/app/build/outputs/apk/debug/app-debug.apk" 20 | }, 21 | "test": { 22 | "port": 3000, 23 | "canStopDevice": false, 24 | "deviceParams": [ 25 | "-no-audio", 26 | "-no-snapshot" 27 | ], 28 | "appFile": "./android/app/build/outputs/apk/debug/app-debug.apk" 29 | }, 30 | "release": { 31 | "deviceParams": [ 32 | "-no-audio", 33 | "-no-snapshot", 34 | "-no-window" 35 | ], 36 | "appFile": "./android/app/build/outputs/apk/release/app-release.apk" 37 | } 38 | }, 39 | "ios": { 40 | "deviceName": "iPhone 14 Plus", 41 | "packageName": "org.reactjs.native.example.demo", 42 | "snapshotsPath": "./snapshots/ios", 43 | "dev": {}, 44 | "debug": { 45 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app" 46 | }, 47 | "test": { 48 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app", 49 | "canStopDevice": false 50 | }, 51 | "testPort": {}, 52 | "testPort2": { 53 | "port": 3001 54 | }, 55 | "debugIphone6": { 56 | "deviceName": "iPhone 6", 57 | "snapshotsPath": "./snapshots/iPhone6", 58 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app" 59 | } 60 | }, 61 | "logLevel": "v", 62 | "timeout": 30000 63 | } 64 | -------------------------------------------------------------------------------- /demo/run_android_debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | export BUNDLE_IN_DEBUG="true" 7 | 8 | cd android 9 | rm -rf build .gradle/ app/build 10 | ./gradlew assembleDebug 11 | cd .. 12 | 13 | ./node_modules/.bin/pixels-catcher android debug 14 | -------------------------------------------------------------------------------- /demo/run_android_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | 7 | cd android 8 | rm -rf build .gradle/ app/build 9 | ./gradlew assembleRelease 10 | cd .. 11 | 12 | ./node_modules/.bin/pixels-catcher android release 13 | -------------------------------------------------------------------------------- /demo/run_android_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | export BUNDLE_IN_DEBUG="true" 7 | 8 | cd android 9 | rm -rf build .gradle/ app/build 10 | ./gradlew assembleDebug 11 | cd .. 12 | 13 | ../node_modules/.bin/flow-node ../src/runner/cli.js android test 14 | -------------------------------------------------------------------------------- /demo/run_ios_debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | BUILD_PATH="./build" 11 | rm -rf $BUILD_PATH 12 | 13 | xcrun xcodebuild \ 14 | -scheme demo \ 15 | -workspace demo.xcworkspace \ 16 | -configuration Debug \ 17 | -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.2' \ 18 | -derivedDataPath $BUILD_PATH \ 19 | ENTRY_FILE="indexSnapshot.js" \ 20 | build 21 | 22 | cd .. 23 | 24 | ./node_modules/.bin/pixels-catcher ios debug 25 | -------------------------------------------------------------------------------- /demo/run_ios_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | echo "ERROR: Not implemented. Requires https" 11 | -------------------------------------------------------------------------------- /demo/run_ios_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | BUILD_PATH="./build" 11 | rm -rf $BUILD_PATH 12 | 13 | xcrun xcodebuild \ 14 | -scheme demo \ 15 | -workspace demo.xcworkspace \ 16 | -configuration Debug \ 17 | -destination 'platform=iOS Simulator,name=iPhone 14 Plus,OS=13.3' \ 18 | -derivedDataPath $BUILD_PATH \ 19 | ENTRY_FILE="indexSnapshot.js" \ 20 | build 21 | 22 | cd .. 23 | 24 | ../node_modules/.bin/flow-node ../src/runner/cli.js ios test 25 | -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/someComponent.png -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | { 3 | "extends": "@tsconfig/react-native/tsconfig.json", /* Recommended React Native TSConfig base */ 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 6 | 7 | /* Completeness */ 8 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixels-catcher", 3 | "version": "0.13.1", 4 | "description": "UI snapshot testing for React Native", 5 | "main": "lib/client/index.js", 6 | "scripts": { 7 | "demo": "cd demo && ../node_modules/.bin/flow-node ../cli.js android debug", 8 | "lint": "eslint --ext .js --ext .ts --ext .tsx ./src/", 9 | "build": "./node_modules/.bin/tsc -p src/client && ./node_modules/.bin/tsc -p src/runner", 10 | "postbuild": "npm run assets", 11 | "prepare": "npm run build", 12 | "prepack": "npm run build", 13 | "check-types-client": "./node_modules/.bin/tsc --noEmit -p src/client", 14 | "check-types-runner": "./node_modules/.bin/tsc --noEmit -p src/runner", 15 | "check-types": "npm run check-types-client && npm run check-types-runner", 16 | "assets": "cp ./src/runner/server/dummy.png ./lib/runner/server/" 17 | }, 18 | "bin": "./lib/runner/cli.js", 19 | "files": [ 20 | "/lib", 21 | "/src/*.js", 22 | "/src/utils/*.js" 23 | ], 24 | "keywords": [ 25 | "react-native view android iOS UI screenshot snapshot testing" 26 | ], 27 | "author": "Maksym Rusynyk ", 28 | "license": "MIT", 29 | "engines": { 30 | "node": ">=14.*" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+ssh://git@github.com/rumax/react-native-PixelsCatcher.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/rumax/react-native-PixelsCatcher/issues" 38 | }, 39 | "homepage": "https://github.com/rumax/react-native-PixelsCatcher#readme", 40 | "dependencies": { 41 | "formidable": "^1.2.2", 42 | "pixelmatch": "^5.2.1", 43 | "pngjs": "^6.0.0", 44 | "react-native-save-view": "^0.2.3" 45 | }, 46 | "devDependencies": { 47 | "@types/formidable": "^1.2.2", 48 | "@types/node": "^18.7.16", 49 | "@types/pixelmatch": "^5.2.3", 50 | "@types/pngjs": "^6.0.0", 51 | "@types/react-native": "^0.64.6", 52 | "@typescript-eslint/eslint-plugin": "^4.25.0", 53 | "@typescript-eslint/parser": "^4.25.0", 54 | "eslint": "^7.27.0", 55 | "eslint-config-airbnb": "^18.2.1", 56 | "eslint-plugin-import": "^2.23.3", 57 | "eslint-plugin-jsx-a11y": "^6.4.1", 58 | "eslint-plugin-react": "^7.23.2", 59 | "eslint-plugin-react-hooks": "^4.2.0", 60 | "react": "17.0.1", 61 | "react-native": "0.64.1", 62 | "react-test-renderer": "17.0.1", 63 | "typescript": "^4.2.4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /res/azureDevops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/res/azureDevops.png -------------------------------------------------------------------------------- /res/testResults.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/res/testResults.png -------------------------------------------------------------------------------- /scripts/create_android_emulator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DEVICE="Nexus 5X" 4 | DEVICE_NAME="Nexus_5X" 5 | SDK="system-images;android-27;default;x86_64" 6 | 7 | # Install AVD files 8 | echo "y" | $ANDROID_HOME/tools/bin/sdkmanager \ 9 | --install $SDK 10 | 11 | # Create emulator 12 | echo "no" | $ANDROID_HOME/tools/bin/avdmanager \ 13 | create avd \ 14 | -n "$DEVICE_NAME" \ 15 | --device "$DEVICE" \ 16 | -k $SDK \ 17 | --force 18 | 19 | $ANDROID_HOME/emulator/emulator -list-avds 20 | 21 | echo "Starting emulator" 22 | 23 | # Start emulator in background 24 | nohup $ANDROID_HOME/emulator/emulator \ 25 | -avd $DEVICE_NAME \ 26 | -no-snapshot > /dev/null 2>&1 & 27 | $ANDROID_HOME/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82' 28 | 29 | $ANDROID_HOME/platform-tools/adb devices 30 | 31 | echo "Emulator started" 32 | -------------------------------------------------------------------------------- /src/client/Snapshot.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | // eslint-disable-next-line no-use-before-define 8 | import React, { Component } from 'react'; 9 | import { InteractionManager, ScrollView } from 'react-native'; 10 | 11 | import log from './utils/log'; 12 | 13 | type Props = { onReady: () => void }; 14 | 15 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOT'; 16 | const ERROR_NO_IMPLEMENTED = 17 | 'Not implemented. Should be implemented by actual snapshot'; 18 | 19 | export default class Snapshot extends Component { 20 | // Should be implemented by actual snapshot 21 | static snapshotName: string = ''; 22 | 23 | componentDidMount(): void { 24 | log.v(TAG, 'Awaiting interaction'); 25 | const startTime = new Date().getTime(); 26 | InteractionManager.runAfterInteractions(() => { 27 | const time = new Date().getTime() - startTime; 28 | log.v(TAG, `Interaction completed in ${time} milliseconds`); 29 | global.setTimeout(() => { 30 | this.props.onReady(); 31 | }, 50); 32 | }); 33 | } 34 | 35 | renderContent(): React.ReactNode { 36 | log.e(TAG, ERROR_NO_IMPLEMENTED); 37 | throw new Error(ERROR_NO_IMPLEMENTED); 38 | } 39 | 40 | render(): React.ReactNode { 41 | const content = this.renderContent(); 42 | return ( 43 | 44 | {content} 45 | 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/client/SnapshotsContainer.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | // eslint-disable-next-line no-use-before-define 8 | import React, { Component } from 'react'; 9 | import { View, Text } from 'react-native'; 10 | // @ts-ignore 11 | import SaveView from 'react-native-save-view'; 12 | 13 | import { getNextSnapshot } from './snapshotsManager'; 14 | import compareToReference from './utils/compareToReference'; 15 | import log from './utils/log'; 16 | import network from './utils/network'; 17 | 18 | import type Snapshot from './Snapshot'; 19 | 20 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOTS_CONTAINER'; 21 | 22 | type NoProps = Record; 23 | 24 | type State = { 25 | isReady: boolean, 26 | ActiveSnapshot: typeof Snapshot | null, 27 | }; 28 | 29 | export default class SnapshotsContainer extends Component { 30 | _viewRef: any; 31 | 32 | _testStartedAt: number = new Date().getTime(); 33 | 34 | _renderStartedAt: number = 0; 35 | 36 | constructor(props: Record) { 37 | super(props); 38 | 39 | this.state = { 40 | ActiveSnapshot: null, 41 | isReady: false, 42 | }; 43 | } 44 | 45 | shouldComponentUpdate( 46 | nextProps: NoProps, 47 | nextState: Readonly, 48 | ): boolean { 49 | return this.state.ActiveSnapshot !== nextState.ActiveSnapshot 50 | || this.state.isReady !== nextState.isReady; 51 | } 52 | 53 | componentDidMount(): void { 54 | requestAnimationFrame(() => { 55 | this._startTesting(); 56 | }); 57 | } 58 | 59 | render(): React.ReactNode { 60 | const { isReady, ActiveSnapshot } = this.state; 61 | 62 | if (!isReady) { 63 | return ( 64 | 65 | Initializing tests 66 | 67 | ); 68 | } 69 | 70 | if (!ActiveSnapshot) { 71 | log.i(TAG, 'No active snapshot'); 72 | return null; 73 | } 74 | 75 | log.i(TAG, `rendering snapshot [${ActiveSnapshot.snapshotName}]`); 76 | 77 | this._renderStartedAt = new Date().getTime(); 78 | 79 | return ; 80 | } 81 | 82 | _startTesting = async (): Promise => { 83 | await network.initTests(); 84 | const ActiveSnapshot = getNextSnapshot(); 85 | if (!ActiveSnapshot) { 86 | this._endOfTest(); 87 | log.e(TAG, 'No snapshots registered'); 88 | this._endOfTest(); 89 | return; 90 | } 91 | log.v(TAG, 'Start testing'); 92 | this.setState({ 93 | ActiveSnapshot, 94 | isReady: true, 95 | }); 96 | }; 97 | 98 | _onRef = (ref: any): void => { 99 | this._viewRef = ref; 100 | }; 101 | 102 | _onSnapshotReady = (): void => { 103 | const renderTime = new Date().getTime() - this._renderStartedAt; 104 | log.v(TAG, 'Snapshot ready'); 105 | 106 | setTimeout(async () => { 107 | const ref = this._viewRef; 108 | 109 | if (!ref) { 110 | const errorMessage = 'Something when wrong, no ref to the component'; 111 | log.e(TAG, errorMessage); 112 | throw new Error(errorMessage); 113 | } 114 | 115 | const { ActiveSnapshot } = this.state; 116 | const name = ActiveSnapshot?.snapshotName; 117 | 118 | log.v(TAG, `snapshotName: [${name || '-'}]`); 119 | 120 | if (!name) { 121 | const errorMessage = 'Snapshot should has a proper name'; 122 | 123 | log.w(TAG, errorMessage); 124 | network.reportTest({ 125 | name: '-', 126 | failure: errorMessage, 127 | time: this._getTestExecutionTime(), 128 | }); 129 | this._nextSnapshot(); 130 | 131 | return; 132 | } 133 | 134 | let failure: any; 135 | 136 | try { 137 | log.v(TAG, '++SaveView.save'); 138 | const base64 = await SaveView.saveToPNGBase64(ref); 139 | log.v(TAG, `--SaveView.save, size is ${base64.length}`); 140 | 141 | failure = await compareToReference(name, base64); 142 | if (failure) { 143 | log.e(TAG, `Snapshot ${name} failed: ${failure}`); 144 | } else { 145 | log.i(TAG, `Snapshot ${name} passed`); 146 | } 147 | } catch (err: unknown) { 148 | failure = `Failed to save view: ${ 149 | err instanceof Error ? err.message : 'Unknown error' 150 | }`; 151 | log.e(TAG, failure); 152 | } 153 | 154 | log.v(TAG, `Reporting [${name}], failure: [${failure}]`); 155 | try { 156 | await network.reportTest({ 157 | name, 158 | failure, 159 | time: this._getTestExecutionTime(), 160 | renderTime, 161 | }); 162 | } catch (err) { 163 | log.e(TAG, 'Failed to report test', err); 164 | } 165 | this._nextSnapshot(); 166 | }, 50); 167 | }; 168 | 169 | _getTestExecutionTime(): number { 170 | const time = new Date().getTime() - this._testStartedAt; 171 | log.v(TAG, `Execution time: ${time}`); 172 | return time; 173 | } 174 | 175 | _nextSnapshot(): void { 176 | log.v(TAG, 'Trying to get next snapshot'); 177 | const NextSnapshot = getNextSnapshot(); 178 | 179 | if (!NextSnapshot) { 180 | log.v('No more snapshots left, exit testing'); 181 | this._endOfTest(); 182 | return; 183 | } 184 | 185 | log.v(`Switching to next snapshot ${NextSnapshot.snapshotName}`); 186 | this._testStartedAt = new Date().getTime(); 187 | this.setState({ ActiveSnapshot: NextSnapshot }); 188 | } 189 | 190 | _endOfTest(): void { 191 | network.endOfTests({ message: 'All tests completed' }); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/client/__tests__/Snapshot.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { View } from 'react-native'; 5 | import renderer from 'react-test-renderer'; 6 | 7 | import Snapshot from '../Snapshot'; 8 | 9 | jest.mock('../utils/log', () => ({ e: () => {}, v: () => {} })); 10 | global.console.error = () => {}; 11 | 12 | describe('Snapshot component', () => { 13 | const onReadyMock = jest.fn(); 14 | 15 | it('throws exception if renderContent is not implemented', () => { 16 | let exception; 17 | 18 | try { 19 | renderer.create(); 20 | } catch (err) { 21 | exception = err; 22 | } 23 | 24 | expect(exception).toMatchSnapshot(); 25 | }); 26 | 27 | it('renders snapshot component', () => { 28 | class SnapshotClass extends Snapshot { 29 | static snapshotName = 'AppSnapshot'; 30 | 31 | renderContent() { 32 | return ( 33 | 34 | ); 35 | } 36 | } 37 | 38 | const tree = renderer.create(); 39 | 40 | expect(tree).toMatchSnapshot(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/client/__tests__/SnapshotsContainer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import SnapshotsContainer from '../SnapshotsContainer'; 7 | import network from '../utils/network'; 8 | import log from '../utils/log'; 9 | import { getNextSnapshot } from '../snapshotsManager'; 10 | 11 | jest.mock('../utils/log', () => ({ 12 | i: jest.fn(), 13 | v: jest.fn(), 14 | w: jest.fn(), 15 | e: jest.fn(), 16 | })); 17 | jest.mock('../utils/network', () => ({ endOfTests: jest.fn() })); 18 | jest.mock('../snapshotsManager', () => ({ getNextSnapshot: jest.fn() })); 19 | 20 | describe('SnapshotsContainer', () => { 21 | beforeEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | it('Renders empty view and reports it to the server if no snapshots are registered', () => { 26 | const tree = renderer.create(); 27 | 28 | expect(tree).toMatchSnapshot(); 29 | expect(network.endOfTests).toHaveBeenCalledTimes(1); 30 | expect(log.e).toMatchSnapshot('loggin error'); 31 | }); 32 | 33 | it('Renders registered snapshot', () => { 34 | getNextSnapshot.mockImplementationOnce(() => 'SomeSnapshot'); 35 | const tree = renderer.create(); 36 | 37 | expect(tree).toMatchSnapshot(); 38 | expect(network.endOfTests).toHaveBeenCalledTimes(0); 39 | expect(log.v).toMatchSnapshot('render snapshot reported'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/Snapshot.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot component renders snapshot component 1`] = ` 4 | 12 | 13 | 14 | 15 | 16 | `; 17 | 18 | exports[`Snapshot component renders snpashot component 1`] = ` 19 | 27 | 28 | 29 | 30 | 31 | `; 32 | 33 | exports[`Snapshot component throws exception if renderContent is not implemented 1`] = `[Error: Not implemented. Should be implemented by actual snapshot]`; 34 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/SnapshotsContainer.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SnapshotsContainer Renders empty view and reports it to the server if no snapshots are registered 1`] = `null`; 4 | 5 | exports[`SnapshotsContainer Renders empty view and reports it to the server if no snapshots are registered: loggin error 1`] = ` 6 | [MockFunction] { 7 | "calls": Array [ 8 | Array [ 9 | "PIXELS_CATCHER::APP::SNAPSHOTS_CONTAINER", 10 | "No snapshots registered", 11 | ], 12 | ], 13 | "results": Array [ 14 | Object { 15 | "type": "return", 16 | "value": undefined, 17 | }, 18 | ], 19 | } 20 | `; 21 | 22 | exports[`SnapshotsContainer Renders registered snapshot 1`] = ` 23 | 26 | `; 27 | 28 | exports[`SnapshotsContainer Renders registered snapshot: render snapshot reported 1`] = `[MockFunction]`; 29 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot component register component and start snapshots: registerComponent 1`] = ` 4 | Array [ 5 | Array [ 6 | "appName", 7 | [Function], 8 | ], 9 | ] 10 | `; 11 | 12 | exports[`Snapshot component register component with custom IP and start snapshots: registerComponent 1`] = ` 13 | Array [ 14 | Array [ 15 | "appName", 16 | [Function], 17 | ], 18 | ] 19 | `; 20 | -------------------------------------------------------------------------------- /src/client/__tests__/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { AppRegistry, View } from 'react-native'; 5 | 6 | import { runSnapshots, Snapshot, registerSnapshot } from '../index'; 7 | import network from '../utils/network'; 8 | 9 | jest.mock('react-native/Libraries/ReactNative/AppRegistry', () => ({ registerComponent: jest.fn() })); 10 | jest.mock('../utils/network', () => ({ setBaseUrl: jest.fn() })); 11 | jest.mock('../SnapshotsContainer', () => 'SnapshotsContainer'); 12 | jest.mock('../utils/log', () => ({ i: jest.fn() })); 13 | jest.mock('../Snapshot', () => ({ default: 'Snapshot' })); 14 | jest.mock('../snapshotsManager', () => ({ registerSnapshot: jest.fn() })); 15 | 16 | describe('Snapshot component', () => { 17 | const appName = 'appName'; 18 | 19 | afterEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | it('provides Snapshot component', () => { 24 | expect(Snapshot).toBe('Snapshot'); 25 | }); 26 | 27 | it('allows to register snapshot', () => { 28 | registerSnapshot(); 29 | expect(registerSnapshot).toHaveBeenCalledTimes(1); 30 | }); 31 | 32 | it('register component and start snapshots', () => { 33 | runSnapshots(appName); 34 | 35 | expect(AppRegistry.registerComponent).toHaveBeenCalledTimes(1); 36 | expect(AppRegistry.registerComponent.mock.calls) 37 | .toMatchSnapshot('registerComponent'); 38 | expect(AppRegistry.registerComponent.mock.calls[0][1]()) 39 | .toBe('SnapshotsContainer'); 40 | expect(network.setBaseUrl).toHaveBeenCalledTimes(0); 41 | }); 42 | 43 | it('register component with custom IP and start snapshots', () => { 44 | const baseUrl = 'baseUrl'; 45 | runSnapshots(appName, { baseUrl }); 46 | 47 | expect(AppRegistry.registerComponent).toHaveBeenCalledTimes(1); 48 | expect(AppRegistry.registerComponent.mock.calls) 49 | .toMatchSnapshot('registerComponent'); 50 | expect(AppRegistry.registerComponent.mock.calls[0][1]()) 51 | .toBe('SnapshotsContainer'); 52 | expect(network.setBaseUrl).toHaveBeenCalledTimes(1); 53 | expect(network.setBaseUrl).toHaveBeenCalledWith(baseUrl); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/client/__tests__/snapshotsManager.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { View, Text } from 'react-native'; 5 | 6 | import { registerSnapshot, getNextSnapshot } from '../snapshotsManager'; 7 | import Snapshot from '../Snapshot'; 8 | 9 | jest.mock('../utils/log', () => ({ i: jest.fn() })); 10 | 11 | describe('snapshotsManager', () => { 12 | it('Default snapshots list is empty', () => { 13 | const nextSnapshot = getNextSnapshot(); 14 | expect(nextSnapshot).toBe(undefined); 15 | }); 16 | 17 | it('register snapshot and get it', () => { 18 | class SnapshotClass extends Snapshot { 19 | static snapshotName = 'someComponent'; 20 | 21 | renderContent() { 22 | return ( 23 | Some component 24 | ); 25 | } 26 | } 27 | registerSnapshot(SnapshotClass); 28 | 29 | expect(getNextSnapshot()).toBe(SnapshotClass); 30 | expect(getNextSnapshot()).toBe(undefined); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type React from 'react'; 8 | import { AppRegistry } from 'react-native'; 9 | 10 | import log from './utils/log'; 11 | import network from './utils/network'; 12 | 13 | import SnapshotsContainer from './SnapshotsContainer'; 14 | 15 | export const Snapshot = require('./Snapshot').default; 16 | 17 | export const { registerSnapshot } = require('./snapshotsManager'); 18 | 19 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOT'; 20 | 21 | export type GetRootElementType = (element: React.ComponentType) => 22 | React.ComponentType 23 | 24 | interface ConfigType { 25 | baseUrl?: string; 26 | 27 | /** 28 | * Callback to override AppRegistry.registerComponent with custom 29 | * implementation. Can be used for projects with react-native-navigation 30 | * @param snapshot Current snapshot 31 | */ 32 | // eslint-disable-next-line no-unused-vars 33 | registerComponent?: (snapshot: typeof SnapshotsContainer) => void; 34 | 35 | /** 36 | * Root element. Allows to wrap the SnapshotsContainer, which could be 37 | * useful to implement some providers, for example for react navigation. 38 | * Example: 39 | * 40 | * import { NavigationContainer } from '@react-navigation/native'; 41 | * import { createStackNavigator } from '@react-navigation/stack'; 42 | * 43 | * const Stack = createStackNavigator(); 44 | * 45 | * function getRootElement(SnapshotsContainer) { 46 | * const RootElement = ({children}) => ( 47 | * 48 | * 49 | * 53 | * 54 | * 55 | * ) 56 | * return RootElement; 57 | * } 58 | * 59 | * runSnapshots(appName, { baseUrl, getRootElement }); 60 | */ 61 | getRootElement?: GetRootElementType; 62 | } 63 | 64 | export const runSnapshots = (appName: string, config: ConfigType = {}): void => { 65 | log.i(TAG, `Run snapshots for ${appName}`); 66 | log.i(TAG, `Config is:\n ${JSON.stringify(config, null, 2)}`); 67 | 68 | if (config.baseUrl) { 69 | network.setBaseUrl(config.baseUrl); 70 | } 71 | 72 | if (config.registerComponent) { 73 | config.registerComponent(SnapshotsContainer); 74 | return; 75 | } 76 | 77 | if (config.getRootElement) { 78 | const RootElement = config.getRootElement(SnapshotsContainer); 79 | AppRegistry.registerComponent(appName, () => RootElement); 80 | return; 81 | } 82 | 83 | AppRegistry.registerComponent(appName, () => SnapshotsContainer); 84 | }; 85 | -------------------------------------------------------------------------------- /src/client/snapshotsManager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type Snapshot from './Snapshot'; 8 | import log from './utils/log'; 9 | import network from './utils/network'; 10 | 11 | const snapshots: Array = []; 12 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOTS_MANAGER'; 13 | 14 | 15 | export function registerSnapshot(Component: typeof Snapshot): void { 16 | log.i(TAG, `Registering snapshot [${Component.snapshotName}]`); 17 | snapshots.push(Component); 18 | network.registerTest(Component.snapshotName); 19 | } 20 | 21 | 22 | export function getNextSnapshot(): typeof Snapshot | undefined { 23 | const NextSnapshot = snapshots.shift(); 24 | return NextSnapshot; 25 | } 26 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es6", 5 | "declaration": true, 6 | "outDir": "../../lib/client", 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true 11 | }, 12 | "include": ["./**/*"], 13 | "exclude": [ 14 | "../../node_modules", 15 | "**/__tests__/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/client/utils/__tests__/__snapshots__/compareToReference.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`compareToReference Returns failure if HTTP status is not 200 1`] = `"Invalid status 404"`; 4 | 5 | exports[`compareToReference Returns failure if result is not OK 1`] = `"Files mismatch with 1 pixels"`; 6 | -------------------------------------------------------------------------------- /src/client/utils/__tests__/compareToReference.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import network from '../network'; 3 | import compareToReference from '../compareToReference'; 4 | 5 | jest.mock('../network', () => ({ postBase64: jest.fn() })); 6 | 7 | describe('compareToReference', () => { 8 | it('Returns failure if HTTP status is not 200', async () => { 9 | const snapshotName = 'snapshotName'; 10 | const base64 = 'base64 data'; 11 | 12 | network.postBase64.mockImplementationOnce(() => ({ status: 404 })); 13 | 14 | const failure = await compareToReference(snapshotName, base64); 15 | expect(failure).toMatchSnapshot(); 16 | }); 17 | 18 | it('Returns failure if result is not OK', async () => { 19 | const snapshotName = 'snapshotName'; 20 | const base64 = 'base64 data'; 21 | 22 | network.postBase64.mockImplementationOnce(() => ({ 23 | status: 200, 24 | json: async () => ({ 25 | result: 'ERROR', 26 | info: 'Files mismatch with 1 pixels', 27 | }), 28 | })); 29 | 30 | const failure = await compareToReference(snapshotName, base64); 31 | expect(failure).toMatchSnapshot(); 32 | }); 33 | 34 | it('Returns nothing if image matches the reference', async () => { 35 | const snapshotName = 'snapshotName'; 36 | const base64 = 'base64 data'; 37 | 38 | network.postBase64.mockImplementationOnce(() => ({ 39 | status: 200, 40 | json: async () => ({ 41 | result: 'OK', 42 | info: { differentPixelsCount: 0 }, 43 | }), 44 | })); 45 | 46 | const result = await compareToReference(snapshotName, base64); 47 | expect(result).toBe(undefined); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/client/utils/compareToReference.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import network from './network'; 8 | 9 | const compareToReference = async (snapshotName: string, base64: string): Promise => { 10 | const response: any = await network.postBase64({ 11 | base64, 12 | fileName: `${snapshotName}.png`, 13 | }); 14 | 15 | if (response.status !== 200) { 16 | return `Invalid status ${response.status}`; 17 | } 18 | 19 | const responseJSON = await response.json(); 20 | 21 | if (responseJSON.result !== 'OK') { 22 | return responseJSON.info; 23 | } 24 | 25 | return undefined; 26 | }; 27 | 28 | export default compareToReference; 29 | -------------------------------------------------------------------------------- /src/client/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import network from './network'; 8 | 9 | const consoleLog = global.console && global.console.log 10 | ? global.console.log : (): void => {}; 11 | 12 | type LogLevelType = 'v' | 'd' | 'i' | 'w' | 'e'; 13 | 14 | const serverLog = async (logLevel: LogLevelType, tag: string, ...args: any): Promise => { 15 | try { 16 | network.serverLog({ 17 | logLevel, 18 | tag, 19 | args, 20 | }); 21 | } catch (err) { 22 | if (__DEV__) { 23 | // eslint-disable-next-line no-console 24 | console.warn('ERROR:serverLog: ', err instanceof Error ? err.message : 'Unknown error', err); 25 | } 26 | } 27 | }; 28 | 29 | const log = { 30 | v: (tag: string, ...args: any): void => { 31 | consoleLog(tag, ...args); 32 | serverLog('v', tag, ...args); 33 | }, 34 | 35 | d: (tag: string, ...args: any): void => { 36 | consoleLog(tag, ...args); 37 | serverLog('d', tag, ...args); 38 | }, 39 | 40 | i: (tag: string, ...args: any): void => { 41 | consoleLog(tag, ...args); 42 | serverLog('i', tag, ...args); 43 | }, 44 | 45 | w: (tag: string, ...args: any): void => { 46 | consoleLog(`${tag} WARNING:`, ...args); 47 | serverLog('w', `${tag} WARNING:`, ...args); 48 | }, 49 | 50 | e: (tag: string, ...args: any): void => { 51 | consoleLog(`${tag} ERROR:`, ...args); 52 | serverLog('e', `${tag} ERROR:`, ...args); 53 | }, 54 | }; 55 | 56 | export default log; 57 | -------------------------------------------------------------------------------- /src/client/utils/network.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import { Platform } from 'react-native'; 8 | 9 | let baseUrl = Platform.select({ 10 | android: 'http://10.0.2.2:3000', 11 | ios: 'http://127.0.0.1:3000', 12 | }); 13 | 14 | type TestCaseType = { 15 | name: string, 16 | failure?: string, 17 | isSkipped?: boolean, 18 | time: number, 19 | renderTime?: number, 20 | }; 21 | 22 | const fetchRequest = async (url: string, body: Object): Promise => { 23 | const response = await fetch(url, { 24 | method: 'POST', 25 | headers: { 26 | Accept: 'application/json', 27 | 'Content-Type': 'application/json', 28 | }, 29 | body: JSON.stringify(body), 30 | }); 31 | 32 | return response; 33 | }; 34 | 35 | 36 | export default { 37 | 38 | setBaseUrl(url: string): void { 39 | baseUrl = url; 40 | }, 41 | 42 | 43 | initTests: async (): Promise => { 44 | await fetchRequest(`${baseUrl}/initTests`, {}); 45 | }, 46 | 47 | 48 | registerTest: async (name: string): Promise => { 49 | await fetchRequest(`${baseUrl}/registerTest`, { name }); 50 | }, 51 | 52 | 53 | postBase64: async (body: Object): Promise => { 54 | const response = await fetchRequest(`${baseUrl}/base64`, body); 55 | return response; 56 | }, 57 | 58 | 59 | serverLog: async (body: Object): Promise => { 60 | await fetchRequest(`${baseUrl}/log`, body); 61 | }, 62 | 63 | 64 | reportTest: async (testCase: TestCaseType): Promise => { 65 | await fetchRequest(`${baseUrl}/reportTest`, testCase); 66 | }, 67 | 68 | 69 | endOfTests: async (body: Object): Promise => { 70 | await fetchRequest(`${baseUrl}/endOfTests`, body); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/runner/TestsRunner.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import log from './utils/log'; 5 | import Reporter from './utils/Reporter'; 6 | import server from './server/server'; 7 | 8 | const TAG = 'PIXELS_CATCHER'; 9 | 10 | type TestsRunnerParamsType = 11 | { 12 | activityName: string, 13 | appFile: string, 14 | device: any, 15 | deviceName: string, 16 | deviceParams: string, 17 | isDevMode: boolean, 18 | locale: string, 19 | packageName: string, 20 | platform: 'ios' | 'android', 21 | port: number, 22 | snapshotsPath: string, 23 | testRunName: string, 24 | timeout: number, 25 | }; 26 | 27 | class TestsRunner { 28 | _activityName: string; 29 | 30 | _appFile: string; 31 | 32 | _appFileFullPath: string | void; 33 | 34 | _device: any; 35 | 36 | _deviceName: string; 37 | 38 | _deviceParams: Object; 39 | 40 | _isDevMode: boolean; 41 | 42 | _locale: string; 43 | 44 | _packageName: string; 45 | 46 | _platform: 'ios' | 'android'; 47 | 48 | _port: number; 49 | 50 | _reporter: Reporter; 51 | 52 | _snapshotsPath: string; 53 | 54 | _stopByTimeoutID: ReturnType | void; 55 | 56 | _timeout: number; 57 | 58 | 59 | constructor(params: TestsRunnerParamsType) { 60 | this._activityName = params.activityName; 61 | this._appFile = params.appFile; 62 | this._device = params.device; 63 | this._deviceName = params.deviceName; 64 | this._deviceParams = params.deviceParams; 65 | this._isDevMode = params.isDevMode; 66 | this._locale = params.locale; 67 | this._packageName = params.packageName; 68 | this._platform = params.platform; 69 | this._port = params.port; 70 | this._snapshotsPath = params.snapshotsPath; 71 | this._timeout = params.timeout; 72 | 73 | if (!this._isDevMode) { 74 | if (!this._appFile) { 75 | log.e(TAG, 'Valid ap file is required, check config'); 76 | process.exit(-1); 77 | } 78 | 79 | this._appFileFullPath = path.isAbsolute(this._appFile) 80 | ? this._appFile : path.join(process.cwd(), this._appFile); 81 | 82 | if (!fs.existsSync(this._appFileFullPath)) { 83 | log.e(TAG, `Valid app file is required, cannot find [${this._appFile}] file`); 84 | process.exit(-1); 85 | } 86 | } 87 | 88 | this._reporter = new Reporter(params.testRunName, this._snapshotsPath); 89 | } 90 | 91 | 92 | _testingCompleted = async (isPassed: boolean = false): Promise => { 93 | if (this._stopByTimeoutID) { 94 | clearTimeout(this._stopByTimeoutID); 95 | } 96 | if (!this._isDevMode) { 97 | log.i(TAG, 'Stopping the server and emulator'); 98 | server.stop(); 99 | await this._device.stop(); 100 | log.i(TAG, 'Server and emulator are stopped'); 101 | 102 | if (!isPassed) { 103 | log.i(TAG, 'Some tests failed, exit with error'); 104 | process.exit(-1); 105 | } else { 106 | log.i(TAG, 'No errors found'); 107 | } 108 | } 109 | }; 110 | 111 | 112 | _onTestingCompleted = async (byTimeOut: boolean = false): Promise => { 113 | const jUnitFile = path.join(process.cwd(), 'junit.xml'); 114 | const deviceLogsFile = path.join( 115 | process.cwd(), 116 | `${this._platform}_logs.log`, 117 | ); 118 | await this._reporter.toLog(); 119 | this._reporter.tojUnit(jUnitFile); 120 | this._reporter.deviceLogsToFile(deviceLogsFile); 121 | this._testingCompleted(byTimeOut ? false : this._reporter.isPassed()); 122 | }; 123 | 124 | 125 | _onAppActivity = (): void => { 126 | this._stopByTimeout(); 127 | } 128 | 129 | 130 | _stopByTimeout = (): void => { 131 | if (this._stopByTimeoutID) { 132 | clearTimeout(this._stopByTimeoutID); 133 | } 134 | this._stopByTimeoutID = setTimeout(() => { 135 | log.e(TAG, 'Stop tests by timeout'); 136 | this._onTestingCompleted(true); 137 | }, this._timeout); 138 | }; 139 | 140 | 141 | async _startAndroid(): Promise { 142 | log.d(TAG, `Start emulator [${this._deviceName}]`); 143 | try { 144 | await this._device.start(this._deviceParams); 145 | } catch (err) { 146 | process.exit(-1); 147 | } 148 | log.d(TAG, 'Emulator started'); 149 | 150 | log.d(TAG, 'Installing APK'); 151 | await this._device.installApp(this._packageName, this._appFileFullPath); 152 | log.d(TAG, 'APK installed'); 153 | 154 | log.d(TAG, 'Starting application'); 155 | if (this._locale) { 156 | log.w(TAG, `[${this._locale} is ignored for android]`); 157 | } 158 | await this._device.startApp(this._packageName, this._activityName); 159 | log.d(TAG, 'Application started'); 160 | 161 | this._stopByTimeout(); 162 | } 163 | 164 | 165 | async _startIOS(): Promise { 166 | log.d(TAG, `Start emulator [${this._deviceName}]`); 167 | try { 168 | await this._device.start(this._deviceParams); 169 | } catch (err) { 170 | log.e(TAG, `Failed to start device: [${err instanceof Error ? err.message : 'Unknown error'}]`); 171 | process.exit(-1); 172 | } 173 | log.d(TAG, 'Emulator started'); 174 | 175 | log.d(TAG, 'Installing APP'); 176 | await this._device.installApp(this._packageName, this._appFileFullPath); 177 | log.d(TAG, 'APP installed'); 178 | 179 | log.d(TAG, 'Starting application'); 180 | await this._device.startApp(this._packageName, this._activityName, this._locale); 181 | log.d(TAG, 'Application started'); 182 | } 183 | 184 | 185 | async start(): Promise { 186 | log.d(TAG, 'Starting server'); 187 | server.start( 188 | this._reporter, 189 | this._onTestingCompleted, 190 | this._snapshotsPath, 191 | this._onAppActivity, 192 | this._port, 193 | ); 194 | 195 | if (this._isDevMode) { 196 | log.d(TAG, 'Only server is used in DEV mode. Waiting for tests'); 197 | return; 198 | } 199 | 200 | if (this._platform === 'ios') { 201 | this._startIOS(); 202 | } else { 203 | this._startAndroid(); 204 | } 205 | 206 | this._reporter.collectDeviceLogs(this._platform, this._packageName); 207 | } 208 | } 209 | 210 | 211 | export default TestsRunner; 212 | -------------------------------------------------------------------------------- /src/runner/azure/AzurePublisher.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as https from 'https'; 3 | import * as path from 'path'; 4 | 5 | import log from '../utils/log'; 6 | import type { TestcaseType } from '../utils/Reporter'; 7 | 8 | const TAG = 'PIXELS_CATCHER::AZURE_PUBLISHER'; 9 | 10 | const processEnv: any = process.env; 11 | const { 12 | BUILD_BUILDURI, 13 | SYSTEM_ACCESSTOKEN, 14 | SYSTEM_TEAMFOUNDATIONCOLLECTIONURI, 15 | SYSTEM_TEAMPROJECT, 16 | } = processEnv; 17 | 18 | const DEFAULT_OPTIONS = { 19 | hostname: 'dev.azure.com', 20 | port: 443, 21 | }; 22 | 23 | const DEFAULT_HEADERS = { 24 | 'Content-Type': 'application/json; charset=utf-8', 25 | 'X-TFS-FedAuthRedirect': 'Suppress', 26 | Accept: 'application/json', 27 | Authorization: `Basic ${Buffer.from(`:${SYSTEM_ACCESSTOKEN}`).toString('base64')}`, 28 | }; 29 | 30 | type ImageType = 'refImages' | 'uploads' | 'diffs'; 31 | 32 | const uploadImageSuffix = { 33 | diffs: 'Diff', 34 | refImages: 'Reference', 35 | uploads: 'Actual', 36 | }; 37 | 38 | const imageTypes = Object.keys(uploadImageSuffix); 39 | 40 | function base64Encode(file: string): string { 41 | if (!fs.existsSync(file)) { 42 | return ''; 43 | } 44 | return Buffer.from(fs.readFileSync(file)).toString('base64'); 45 | } 46 | 47 | class AzurePublisher { 48 | _workingDir: string; 49 | 50 | _testRunName: string; 51 | 52 | _urlBasePath: string; 53 | 54 | constructor(workingDir: string, testRunName: string) { 55 | this._workingDir = workingDir; 56 | this._testRunName = testRunName; 57 | const organization = SYSTEM_TEAMFOUNDATIONCOLLECTIONURI 58 | .split('/') 59 | .filter((str: string) => Boolean(str)) 60 | .reduce((acc: string, curr: string) => curr); 61 | this._urlBasePath = `/${organization}/${SYSTEM_TEAMPROJECT}/_apis/test`; 62 | } 63 | 64 | async publish(): Promise { 65 | try { 66 | const buildRunId = await this._getBuildRunId(BUILD_BUILDURI); 67 | log.i(TAG, `buildRunId [${buildRunId}]`); 68 | const failedTests = await this._getTestFailures(buildRunId); 69 | log.i(TAG, `failedTests count [${failedTests.length}]`); 70 | failedTests.forEach(async (test: any) => { 71 | log.v(TAG, `Uploading results for test [${test.testCaseTitle}] from [${test.automatedTestStorage}]`); 72 | let type: any; 73 | for (let ind = 0; ind < imageTypes.length; ++ind) { 74 | type = imageTypes[ind]; 75 | await this._uploadScreenshot( 76 | buildRunId, 77 | test.id, 78 | test.testCaseTitle, 79 | test.automatedTestStorage, 80 | type, 81 | ); 82 | } 83 | }); 84 | } catch (err) { 85 | log.e(TAG, `Failed to upload results: ${err instanceof Error ? err.message : 'Unknown error'}`); 86 | process.exit(-1); 87 | } 88 | } 89 | 90 | async _getBuildRunId(_buildUri: string): Promise { 91 | const data: any = await this._httpsRequest({ 92 | method: 'GET', 93 | path: `${this._urlBasePath}/runs?api-version=5.1&buildUri=${_buildUri}`, 94 | }); 95 | if (!data || !data.value || data.value.length === 0) { 96 | throw new Error('Failed to get build run, no data'); 97 | } 98 | let id; 99 | log.v(TAG, `Runs count: ${data.value.length}, searching for [${this._testRunName}]`); 100 | for (let ind = data.value.length - 1; ind >= 0; --ind) { 101 | log.v(TAG, `Name: [${data.value[ind].name}], id: ${data.value[ind].id}`); 102 | if (data.value[ind].name === this._testRunName) { 103 | log.v(TAG, `Id ${data.value[ind].id} found`); 104 | id = data.value[ind].id; 105 | break; 106 | } 107 | } 108 | 109 | if (id === undefined) { 110 | throw new Error(`Failed to get build run id for ${this._testRunName}`); 111 | } 112 | 113 | return id; 114 | } 115 | 116 | async _getTestFailures(runId: string): Promise> { 117 | const data: any = await this._httpsRequest({ 118 | method: 'GET', 119 | path: `${this._urlBasePath}/Runs/${runId}/results?outcomes=3&api-version=5.1&outcomes=3`, 120 | }); 121 | 122 | return data.value; 123 | } 124 | 125 | async _upload( 126 | buildRunId: string, 127 | id: string, 128 | fileToUpload: string, 129 | fileNameToShow: string, 130 | ): Promise { 131 | const postData = { 132 | stream: base64Encode(fileToUpload), 133 | fileName: fileNameToShow, 134 | comment: 'Diff uploaded by REST from pipeline', 135 | attachmentType: 'GeneralAttachment', 136 | }; 137 | 138 | const data: any = await this._httpsRequest({ 139 | method: 'POST', 140 | path: `${this._urlBasePath}/Runs/${buildRunId}/Results/${id}/attachments?api-version=5.1-preview.1`, 141 | }, postData); 142 | 143 | return data.value; 144 | } 145 | 146 | async _uploadScreenshot( 147 | buildRunId: string, 148 | id: string, 149 | testCaseTitle: string, 150 | className: string, 151 | type: ImageType, 152 | ): Promise { 153 | const suffix = uploadImageSuffix[type]; 154 | log.v(TAG, `Uploading ${suffix}`); 155 | await this._upload( 156 | buildRunId, 157 | id, 158 | path.join(this._workingDir, className, type, `${testCaseTitle}.png`), 159 | `${testCaseTitle}${suffix}.png`, 160 | ); 161 | log.v(TAG, `${suffix} uploaded`); 162 | } 163 | 164 | async _httpsRequest(options: any, postData: any = undefined): Promise { 165 | let _options = { 166 | ...DEFAULT_OPTIONS, 167 | ...options, 168 | headers: { 169 | ...DEFAULT_HEADERS, 170 | ...(options.headers ? options.headers : {}), 171 | }, 172 | }; 173 | const _postData = postData ? JSON.stringify(postData) : undefined; 174 | 175 | return new Promise((resolve: Function, reject: Function) => { 176 | if (_postData) { 177 | _options = { 178 | ..._options, 179 | 'Content-Length': Buffer.byteLength(_postData), 180 | }; 181 | } 182 | const req = https.request(_options, (resp: any) => { 183 | if (resp.statusCode >= 300) { 184 | log.e(TAG, `Failed to ${_options.method} [${_options.path}]`); 185 | reject(new Error(`Status code: ${resp.statusCode}, statusMessage: ${resp.statusMessage}`)); 186 | return; 187 | } 188 | 189 | let data = ''; 190 | 191 | resp.on('data', (chunk: string) => { 192 | data += chunk; 193 | }); 194 | 195 | resp.on('end', () => { 196 | resolve(JSON.parse(data)); 197 | }); 198 | }).on('error', (err: Error) => { 199 | reject(new Error(`Error: ${err.message}`)); 200 | }); 201 | 202 | if (_postData) { 203 | req.write(_postData); 204 | } 205 | 206 | req.end(); 207 | }); 208 | } 209 | } 210 | 211 | export default AzurePublisher; 212 | -------------------------------------------------------------------------------- /src/runner/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Maksym Rusynyk 2018 - present 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | import type { DeviceInterface } from './utils/device/DeviceInterface'; 9 | 10 | import log from './utils/log'; 11 | import readConfig from './utils/readConfig'; 12 | import getDevice from './utils/device/deviceProvider'; 13 | import AzurePublisher from './azure/AzurePublisher'; 14 | import TestsRunner from './TestsRunner'; 15 | 16 | const TAG = 'PIXELS_CATCHER'; 17 | const AZURE_PUBLISH_ACTION = 'azureAttachments'; 18 | const [,, platform, configuration, action] = process.argv; 19 | 20 | if (!platform || !(platform === 'ios' || platform === 'android')) { 21 | log.e(TAG, `Valid platform is required, specify "ios" or "android". Example: 22 | 23 | $ pixels-catcher android debug 24 | 25 | or 26 | 27 | $ pixels-catcher ios debug 28 | `); 29 | process.exit(-1); 30 | } 31 | 32 | if (!configuration) { 33 | log.e(TAG, `Configuration is required. Example: 34 | 35 | $ pixels-catcher android debug 36 | 37 | or 38 | 39 | $ pixels-catcher ios debug 40 | `); 41 | process.exit(-1); 42 | } 43 | 44 | if (action !== undefined && action !== AZURE_PUBLISH_ACTION) { 45 | log.e(TAG, `Only "${AZURE_PUBLISH_ACTION}" is available. Example: 46 | 47 | $ pixels-catcher android debug ${AZURE_PUBLISH_ACTION} 48 | 49 | or 50 | 51 | $ pixels-catcher ios debug ${AZURE_PUBLISH_ACTION} 52 | `); 53 | process.exit(-1); 54 | } 55 | 56 | 57 | const fullConfig = readConfig(); 58 | const config = fullConfig[platform]; 59 | 60 | if (!config) { 61 | log.e(TAG, `Cannot find configuration for plarform [${platform}] in ` 62 | + `config:\n ${JSON.stringify(fullConfig, null, 2)}`); 63 | process.exit(-1); 64 | } 65 | 66 | log.setLevel(fullConfig.logLevel); 67 | log.i(TAG, `Starting with [${configuration}] configuration for [${platform}]`); 68 | log.v(TAG, `Config\n${JSON.stringify(config, null, 2)}`); 69 | 70 | const getParamFromConfig = (paramName: string): any => { 71 | const value = (config[configuration] || {})[paramName]; 72 | return value !== undefined ? value : config[paramName]; 73 | }; 74 | 75 | const activityName = getParamFromConfig('activityName') || 'MainActivity'; 76 | const appFile = getParamFromConfig('appFile'); 77 | const canStopDevice = getParamFromConfig('canStopDevice'); 78 | const deviceName = getParamFromConfig('deviceName'); 79 | const deviceParams = getParamFromConfig('deviceParams'); 80 | const isPhysicalDevice = getParamFromConfig('physicalDevice'); 81 | const packageName = getParamFromConfig('packageName'); 82 | const snapshotsPath = getParamFromConfig('snapshotsPath'); 83 | const port = getParamFromConfig('port'); 84 | const locale = getParamFromConfig('locale'); 85 | const timeout = fullConfig.timeout || 25 * 1000; // 25 sec is default 86 | 87 | if (!deviceName) { 88 | log.e(TAG, 'Valid device name is required, check "PixelsCatcher.deviceName" ' 89 | + 'property in package.json'); 90 | process.exit(-1); 91 | } 92 | 93 | const device: DeviceInterface = getDevice( 94 | deviceName, 95 | platform, 96 | isPhysicalDevice, 97 | canStopDevice, 98 | ); 99 | 100 | log.i(TAG, `Starting with: 101 | - activityName: [${activityName}] 102 | - appFile: [${appFile}] 103 | - deviceName: [${deviceName}] 104 | - deviceParams: [${deviceParams}] 105 | - packageName: [${packageName}] 106 | - snapshotsPath: [${snapshotsPath}] 107 | - canStopDevice: [${canStopDevice}] 108 | - port: [${port}] 109 | - locale: [${locale}]`); 110 | 111 | if (!packageName) { 112 | log.e(TAG, 'Package name is required'); 113 | process.exit(-1); 114 | } 115 | 116 | const testRunName = `UI tests for ${platform}/${deviceName}`; 117 | 118 | if (action === AZURE_PUBLISH_ACTION) { 119 | const azurePublisher = new AzurePublisher(process.cwd(), testRunName); 120 | azurePublisher.publish(); 121 | } else { 122 | const isDevMode = !appFile; 123 | log.i(TAG, `Starting in ${isDevMode ? 'development' : 'ci'} mode`); 124 | const testsRunner = new TestsRunner({ 125 | testRunName, 126 | isDevMode, 127 | timeout, 128 | device, 129 | appFile, 130 | port, 131 | platform, 132 | deviceName, 133 | snapshotsPath, 134 | deviceParams, 135 | packageName, 136 | locale, 137 | activityName, 138 | }); 139 | testsRunner.start(); 140 | } 141 | -------------------------------------------------------------------------------- /src/runner/server/compareImages.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as fs from 'fs'; 8 | import { PNG } from 'pngjs'; 9 | import * as pixelmatch from 'pixelmatch'; 10 | 11 | export default (actual: any, expected: any, diffFile: any): number => { 12 | if (!actual || !fs.existsSync(actual)) { 13 | throw new Error(`Actual file is required, cannot get [${actual}] file`); 14 | } 15 | if (!expected || !fs.existsSync(expected)) { 16 | throw new Error(`Expected file is required, cannot get [${expected}] file`); 17 | } 18 | 19 | const imageActual = PNG.sync.read(fs.readFileSync(actual)); 20 | const imageExpected = PNG.sync.read(fs.readFileSync(expected)); 21 | 22 | if (imageActual.width !== imageExpected.width) { 23 | throw new Error(`Width mismatch: expected ${imageExpected.width}, actual: ${imageActual.width}`); 24 | } 25 | 26 | if (imageActual.height !== imageExpected.height) { 27 | throw new Error(`Height mismatch: expected ${imageExpected.height}, actual: ${imageActual.height}`); 28 | } 29 | 30 | const diff = new PNG({ width: imageExpected.width, height: imageExpected.height }); 31 | 32 | const differentPixelsCount = pixelmatch( 33 | imageActual.data, 34 | imageExpected.data, 35 | diff.data, 36 | imageActual.width, 37 | imageActual.height, 38 | { threshold: 0.1 }, 39 | ); 40 | 41 | if (diffFile) { 42 | diff.pack().pipe(fs.createWriteStream(diffFile)); 43 | } 44 | 45 | return differentPixelsCount; 46 | }; 47 | -------------------------------------------------------------------------------- /src/runner/server/dummy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/src/runner/server/dummy.png -------------------------------------------------------------------------------- /src/runner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es5", 5 | "declaration": true, 6 | "outDir": "../../lib/runner", 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true 11 | }, 12 | "include": ["./**/*"], 13 | "exclude": [ 14 | "../../node_modules", 15 | "**/__tests__/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/runner/utils/Reporter.ts: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as fs from 'fs'; 3 | 4 | import timeToSec from './timeToSec'; 5 | import exec from './exec'; 6 | import delay from './delay'; 7 | 8 | const { spawn } = require('child_process'); 9 | 10 | export type TestcaseType = { 11 | failure: string | void, 12 | isSkipped: boolean | void, 13 | name: string, 14 | renderTime?: number, 15 | time: number, 16 | }; 17 | 18 | const timeReducer = (time: number, testcase: TestcaseType): number => time + testcase.time; 19 | 20 | const filterSkipped = (testcase: TestcaseType): boolean => !testcase.isSkipped; 21 | 22 | const filterFailed = (testcase: TestcaseType): boolean => !testcase.failure; 23 | 24 | class TestReporter { 25 | _name: string; 26 | 27 | _className: string; 28 | 29 | _tests: Array = []; 30 | 31 | _deviceLogs: Array = []; 32 | 33 | _stopDeviceLogger: Function | undefined = undefined; 34 | 35 | _minRenderTime = { 36 | name: '-', 37 | time: Number.MAX_VALUE, 38 | }; 39 | 40 | _maxRenderTime = { 41 | name: '-', 42 | time: Number.MIN_VALUE, 43 | }; 44 | 45 | constructor(name: string, className: string) { 46 | this._name = name; 47 | this._className = className; 48 | } 49 | 50 | registerTest(name: string): void { 51 | this._tests.push({ 52 | failure: undefined, 53 | isSkipped: true, 54 | name, 55 | renderTime: 0, 56 | time: 0, 57 | }); 58 | } 59 | 60 | _updateTestResult(testCase: TestcaseType): void { 61 | const ind = this._tests.findIndex((test) => test.name === testCase.name); 62 | 63 | if (ind >= 0) { 64 | this._tests[ind] = testCase; 65 | } else { 66 | this._tests.push(testCase); 67 | } 68 | } 69 | 70 | reportTest(testCase: TestcaseType): void { 71 | this._updateTestResult(testCase); 72 | 73 | if (testCase.renderTime === undefined) { 74 | return; 75 | } 76 | if (testCase.renderTime < this._minRenderTime.time) { 77 | this._minRenderTime.time = testCase.renderTime; 78 | this._minRenderTime.name = testCase.name; 79 | } 80 | if (testCase.renderTime > this._maxRenderTime.time) { 81 | this._maxRenderTime.time = testCase.renderTime; 82 | this._maxRenderTime.name = testCase.name; 83 | } 84 | } 85 | 86 | isPassed(): boolean { 87 | return this._getFailedTests().length === 0; 88 | } 89 | 90 | async toLog(): Promise { 91 | global.console.log(''); 92 | global.console.log('==> All tests completed: <=='); 93 | 94 | const failedTests = this._getFailedTests(); 95 | const passedTests = this._getPassedTests(); 96 | const skippedTests = this._getSkippedTests(); 97 | const reportTable: any = []; 98 | 99 | this._tests.forEach((testcase: TestcaseType) => { 100 | let status = 'PASSED'; 101 | 102 | if (testcase.failure) { 103 | status = 'FAILED'; 104 | } else if (testcase.isSkipped) { 105 | status = 'SKIPPED'; 106 | } 107 | 108 | reportTable.push({ 109 | name: testcase.name, 110 | status, 111 | time: timeToSec(testcase.time), 112 | renderTime: testcase.renderTime !== undefined ? timeToSec(testcase.renderTime) : '-', 113 | failure: testcase.failure || '-', 114 | }); 115 | }); 116 | 117 | global.console.table(reportTable); 118 | 119 | global.console.log(''); 120 | global.console.log('==> Summary: <=='); 121 | 122 | global.console.table([ 123 | ['Total tests', this._tests.length], 124 | ['Passed tests', passedTests.length], 125 | ['Skipped tests', skippedTests.length], 126 | ['Failed tests', failedTests.length], 127 | ['Min render time', `${this._minRenderTime.time}ms (${this._minRenderTime.name})`], 128 | ['Max render time', `${this._maxRenderTime.time}ms (${this._maxRenderTime.name})`], 129 | ]); 130 | 131 | if (failedTests.length > 0) { 132 | global.console.log('==> Failed tests: <=='); 133 | global.console.table(failedTests.map((testCase: TestcaseType) => testCase.name)); 134 | } 135 | 136 | // on CI some logs are not available, adding a delay to fix it 137 | await delay(300); 138 | } 139 | 140 | tojUnit(jUnitFile: string): void { 141 | const xmlResult = ['']; 142 | xmlResult.push('`); 149 | xmlResult.push(' `); 156 | this._tests.forEach((testcase: TestcaseType) => { 157 | xmlResult.push(' `); 161 | if (testcase.failure) { 162 | xmlResult.push(` ${testcase.failure}`); 163 | } else if (testcase.isSkipped) { 164 | xmlResult.push(' '); 165 | } 166 | xmlResult.push(' '); 167 | }); 168 | xmlResult.push(' '); 169 | xmlResult.push(''); 170 | xmlResult.push(''); 171 | fs.writeFileSync(jUnitFile, xmlResult.join('\n')); 172 | } 173 | 174 | collectDeviceLogs(platform: 'ios' | 'android', packageName: string): void { 175 | let spawnProcess: any; 176 | if (platform === 'android') { 177 | exec('adb logcat -c'); 178 | spawnProcess = spawn('adb', [ 179 | 'logcat', `${packageName}:I`, '*:V', 180 | ]); 181 | } else if (platform === 'ios') { 182 | spawnProcess = spawn('xcrun', [ 183 | 'simctl', 'spawn', 'booted', 'log', 'stream', 184 | ]); 185 | } 186 | 187 | spawnProcess.stdout.on('data', (data: any): any => { 188 | const stringRepresentation = data.toString(); 189 | this._deviceLogs.push(stringRepresentation); 190 | }); 191 | 192 | this._stopDeviceLogger = (): void => { 193 | spawnProcess.stdin.pause(); 194 | spawnProcess.kill(); 195 | }; 196 | } 197 | 198 | deviceLogsToFile(fileName: string): void { 199 | if (this._stopDeviceLogger) { 200 | this._stopDeviceLogger(); 201 | this._stopDeviceLogger = undefined; 202 | } 203 | fs.writeFileSync(fileName, this._deviceLogs.join('')); 204 | } 205 | 206 | _getPassedTests(): Array { 207 | return this._tests 208 | .filter(filterSkipped) 209 | .filter(filterFailed); 210 | } 211 | 212 | _getSkippedTests(): Array { 213 | return this._tests 214 | .filter((testcase: TestcaseType): boolean => Boolean(testcase.isSkipped)); 215 | } 216 | 217 | _getFailedTests(): Array { 218 | return this._tests 219 | .filter(filterSkipped) 220 | .filter((test: TestcaseType) => Boolean(test.failure)); 221 | } 222 | 223 | _getTotalTime(): number { 224 | return this._tests 225 | .filter(filterSkipped) 226 | .reduce(timeReducer, 0); 227 | } 228 | } 229 | 230 | export default TestReporter; 231 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/__snapshots__/log.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`logging Allows to log including d 1`] = ` 4 | Array [ 5 | Array [ 6 | "i:", 7 | ], 8 | Array [ 9 | "w WARNING:", 10 | ], 11 | Array [ 12 | "e ERROR:", 13 | ], 14 | Array [ 15 | "v:", 16 | ], 17 | Array [ 18 | "d:", 19 | ], 20 | Array [ 21 | "i:", 22 | ], 23 | Array [ 24 | "w WARNING:", 25 | ], 26 | Array [ 27 | "e ERROR:", 28 | ], 29 | Array [ 30 | "d:", 31 | ], 32 | Array [ 33 | "i:", 34 | ], 35 | Array [ 36 | "w WARNING:", 37 | ], 38 | Array [ 39 | "e ERROR:", 40 | ], 41 | ] 42 | `; 43 | 44 | exports[`logging Allows to log including e 1`] = ` 45 | Array [ 46 | Array [ 47 | "i:", 48 | ], 49 | Array [ 50 | "w WARNING:", 51 | ], 52 | Array [ 53 | "e ERROR:", 54 | ], 55 | Array [ 56 | "v:", 57 | ], 58 | Array [ 59 | "d:", 60 | ], 61 | Array [ 62 | "i:", 63 | ], 64 | Array [ 65 | "w WARNING:", 66 | ], 67 | Array [ 68 | "e ERROR:", 69 | ], 70 | Array [ 71 | "d:", 72 | ], 73 | Array [ 74 | "i:", 75 | ], 76 | Array [ 77 | "w WARNING:", 78 | ], 79 | Array [ 80 | "e ERROR:", 81 | ], 82 | Array [ 83 | "i:", 84 | ], 85 | Array [ 86 | "w WARNING:", 87 | ], 88 | Array [ 89 | "e ERROR:", 90 | ], 91 | Array [ 92 | "w WARNING:", 93 | ], 94 | Array [ 95 | "e ERROR:", 96 | ], 97 | Array [ 98 | "e ERROR:", 99 | ], 100 | ] 101 | `; 102 | 103 | exports[`logging Allows to log including i 1`] = ` 104 | Array [ 105 | Array [ 106 | "i:", 107 | ], 108 | Array [ 109 | "w WARNING:", 110 | ], 111 | Array [ 112 | "e ERROR:", 113 | ], 114 | Array [ 115 | "v:", 116 | ], 117 | Array [ 118 | "d:", 119 | ], 120 | Array [ 121 | "i:", 122 | ], 123 | Array [ 124 | "w WARNING:", 125 | ], 126 | Array [ 127 | "e ERROR:", 128 | ], 129 | Array [ 130 | "d:", 131 | ], 132 | Array [ 133 | "i:", 134 | ], 135 | Array [ 136 | "w WARNING:", 137 | ], 138 | Array [ 139 | "e ERROR:", 140 | ], 141 | Array [ 142 | "i:", 143 | ], 144 | Array [ 145 | "w WARNING:", 146 | ], 147 | Array [ 148 | "e ERROR:", 149 | ], 150 | ] 151 | `; 152 | 153 | exports[`logging Allows to log including v 1`] = ` 154 | Array [ 155 | Array [ 156 | "i:", 157 | ], 158 | Array [ 159 | "w WARNING:", 160 | ], 161 | Array [ 162 | "e ERROR:", 163 | ], 164 | Array [ 165 | "v:", 166 | ], 167 | Array [ 168 | "d:", 169 | ], 170 | Array [ 171 | "i:", 172 | ], 173 | Array [ 174 | "w WARNING:", 175 | ], 176 | Array [ 177 | "e ERROR:", 178 | ], 179 | ] 180 | `; 181 | 182 | exports[`logging Allows to log including w 1`] = ` 183 | Array [ 184 | Array [ 185 | "i:", 186 | ], 187 | Array [ 188 | "w WARNING:", 189 | ], 190 | Array [ 191 | "e ERROR:", 192 | ], 193 | Array [ 194 | "v:", 195 | ], 196 | Array [ 197 | "d:", 198 | ], 199 | Array [ 200 | "i:", 201 | ], 202 | Array [ 203 | "w WARNING:", 204 | ], 205 | Array [ 206 | "e ERROR:", 207 | ], 208 | Array [ 209 | "d:", 210 | ], 211 | Array [ 212 | "i:", 213 | ], 214 | Array [ 215 | "w WARNING:", 216 | ], 217 | Array [ 218 | "e ERROR:", 219 | ], 220 | Array [ 221 | "i:", 222 | ], 223 | Array [ 224 | "w WARNING:", 225 | ], 226 | Array [ 227 | "e ERROR:", 228 | ], 229 | Array [ 230 | "w WARNING:", 231 | ], 232 | Array [ 233 | "e ERROR:", 234 | ], 235 | ] 236 | `; 237 | 238 | exports[`logging Default logs i, w and e 1`] = ` 239 | Array [ 240 | Array [ 241 | "i:", 242 | ], 243 | Array [ 244 | "w WARNING:", 245 | ], 246 | Array [ 247 | "e ERROR:", 248 | ], 249 | ] 250 | `; 251 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/__snapshots__/readConfig.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`readConfig read config from package.json 1`] = ` 4 | Object { 5 | "android": Object { 6 | "_content": "platform config content from package.json", 7 | }, 8 | } 9 | `; 10 | 11 | exports[`readConfig read config from pixels-catcher.json 1`] = ` 12 | Object { 13 | "ios": Object { 14 | "_content": "platform config content from pixels-catcher.json", 15 | }, 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/isCommand.js: -------------------------------------------------------------------------------- 1 | import isCommand from '../isCommand'; 2 | import exec from '../exec'; 3 | 4 | jest.mock('../exec', () => jest.fn()); 5 | 6 | describe('isCommand', () => { 7 | afterEach(() => { 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | it('Returns true if command exists', () => { 12 | exec.mockReturnValueOnce('/bin/ls'); 13 | 14 | const exists = isCommand('ls'); 15 | 16 | expect(exec).toHaveBeenCalledWith('whereis ls'); 17 | expect(exists).toBe(true); 18 | }); 19 | 20 | it('Returns false if command does not exists', () => { 21 | exec.mockReturnValueOnce(''); 22 | 23 | const exists = isCommand('sl'); 24 | 25 | expect(exec).toHaveBeenCalledWith('whereis sl'); 26 | expect(exists).toBe(false); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/log.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import log from '../log'; 3 | 4 | global.console.log = jest.fn(); 5 | 6 | describe('logging', () => { 7 | const consoleLog = global.console.log; 8 | const logAll = () => { 9 | log.v('v'); 10 | log.d('d'); 11 | log.i('i'); 12 | log.w('w'); 13 | log.e('e'); 14 | }; 15 | 16 | afterEach(() => { 17 | log.setLevel('v'); 18 | }); 19 | 20 | it('Default logs i, w and e', () => { 21 | logAll(); 22 | expect(consoleLog.mock.calls).toMatchSnapshot(); 23 | }); 24 | 25 | it('Allows to log including v', () => { 26 | log.setLevel('v'); 27 | logAll(); 28 | expect(consoleLog.mock.calls).toMatchSnapshot(); 29 | }); 30 | 31 | it('Allows to log including d', () => { 32 | log.setLevel('d'); 33 | logAll(); 34 | expect(consoleLog.mock.calls).toMatchSnapshot(); 35 | }); 36 | 37 | it('Allows to log including i', () => { 38 | log.setLevel('i'); 39 | logAll(); 40 | expect(consoleLog.mock.calls).toMatchSnapshot(); 41 | }); 42 | 43 | it('Allows to log including w', () => { 44 | log.setLevel('w'); 45 | logAll(); 46 | expect(consoleLog.mock.calls).toMatchSnapshot(); 47 | }); 48 | 49 | it('Allows to log including e', () => { 50 | log.setLevel('e'); 51 | logAll(); 52 | expect(consoleLog.mock.calls).toMatchSnapshot(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/readConfig.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import readConfig from '../readConfig'; 4 | 5 | jest.mock('fs', () => ({ 6 | existsSync: jest.fn(), 7 | readFileSync: jest.fn(), 8 | })); 9 | jest.mock('path', () => ({ 10 | join: jest.fn((...args) => args.join('/')), 11 | })); 12 | 13 | process.exit = jest.fn(); 14 | process.cwd = jest.fn(() => 'path_to_file'); 15 | 16 | describe('readConfig', () => { 17 | afterEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | it('read config from package.json', () => { 22 | const rawConfig = '{"PixelsCatcher":{"android":{"_content":"platform ' 23 | + 'config content from package.json"}}}'; 24 | 25 | fs.existsSync.mockImplementationOnce(() => true); 26 | fs.readFileSync.mockImplementationOnce(() => rawConfig); 27 | 28 | const config = readConfig(); 29 | 30 | expect(config).toMatchSnapshot(); 31 | }); 32 | 33 | it('read config from pixels-catcher.json', () => { 34 | // package.json mock 35 | fs.existsSync.mockImplementationOnce(() => true); 36 | fs.readFileSync.mockImplementationOnce(() => '{}'); 37 | // pixels-catcher.json mock 38 | const rawConfig = '{"ios":{"_content":"platform config content from ' 39 | + 'pixels-catcher.json"}}'; 40 | fs.existsSync.mockImplementationOnce(() => true); 41 | fs.readFileSync.mockImplementationOnce(() => rawConfig); 42 | 43 | const config = readConfig(); 44 | 45 | expect(config).toMatchSnapshot(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/runner/utils/delay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | function delay(milliseconds: number): Promise { 8 | return new Promise((resolve: Function) => { 9 | setTimeout(resolve, milliseconds); 10 | }); 11 | } 12 | 13 | export default delay; 14 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidDevice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2019 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type { DeviceInterface } from './DeviceInterface'; 8 | 9 | import exec from '../exec'; 10 | import delay from '../delay'; 11 | import log from '../log'; 12 | 13 | const TAG = 'PIXELS_CATCHER::ANDROID_DEVICE'; 14 | 15 | class AndroidDevice implements DeviceInterface { 16 | _name: string; 17 | 18 | constructor(name: string) { 19 | this._name = name; 20 | } 21 | 22 | 23 | _getDevices(): Array { 24 | const cmd = 'adb devices'; 25 | const devices = exec(cmd).split('\n').slice(1) 26 | .filter((line: string): boolean => Boolean(line)) 27 | .map((line: string): string => line.split('\t')[0]); 28 | 29 | return devices; 30 | } 31 | 32 | 33 | _isDeviceAvailable(name: string): boolean { 34 | const devices = this._getDevices(); 35 | let isAvailable = false; 36 | 37 | for (let ind = devices.length - 1; ind >= 0; --ind) { 38 | if (devices[ind].indexOf(name) >= 0) { 39 | isAvailable = true; 40 | break; 41 | } 42 | } 43 | 44 | return isAvailable; 45 | } 46 | 47 | 48 | async start(params: any = []): Promise { 49 | if (params.length !== 0) { 50 | log.e(TAG, 'There are currently no supported device parameters for physical devices, yet you tried to pass some im'); 51 | process.exit(-1); 52 | } 53 | if (!this._isDeviceAvailable(this._name)) { 54 | log.e(TAG, `Invalid name provided [${this._name}], check that the name is \ 55 | correct and device is available. Available devices: 56 | ${this._getDevices().map((device: any): any => ` - ${device}`).join('\n')}`); 57 | throw new Error(`Invalid emulator ${this._name}`); 58 | } 59 | } 60 | 61 | 62 | async stop(): Promise { 63 | log.v(TAG, 'Not stopping anything as it is assumed to be a physical device. Your responsibility!'); 64 | } 65 | 66 | 67 | isAppInstalled(packageName: string): boolean { 68 | const cmd = `adb -s ${this._name} shell pm list packages`; 69 | 70 | log.v(TAG, `Checking if [${packageName}] is installed`); 71 | 72 | const allPackages = exec(cmd); 73 | const isInstalled = allPackages.indexOf(packageName) >= 0; 74 | 75 | log.v(TAG, `Package [${packageName}] is ${isInstalled ? 'Installed' : 'Not installed'}`); 76 | 77 | return isInstalled; 78 | } 79 | 80 | 81 | async uninstallApp(name: string): Promise { 82 | log.v(TAG, `Uninstalling ${name}`); 83 | const isInstalled = await this.isAppInstalled(name); 84 | if (isInstalled) { 85 | const cmd = `adb -s ${this._name} uninstall ${name}`; 86 | exec(cmd); 87 | } 88 | log.v(TAG, 'Uninstalling completed'); 89 | } 90 | 91 | 92 | async installApp(name: string, apkFile: string): Promise { 93 | log.v(TAG, `Installing apk [${apkFile}]`); 94 | 95 | await this.uninstallApp(name); 96 | 97 | let tryCnt = 3; 98 | 99 | while (tryCnt >= 0) { 100 | const cmd = `adb -s ${this._name} install -r ${apkFile}`; 101 | const res = exec(cmd); 102 | log.v(TAG, 'Installed', res); 103 | const isOffline = res.indexOf('device offline') >= 0; 104 | if (isOffline) { 105 | await delay(1000); 106 | } else { 107 | const isSuccess = res.indexOf('Success') >= 0; 108 | if (isSuccess) { 109 | break; 110 | } else { 111 | log.e(TAG, `ERROR: Failed install apk [${apkFile}]`); 112 | process.exit(-1); 113 | } 114 | } 115 | tryCnt--; 116 | } 117 | } 118 | 119 | 120 | startApp(packageName: string, activityName: string): void { 121 | log.v(TAG, `Starting application [${packageName}]`); 122 | 123 | const cmd = `adb -s ${this._name} shell am start -n ${packageName}/${activityName}`; 124 | const result = exec(cmd); 125 | 126 | if (result.indexOf('does not exist') >= 0 || result.indexOf('Error') >= 0) { 127 | log.e(TAG, `Cannot start [${packageName}] with activity [${activityName}]`); 128 | process.exit(-1); 129 | } 130 | 131 | log.v(TAG, 'Application started'); 132 | } 133 | } 134 | 135 | export default AndroidDevice; 136 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidEmulator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import { spawn } from 'child_process'; 8 | 9 | import exec from '../exec'; 10 | import delay from '../delay'; 11 | import log from '../log'; 12 | import emulatorCmd from './AndroidEmulatorCmd'; 13 | 14 | import type { DeviceInterface } from './DeviceInterface'; 15 | 16 | const TAG = 'PIXELS_CATCHER::UTIL_EMULATOR'; 17 | 18 | const startupErrorsDataToIgnore = [ 19 | // Some data appears in stderr when running the emulator first time 20 | '.avd/snapshots/default_boot/ram.img', 21 | 'qemu: unsupported keyboard', 22 | 'WARNING', 23 | ]; 24 | 25 | const canIgnoreErrorData = (data: string): boolean => { 26 | for (let i = 0; i < startupErrorsDataToIgnore.length; ++i) { 27 | if (data.indexOf(startupErrorsDataToIgnore[i]) !== -1) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | }; 34 | 35 | class AndroidEmulator implements DeviceInterface { 36 | _name: string; 37 | 38 | _canStopDevice: boolean; 39 | 40 | constructor(name: string, canStopDevice?: boolean) { 41 | this._name = name; 42 | this._canStopDevice = Boolean(canStopDevice); 43 | } 44 | 45 | _getDevices(): Array { 46 | const cmd = 'emulator -avd -list-avds'; 47 | const devices = exec(cmd).split('\n') 48 | .filter((line: string): boolean => Boolean(line)); 49 | 50 | return devices; 51 | } 52 | 53 | _isDeviceAvailable(name: string): boolean { 54 | const devices = this._getDevices(); 55 | let isAvailable = false; 56 | 57 | for (let ind = devices.length - 1; ind >= 0; --ind) { 58 | if (devices[ind].indexOf(name) >= 0) { 59 | isAvailable = true; 60 | break; 61 | } 62 | } 63 | 64 | return isAvailable; 65 | } 66 | 67 | _getActiveDevice(): any { 68 | log.v(TAG, 'Get active device'); 69 | const device = exec('adb devices').split('\n') 70 | .filter((line: string): boolean => line.indexOf('emulator') === 0)[0]; 71 | 72 | if (!device) { 73 | log.v(TAG, 'No active devices'); 74 | return undefined; 75 | } 76 | const name = device.split('\t')[0]; 77 | 78 | log.v(TAG, 'Active device', name); 79 | return name; 80 | } 81 | 82 | async start(params: any = []): Promise { 83 | if (!this._isDeviceAvailable(this._name)) { 84 | log.e(TAG, `Invalid name provided [${this._name}], check that the name is \ 85 | correct and device is available. Available devices: 86 | ${this._getDevices().map((device: any): any => ` - ${device}`).join('\n')}`); 87 | throw new Error(`Invalid emulator ${this._name}`); 88 | } 89 | 90 | if (this._getActiveDevice()) { 91 | log.e(TAG, 'Other emulator already started'); 92 | if (this._canStopDevice) { 93 | log.e(TAG, 'Stopping emulator'); 94 | await this.stop(); 95 | } else { 96 | log.d(TAG, 'Using active emulator'); 97 | return; 98 | } 99 | } 100 | 101 | log.d(TAG, `Starting emulator [${this._name}]`); 102 | log.v(TAG, `cmd: ${emulatorCmd}`); 103 | log.v(TAG, `params: ${[ 104 | '-avd', this._name, 105 | ...params, 106 | ].filter((value: any): any => Boolean(value))}`); 107 | const result = spawn(emulatorCmd, [ 108 | '-avd', this._name, 109 | ...params, 110 | ].filter((value: any): any => Boolean(value))); 111 | 112 | let deviceBooted = false; 113 | 114 | result.stdout.on('data', (data: any): any => { 115 | log.d(TAG, `stdout: ${data}`); 116 | if (data.toString().toLowerCase().includes('boot completed')) { 117 | deviceBooted = true; 118 | } 119 | }); 120 | 121 | result.stderr.on('data', (data: any): any => { 122 | // Some data appears in stderr when running the emulator first time 123 | const stringRepresentation = data.toString(); 124 | if (canIgnoreErrorData(stringRepresentation)) { 125 | log.w(TAG, `Ignore: ${stringRepresentation}`); 126 | return; 127 | } 128 | log.e(TAG, `Failed to load emulator, stderr: ${data}`); 129 | process.exit(-1); 130 | }); 131 | 132 | result.on('close', (code: any): any => { 133 | log.v(TAG, `on close: child process exited with code ${code}`); 134 | }); 135 | 136 | let tryCnt = (60 * 2) / 5; // 2 minutes with 5000 delay 137 | 138 | while (--tryCnt >= 0 && !deviceBooted) { 139 | log.v(TAG, 'awaiting when device is booted'); 140 | await delay(5000); 141 | } 142 | 143 | if (!deviceBooted) { 144 | log.e(TAG, 'Failed to load emulator in 30 seconds. Check your emulator. Or try to run it with "-no-snapshot"'); 145 | throw new Error('Device is not loaded in 30 seconds'); 146 | } 147 | } 148 | 149 | async stop(): Promise { 150 | if (!this._canStopDevice) { 151 | log.v(TAG, 'Stopping device is restricted in config'); 152 | return; 153 | } 154 | log.v(TAG, 'Stopping active device'); 155 | try { 156 | exec(`adb -s ${this._getActiveDevice()} emu kill;`); 157 | } catch (err) { 158 | log.e(err instanceof Error ? err.message : 'Unknown error'); 159 | } 160 | await delay(5000); 161 | log.v(TAG, 'Active device stopped'); 162 | } 163 | 164 | isAppInstalled(packageName: string): boolean { 165 | const cmd = 'adb shell pm list packages'; 166 | 167 | log.v(TAG, `Checking if [${packageName}] is installed`); 168 | 169 | const allPackages = exec(cmd); 170 | const isInstalled = allPackages.indexOf(packageName) >= 0; 171 | 172 | log.v(TAG, `Package [${packageName}] is ${isInstalled ? 'Installed' : 'Not installed'}`); 173 | 174 | return isInstalled; 175 | } 176 | 177 | async uninstallApp(name: string): Promise { 178 | log.v(TAG, `Uninstalling ${name}`); 179 | const isInstalled = await this.isAppInstalled(name); 180 | if (isInstalled) { 181 | const cmd = `adb uninstall ${name}`; 182 | exec(cmd); 183 | } 184 | log.v(TAG, 'Uninstalling completed'); 185 | } 186 | 187 | async installApp(name: string, apkFile: string): Promise { 188 | let tryCnt = 3; 189 | 190 | log.v(TAG, `Installing apk [${apkFile}]`); 191 | 192 | await this.uninstallApp(name); 193 | 194 | while (tryCnt-- >= 0) { 195 | const cmd = `adb install -r ${apkFile}`; 196 | const res = exec(cmd); 197 | log.v(TAG, 'Installed', res); 198 | const isOffline = res.indexOf('device offline') >= 0; 199 | if (isOffline) { 200 | await delay(1000); 201 | } else { 202 | const isSuccess = res.indexOf('Success') >= 0; 203 | if (isSuccess) { 204 | break; 205 | } else { 206 | log.e(TAG, `ERROR: Failed install apk [${apkFile}]`); 207 | process.exit(-1); 208 | } 209 | } 210 | } 211 | } 212 | 213 | startApp(packageName: string, activityName: string): void { 214 | log.v(TAG, `Starting application [${packageName}]`); 215 | 216 | const cmd = `adb shell am start -n ${packageName}/${activityName}`; 217 | const result = exec(cmd); 218 | 219 | if (result.indexOf('does not exist') >= 0 || result.indexOf('Error') >= 0) { 220 | log.e(TAG, `Cannot start [${packageName}] with activity [${activityName}]`); 221 | process.exit(-1); 222 | } 223 | 224 | log.v(TAG, 'Application started'); 225 | } 226 | } 227 | 228 | export default AndroidEmulator; 229 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidEmulatorCmd.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import exec from '../exec'; 8 | import isCommand from '../isCommand'; 9 | 10 | export default process.env.ANDROID_EMULATOR 11 | || (isCommand('emulator') ? 'emulator' : undefined) 12 | || ( 13 | exec('uname -s').trim() === 'Darwin' 14 | ? `${process.env.HOME || ''}/Library/Android/sdk/emulator/emulator` 15 | : 'emulator'); 16 | -------------------------------------------------------------------------------- /src/runner/utils/device/DeviceInterface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /** 3 | * Copyright (c) Maksym Rusynyk 2018 - present 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | export type StartParamsType = Array; 10 | 11 | export interface DeviceInterface { 12 | // constructor(deviceName: string, canStopDevice?: boolean): void, 13 | 14 | start(params: StartParamsType): Promise, 15 | 16 | isAppInstalled(appName: string): boolean, 17 | 18 | installApp(appName: string, appFile: string): Promise, 19 | 20 | startApp(appName: string, activityName: string, locale?: string): void, 21 | 22 | uninstallApp(name: string): Promise, 23 | 24 | stop(): Promise, 25 | } 26 | -------------------------------------------------------------------------------- /src/runner/utils/device/IosSimulator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | /* @flow */ 8 | import type { DeviceInterface, StartParamsType } from './DeviceInterface'; 9 | 10 | import exec from '../exec'; 11 | import log from '../log'; 12 | import delay from '../delay'; 13 | 14 | const TAG = 'PIXELS_CATCHER::UTIL_SIMULATOR'; 15 | 16 | type DeviceType = { 17 | availability: string, 18 | state: string, 19 | isAvailable: boolean, 20 | name: string, 21 | udid: string, 22 | availabilityError: string, 23 | }; 24 | 25 | class IOSSimulator implements DeviceInterface { 26 | _name: string; 27 | 28 | _canStopDevice: boolean; 29 | 30 | constructor(name: string, canStopDevice?: boolean) { 31 | this._name = name; 32 | this._canStopDevice = Boolean(canStopDevice); 33 | } 34 | 35 | 36 | _getAvailableDevices(): Array { 37 | const cmd = 'xcrun simctl list --json'; 38 | const response = JSON.parse(exec(cmd)); 39 | const { devices } = response; 40 | const availableDevices: Array = []; 41 | 42 | Object.keys(devices).forEach((name: string) => { 43 | devices[name].forEach((device: DeviceType) => { 44 | if (device.isAvailable) { 45 | availableDevices.push(device); 46 | } 47 | }); 48 | }); 49 | 50 | return availableDevices; 51 | } 52 | 53 | 54 | _getDeviceByName(name: string): DeviceType | void { 55 | const devices = this._getAvailableDevices(); 56 | let device; 57 | 58 | for (let ind = devices.length - 1; ind >= 0; --ind) { 59 | if (devices[ind].name === name) { 60 | device = devices[ind]; 61 | break; 62 | } 63 | } 64 | 65 | return device; 66 | } 67 | 68 | 69 | _getDeviceByUid(uid: string): DeviceType | void { 70 | const devices = this._getAvailableDevices(); 71 | let device; 72 | 73 | for (let ind = devices.length - 1; ind >= 0; --ind) { 74 | if (devices[ind].udid === uid) { 75 | device = devices[ind]; 76 | break; 77 | } 78 | } 79 | 80 | return device; 81 | } 82 | 83 | 84 | _getDeviceWithStatus(status: string): DeviceType | void { 85 | const devices = this._getAvailableDevices(); 86 | let device; 87 | 88 | for (let ind = devices.length - 1; ind >= 0; --ind) { 89 | if (devices[ind].state === status) { 90 | device = devices[ind]; 91 | break; 92 | } 93 | } 94 | 95 | return device; 96 | } 97 | 98 | 99 | _getUid(name: string): string | void { 100 | const device = this._getDeviceByName(name); 101 | log.v(TAG, `Device ${name} is:`, device); 102 | return device ? device.udid : undefined; 103 | } 104 | 105 | 106 | async _boot(uid: string): Promise { 107 | const device = this._getDeviceByUid(uid); 108 | if (!device) { 109 | throw new Error(`Invalid device uid [${uid}], cannot find it`); 110 | } 111 | if (device.state === 'Booted') { 112 | log.i(TAG, `Device [${device.name}] already booted`); 113 | return; 114 | } 115 | const response = exec(`xcrun simctl boot ${uid}`); 116 | if (response) { 117 | log.v(TAG, 'boot response:', response); 118 | } 119 | } 120 | 121 | 122 | async _open(uid: string): Promise { 123 | const activeXcode = exec('xcode-select -p').trim(); 124 | log.v(TAG, `Active Xcode: ${activeXcode}`); 125 | const simulatorApp = `${activeXcode}/Applications/Simulator.app`; 126 | log.v(TAG, `starting ${simulatorApp}`); 127 | exec(`open -a ${simulatorApp} --args -CurrentDeviceUDID ${uid}`); 128 | log.v(TAG, 'started'); 129 | } 130 | 131 | 132 | async start(params: StartParamsType): Promise { 133 | log.v(TAG, 'Starting device with params:', params); 134 | 135 | this.stop(); 136 | 137 | const uid = this._getUid(this._name); 138 | log.i(TAG, `Uid of the device is [${uid || '-'}]`); 139 | 140 | if (!uid) { 141 | throw new Error(`Invalid simulator [${this._name}], cannot find uid`); 142 | } 143 | 144 | await this._boot(uid); 145 | await this._open(uid); 146 | 147 | log.v(TAG, 'Device started', this._getDeviceByUid(uid)); 148 | } 149 | 150 | 151 | isAppInstalled(appName: string): boolean { 152 | log.v(`isAppInstalled: appName [${appName}]`); 153 | return false; 154 | } 155 | 156 | 157 | async installApp(appName: string, appFile: string): Promise { 158 | this.uninstallApp(appName); 159 | log.v(TAG, `Installing application [${appName}], appFile [${appFile}]`); 160 | exec(`xcrun simctl install booted ${appFile}`); 161 | } 162 | 163 | 164 | startApp(appName: string, activityName: string, locale?: string): void { 165 | const withLocale = locale ? `-AppleLanguages "(${locale})"` : ''; 166 | log.v(TAG, `startApp: appName [${appName}], activityName [${activityName}], locale [${locale || '-'}]`); 167 | exec(`xcrun simctl launch booted ${appName} ${withLocale}`); 168 | } 169 | 170 | 171 | async uninstallApp(appName: string): Promise { 172 | log.v(TAG, `Uninstalling application [${appName}]`); 173 | exec(`xcrun simctl uninstall booted ${appName}`); 174 | } 175 | 176 | 177 | async stop(): Promise { 178 | if (!this._canStopDevice) { 179 | log.v(TAG, 'Stopping device is restricted in config'); 180 | return; 181 | } 182 | 183 | log.v(TAG, 'Stopping all devices'); 184 | 185 | exec('osascript -e \'tell application "iOS Simulator" to quit\''); 186 | exec('osascript -e \'tell application "Simulator" to quit\''); 187 | 188 | let device = this._getDeviceWithStatus('Shutting Down'); 189 | 190 | while (device) { 191 | log.v(TAG, `Awaiting for shutdown completed (Device ${device.name} has ` 192 | + `state ${device.state})`); 193 | await delay(1000); 194 | device = this._getDeviceWithStatus('Shutting Down'); 195 | } 196 | 197 | log.v(TAG, 'Devices stopped'); 198 | } 199 | } 200 | 201 | export default IOSSimulator; 202 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidDevice.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | describe('AndroidDevice', () => { 3 | it('initialise', () => {}); 4 | }); 5 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidEmulator.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | jest.mock('child_process', () => ({ spawn: jest.fn() })); 3 | jest.mock('../../exec', () => jest.fn(() => '')); 4 | jest.mock('../../delay', () => jest.fn()); 5 | jest.mock('../../log', () => ({ 6 | v: jest.fn(), 7 | d: jest.fn(), 8 | e: jest.fn(), 9 | })); 10 | jest.mock('../AndroidEmulatorCmd', () => 'emulator'); 11 | 12 | const { spawn } = require('child_process'); 13 | 14 | const AndroidEmulator = require('../AndroidEmulator').default; 15 | const exec = require('../../exec'); 16 | const delay = require('../../delay'); 17 | 18 | describe('AndroidEmulator', () => { 19 | const name = 'emulator_name'; 20 | 21 | beforeEach(() => { 22 | jest.resetAllMocks(); 23 | }); 24 | 25 | it('initialise emulator', () => { 26 | // $FlowFixMe: ignore for mock 27 | exec.mockImplementationOnce(() => 'avd devices'); 28 | 29 | const emularor = new AndroidEmulator(name); 30 | expect(emularor).toMatchSnapshot(); 31 | }); 32 | 33 | it('start emulator when it is not available should throw error', async () => { 34 | // $FlowFixMe: ignore for mocks 35 | exec.mockImplementation(() => 'avd devices'); 36 | 37 | const emularor = new AndroidEmulator(name); 38 | let exception; 39 | 40 | try { 41 | await emularor.start(); 42 | } catch (err) { 43 | exception = err; 44 | } 45 | 46 | expect(exception).toMatchSnapshot(); 47 | }); 48 | 49 | it('start emulator when it is available but not started throws error if not started', async () => { 50 | // $FlowFixMe: ignore for mock 51 | exec.mockImplementation(() => `avd devices including ${name}`); 52 | const spawnMock = { 53 | stdout: { on: jest.fn() }, 54 | stderr: { on: jest.fn() }, 55 | on: jest.fn(), 56 | }; 57 | spawn.mockImplementationOnce(() => spawnMock); 58 | 59 | const emularor = new AndroidEmulator(name); 60 | 61 | let exception; 62 | 63 | try { 64 | await emularor.start(); 65 | } catch (err) { 66 | exception = err; 67 | } 68 | 69 | expect(exception).toMatchSnapshot(); 70 | }); 71 | 72 | it('start emulator when it is available but not started', async () => { 73 | // $FlowFixMe: ignore for mock 74 | exec.mockImplementation(() => `avd devices including ${name}`); 75 | const spawnMock = { 76 | stdout: { on: jest.fn() }, 77 | stderr: { on: jest.fn() }, 78 | on: jest.fn(), 79 | }; 80 | spawn.mockImplementationOnce(() => spawnMock); 81 | 82 | const emularor = new AndroidEmulator(name); 83 | 84 | const startPromise = emularor.start(); 85 | 86 | const dataCallback = spawnMock.stdout.on.mock.calls[0][1]; 87 | dataCallback('boot completed'); 88 | 89 | await startPromise; 90 | }); 91 | 92 | it('start emulator when it is available and already started should stop it before starting', async () => { 93 | // $FlowFixMe: ignore for mock 94 | exec.mockImplementation((cmd) => { 95 | if (cmd === 'emulator -avd -list-avds') { 96 | return `avd devices including ${name}`; 97 | } 98 | return 'List of devices attached\nemulator-5554 device'; 99 | }); 100 | const spawnMock = { 101 | stdout: { on: jest.fn() }, 102 | stderr: { on: jest.fn() }, 103 | on: jest.fn(), 104 | }; 105 | 106 | spawn.mockImplementationOnce(() => spawnMock); 107 | // $FlowFixMe: ignore for mocks 108 | delay.mockImplementation(() => { 109 | const spawnMockCalls = spawnMock.stdout.on.mock.calls; 110 | if (spawnMockCalls && spawnMockCalls[0] && spawnMockCalls[0][1]) { 111 | const dataCallback = spawnMock.stdout.on.mock.calls[0][1]; 112 | dataCallback('boot completed'); 113 | } 114 | }); 115 | 116 | const emularor = new AndroidEmulator(name); 117 | 118 | const startPromise = emularor.start(); 119 | 120 | await startPromise; 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidEmulatorCmd.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | describe('AndroidEmulatorCmd', () => { 4 | beforeEach(() => { 5 | process.env.ANDROID_EMULATOR = ''; 6 | jest.resetModules(); 7 | }); 8 | 9 | it('returns command provided via ANDROID_EMULATOR', () => { 10 | process.env.ANDROID_EMULATOR = 'cmdFrom_ANDROID_EMULATOR'; 11 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 12 | 13 | expect(emulatorCmd).toBe(process.env.ANDROID_EMULATOR); 14 | }); 15 | 16 | it('returns command available in PATH', () => { 17 | process.env.ANDROID_EMULATOR = ''; 18 | jest.mock('../../isCommand', () => () => true); 19 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 20 | 21 | expect(emulatorCmd).toBe('emulator'); 22 | }); 23 | 24 | it('gets command from /Library/Android/sdk/emulator/emulator on mac', () => { 25 | process.env.ANDROID_EMULATOR = ''; 26 | jest.mock('../../isCommand', () => () => false); 27 | jest.mock('../../exec', () => () => 'Darwin'); 28 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 29 | 30 | expect(emulatorCmd 31 | .indexOf('Library/Android/sdk/emulator/emulator') > 0).toBe(true); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/IosSimulator.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | describe('IOSSimulator', () => { 3 | it('initialise', () => {}); 4 | }); 5 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/__snapshots__/AndroidEmulator.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AndroidEmulator initialise emulator 1`] = ` 4 | AndroidEmulator { 5 | "_canStopDevice": false, 6 | "_name": "emulator_name", 7 | } 8 | `; 9 | 10 | exports[`AndroidEmulator start emulator when it is available but not started throws error if not started 1`] = `[Error: Device is not loaded in 30 seconds]`; 11 | 12 | exports[`AndroidEmulator start emulator when it is not available should throw error 1`] = `[Error: Invalid emulator emulator_name]`; 13 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/__snapshots__/deviceProvider.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`deviceProvider provide Android Emulator 1`] = ` 4 | _class { 5 | "_name": "AndroidEmulator", 6 | "deviceName": "test", 7 | } 8 | `; 9 | 10 | exports[`deviceProvider provide AndroidDevice 1`] = ` 11 | _class2 { 12 | "_name": "AndroidDevice", 13 | "deviceName": "test", 14 | } 15 | `; 16 | 17 | exports[`deviceProvider provide iOS device throws error (not implemented) 1`] = `[Error: iOS devices are not supported yet]`; 18 | 19 | exports[`deviceProvider provide iOS simulator 1`] = ` 20 | _class3 { 21 | "_name": "IosSimulator", 22 | "deviceName": "test", 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/deviceProvider.js: -------------------------------------------------------------------------------- 1 | import getDevice from '../deviceProvider'; 2 | 3 | jest.mock('../AndroidEmulator', () => (class { 4 | constructor(name) { 5 | this._name = 'AndroidEmulator'; 6 | this.deviceName = name; 7 | } 8 | })); 9 | jest.mock('../AndroidDevice', () => (class { 10 | constructor(name) { 11 | this._name = 'AndroidDevice'; 12 | this.deviceName = name; 13 | } 14 | })); 15 | jest.mock('../IosSimulator', () => (class { 16 | constructor(name) { 17 | this._name = 'IosSimulator'; 18 | this.deviceName = name; 19 | } 20 | })); 21 | 22 | describe('deviceProvider', () => { 23 | it('provide Android Emulator', () => { 24 | const device = getDevice('test', 'android'); 25 | expect(device).toMatchSnapshot(); 26 | }); 27 | 28 | it('provide AndroidDevice', () => { 29 | const device = getDevice('test', 'android', true); 30 | expect(device).toMatchSnapshot(); 31 | }); 32 | 33 | it('provide iOS simulator', () => { 34 | const device = getDevice('test', 'ios'); 35 | expect(device).toMatchSnapshot(); 36 | }); 37 | 38 | it('provide iOS device throws error (not implemented)', () => { 39 | let error; 40 | let device; 41 | try { 42 | device = getDevice('test', 'ios', true); 43 | } catch (err) { 44 | error = err; 45 | } 46 | 47 | expect(device).toBe(undefined); 48 | expect(error).toMatchSnapshot(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/runner/utils/device/deviceProvider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2019 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type { DeviceInterface } from './DeviceInterface'; 8 | 9 | import log from '../log'; 10 | import AndroidEmulator from './AndroidEmulator'; 11 | import AndroidDevice from './AndroidDevice'; 12 | import IosSimulator from './IosSimulator'; 13 | 14 | const TAG = 'PIXELS_CATCHER::DEVICE_PROVIDER'; 15 | 16 | export default ( 17 | name: string, 18 | platform: string, 19 | isPhysicalDevice?: boolean, 20 | canStopDevice: boolean = true, 21 | ): DeviceInterface => { 22 | if (platform === 'android') { 23 | return isPhysicalDevice 24 | ? new AndroidDevice(name) 25 | : new AndroidEmulator(name, canStopDevice); 26 | } 27 | 28 | if (!isPhysicalDevice) { 29 | return new IosSimulator(name, canStopDevice); 30 | } 31 | 32 | log.e(TAG, 'iOS devices are not supported yet'); 33 | throw new Error('iOS devices are not supported yet'); 34 | }; 35 | -------------------------------------------------------------------------------- /src/runner/utils/exec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as childProcess from 'child_process'; 8 | 9 | import log from './log'; 10 | 11 | const TAG = 'PIXELS_CATCHER::UTIL_EXEC'; 12 | 13 | export default function exec(cmd: string): string { 14 | let result = ''; 15 | 16 | try { 17 | result = childProcess.execSync(cmd).toString(); 18 | } catch (err) { 19 | log.e(TAG, `Failed to execute [${cmd}], error: [${err instanceof Error ? err.message : 'Unknown error'}]`, err); 20 | } 21 | 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /src/runner/utils/isCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import exec from './exec'; 8 | 9 | function isCommand(cmd: string): boolean { 10 | const out = exec(`whereis ${cmd}`); 11 | 12 | return Boolean(out.trim()); 13 | } 14 | 15 | export default isCommand; 16 | -------------------------------------------------------------------------------- /src/runner/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | const TAG = 'PIXELS_CATCHER::UTIL_LOG'; 8 | const logLevels: { 9 | [key: string]: number 10 | } = { 11 | v: 4, 12 | d: 3, 13 | i: 2, 14 | w: 1, 15 | e: 0, 16 | }; 17 | let activeLevel = logLevels.i; 18 | 19 | const log: { 20 | [key: string]: Function 21 | } = { 22 | v(tag: string, ...args: any) { 23 | if (activeLevel >= logLevels.v) { 24 | global.console.log(`${tag}:`, ...args); 25 | } 26 | }, 27 | 28 | d: (tag: string, ...args: any) => { 29 | if (activeLevel >= logLevels.d) { 30 | global.console.log(`${tag}:`, ...args); 31 | } 32 | }, 33 | 34 | i: (tag: string, ...args: any) => { 35 | if (activeLevel >= logLevels.i) { 36 | global.console.log(`${tag}:`, ...args); 37 | } 38 | }, 39 | 40 | w: (tag: string, ...args: any) => { 41 | if (activeLevel >= logLevels.w) { 42 | global.console.log(`${tag} WARNING:`, ...args); 43 | } 44 | }, 45 | 46 | e: (tag: string, ...args: any) => { 47 | if (activeLevel >= logLevels.e) { 48 | global.console.log(`${tag} ERROR:`, ...args); 49 | } 50 | }, 51 | 52 | setLevel(level: string | undefined = 'i') { 53 | let nextLevel = logLevels[level]; 54 | if (nextLevel === undefined) { 55 | global.console.log(`${TAG} WARNING:`, `Invalid level [${level}]. Supported levels: ${Object.keys(logLevels).join(', ')}`); 56 | nextLevel = logLevels.i; 57 | } 58 | activeLevel = nextLevel; 59 | }, 60 | }; 61 | 62 | export default log; 63 | -------------------------------------------------------------------------------- /src/runner/utils/readConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as fs from 'fs'; 8 | import * as path from 'path'; 9 | 10 | import log from './log'; 11 | 12 | const TAG = 'PIXELS_CATCHER::UTIL_READ_CONFIG'; 13 | const CONFIG_FILE = 'pixels-catcher.json'; 14 | const PACKAGE_JSON_FILE = 'package.json'; 15 | 16 | const readConfigFromPackageJSON = (): any => { 17 | const projectPackageFile = path.join(process.cwd(), PACKAGE_JSON_FILE); 18 | 19 | if (!fs.existsSync(projectPackageFile)) { 20 | log.e(TAG, `Cannot find ${PACKAGE_JSON_FILE} file [${projectPackageFile}]. ` 21 | + 'Check that you started the script from the root of your application'); 22 | process.exit(-1); 23 | } 24 | 25 | const fileContent = fs.readFileSync(projectPackageFile, 'utf8'); 26 | 27 | return JSON.parse(fileContent).PixelsCatcher; 28 | }; 29 | 30 | const readConfigFromFile = (): any => { 31 | const configFile = path.join(process.cwd(), CONFIG_FILE); 32 | 33 | if (!fs.existsSync(configFile)) { 34 | log.w(TAG, `Cannot find [${configFile}] file`); 35 | return undefined; 36 | } 37 | 38 | const fileContent = fs.readFileSync(configFile, 'utf8'); 39 | 40 | return JSON.parse(fileContent); 41 | }; 42 | 43 | export default (): any => { 44 | const pixelsCatcherConfig = readConfigFromPackageJSON() 45 | || readConfigFromFile(); 46 | 47 | if (!pixelsCatcherConfig) { 48 | log.e(TAG, 'Cannot find "PixelsCatcher" in package.json or find ' 49 | + 'pixels-catcher.json file'); 50 | process.exit(-1); 51 | } 52 | 53 | return pixelsCatcherConfig; 54 | }; 55 | -------------------------------------------------------------------------------- /src/runner/utils/timeToSec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const timeToSec = (ms: number): number => { 9 | const sec = ms / 1000; 10 | return Math.round(sec * 1000) / 1000; 11 | }; 12 | 13 | export default timeToSec; 14 | --------------------------------------------------------------------------------
Please note that this class is used ONLY if you opt-in for the New Architecture (see the 33 | * `newArchEnabled` property). Is ignored otherwise. 34 | */ 35 | public class MainApplicationReactNativeHost extends ReactNativeHost { 36 | public MainApplicationReactNativeHost(Application application) { 37 | super(application); 38 | } 39 | 40 | @Override 41 | public boolean getUseDeveloperSupport() { 42 | return BuildConfig.DEBUG; 43 | } 44 | 45 | @Override 46 | protected List getPackages() { 47 | List packages = new PackageList(this).getPackages(); 48 | // Packages that cannot be autolinked yet can be added manually here, for example: 49 | // packages.add(new MyReactNativePackage()); 50 | // TurboModules must also be loaded here providing a valid TurboReactPackage implementation: 51 | // packages.add(new TurboReactPackage() { ... }); 52 | // If you have custom Fabric Components, their ViewManagers should also be loaded here 53 | // inside a ReactPackage. 54 | return packages; 55 | } 56 | 57 | @Override 58 | protected String getJSMainModuleName() { 59 | return "index"; 60 | } 61 | 62 | @NonNull 63 | @Override 64 | protected ReactPackageTurboModuleManagerDelegate.Builder 65 | getReactPackageTurboModuleManagerDelegateBuilder() { 66 | // Here we provide the ReactPackageTurboModuleManagerDelegate Builder. This is necessary 67 | // for the new architecture and to use TurboModules correctly. 68 | return new MainApplicationTurboModuleManagerDelegate.Builder(); 69 | } 70 | 71 | @Override 72 | protected JSIModulePackage getJSIModulePackage() { 73 | return new JSIModulePackage() { 74 | @Override 75 | public List getJSIModules( 76 | final ReactApplicationContext reactApplicationContext, 77 | final JavaScriptContextHolder jsContext) { 78 | final List specs = new ArrayList<>(); 79 | 80 | // Here we provide a new JSIModuleSpec that will be responsible of providing the 81 | // custom Fabric Components. 82 | specs.add( 83 | new JSIModuleSpec() { 84 | @Override 85 | public JSIModuleType getJSIModuleType() { 86 | return JSIModuleType.UIManager; 87 | } 88 | 89 | @Override 90 | public JSIModuleProvider getJSIModuleProvider() { 91 | final ComponentFactory componentFactory = new ComponentFactory(); 92 | CoreComponentsRegistry.register(componentFactory); 93 | 94 | // Here we register a Components Registry. 95 | // The one that is generated with the template contains no components 96 | // and just provides you the one from React Native core. 97 | MainComponentsRegistry.register(componentFactory); 98 | 99 | final ReactInstanceManager reactInstanceManager = getReactInstanceManager(); 100 | 101 | ViewManagerRegistry viewManagerRegistry = 102 | new ViewManagerRegistry( 103 | reactInstanceManager.getOrCreateViewManagers(reactApplicationContext)); 104 | 105 | return new FabricJSIModuleProvider( 106 | reactApplicationContext, 107 | componentFactory, 108 | ReactNativeConfig.DEFAULT_CONFIG, 109 | viewManagerRegistry); 110 | } 111 | }); 112 | return specs; 113 | } 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/newarchitecture/components/MainComponentsRegistry.java: -------------------------------------------------------------------------------- 1 | package com.demo.newarchitecture.components; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.proguard.annotations.DoNotStrip; 5 | import com.facebook.react.fabric.ComponentFactory; 6 | import com.facebook.soloader.SoLoader; 7 | 8 | /** 9 | * Class responsible to load the custom Fabric Components. This class has native methods and needs a 10 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 11 | * folder for you). 12 | * 13 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the 14 | * `newArchEnabled` property). Is ignored otherwise. 15 | */ 16 | @DoNotStrip 17 | public class MainComponentsRegistry { 18 | static { 19 | SoLoader.loadLibrary("fabricjni"); 20 | } 21 | 22 | @DoNotStrip private final HybridData mHybridData; 23 | 24 | @DoNotStrip 25 | private native HybridData initHybrid(ComponentFactory componentFactory); 26 | 27 | @DoNotStrip 28 | private MainComponentsRegistry(ComponentFactory componentFactory) { 29 | mHybridData = initHybrid(componentFactory); 30 | } 31 | 32 | @DoNotStrip 33 | public static MainComponentsRegistry register(ComponentFactory componentFactory) { 34 | return new MainComponentsRegistry(componentFactory); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java: -------------------------------------------------------------------------------- 1 | package com.demo.newarchitecture.modules; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.soloader.SoLoader; 8 | import java.util.List; 9 | 10 | /** 11 | * Class responsible to load the TurboModules. This class has native methods and needs a 12 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 13 | * folder for you). 14 | * 15 | * Please note that this class is used ONLY if you opt-in for the New Architecture (see the 16 | * `newArchEnabled` property). Is ignored otherwise. 17 | */ 18 | public class MainApplicationTurboModuleManagerDelegate 19 | extends ReactPackageTurboModuleManagerDelegate { 20 | 21 | private static volatile boolean sIsSoLibraryLoaded; 22 | 23 | protected MainApplicationTurboModuleManagerDelegate( 24 | ReactApplicationContext reactApplicationContext, List packages) { 25 | super(reactApplicationContext, packages); 26 | } 27 | 28 | protected native HybridData initHybrid(); 29 | 30 | native boolean canCreateTurboModule(String moduleName); 31 | 32 | public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder { 33 | protected MainApplicationTurboModuleManagerDelegate build( 34 | ReactApplicationContext context, List packages) { 35 | return new MainApplicationTurboModuleManagerDelegate(context, packages); 36 | } 37 | } 38 | 39 | @Override 40 | protected synchronized void maybeLoadOtherSoLibraries() { 41 | if (!sIsSoLibraryLoaded) { 42 | // If you change the name of your application .so file in the Android.mk file, 43 | // make sure you update the name here as well. 44 | SoLoader.loadLibrary("demo_appmodules"); 45 | sIsSoLibraryLoaded = true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | # Define the library name here. 4 | project(demo_appmodules) 5 | 6 | # This file includes all the necessary to let you build your application with the New Architecture. 7 | include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake) 8 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationModuleProvider.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationModuleProvider.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace facebook { 7 | namespace react { 8 | 9 | std::shared_ptr MainApplicationModuleProvider( 10 | const std::string &moduleName, 11 | const JavaTurboModule::InitParams ¶ms) { 12 | // Here you can provide your own module provider for TurboModules coming from 13 | // either your application or from external libraries. The approach to follow 14 | // is similar to the following (for a library called `samplelibrary`: 15 | // 16 | // auto module = samplelibrary_ModuleProvider(moduleName, params); 17 | // if (module != nullptr) { 18 | // return module; 19 | // } 20 | // return rncore_ModuleProvider(moduleName, params); 21 | 22 | // Module providers autolinked by RN CLI 23 | auto rncli_module = rncli_ModuleProvider(moduleName, params); 24 | if (rncli_module != nullptr) { 25 | return rncli_module; 26 | } 27 | 28 | return rncore_ModuleProvider(moduleName, params); 29 | } 30 | 31 | } // namespace react 32 | } // namespace facebook 33 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationModuleProvider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | std::shared_ptr MainApplicationModuleProvider( 12 | const std::string &moduleName, 13 | const JavaTurboModule::InitParams ¶ms); 14 | 15 | } // namespace react 16 | } // namespace facebook 17 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationTurboModuleManagerDelegate.h" 2 | #include "MainApplicationModuleProvider.h" 3 | 4 | namespace facebook { 5 | namespace react { 6 | 7 | jni::local_ref 8 | MainApplicationTurboModuleManagerDelegate::initHybrid( 9 | jni::alias_ref) { 10 | return makeCxxInstance(); 11 | } 12 | 13 | void MainApplicationTurboModuleManagerDelegate::registerNatives() { 14 | registerHybrid({ 15 | makeNativeMethod( 16 | "initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid), 17 | makeNativeMethod( 18 | "canCreateTurboModule", 19 | MainApplicationTurboModuleManagerDelegate::canCreateTurboModule), 20 | }); 21 | } 22 | 23 | std::shared_ptr 24 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 25 | const std::string &name, 26 | const std::shared_ptr &jsInvoker) { 27 | // Not implemented yet: provide pure-C++ NativeModules here. 28 | return nullptr; 29 | } 30 | 31 | std::shared_ptr 32 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 33 | const std::string &name, 34 | const JavaTurboModule::InitParams ¶ms) { 35 | return MainApplicationModuleProvider(name, params); 36 | } 37 | 38 | bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule( 39 | const std::string &name) { 40 | return getTurboModule(name, nullptr) != nullptr || 41 | getTurboModule(name, {.moduleName = name}) != nullptr; 42 | } 43 | 44 | } // namespace react 45 | } // namespace facebook 46 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | namespace facebook { 8 | namespace react { 9 | 10 | class MainApplicationTurboModuleManagerDelegate 11 | : public jni::HybridClass< 12 | MainApplicationTurboModuleManagerDelegate, 13 | TurboModuleManagerDelegate> { 14 | public: 15 | // Adapt it to the package you used for your Java class. 16 | static constexpr auto kJavaDescriptor = 17 | "Lcom/demo/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;"; 18 | 19 | static jni::local_ref initHybrid(jni::alias_ref); 20 | 21 | static void registerNatives(); 22 | 23 | std::shared_ptr getTurboModule( 24 | const std::string &name, 25 | const std::shared_ptr &jsInvoker) override; 26 | std::shared_ptr getTurboModule( 27 | const std::string &name, 28 | const JavaTurboModule::InitParams ¶ms) override; 29 | 30 | /** 31 | * Test-only method. Allows user to verify whether a TurboModule can be 32 | * created by instances of this class. 33 | */ 34 | bool canCreateTurboModule(const std::string &name); 35 | }; 36 | 37 | } // namespace react 38 | } // namespace facebook 39 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainComponentsRegistry.cpp: -------------------------------------------------------------------------------- 1 | #include "MainComponentsRegistry.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace facebook { 10 | namespace react { 11 | 12 | MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {} 13 | 14 | std::shared_ptr 15 | MainComponentsRegistry::sharedProviderRegistry() { 16 | auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry(); 17 | 18 | // Autolinked providers registered by RN CLI 19 | rncli_registerProviders(providerRegistry); 20 | 21 | // Custom Fabric Components go here. You can register custom 22 | // components coming from your App or from 3rd party libraries here. 23 | // 24 | // providerRegistry->add(concreteComponentDescriptorProvider< 25 | // AocViewerComponentDescriptor>()); 26 | return providerRegistry; 27 | } 28 | 29 | jni::local_ref 30 | MainComponentsRegistry::initHybrid( 31 | jni::alias_ref, 32 | ComponentFactory *delegate) { 33 | auto instance = makeCxxInstance(delegate); 34 | 35 | auto buildRegistryFunction = 36 | [](EventDispatcher::Weak const &eventDispatcher, 37 | ContextContainer::Shared const &contextContainer) 38 | -> ComponentDescriptorRegistry::Shared { 39 | auto registry = MainComponentsRegistry::sharedProviderRegistry() 40 | ->createComponentDescriptorRegistry( 41 | {eventDispatcher, contextContainer}); 42 | 43 | auto mutableRegistry = 44 | std::const_pointer_cast(registry); 45 | 46 | mutableRegistry->setFallbackComponentDescriptor( 47 | std::make_shared( 48 | ComponentDescriptorParameters{ 49 | eventDispatcher, contextContainer, nullptr})); 50 | 51 | return registry; 52 | }; 53 | 54 | delegate->buildRegistryFunction = buildRegistryFunction; 55 | return instance; 56 | } 57 | 58 | void MainComponentsRegistry::registerNatives() { 59 | registerHybrid({ 60 | makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid), 61 | }); 62 | } 63 | 64 | } // namespace react 65 | } // namespace facebook 66 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainComponentsRegistry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | class MainComponentsRegistry 12 | : public facebook::jni::HybridClass { 13 | public: 14 | // Adapt it to the package you used for your Java class. 15 | constexpr static auto kJavaDescriptor = 16 | "Lcom/demo/newarchitecture/components/MainComponentsRegistry;"; 17 | 18 | static void registerNatives(); 19 | 20 | MainComponentsRegistry(ComponentFactory *delegate); 21 | 22 | private: 23 | static std::shared_ptr 24 | sharedProviderRegistry(); 25 | 26 | static jni::local_ref initHybrid( 27 | jni::alias_ref, 28 | ComponentFactory *delegate); 29 | }; 30 | 31 | } // namespace react 32 | } // namespace facebook 33 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/OnLoad.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "MainApplicationTurboModuleManagerDelegate.h" 3 | #include "MainComponentsRegistry.h" 4 | 5 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { 6 | return facebook::jni::initialize(vm, [] { 7 | facebook::react::MainApplicationTurboModuleManagerDelegate:: 8 | registerNatives(); 9 | facebook::react::MainComponentsRegistry::registerNatives(); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | demo 3 | 4 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "33.0.0" 6 | minSdkVersion = 21 7 | compileSdkVersion = 33 8 | targetSdkVersion = 33 9 | 10 | if (System.properties['os.arch'] == "aarch64") { 11 | // For M1 Users we need to use the NDK 24 which added support for aarch64 12 | ndkVersion = "24.0.8215888" 13 | } else { 14 | // Otherwise we default to the side-by-side NDK version from AGP. 15 | ndkVersion = "21.4.7075529" 16 | } 17 | } 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | dependencies { 23 | classpath("com.android.tools.build:gradle:7.2.1") 24 | classpath("com.facebook.react:react-native-gradle-plugin") 25 | classpath("de.undercouch:gradle-download-task:5.0.1") 26 | // NOTE: Do not place your application dependencies here; they belong 27 | // in the individual module build.gradle files 28 | } 29 | } 30 | 31 | allprojects { 32 | repositories { 33 | maven { 34 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 35 | url("$rootDir/../node_modules/react-native/android") 36 | } 37 | maven { 38 | // Android JSC is installed from npm 39 | url("$rootDir/../node_modules/jsc-android/dist") 40 | } 41 | mavenCentral { 42 | // We don't want to fetch react-native from Maven Central as there are 43 | // older versions over there. 44 | content { 45 | excludeGroup "com.facebook.react" 46 | } 47 | } 48 | google() 49 | maven { url 'https://www.jitpack.io' } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /demo/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.125.0 29 | 30 | # Use this property to specify which architecture you want to build. 31 | # You can also override it from the CLI using 32 | # ./gradlew -PreactNativeArchitectures=x86_64 33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 34 | 35 | # Use this property to enable support to the new architecture. 36 | # This will allow you to use TurboModules and the Fabric render in 37 | # your application. You should enable this flag either if you want 38 | # to write custom TurboModules/Fabric components OR use libraries that 39 | # are providing them. 40 | newArchEnabled=false 41 | -------------------------------------------------------------------------------- /demo/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /demo/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /demo/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/react-native-gradle-plugin') 5 | 6 | if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") { 7 | include(":ReactAndroid") 8 | project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid') 9 | include(":ReactAndroid:hermes-engine") 10 | project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine') 11 | } 12 | -------------------------------------------------------------------------------- /demo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "displayName": "demo" 4 | } -------------------------------------------------------------------------------- /demo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './App'; 7 | import AppWithNavigation from './AppWithNavigation'; 8 | import {name as appName} from './app.json'; 9 | 10 | const snapshots = true; 11 | const checkNavigation = false 12 | 13 | if (snapshots) { 14 | require('./indexSnapshot'); 15 | } else if (checkNavigation) { 16 | AppRegistry.registerComponent(appName, () => AppWithNavigation); 17 | } else { 18 | AppRegistry.registerComponent(appName, () => App); 19 | } 20 | -------------------------------------------------------------------------------- /demo/indexSnapshot.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, Text, View } from 'react-native'; 3 | import { WebView } from 'react-native-webview'; 4 | import { registerSnapshot, runSnapshots, Snapshot } from 'pixels-catcher'; 5 | import { NavigationContainer } from '@react-navigation/native'; 6 | import { createStackNavigator } from '@react-navigation/stack'; 7 | 8 | import App from './App'; 9 | import { HomeScreen } from './AppWithNavigation'; 10 | import { name as appName } from './app.json'; 11 | 12 | const baseUrl = Platform.select({ 13 | // Put real IP of your server to run on real device 14 | android: 'http://10.0.2.2:3000', 15 | ios: 'http://127.0.0.1:3000', 16 | }); 17 | const useFailedTest = false; 18 | 19 | const appSnapshot = false; 20 | 21 | if (appSnapshot) { 22 | registerSnapshot( 23 | class SnapshotClass extends Snapshot { 24 | static snapshotName = 'AppSnapshot'; 25 | 26 | componentDidMount() { 27 | setTimeout(() => { 28 | // delay for rendering images 29 | this.props.onReady(); 30 | }, 1000); 31 | } 32 | 33 | renderContent() { 34 | return ; 35 | } 36 | }, 37 | ); 38 | } 39 | 40 | registerSnapshot( 41 | class SnapshotClass extends Snapshot { 42 | static snapshotName = 'HomeScreen'; 43 | 44 | renderContent() { 45 | return ; 46 | } 47 | }, 48 | ); 49 | 50 | if (useFailedTest) { 51 | registerSnapshot( 52 | class SnapshotClass extends Snapshot { 53 | static snapshotName = 'AppSnapshotWithWrongRefImg'; 54 | 55 | renderContent() { 56 | return ; 57 | } 58 | }, 59 | ); 60 | } 61 | 62 | registerSnapshot( 63 | class SnapshotClass extends Snapshot { 64 | static snapshotName = 'someComponent'; 65 | 66 | renderContent() { 67 | return ( 68 | 69 | Some component 70 | 71 | ); 72 | } 73 | }, 74 | ); 75 | 76 | const useWebView = false; 77 | 78 | if (useWebView) { 79 | registerSnapshot( 80 | class SnapshotClass extends Snapshot { 81 | static snapshotName = 'WebViewTest'; 82 | 83 | componentDidMount() { 84 | // override default componentDidMount from Snapshot to delay it 85 | // until WebView is loaded. onLoad from WebView is used 86 | } 87 | 88 | renderContent() { 89 | return ( 90 | { 96 | setTimeout(() => { 97 | this.props.onReady(); 98 | }, 50); 99 | }} 100 | /> 101 | ); 102 | } 103 | }, 104 | ); 105 | } 106 | 107 | registerSnapshot( 108 | class SnapshotClass extends Snapshot { 109 | static snapshotName = 'longContent'; 110 | 111 | renderContent() { 112 | return ( 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | ); 121 | } 122 | }, 123 | ); 124 | 125 | const Stack = createStackNavigator(); 126 | 127 | function getRootElement(SnapshotsContainer) { 128 | const RootElement = ({ children }) => ( 129 | 130 | 131 | 139 | 140 | 141 | ); 142 | return RootElement; 143 | } 144 | 145 | runSnapshots(appName, { baseUrl, getRootElement }); 146 | -------------------------------------------------------------------------------- /demo/ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, '12.4' 5 | install! 'cocoapods', :deterministic_uuids => false 6 | 7 | target 'demo' do 8 | config = use_native_modules! 9 | 10 | # Flags change depending on the env values. 11 | flags = get_default_flags() 12 | 13 | use_react_native!( 14 | :path => config[:reactNativePath], 15 | # Hermes is now enabled by default. Disable by setting this flag to false. 16 | # Upcoming versions of React Native may rely on get_default_flags(), but 17 | # we make it explicit here to aid in the React Native upgrade process. 18 | :hermes_enabled => true, 19 | :fabric_enabled => flags[:fabric_enabled], 20 | # Enables Flipper. 21 | # 22 | # Note that if you have use_frameworks! enabled, Flipper will not work and 23 | # you should disable the next line. 24 | # :flipper_configuration => FlipperConfiguration.enabled, 25 | # An absolute path to your application root. 26 | :app_path => "#{Pod::Config.instance.installation_root}/.." 27 | ) 28 | 29 | target 'demoTests' do 30 | inherit! :complete 31 | # Pods for testing 32 | end 33 | 34 | post_install do |installer| 35 | react_native_post_install( 36 | installer, 37 | # Set `mac_catalyst_enabled` to `true` in order to apply patches 38 | # necessary for Mac Catalyst builds 39 | :mac_catalyst_enabled => false 40 | ) 41 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /demo/ios/_xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /demo/ios/demo.xcodeproj/xcshareddata/xcschemes/demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /demo/ios/demo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/ios/demo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/ios/demo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /demo/ios/demo/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | #import 8 | 9 | #if RCT_NEW_ARCH_ENABLED 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | 17 | #import 18 | 19 | static NSString *const kRNConcurrentRoot = @"concurrentRoot"; 20 | 21 | @interface AppDelegate () { 22 | RCTTurboModuleManager *_turboModuleManager; 23 | RCTSurfacePresenterBridgeAdapter *_bridgeAdapter; 24 | std::shared_ptr _reactNativeConfig; 25 | facebook::react::ContextContainer::Shared _contextContainer; 26 | } 27 | @end 28 | #endif 29 | 30 | @implementation AppDelegate 31 | 32 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 33 | { 34 | RCTAppSetupPrepareApp(application); 35 | 36 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 37 | 38 | #if RCT_NEW_ARCH_ENABLED 39 | _contextContainer = std::make_shared(); 40 | _reactNativeConfig = std::make_shared(); 41 | _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); 42 | _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; 43 | bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; 44 | #endif 45 | 46 | NSDictionary *initProps = [self prepareInitialProps]; 47 | UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"demo", initProps); 48 | 49 | if (@available(iOS 13.0, *)) { 50 | rootView.backgroundColor = [UIColor systemBackgroundColor]; 51 | } else { 52 | rootView.backgroundColor = [UIColor whiteColor]; 53 | } 54 | 55 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 56 | UIViewController *rootViewController = [UIViewController new]; 57 | rootViewController.view = rootView; 58 | self.window.rootViewController = rootViewController; 59 | [self.window makeKeyAndVisible]; 60 | return YES; 61 | } 62 | 63 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. 64 | /// 65 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html 66 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). 67 | /// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`. 68 | - (BOOL)concurrentRootEnabled 69 | { 70 | // Switch this bool to turn on and off the concurrent root 71 | return true; 72 | } 73 | 74 | - (NSDictionary *)prepareInitialProps 75 | { 76 | NSMutableDictionary *initProps = [NSMutableDictionary new]; 77 | 78 | #ifdef RCT_NEW_ARCH_ENABLED 79 | initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]); 80 | #endif 81 | 82 | return initProps; 83 | } 84 | 85 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 86 | { 87 | #if DEBUG 88 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 89 | #else 90 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 91 | #endif 92 | } 93 | 94 | #if RCT_NEW_ARCH_ENABLED 95 | 96 | #pragma mark - RCTCxxBridgeDelegate 97 | 98 | - (std::unique_ptr)jsExecutorFactoryForBridge:(RCTBridge *)bridge 99 | { 100 | _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge 101 | delegate:self 102 | jsInvoker:bridge.jsCallInvoker]; 103 | return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager); 104 | } 105 | 106 | #pragma mark RCTTurboModuleManagerDelegate 107 | 108 | - (Class)getModuleClassFromName:(const char *)name 109 | { 110 | return RCTCoreModulesClassProvider(name); 111 | } 112 | 113 | - (std::shared_ptr)getTurboModule:(const std::string &)name 114 | jsInvoker:(std::shared_ptr)jsInvoker 115 | { 116 | return nullptr; 117 | } 118 | 119 | - (std::shared_ptr)getTurboModule:(const std::string &)name 120 | initParams: 121 | (const facebook::react::ObjCTurboModule::InitParams &)params 122 | { 123 | return nullptr; 124 | } 125 | 126 | - (id)getModuleInstanceFromClass:(Class)moduleClass 127 | { 128 | return RCTAppSetupDefaultModuleFromClass(moduleClass); 129 | } 130 | 131 | #endif 132 | 133 | @end 134 | -------------------------------------------------------------------------------- /demo/ios/demo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /demo/ios/demo/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/ios/demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | demo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | localhost 32 | 33 | NSExceptionAllowsInsecureHTTPLoads 34 | 35 | 36 | 37 | 38 | NSLocationWhenInUseUsageDescription 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /demo/ios/demo/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/ios/demo/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/ios/demoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/ios/demoTests/demoTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface demoTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation demoTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /demo/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: true, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "cd ios/ && pod install", 7 | "android": "react-native run-android", 8 | "preinstall": "cd .. && rm -rf pixels-catcher-*.tgz && npm i && npm pack && mv pixels-catcher-*.tgz pixels-catcher.tgz", 9 | "ios": "react-native run-ios", 10 | "start": "react-native start", 11 | "test": "jest", 12 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx" 13 | }, 14 | "dependencies": { 15 | "@react-navigation/native": "^6.0.12", 16 | "@react-navigation/stack": "^6.2.3", 17 | "react": "18.1.0", 18 | "react-native": "0.70.0", 19 | "react-native-gesture-handler": "^2.6.0", 20 | "react-native-safe-area-context": "^4.3.3", 21 | "react-native-save-view": "^0.2.3", 22 | "react-native-screens": "^3.17.0", 23 | "react-native-webview": "^11.23.1" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.12.9", 27 | "@babel/runtime": "^7.12.5", 28 | "@react-native-community/eslint-config": "^2.0.0", 29 | "@tsconfig/react-native": "^2.0.2", 30 | "@types/jest": "^26.0.23", 31 | "@types/react-native": "^0.70.0", 32 | "@types/react-test-renderer": "^18.0.0", 33 | "@typescript-eslint/eslint-plugin": "^5.37.0", 34 | "@typescript-eslint/parser": "^5.37.0", 35 | "babel-jest": "^26.6.3", 36 | "eslint": "^7.32.0", 37 | "jest": "^26.6.3", 38 | "metro-react-native-babel-preset": "^0.72.1", 39 | "pixels-catcher": "../pixels-catcher.tgz", 40 | "react-test-renderer": "18.1.0", 41 | "typescript": "^4.8.3" 42 | }, 43 | "jest": { 44 | "preset": "react-native", 45 | "moduleFileExtensions": [ 46 | "ts", 47 | "tsx", 48 | "js", 49 | "jsx", 50 | "json", 51 | "node" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo/pixels-catcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "android": { 3 | "activityName": "com.demo.MainActivity", 4 | "deviceName": "Nexus_5X", 5 | "packageName": "com.demo", 6 | "snapshotsPath": "./snapshots/android", 7 | "dev": { 8 | "deviceParams": [ 9 | "-no-audio", 10 | "-no-snapshot" 11 | ] 12 | }, 13 | "debug": { 14 | "deviceParams": [ 15 | "-no-audio", 16 | "-no-snapshot" 17 | ], 18 | "canStopDevice": false, 19 | "appFile": "./android/app/build/outputs/apk/debug/app-debug.apk" 20 | }, 21 | "test": { 22 | "port": 3000, 23 | "canStopDevice": false, 24 | "deviceParams": [ 25 | "-no-audio", 26 | "-no-snapshot" 27 | ], 28 | "appFile": "./android/app/build/outputs/apk/debug/app-debug.apk" 29 | }, 30 | "release": { 31 | "deviceParams": [ 32 | "-no-audio", 33 | "-no-snapshot", 34 | "-no-window" 35 | ], 36 | "appFile": "./android/app/build/outputs/apk/release/app-release.apk" 37 | } 38 | }, 39 | "ios": { 40 | "deviceName": "iPhone 14 Plus", 41 | "packageName": "org.reactjs.native.example.demo", 42 | "snapshotsPath": "./snapshots/ios", 43 | "dev": {}, 44 | "debug": { 45 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app" 46 | }, 47 | "test": { 48 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app", 49 | "canStopDevice": false 50 | }, 51 | "testPort": {}, 52 | "testPort2": { 53 | "port": 3001 54 | }, 55 | "debugIphone6": { 56 | "deviceName": "iPhone 6", 57 | "snapshotsPath": "./snapshots/iPhone6", 58 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app" 59 | } 60 | }, 61 | "logLevel": "v", 62 | "timeout": 30000 63 | } 64 | -------------------------------------------------------------------------------- /demo/run_android_debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | export BUNDLE_IN_DEBUG="true" 7 | 8 | cd android 9 | rm -rf build .gradle/ app/build 10 | ./gradlew assembleDebug 11 | cd .. 12 | 13 | ./node_modules/.bin/pixels-catcher android debug 14 | -------------------------------------------------------------------------------- /demo/run_android_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | 7 | cd android 8 | rm -rf build .gradle/ app/build 9 | ./gradlew assembleRelease 10 | cd .. 11 | 12 | ./node_modules/.bin/pixels-catcher android release 13 | -------------------------------------------------------------------------------- /demo/run_android_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | export BUNDLE_IN_DEBUG="true" 7 | 8 | cd android 9 | rm -rf build .gradle/ app/build 10 | ./gradlew assembleDebug 11 | cd .. 12 | 13 | ../node_modules/.bin/flow-node ../src/runner/cli.js android test 14 | -------------------------------------------------------------------------------- /demo/run_ios_debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | BUILD_PATH="./build" 11 | rm -rf $BUILD_PATH 12 | 13 | xcrun xcodebuild \ 14 | -scheme demo \ 15 | -workspace demo.xcworkspace \ 16 | -configuration Debug \ 17 | -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.2' \ 18 | -derivedDataPath $BUILD_PATH \ 19 | ENTRY_FILE="indexSnapshot.js" \ 20 | build 21 | 22 | cd .. 23 | 24 | ./node_modules/.bin/pixels-catcher ios debug 25 | -------------------------------------------------------------------------------- /demo/run_ios_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | echo "ERROR: Not implemented. Requires https" 11 | -------------------------------------------------------------------------------- /demo/run_ios_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | BUILD_PATH="./build" 11 | rm -rf $BUILD_PATH 12 | 13 | xcrun xcodebuild \ 14 | -scheme demo \ 15 | -workspace demo.xcworkspace \ 16 | -configuration Debug \ 17 | -destination 'platform=iOS Simulator,name=iPhone 14 Plus,OS=13.3' \ 18 | -derivedDataPath $BUILD_PATH \ 19 | ENTRY_FILE="indexSnapshot.js" \ 20 | build 21 | 22 | cd .. 23 | 24 | ../node_modules/.bin/flow-node ../src/runner/cli.js ios test 25 | -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/someComponent.png -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | { 3 | "extends": "@tsconfig/react-native/tsconfig.json", /* Recommended React Native TSConfig base */ 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 6 | 7 | /* Completeness */ 8 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixels-catcher", 3 | "version": "0.13.1", 4 | "description": "UI snapshot testing for React Native", 5 | "main": "lib/client/index.js", 6 | "scripts": { 7 | "demo": "cd demo && ../node_modules/.bin/flow-node ../cli.js android debug", 8 | "lint": "eslint --ext .js --ext .ts --ext .tsx ./src/", 9 | "build": "./node_modules/.bin/tsc -p src/client && ./node_modules/.bin/tsc -p src/runner", 10 | "postbuild": "npm run assets", 11 | "prepare": "npm run build", 12 | "prepack": "npm run build", 13 | "check-types-client": "./node_modules/.bin/tsc --noEmit -p src/client", 14 | "check-types-runner": "./node_modules/.bin/tsc --noEmit -p src/runner", 15 | "check-types": "npm run check-types-client && npm run check-types-runner", 16 | "assets": "cp ./src/runner/server/dummy.png ./lib/runner/server/" 17 | }, 18 | "bin": "./lib/runner/cli.js", 19 | "files": [ 20 | "/lib", 21 | "/src/*.js", 22 | "/src/utils/*.js" 23 | ], 24 | "keywords": [ 25 | "react-native view android iOS UI screenshot snapshot testing" 26 | ], 27 | "author": "Maksym Rusynyk ", 28 | "license": "MIT", 29 | "engines": { 30 | "node": ">=14.*" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+ssh://git@github.com/rumax/react-native-PixelsCatcher.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/rumax/react-native-PixelsCatcher/issues" 38 | }, 39 | "homepage": "https://github.com/rumax/react-native-PixelsCatcher#readme", 40 | "dependencies": { 41 | "formidable": "^1.2.2", 42 | "pixelmatch": "^5.2.1", 43 | "pngjs": "^6.0.0", 44 | "react-native-save-view": "^0.2.3" 45 | }, 46 | "devDependencies": { 47 | "@types/formidable": "^1.2.2", 48 | "@types/node": "^18.7.16", 49 | "@types/pixelmatch": "^5.2.3", 50 | "@types/pngjs": "^6.0.0", 51 | "@types/react-native": "^0.64.6", 52 | "@typescript-eslint/eslint-plugin": "^4.25.0", 53 | "@typescript-eslint/parser": "^4.25.0", 54 | "eslint": "^7.27.0", 55 | "eslint-config-airbnb": "^18.2.1", 56 | "eslint-plugin-import": "^2.23.3", 57 | "eslint-plugin-jsx-a11y": "^6.4.1", 58 | "eslint-plugin-react": "^7.23.2", 59 | "eslint-plugin-react-hooks": "^4.2.0", 60 | "react": "17.0.1", 61 | "react-native": "0.64.1", 62 | "react-test-renderer": "17.0.1", 63 | "typescript": "^4.2.4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /res/azureDevops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/res/azureDevops.png -------------------------------------------------------------------------------- /res/testResults.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/res/testResults.png -------------------------------------------------------------------------------- /scripts/create_android_emulator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DEVICE="Nexus 5X" 4 | DEVICE_NAME="Nexus_5X" 5 | SDK="system-images;android-27;default;x86_64" 6 | 7 | # Install AVD files 8 | echo "y" | $ANDROID_HOME/tools/bin/sdkmanager \ 9 | --install $SDK 10 | 11 | # Create emulator 12 | echo "no" | $ANDROID_HOME/tools/bin/avdmanager \ 13 | create avd \ 14 | -n "$DEVICE_NAME" \ 15 | --device "$DEVICE" \ 16 | -k $SDK \ 17 | --force 18 | 19 | $ANDROID_HOME/emulator/emulator -list-avds 20 | 21 | echo "Starting emulator" 22 | 23 | # Start emulator in background 24 | nohup $ANDROID_HOME/emulator/emulator \ 25 | -avd $DEVICE_NAME \ 26 | -no-snapshot > /dev/null 2>&1 & 27 | $ANDROID_HOME/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82' 28 | 29 | $ANDROID_HOME/platform-tools/adb devices 30 | 31 | echo "Emulator started" 32 | -------------------------------------------------------------------------------- /src/client/Snapshot.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | // eslint-disable-next-line no-use-before-define 8 | import React, { Component } from 'react'; 9 | import { InteractionManager, ScrollView } from 'react-native'; 10 | 11 | import log from './utils/log'; 12 | 13 | type Props = { onReady: () => void }; 14 | 15 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOT'; 16 | const ERROR_NO_IMPLEMENTED = 17 | 'Not implemented. Should be implemented by actual snapshot'; 18 | 19 | export default class Snapshot extends Component { 20 | // Should be implemented by actual snapshot 21 | static snapshotName: string = ''; 22 | 23 | componentDidMount(): void { 24 | log.v(TAG, 'Awaiting interaction'); 25 | const startTime = new Date().getTime(); 26 | InteractionManager.runAfterInteractions(() => { 27 | const time = new Date().getTime() - startTime; 28 | log.v(TAG, `Interaction completed in ${time} milliseconds`); 29 | global.setTimeout(() => { 30 | this.props.onReady(); 31 | }, 50); 32 | }); 33 | } 34 | 35 | renderContent(): React.ReactNode { 36 | log.e(TAG, ERROR_NO_IMPLEMENTED); 37 | throw new Error(ERROR_NO_IMPLEMENTED); 38 | } 39 | 40 | render(): React.ReactNode { 41 | const content = this.renderContent(); 42 | return ( 43 | 44 | {content} 45 | 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/client/SnapshotsContainer.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | // eslint-disable-next-line no-use-before-define 8 | import React, { Component } from 'react'; 9 | import { View, Text } from 'react-native'; 10 | // @ts-ignore 11 | import SaveView from 'react-native-save-view'; 12 | 13 | import { getNextSnapshot } from './snapshotsManager'; 14 | import compareToReference from './utils/compareToReference'; 15 | import log from './utils/log'; 16 | import network from './utils/network'; 17 | 18 | import type Snapshot from './Snapshot'; 19 | 20 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOTS_CONTAINER'; 21 | 22 | type NoProps = Record; 23 | 24 | type State = { 25 | isReady: boolean, 26 | ActiveSnapshot: typeof Snapshot | null, 27 | }; 28 | 29 | export default class SnapshotsContainer extends Component { 30 | _viewRef: any; 31 | 32 | _testStartedAt: number = new Date().getTime(); 33 | 34 | _renderStartedAt: number = 0; 35 | 36 | constructor(props: Record) { 37 | super(props); 38 | 39 | this.state = { 40 | ActiveSnapshot: null, 41 | isReady: false, 42 | }; 43 | } 44 | 45 | shouldComponentUpdate( 46 | nextProps: NoProps, 47 | nextState: Readonly, 48 | ): boolean { 49 | return this.state.ActiveSnapshot !== nextState.ActiveSnapshot 50 | || this.state.isReady !== nextState.isReady; 51 | } 52 | 53 | componentDidMount(): void { 54 | requestAnimationFrame(() => { 55 | this._startTesting(); 56 | }); 57 | } 58 | 59 | render(): React.ReactNode { 60 | const { isReady, ActiveSnapshot } = this.state; 61 | 62 | if (!isReady) { 63 | return ( 64 | 65 | Initializing tests 66 | 67 | ); 68 | } 69 | 70 | if (!ActiveSnapshot) { 71 | log.i(TAG, 'No active snapshot'); 72 | return null; 73 | } 74 | 75 | log.i(TAG, `rendering snapshot [${ActiveSnapshot.snapshotName}]`); 76 | 77 | this._renderStartedAt = new Date().getTime(); 78 | 79 | return ; 80 | } 81 | 82 | _startTesting = async (): Promise => { 83 | await network.initTests(); 84 | const ActiveSnapshot = getNextSnapshot(); 85 | if (!ActiveSnapshot) { 86 | this._endOfTest(); 87 | log.e(TAG, 'No snapshots registered'); 88 | this._endOfTest(); 89 | return; 90 | } 91 | log.v(TAG, 'Start testing'); 92 | this.setState({ 93 | ActiveSnapshot, 94 | isReady: true, 95 | }); 96 | }; 97 | 98 | _onRef = (ref: any): void => { 99 | this._viewRef = ref; 100 | }; 101 | 102 | _onSnapshotReady = (): void => { 103 | const renderTime = new Date().getTime() - this._renderStartedAt; 104 | log.v(TAG, 'Snapshot ready'); 105 | 106 | setTimeout(async () => { 107 | const ref = this._viewRef; 108 | 109 | if (!ref) { 110 | const errorMessage = 'Something when wrong, no ref to the component'; 111 | log.e(TAG, errorMessage); 112 | throw new Error(errorMessage); 113 | } 114 | 115 | const { ActiveSnapshot } = this.state; 116 | const name = ActiveSnapshot?.snapshotName; 117 | 118 | log.v(TAG, `snapshotName: [${name || '-'}]`); 119 | 120 | if (!name) { 121 | const errorMessage = 'Snapshot should has a proper name'; 122 | 123 | log.w(TAG, errorMessage); 124 | network.reportTest({ 125 | name: '-', 126 | failure: errorMessage, 127 | time: this._getTestExecutionTime(), 128 | }); 129 | this._nextSnapshot(); 130 | 131 | return; 132 | } 133 | 134 | let failure: any; 135 | 136 | try { 137 | log.v(TAG, '++SaveView.save'); 138 | const base64 = await SaveView.saveToPNGBase64(ref); 139 | log.v(TAG, `--SaveView.save, size is ${base64.length}`); 140 | 141 | failure = await compareToReference(name, base64); 142 | if (failure) { 143 | log.e(TAG, `Snapshot ${name} failed: ${failure}`); 144 | } else { 145 | log.i(TAG, `Snapshot ${name} passed`); 146 | } 147 | } catch (err: unknown) { 148 | failure = `Failed to save view: ${ 149 | err instanceof Error ? err.message : 'Unknown error' 150 | }`; 151 | log.e(TAG, failure); 152 | } 153 | 154 | log.v(TAG, `Reporting [${name}], failure: [${failure}]`); 155 | try { 156 | await network.reportTest({ 157 | name, 158 | failure, 159 | time: this._getTestExecutionTime(), 160 | renderTime, 161 | }); 162 | } catch (err) { 163 | log.e(TAG, 'Failed to report test', err); 164 | } 165 | this._nextSnapshot(); 166 | }, 50); 167 | }; 168 | 169 | _getTestExecutionTime(): number { 170 | const time = new Date().getTime() - this._testStartedAt; 171 | log.v(TAG, `Execution time: ${time}`); 172 | return time; 173 | } 174 | 175 | _nextSnapshot(): void { 176 | log.v(TAG, 'Trying to get next snapshot'); 177 | const NextSnapshot = getNextSnapshot(); 178 | 179 | if (!NextSnapshot) { 180 | log.v('No more snapshots left, exit testing'); 181 | this._endOfTest(); 182 | return; 183 | } 184 | 185 | log.v(`Switching to next snapshot ${NextSnapshot.snapshotName}`); 186 | this._testStartedAt = new Date().getTime(); 187 | this.setState({ ActiveSnapshot: NextSnapshot }); 188 | } 189 | 190 | _endOfTest(): void { 191 | network.endOfTests({ message: 'All tests completed' }); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/client/__tests__/Snapshot.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { View } from 'react-native'; 5 | import renderer from 'react-test-renderer'; 6 | 7 | import Snapshot from '../Snapshot'; 8 | 9 | jest.mock('../utils/log', () => ({ e: () => {}, v: () => {} })); 10 | global.console.error = () => {}; 11 | 12 | describe('Snapshot component', () => { 13 | const onReadyMock = jest.fn(); 14 | 15 | it('throws exception if renderContent is not implemented', () => { 16 | let exception; 17 | 18 | try { 19 | renderer.create(); 20 | } catch (err) { 21 | exception = err; 22 | } 23 | 24 | expect(exception).toMatchSnapshot(); 25 | }); 26 | 27 | it('renders snapshot component', () => { 28 | class SnapshotClass extends Snapshot { 29 | static snapshotName = 'AppSnapshot'; 30 | 31 | renderContent() { 32 | return ( 33 | 34 | ); 35 | } 36 | } 37 | 38 | const tree = renderer.create(); 39 | 40 | expect(tree).toMatchSnapshot(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/client/__tests__/SnapshotsContainer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import SnapshotsContainer from '../SnapshotsContainer'; 7 | import network from '../utils/network'; 8 | import log from '../utils/log'; 9 | import { getNextSnapshot } from '../snapshotsManager'; 10 | 11 | jest.mock('../utils/log', () => ({ 12 | i: jest.fn(), 13 | v: jest.fn(), 14 | w: jest.fn(), 15 | e: jest.fn(), 16 | })); 17 | jest.mock('../utils/network', () => ({ endOfTests: jest.fn() })); 18 | jest.mock('../snapshotsManager', () => ({ getNextSnapshot: jest.fn() })); 19 | 20 | describe('SnapshotsContainer', () => { 21 | beforeEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | it('Renders empty view and reports it to the server if no snapshots are registered', () => { 26 | const tree = renderer.create(); 27 | 28 | expect(tree).toMatchSnapshot(); 29 | expect(network.endOfTests).toHaveBeenCalledTimes(1); 30 | expect(log.e).toMatchSnapshot('loggin error'); 31 | }); 32 | 33 | it('Renders registered snapshot', () => { 34 | getNextSnapshot.mockImplementationOnce(() => 'SomeSnapshot'); 35 | const tree = renderer.create(); 36 | 37 | expect(tree).toMatchSnapshot(); 38 | expect(network.endOfTests).toHaveBeenCalledTimes(0); 39 | expect(log.v).toMatchSnapshot('render snapshot reported'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/Snapshot.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot component renders snapshot component 1`] = ` 4 | 12 | 13 | 14 | 15 | 16 | `; 17 | 18 | exports[`Snapshot component renders snpashot component 1`] = ` 19 | 27 | 28 | 29 | 30 | 31 | `; 32 | 33 | exports[`Snapshot component throws exception if renderContent is not implemented 1`] = `[Error: Not implemented. Should be implemented by actual snapshot]`; 34 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/SnapshotsContainer.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SnapshotsContainer Renders empty view and reports it to the server if no snapshots are registered 1`] = `null`; 4 | 5 | exports[`SnapshotsContainer Renders empty view and reports it to the server if no snapshots are registered: loggin error 1`] = ` 6 | [MockFunction] { 7 | "calls": Array [ 8 | Array [ 9 | "PIXELS_CATCHER::APP::SNAPSHOTS_CONTAINER", 10 | "No snapshots registered", 11 | ], 12 | ], 13 | "results": Array [ 14 | Object { 15 | "type": "return", 16 | "value": undefined, 17 | }, 18 | ], 19 | } 20 | `; 21 | 22 | exports[`SnapshotsContainer Renders registered snapshot 1`] = ` 23 | 26 | `; 27 | 28 | exports[`SnapshotsContainer Renders registered snapshot: render snapshot reported 1`] = `[MockFunction]`; 29 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot component register component and start snapshots: registerComponent 1`] = ` 4 | Array [ 5 | Array [ 6 | "appName", 7 | [Function], 8 | ], 9 | ] 10 | `; 11 | 12 | exports[`Snapshot component register component with custom IP and start snapshots: registerComponent 1`] = ` 13 | Array [ 14 | Array [ 15 | "appName", 16 | [Function], 17 | ], 18 | ] 19 | `; 20 | -------------------------------------------------------------------------------- /src/client/__tests__/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { AppRegistry, View } from 'react-native'; 5 | 6 | import { runSnapshots, Snapshot, registerSnapshot } from '../index'; 7 | import network from '../utils/network'; 8 | 9 | jest.mock('react-native/Libraries/ReactNative/AppRegistry', () => ({ registerComponent: jest.fn() })); 10 | jest.mock('../utils/network', () => ({ setBaseUrl: jest.fn() })); 11 | jest.mock('../SnapshotsContainer', () => 'SnapshotsContainer'); 12 | jest.mock('../utils/log', () => ({ i: jest.fn() })); 13 | jest.mock('../Snapshot', () => ({ default: 'Snapshot' })); 14 | jest.mock('../snapshotsManager', () => ({ registerSnapshot: jest.fn() })); 15 | 16 | describe('Snapshot component', () => { 17 | const appName = 'appName'; 18 | 19 | afterEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | it('provides Snapshot component', () => { 24 | expect(Snapshot).toBe('Snapshot'); 25 | }); 26 | 27 | it('allows to register snapshot', () => { 28 | registerSnapshot(); 29 | expect(registerSnapshot).toHaveBeenCalledTimes(1); 30 | }); 31 | 32 | it('register component and start snapshots', () => { 33 | runSnapshots(appName); 34 | 35 | expect(AppRegistry.registerComponent).toHaveBeenCalledTimes(1); 36 | expect(AppRegistry.registerComponent.mock.calls) 37 | .toMatchSnapshot('registerComponent'); 38 | expect(AppRegistry.registerComponent.mock.calls[0][1]()) 39 | .toBe('SnapshotsContainer'); 40 | expect(network.setBaseUrl).toHaveBeenCalledTimes(0); 41 | }); 42 | 43 | it('register component with custom IP and start snapshots', () => { 44 | const baseUrl = 'baseUrl'; 45 | runSnapshots(appName, { baseUrl }); 46 | 47 | expect(AppRegistry.registerComponent).toHaveBeenCalledTimes(1); 48 | expect(AppRegistry.registerComponent.mock.calls) 49 | .toMatchSnapshot('registerComponent'); 50 | expect(AppRegistry.registerComponent.mock.calls[0][1]()) 51 | .toBe('SnapshotsContainer'); 52 | expect(network.setBaseUrl).toHaveBeenCalledTimes(1); 53 | expect(network.setBaseUrl).toHaveBeenCalledWith(baseUrl); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/client/__tests__/snapshotsManager.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { View, Text } from 'react-native'; 5 | 6 | import { registerSnapshot, getNextSnapshot } from '../snapshotsManager'; 7 | import Snapshot from '../Snapshot'; 8 | 9 | jest.mock('../utils/log', () => ({ i: jest.fn() })); 10 | 11 | describe('snapshotsManager', () => { 12 | it('Default snapshots list is empty', () => { 13 | const nextSnapshot = getNextSnapshot(); 14 | expect(nextSnapshot).toBe(undefined); 15 | }); 16 | 17 | it('register snapshot and get it', () => { 18 | class SnapshotClass extends Snapshot { 19 | static snapshotName = 'someComponent'; 20 | 21 | renderContent() { 22 | return ( 23 | Some component 24 | ); 25 | } 26 | } 27 | registerSnapshot(SnapshotClass); 28 | 29 | expect(getNextSnapshot()).toBe(SnapshotClass); 30 | expect(getNextSnapshot()).toBe(undefined); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type React from 'react'; 8 | import { AppRegistry } from 'react-native'; 9 | 10 | import log from './utils/log'; 11 | import network from './utils/network'; 12 | 13 | import SnapshotsContainer from './SnapshotsContainer'; 14 | 15 | export const Snapshot = require('./Snapshot').default; 16 | 17 | export const { registerSnapshot } = require('./snapshotsManager'); 18 | 19 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOT'; 20 | 21 | export type GetRootElementType = (element: React.ComponentType) => 22 | React.ComponentType 23 | 24 | interface ConfigType { 25 | baseUrl?: string; 26 | 27 | /** 28 | * Callback to override AppRegistry.registerComponent with custom 29 | * implementation. Can be used for projects with react-native-navigation 30 | * @param snapshot Current snapshot 31 | */ 32 | // eslint-disable-next-line no-unused-vars 33 | registerComponent?: (snapshot: typeof SnapshotsContainer) => void; 34 | 35 | /** 36 | * Root element. Allows to wrap the SnapshotsContainer, which could be 37 | * useful to implement some providers, for example for react navigation. 38 | * Example: 39 | * 40 | * import { NavigationContainer } from '@react-navigation/native'; 41 | * import { createStackNavigator } from '@react-navigation/stack'; 42 | * 43 | * const Stack = createStackNavigator(); 44 | * 45 | * function getRootElement(SnapshotsContainer) { 46 | * const RootElement = ({children}) => ( 47 | * 48 | * 49 | * 53 | * 54 | * 55 | * ) 56 | * return RootElement; 57 | * } 58 | * 59 | * runSnapshots(appName, { baseUrl, getRootElement }); 60 | */ 61 | getRootElement?: GetRootElementType; 62 | } 63 | 64 | export const runSnapshots = (appName: string, config: ConfigType = {}): void => { 65 | log.i(TAG, `Run snapshots for ${appName}`); 66 | log.i(TAG, `Config is:\n ${JSON.stringify(config, null, 2)}`); 67 | 68 | if (config.baseUrl) { 69 | network.setBaseUrl(config.baseUrl); 70 | } 71 | 72 | if (config.registerComponent) { 73 | config.registerComponent(SnapshotsContainer); 74 | return; 75 | } 76 | 77 | if (config.getRootElement) { 78 | const RootElement = config.getRootElement(SnapshotsContainer); 79 | AppRegistry.registerComponent(appName, () => RootElement); 80 | return; 81 | } 82 | 83 | AppRegistry.registerComponent(appName, () => SnapshotsContainer); 84 | }; 85 | -------------------------------------------------------------------------------- /src/client/snapshotsManager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type Snapshot from './Snapshot'; 8 | import log from './utils/log'; 9 | import network from './utils/network'; 10 | 11 | const snapshots: Array = []; 12 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOTS_MANAGER'; 13 | 14 | 15 | export function registerSnapshot(Component: typeof Snapshot): void { 16 | log.i(TAG, `Registering snapshot [${Component.snapshotName}]`); 17 | snapshots.push(Component); 18 | network.registerTest(Component.snapshotName); 19 | } 20 | 21 | 22 | export function getNextSnapshot(): typeof Snapshot | undefined { 23 | const NextSnapshot = snapshots.shift(); 24 | return NextSnapshot; 25 | } 26 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es6", 5 | "declaration": true, 6 | "outDir": "../../lib/client", 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true 11 | }, 12 | "include": ["./**/*"], 13 | "exclude": [ 14 | "../../node_modules", 15 | "**/__tests__/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/client/utils/__tests__/__snapshots__/compareToReference.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`compareToReference Returns failure if HTTP status is not 200 1`] = `"Invalid status 404"`; 4 | 5 | exports[`compareToReference Returns failure if result is not OK 1`] = `"Files mismatch with 1 pixels"`; 6 | -------------------------------------------------------------------------------- /src/client/utils/__tests__/compareToReference.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import network from '../network'; 3 | import compareToReference from '../compareToReference'; 4 | 5 | jest.mock('../network', () => ({ postBase64: jest.fn() })); 6 | 7 | describe('compareToReference', () => { 8 | it('Returns failure if HTTP status is not 200', async () => { 9 | const snapshotName = 'snapshotName'; 10 | const base64 = 'base64 data'; 11 | 12 | network.postBase64.mockImplementationOnce(() => ({ status: 404 })); 13 | 14 | const failure = await compareToReference(snapshotName, base64); 15 | expect(failure).toMatchSnapshot(); 16 | }); 17 | 18 | it('Returns failure if result is not OK', async () => { 19 | const snapshotName = 'snapshotName'; 20 | const base64 = 'base64 data'; 21 | 22 | network.postBase64.mockImplementationOnce(() => ({ 23 | status: 200, 24 | json: async () => ({ 25 | result: 'ERROR', 26 | info: 'Files mismatch with 1 pixels', 27 | }), 28 | })); 29 | 30 | const failure = await compareToReference(snapshotName, base64); 31 | expect(failure).toMatchSnapshot(); 32 | }); 33 | 34 | it('Returns nothing if image matches the reference', async () => { 35 | const snapshotName = 'snapshotName'; 36 | const base64 = 'base64 data'; 37 | 38 | network.postBase64.mockImplementationOnce(() => ({ 39 | status: 200, 40 | json: async () => ({ 41 | result: 'OK', 42 | info: { differentPixelsCount: 0 }, 43 | }), 44 | })); 45 | 46 | const result = await compareToReference(snapshotName, base64); 47 | expect(result).toBe(undefined); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/client/utils/compareToReference.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import network from './network'; 8 | 9 | const compareToReference = async (snapshotName: string, base64: string): Promise => { 10 | const response: any = await network.postBase64({ 11 | base64, 12 | fileName: `${snapshotName}.png`, 13 | }); 14 | 15 | if (response.status !== 200) { 16 | return `Invalid status ${response.status}`; 17 | } 18 | 19 | const responseJSON = await response.json(); 20 | 21 | if (responseJSON.result !== 'OK') { 22 | return responseJSON.info; 23 | } 24 | 25 | return undefined; 26 | }; 27 | 28 | export default compareToReference; 29 | -------------------------------------------------------------------------------- /src/client/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import network from './network'; 8 | 9 | const consoleLog = global.console && global.console.log 10 | ? global.console.log : (): void => {}; 11 | 12 | type LogLevelType = 'v' | 'd' | 'i' | 'w' | 'e'; 13 | 14 | const serverLog = async (logLevel: LogLevelType, tag: string, ...args: any): Promise => { 15 | try { 16 | network.serverLog({ 17 | logLevel, 18 | tag, 19 | args, 20 | }); 21 | } catch (err) { 22 | if (__DEV__) { 23 | // eslint-disable-next-line no-console 24 | console.warn('ERROR:serverLog: ', err instanceof Error ? err.message : 'Unknown error', err); 25 | } 26 | } 27 | }; 28 | 29 | const log = { 30 | v: (tag: string, ...args: any): void => { 31 | consoleLog(tag, ...args); 32 | serverLog('v', tag, ...args); 33 | }, 34 | 35 | d: (tag: string, ...args: any): void => { 36 | consoleLog(tag, ...args); 37 | serverLog('d', tag, ...args); 38 | }, 39 | 40 | i: (tag: string, ...args: any): void => { 41 | consoleLog(tag, ...args); 42 | serverLog('i', tag, ...args); 43 | }, 44 | 45 | w: (tag: string, ...args: any): void => { 46 | consoleLog(`${tag} WARNING:`, ...args); 47 | serverLog('w', `${tag} WARNING:`, ...args); 48 | }, 49 | 50 | e: (tag: string, ...args: any): void => { 51 | consoleLog(`${tag} ERROR:`, ...args); 52 | serverLog('e', `${tag} ERROR:`, ...args); 53 | }, 54 | }; 55 | 56 | export default log; 57 | -------------------------------------------------------------------------------- /src/client/utils/network.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import { Platform } from 'react-native'; 8 | 9 | let baseUrl = Platform.select({ 10 | android: 'http://10.0.2.2:3000', 11 | ios: 'http://127.0.0.1:3000', 12 | }); 13 | 14 | type TestCaseType = { 15 | name: string, 16 | failure?: string, 17 | isSkipped?: boolean, 18 | time: number, 19 | renderTime?: number, 20 | }; 21 | 22 | const fetchRequest = async (url: string, body: Object): Promise => { 23 | const response = await fetch(url, { 24 | method: 'POST', 25 | headers: { 26 | Accept: 'application/json', 27 | 'Content-Type': 'application/json', 28 | }, 29 | body: JSON.stringify(body), 30 | }); 31 | 32 | return response; 33 | }; 34 | 35 | 36 | export default { 37 | 38 | setBaseUrl(url: string): void { 39 | baseUrl = url; 40 | }, 41 | 42 | 43 | initTests: async (): Promise => { 44 | await fetchRequest(`${baseUrl}/initTests`, {}); 45 | }, 46 | 47 | 48 | registerTest: async (name: string): Promise => { 49 | await fetchRequest(`${baseUrl}/registerTest`, { name }); 50 | }, 51 | 52 | 53 | postBase64: async (body: Object): Promise => { 54 | const response = await fetchRequest(`${baseUrl}/base64`, body); 55 | return response; 56 | }, 57 | 58 | 59 | serverLog: async (body: Object): Promise => { 60 | await fetchRequest(`${baseUrl}/log`, body); 61 | }, 62 | 63 | 64 | reportTest: async (testCase: TestCaseType): Promise => { 65 | await fetchRequest(`${baseUrl}/reportTest`, testCase); 66 | }, 67 | 68 | 69 | endOfTests: async (body: Object): Promise => { 70 | await fetchRequest(`${baseUrl}/endOfTests`, body); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/runner/TestsRunner.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import log from './utils/log'; 5 | import Reporter from './utils/Reporter'; 6 | import server from './server/server'; 7 | 8 | const TAG = 'PIXELS_CATCHER'; 9 | 10 | type TestsRunnerParamsType = 11 | { 12 | activityName: string, 13 | appFile: string, 14 | device: any, 15 | deviceName: string, 16 | deviceParams: string, 17 | isDevMode: boolean, 18 | locale: string, 19 | packageName: string, 20 | platform: 'ios' | 'android', 21 | port: number, 22 | snapshotsPath: string, 23 | testRunName: string, 24 | timeout: number, 25 | }; 26 | 27 | class TestsRunner { 28 | _activityName: string; 29 | 30 | _appFile: string; 31 | 32 | _appFileFullPath: string | void; 33 | 34 | _device: any; 35 | 36 | _deviceName: string; 37 | 38 | _deviceParams: Object; 39 | 40 | _isDevMode: boolean; 41 | 42 | _locale: string; 43 | 44 | _packageName: string; 45 | 46 | _platform: 'ios' | 'android'; 47 | 48 | _port: number; 49 | 50 | _reporter: Reporter; 51 | 52 | _snapshotsPath: string; 53 | 54 | _stopByTimeoutID: ReturnType | void; 55 | 56 | _timeout: number; 57 | 58 | 59 | constructor(params: TestsRunnerParamsType) { 60 | this._activityName = params.activityName; 61 | this._appFile = params.appFile; 62 | this._device = params.device; 63 | this._deviceName = params.deviceName; 64 | this._deviceParams = params.deviceParams; 65 | this._isDevMode = params.isDevMode; 66 | this._locale = params.locale; 67 | this._packageName = params.packageName; 68 | this._platform = params.platform; 69 | this._port = params.port; 70 | this._snapshotsPath = params.snapshotsPath; 71 | this._timeout = params.timeout; 72 | 73 | if (!this._isDevMode) { 74 | if (!this._appFile) { 75 | log.e(TAG, 'Valid ap file is required, check config'); 76 | process.exit(-1); 77 | } 78 | 79 | this._appFileFullPath = path.isAbsolute(this._appFile) 80 | ? this._appFile : path.join(process.cwd(), this._appFile); 81 | 82 | if (!fs.existsSync(this._appFileFullPath)) { 83 | log.e(TAG, `Valid app file is required, cannot find [${this._appFile}] file`); 84 | process.exit(-1); 85 | } 86 | } 87 | 88 | this._reporter = new Reporter(params.testRunName, this._snapshotsPath); 89 | } 90 | 91 | 92 | _testingCompleted = async (isPassed: boolean = false): Promise => { 93 | if (this._stopByTimeoutID) { 94 | clearTimeout(this._stopByTimeoutID); 95 | } 96 | if (!this._isDevMode) { 97 | log.i(TAG, 'Stopping the server and emulator'); 98 | server.stop(); 99 | await this._device.stop(); 100 | log.i(TAG, 'Server and emulator are stopped'); 101 | 102 | if (!isPassed) { 103 | log.i(TAG, 'Some tests failed, exit with error'); 104 | process.exit(-1); 105 | } else { 106 | log.i(TAG, 'No errors found'); 107 | } 108 | } 109 | }; 110 | 111 | 112 | _onTestingCompleted = async (byTimeOut: boolean = false): Promise => { 113 | const jUnitFile = path.join(process.cwd(), 'junit.xml'); 114 | const deviceLogsFile = path.join( 115 | process.cwd(), 116 | `${this._platform}_logs.log`, 117 | ); 118 | await this._reporter.toLog(); 119 | this._reporter.tojUnit(jUnitFile); 120 | this._reporter.deviceLogsToFile(deviceLogsFile); 121 | this._testingCompleted(byTimeOut ? false : this._reporter.isPassed()); 122 | }; 123 | 124 | 125 | _onAppActivity = (): void => { 126 | this._stopByTimeout(); 127 | } 128 | 129 | 130 | _stopByTimeout = (): void => { 131 | if (this._stopByTimeoutID) { 132 | clearTimeout(this._stopByTimeoutID); 133 | } 134 | this._stopByTimeoutID = setTimeout(() => { 135 | log.e(TAG, 'Stop tests by timeout'); 136 | this._onTestingCompleted(true); 137 | }, this._timeout); 138 | }; 139 | 140 | 141 | async _startAndroid(): Promise { 142 | log.d(TAG, `Start emulator [${this._deviceName}]`); 143 | try { 144 | await this._device.start(this._deviceParams); 145 | } catch (err) { 146 | process.exit(-1); 147 | } 148 | log.d(TAG, 'Emulator started'); 149 | 150 | log.d(TAG, 'Installing APK'); 151 | await this._device.installApp(this._packageName, this._appFileFullPath); 152 | log.d(TAG, 'APK installed'); 153 | 154 | log.d(TAG, 'Starting application'); 155 | if (this._locale) { 156 | log.w(TAG, `[${this._locale} is ignored for android]`); 157 | } 158 | await this._device.startApp(this._packageName, this._activityName); 159 | log.d(TAG, 'Application started'); 160 | 161 | this._stopByTimeout(); 162 | } 163 | 164 | 165 | async _startIOS(): Promise { 166 | log.d(TAG, `Start emulator [${this._deviceName}]`); 167 | try { 168 | await this._device.start(this._deviceParams); 169 | } catch (err) { 170 | log.e(TAG, `Failed to start device: [${err instanceof Error ? err.message : 'Unknown error'}]`); 171 | process.exit(-1); 172 | } 173 | log.d(TAG, 'Emulator started'); 174 | 175 | log.d(TAG, 'Installing APP'); 176 | await this._device.installApp(this._packageName, this._appFileFullPath); 177 | log.d(TAG, 'APP installed'); 178 | 179 | log.d(TAG, 'Starting application'); 180 | await this._device.startApp(this._packageName, this._activityName, this._locale); 181 | log.d(TAG, 'Application started'); 182 | } 183 | 184 | 185 | async start(): Promise { 186 | log.d(TAG, 'Starting server'); 187 | server.start( 188 | this._reporter, 189 | this._onTestingCompleted, 190 | this._snapshotsPath, 191 | this._onAppActivity, 192 | this._port, 193 | ); 194 | 195 | if (this._isDevMode) { 196 | log.d(TAG, 'Only server is used in DEV mode. Waiting for tests'); 197 | return; 198 | } 199 | 200 | if (this._platform === 'ios') { 201 | this._startIOS(); 202 | } else { 203 | this._startAndroid(); 204 | } 205 | 206 | this._reporter.collectDeviceLogs(this._platform, this._packageName); 207 | } 208 | } 209 | 210 | 211 | export default TestsRunner; 212 | -------------------------------------------------------------------------------- /src/runner/azure/AzurePublisher.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as https from 'https'; 3 | import * as path from 'path'; 4 | 5 | import log from '../utils/log'; 6 | import type { TestcaseType } from '../utils/Reporter'; 7 | 8 | const TAG = 'PIXELS_CATCHER::AZURE_PUBLISHER'; 9 | 10 | const processEnv: any = process.env; 11 | const { 12 | BUILD_BUILDURI, 13 | SYSTEM_ACCESSTOKEN, 14 | SYSTEM_TEAMFOUNDATIONCOLLECTIONURI, 15 | SYSTEM_TEAMPROJECT, 16 | } = processEnv; 17 | 18 | const DEFAULT_OPTIONS = { 19 | hostname: 'dev.azure.com', 20 | port: 443, 21 | }; 22 | 23 | const DEFAULT_HEADERS = { 24 | 'Content-Type': 'application/json; charset=utf-8', 25 | 'X-TFS-FedAuthRedirect': 'Suppress', 26 | Accept: 'application/json', 27 | Authorization: `Basic ${Buffer.from(`:${SYSTEM_ACCESSTOKEN}`).toString('base64')}`, 28 | }; 29 | 30 | type ImageType = 'refImages' | 'uploads' | 'diffs'; 31 | 32 | const uploadImageSuffix = { 33 | diffs: 'Diff', 34 | refImages: 'Reference', 35 | uploads: 'Actual', 36 | }; 37 | 38 | const imageTypes = Object.keys(uploadImageSuffix); 39 | 40 | function base64Encode(file: string): string { 41 | if (!fs.existsSync(file)) { 42 | return ''; 43 | } 44 | return Buffer.from(fs.readFileSync(file)).toString('base64'); 45 | } 46 | 47 | class AzurePublisher { 48 | _workingDir: string; 49 | 50 | _testRunName: string; 51 | 52 | _urlBasePath: string; 53 | 54 | constructor(workingDir: string, testRunName: string) { 55 | this._workingDir = workingDir; 56 | this._testRunName = testRunName; 57 | const organization = SYSTEM_TEAMFOUNDATIONCOLLECTIONURI 58 | .split('/') 59 | .filter((str: string) => Boolean(str)) 60 | .reduce((acc: string, curr: string) => curr); 61 | this._urlBasePath = `/${organization}/${SYSTEM_TEAMPROJECT}/_apis/test`; 62 | } 63 | 64 | async publish(): Promise { 65 | try { 66 | const buildRunId = await this._getBuildRunId(BUILD_BUILDURI); 67 | log.i(TAG, `buildRunId [${buildRunId}]`); 68 | const failedTests = await this._getTestFailures(buildRunId); 69 | log.i(TAG, `failedTests count [${failedTests.length}]`); 70 | failedTests.forEach(async (test: any) => { 71 | log.v(TAG, `Uploading results for test [${test.testCaseTitle}] from [${test.automatedTestStorage}]`); 72 | let type: any; 73 | for (let ind = 0; ind < imageTypes.length; ++ind) { 74 | type = imageTypes[ind]; 75 | await this._uploadScreenshot( 76 | buildRunId, 77 | test.id, 78 | test.testCaseTitle, 79 | test.automatedTestStorage, 80 | type, 81 | ); 82 | } 83 | }); 84 | } catch (err) { 85 | log.e(TAG, `Failed to upload results: ${err instanceof Error ? err.message : 'Unknown error'}`); 86 | process.exit(-1); 87 | } 88 | } 89 | 90 | async _getBuildRunId(_buildUri: string): Promise { 91 | const data: any = await this._httpsRequest({ 92 | method: 'GET', 93 | path: `${this._urlBasePath}/runs?api-version=5.1&buildUri=${_buildUri}`, 94 | }); 95 | if (!data || !data.value || data.value.length === 0) { 96 | throw new Error('Failed to get build run, no data'); 97 | } 98 | let id; 99 | log.v(TAG, `Runs count: ${data.value.length}, searching for [${this._testRunName}]`); 100 | for (let ind = data.value.length - 1; ind >= 0; --ind) { 101 | log.v(TAG, `Name: [${data.value[ind].name}], id: ${data.value[ind].id}`); 102 | if (data.value[ind].name === this._testRunName) { 103 | log.v(TAG, `Id ${data.value[ind].id} found`); 104 | id = data.value[ind].id; 105 | break; 106 | } 107 | } 108 | 109 | if (id === undefined) { 110 | throw new Error(`Failed to get build run id for ${this._testRunName}`); 111 | } 112 | 113 | return id; 114 | } 115 | 116 | async _getTestFailures(runId: string): Promise> { 117 | const data: any = await this._httpsRequest({ 118 | method: 'GET', 119 | path: `${this._urlBasePath}/Runs/${runId}/results?outcomes=3&api-version=5.1&outcomes=3`, 120 | }); 121 | 122 | return data.value; 123 | } 124 | 125 | async _upload( 126 | buildRunId: string, 127 | id: string, 128 | fileToUpload: string, 129 | fileNameToShow: string, 130 | ): Promise { 131 | const postData = { 132 | stream: base64Encode(fileToUpload), 133 | fileName: fileNameToShow, 134 | comment: 'Diff uploaded by REST from pipeline', 135 | attachmentType: 'GeneralAttachment', 136 | }; 137 | 138 | const data: any = await this._httpsRequest({ 139 | method: 'POST', 140 | path: `${this._urlBasePath}/Runs/${buildRunId}/Results/${id}/attachments?api-version=5.1-preview.1`, 141 | }, postData); 142 | 143 | return data.value; 144 | } 145 | 146 | async _uploadScreenshot( 147 | buildRunId: string, 148 | id: string, 149 | testCaseTitle: string, 150 | className: string, 151 | type: ImageType, 152 | ): Promise { 153 | const suffix = uploadImageSuffix[type]; 154 | log.v(TAG, `Uploading ${suffix}`); 155 | await this._upload( 156 | buildRunId, 157 | id, 158 | path.join(this._workingDir, className, type, `${testCaseTitle}.png`), 159 | `${testCaseTitle}${suffix}.png`, 160 | ); 161 | log.v(TAG, `${suffix} uploaded`); 162 | } 163 | 164 | async _httpsRequest(options: any, postData: any = undefined): Promise { 165 | let _options = { 166 | ...DEFAULT_OPTIONS, 167 | ...options, 168 | headers: { 169 | ...DEFAULT_HEADERS, 170 | ...(options.headers ? options.headers : {}), 171 | }, 172 | }; 173 | const _postData = postData ? JSON.stringify(postData) : undefined; 174 | 175 | return new Promise((resolve: Function, reject: Function) => { 176 | if (_postData) { 177 | _options = { 178 | ..._options, 179 | 'Content-Length': Buffer.byteLength(_postData), 180 | }; 181 | } 182 | const req = https.request(_options, (resp: any) => { 183 | if (resp.statusCode >= 300) { 184 | log.e(TAG, `Failed to ${_options.method} [${_options.path}]`); 185 | reject(new Error(`Status code: ${resp.statusCode}, statusMessage: ${resp.statusMessage}`)); 186 | return; 187 | } 188 | 189 | let data = ''; 190 | 191 | resp.on('data', (chunk: string) => { 192 | data += chunk; 193 | }); 194 | 195 | resp.on('end', () => { 196 | resolve(JSON.parse(data)); 197 | }); 198 | }).on('error', (err: Error) => { 199 | reject(new Error(`Error: ${err.message}`)); 200 | }); 201 | 202 | if (_postData) { 203 | req.write(_postData); 204 | } 205 | 206 | req.end(); 207 | }); 208 | } 209 | } 210 | 211 | export default AzurePublisher; 212 | -------------------------------------------------------------------------------- /src/runner/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Maksym Rusynyk 2018 - present 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | import type { DeviceInterface } from './utils/device/DeviceInterface'; 9 | 10 | import log from './utils/log'; 11 | import readConfig from './utils/readConfig'; 12 | import getDevice from './utils/device/deviceProvider'; 13 | import AzurePublisher from './azure/AzurePublisher'; 14 | import TestsRunner from './TestsRunner'; 15 | 16 | const TAG = 'PIXELS_CATCHER'; 17 | const AZURE_PUBLISH_ACTION = 'azureAttachments'; 18 | const [,, platform, configuration, action] = process.argv; 19 | 20 | if (!platform || !(platform === 'ios' || platform === 'android')) { 21 | log.e(TAG, `Valid platform is required, specify "ios" or "android". Example: 22 | 23 | $ pixels-catcher android debug 24 | 25 | or 26 | 27 | $ pixels-catcher ios debug 28 | `); 29 | process.exit(-1); 30 | } 31 | 32 | if (!configuration) { 33 | log.e(TAG, `Configuration is required. Example: 34 | 35 | $ pixels-catcher android debug 36 | 37 | or 38 | 39 | $ pixels-catcher ios debug 40 | `); 41 | process.exit(-1); 42 | } 43 | 44 | if (action !== undefined && action !== AZURE_PUBLISH_ACTION) { 45 | log.e(TAG, `Only "${AZURE_PUBLISH_ACTION}" is available. Example: 46 | 47 | $ pixels-catcher android debug ${AZURE_PUBLISH_ACTION} 48 | 49 | or 50 | 51 | $ pixels-catcher ios debug ${AZURE_PUBLISH_ACTION} 52 | `); 53 | process.exit(-1); 54 | } 55 | 56 | 57 | const fullConfig = readConfig(); 58 | const config = fullConfig[platform]; 59 | 60 | if (!config) { 61 | log.e(TAG, `Cannot find configuration for plarform [${platform}] in ` 62 | + `config:\n ${JSON.stringify(fullConfig, null, 2)}`); 63 | process.exit(-1); 64 | } 65 | 66 | log.setLevel(fullConfig.logLevel); 67 | log.i(TAG, `Starting with [${configuration}] configuration for [${platform}]`); 68 | log.v(TAG, `Config\n${JSON.stringify(config, null, 2)}`); 69 | 70 | const getParamFromConfig = (paramName: string): any => { 71 | const value = (config[configuration] || {})[paramName]; 72 | return value !== undefined ? value : config[paramName]; 73 | }; 74 | 75 | const activityName = getParamFromConfig('activityName') || 'MainActivity'; 76 | const appFile = getParamFromConfig('appFile'); 77 | const canStopDevice = getParamFromConfig('canStopDevice'); 78 | const deviceName = getParamFromConfig('deviceName'); 79 | const deviceParams = getParamFromConfig('deviceParams'); 80 | const isPhysicalDevice = getParamFromConfig('physicalDevice'); 81 | const packageName = getParamFromConfig('packageName'); 82 | const snapshotsPath = getParamFromConfig('snapshotsPath'); 83 | const port = getParamFromConfig('port'); 84 | const locale = getParamFromConfig('locale'); 85 | const timeout = fullConfig.timeout || 25 * 1000; // 25 sec is default 86 | 87 | if (!deviceName) { 88 | log.e(TAG, 'Valid device name is required, check "PixelsCatcher.deviceName" ' 89 | + 'property in package.json'); 90 | process.exit(-1); 91 | } 92 | 93 | const device: DeviceInterface = getDevice( 94 | deviceName, 95 | platform, 96 | isPhysicalDevice, 97 | canStopDevice, 98 | ); 99 | 100 | log.i(TAG, `Starting with: 101 | - activityName: [${activityName}] 102 | - appFile: [${appFile}] 103 | - deviceName: [${deviceName}] 104 | - deviceParams: [${deviceParams}] 105 | - packageName: [${packageName}] 106 | - snapshotsPath: [${snapshotsPath}] 107 | - canStopDevice: [${canStopDevice}] 108 | - port: [${port}] 109 | - locale: [${locale}]`); 110 | 111 | if (!packageName) { 112 | log.e(TAG, 'Package name is required'); 113 | process.exit(-1); 114 | } 115 | 116 | const testRunName = `UI tests for ${platform}/${deviceName}`; 117 | 118 | if (action === AZURE_PUBLISH_ACTION) { 119 | const azurePublisher = new AzurePublisher(process.cwd(), testRunName); 120 | azurePublisher.publish(); 121 | } else { 122 | const isDevMode = !appFile; 123 | log.i(TAG, `Starting in ${isDevMode ? 'development' : 'ci'} mode`); 124 | const testsRunner = new TestsRunner({ 125 | testRunName, 126 | isDevMode, 127 | timeout, 128 | device, 129 | appFile, 130 | port, 131 | platform, 132 | deviceName, 133 | snapshotsPath, 134 | deviceParams, 135 | packageName, 136 | locale, 137 | activityName, 138 | }); 139 | testsRunner.start(); 140 | } 141 | -------------------------------------------------------------------------------- /src/runner/server/compareImages.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as fs from 'fs'; 8 | import { PNG } from 'pngjs'; 9 | import * as pixelmatch from 'pixelmatch'; 10 | 11 | export default (actual: any, expected: any, diffFile: any): number => { 12 | if (!actual || !fs.existsSync(actual)) { 13 | throw new Error(`Actual file is required, cannot get [${actual}] file`); 14 | } 15 | if (!expected || !fs.existsSync(expected)) { 16 | throw new Error(`Expected file is required, cannot get [${expected}] file`); 17 | } 18 | 19 | const imageActual = PNG.sync.read(fs.readFileSync(actual)); 20 | const imageExpected = PNG.sync.read(fs.readFileSync(expected)); 21 | 22 | if (imageActual.width !== imageExpected.width) { 23 | throw new Error(`Width mismatch: expected ${imageExpected.width}, actual: ${imageActual.width}`); 24 | } 25 | 26 | if (imageActual.height !== imageExpected.height) { 27 | throw new Error(`Height mismatch: expected ${imageExpected.height}, actual: ${imageActual.height}`); 28 | } 29 | 30 | const diff = new PNG({ width: imageExpected.width, height: imageExpected.height }); 31 | 32 | const differentPixelsCount = pixelmatch( 33 | imageActual.data, 34 | imageExpected.data, 35 | diff.data, 36 | imageActual.width, 37 | imageActual.height, 38 | { threshold: 0.1 }, 39 | ); 40 | 41 | if (diffFile) { 42 | diff.pack().pipe(fs.createWriteStream(diffFile)); 43 | } 44 | 45 | return differentPixelsCount; 46 | }; 47 | -------------------------------------------------------------------------------- /src/runner/server/dummy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/src/runner/server/dummy.png -------------------------------------------------------------------------------- /src/runner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es5", 5 | "declaration": true, 6 | "outDir": "../../lib/runner", 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true 11 | }, 12 | "include": ["./**/*"], 13 | "exclude": [ 14 | "../../node_modules", 15 | "**/__tests__/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/runner/utils/Reporter.ts: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as fs from 'fs'; 3 | 4 | import timeToSec from './timeToSec'; 5 | import exec from './exec'; 6 | import delay from './delay'; 7 | 8 | const { spawn } = require('child_process'); 9 | 10 | export type TestcaseType = { 11 | failure: string | void, 12 | isSkipped: boolean | void, 13 | name: string, 14 | renderTime?: number, 15 | time: number, 16 | }; 17 | 18 | const timeReducer = (time: number, testcase: TestcaseType): number => time + testcase.time; 19 | 20 | const filterSkipped = (testcase: TestcaseType): boolean => !testcase.isSkipped; 21 | 22 | const filterFailed = (testcase: TestcaseType): boolean => !testcase.failure; 23 | 24 | class TestReporter { 25 | _name: string; 26 | 27 | _className: string; 28 | 29 | _tests: Array = []; 30 | 31 | _deviceLogs: Array = []; 32 | 33 | _stopDeviceLogger: Function | undefined = undefined; 34 | 35 | _minRenderTime = { 36 | name: '-', 37 | time: Number.MAX_VALUE, 38 | }; 39 | 40 | _maxRenderTime = { 41 | name: '-', 42 | time: Number.MIN_VALUE, 43 | }; 44 | 45 | constructor(name: string, className: string) { 46 | this._name = name; 47 | this._className = className; 48 | } 49 | 50 | registerTest(name: string): void { 51 | this._tests.push({ 52 | failure: undefined, 53 | isSkipped: true, 54 | name, 55 | renderTime: 0, 56 | time: 0, 57 | }); 58 | } 59 | 60 | _updateTestResult(testCase: TestcaseType): void { 61 | const ind = this._tests.findIndex((test) => test.name === testCase.name); 62 | 63 | if (ind >= 0) { 64 | this._tests[ind] = testCase; 65 | } else { 66 | this._tests.push(testCase); 67 | } 68 | } 69 | 70 | reportTest(testCase: TestcaseType): void { 71 | this._updateTestResult(testCase); 72 | 73 | if (testCase.renderTime === undefined) { 74 | return; 75 | } 76 | if (testCase.renderTime < this._minRenderTime.time) { 77 | this._minRenderTime.time = testCase.renderTime; 78 | this._minRenderTime.name = testCase.name; 79 | } 80 | if (testCase.renderTime > this._maxRenderTime.time) { 81 | this._maxRenderTime.time = testCase.renderTime; 82 | this._maxRenderTime.name = testCase.name; 83 | } 84 | } 85 | 86 | isPassed(): boolean { 87 | return this._getFailedTests().length === 0; 88 | } 89 | 90 | async toLog(): Promise { 91 | global.console.log(''); 92 | global.console.log('==> All tests completed: <=='); 93 | 94 | const failedTests = this._getFailedTests(); 95 | const passedTests = this._getPassedTests(); 96 | const skippedTests = this._getSkippedTests(); 97 | const reportTable: any = []; 98 | 99 | this._tests.forEach((testcase: TestcaseType) => { 100 | let status = 'PASSED'; 101 | 102 | if (testcase.failure) { 103 | status = 'FAILED'; 104 | } else if (testcase.isSkipped) { 105 | status = 'SKIPPED'; 106 | } 107 | 108 | reportTable.push({ 109 | name: testcase.name, 110 | status, 111 | time: timeToSec(testcase.time), 112 | renderTime: testcase.renderTime !== undefined ? timeToSec(testcase.renderTime) : '-', 113 | failure: testcase.failure || '-', 114 | }); 115 | }); 116 | 117 | global.console.table(reportTable); 118 | 119 | global.console.log(''); 120 | global.console.log('==> Summary: <=='); 121 | 122 | global.console.table([ 123 | ['Total tests', this._tests.length], 124 | ['Passed tests', passedTests.length], 125 | ['Skipped tests', skippedTests.length], 126 | ['Failed tests', failedTests.length], 127 | ['Min render time', `${this._minRenderTime.time}ms (${this._minRenderTime.name})`], 128 | ['Max render time', `${this._maxRenderTime.time}ms (${this._maxRenderTime.name})`], 129 | ]); 130 | 131 | if (failedTests.length > 0) { 132 | global.console.log('==> Failed tests: <=='); 133 | global.console.table(failedTests.map((testCase: TestcaseType) => testCase.name)); 134 | } 135 | 136 | // on CI some logs are not available, adding a delay to fix it 137 | await delay(300); 138 | } 139 | 140 | tojUnit(jUnitFile: string): void { 141 | const xmlResult = ['']; 142 | xmlResult.push('`); 149 | xmlResult.push(' `); 156 | this._tests.forEach((testcase: TestcaseType) => { 157 | xmlResult.push(' `); 161 | if (testcase.failure) { 162 | xmlResult.push(` ${testcase.failure}`); 163 | } else if (testcase.isSkipped) { 164 | xmlResult.push(' '); 165 | } 166 | xmlResult.push(' '); 167 | }); 168 | xmlResult.push(' '); 169 | xmlResult.push(''); 170 | xmlResult.push(''); 171 | fs.writeFileSync(jUnitFile, xmlResult.join('\n')); 172 | } 173 | 174 | collectDeviceLogs(platform: 'ios' | 'android', packageName: string): void { 175 | let spawnProcess: any; 176 | if (platform === 'android') { 177 | exec('adb logcat -c'); 178 | spawnProcess = spawn('adb', [ 179 | 'logcat', `${packageName}:I`, '*:V', 180 | ]); 181 | } else if (platform === 'ios') { 182 | spawnProcess = spawn('xcrun', [ 183 | 'simctl', 'spawn', 'booted', 'log', 'stream', 184 | ]); 185 | } 186 | 187 | spawnProcess.stdout.on('data', (data: any): any => { 188 | const stringRepresentation = data.toString(); 189 | this._deviceLogs.push(stringRepresentation); 190 | }); 191 | 192 | this._stopDeviceLogger = (): void => { 193 | spawnProcess.stdin.pause(); 194 | spawnProcess.kill(); 195 | }; 196 | } 197 | 198 | deviceLogsToFile(fileName: string): void { 199 | if (this._stopDeviceLogger) { 200 | this._stopDeviceLogger(); 201 | this._stopDeviceLogger = undefined; 202 | } 203 | fs.writeFileSync(fileName, this._deviceLogs.join('')); 204 | } 205 | 206 | _getPassedTests(): Array { 207 | return this._tests 208 | .filter(filterSkipped) 209 | .filter(filterFailed); 210 | } 211 | 212 | _getSkippedTests(): Array { 213 | return this._tests 214 | .filter((testcase: TestcaseType): boolean => Boolean(testcase.isSkipped)); 215 | } 216 | 217 | _getFailedTests(): Array { 218 | return this._tests 219 | .filter(filterSkipped) 220 | .filter((test: TestcaseType) => Boolean(test.failure)); 221 | } 222 | 223 | _getTotalTime(): number { 224 | return this._tests 225 | .filter(filterSkipped) 226 | .reduce(timeReducer, 0); 227 | } 228 | } 229 | 230 | export default TestReporter; 231 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/__snapshots__/log.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`logging Allows to log including d 1`] = ` 4 | Array [ 5 | Array [ 6 | "i:", 7 | ], 8 | Array [ 9 | "w WARNING:", 10 | ], 11 | Array [ 12 | "e ERROR:", 13 | ], 14 | Array [ 15 | "v:", 16 | ], 17 | Array [ 18 | "d:", 19 | ], 20 | Array [ 21 | "i:", 22 | ], 23 | Array [ 24 | "w WARNING:", 25 | ], 26 | Array [ 27 | "e ERROR:", 28 | ], 29 | Array [ 30 | "d:", 31 | ], 32 | Array [ 33 | "i:", 34 | ], 35 | Array [ 36 | "w WARNING:", 37 | ], 38 | Array [ 39 | "e ERROR:", 40 | ], 41 | ] 42 | `; 43 | 44 | exports[`logging Allows to log including e 1`] = ` 45 | Array [ 46 | Array [ 47 | "i:", 48 | ], 49 | Array [ 50 | "w WARNING:", 51 | ], 52 | Array [ 53 | "e ERROR:", 54 | ], 55 | Array [ 56 | "v:", 57 | ], 58 | Array [ 59 | "d:", 60 | ], 61 | Array [ 62 | "i:", 63 | ], 64 | Array [ 65 | "w WARNING:", 66 | ], 67 | Array [ 68 | "e ERROR:", 69 | ], 70 | Array [ 71 | "d:", 72 | ], 73 | Array [ 74 | "i:", 75 | ], 76 | Array [ 77 | "w WARNING:", 78 | ], 79 | Array [ 80 | "e ERROR:", 81 | ], 82 | Array [ 83 | "i:", 84 | ], 85 | Array [ 86 | "w WARNING:", 87 | ], 88 | Array [ 89 | "e ERROR:", 90 | ], 91 | Array [ 92 | "w WARNING:", 93 | ], 94 | Array [ 95 | "e ERROR:", 96 | ], 97 | Array [ 98 | "e ERROR:", 99 | ], 100 | ] 101 | `; 102 | 103 | exports[`logging Allows to log including i 1`] = ` 104 | Array [ 105 | Array [ 106 | "i:", 107 | ], 108 | Array [ 109 | "w WARNING:", 110 | ], 111 | Array [ 112 | "e ERROR:", 113 | ], 114 | Array [ 115 | "v:", 116 | ], 117 | Array [ 118 | "d:", 119 | ], 120 | Array [ 121 | "i:", 122 | ], 123 | Array [ 124 | "w WARNING:", 125 | ], 126 | Array [ 127 | "e ERROR:", 128 | ], 129 | Array [ 130 | "d:", 131 | ], 132 | Array [ 133 | "i:", 134 | ], 135 | Array [ 136 | "w WARNING:", 137 | ], 138 | Array [ 139 | "e ERROR:", 140 | ], 141 | Array [ 142 | "i:", 143 | ], 144 | Array [ 145 | "w WARNING:", 146 | ], 147 | Array [ 148 | "e ERROR:", 149 | ], 150 | ] 151 | `; 152 | 153 | exports[`logging Allows to log including v 1`] = ` 154 | Array [ 155 | Array [ 156 | "i:", 157 | ], 158 | Array [ 159 | "w WARNING:", 160 | ], 161 | Array [ 162 | "e ERROR:", 163 | ], 164 | Array [ 165 | "v:", 166 | ], 167 | Array [ 168 | "d:", 169 | ], 170 | Array [ 171 | "i:", 172 | ], 173 | Array [ 174 | "w WARNING:", 175 | ], 176 | Array [ 177 | "e ERROR:", 178 | ], 179 | ] 180 | `; 181 | 182 | exports[`logging Allows to log including w 1`] = ` 183 | Array [ 184 | Array [ 185 | "i:", 186 | ], 187 | Array [ 188 | "w WARNING:", 189 | ], 190 | Array [ 191 | "e ERROR:", 192 | ], 193 | Array [ 194 | "v:", 195 | ], 196 | Array [ 197 | "d:", 198 | ], 199 | Array [ 200 | "i:", 201 | ], 202 | Array [ 203 | "w WARNING:", 204 | ], 205 | Array [ 206 | "e ERROR:", 207 | ], 208 | Array [ 209 | "d:", 210 | ], 211 | Array [ 212 | "i:", 213 | ], 214 | Array [ 215 | "w WARNING:", 216 | ], 217 | Array [ 218 | "e ERROR:", 219 | ], 220 | Array [ 221 | "i:", 222 | ], 223 | Array [ 224 | "w WARNING:", 225 | ], 226 | Array [ 227 | "e ERROR:", 228 | ], 229 | Array [ 230 | "w WARNING:", 231 | ], 232 | Array [ 233 | "e ERROR:", 234 | ], 235 | ] 236 | `; 237 | 238 | exports[`logging Default logs i, w and e 1`] = ` 239 | Array [ 240 | Array [ 241 | "i:", 242 | ], 243 | Array [ 244 | "w WARNING:", 245 | ], 246 | Array [ 247 | "e ERROR:", 248 | ], 249 | ] 250 | `; 251 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/__snapshots__/readConfig.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`readConfig read config from package.json 1`] = ` 4 | Object { 5 | "android": Object { 6 | "_content": "platform config content from package.json", 7 | }, 8 | } 9 | `; 10 | 11 | exports[`readConfig read config from pixels-catcher.json 1`] = ` 12 | Object { 13 | "ios": Object { 14 | "_content": "platform config content from pixels-catcher.json", 15 | }, 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/isCommand.js: -------------------------------------------------------------------------------- 1 | import isCommand from '../isCommand'; 2 | import exec from '../exec'; 3 | 4 | jest.mock('../exec', () => jest.fn()); 5 | 6 | describe('isCommand', () => { 7 | afterEach(() => { 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | it('Returns true if command exists', () => { 12 | exec.mockReturnValueOnce('/bin/ls'); 13 | 14 | const exists = isCommand('ls'); 15 | 16 | expect(exec).toHaveBeenCalledWith('whereis ls'); 17 | expect(exists).toBe(true); 18 | }); 19 | 20 | it('Returns false if command does not exists', () => { 21 | exec.mockReturnValueOnce(''); 22 | 23 | const exists = isCommand('sl'); 24 | 25 | expect(exec).toHaveBeenCalledWith('whereis sl'); 26 | expect(exists).toBe(false); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/log.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import log from '../log'; 3 | 4 | global.console.log = jest.fn(); 5 | 6 | describe('logging', () => { 7 | const consoleLog = global.console.log; 8 | const logAll = () => { 9 | log.v('v'); 10 | log.d('d'); 11 | log.i('i'); 12 | log.w('w'); 13 | log.e('e'); 14 | }; 15 | 16 | afterEach(() => { 17 | log.setLevel('v'); 18 | }); 19 | 20 | it('Default logs i, w and e', () => { 21 | logAll(); 22 | expect(consoleLog.mock.calls).toMatchSnapshot(); 23 | }); 24 | 25 | it('Allows to log including v', () => { 26 | log.setLevel('v'); 27 | logAll(); 28 | expect(consoleLog.mock.calls).toMatchSnapshot(); 29 | }); 30 | 31 | it('Allows to log including d', () => { 32 | log.setLevel('d'); 33 | logAll(); 34 | expect(consoleLog.mock.calls).toMatchSnapshot(); 35 | }); 36 | 37 | it('Allows to log including i', () => { 38 | log.setLevel('i'); 39 | logAll(); 40 | expect(consoleLog.mock.calls).toMatchSnapshot(); 41 | }); 42 | 43 | it('Allows to log including w', () => { 44 | log.setLevel('w'); 45 | logAll(); 46 | expect(consoleLog.mock.calls).toMatchSnapshot(); 47 | }); 48 | 49 | it('Allows to log including e', () => { 50 | log.setLevel('e'); 51 | logAll(); 52 | expect(consoleLog.mock.calls).toMatchSnapshot(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/readConfig.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import readConfig from '../readConfig'; 4 | 5 | jest.mock('fs', () => ({ 6 | existsSync: jest.fn(), 7 | readFileSync: jest.fn(), 8 | })); 9 | jest.mock('path', () => ({ 10 | join: jest.fn((...args) => args.join('/')), 11 | })); 12 | 13 | process.exit = jest.fn(); 14 | process.cwd = jest.fn(() => 'path_to_file'); 15 | 16 | describe('readConfig', () => { 17 | afterEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | it('read config from package.json', () => { 22 | const rawConfig = '{"PixelsCatcher":{"android":{"_content":"platform ' 23 | + 'config content from package.json"}}}'; 24 | 25 | fs.existsSync.mockImplementationOnce(() => true); 26 | fs.readFileSync.mockImplementationOnce(() => rawConfig); 27 | 28 | const config = readConfig(); 29 | 30 | expect(config).toMatchSnapshot(); 31 | }); 32 | 33 | it('read config from pixels-catcher.json', () => { 34 | // package.json mock 35 | fs.existsSync.mockImplementationOnce(() => true); 36 | fs.readFileSync.mockImplementationOnce(() => '{}'); 37 | // pixels-catcher.json mock 38 | const rawConfig = '{"ios":{"_content":"platform config content from ' 39 | + 'pixels-catcher.json"}}'; 40 | fs.existsSync.mockImplementationOnce(() => true); 41 | fs.readFileSync.mockImplementationOnce(() => rawConfig); 42 | 43 | const config = readConfig(); 44 | 45 | expect(config).toMatchSnapshot(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/runner/utils/delay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | function delay(milliseconds: number): Promise { 8 | return new Promise((resolve: Function) => { 9 | setTimeout(resolve, milliseconds); 10 | }); 11 | } 12 | 13 | export default delay; 14 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidDevice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2019 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type { DeviceInterface } from './DeviceInterface'; 8 | 9 | import exec from '../exec'; 10 | import delay from '../delay'; 11 | import log from '../log'; 12 | 13 | const TAG = 'PIXELS_CATCHER::ANDROID_DEVICE'; 14 | 15 | class AndroidDevice implements DeviceInterface { 16 | _name: string; 17 | 18 | constructor(name: string) { 19 | this._name = name; 20 | } 21 | 22 | 23 | _getDevices(): Array { 24 | const cmd = 'adb devices'; 25 | const devices = exec(cmd).split('\n').slice(1) 26 | .filter((line: string): boolean => Boolean(line)) 27 | .map((line: string): string => line.split('\t')[0]); 28 | 29 | return devices; 30 | } 31 | 32 | 33 | _isDeviceAvailable(name: string): boolean { 34 | const devices = this._getDevices(); 35 | let isAvailable = false; 36 | 37 | for (let ind = devices.length - 1; ind >= 0; --ind) { 38 | if (devices[ind].indexOf(name) >= 0) { 39 | isAvailable = true; 40 | break; 41 | } 42 | } 43 | 44 | return isAvailable; 45 | } 46 | 47 | 48 | async start(params: any = []): Promise { 49 | if (params.length !== 0) { 50 | log.e(TAG, 'There are currently no supported device parameters for physical devices, yet you tried to pass some im'); 51 | process.exit(-1); 52 | } 53 | if (!this._isDeviceAvailable(this._name)) { 54 | log.e(TAG, `Invalid name provided [${this._name}], check that the name is \ 55 | correct and device is available. Available devices: 56 | ${this._getDevices().map((device: any): any => ` - ${device}`).join('\n')}`); 57 | throw new Error(`Invalid emulator ${this._name}`); 58 | } 59 | } 60 | 61 | 62 | async stop(): Promise { 63 | log.v(TAG, 'Not stopping anything as it is assumed to be a physical device. Your responsibility!'); 64 | } 65 | 66 | 67 | isAppInstalled(packageName: string): boolean { 68 | const cmd = `adb -s ${this._name} shell pm list packages`; 69 | 70 | log.v(TAG, `Checking if [${packageName}] is installed`); 71 | 72 | const allPackages = exec(cmd); 73 | const isInstalled = allPackages.indexOf(packageName) >= 0; 74 | 75 | log.v(TAG, `Package [${packageName}] is ${isInstalled ? 'Installed' : 'Not installed'}`); 76 | 77 | return isInstalled; 78 | } 79 | 80 | 81 | async uninstallApp(name: string): Promise { 82 | log.v(TAG, `Uninstalling ${name}`); 83 | const isInstalled = await this.isAppInstalled(name); 84 | if (isInstalled) { 85 | const cmd = `adb -s ${this._name} uninstall ${name}`; 86 | exec(cmd); 87 | } 88 | log.v(TAG, 'Uninstalling completed'); 89 | } 90 | 91 | 92 | async installApp(name: string, apkFile: string): Promise { 93 | log.v(TAG, `Installing apk [${apkFile}]`); 94 | 95 | await this.uninstallApp(name); 96 | 97 | let tryCnt = 3; 98 | 99 | while (tryCnt >= 0) { 100 | const cmd = `adb -s ${this._name} install -r ${apkFile}`; 101 | const res = exec(cmd); 102 | log.v(TAG, 'Installed', res); 103 | const isOffline = res.indexOf('device offline') >= 0; 104 | if (isOffline) { 105 | await delay(1000); 106 | } else { 107 | const isSuccess = res.indexOf('Success') >= 0; 108 | if (isSuccess) { 109 | break; 110 | } else { 111 | log.e(TAG, `ERROR: Failed install apk [${apkFile}]`); 112 | process.exit(-1); 113 | } 114 | } 115 | tryCnt--; 116 | } 117 | } 118 | 119 | 120 | startApp(packageName: string, activityName: string): void { 121 | log.v(TAG, `Starting application [${packageName}]`); 122 | 123 | const cmd = `adb -s ${this._name} shell am start -n ${packageName}/${activityName}`; 124 | const result = exec(cmd); 125 | 126 | if (result.indexOf('does not exist') >= 0 || result.indexOf('Error') >= 0) { 127 | log.e(TAG, `Cannot start [${packageName}] with activity [${activityName}]`); 128 | process.exit(-1); 129 | } 130 | 131 | log.v(TAG, 'Application started'); 132 | } 133 | } 134 | 135 | export default AndroidDevice; 136 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidEmulator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import { spawn } from 'child_process'; 8 | 9 | import exec from '../exec'; 10 | import delay from '../delay'; 11 | import log from '../log'; 12 | import emulatorCmd from './AndroidEmulatorCmd'; 13 | 14 | import type { DeviceInterface } from './DeviceInterface'; 15 | 16 | const TAG = 'PIXELS_CATCHER::UTIL_EMULATOR'; 17 | 18 | const startupErrorsDataToIgnore = [ 19 | // Some data appears in stderr when running the emulator first time 20 | '.avd/snapshots/default_boot/ram.img', 21 | 'qemu: unsupported keyboard', 22 | 'WARNING', 23 | ]; 24 | 25 | const canIgnoreErrorData = (data: string): boolean => { 26 | for (let i = 0; i < startupErrorsDataToIgnore.length; ++i) { 27 | if (data.indexOf(startupErrorsDataToIgnore[i]) !== -1) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | }; 34 | 35 | class AndroidEmulator implements DeviceInterface { 36 | _name: string; 37 | 38 | _canStopDevice: boolean; 39 | 40 | constructor(name: string, canStopDevice?: boolean) { 41 | this._name = name; 42 | this._canStopDevice = Boolean(canStopDevice); 43 | } 44 | 45 | _getDevices(): Array { 46 | const cmd = 'emulator -avd -list-avds'; 47 | const devices = exec(cmd).split('\n') 48 | .filter((line: string): boolean => Boolean(line)); 49 | 50 | return devices; 51 | } 52 | 53 | _isDeviceAvailable(name: string): boolean { 54 | const devices = this._getDevices(); 55 | let isAvailable = false; 56 | 57 | for (let ind = devices.length - 1; ind >= 0; --ind) { 58 | if (devices[ind].indexOf(name) >= 0) { 59 | isAvailable = true; 60 | break; 61 | } 62 | } 63 | 64 | return isAvailable; 65 | } 66 | 67 | _getActiveDevice(): any { 68 | log.v(TAG, 'Get active device'); 69 | const device = exec('adb devices').split('\n') 70 | .filter((line: string): boolean => line.indexOf('emulator') === 0)[0]; 71 | 72 | if (!device) { 73 | log.v(TAG, 'No active devices'); 74 | return undefined; 75 | } 76 | const name = device.split('\t')[0]; 77 | 78 | log.v(TAG, 'Active device', name); 79 | return name; 80 | } 81 | 82 | async start(params: any = []): Promise { 83 | if (!this._isDeviceAvailable(this._name)) { 84 | log.e(TAG, `Invalid name provided [${this._name}], check that the name is \ 85 | correct and device is available. Available devices: 86 | ${this._getDevices().map((device: any): any => ` - ${device}`).join('\n')}`); 87 | throw new Error(`Invalid emulator ${this._name}`); 88 | } 89 | 90 | if (this._getActiveDevice()) { 91 | log.e(TAG, 'Other emulator already started'); 92 | if (this._canStopDevice) { 93 | log.e(TAG, 'Stopping emulator'); 94 | await this.stop(); 95 | } else { 96 | log.d(TAG, 'Using active emulator'); 97 | return; 98 | } 99 | } 100 | 101 | log.d(TAG, `Starting emulator [${this._name}]`); 102 | log.v(TAG, `cmd: ${emulatorCmd}`); 103 | log.v(TAG, `params: ${[ 104 | '-avd', this._name, 105 | ...params, 106 | ].filter((value: any): any => Boolean(value))}`); 107 | const result = spawn(emulatorCmd, [ 108 | '-avd', this._name, 109 | ...params, 110 | ].filter((value: any): any => Boolean(value))); 111 | 112 | let deviceBooted = false; 113 | 114 | result.stdout.on('data', (data: any): any => { 115 | log.d(TAG, `stdout: ${data}`); 116 | if (data.toString().toLowerCase().includes('boot completed')) { 117 | deviceBooted = true; 118 | } 119 | }); 120 | 121 | result.stderr.on('data', (data: any): any => { 122 | // Some data appears in stderr when running the emulator first time 123 | const stringRepresentation = data.toString(); 124 | if (canIgnoreErrorData(stringRepresentation)) { 125 | log.w(TAG, `Ignore: ${stringRepresentation}`); 126 | return; 127 | } 128 | log.e(TAG, `Failed to load emulator, stderr: ${data}`); 129 | process.exit(-1); 130 | }); 131 | 132 | result.on('close', (code: any): any => { 133 | log.v(TAG, `on close: child process exited with code ${code}`); 134 | }); 135 | 136 | let tryCnt = (60 * 2) / 5; // 2 minutes with 5000 delay 137 | 138 | while (--tryCnt >= 0 && !deviceBooted) { 139 | log.v(TAG, 'awaiting when device is booted'); 140 | await delay(5000); 141 | } 142 | 143 | if (!deviceBooted) { 144 | log.e(TAG, 'Failed to load emulator in 30 seconds. Check your emulator. Or try to run it with "-no-snapshot"'); 145 | throw new Error('Device is not loaded in 30 seconds'); 146 | } 147 | } 148 | 149 | async stop(): Promise { 150 | if (!this._canStopDevice) { 151 | log.v(TAG, 'Stopping device is restricted in config'); 152 | return; 153 | } 154 | log.v(TAG, 'Stopping active device'); 155 | try { 156 | exec(`adb -s ${this._getActiveDevice()} emu kill;`); 157 | } catch (err) { 158 | log.e(err instanceof Error ? err.message : 'Unknown error'); 159 | } 160 | await delay(5000); 161 | log.v(TAG, 'Active device stopped'); 162 | } 163 | 164 | isAppInstalled(packageName: string): boolean { 165 | const cmd = 'adb shell pm list packages'; 166 | 167 | log.v(TAG, `Checking if [${packageName}] is installed`); 168 | 169 | const allPackages = exec(cmd); 170 | const isInstalled = allPackages.indexOf(packageName) >= 0; 171 | 172 | log.v(TAG, `Package [${packageName}] is ${isInstalled ? 'Installed' : 'Not installed'}`); 173 | 174 | return isInstalled; 175 | } 176 | 177 | async uninstallApp(name: string): Promise { 178 | log.v(TAG, `Uninstalling ${name}`); 179 | const isInstalled = await this.isAppInstalled(name); 180 | if (isInstalled) { 181 | const cmd = `adb uninstall ${name}`; 182 | exec(cmd); 183 | } 184 | log.v(TAG, 'Uninstalling completed'); 185 | } 186 | 187 | async installApp(name: string, apkFile: string): Promise { 188 | let tryCnt = 3; 189 | 190 | log.v(TAG, `Installing apk [${apkFile}]`); 191 | 192 | await this.uninstallApp(name); 193 | 194 | while (tryCnt-- >= 0) { 195 | const cmd = `adb install -r ${apkFile}`; 196 | const res = exec(cmd); 197 | log.v(TAG, 'Installed', res); 198 | const isOffline = res.indexOf('device offline') >= 0; 199 | if (isOffline) { 200 | await delay(1000); 201 | } else { 202 | const isSuccess = res.indexOf('Success') >= 0; 203 | if (isSuccess) { 204 | break; 205 | } else { 206 | log.e(TAG, `ERROR: Failed install apk [${apkFile}]`); 207 | process.exit(-1); 208 | } 209 | } 210 | } 211 | } 212 | 213 | startApp(packageName: string, activityName: string): void { 214 | log.v(TAG, `Starting application [${packageName}]`); 215 | 216 | const cmd = `adb shell am start -n ${packageName}/${activityName}`; 217 | const result = exec(cmd); 218 | 219 | if (result.indexOf('does not exist') >= 0 || result.indexOf('Error') >= 0) { 220 | log.e(TAG, `Cannot start [${packageName}] with activity [${activityName}]`); 221 | process.exit(-1); 222 | } 223 | 224 | log.v(TAG, 'Application started'); 225 | } 226 | } 227 | 228 | export default AndroidEmulator; 229 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidEmulatorCmd.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import exec from '../exec'; 8 | import isCommand from '../isCommand'; 9 | 10 | export default process.env.ANDROID_EMULATOR 11 | || (isCommand('emulator') ? 'emulator' : undefined) 12 | || ( 13 | exec('uname -s').trim() === 'Darwin' 14 | ? `${process.env.HOME || ''}/Library/Android/sdk/emulator/emulator` 15 | : 'emulator'); 16 | -------------------------------------------------------------------------------- /src/runner/utils/device/DeviceInterface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /** 3 | * Copyright (c) Maksym Rusynyk 2018 - present 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | export type StartParamsType = Array; 10 | 11 | export interface DeviceInterface { 12 | // constructor(deviceName: string, canStopDevice?: boolean): void, 13 | 14 | start(params: StartParamsType): Promise, 15 | 16 | isAppInstalled(appName: string): boolean, 17 | 18 | installApp(appName: string, appFile: string): Promise, 19 | 20 | startApp(appName: string, activityName: string, locale?: string): void, 21 | 22 | uninstallApp(name: string): Promise, 23 | 24 | stop(): Promise, 25 | } 26 | -------------------------------------------------------------------------------- /src/runner/utils/device/IosSimulator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | /* @flow */ 8 | import type { DeviceInterface, StartParamsType } from './DeviceInterface'; 9 | 10 | import exec from '../exec'; 11 | import log from '../log'; 12 | import delay from '../delay'; 13 | 14 | const TAG = 'PIXELS_CATCHER::UTIL_SIMULATOR'; 15 | 16 | type DeviceType = { 17 | availability: string, 18 | state: string, 19 | isAvailable: boolean, 20 | name: string, 21 | udid: string, 22 | availabilityError: string, 23 | }; 24 | 25 | class IOSSimulator implements DeviceInterface { 26 | _name: string; 27 | 28 | _canStopDevice: boolean; 29 | 30 | constructor(name: string, canStopDevice?: boolean) { 31 | this._name = name; 32 | this._canStopDevice = Boolean(canStopDevice); 33 | } 34 | 35 | 36 | _getAvailableDevices(): Array { 37 | const cmd = 'xcrun simctl list --json'; 38 | const response = JSON.parse(exec(cmd)); 39 | const { devices } = response; 40 | const availableDevices: Array = []; 41 | 42 | Object.keys(devices).forEach((name: string) => { 43 | devices[name].forEach((device: DeviceType) => { 44 | if (device.isAvailable) { 45 | availableDevices.push(device); 46 | } 47 | }); 48 | }); 49 | 50 | return availableDevices; 51 | } 52 | 53 | 54 | _getDeviceByName(name: string): DeviceType | void { 55 | const devices = this._getAvailableDevices(); 56 | let device; 57 | 58 | for (let ind = devices.length - 1; ind >= 0; --ind) { 59 | if (devices[ind].name === name) { 60 | device = devices[ind]; 61 | break; 62 | } 63 | } 64 | 65 | return device; 66 | } 67 | 68 | 69 | _getDeviceByUid(uid: string): DeviceType | void { 70 | const devices = this._getAvailableDevices(); 71 | let device; 72 | 73 | for (let ind = devices.length - 1; ind >= 0; --ind) { 74 | if (devices[ind].udid === uid) { 75 | device = devices[ind]; 76 | break; 77 | } 78 | } 79 | 80 | return device; 81 | } 82 | 83 | 84 | _getDeviceWithStatus(status: string): DeviceType | void { 85 | const devices = this._getAvailableDevices(); 86 | let device; 87 | 88 | for (let ind = devices.length - 1; ind >= 0; --ind) { 89 | if (devices[ind].state === status) { 90 | device = devices[ind]; 91 | break; 92 | } 93 | } 94 | 95 | return device; 96 | } 97 | 98 | 99 | _getUid(name: string): string | void { 100 | const device = this._getDeviceByName(name); 101 | log.v(TAG, `Device ${name} is:`, device); 102 | return device ? device.udid : undefined; 103 | } 104 | 105 | 106 | async _boot(uid: string): Promise { 107 | const device = this._getDeviceByUid(uid); 108 | if (!device) { 109 | throw new Error(`Invalid device uid [${uid}], cannot find it`); 110 | } 111 | if (device.state === 'Booted') { 112 | log.i(TAG, `Device [${device.name}] already booted`); 113 | return; 114 | } 115 | const response = exec(`xcrun simctl boot ${uid}`); 116 | if (response) { 117 | log.v(TAG, 'boot response:', response); 118 | } 119 | } 120 | 121 | 122 | async _open(uid: string): Promise { 123 | const activeXcode = exec('xcode-select -p').trim(); 124 | log.v(TAG, `Active Xcode: ${activeXcode}`); 125 | const simulatorApp = `${activeXcode}/Applications/Simulator.app`; 126 | log.v(TAG, `starting ${simulatorApp}`); 127 | exec(`open -a ${simulatorApp} --args -CurrentDeviceUDID ${uid}`); 128 | log.v(TAG, 'started'); 129 | } 130 | 131 | 132 | async start(params: StartParamsType): Promise { 133 | log.v(TAG, 'Starting device with params:', params); 134 | 135 | this.stop(); 136 | 137 | const uid = this._getUid(this._name); 138 | log.i(TAG, `Uid of the device is [${uid || '-'}]`); 139 | 140 | if (!uid) { 141 | throw new Error(`Invalid simulator [${this._name}], cannot find uid`); 142 | } 143 | 144 | await this._boot(uid); 145 | await this._open(uid); 146 | 147 | log.v(TAG, 'Device started', this._getDeviceByUid(uid)); 148 | } 149 | 150 | 151 | isAppInstalled(appName: string): boolean { 152 | log.v(`isAppInstalled: appName [${appName}]`); 153 | return false; 154 | } 155 | 156 | 157 | async installApp(appName: string, appFile: string): Promise { 158 | this.uninstallApp(appName); 159 | log.v(TAG, `Installing application [${appName}], appFile [${appFile}]`); 160 | exec(`xcrun simctl install booted ${appFile}`); 161 | } 162 | 163 | 164 | startApp(appName: string, activityName: string, locale?: string): void { 165 | const withLocale = locale ? `-AppleLanguages "(${locale})"` : ''; 166 | log.v(TAG, `startApp: appName [${appName}], activityName [${activityName}], locale [${locale || '-'}]`); 167 | exec(`xcrun simctl launch booted ${appName} ${withLocale}`); 168 | } 169 | 170 | 171 | async uninstallApp(appName: string): Promise { 172 | log.v(TAG, `Uninstalling application [${appName}]`); 173 | exec(`xcrun simctl uninstall booted ${appName}`); 174 | } 175 | 176 | 177 | async stop(): Promise { 178 | if (!this._canStopDevice) { 179 | log.v(TAG, 'Stopping device is restricted in config'); 180 | return; 181 | } 182 | 183 | log.v(TAG, 'Stopping all devices'); 184 | 185 | exec('osascript -e \'tell application "iOS Simulator" to quit\''); 186 | exec('osascript -e \'tell application "Simulator" to quit\''); 187 | 188 | let device = this._getDeviceWithStatus('Shutting Down'); 189 | 190 | while (device) { 191 | log.v(TAG, `Awaiting for shutdown completed (Device ${device.name} has ` 192 | + `state ${device.state})`); 193 | await delay(1000); 194 | device = this._getDeviceWithStatus('Shutting Down'); 195 | } 196 | 197 | log.v(TAG, 'Devices stopped'); 198 | } 199 | } 200 | 201 | export default IOSSimulator; 202 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidDevice.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | describe('AndroidDevice', () => { 3 | it('initialise', () => {}); 4 | }); 5 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidEmulator.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | jest.mock('child_process', () => ({ spawn: jest.fn() })); 3 | jest.mock('../../exec', () => jest.fn(() => '')); 4 | jest.mock('../../delay', () => jest.fn()); 5 | jest.mock('../../log', () => ({ 6 | v: jest.fn(), 7 | d: jest.fn(), 8 | e: jest.fn(), 9 | })); 10 | jest.mock('../AndroidEmulatorCmd', () => 'emulator'); 11 | 12 | const { spawn } = require('child_process'); 13 | 14 | const AndroidEmulator = require('../AndroidEmulator').default; 15 | const exec = require('../../exec'); 16 | const delay = require('../../delay'); 17 | 18 | describe('AndroidEmulator', () => { 19 | const name = 'emulator_name'; 20 | 21 | beforeEach(() => { 22 | jest.resetAllMocks(); 23 | }); 24 | 25 | it('initialise emulator', () => { 26 | // $FlowFixMe: ignore for mock 27 | exec.mockImplementationOnce(() => 'avd devices'); 28 | 29 | const emularor = new AndroidEmulator(name); 30 | expect(emularor).toMatchSnapshot(); 31 | }); 32 | 33 | it('start emulator when it is not available should throw error', async () => { 34 | // $FlowFixMe: ignore for mocks 35 | exec.mockImplementation(() => 'avd devices'); 36 | 37 | const emularor = new AndroidEmulator(name); 38 | let exception; 39 | 40 | try { 41 | await emularor.start(); 42 | } catch (err) { 43 | exception = err; 44 | } 45 | 46 | expect(exception).toMatchSnapshot(); 47 | }); 48 | 49 | it('start emulator when it is available but not started throws error if not started', async () => { 50 | // $FlowFixMe: ignore for mock 51 | exec.mockImplementation(() => `avd devices including ${name}`); 52 | const spawnMock = { 53 | stdout: { on: jest.fn() }, 54 | stderr: { on: jest.fn() }, 55 | on: jest.fn(), 56 | }; 57 | spawn.mockImplementationOnce(() => spawnMock); 58 | 59 | const emularor = new AndroidEmulator(name); 60 | 61 | let exception; 62 | 63 | try { 64 | await emularor.start(); 65 | } catch (err) { 66 | exception = err; 67 | } 68 | 69 | expect(exception).toMatchSnapshot(); 70 | }); 71 | 72 | it('start emulator when it is available but not started', async () => { 73 | // $FlowFixMe: ignore for mock 74 | exec.mockImplementation(() => `avd devices including ${name}`); 75 | const spawnMock = { 76 | stdout: { on: jest.fn() }, 77 | stderr: { on: jest.fn() }, 78 | on: jest.fn(), 79 | }; 80 | spawn.mockImplementationOnce(() => spawnMock); 81 | 82 | const emularor = new AndroidEmulator(name); 83 | 84 | const startPromise = emularor.start(); 85 | 86 | const dataCallback = spawnMock.stdout.on.mock.calls[0][1]; 87 | dataCallback('boot completed'); 88 | 89 | await startPromise; 90 | }); 91 | 92 | it('start emulator when it is available and already started should stop it before starting', async () => { 93 | // $FlowFixMe: ignore for mock 94 | exec.mockImplementation((cmd) => { 95 | if (cmd === 'emulator -avd -list-avds') { 96 | return `avd devices including ${name}`; 97 | } 98 | return 'List of devices attached\nemulator-5554 device'; 99 | }); 100 | const spawnMock = { 101 | stdout: { on: jest.fn() }, 102 | stderr: { on: jest.fn() }, 103 | on: jest.fn(), 104 | }; 105 | 106 | spawn.mockImplementationOnce(() => spawnMock); 107 | // $FlowFixMe: ignore for mocks 108 | delay.mockImplementation(() => { 109 | const spawnMockCalls = spawnMock.stdout.on.mock.calls; 110 | if (spawnMockCalls && spawnMockCalls[0] && spawnMockCalls[0][1]) { 111 | const dataCallback = spawnMock.stdout.on.mock.calls[0][1]; 112 | dataCallback('boot completed'); 113 | } 114 | }); 115 | 116 | const emularor = new AndroidEmulator(name); 117 | 118 | const startPromise = emularor.start(); 119 | 120 | await startPromise; 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidEmulatorCmd.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | describe('AndroidEmulatorCmd', () => { 4 | beforeEach(() => { 5 | process.env.ANDROID_EMULATOR = ''; 6 | jest.resetModules(); 7 | }); 8 | 9 | it('returns command provided via ANDROID_EMULATOR', () => { 10 | process.env.ANDROID_EMULATOR = 'cmdFrom_ANDROID_EMULATOR'; 11 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 12 | 13 | expect(emulatorCmd).toBe(process.env.ANDROID_EMULATOR); 14 | }); 15 | 16 | it('returns command available in PATH', () => { 17 | process.env.ANDROID_EMULATOR = ''; 18 | jest.mock('../../isCommand', () => () => true); 19 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 20 | 21 | expect(emulatorCmd).toBe('emulator'); 22 | }); 23 | 24 | it('gets command from /Library/Android/sdk/emulator/emulator on mac', () => { 25 | process.env.ANDROID_EMULATOR = ''; 26 | jest.mock('../../isCommand', () => () => false); 27 | jest.mock('../../exec', () => () => 'Darwin'); 28 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 29 | 30 | expect(emulatorCmd 31 | .indexOf('Library/Android/sdk/emulator/emulator') > 0).toBe(true); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/IosSimulator.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | describe('IOSSimulator', () => { 3 | it('initialise', () => {}); 4 | }); 5 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/__snapshots__/AndroidEmulator.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AndroidEmulator initialise emulator 1`] = ` 4 | AndroidEmulator { 5 | "_canStopDevice": false, 6 | "_name": "emulator_name", 7 | } 8 | `; 9 | 10 | exports[`AndroidEmulator start emulator when it is available but not started throws error if not started 1`] = `[Error: Device is not loaded in 30 seconds]`; 11 | 12 | exports[`AndroidEmulator start emulator when it is not available should throw error 1`] = `[Error: Invalid emulator emulator_name]`; 13 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/__snapshots__/deviceProvider.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`deviceProvider provide Android Emulator 1`] = ` 4 | _class { 5 | "_name": "AndroidEmulator", 6 | "deviceName": "test", 7 | } 8 | `; 9 | 10 | exports[`deviceProvider provide AndroidDevice 1`] = ` 11 | _class2 { 12 | "_name": "AndroidDevice", 13 | "deviceName": "test", 14 | } 15 | `; 16 | 17 | exports[`deviceProvider provide iOS device throws error (not implemented) 1`] = `[Error: iOS devices are not supported yet]`; 18 | 19 | exports[`deviceProvider provide iOS simulator 1`] = ` 20 | _class3 { 21 | "_name": "IosSimulator", 22 | "deviceName": "test", 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/deviceProvider.js: -------------------------------------------------------------------------------- 1 | import getDevice from '../deviceProvider'; 2 | 3 | jest.mock('../AndroidEmulator', () => (class { 4 | constructor(name) { 5 | this._name = 'AndroidEmulator'; 6 | this.deviceName = name; 7 | } 8 | })); 9 | jest.mock('../AndroidDevice', () => (class { 10 | constructor(name) { 11 | this._name = 'AndroidDevice'; 12 | this.deviceName = name; 13 | } 14 | })); 15 | jest.mock('../IosSimulator', () => (class { 16 | constructor(name) { 17 | this._name = 'IosSimulator'; 18 | this.deviceName = name; 19 | } 20 | })); 21 | 22 | describe('deviceProvider', () => { 23 | it('provide Android Emulator', () => { 24 | const device = getDevice('test', 'android'); 25 | expect(device).toMatchSnapshot(); 26 | }); 27 | 28 | it('provide AndroidDevice', () => { 29 | const device = getDevice('test', 'android', true); 30 | expect(device).toMatchSnapshot(); 31 | }); 32 | 33 | it('provide iOS simulator', () => { 34 | const device = getDevice('test', 'ios'); 35 | expect(device).toMatchSnapshot(); 36 | }); 37 | 38 | it('provide iOS device throws error (not implemented)', () => { 39 | let error; 40 | let device; 41 | try { 42 | device = getDevice('test', 'ios', true); 43 | } catch (err) { 44 | error = err; 45 | } 46 | 47 | expect(device).toBe(undefined); 48 | expect(error).toMatchSnapshot(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/runner/utils/device/deviceProvider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2019 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type { DeviceInterface } from './DeviceInterface'; 8 | 9 | import log from '../log'; 10 | import AndroidEmulator from './AndroidEmulator'; 11 | import AndroidDevice from './AndroidDevice'; 12 | import IosSimulator from './IosSimulator'; 13 | 14 | const TAG = 'PIXELS_CATCHER::DEVICE_PROVIDER'; 15 | 16 | export default ( 17 | name: string, 18 | platform: string, 19 | isPhysicalDevice?: boolean, 20 | canStopDevice: boolean = true, 21 | ): DeviceInterface => { 22 | if (platform === 'android') { 23 | return isPhysicalDevice 24 | ? new AndroidDevice(name) 25 | : new AndroidEmulator(name, canStopDevice); 26 | } 27 | 28 | if (!isPhysicalDevice) { 29 | return new IosSimulator(name, canStopDevice); 30 | } 31 | 32 | log.e(TAG, 'iOS devices are not supported yet'); 33 | throw new Error('iOS devices are not supported yet'); 34 | }; 35 | -------------------------------------------------------------------------------- /src/runner/utils/exec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as childProcess from 'child_process'; 8 | 9 | import log from './log'; 10 | 11 | const TAG = 'PIXELS_CATCHER::UTIL_EXEC'; 12 | 13 | export default function exec(cmd: string): string { 14 | let result = ''; 15 | 16 | try { 17 | result = childProcess.execSync(cmd).toString(); 18 | } catch (err) { 19 | log.e(TAG, `Failed to execute [${cmd}], error: [${err instanceof Error ? err.message : 'Unknown error'}]`, err); 20 | } 21 | 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /src/runner/utils/isCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import exec from './exec'; 8 | 9 | function isCommand(cmd: string): boolean { 10 | const out = exec(`whereis ${cmd}`); 11 | 12 | return Boolean(out.trim()); 13 | } 14 | 15 | export default isCommand; 16 | -------------------------------------------------------------------------------- /src/runner/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | const TAG = 'PIXELS_CATCHER::UTIL_LOG'; 8 | const logLevels: { 9 | [key: string]: number 10 | } = { 11 | v: 4, 12 | d: 3, 13 | i: 2, 14 | w: 1, 15 | e: 0, 16 | }; 17 | let activeLevel = logLevels.i; 18 | 19 | const log: { 20 | [key: string]: Function 21 | } = { 22 | v(tag: string, ...args: any) { 23 | if (activeLevel >= logLevels.v) { 24 | global.console.log(`${tag}:`, ...args); 25 | } 26 | }, 27 | 28 | d: (tag: string, ...args: any) => { 29 | if (activeLevel >= logLevels.d) { 30 | global.console.log(`${tag}:`, ...args); 31 | } 32 | }, 33 | 34 | i: (tag: string, ...args: any) => { 35 | if (activeLevel >= logLevels.i) { 36 | global.console.log(`${tag}:`, ...args); 37 | } 38 | }, 39 | 40 | w: (tag: string, ...args: any) => { 41 | if (activeLevel >= logLevels.w) { 42 | global.console.log(`${tag} WARNING:`, ...args); 43 | } 44 | }, 45 | 46 | e: (tag: string, ...args: any) => { 47 | if (activeLevel >= logLevels.e) { 48 | global.console.log(`${tag} ERROR:`, ...args); 49 | } 50 | }, 51 | 52 | setLevel(level: string | undefined = 'i') { 53 | let nextLevel = logLevels[level]; 54 | if (nextLevel === undefined) { 55 | global.console.log(`${TAG} WARNING:`, `Invalid level [${level}]. Supported levels: ${Object.keys(logLevels).join(', ')}`); 56 | nextLevel = logLevels.i; 57 | } 58 | activeLevel = nextLevel; 59 | }, 60 | }; 61 | 62 | export default log; 63 | -------------------------------------------------------------------------------- /src/runner/utils/readConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as fs from 'fs'; 8 | import * as path from 'path'; 9 | 10 | import log from './log'; 11 | 12 | const TAG = 'PIXELS_CATCHER::UTIL_READ_CONFIG'; 13 | const CONFIG_FILE = 'pixels-catcher.json'; 14 | const PACKAGE_JSON_FILE = 'package.json'; 15 | 16 | const readConfigFromPackageJSON = (): any => { 17 | const projectPackageFile = path.join(process.cwd(), PACKAGE_JSON_FILE); 18 | 19 | if (!fs.existsSync(projectPackageFile)) { 20 | log.e(TAG, `Cannot find ${PACKAGE_JSON_FILE} file [${projectPackageFile}]. ` 21 | + 'Check that you started the script from the root of your application'); 22 | process.exit(-1); 23 | } 24 | 25 | const fileContent = fs.readFileSync(projectPackageFile, 'utf8'); 26 | 27 | return JSON.parse(fileContent).PixelsCatcher; 28 | }; 29 | 30 | const readConfigFromFile = (): any => { 31 | const configFile = path.join(process.cwd(), CONFIG_FILE); 32 | 33 | if (!fs.existsSync(configFile)) { 34 | log.w(TAG, `Cannot find [${configFile}] file`); 35 | return undefined; 36 | } 37 | 38 | const fileContent = fs.readFileSync(configFile, 'utf8'); 39 | 40 | return JSON.parse(fileContent); 41 | }; 42 | 43 | export default (): any => { 44 | const pixelsCatcherConfig = readConfigFromPackageJSON() 45 | || readConfigFromFile(); 46 | 47 | if (!pixelsCatcherConfig) { 48 | log.e(TAG, 'Cannot find "PixelsCatcher" in package.json or find ' 49 | + 'pixels-catcher.json file'); 50 | process.exit(-1); 51 | } 52 | 53 | return pixelsCatcherConfig; 54 | }; 55 | -------------------------------------------------------------------------------- /src/runner/utils/timeToSec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const timeToSec = (ms: number): number => { 9 | const sec = ms / 1000; 10 | return Math.round(sec * 1000) / 1000; 11 | }; 12 | 13 | export default timeToSec; 14 | --------------------------------------------------------------------------------
Please note that this class is used ONLY if you opt-in for the New Architecture (see the 14 | * `newArchEnabled` property). Is ignored otherwise. 15 | */ 16 | @DoNotStrip 17 | public class MainComponentsRegistry { 18 | static { 19 | SoLoader.loadLibrary("fabricjni"); 20 | } 21 | 22 | @DoNotStrip private final HybridData mHybridData; 23 | 24 | @DoNotStrip 25 | private native HybridData initHybrid(ComponentFactory componentFactory); 26 | 27 | @DoNotStrip 28 | private MainComponentsRegistry(ComponentFactory componentFactory) { 29 | mHybridData = initHybrid(componentFactory); 30 | } 31 | 32 | @DoNotStrip 33 | public static MainComponentsRegistry register(ComponentFactory componentFactory) { 34 | return new MainComponentsRegistry(componentFactory); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/android/app/src/main/java/com/demo/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java: -------------------------------------------------------------------------------- 1 | package com.demo.newarchitecture.modules; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.soloader.SoLoader; 8 | import java.util.List; 9 | 10 | /** 11 | * Class responsible to load the TurboModules. This class has native methods and needs a 12 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 13 | * folder for you). 14 | * 15 | *
Please note that this class is used ONLY if you opt-in for the New Architecture (see the 16 | * `newArchEnabled` property). Is ignored otherwise. 17 | */ 18 | public class MainApplicationTurboModuleManagerDelegate 19 | extends ReactPackageTurboModuleManagerDelegate { 20 | 21 | private static volatile boolean sIsSoLibraryLoaded; 22 | 23 | protected MainApplicationTurboModuleManagerDelegate( 24 | ReactApplicationContext reactApplicationContext, List packages) { 25 | super(reactApplicationContext, packages); 26 | } 27 | 28 | protected native HybridData initHybrid(); 29 | 30 | native boolean canCreateTurboModule(String moduleName); 31 | 32 | public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder { 33 | protected MainApplicationTurboModuleManagerDelegate build( 34 | ReactApplicationContext context, List packages) { 35 | return new MainApplicationTurboModuleManagerDelegate(context, packages); 36 | } 37 | } 38 | 39 | @Override 40 | protected synchronized void maybeLoadOtherSoLibraries() { 41 | if (!sIsSoLibraryLoaded) { 42 | // If you change the name of your application .so file in the Android.mk file, 43 | // make sure you update the name here as well. 44 | SoLoader.loadLibrary("demo_appmodules"); 45 | sIsSoLibraryLoaded = true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | # Define the library name here. 4 | project(demo_appmodules) 5 | 6 | # This file includes all the necessary to let you build your application with the New Architecture. 7 | include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake) 8 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationModuleProvider.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationModuleProvider.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace facebook { 7 | namespace react { 8 | 9 | std::shared_ptr MainApplicationModuleProvider( 10 | const std::string &moduleName, 11 | const JavaTurboModule::InitParams ¶ms) { 12 | // Here you can provide your own module provider for TurboModules coming from 13 | // either your application or from external libraries. The approach to follow 14 | // is similar to the following (for a library called `samplelibrary`: 15 | // 16 | // auto module = samplelibrary_ModuleProvider(moduleName, params); 17 | // if (module != nullptr) { 18 | // return module; 19 | // } 20 | // return rncore_ModuleProvider(moduleName, params); 21 | 22 | // Module providers autolinked by RN CLI 23 | auto rncli_module = rncli_ModuleProvider(moduleName, params); 24 | if (rncli_module != nullptr) { 25 | return rncli_module; 26 | } 27 | 28 | return rncore_ModuleProvider(moduleName, params); 29 | } 30 | 31 | } // namespace react 32 | } // namespace facebook 33 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationModuleProvider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | std::shared_ptr MainApplicationModuleProvider( 12 | const std::string &moduleName, 13 | const JavaTurboModule::InitParams ¶ms); 14 | 15 | } // namespace react 16 | } // namespace facebook 17 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationTurboModuleManagerDelegate.h" 2 | #include "MainApplicationModuleProvider.h" 3 | 4 | namespace facebook { 5 | namespace react { 6 | 7 | jni::local_ref 8 | MainApplicationTurboModuleManagerDelegate::initHybrid( 9 | jni::alias_ref) { 10 | return makeCxxInstance(); 11 | } 12 | 13 | void MainApplicationTurboModuleManagerDelegate::registerNatives() { 14 | registerHybrid({ 15 | makeNativeMethod( 16 | "initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid), 17 | makeNativeMethod( 18 | "canCreateTurboModule", 19 | MainApplicationTurboModuleManagerDelegate::canCreateTurboModule), 20 | }); 21 | } 22 | 23 | std::shared_ptr 24 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 25 | const std::string &name, 26 | const std::shared_ptr &jsInvoker) { 27 | // Not implemented yet: provide pure-C++ NativeModules here. 28 | return nullptr; 29 | } 30 | 31 | std::shared_ptr 32 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 33 | const std::string &name, 34 | const JavaTurboModule::InitParams ¶ms) { 35 | return MainApplicationModuleProvider(name, params); 36 | } 37 | 38 | bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule( 39 | const std::string &name) { 40 | return getTurboModule(name, nullptr) != nullptr || 41 | getTurboModule(name, {.moduleName = name}) != nullptr; 42 | } 43 | 44 | } // namespace react 45 | } // namespace facebook 46 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | namespace facebook { 8 | namespace react { 9 | 10 | class MainApplicationTurboModuleManagerDelegate 11 | : public jni::HybridClass< 12 | MainApplicationTurboModuleManagerDelegate, 13 | TurboModuleManagerDelegate> { 14 | public: 15 | // Adapt it to the package you used for your Java class. 16 | static constexpr auto kJavaDescriptor = 17 | "Lcom/demo/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;"; 18 | 19 | static jni::local_ref initHybrid(jni::alias_ref); 20 | 21 | static void registerNatives(); 22 | 23 | std::shared_ptr getTurboModule( 24 | const std::string &name, 25 | const std::shared_ptr &jsInvoker) override; 26 | std::shared_ptr getTurboModule( 27 | const std::string &name, 28 | const JavaTurboModule::InitParams ¶ms) override; 29 | 30 | /** 31 | * Test-only method. Allows user to verify whether a TurboModule can be 32 | * created by instances of this class. 33 | */ 34 | bool canCreateTurboModule(const std::string &name); 35 | }; 36 | 37 | } // namespace react 38 | } // namespace facebook 39 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainComponentsRegistry.cpp: -------------------------------------------------------------------------------- 1 | #include "MainComponentsRegistry.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace facebook { 10 | namespace react { 11 | 12 | MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {} 13 | 14 | std::shared_ptr 15 | MainComponentsRegistry::sharedProviderRegistry() { 16 | auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry(); 17 | 18 | // Autolinked providers registered by RN CLI 19 | rncli_registerProviders(providerRegistry); 20 | 21 | // Custom Fabric Components go here. You can register custom 22 | // components coming from your App or from 3rd party libraries here. 23 | // 24 | // providerRegistry->add(concreteComponentDescriptorProvider< 25 | // AocViewerComponentDescriptor>()); 26 | return providerRegistry; 27 | } 28 | 29 | jni::local_ref 30 | MainComponentsRegistry::initHybrid( 31 | jni::alias_ref, 32 | ComponentFactory *delegate) { 33 | auto instance = makeCxxInstance(delegate); 34 | 35 | auto buildRegistryFunction = 36 | [](EventDispatcher::Weak const &eventDispatcher, 37 | ContextContainer::Shared const &contextContainer) 38 | -> ComponentDescriptorRegistry::Shared { 39 | auto registry = MainComponentsRegistry::sharedProviderRegistry() 40 | ->createComponentDescriptorRegistry( 41 | {eventDispatcher, contextContainer}); 42 | 43 | auto mutableRegistry = 44 | std::const_pointer_cast(registry); 45 | 46 | mutableRegistry->setFallbackComponentDescriptor( 47 | std::make_shared( 48 | ComponentDescriptorParameters{ 49 | eventDispatcher, contextContainer, nullptr})); 50 | 51 | return registry; 52 | }; 53 | 54 | delegate->buildRegistryFunction = buildRegistryFunction; 55 | return instance; 56 | } 57 | 58 | void MainComponentsRegistry::registerNatives() { 59 | registerHybrid({ 60 | makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid), 61 | }); 62 | } 63 | 64 | } // namespace react 65 | } // namespace facebook 66 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/MainComponentsRegistry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | class MainComponentsRegistry 12 | : public facebook::jni::HybridClass { 13 | public: 14 | // Adapt it to the package you used for your Java class. 15 | constexpr static auto kJavaDescriptor = 16 | "Lcom/demo/newarchitecture/components/MainComponentsRegistry;"; 17 | 18 | static void registerNatives(); 19 | 20 | MainComponentsRegistry(ComponentFactory *delegate); 21 | 22 | private: 23 | static std::shared_ptr 24 | sharedProviderRegistry(); 25 | 26 | static jni::local_ref initHybrid( 27 | jni::alias_ref, 28 | ComponentFactory *delegate); 29 | }; 30 | 31 | } // namespace react 32 | } // namespace facebook 33 | -------------------------------------------------------------------------------- /demo/android/app/src/main/jni/OnLoad.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "MainApplicationTurboModuleManagerDelegate.h" 3 | #include "MainComponentsRegistry.h" 4 | 5 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { 6 | return facebook::jni::initialize(vm, [] { 7 | facebook::react::MainApplicationTurboModuleManagerDelegate:: 8 | registerNatives(); 9 | facebook::react::MainComponentsRegistry::registerNatives(); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | demo 3 | 4 | -------------------------------------------------------------------------------- /demo/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "33.0.0" 6 | minSdkVersion = 21 7 | compileSdkVersion = 33 8 | targetSdkVersion = 33 9 | 10 | if (System.properties['os.arch'] == "aarch64") { 11 | // For M1 Users we need to use the NDK 24 which added support for aarch64 12 | ndkVersion = "24.0.8215888" 13 | } else { 14 | // Otherwise we default to the side-by-side NDK version from AGP. 15 | ndkVersion = "21.4.7075529" 16 | } 17 | } 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | dependencies { 23 | classpath("com.android.tools.build:gradle:7.2.1") 24 | classpath("com.facebook.react:react-native-gradle-plugin") 25 | classpath("de.undercouch:gradle-download-task:5.0.1") 26 | // NOTE: Do not place your application dependencies here; they belong 27 | // in the individual module build.gradle files 28 | } 29 | } 30 | 31 | allprojects { 32 | repositories { 33 | maven { 34 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 35 | url("$rootDir/../node_modules/react-native/android") 36 | } 37 | maven { 38 | // Android JSC is installed from npm 39 | url("$rootDir/../node_modules/jsc-android/dist") 40 | } 41 | mavenCentral { 42 | // We don't want to fetch react-native from Maven Central as there are 43 | // older versions over there. 44 | content { 45 | excludeGroup "com.facebook.react" 46 | } 47 | } 48 | google() 49 | maven { url 'https://www.jitpack.io' } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /demo/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.125.0 29 | 30 | # Use this property to specify which architecture you want to build. 31 | # You can also override it from the CLI using 32 | # ./gradlew -PreactNativeArchitectures=x86_64 33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 34 | 35 | # Use this property to enable support to the new architecture. 36 | # This will allow you to use TurboModules and the Fabric render in 37 | # your application. You should enable this flag either if you want 38 | # to write custom TurboModules/Fabric components OR use libraries that 39 | # are providing them. 40 | newArchEnabled=false 41 | -------------------------------------------------------------------------------- /demo/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /demo/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /demo/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/react-native-gradle-plugin') 5 | 6 | if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") { 7 | include(":ReactAndroid") 8 | project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid') 9 | include(":ReactAndroid:hermes-engine") 10 | project(":ReactAndroid:hermes-engine").projectDir = file('../node_modules/react-native/ReactAndroid/hermes-engine') 11 | } 12 | -------------------------------------------------------------------------------- /demo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "displayName": "demo" 4 | } -------------------------------------------------------------------------------- /demo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './App'; 7 | import AppWithNavigation from './AppWithNavigation'; 8 | import {name as appName} from './app.json'; 9 | 10 | const snapshots = true; 11 | const checkNavigation = false 12 | 13 | if (snapshots) { 14 | require('./indexSnapshot'); 15 | } else if (checkNavigation) { 16 | AppRegistry.registerComponent(appName, () => AppWithNavigation); 17 | } else { 18 | AppRegistry.registerComponent(appName, () => App); 19 | } 20 | -------------------------------------------------------------------------------- /demo/indexSnapshot.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, Text, View } from 'react-native'; 3 | import { WebView } from 'react-native-webview'; 4 | import { registerSnapshot, runSnapshots, Snapshot } from 'pixels-catcher'; 5 | import { NavigationContainer } from '@react-navigation/native'; 6 | import { createStackNavigator } from '@react-navigation/stack'; 7 | 8 | import App from './App'; 9 | import { HomeScreen } from './AppWithNavigation'; 10 | import { name as appName } from './app.json'; 11 | 12 | const baseUrl = Platform.select({ 13 | // Put real IP of your server to run on real device 14 | android: 'http://10.0.2.2:3000', 15 | ios: 'http://127.0.0.1:3000', 16 | }); 17 | const useFailedTest = false; 18 | 19 | const appSnapshot = false; 20 | 21 | if (appSnapshot) { 22 | registerSnapshot( 23 | class SnapshotClass extends Snapshot { 24 | static snapshotName = 'AppSnapshot'; 25 | 26 | componentDidMount() { 27 | setTimeout(() => { 28 | // delay for rendering images 29 | this.props.onReady(); 30 | }, 1000); 31 | } 32 | 33 | renderContent() { 34 | return ; 35 | } 36 | }, 37 | ); 38 | } 39 | 40 | registerSnapshot( 41 | class SnapshotClass extends Snapshot { 42 | static snapshotName = 'HomeScreen'; 43 | 44 | renderContent() { 45 | return ; 46 | } 47 | }, 48 | ); 49 | 50 | if (useFailedTest) { 51 | registerSnapshot( 52 | class SnapshotClass extends Snapshot { 53 | static snapshotName = 'AppSnapshotWithWrongRefImg'; 54 | 55 | renderContent() { 56 | return ; 57 | } 58 | }, 59 | ); 60 | } 61 | 62 | registerSnapshot( 63 | class SnapshotClass extends Snapshot { 64 | static snapshotName = 'someComponent'; 65 | 66 | renderContent() { 67 | return ( 68 | 69 | Some component 70 | 71 | ); 72 | } 73 | }, 74 | ); 75 | 76 | const useWebView = false; 77 | 78 | if (useWebView) { 79 | registerSnapshot( 80 | class SnapshotClass extends Snapshot { 81 | static snapshotName = 'WebViewTest'; 82 | 83 | componentDidMount() { 84 | // override default componentDidMount from Snapshot to delay it 85 | // until WebView is loaded. onLoad from WebView is used 86 | } 87 | 88 | renderContent() { 89 | return ( 90 | { 96 | setTimeout(() => { 97 | this.props.onReady(); 98 | }, 50); 99 | }} 100 | /> 101 | ); 102 | } 103 | }, 104 | ); 105 | } 106 | 107 | registerSnapshot( 108 | class SnapshotClass extends Snapshot { 109 | static snapshotName = 'longContent'; 110 | 111 | renderContent() { 112 | return ( 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | ); 121 | } 122 | }, 123 | ); 124 | 125 | const Stack = createStackNavigator(); 126 | 127 | function getRootElement(SnapshotsContainer) { 128 | const RootElement = ({ children }) => ( 129 | 130 | 131 | 139 | 140 | 141 | ); 142 | return RootElement; 143 | } 144 | 145 | runSnapshots(appName, { baseUrl, getRootElement }); 146 | -------------------------------------------------------------------------------- /demo/ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, '12.4' 5 | install! 'cocoapods', :deterministic_uuids => false 6 | 7 | target 'demo' do 8 | config = use_native_modules! 9 | 10 | # Flags change depending on the env values. 11 | flags = get_default_flags() 12 | 13 | use_react_native!( 14 | :path => config[:reactNativePath], 15 | # Hermes is now enabled by default. Disable by setting this flag to false. 16 | # Upcoming versions of React Native may rely on get_default_flags(), but 17 | # we make it explicit here to aid in the React Native upgrade process. 18 | :hermes_enabled => true, 19 | :fabric_enabled => flags[:fabric_enabled], 20 | # Enables Flipper. 21 | # 22 | # Note that if you have use_frameworks! enabled, Flipper will not work and 23 | # you should disable the next line. 24 | # :flipper_configuration => FlipperConfiguration.enabled, 25 | # An absolute path to your application root. 26 | :app_path => "#{Pod::Config.instance.installation_root}/.." 27 | ) 28 | 29 | target 'demoTests' do 30 | inherit! :complete 31 | # Pods for testing 32 | end 33 | 34 | post_install do |installer| 35 | react_native_post_install( 36 | installer, 37 | # Set `mac_catalyst_enabled` to `true` in order to apply patches 38 | # necessary for Mac Catalyst builds 39 | :mac_catalyst_enabled => false 40 | ) 41 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /demo/ios/_xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /demo/ios/demo.xcodeproj/xcshareddata/xcschemes/demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /demo/ios/demo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/ios/demo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/ios/demo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : UIResponder 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /demo/ios/demo/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | #import 8 | 9 | #if RCT_NEW_ARCH_ENABLED 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | 17 | #import 18 | 19 | static NSString *const kRNConcurrentRoot = @"concurrentRoot"; 20 | 21 | @interface AppDelegate () { 22 | RCTTurboModuleManager *_turboModuleManager; 23 | RCTSurfacePresenterBridgeAdapter *_bridgeAdapter; 24 | std::shared_ptr _reactNativeConfig; 25 | facebook::react::ContextContainer::Shared _contextContainer; 26 | } 27 | @end 28 | #endif 29 | 30 | @implementation AppDelegate 31 | 32 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 33 | { 34 | RCTAppSetupPrepareApp(application); 35 | 36 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 37 | 38 | #if RCT_NEW_ARCH_ENABLED 39 | _contextContainer = std::make_shared(); 40 | _reactNativeConfig = std::make_shared(); 41 | _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); 42 | _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; 43 | bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; 44 | #endif 45 | 46 | NSDictionary *initProps = [self prepareInitialProps]; 47 | UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"demo", initProps); 48 | 49 | if (@available(iOS 13.0, *)) { 50 | rootView.backgroundColor = [UIColor systemBackgroundColor]; 51 | } else { 52 | rootView.backgroundColor = [UIColor whiteColor]; 53 | } 54 | 55 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 56 | UIViewController *rootViewController = [UIViewController new]; 57 | rootViewController.view = rootView; 58 | self.window.rootViewController = rootViewController; 59 | [self.window makeKeyAndVisible]; 60 | return YES; 61 | } 62 | 63 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. 64 | /// 65 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html 66 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). 67 | /// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`. 68 | - (BOOL)concurrentRootEnabled 69 | { 70 | // Switch this bool to turn on and off the concurrent root 71 | return true; 72 | } 73 | 74 | - (NSDictionary *)prepareInitialProps 75 | { 76 | NSMutableDictionary *initProps = [NSMutableDictionary new]; 77 | 78 | #ifdef RCT_NEW_ARCH_ENABLED 79 | initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]); 80 | #endif 81 | 82 | return initProps; 83 | } 84 | 85 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 86 | { 87 | #if DEBUG 88 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 89 | #else 90 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 91 | #endif 92 | } 93 | 94 | #if RCT_NEW_ARCH_ENABLED 95 | 96 | #pragma mark - RCTCxxBridgeDelegate 97 | 98 | - (std::unique_ptr)jsExecutorFactoryForBridge:(RCTBridge *)bridge 99 | { 100 | _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge 101 | delegate:self 102 | jsInvoker:bridge.jsCallInvoker]; 103 | return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager); 104 | } 105 | 106 | #pragma mark RCTTurboModuleManagerDelegate 107 | 108 | - (Class)getModuleClassFromName:(const char *)name 109 | { 110 | return RCTCoreModulesClassProvider(name); 111 | } 112 | 113 | - (std::shared_ptr)getTurboModule:(const std::string &)name 114 | jsInvoker:(std::shared_ptr)jsInvoker 115 | { 116 | return nullptr; 117 | } 118 | 119 | - (std::shared_ptr)getTurboModule:(const std::string &)name 120 | initParams: 121 | (const facebook::react::ObjCTurboModule::InitParams &)params 122 | { 123 | return nullptr; 124 | } 125 | 126 | - (id)getModuleInstanceFromClass:(Class)moduleClass 127 | { 128 | return RCTAppSetupDefaultModuleFromClass(moduleClass); 129 | } 130 | 131 | #endif 132 | 133 | @end 134 | -------------------------------------------------------------------------------- /demo/ios/demo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /demo/ios/demo/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/ios/demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | demo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | localhost 32 | 33 | NSExceptionAllowsInsecureHTTPLoads 34 | 35 | 36 | 37 | 38 | NSLocationWhenInUseUsageDescription 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /demo/ios/demo/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/ios/demo/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/ios/demoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/ios/demoTests/demoTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface demoTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation demoTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /demo/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: true, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "cd ios/ && pod install", 7 | "android": "react-native run-android", 8 | "preinstall": "cd .. && rm -rf pixels-catcher-*.tgz && npm i && npm pack && mv pixels-catcher-*.tgz pixels-catcher.tgz", 9 | "ios": "react-native run-ios", 10 | "start": "react-native start", 11 | "test": "jest", 12 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx" 13 | }, 14 | "dependencies": { 15 | "@react-navigation/native": "^6.0.12", 16 | "@react-navigation/stack": "^6.2.3", 17 | "react": "18.1.0", 18 | "react-native": "0.70.0", 19 | "react-native-gesture-handler": "^2.6.0", 20 | "react-native-safe-area-context": "^4.3.3", 21 | "react-native-save-view": "^0.2.3", 22 | "react-native-screens": "^3.17.0", 23 | "react-native-webview": "^11.23.1" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.12.9", 27 | "@babel/runtime": "^7.12.5", 28 | "@react-native-community/eslint-config": "^2.0.0", 29 | "@tsconfig/react-native": "^2.0.2", 30 | "@types/jest": "^26.0.23", 31 | "@types/react-native": "^0.70.0", 32 | "@types/react-test-renderer": "^18.0.0", 33 | "@typescript-eslint/eslint-plugin": "^5.37.0", 34 | "@typescript-eslint/parser": "^5.37.0", 35 | "babel-jest": "^26.6.3", 36 | "eslint": "^7.32.0", 37 | "jest": "^26.6.3", 38 | "metro-react-native-babel-preset": "^0.72.1", 39 | "pixels-catcher": "../pixels-catcher.tgz", 40 | "react-test-renderer": "18.1.0", 41 | "typescript": "^4.8.3" 42 | }, 43 | "jest": { 44 | "preset": "react-native", 45 | "moduleFileExtensions": [ 46 | "ts", 47 | "tsx", 48 | "js", 49 | "jsx", 50 | "json", 51 | "node" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo/pixels-catcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "android": { 3 | "activityName": "com.demo.MainActivity", 4 | "deviceName": "Nexus_5X", 5 | "packageName": "com.demo", 6 | "snapshotsPath": "./snapshots/android", 7 | "dev": { 8 | "deviceParams": [ 9 | "-no-audio", 10 | "-no-snapshot" 11 | ] 12 | }, 13 | "debug": { 14 | "deviceParams": [ 15 | "-no-audio", 16 | "-no-snapshot" 17 | ], 18 | "canStopDevice": false, 19 | "appFile": "./android/app/build/outputs/apk/debug/app-debug.apk" 20 | }, 21 | "test": { 22 | "port": 3000, 23 | "canStopDevice": false, 24 | "deviceParams": [ 25 | "-no-audio", 26 | "-no-snapshot" 27 | ], 28 | "appFile": "./android/app/build/outputs/apk/debug/app-debug.apk" 29 | }, 30 | "release": { 31 | "deviceParams": [ 32 | "-no-audio", 33 | "-no-snapshot", 34 | "-no-window" 35 | ], 36 | "appFile": "./android/app/build/outputs/apk/release/app-release.apk" 37 | } 38 | }, 39 | "ios": { 40 | "deviceName": "iPhone 14 Plus", 41 | "packageName": "org.reactjs.native.example.demo", 42 | "snapshotsPath": "./snapshots/ios", 43 | "dev": {}, 44 | "debug": { 45 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app" 46 | }, 47 | "test": { 48 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app", 49 | "canStopDevice": false 50 | }, 51 | "testPort": {}, 52 | "testPort2": { 53 | "port": 3001 54 | }, 55 | "debugIphone6": { 56 | "deviceName": "iPhone 6", 57 | "snapshotsPath": "./snapshots/iPhone6", 58 | "appFile": "./ios/build/Build/Products/Debug-iphonesimulator/demo.app" 59 | } 60 | }, 61 | "logLevel": "v", 62 | "timeout": 30000 63 | } 64 | -------------------------------------------------------------------------------- /demo/run_android_debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | export BUNDLE_IN_DEBUG="true" 7 | 8 | cd android 9 | rm -rf build .gradle/ app/build 10 | ./gradlew assembleDebug 11 | cd .. 12 | 13 | ./node_modules/.bin/pixels-catcher android debug 14 | -------------------------------------------------------------------------------- /demo/run_android_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | 7 | cd android 8 | rm -rf build .gradle/ app/build 9 | ./gradlew assembleRelease 10 | cd .. 11 | 12 | ./node_modules/.bin/pixels-catcher android release 13 | -------------------------------------------------------------------------------- /demo/run_android_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export ENTRY_FILE="indexSnapshot.js" 6 | export BUNDLE_IN_DEBUG="true" 7 | 8 | cd android 9 | rm -rf build .gradle/ app/build 10 | ./gradlew assembleDebug 11 | cd .. 12 | 13 | ../node_modules/.bin/flow-node ../src/runner/cli.js android test 14 | -------------------------------------------------------------------------------- /demo/run_ios_debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | BUILD_PATH="./build" 11 | rm -rf $BUILD_PATH 12 | 13 | xcrun xcodebuild \ 14 | -scheme demo \ 15 | -workspace demo.xcworkspace \ 16 | -configuration Debug \ 17 | -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.2' \ 18 | -derivedDataPath $BUILD_PATH \ 19 | ENTRY_FILE="indexSnapshot.js" \ 20 | build 21 | 22 | cd .. 23 | 24 | ./node_modules/.bin/pixels-catcher ios debug 25 | -------------------------------------------------------------------------------- /demo/run_ios_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | echo "ERROR: Not implemented. Requires https" 11 | -------------------------------------------------------------------------------- /demo/run_ios_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | set -e 4 | 5 | export FORCE_BUNDLING=1 6 | export RCT_NO_LAUNCH_PACKAGER=1 7 | 8 | cd ios 9 | 10 | BUILD_PATH="./build" 11 | rm -rf $BUILD_PATH 12 | 13 | xcrun xcodebuild \ 14 | -scheme demo \ 15 | -workspace demo.xcworkspace \ 16 | -configuration Debug \ 17 | -destination 'platform=iOS Simulator,name=iPhone 14 Plus,OS=13.3' \ 18 | -derivedDataPath $BUILD_PATH \ 19 | ENTRY_FILE="indexSnapshot.js" \ 20 | build 21 | 22 | cd .. 23 | 24 | ../node_modules/.bin/flow-node ../src/runner/cli.js ios test 25 | -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/diffs/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/diffs/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/android/uploads/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/android/uploads/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/iPhone6/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/iPhone6/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/diffs/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/diffs/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/AppSnapshotWithWrongRefImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/AppSnapshotWithWrongRefImg.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/refImages/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/refImages/someComponent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/AppSnapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/AppSnapshot.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/HomeScreen.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/WebViewTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/WebViewTest.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/longContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/longContent.png -------------------------------------------------------------------------------- /demo/snapshots/ios/uploads/someComponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/demo/snapshots/ios/uploads/someComponent.png -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | { 3 | "extends": "@tsconfig/react-native/tsconfig.json", /* Recommended React Native TSConfig base */ 4 | "compilerOptions": { 5 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 6 | 7 | /* Completeness */ 8 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixels-catcher", 3 | "version": "0.13.1", 4 | "description": "UI snapshot testing for React Native", 5 | "main": "lib/client/index.js", 6 | "scripts": { 7 | "demo": "cd demo && ../node_modules/.bin/flow-node ../cli.js android debug", 8 | "lint": "eslint --ext .js --ext .ts --ext .tsx ./src/", 9 | "build": "./node_modules/.bin/tsc -p src/client && ./node_modules/.bin/tsc -p src/runner", 10 | "postbuild": "npm run assets", 11 | "prepare": "npm run build", 12 | "prepack": "npm run build", 13 | "check-types-client": "./node_modules/.bin/tsc --noEmit -p src/client", 14 | "check-types-runner": "./node_modules/.bin/tsc --noEmit -p src/runner", 15 | "check-types": "npm run check-types-client && npm run check-types-runner", 16 | "assets": "cp ./src/runner/server/dummy.png ./lib/runner/server/" 17 | }, 18 | "bin": "./lib/runner/cli.js", 19 | "files": [ 20 | "/lib", 21 | "/src/*.js", 22 | "/src/utils/*.js" 23 | ], 24 | "keywords": [ 25 | "react-native view android iOS UI screenshot snapshot testing" 26 | ], 27 | "author": "Maksym Rusynyk ", 28 | "license": "MIT", 29 | "engines": { 30 | "node": ">=14.*" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+ssh://git@github.com/rumax/react-native-PixelsCatcher.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/rumax/react-native-PixelsCatcher/issues" 38 | }, 39 | "homepage": "https://github.com/rumax/react-native-PixelsCatcher#readme", 40 | "dependencies": { 41 | "formidable": "^1.2.2", 42 | "pixelmatch": "^5.2.1", 43 | "pngjs": "^6.0.0", 44 | "react-native-save-view": "^0.2.3" 45 | }, 46 | "devDependencies": { 47 | "@types/formidable": "^1.2.2", 48 | "@types/node": "^18.7.16", 49 | "@types/pixelmatch": "^5.2.3", 50 | "@types/pngjs": "^6.0.0", 51 | "@types/react-native": "^0.64.6", 52 | "@typescript-eslint/eslint-plugin": "^4.25.0", 53 | "@typescript-eslint/parser": "^4.25.0", 54 | "eslint": "^7.27.0", 55 | "eslint-config-airbnb": "^18.2.1", 56 | "eslint-plugin-import": "^2.23.3", 57 | "eslint-plugin-jsx-a11y": "^6.4.1", 58 | "eslint-plugin-react": "^7.23.2", 59 | "eslint-plugin-react-hooks": "^4.2.0", 60 | "react": "17.0.1", 61 | "react-native": "0.64.1", 62 | "react-test-renderer": "17.0.1", 63 | "typescript": "^4.2.4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /res/azureDevops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/res/azureDevops.png -------------------------------------------------------------------------------- /res/testResults.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/res/testResults.png -------------------------------------------------------------------------------- /scripts/create_android_emulator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DEVICE="Nexus 5X" 4 | DEVICE_NAME="Nexus_5X" 5 | SDK="system-images;android-27;default;x86_64" 6 | 7 | # Install AVD files 8 | echo "y" | $ANDROID_HOME/tools/bin/sdkmanager \ 9 | --install $SDK 10 | 11 | # Create emulator 12 | echo "no" | $ANDROID_HOME/tools/bin/avdmanager \ 13 | create avd \ 14 | -n "$DEVICE_NAME" \ 15 | --device "$DEVICE" \ 16 | -k $SDK \ 17 | --force 18 | 19 | $ANDROID_HOME/emulator/emulator -list-avds 20 | 21 | echo "Starting emulator" 22 | 23 | # Start emulator in background 24 | nohup $ANDROID_HOME/emulator/emulator \ 25 | -avd $DEVICE_NAME \ 26 | -no-snapshot > /dev/null 2>&1 & 27 | $ANDROID_HOME/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82' 28 | 29 | $ANDROID_HOME/platform-tools/adb devices 30 | 31 | echo "Emulator started" 32 | -------------------------------------------------------------------------------- /src/client/Snapshot.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | // eslint-disable-next-line no-use-before-define 8 | import React, { Component } from 'react'; 9 | import { InteractionManager, ScrollView } from 'react-native'; 10 | 11 | import log from './utils/log'; 12 | 13 | type Props = { onReady: () => void }; 14 | 15 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOT'; 16 | const ERROR_NO_IMPLEMENTED = 17 | 'Not implemented. Should be implemented by actual snapshot'; 18 | 19 | export default class Snapshot extends Component { 20 | // Should be implemented by actual snapshot 21 | static snapshotName: string = ''; 22 | 23 | componentDidMount(): void { 24 | log.v(TAG, 'Awaiting interaction'); 25 | const startTime = new Date().getTime(); 26 | InteractionManager.runAfterInteractions(() => { 27 | const time = new Date().getTime() - startTime; 28 | log.v(TAG, `Interaction completed in ${time} milliseconds`); 29 | global.setTimeout(() => { 30 | this.props.onReady(); 31 | }, 50); 32 | }); 33 | } 34 | 35 | renderContent(): React.ReactNode { 36 | log.e(TAG, ERROR_NO_IMPLEMENTED); 37 | throw new Error(ERROR_NO_IMPLEMENTED); 38 | } 39 | 40 | render(): React.ReactNode { 41 | const content = this.renderContent(); 42 | return ( 43 | 44 | {content} 45 | 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/client/SnapshotsContainer.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | // eslint-disable-next-line no-use-before-define 8 | import React, { Component } from 'react'; 9 | import { View, Text } from 'react-native'; 10 | // @ts-ignore 11 | import SaveView from 'react-native-save-view'; 12 | 13 | import { getNextSnapshot } from './snapshotsManager'; 14 | import compareToReference from './utils/compareToReference'; 15 | import log from './utils/log'; 16 | import network from './utils/network'; 17 | 18 | import type Snapshot from './Snapshot'; 19 | 20 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOTS_CONTAINER'; 21 | 22 | type NoProps = Record; 23 | 24 | type State = { 25 | isReady: boolean, 26 | ActiveSnapshot: typeof Snapshot | null, 27 | }; 28 | 29 | export default class SnapshotsContainer extends Component { 30 | _viewRef: any; 31 | 32 | _testStartedAt: number = new Date().getTime(); 33 | 34 | _renderStartedAt: number = 0; 35 | 36 | constructor(props: Record) { 37 | super(props); 38 | 39 | this.state = { 40 | ActiveSnapshot: null, 41 | isReady: false, 42 | }; 43 | } 44 | 45 | shouldComponentUpdate( 46 | nextProps: NoProps, 47 | nextState: Readonly, 48 | ): boolean { 49 | return this.state.ActiveSnapshot !== nextState.ActiveSnapshot 50 | || this.state.isReady !== nextState.isReady; 51 | } 52 | 53 | componentDidMount(): void { 54 | requestAnimationFrame(() => { 55 | this._startTesting(); 56 | }); 57 | } 58 | 59 | render(): React.ReactNode { 60 | const { isReady, ActiveSnapshot } = this.state; 61 | 62 | if (!isReady) { 63 | return ( 64 | 65 | Initializing tests 66 | 67 | ); 68 | } 69 | 70 | if (!ActiveSnapshot) { 71 | log.i(TAG, 'No active snapshot'); 72 | return null; 73 | } 74 | 75 | log.i(TAG, `rendering snapshot [${ActiveSnapshot.snapshotName}]`); 76 | 77 | this._renderStartedAt = new Date().getTime(); 78 | 79 | return ; 80 | } 81 | 82 | _startTesting = async (): Promise => { 83 | await network.initTests(); 84 | const ActiveSnapshot = getNextSnapshot(); 85 | if (!ActiveSnapshot) { 86 | this._endOfTest(); 87 | log.e(TAG, 'No snapshots registered'); 88 | this._endOfTest(); 89 | return; 90 | } 91 | log.v(TAG, 'Start testing'); 92 | this.setState({ 93 | ActiveSnapshot, 94 | isReady: true, 95 | }); 96 | }; 97 | 98 | _onRef = (ref: any): void => { 99 | this._viewRef = ref; 100 | }; 101 | 102 | _onSnapshotReady = (): void => { 103 | const renderTime = new Date().getTime() - this._renderStartedAt; 104 | log.v(TAG, 'Snapshot ready'); 105 | 106 | setTimeout(async () => { 107 | const ref = this._viewRef; 108 | 109 | if (!ref) { 110 | const errorMessage = 'Something when wrong, no ref to the component'; 111 | log.e(TAG, errorMessage); 112 | throw new Error(errorMessage); 113 | } 114 | 115 | const { ActiveSnapshot } = this.state; 116 | const name = ActiveSnapshot?.snapshotName; 117 | 118 | log.v(TAG, `snapshotName: [${name || '-'}]`); 119 | 120 | if (!name) { 121 | const errorMessage = 'Snapshot should has a proper name'; 122 | 123 | log.w(TAG, errorMessage); 124 | network.reportTest({ 125 | name: '-', 126 | failure: errorMessage, 127 | time: this._getTestExecutionTime(), 128 | }); 129 | this._nextSnapshot(); 130 | 131 | return; 132 | } 133 | 134 | let failure: any; 135 | 136 | try { 137 | log.v(TAG, '++SaveView.save'); 138 | const base64 = await SaveView.saveToPNGBase64(ref); 139 | log.v(TAG, `--SaveView.save, size is ${base64.length}`); 140 | 141 | failure = await compareToReference(name, base64); 142 | if (failure) { 143 | log.e(TAG, `Snapshot ${name} failed: ${failure}`); 144 | } else { 145 | log.i(TAG, `Snapshot ${name} passed`); 146 | } 147 | } catch (err: unknown) { 148 | failure = `Failed to save view: ${ 149 | err instanceof Error ? err.message : 'Unknown error' 150 | }`; 151 | log.e(TAG, failure); 152 | } 153 | 154 | log.v(TAG, `Reporting [${name}], failure: [${failure}]`); 155 | try { 156 | await network.reportTest({ 157 | name, 158 | failure, 159 | time: this._getTestExecutionTime(), 160 | renderTime, 161 | }); 162 | } catch (err) { 163 | log.e(TAG, 'Failed to report test', err); 164 | } 165 | this._nextSnapshot(); 166 | }, 50); 167 | }; 168 | 169 | _getTestExecutionTime(): number { 170 | const time = new Date().getTime() - this._testStartedAt; 171 | log.v(TAG, `Execution time: ${time}`); 172 | return time; 173 | } 174 | 175 | _nextSnapshot(): void { 176 | log.v(TAG, 'Trying to get next snapshot'); 177 | const NextSnapshot = getNextSnapshot(); 178 | 179 | if (!NextSnapshot) { 180 | log.v('No more snapshots left, exit testing'); 181 | this._endOfTest(); 182 | return; 183 | } 184 | 185 | log.v(`Switching to next snapshot ${NextSnapshot.snapshotName}`); 186 | this._testStartedAt = new Date().getTime(); 187 | this.setState({ ActiveSnapshot: NextSnapshot }); 188 | } 189 | 190 | _endOfTest(): void { 191 | network.endOfTests({ message: 'All tests completed' }); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/client/__tests__/Snapshot.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { View } from 'react-native'; 5 | import renderer from 'react-test-renderer'; 6 | 7 | import Snapshot from '../Snapshot'; 8 | 9 | jest.mock('../utils/log', () => ({ e: () => {}, v: () => {} })); 10 | global.console.error = () => {}; 11 | 12 | describe('Snapshot component', () => { 13 | const onReadyMock = jest.fn(); 14 | 15 | it('throws exception if renderContent is not implemented', () => { 16 | let exception; 17 | 18 | try { 19 | renderer.create(); 20 | } catch (err) { 21 | exception = err; 22 | } 23 | 24 | expect(exception).toMatchSnapshot(); 25 | }); 26 | 27 | it('renders snapshot component', () => { 28 | class SnapshotClass extends Snapshot { 29 | static snapshotName = 'AppSnapshot'; 30 | 31 | renderContent() { 32 | return ( 33 | 34 | ); 35 | } 36 | } 37 | 38 | const tree = renderer.create(); 39 | 40 | expect(tree).toMatchSnapshot(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/client/__tests__/SnapshotsContainer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | import SnapshotsContainer from '../SnapshotsContainer'; 7 | import network from '../utils/network'; 8 | import log from '../utils/log'; 9 | import { getNextSnapshot } from '../snapshotsManager'; 10 | 11 | jest.mock('../utils/log', () => ({ 12 | i: jest.fn(), 13 | v: jest.fn(), 14 | w: jest.fn(), 15 | e: jest.fn(), 16 | })); 17 | jest.mock('../utils/network', () => ({ endOfTests: jest.fn() })); 18 | jest.mock('../snapshotsManager', () => ({ getNextSnapshot: jest.fn() })); 19 | 20 | describe('SnapshotsContainer', () => { 21 | beforeEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | it('Renders empty view and reports it to the server if no snapshots are registered', () => { 26 | const tree = renderer.create(); 27 | 28 | expect(tree).toMatchSnapshot(); 29 | expect(network.endOfTests).toHaveBeenCalledTimes(1); 30 | expect(log.e).toMatchSnapshot('loggin error'); 31 | }); 32 | 33 | it('Renders registered snapshot', () => { 34 | getNextSnapshot.mockImplementationOnce(() => 'SomeSnapshot'); 35 | const tree = renderer.create(); 36 | 37 | expect(tree).toMatchSnapshot(); 38 | expect(network.endOfTests).toHaveBeenCalledTimes(0); 39 | expect(log.v).toMatchSnapshot('render snapshot reported'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/Snapshot.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot component renders snapshot component 1`] = ` 4 | 12 | 13 | 14 | 15 | 16 | `; 17 | 18 | exports[`Snapshot component renders snpashot component 1`] = ` 19 | 27 | 28 | 29 | 30 | 31 | `; 32 | 33 | exports[`Snapshot component throws exception if renderContent is not implemented 1`] = `[Error: Not implemented. Should be implemented by actual snapshot]`; 34 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/SnapshotsContainer.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SnapshotsContainer Renders empty view and reports it to the server if no snapshots are registered 1`] = `null`; 4 | 5 | exports[`SnapshotsContainer Renders empty view and reports it to the server if no snapshots are registered: loggin error 1`] = ` 6 | [MockFunction] { 7 | "calls": Array [ 8 | Array [ 9 | "PIXELS_CATCHER::APP::SNAPSHOTS_CONTAINER", 10 | "No snapshots registered", 11 | ], 12 | ], 13 | "results": Array [ 14 | Object { 15 | "type": "return", 16 | "value": undefined, 17 | }, 18 | ], 19 | } 20 | `; 21 | 22 | exports[`SnapshotsContainer Renders registered snapshot 1`] = ` 23 | 26 | `; 27 | 28 | exports[`SnapshotsContainer Renders registered snapshot: render snapshot reported 1`] = `[MockFunction]`; 29 | -------------------------------------------------------------------------------- /src/client/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot component register component and start snapshots: registerComponent 1`] = ` 4 | Array [ 5 | Array [ 6 | "appName", 7 | [Function], 8 | ], 9 | ] 10 | `; 11 | 12 | exports[`Snapshot component register component with custom IP and start snapshots: registerComponent 1`] = ` 13 | Array [ 14 | Array [ 15 | "appName", 16 | [Function], 17 | ], 18 | ] 19 | `; 20 | -------------------------------------------------------------------------------- /src/client/__tests__/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { AppRegistry, View } from 'react-native'; 5 | 6 | import { runSnapshots, Snapshot, registerSnapshot } from '../index'; 7 | import network from '../utils/network'; 8 | 9 | jest.mock('react-native/Libraries/ReactNative/AppRegistry', () => ({ registerComponent: jest.fn() })); 10 | jest.mock('../utils/network', () => ({ setBaseUrl: jest.fn() })); 11 | jest.mock('../SnapshotsContainer', () => 'SnapshotsContainer'); 12 | jest.mock('../utils/log', () => ({ i: jest.fn() })); 13 | jest.mock('../Snapshot', () => ({ default: 'Snapshot' })); 14 | jest.mock('../snapshotsManager', () => ({ registerSnapshot: jest.fn() })); 15 | 16 | describe('Snapshot component', () => { 17 | const appName = 'appName'; 18 | 19 | afterEach(() => { 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | it('provides Snapshot component', () => { 24 | expect(Snapshot).toBe('Snapshot'); 25 | }); 26 | 27 | it('allows to register snapshot', () => { 28 | registerSnapshot(); 29 | expect(registerSnapshot).toHaveBeenCalledTimes(1); 30 | }); 31 | 32 | it('register component and start snapshots', () => { 33 | runSnapshots(appName); 34 | 35 | expect(AppRegistry.registerComponent).toHaveBeenCalledTimes(1); 36 | expect(AppRegistry.registerComponent.mock.calls) 37 | .toMatchSnapshot('registerComponent'); 38 | expect(AppRegistry.registerComponent.mock.calls[0][1]()) 39 | .toBe('SnapshotsContainer'); 40 | expect(network.setBaseUrl).toHaveBeenCalledTimes(0); 41 | }); 42 | 43 | it('register component with custom IP and start snapshots', () => { 44 | const baseUrl = 'baseUrl'; 45 | runSnapshots(appName, { baseUrl }); 46 | 47 | expect(AppRegistry.registerComponent).toHaveBeenCalledTimes(1); 48 | expect(AppRegistry.registerComponent.mock.calls) 49 | .toMatchSnapshot('registerComponent'); 50 | expect(AppRegistry.registerComponent.mock.calls[0][1]()) 51 | .toBe('SnapshotsContainer'); 52 | expect(network.setBaseUrl).toHaveBeenCalledTimes(1); 53 | expect(network.setBaseUrl).toHaveBeenCalledWith(baseUrl); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/client/__tests__/snapshotsManager.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line no-use-before-define 3 | import React from 'react'; 4 | import { View, Text } from 'react-native'; 5 | 6 | import { registerSnapshot, getNextSnapshot } from '../snapshotsManager'; 7 | import Snapshot from '../Snapshot'; 8 | 9 | jest.mock('../utils/log', () => ({ i: jest.fn() })); 10 | 11 | describe('snapshotsManager', () => { 12 | it('Default snapshots list is empty', () => { 13 | const nextSnapshot = getNextSnapshot(); 14 | expect(nextSnapshot).toBe(undefined); 15 | }); 16 | 17 | it('register snapshot and get it', () => { 18 | class SnapshotClass extends Snapshot { 19 | static snapshotName = 'someComponent'; 20 | 21 | renderContent() { 22 | return ( 23 | Some component 24 | ); 25 | } 26 | } 27 | registerSnapshot(SnapshotClass); 28 | 29 | expect(getNextSnapshot()).toBe(SnapshotClass); 30 | expect(getNextSnapshot()).toBe(undefined); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type React from 'react'; 8 | import { AppRegistry } from 'react-native'; 9 | 10 | import log from './utils/log'; 11 | import network from './utils/network'; 12 | 13 | import SnapshotsContainer from './SnapshotsContainer'; 14 | 15 | export const Snapshot = require('./Snapshot').default; 16 | 17 | export const { registerSnapshot } = require('./snapshotsManager'); 18 | 19 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOT'; 20 | 21 | export type GetRootElementType = (element: React.ComponentType) => 22 | React.ComponentType 23 | 24 | interface ConfigType { 25 | baseUrl?: string; 26 | 27 | /** 28 | * Callback to override AppRegistry.registerComponent with custom 29 | * implementation. Can be used for projects with react-native-navigation 30 | * @param snapshot Current snapshot 31 | */ 32 | // eslint-disable-next-line no-unused-vars 33 | registerComponent?: (snapshot: typeof SnapshotsContainer) => void; 34 | 35 | /** 36 | * Root element. Allows to wrap the SnapshotsContainer, which could be 37 | * useful to implement some providers, for example for react navigation. 38 | * Example: 39 | * 40 | * import { NavigationContainer } from '@react-navigation/native'; 41 | * import { createStackNavigator } from '@react-navigation/stack'; 42 | * 43 | * const Stack = createStackNavigator(); 44 | * 45 | * function getRootElement(SnapshotsContainer) { 46 | * const RootElement = ({children}) => ( 47 | * 48 | * 49 | * 53 | * 54 | * 55 | * ) 56 | * return RootElement; 57 | * } 58 | * 59 | * runSnapshots(appName, { baseUrl, getRootElement }); 60 | */ 61 | getRootElement?: GetRootElementType; 62 | } 63 | 64 | export const runSnapshots = (appName: string, config: ConfigType = {}): void => { 65 | log.i(TAG, `Run snapshots for ${appName}`); 66 | log.i(TAG, `Config is:\n ${JSON.stringify(config, null, 2)}`); 67 | 68 | if (config.baseUrl) { 69 | network.setBaseUrl(config.baseUrl); 70 | } 71 | 72 | if (config.registerComponent) { 73 | config.registerComponent(SnapshotsContainer); 74 | return; 75 | } 76 | 77 | if (config.getRootElement) { 78 | const RootElement = config.getRootElement(SnapshotsContainer); 79 | AppRegistry.registerComponent(appName, () => RootElement); 80 | return; 81 | } 82 | 83 | AppRegistry.registerComponent(appName, () => SnapshotsContainer); 84 | }; 85 | -------------------------------------------------------------------------------- /src/client/snapshotsManager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type Snapshot from './Snapshot'; 8 | import log from './utils/log'; 9 | import network from './utils/network'; 10 | 11 | const snapshots: Array = []; 12 | const TAG = 'PIXELS_CATCHER::APP::SNAPSHOTS_MANAGER'; 13 | 14 | 15 | export function registerSnapshot(Component: typeof Snapshot): void { 16 | log.i(TAG, `Registering snapshot [${Component.snapshotName}]`); 17 | snapshots.push(Component); 18 | network.registerTest(Component.snapshotName); 19 | } 20 | 21 | 22 | export function getNextSnapshot(): typeof Snapshot | undefined { 23 | const NextSnapshot = snapshots.shift(); 24 | return NextSnapshot; 25 | } 26 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es6", 5 | "declaration": true, 6 | "outDir": "../../lib/client", 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true 11 | }, 12 | "include": ["./**/*"], 13 | "exclude": [ 14 | "../../node_modules", 15 | "**/__tests__/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/client/utils/__tests__/__snapshots__/compareToReference.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`compareToReference Returns failure if HTTP status is not 200 1`] = `"Invalid status 404"`; 4 | 5 | exports[`compareToReference Returns failure if result is not OK 1`] = `"Files mismatch with 1 pixels"`; 6 | -------------------------------------------------------------------------------- /src/client/utils/__tests__/compareToReference.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import network from '../network'; 3 | import compareToReference from '../compareToReference'; 4 | 5 | jest.mock('../network', () => ({ postBase64: jest.fn() })); 6 | 7 | describe('compareToReference', () => { 8 | it('Returns failure if HTTP status is not 200', async () => { 9 | const snapshotName = 'snapshotName'; 10 | const base64 = 'base64 data'; 11 | 12 | network.postBase64.mockImplementationOnce(() => ({ status: 404 })); 13 | 14 | const failure = await compareToReference(snapshotName, base64); 15 | expect(failure).toMatchSnapshot(); 16 | }); 17 | 18 | it('Returns failure if result is not OK', async () => { 19 | const snapshotName = 'snapshotName'; 20 | const base64 = 'base64 data'; 21 | 22 | network.postBase64.mockImplementationOnce(() => ({ 23 | status: 200, 24 | json: async () => ({ 25 | result: 'ERROR', 26 | info: 'Files mismatch with 1 pixels', 27 | }), 28 | })); 29 | 30 | const failure = await compareToReference(snapshotName, base64); 31 | expect(failure).toMatchSnapshot(); 32 | }); 33 | 34 | it('Returns nothing if image matches the reference', async () => { 35 | const snapshotName = 'snapshotName'; 36 | const base64 = 'base64 data'; 37 | 38 | network.postBase64.mockImplementationOnce(() => ({ 39 | status: 200, 40 | json: async () => ({ 41 | result: 'OK', 42 | info: { differentPixelsCount: 0 }, 43 | }), 44 | })); 45 | 46 | const result = await compareToReference(snapshotName, base64); 47 | expect(result).toBe(undefined); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/client/utils/compareToReference.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import network from './network'; 8 | 9 | const compareToReference = async (snapshotName: string, base64: string): Promise => { 10 | const response: any = await network.postBase64({ 11 | base64, 12 | fileName: `${snapshotName}.png`, 13 | }); 14 | 15 | if (response.status !== 200) { 16 | return `Invalid status ${response.status}`; 17 | } 18 | 19 | const responseJSON = await response.json(); 20 | 21 | if (responseJSON.result !== 'OK') { 22 | return responseJSON.info; 23 | } 24 | 25 | return undefined; 26 | }; 27 | 28 | export default compareToReference; 29 | -------------------------------------------------------------------------------- /src/client/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import network from './network'; 8 | 9 | const consoleLog = global.console && global.console.log 10 | ? global.console.log : (): void => {}; 11 | 12 | type LogLevelType = 'v' | 'd' | 'i' | 'w' | 'e'; 13 | 14 | const serverLog = async (logLevel: LogLevelType, tag: string, ...args: any): Promise => { 15 | try { 16 | network.serverLog({ 17 | logLevel, 18 | tag, 19 | args, 20 | }); 21 | } catch (err) { 22 | if (__DEV__) { 23 | // eslint-disable-next-line no-console 24 | console.warn('ERROR:serverLog: ', err instanceof Error ? err.message : 'Unknown error', err); 25 | } 26 | } 27 | }; 28 | 29 | const log = { 30 | v: (tag: string, ...args: any): void => { 31 | consoleLog(tag, ...args); 32 | serverLog('v', tag, ...args); 33 | }, 34 | 35 | d: (tag: string, ...args: any): void => { 36 | consoleLog(tag, ...args); 37 | serverLog('d', tag, ...args); 38 | }, 39 | 40 | i: (tag: string, ...args: any): void => { 41 | consoleLog(tag, ...args); 42 | serverLog('i', tag, ...args); 43 | }, 44 | 45 | w: (tag: string, ...args: any): void => { 46 | consoleLog(`${tag} WARNING:`, ...args); 47 | serverLog('w', `${tag} WARNING:`, ...args); 48 | }, 49 | 50 | e: (tag: string, ...args: any): void => { 51 | consoleLog(`${tag} ERROR:`, ...args); 52 | serverLog('e', `${tag} ERROR:`, ...args); 53 | }, 54 | }; 55 | 56 | export default log; 57 | -------------------------------------------------------------------------------- /src/client/utils/network.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import { Platform } from 'react-native'; 8 | 9 | let baseUrl = Platform.select({ 10 | android: 'http://10.0.2.2:3000', 11 | ios: 'http://127.0.0.1:3000', 12 | }); 13 | 14 | type TestCaseType = { 15 | name: string, 16 | failure?: string, 17 | isSkipped?: boolean, 18 | time: number, 19 | renderTime?: number, 20 | }; 21 | 22 | const fetchRequest = async (url: string, body: Object): Promise => { 23 | const response = await fetch(url, { 24 | method: 'POST', 25 | headers: { 26 | Accept: 'application/json', 27 | 'Content-Type': 'application/json', 28 | }, 29 | body: JSON.stringify(body), 30 | }); 31 | 32 | return response; 33 | }; 34 | 35 | 36 | export default { 37 | 38 | setBaseUrl(url: string): void { 39 | baseUrl = url; 40 | }, 41 | 42 | 43 | initTests: async (): Promise => { 44 | await fetchRequest(`${baseUrl}/initTests`, {}); 45 | }, 46 | 47 | 48 | registerTest: async (name: string): Promise => { 49 | await fetchRequest(`${baseUrl}/registerTest`, { name }); 50 | }, 51 | 52 | 53 | postBase64: async (body: Object): Promise => { 54 | const response = await fetchRequest(`${baseUrl}/base64`, body); 55 | return response; 56 | }, 57 | 58 | 59 | serverLog: async (body: Object): Promise => { 60 | await fetchRequest(`${baseUrl}/log`, body); 61 | }, 62 | 63 | 64 | reportTest: async (testCase: TestCaseType): Promise => { 65 | await fetchRequest(`${baseUrl}/reportTest`, testCase); 66 | }, 67 | 68 | 69 | endOfTests: async (body: Object): Promise => { 70 | await fetchRequest(`${baseUrl}/endOfTests`, body); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/runner/TestsRunner.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import log from './utils/log'; 5 | import Reporter from './utils/Reporter'; 6 | import server from './server/server'; 7 | 8 | const TAG = 'PIXELS_CATCHER'; 9 | 10 | type TestsRunnerParamsType = 11 | { 12 | activityName: string, 13 | appFile: string, 14 | device: any, 15 | deviceName: string, 16 | deviceParams: string, 17 | isDevMode: boolean, 18 | locale: string, 19 | packageName: string, 20 | platform: 'ios' | 'android', 21 | port: number, 22 | snapshotsPath: string, 23 | testRunName: string, 24 | timeout: number, 25 | }; 26 | 27 | class TestsRunner { 28 | _activityName: string; 29 | 30 | _appFile: string; 31 | 32 | _appFileFullPath: string | void; 33 | 34 | _device: any; 35 | 36 | _deviceName: string; 37 | 38 | _deviceParams: Object; 39 | 40 | _isDevMode: boolean; 41 | 42 | _locale: string; 43 | 44 | _packageName: string; 45 | 46 | _platform: 'ios' | 'android'; 47 | 48 | _port: number; 49 | 50 | _reporter: Reporter; 51 | 52 | _snapshotsPath: string; 53 | 54 | _stopByTimeoutID: ReturnType | void; 55 | 56 | _timeout: number; 57 | 58 | 59 | constructor(params: TestsRunnerParamsType) { 60 | this._activityName = params.activityName; 61 | this._appFile = params.appFile; 62 | this._device = params.device; 63 | this._deviceName = params.deviceName; 64 | this._deviceParams = params.deviceParams; 65 | this._isDevMode = params.isDevMode; 66 | this._locale = params.locale; 67 | this._packageName = params.packageName; 68 | this._platform = params.platform; 69 | this._port = params.port; 70 | this._snapshotsPath = params.snapshotsPath; 71 | this._timeout = params.timeout; 72 | 73 | if (!this._isDevMode) { 74 | if (!this._appFile) { 75 | log.e(TAG, 'Valid ap file is required, check config'); 76 | process.exit(-1); 77 | } 78 | 79 | this._appFileFullPath = path.isAbsolute(this._appFile) 80 | ? this._appFile : path.join(process.cwd(), this._appFile); 81 | 82 | if (!fs.existsSync(this._appFileFullPath)) { 83 | log.e(TAG, `Valid app file is required, cannot find [${this._appFile}] file`); 84 | process.exit(-1); 85 | } 86 | } 87 | 88 | this._reporter = new Reporter(params.testRunName, this._snapshotsPath); 89 | } 90 | 91 | 92 | _testingCompleted = async (isPassed: boolean = false): Promise => { 93 | if (this._stopByTimeoutID) { 94 | clearTimeout(this._stopByTimeoutID); 95 | } 96 | if (!this._isDevMode) { 97 | log.i(TAG, 'Stopping the server and emulator'); 98 | server.stop(); 99 | await this._device.stop(); 100 | log.i(TAG, 'Server and emulator are stopped'); 101 | 102 | if (!isPassed) { 103 | log.i(TAG, 'Some tests failed, exit with error'); 104 | process.exit(-1); 105 | } else { 106 | log.i(TAG, 'No errors found'); 107 | } 108 | } 109 | }; 110 | 111 | 112 | _onTestingCompleted = async (byTimeOut: boolean = false): Promise => { 113 | const jUnitFile = path.join(process.cwd(), 'junit.xml'); 114 | const deviceLogsFile = path.join( 115 | process.cwd(), 116 | `${this._platform}_logs.log`, 117 | ); 118 | await this._reporter.toLog(); 119 | this._reporter.tojUnit(jUnitFile); 120 | this._reporter.deviceLogsToFile(deviceLogsFile); 121 | this._testingCompleted(byTimeOut ? false : this._reporter.isPassed()); 122 | }; 123 | 124 | 125 | _onAppActivity = (): void => { 126 | this._stopByTimeout(); 127 | } 128 | 129 | 130 | _stopByTimeout = (): void => { 131 | if (this._stopByTimeoutID) { 132 | clearTimeout(this._stopByTimeoutID); 133 | } 134 | this._stopByTimeoutID = setTimeout(() => { 135 | log.e(TAG, 'Stop tests by timeout'); 136 | this._onTestingCompleted(true); 137 | }, this._timeout); 138 | }; 139 | 140 | 141 | async _startAndroid(): Promise { 142 | log.d(TAG, `Start emulator [${this._deviceName}]`); 143 | try { 144 | await this._device.start(this._deviceParams); 145 | } catch (err) { 146 | process.exit(-1); 147 | } 148 | log.d(TAG, 'Emulator started'); 149 | 150 | log.d(TAG, 'Installing APK'); 151 | await this._device.installApp(this._packageName, this._appFileFullPath); 152 | log.d(TAG, 'APK installed'); 153 | 154 | log.d(TAG, 'Starting application'); 155 | if (this._locale) { 156 | log.w(TAG, `[${this._locale} is ignored for android]`); 157 | } 158 | await this._device.startApp(this._packageName, this._activityName); 159 | log.d(TAG, 'Application started'); 160 | 161 | this._stopByTimeout(); 162 | } 163 | 164 | 165 | async _startIOS(): Promise { 166 | log.d(TAG, `Start emulator [${this._deviceName}]`); 167 | try { 168 | await this._device.start(this._deviceParams); 169 | } catch (err) { 170 | log.e(TAG, `Failed to start device: [${err instanceof Error ? err.message : 'Unknown error'}]`); 171 | process.exit(-1); 172 | } 173 | log.d(TAG, 'Emulator started'); 174 | 175 | log.d(TAG, 'Installing APP'); 176 | await this._device.installApp(this._packageName, this._appFileFullPath); 177 | log.d(TAG, 'APP installed'); 178 | 179 | log.d(TAG, 'Starting application'); 180 | await this._device.startApp(this._packageName, this._activityName, this._locale); 181 | log.d(TAG, 'Application started'); 182 | } 183 | 184 | 185 | async start(): Promise { 186 | log.d(TAG, 'Starting server'); 187 | server.start( 188 | this._reporter, 189 | this._onTestingCompleted, 190 | this._snapshotsPath, 191 | this._onAppActivity, 192 | this._port, 193 | ); 194 | 195 | if (this._isDevMode) { 196 | log.d(TAG, 'Only server is used in DEV mode. Waiting for tests'); 197 | return; 198 | } 199 | 200 | if (this._platform === 'ios') { 201 | this._startIOS(); 202 | } else { 203 | this._startAndroid(); 204 | } 205 | 206 | this._reporter.collectDeviceLogs(this._platform, this._packageName); 207 | } 208 | } 209 | 210 | 211 | export default TestsRunner; 212 | -------------------------------------------------------------------------------- /src/runner/azure/AzurePublisher.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as https from 'https'; 3 | import * as path from 'path'; 4 | 5 | import log from '../utils/log'; 6 | import type { TestcaseType } from '../utils/Reporter'; 7 | 8 | const TAG = 'PIXELS_CATCHER::AZURE_PUBLISHER'; 9 | 10 | const processEnv: any = process.env; 11 | const { 12 | BUILD_BUILDURI, 13 | SYSTEM_ACCESSTOKEN, 14 | SYSTEM_TEAMFOUNDATIONCOLLECTIONURI, 15 | SYSTEM_TEAMPROJECT, 16 | } = processEnv; 17 | 18 | const DEFAULT_OPTIONS = { 19 | hostname: 'dev.azure.com', 20 | port: 443, 21 | }; 22 | 23 | const DEFAULT_HEADERS = { 24 | 'Content-Type': 'application/json; charset=utf-8', 25 | 'X-TFS-FedAuthRedirect': 'Suppress', 26 | Accept: 'application/json', 27 | Authorization: `Basic ${Buffer.from(`:${SYSTEM_ACCESSTOKEN}`).toString('base64')}`, 28 | }; 29 | 30 | type ImageType = 'refImages' | 'uploads' | 'diffs'; 31 | 32 | const uploadImageSuffix = { 33 | diffs: 'Diff', 34 | refImages: 'Reference', 35 | uploads: 'Actual', 36 | }; 37 | 38 | const imageTypes = Object.keys(uploadImageSuffix); 39 | 40 | function base64Encode(file: string): string { 41 | if (!fs.existsSync(file)) { 42 | return ''; 43 | } 44 | return Buffer.from(fs.readFileSync(file)).toString('base64'); 45 | } 46 | 47 | class AzurePublisher { 48 | _workingDir: string; 49 | 50 | _testRunName: string; 51 | 52 | _urlBasePath: string; 53 | 54 | constructor(workingDir: string, testRunName: string) { 55 | this._workingDir = workingDir; 56 | this._testRunName = testRunName; 57 | const organization = SYSTEM_TEAMFOUNDATIONCOLLECTIONURI 58 | .split('/') 59 | .filter((str: string) => Boolean(str)) 60 | .reduce((acc: string, curr: string) => curr); 61 | this._urlBasePath = `/${organization}/${SYSTEM_TEAMPROJECT}/_apis/test`; 62 | } 63 | 64 | async publish(): Promise { 65 | try { 66 | const buildRunId = await this._getBuildRunId(BUILD_BUILDURI); 67 | log.i(TAG, `buildRunId [${buildRunId}]`); 68 | const failedTests = await this._getTestFailures(buildRunId); 69 | log.i(TAG, `failedTests count [${failedTests.length}]`); 70 | failedTests.forEach(async (test: any) => { 71 | log.v(TAG, `Uploading results for test [${test.testCaseTitle}] from [${test.automatedTestStorage}]`); 72 | let type: any; 73 | for (let ind = 0; ind < imageTypes.length; ++ind) { 74 | type = imageTypes[ind]; 75 | await this._uploadScreenshot( 76 | buildRunId, 77 | test.id, 78 | test.testCaseTitle, 79 | test.automatedTestStorage, 80 | type, 81 | ); 82 | } 83 | }); 84 | } catch (err) { 85 | log.e(TAG, `Failed to upload results: ${err instanceof Error ? err.message : 'Unknown error'}`); 86 | process.exit(-1); 87 | } 88 | } 89 | 90 | async _getBuildRunId(_buildUri: string): Promise { 91 | const data: any = await this._httpsRequest({ 92 | method: 'GET', 93 | path: `${this._urlBasePath}/runs?api-version=5.1&buildUri=${_buildUri}`, 94 | }); 95 | if (!data || !data.value || data.value.length === 0) { 96 | throw new Error('Failed to get build run, no data'); 97 | } 98 | let id; 99 | log.v(TAG, `Runs count: ${data.value.length}, searching for [${this._testRunName}]`); 100 | for (let ind = data.value.length - 1; ind >= 0; --ind) { 101 | log.v(TAG, `Name: [${data.value[ind].name}], id: ${data.value[ind].id}`); 102 | if (data.value[ind].name === this._testRunName) { 103 | log.v(TAG, `Id ${data.value[ind].id} found`); 104 | id = data.value[ind].id; 105 | break; 106 | } 107 | } 108 | 109 | if (id === undefined) { 110 | throw new Error(`Failed to get build run id for ${this._testRunName}`); 111 | } 112 | 113 | return id; 114 | } 115 | 116 | async _getTestFailures(runId: string): Promise> { 117 | const data: any = await this._httpsRequest({ 118 | method: 'GET', 119 | path: `${this._urlBasePath}/Runs/${runId}/results?outcomes=3&api-version=5.1&outcomes=3`, 120 | }); 121 | 122 | return data.value; 123 | } 124 | 125 | async _upload( 126 | buildRunId: string, 127 | id: string, 128 | fileToUpload: string, 129 | fileNameToShow: string, 130 | ): Promise { 131 | const postData = { 132 | stream: base64Encode(fileToUpload), 133 | fileName: fileNameToShow, 134 | comment: 'Diff uploaded by REST from pipeline', 135 | attachmentType: 'GeneralAttachment', 136 | }; 137 | 138 | const data: any = await this._httpsRequest({ 139 | method: 'POST', 140 | path: `${this._urlBasePath}/Runs/${buildRunId}/Results/${id}/attachments?api-version=5.1-preview.1`, 141 | }, postData); 142 | 143 | return data.value; 144 | } 145 | 146 | async _uploadScreenshot( 147 | buildRunId: string, 148 | id: string, 149 | testCaseTitle: string, 150 | className: string, 151 | type: ImageType, 152 | ): Promise { 153 | const suffix = uploadImageSuffix[type]; 154 | log.v(TAG, `Uploading ${suffix}`); 155 | await this._upload( 156 | buildRunId, 157 | id, 158 | path.join(this._workingDir, className, type, `${testCaseTitle}.png`), 159 | `${testCaseTitle}${suffix}.png`, 160 | ); 161 | log.v(TAG, `${suffix} uploaded`); 162 | } 163 | 164 | async _httpsRequest(options: any, postData: any = undefined): Promise { 165 | let _options = { 166 | ...DEFAULT_OPTIONS, 167 | ...options, 168 | headers: { 169 | ...DEFAULT_HEADERS, 170 | ...(options.headers ? options.headers : {}), 171 | }, 172 | }; 173 | const _postData = postData ? JSON.stringify(postData) : undefined; 174 | 175 | return new Promise((resolve: Function, reject: Function) => { 176 | if (_postData) { 177 | _options = { 178 | ..._options, 179 | 'Content-Length': Buffer.byteLength(_postData), 180 | }; 181 | } 182 | const req = https.request(_options, (resp: any) => { 183 | if (resp.statusCode >= 300) { 184 | log.e(TAG, `Failed to ${_options.method} [${_options.path}]`); 185 | reject(new Error(`Status code: ${resp.statusCode}, statusMessage: ${resp.statusMessage}`)); 186 | return; 187 | } 188 | 189 | let data = ''; 190 | 191 | resp.on('data', (chunk: string) => { 192 | data += chunk; 193 | }); 194 | 195 | resp.on('end', () => { 196 | resolve(JSON.parse(data)); 197 | }); 198 | }).on('error', (err: Error) => { 199 | reject(new Error(`Error: ${err.message}`)); 200 | }); 201 | 202 | if (_postData) { 203 | req.write(_postData); 204 | } 205 | 206 | req.end(); 207 | }); 208 | } 209 | } 210 | 211 | export default AzurePublisher; 212 | -------------------------------------------------------------------------------- /src/runner/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Maksym Rusynyk 2018 - present 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | import type { DeviceInterface } from './utils/device/DeviceInterface'; 9 | 10 | import log from './utils/log'; 11 | import readConfig from './utils/readConfig'; 12 | import getDevice from './utils/device/deviceProvider'; 13 | import AzurePublisher from './azure/AzurePublisher'; 14 | import TestsRunner from './TestsRunner'; 15 | 16 | const TAG = 'PIXELS_CATCHER'; 17 | const AZURE_PUBLISH_ACTION = 'azureAttachments'; 18 | const [,, platform, configuration, action] = process.argv; 19 | 20 | if (!platform || !(platform === 'ios' || platform === 'android')) { 21 | log.e(TAG, `Valid platform is required, specify "ios" or "android". Example: 22 | 23 | $ pixels-catcher android debug 24 | 25 | or 26 | 27 | $ pixels-catcher ios debug 28 | `); 29 | process.exit(-1); 30 | } 31 | 32 | if (!configuration) { 33 | log.e(TAG, `Configuration is required. Example: 34 | 35 | $ pixels-catcher android debug 36 | 37 | or 38 | 39 | $ pixels-catcher ios debug 40 | `); 41 | process.exit(-1); 42 | } 43 | 44 | if (action !== undefined && action !== AZURE_PUBLISH_ACTION) { 45 | log.e(TAG, `Only "${AZURE_PUBLISH_ACTION}" is available. Example: 46 | 47 | $ pixels-catcher android debug ${AZURE_PUBLISH_ACTION} 48 | 49 | or 50 | 51 | $ pixels-catcher ios debug ${AZURE_PUBLISH_ACTION} 52 | `); 53 | process.exit(-1); 54 | } 55 | 56 | 57 | const fullConfig = readConfig(); 58 | const config = fullConfig[platform]; 59 | 60 | if (!config) { 61 | log.e(TAG, `Cannot find configuration for plarform [${platform}] in ` 62 | + `config:\n ${JSON.stringify(fullConfig, null, 2)}`); 63 | process.exit(-1); 64 | } 65 | 66 | log.setLevel(fullConfig.logLevel); 67 | log.i(TAG, `Starting with [${configuration}] configuration for [${platform}]`); 68 | log.v(TAG, `Config\n${JSON.stringify(config, null, 2)}`); 69 | 70 | const getParamFromConfig = (paramName: string): any => { 71 | const value = (config[configuration] || {})[paramName]; 72 | return value !== undefined ? value : config[paramName]; 73 | }; 74 | 75 | const activityName = getParamFromConfig('activityName') || 'MainActivity'; 76 | const appFile = getParamFromConfig('appFile'); 77 | const canStopDevice = getParamFromConfig('canStopDevice'); 78 | const deviceName = getParamFromConfig('deviceName'); 79 | const deviceParams = getParamFromConfig('deviceParams'); 80 | const isPhysicalDevice = getParamFromConfig('physicalDevice'); 81 | const packageName = getParamFromConfig('packageName'); 82 | const snapshotsPath = getParamFromConfig('snapshotsPath'); 83 | const port = getParamFromConfig('port'); 84 | const locale = getParamFromConfig('locale'); 85 | const timeout = fullConfig.timeout || 25 * 1000; // 25 sec is default 86 | 87 | if (!deviceName) { 88 | log.e(TAG, 'Valid device name is required, check "PixelsCatcher.deviceName" ' 89 | + 'property in package.json'); 90 | process.exit(-1); 91 | } 92 | 93 | const device: DeviceInterface = getDevice( 94 | deviceName, 95 | platform, 96 | isPhysicalDevice, 97 | canStopDevice, 98 | ); 99 | 100 | log.i(TAG, `Starting with: 101 | - activityName: [${activityName}] 102 | - appFile: [${appFile}] 103 | - deviceName: [${deviceName}] 104 | - deviceParams: [${deviceParams}] 105 | - packageName: [${packageName}] 106 | - snapshotsPath: [${snapshotsPath}] 107 | - canStopDevice: [${canStopDevice}] 108 | - port: [${port}] 109 | - locale: [${locale}]`); 110 | 111 | if (!packageName) { 112 | log.e(TAG, 'Package name is required'); 113 | process.exit(-1); 114 | } 115 | 116 | const testRunName = `UI tests for ${platform}/${deviceName}`; 117 | 118 | if (action === AZURE_PUBLISH_ACTION) { 119 | const azurePublisher = new AzurePublisher(process.cwd(), testRunName); 120 | azurePublisher.publish(); 121 | } else { 122 | const isDevMode = !appFile; 123 | log.i(TAG, `Starting in ${isDevMode ? 'development' : 'ci'} mode`); 124 | const testsRunner = new TestsRunner({ 125 | testRunName, 126 | isDevMode, 127 | timeout, 128 | device, 129 | appFile, 130 | port, 131 | platform, 132 | deviceName, 133 | snapshotsPath, 134 | deviceParams, 135 | packageName, 136 | locale, 137 | activityName, 138 | }); 139 | testsRunner.start(); 140 | } 141 | -------------------------------------------------------------------------------- /src/runner/server/compareImages.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as fs from 'fs'; 8 | import { PNG } from 'pngjs'; 9 | import * as pixelmatch from 'pixelmatch'; 10 | 11 | export default (actual: any, expected: any, diffFile: any): number => { 12 | if (!actual || !fs.existsSync(actual)) { 13 | throw new Error(`Actual file is required, cannot get [${actual}] file`); 14 | } 15 | if (!expected || !fs.existsSync(expected)) { 16 | throw new Error(`Expected file is required, cannot get [${expected}] file`); 17 | } 18 | 19 | const imageActual = PNG.sync.read(fs.readFileSync(actual)); 20 | const imageExpected = PNG.sync.read(fs.readFileSync(expected)); 21 | 22 | if (imageActual.width !== imageExpected.width) { 23 | throw new Error(`Width mismatch: expected ${imageExpected.width}, actual: ${imageActual.width}`); 24 | } 25 | 26 | if (imageActual.height !== imageExpected.height) { 27 | throw new Error(`Height mismatch: expected ${imageExpected.height}, actual: ${imageActual.height}`); 28 | } 29 | 30 | const diff = new PNG({ width: imageExpected.width, height: imageExpected.height }); 31 | 32 | const differentPixelsCount = pixelmatch( 33 | imageActual.data, 34 | imageExpected.data, 35 | diff.data, 36 | imageActual.width, 37 | imageActual.height, 38 | { threshold: 0.1 }, 39 | ); 40 | 41 | if (diffFile) { 42 | diff.pack().pipe(fs.createWriteStream(diffFile)); 43 | } 44 | 45 | return differentPixelsCount; 46 | }; 47 | -------------------------------------------------------------------------------- /src/runner/server/dummy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumax/react-native-PixelsCatcher/8ab309551fb5b41426909dc31cf47302580f08f2/src/runner/server/dummy.png -------------------------------------------------------------------------------- /src/runner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es5", 5 | "declaration": true, 6 | "outDir": "../../lib/runner", 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true 11 | }, 12 | "include": ["./**/*"], 13 | "exclude": [ 14 | "../../node_modules", 15 | "**/__tests__/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/runner/utils/Reporter.ts: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as fs from 'fs'; 3 | 4 | import timeToSec from './timeToSec'; 5 | import exec from './exec'; 6 | import delay from './delay'; 7 | 8 | const { spawn } = require('child_process'); 9 | 10 | export type TestcaseType = { 11 | failure: string | void, 12 | isSkipped: boolean | void, 13 | name: string, 14 | renderTime?: number, 15 | time: number, 16 | }; 17 | 18 | const timeReducer = (time: number, testcase: TestcaseType): number => time + testcase.time; 19 | 20 | const filterSkipped = (testcase: TestcaseType): boolean => !testcase.isSkipped; 21 | 22 | const filterFailed = (testcase: TestcaseType): boolean => !testcase.failure; 23 | 24 | class TestReporter { 25 | _name: string; 26 | 27 | _className: string; 28 | 29 | _tests: Array = []; 30 | 31 | _deviceLogs: Array = []; 32 | 33 | _stopDeviceLogger: Function | undefined = undefined; 34 | 35 | _minRenderTime = { 36 | name: '-', 37 | time: Number.MAX_VALUE, 38 | }; 39 | 40 | _maxRenderTime = { 41 | name: '-', 42 | time: Number.MIN_VALUE, 43 | }; 44 | 45 | constructor(name: string, className: string) { 46 | this._name = name; 47 | this._className = className; 48 | } 49 | 50 | registerTest(name: string): void { 51 | this._tests.push({ 52 | failure: undefined, 53 | isSkipped: true, 54 | name, 55 | renderTime: 0, 56 | time: 0, 57 | }); 58 | } 59 | 60 | _updateTestResult(testCase: TestcaseType): void { 61 | const ind = this._tests.findIndex((test) => test.name === testCase.name); 62 | 63 | if (ind >= 0) { 64 | this._tests[ind] = testCase; 65 | } else { 66 | this._tests.push(testCase); 67 | } 68 | } 69 | 70 | reportTest(testCase: TestcaseType): void { 71 | this._updateTestResult(testCase); 72 | 73 | if (testCase.renderTime === undefined) { 74 | return; 75 | } 76 | if (testCase.renderTime < this._minRenderTime.time) { 77 | this._minRenderTime.time = testCase.renderTime; 78 | this._minRenderTime.name = testCase.name; 79 | } 80 | if (testCase.renderTime > this._maxRenderTime.time) { 81 | this._maxRenderTime.time = testCase.renderTime; 82 | this._maxRenderTime.name = testCase.name; 83 | } 84 | } 85 | 86 | isPassed(): boolean { 87 | return this._getFailedTests().length === 0; 88 | } 89 | 90 | async toLog(): Promise { 91 | global.console.log(''); 92 | global.console.log('==> All tests completed: <=='); 93 | 94 | const failedTests = this._getFailedTests(); 95 | const passedTests = this._getPassedTests(); 96 | const skippedTests = this._getSkippedTests(); 97 | const reportTable: any = []; 98 | 99 | this._tests.forEach((testcase: TestcaseType) => { 100 | let status = 'PASSED'; 101 | 102 | if (testcase.failure) { 103 | status = 'FAILED'; 104 | } else if (testcase.isSkipped) { 105 | status = 'SKIPPED'; 106 | } 107 | 108 | reportTable.push({ 109 | name: testcase.name, 110 | status, 111 | time: timeToSec(testcase.time), 112 | renderTime: testcase.renderTime !== undefined ? timeToSec(testcase.renderTime) : '-', 113 | failure: testcase.failure || '-', 114 | }); 115 | }); 116 | 117 | global.console.table(reportTable); 118 | 119 | global.console.log(''); 120 | global.console.log('==> Summary: <=='); 121 | 122 | global.console.table([ 123 | ['Total tests', this._tests.length], 124 | ['Passed tests', passedTests.length], 125 | ['Skipped tests', skippedTests.length], 126 | ['Failed tests', failedTests.length], 127 | ['Min render time', `${this._minRenderTime.time}ms (${this._minRenderTime.name})`], 128 | ['Max render time', `${this._maxRenderTime.time}ms (${this._maxRenderTime.name})`], 129 | ]); 130 | 131 | if (failedTests.length > 0) { 132 | global.console.log('==> Failed tests: <=='); 133 | global.console.table(failedTests.map((testCase: TestcaseType) => testCase.name)); 134 | } 135 | 136 | // on CI some logs are not available, adding a delay to fix it 137 | await delay(300); 138 | } 139 | 140 | tojUnit(jUnitFile: string): void { 141 | const xmlResult = ['']; 142 | xmlResult.push('`); 149 | xmlResult.push(' `); 156 | this._tests.forEach((testcase: TestcaseType) => { 157 | xmlResult.push(' `); 161 | if (testcase.failure) { 162 | xmlResult.push(` ${testcase.failure}`); 163 | } else if (testcase.isSkipped) { 164 | xmlResult.push(' '); 165 | } 166 | xmlResult.push(' '); 167 | }); 168 | xmlResult.push(' '); 169 | xmlResult.push(''); 170 | xmlResult.push(''); 171 | fs.writeFileSync(jUnitFile, xmlResult.join('\n')); 172 | } 173 | 174 | collectDeviceLogs(platform: 'ios' | 'android', packageName: string): void { 175 | let spawnProcess: any; 176 | if (platform === 'android') { 177 | exec('adb logcat -c'); 178 | spawnProcess = spawn('adb', [ 179 | 'logcat', `${packageName}:I`, '*:V', 180 | ]); 181 | } else if (platform === 'ios') { 182 | spawnProcess = spawn('xcrun', [ 183 | 'simctl', 'spawn', 'booted', 'log', 'stream', 184 | ]); 185 | } 186 | 187 | spawnProcess.stdout.on('data', (data: any): any => { 188 | const stringRepresentation = data.toString(); 189 | this._deviceLogs.push(stringRepresentation); 190 | }); 191 | 192 | this._stopDeviceLogger = (): void => { 193 | spawnProcess.stdin.pause(); 194 | spawnProcess.kill(); 195 | }; 196 | } 197 | 198 | deviceLogsToFile(fileName: string): void { 199 | if (this._stopDeviceLogger) { 200 | this._stopDeviceLogger(); 201 | this._stopDeviceLogger = undefined; 202 | } 203 | fs.writeFileSync(fileName, this._deviceLogs.join('')); 204 | } 205 | 206 | _getPassedTests(): Array { 207 | return this._tests 208 | .filter(filterSkipped) 209 | .filter(filterFailed); 210 | } 211 | 212 | _getSkippedTests(): Array { 213 | return this._tests 214 | .filter((testcase: TestcaseType): boolean => Boolean(testcase.isSkipped)); 215 | } 216 | 217 | _getFailedTests(): Array { 218 | return this._tests 219 | .filter(filterSkipped) 220 | .filter((test: TestcaseType) => Boolean(test.failure)); 221 | } 222 | 223 | _getTotalTime(): number { 224 | return this._tests 225 | .filter(filterSkipped) 226 | .reduce(timeReducer, 0); 227 | } 228 | } 229 | 230 | export default TestReporter; 231 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/__snapshots__/log.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`logging Allows to log including d 1`] = ` 4 | Array [ 5 | Array [ 6 | "i:", 7 | ], 8 | Array [ 9 | "w WARNING:", 10 | ], 11 | Array [ 12 | "e ERROR:", 13 | ], 14 | Array [ 15 | "v:", 16 | ], 17 | Array [ 18 | "d:", 19 | ], 20 | Array [ 21 | "i:", 22 | ], 23 | Array [ 24 | "w WARNING:", 25 | ], 26 | Array [ 27 | "e ERROR:", 28 | ], 29 | Array [ 30 | "d:", 31 | ], 32 | Array [ 33 | "i:", 34 | ], 35 | Array [ 36 | "w WARNING:", 37 | ], 38 | Array [ 39 | "e ERROR:", 40 | ], 41 | ] 42 | `; 43 | 44 | exports[`logging Allows to log including e 1`] = ` 45 | Array [ 46 | Array [ 47 | "i:", 48 | ], 49 | Array [ 50 | "w WARNING:", 51 | ], 52 | Array [ 53 | "e ERROR:", 54 | ], 55 | Array [ 56 | "v:", 57 | ], 58 | Array [ 59 | "d:", 60 | ], 61 | Array [ 62 | "i:", 63 | ], 64 | Array [ 65 | "w WARNING:", 66 | ], 67 | Array [ 68 | "e ERROR:", 69 | ], 70 | Array [ 71 | "d:", 72 | ], 73 | Array [ 74 | "i:", 75 | ], 76 | Array [ 77 | "w WARNING:", 78 | ], 79 | Array [ 80 | "e ERROR:", 81 | ], 82 | Array [ 83 | "i:", 84 | ], 85 | Array [ 86 | "w WARNING:", 87 | ], 88 | Array [ 89 | "e ERROR:", 90 | ], 91 | Array [ 92 | "w WARNING:", 93 | ], 94 | Array [ 95 | "e ERROR:", 96 | ], 97 | Array [ 98 | "e ERROR:", 99 | ], 100 | ] 101 | `; 102 | 103 | exports[`logging Allows to log including i 1`] = ` 104 | Array [ 105 | Array [ 106 | "i:", 107 | ], 108 | Array [ 109 | "w WARNING:", 110 | ], 111 | Array [ 112 | "e ERROR:", 113 | ], 114 | Array [ 115 | "v:", 116 | ], 117 | Array [ 118 | "d:", 119 | ], 120 | Array [ 121 | "i:", 122 | ], 123 | Array [ 124 | "w WARNING:", 125 | ], 126 | Array [ 127 | "e ERROR:", 128 | ], 129 | Array [ 130 | "d:", 131 | ], 132 | Array [ 133 | "i:", 134 | ], 135 | Array [ 136 | "w WARNING:", 137 | ], 138 | Array [ 139 | "e ERROR:", 140 | ], 141 | Array [ 142 | "i:", 143 | ], 144 | Array [ 145 | "w WARNING:", 146 | ], 147 | Array [ 148 | "e ERROR:", 149 | ], 150 | ] 151 | `; 152 | 153 | exports[`logging Allows to log including v 1`] = ` 154 | Array [ 155 | Array [ 156 | "i:", 157 | ], 158 | Array [ 159 | "w WARNING:", 160 | ], 161 | Array [ 162 | "e ERROR:", 163 | ], 164 | Array [ 165 | "v:", 166 | ], 167 | Array [ 168 | "d:", 169 | ], 170 | Array [ 171 | "i:", 172 | ], 173 | Array [ 174 | "w WARNING:", 175 | ], 176 | Array [ 177 | "e ERROR:", 178 | ], 179 | ] 180 | `; 181 | 182 | exports[`logging Allows to log including w 1`] = ` 183 | Array [ 184 | Array [ 185 | "i:", 186 | ], 187 | Array [ 188 | "w WARNING:", 189 | ], 190 | Array [ 191 | "e ERROR:", 192 | ], 193 | Array [ 194 | "v:", 195 | ], 196 | Array [ 197 | "d:", 198 | ], 199 | Array [ 200 | "i:", 201 | ], 202 | Array [ 203 | "w WARNING:", 204 | ], 205 | Array [ 206 | "e ERROR:", 207 | ], 208 | Array [ 209 | "d:", 210 | ], 211 | Array [ 212 | "i:", 213 | ], 214 | Array [ 215 | "w WARNING:", 216 | ], 217 | Array [ 218 | "e ERROR:", 219 | ], 220 | Array [ 221 | "i:", 222 | ], 223 | Array [ 224 | "w WARNING:", 225 | ], 226 | Array [ 227 | "e ERROR:", 228 | ], 229 | Array [ 230 | "w WARNING:", 231 | ], 232 | Array [ 233 | "e ERROR:", 234 | ], 235 | ] 236 | `; 237 | 238 | exports[`logging Default logs i, w and e 1`] = ` 239 | Array [ 240 | Array [ 241 | "i:", 242 | ], 243 | Array [ 244 | "w WARNING:", 245 | ], 246 | Array [ 247 | "e ERROR:", 248 | ], 249 | ] 250 | `; 251 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/__snapshots__/readConfig.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`readConfig read config from package.json 1`] = ` 4 | Object { 5 | "android": Object { 6 | "_content": "platform config content from package.json", 7 | }, 8 | } 9 | `; 10 | 11 | exports[`readConfig read config from pixels-catcher.json 1`] = ` 12 | Object { 13 | "ios": Object { 14 | "_content": "platform config content from pixels-catcher.json", 15 | }, 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/isCommand.js: -------------------------------------------------------------------------------- 1 | import isCommand from '../isCommand'; 2 | import exec from '../exec'; 3 | 4 | jest.mock('../exec', () => jest.fn()); 5 | 6 | describe('isCommand', () => { 7 | afterEach(() => { 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | it('Returns true if command exists', () => { 12 | exec.mockReturnValueOnce('/bin/ls'); 13 | 14 | const exists = isCommand('ls'); 15 | 16 | expect(exec).toHaveBeenCalledWith('whereis ls'); 17 | expect(exists).toBe(true); 18 | }); 19 | 20 | it('Returns false if command does not exists', () => { 21 | exec.mockReturnValueOnce(''); 22 | 23 | const exists = isCommand('sl'); 24 | 25 | expect(exec).toHaveBeenCalledWith('whereis sl'); 26 | expect(exists).toBe(false); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/log.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import log from '../log'; 3 | 4 | global.console.log = jest.fn(); 5 | 6 | describe('logging', () => { 7 | const consoleLog = global.console.log; 8 | const logAll = () => { 9 | log.v('v'); 10 | log.d('d'); 11 | log.i('i'); 12 | log.w('w'); 13 | log.e('e'); 14 | }; 15 | 16 | afterEach(() => { 17 | log.setLevel('v'); 18 | }); 19 | 20 | it('Default logs i, w and e', () => { 21 | logAll(); 22 | expect(consoleLog.mock.calls).toMatchSnapshot(); 23 | }); 24 | 25 | it('Allows to log including v', () => { 26 | log.setLevel('v'); 27 | logAll(); 28 | expect(consoleLog.mock.calls).toMatchSnapshot(); 29 | }); 30 | 31 | it('Allows to log including d', () => { 32 | log.setLevel('d'); 33 | logAll(); 34 | expect(consoleLog.mock.calls).toMatchSnapshot(); 35 | }); 36 | 37 | it('Allows to log including i', () => { 38 | log.setLevel('i'); 39 | logAll(); 40 | expect(consoleLog.mock.calls).toMatchSnapshot(); 41 | }); 42 | 43 | it('Allows to log including w', () => { 44 | log.setLevel('w'); 45 | logAll(); 46 | expect(consoleLog.mock.calls).toMatchSnapshot(); 47 | }); 48 | 49 | it('Allows to log including e', () => { 50 | log.setLevel('e'); 51 | logAll(); 52 | expect(consoleLog.mock.calls).toMatchSnapshot(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/runner/utils/__tests__/readConfig.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import readConfig from '../readConfig'; 4 | 5 | jest.mock('fs', () => ({ 6 | existsSync: jest.fn(), 7 | readFileSync: jest.fn(), 8 | })); 9 | jest.mock('path', () => ({ 10 | join: jest.fn((...args) => args.join('/')), 11 | })); 12 | 13 | process.exit = jest.fn(); 14 | process.cwd = jest.fn(() => 'path_to_file'); 15 | 16 | describe('readConfig', () => { 17 | afterEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | it('read config from package.json', () => { 22 | const rawConfig = '{"PixelsCatcher":{"android":{"_content":"platform ' 23 | + 'config content from package.json"}}}'; 24 | 25 | fs.existsSync.mockImplementationOnce(() => true); 26 | fs.readFileSync.mockImplementationOnce(() => rawConfig); 27 | 28 | const config = readConfig(); 29 | 30 | expect(config).toMatchSnapshot(); 31 | }); 32 | 33 | it('read config from pixels-catcher.json', () => { 34 | // package.json mock 35 | fs.existsSync.mockImplementationOnce(() => true); 36 | fs.readFileSync.mockImplementationOnce(() => '{}'); 37 | // pixels-catcher.json mock 38 | const rawConfig = '{"ios":{"_content":"platform config content from ' 39 | + 'pixels-catcher.json"}}'; 40 | fs.existsSync.mockImplementationOnce(() => true); 41 | fs.readFileSync.mockImplementationOnce(() => rawConfig); 42 | 43 | const config = readConfig(); 44 | 45 | expect(config).toMatchSnapshot(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/runner/utils/delay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | function delay(milliseconds: number): Promise { 8 | return new Promise((resolve: Function) => { 9 | setTimeout(resolve, milliseconds); 10 | }); 11 | } 12 | 13 | export default delay; 14 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidDevice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2019 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type { DeviceInterface } from './DeviceInterface'; 8 | 9 | import exec from '../exec'; 10 | import delay from '../delay'; 11 | import log from '../log'; 12 | 13 | const TAG = 'PIXELS_CATCHER::ANDROID_DEVICE'; 14 | 15 | class AndroidDevice implements DeviceInterface { 16 | _name: string; 17 | 18 | constructor(name: string) { 19 | this._name = name; 20 | } 21 | 22 | 23 | _getDevices(): Array { 24 | const cmd = 'adb devices'; 25 | const devices = exec(cmd).split('\n').slice(1) 26 | .filter((line: string): boolean => Boolean(line)) 27 | .map((line: string): string => line.split('\t')[0]); 28 | 29 | return devices; 30 | } 31 | 32 | 33 | _isDeviceAvailable(name: string): boolean { 34 | const devices = this._getDevices(); 35 | let isAvailable = false; 36 | 37 | for (let ind = devices.length - 1; ind >= 0; --ind) { 38 | if (devices[ind].indexOf(name) >= 0) { 39 | isAvailable = true; 40 | break; 41 | } 42 | } 43 | 44 | return isAvailable; 45 | } 46 | 47 | 48 | async start(params: any = []): Promise { 49 | if (params.length !== 0) { 50 | log.e(TAG, 'There are currently no supported device parameters for physical devices, yet you tried to pass some im'); 51 | process.exit(-1); 52 | } 53 | if (!this._isDeviceAvailable(this._name)) { 54 | log.e(TAG, `Invalid name provided [${this._name}], check that the name is \ 55 | correct and device is available. Available devices: 56 | ${this._getDevices().map((device: any): any => ` - ${device}`).join('\n')}`); 57 | throw new Error(`Invalid emulator ${this._name}`); 58 | } 59 | } 60 | 61 | 62 | async stop(): Promise { 63 | log.v(TAG, 'Not stopping anything as it is assumed to be a physical device. Your responsibility!'); 64 | } 65 | 66 | 67 | isAppInstalled(packageName: string): boolean { 68 | const cmd = `adb -s ${this._name} shell pm list packages`; 69 | 70 | log.v(TAG, `Checking if [${packageName}] is installed`); 71 | 72 | const allPackages = exec(cmd); 73 | const isInstalled = allPackages.indexOf(packageName) >= 0; 74 | 75 | log.v(TAG, `Package [${packageName}] is ${isInstalled ? 'Installed' : 'Not installed'}`); 76 | 77 | return isInstalled; 78 | } 79 | 80 | 81 | async uninstallApp(name: string): Promise { 82 | log.v(TAG, `Uninstalling ${name}`); 83 | const isInstalled = await this.isAppInstalled(name); 84 | if (isInstalled) { 85 | const cmd = `adb -s ${this._name} uninstall ${name}`; 86 | exec(cmd); 87 | } 88 | log.v(TAG, 'Uninstalling completed'); 89 | } 90 | 91 | 92 | async installApp(name: string, apkFile: string): Promise { 93 | log.v(TAG, `Installing apk [${apkFile}]`); 94 | 95 | await this.uninstallApp(name); 96 | 97 | let tryCnt = 3; 98 | 99 | while (tryCnt >= 0) { 100 | const cmd = `adb -s ${this._name} install -r ${apkFile}`; 101 | const res = exec(cmd); 102 | log.v(TAG, 'Installed', res); 103 | const isOffline = res.indexOf('device offline') >= 0; 104 | if (isOffline) { 105 | await delay(1000); 106 | } else { 107 | const isSuccess = res.indexOf('Success') >= 0; 108 | if (isSuccess) { 109 | break; 110 | } else { 111 | log.e(TAG, `ERROR: Failed install apk [${apkFile}]`); 112 | process.exit(-1); 113 | } 114 | } 115 | tryCnt--; 116 | } 117 | } 118 | 119 | 120 | startApp(packageName: string, activityName: string): void { 121 | log.v(TAG, `Starting application [${packageName}]`); 122 | 123 | const cmd = `adb -s ${this._name} shell am start -n ${packageName}/${activityName}`; 124 | const result = exec(cmd); 125 | 126 | if (result.indexOf('does not exist') >= 0 || result.indexOf('Error') >= 0) { 127 | log.e(TAG, `Cannot start [${packageName}] with activity [${activityName}]`); 128 | process.exit(-1); 129 | } 130 | 131 | log.v(TAG, 'Application started'); 132 | } 133 | } 134 | 135 | export default AndroidDevice; 136 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidEmulator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import { spawn } from 'child_process'; 8 | 9 | import exec from '../exec'; 10 | import delay from '../delay'; 11 | import log from '../log'; 12 | import emulatorCmd from './AndroidEmulatorCmd'; 13 | 14 | import type { DeviceInterface } from './DeviceInterface'; 15 | 16 | const TAG = 'PIXELS_CATCHER::UTIL_EMULATOR'; 17 | 18 | const startupErrorsDataToIgnore = [ 19 | // Some data appears in stderr when running the emulator first time 20 | '.avd/snapshots/default_boot/ram.img', 21 | 'qemu: unsupported keyboard', 22 | 'WARNING', 23 | ]; 24 | 25 | const canIgnoreErrorData = (data: string): boolean => { 26 | for (let i = 0; i < startupErrorsDataToIgnore.length; ++i) { 27 | if (data.indexOf(startupErrorsDataToIgnore[i]) !== -1) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | }; 34 | 35 | class AndroidEmulator implements DeviceInterface { 36 | _name: string; 37 | 38 | _canStopDevice: boolean; 39 | 40 | constructor(name: string, canStopDevice?: boolean) { 41 | this._name = name; 42 | this._canStopDevice = Boolean(canStopDevice); 43 | } 44 | 45 | _getDevices(): Array { 46 | const cmd = 'emulator -avd -list-avds'; 47 | const devices = exec(cmd).split('\n') 48 | .filter((line: string): boolean => Boolean(line)); 49 | 50 | return devices; 51 | } 52 | 53 | _isDeviceAvailable(name: string): boolean { 54 | const devices = this._getDevices(); 55 | let isAvailable = false; 56 | 57 | for (let ind = devices.length - 1; ind >= 0; --ind) { 58 | if (devices[ind].indexOf(name) >= 0) { 59 | isAvailable = true; 60 | break; 61 | } 62 | } 63 | 64 | return isAvailable; 65 | } 66 | 67 | _getActiveDevice(): any { 68 | log.v(TAG, 'Get active device'); 69 | const device = exec('adb devices').split('\n') 70 | .filter((line: string): boolean => line.indexOf('emulator') === 0)[0]; 71 | 72 | if (!device) { 73 | log.v(TAG, 'No active devices'); 74 | return undefined; 75 | } 76 | const name = device.split('\t')[0]; 77 | 78 | log.v(TAG, 'Active device', name); 79 | return name; 80 | } 81 | 82 | async start(params: any = []): Promise { 83 | if (!this._isDeviceAvailable(this._name)) { 84 | log.e(TAG, `Invalid name provided [${this._name}], check that the name is \ 85 | correct and device is available. Available devices: 86 | ${this._getDevices().map((device: any): any => ` - ${device}`).join('\n')}`); 87 | throw new Error(`Invalid emulator ${this._name}`); 88 | } 89 | 90 | if (this._getActiveDevice()) { 91 | log.e(TAG, 'Other emulator already started'); 92 | if (this._canStopDevice) { 93 | log.e(TAG, 'Stopping emulator'); 94 | await this.stop(); 95 | } else { 96 | log.d(TAG, 'Using active emulator'); 97 | return; 98 | } 99 | } 100 | 101 | log.d(TAG, `Starting emulator [${this._name}]`); 102 | log.v(TAG, `cmd: ${emulatorCmd}`); 103 | log.v(TAG, `params: ${[ 104 | '-avd', this._name, 105 | ...params, 106 | ].filter((value: any): any => Boolean(value))}`); 107 | const result = spawn(emulatorCmd, [ 108 | '-avd', this._name, 109 | ...params, 110 | ].filter((value: any): any => Boolean(value))); 111 | 112 | let deviceBooted = false; 113 | 114 | result.stdout.on('data', (data: any): any => { 115 | log.d(TAG, `stdout: ${data}`); 116 | if (data.toString().toLowerCase().includes('boot completed')) { 117 | deviceBooted = true; 118 | } 119 | }); 120 | 121 | result.stderr.on('data', (data: any): any => { 122 | // Some data appears in stderr when running the emulator first time 123 | const stringRepresentation = data.toString(); 124 | if (canIgnoreErrorData(stringRepresentation)) { 125 | log.w(TAG, `Ignore: ${stringRepresentation}`); 126 | return; 127 | } 128 | log.e(TAG, `Failed to load emulator, stderr: ${data}`); 129 | process.exit(-1); 130 | }); 131 | 132 | result.on('close', (code: any): any => { 133 | log.v(TAG, `on close: child process exited with code ${code}`); 134 | }); 135 | 136 | let tryCnt = (60 * 2) / 5; // 2 minutes with 5000 delay 137 | 138 | while (--tryCnt >= 0 && !deviceBooted) { 139 | log.v(TAG, 'awaiting when device is booted'); 140 | await delay(5000); 141 | } 142 | 143 | if (!deviceBooted) { 144 | log.e(TAG, 'Failed to load emulator in 30 seconds. Check your emulator. Or try to run it with "-no-snapshot"'); 145 | throw new Error('Device is not loaded in 30 seconds'); 146 | } 147 | } 148 | 149 | async stop(): Promise { 150 | if (!this._canStopDevice) { 151 | log.v(TAG, 'Stopping device is restricted in config'); 152 | return; 153 | } 154 | log.v(TAG, 'Stopping active device'); 155 | try { 156 | exec(`adb -s ${this._getActiveDevice()} emu kill;`); 157 | } catch (err) { 158 | log.e(err instanceof Error ? err.message : 'Unknown error'); 159 | } 160 | await delay(5000); 161 | log.v(TAG, 'Active device stopped'); 162 | } 163 | 164 | isAppInstalled(packageName: string): boolean { 165 | const cmd = 'adb shell pm list packages'; 166 | 167 | log.v(TAG, `Checking if [${packageName}] is installed`); 168 | 169 | const allPackages = exec(cmd); 170 | const isInstalled = allPackages.indexOf(packageName) >= 0; 171 | 172 | log.v(TAG, `Package [${packageName}] is ${isInstalled ? 'Installed' : 'Not installed'}`); 173 | 174 | return isInstalled; 175 | } 176 | 177 | async uninstallApp(name: string): Promise { 178 | log.v(TAG, `Uninstalling ${name}`); 179 | const isInstalled = await this.isAppInstalled(name); 180 | if (isInstalled) { 181 | const cmd = `adb uninstall ${name}`; 182 | exec(cmd); 183 | } 184 | log.v(TAG, 'Uninstalling completed'); 185 | } 186 | 187 | async installApp(name: string, apkFile: string): Promise { 188 | let tryCnt = 3; 189 | 190 | log.v(TAG, `Installing apk [${apkFile}]`); 191 | 192 | await this.uninstallApp(name); 193 | 194 | while (tryCnt-- >= 0) { 195 | const cmd = `adb install -r ${apkFile}`; 196 | const res = exec(cmd); 197 | log.v(TAG, 'Installed', res); 198 | const isOffline = res.indexOf('device offline') >= 0; 199 | if (isOffline) { 200 | await delay(1000); 201 | } else { 202 | const isSuccess = res.indexOf('Success') >= 0; 203 | if (isSuccess) { 204 | break; 205 | } else { 206 | log.e(TAG, `ERROR: Failed install apk [${apkFile}]`); 207 | process.exit(-1); 208 | } 209 | } 210 | } 211 | } 212 | 213 | startApp(packageName: string, activityName: string): void { 214 | log.v(TAG, `Starting application [${packageName}]`); 215 | 216 | const cmd = `adb shell am start -n ${packageName}/${activityName}`; 217 | const result = exec(cmd); 218 | 219 | if (result.indexOf('does not exist') >= 0 || result.indexOf('Error') >= 0) { 220 | log.e(TAG, `Cannot start [${packageName}] with activity [${activityName}]`); 221 | process.exit(-1); 222 | } 223 | 224 | log.v(TAG, 'Application started'); 225 | } 226 | } 227 | 228 | export default AndroidEmulator; 229 | -------------------------------------------------------------------------------- /src/runner/utils/device/AndroidEmulatorCmd.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import exec from '../exec'; 8 | import isCommand from '../isCommand'; 9 | 10 | export default process.env.ANDROID_EMULATOR 11 | || (isCommand('emulator') ? 'emulator' : undefined) 12 | || ( 13 | exec('uname -s').trim() === 'Darwin' 14 | ? `${process.env.HOME || ''}/Library/Android/sdk/emulator/emulator` 15 | : 'emulator'); 16 | -------------------------------------------------------------------------------- /src/runner/utils/device/DeviceInterface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /** 3 | * Copyright (c) Maksym Rusynyk 2018 - present 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | export type StartParamsType = Array; 10 | 11 | export interface DeviceInterface { 12 | // constructor(deviceName: string, canStopDevice?: boolean): void, 13 | 14 | start(params: StartParamsType): Promise, 15 | 16 | isAppInstalled(appName: string): boolean, 17 | 18 | installApp(appName: string, appFile: string): Promise, 19 | 20 | startApp(appName: string, activityName: string, locale?: string): void, 21 | 22 | uninstallApp(name: string): Promise, 23 | 24 | stop(): Promise, 25 | } 26 | -------------------------------------------------------------------------------- /src/runner/utils/device/IosSimulator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | /* @flow */ 8 | import type { DeviceInterface, StartParamsType } from './DeviceInterface'; 9 | 10 | import exec from '../exec'; 11 | import log from '../log'; 12 | import delay from '../delay'; 13 | 14 | const TAG = 'PIXELS_CATCHER::UTIL_SIMULATOR'; 15 | 16 | type DeviceType = { 17 | availability: string, 18 | state: string, 19 | isAvailable: boolean, 20 | name: string, 21 | udid: string, 22 | availabilityError: string, 23 | }; 24 | 25 | class IOSSimulator implements DeviceInterface { 26 | _name: string; 27 | 28 | _canStopDevice: boolean; 29 | 30 | constructor(name: string, canStopDevice?: boolean) { 31 | this._name = name; 32 | this._canStopDevice = Boolean(canStopDevice); 33 | } 34 | 35 | 36 | _getAvailableDevices(): Array { 37 | const cmd = 'xcrun simctl list --json'; 38 | const response = JSON.parse(exec(cmd)); 39 | const { devices } = response; 40 | const availableDevices: Array = []; 41 | 42 | Object.keys(devices).forEach((name: string) => { 43 | devices[name].forEach((device: DeviceType) => { 44 | if (device.isAvailable) { 45 | availableDevices.push(device); 46 | } 47 | }); 48 | }); 49 | 50 | return availableDevices; 51 | } 52 | 53 | 54 | _getDeviceByName(name: string): DeviceType | void { 55 | const devices = this._getAvailableDevices(); 56 | let device; 57 | 58 | for (let ind = devices.length - 1; ind >= 0; --ind) { 59 | if (devices[ind].name === name) { 60 | device = devices[ind]; 61 | break; 62 | } 63 | } 64 | 65 | return device; 66 | } 67 | 68 | 69 | _getDeviceByUid(uid: string): DeviceType | void { 70 | const devices = this._getAvailableDevices(); 71 | let device; 72 | 73 | for (let ind = devices.length - 1; ind >= 0; --ind) { 74 | if (devices[ind].udid === uid) { 75 | device = devices[ind]; 76 | break; 77 | } 78 | } 79 | 80 | return device; 81 | } 82 | 83 | 84 | _getDeviceWithStatus(status: string): DeviceType | void { 85 | const devices = this._getAvailableDevices(); 86 | let device; 87 | 88 | for (let ind = devices.length - 1; ind >= 0; --ind) { 89 | if (devices[ind].state === status) { 90 | device = devices[ind]; 91 | break; 92 | } 93 | } 94 | 95 | return device; 96 | } 97 | 98 | 99 | _getUid(name: string): string | void { 100 | const device = this._getDeviceByName(name); 101 | log.v(TAG, `Device ${name} is:`, device); 102 | return device ? device.udid : undefined; 103 | } 104 | 105 | 106 | async _boot(uid: string): Promise { 107 | const device = this._getDeviceByUid(uid); 108 | if (!device) { 109 | throw new Error(`Invalid device uid [${uid}], cannot find it`); 110 | } 111 | if (device.state === 'Booted') { 112 | log.i(TAG, `Device [${device.name}] already booted`); 113 | return; 114 | } 115 | const response = exec(`xcrun simctl boot ${uid}`); 116 | if (response) { 117 | log.v(TAG, 'boot response:', response); 118 | } 119 | } 120 | 121 | 122 | async _open(uid: string): Promise { 123 | const activeXcode = exec('xcode-select -p').trim(); 124 | log.v(TAG, `Active Xcode: ${activeXcode}`); 125 | const simulatorApp = `${activeXcode}/Applications/Simulator.app`; 126 | log.v(TAG, `starting ${simulatorApp}`); 127 | exec(`open -a ${simulatorApp} --args -CurrentDeviceUDID ${uid}`); 128 | log.v(TAG, 'started'); 129 | } 130 | 131 | 132 | async start(params: StartParamsType): Promise { 133 | log.v(TAG, 'Starting device with params:', params); 134 | 135 | this.stop(); 136 | 137 | const uid = this._getUid(this._name); 138 | log.i(TAG, `Uid of the device is [${uid || '-'}]`); 139 | 140 | if (!uid) { 141 | throw new Error(`Invalid simulator [${this._name}], cannot find uid`); 142 | } 143 | 144 | await this._boot(uid); 145 | await this._open(uid); 146 | 147 | log.v(TAG, 'Device started', this._getDeviceByUid(uid)); 148 | } 149 | 150 | 151 | isAppInstalled(appName: string): boolean { 152 | log.v(`isAppInstalled: appName [${appName}]`); 153 | return false; 154 | } 155 | 156 | 157 | async installApp(appName: string, appFile: string): Promise { 158 | this.uninstallApp(appName); 159 | log.v(TAG, `Installing application [${appName}], appFile [${appFile}]`); 160 | exec(`xcrun simctl install booted ${appFile}`); 161 | } 162 | 163 | 164 | startApp(appName: string, activityName: string, locale?: string): void { 165 | const withLocale = locale ? `-AppleLanguages "(${locale})"` : ''; 166 | log.v(TAG, `startApp: appName [${appName}], activityName [${activityName}], locale [${locale || '-'}]`); 167 | exec(`xcrun simctl launch booted ${appName} ${withLocale}`); 168 | } 169 | 170 | 171 | async uninstallApp(appName: string): Promise { 172 | log.v(TAG, `Uninstalling application [${appName}]`); 173 | exec(`xcrun simctl uninstall booted ${appName}`); 174 | } 175 | 176 | 177 | async stop(): Promise { 178 | if (!this._canStopDevice) { 179 | log.v(TAG, 'Stopping device is restricted in config'); 180 | return; 181 | } 182 | 183 | log.v(TAG, 'Stopping all devices'); 184 | 185 | exec('osascript -e \'tell application "iOS Simulator" to quit\''); 186 | exec('osascript -e \'tell application "Simulator" to quit\''); 187 | 188 | let device = this._getDeviceWithStatus('Shutting Down'); 189 | 190 | while (device) { 191 | log.v(TAG, `Awaiting for shutdown completed (Device ${device.name} has ` 192 | + `state ${device.state})`); 193 | await delay(1000); 194 | device = this._getDeviceWithStatus('Shutting Down'); 195 | } 196 | 197 | log.v(TAG, 'Devices stopped'); 198 | } 199 | } 200 | 201 | export default IOSSimulator; 202 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidDevice.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | describe('AndroidDevice', () => { 3 | it('initialise', () => {}); 4 | }); 5 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidEmulator.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | jest.mock('child_process', () => ({ spawn: jest.fn() })); 3 | jest.mock('../../exec', () => jest.fn(() => '')); 4 | jest.mock('../../delay', () => jest.fn()); 5 | jest.mock('../../log', () => ({ 6 | v: jest.fn(), 7 | d: jest.fn(), 8 | e: jest.fn(), 9 | })); 10 | jest.mock('../AndroidEmulatorCmd', () => 'emulator'); 11 | 12 | const { spawn } = require('child_process'); 13 | 14 | const AndroidEmulator = require('../AndroidEmulator').default; 15 | const exec = require('../../exec'); 16 | const delay = require('../../delay'); 17 | 18 | describe('AndroidEmulator', () => { 19 | const name = 'emulator_name'; 20 | 21 | beforeEach(() => { 22 | jest.resetAllMocks(); 23 | }); 24 | 25 | it('initialise emulator', () => { 26 | // $FlowFixMe: ignore for mock 27 | exec.mockImplementationOnce(() => 'avd devices'); 28 | 29 | const emularor = new AndroidEmulator(name); 30 | expect(emularor).toMatchSnapshot(); 31 | }); 32 | 33 | it('start emulator when it is not available should throw error', async () => { 34 | // $FlowFixMe: ignore for mocks 35 | exec.mockImplementation(() => 'avd devices'); 36 | 37 | const emularor = new AndroidEmulator(name); 38 | let exception; 39 | 40 | try { 41 | await emularor.start(); 42 | } catch (err) { 43 | exception = err; 44 | } 45 | 46 | expect(exception).toMatchSnapshot(); 47 | }); 48 | 49 | it('start emulator when it is available but not started throws error if not started', async () => { 50 | // $FlowFixMe: ignore for mock 51 | exec.mockImplementation(() => `avd devices including ${name}`); 52 | const spawnMock = { 53 | stdout: { on: jest.fn() }, 54 | stderr: { on: jest.fn() }, 55 | on: jest.fn(), 56 | }; 57 | spawn.mockImplementationOnce(() => spawnMock); 58 | 59 | const emularor = new AndroidEmulator(name); 60 | 61 | let exception; 62 | 63 | try { 64 | await emularor.start(); 65 | } catch (err) { 66 | exception = err; 67 | } 68 | 69 | expect(exception).toMatchSnapshot(); 70 | }); 71 | 72 | it('start emulator when it is available but not started', async () => { 73 | // $FlowFixMe: ignore for mock 74 | exec.mockImplementation(() => `avd devices including ${name}`); 75 | const spawnMock = { 76 | stdout: { on: jest.fn() }, 77 | stderr: { on: jest.fn() }, 78 | on: jest.fn(), 79 | }; 80 | spawn.mockImplementationOnce(() => spawnMock); 81 | 82 | const emularor = new AndroidEmulator(name); 83 | 84 | const startPromise = emularor.start(); 85 | 86 | const dataCallback = spawnMock.stdout.on.mock.calls[0][1]; 87 | dataCallback('boot completed'); 88 | 89 | await startPromise; 90 | }); 91 | 92 | it('start emulator when it is available and already started should stop it before starting', async () => { 93 | // $FlowFixMe: ignore for mock 94 | exec.mockImplementation((cmd) => { 95 | if (cmd === 'emulator -avd -list-avds') { 96 | return `avd devices including ${name}`; 97 | } 98 | return 'List of devices attached\nemulator-5554 device'; 99 | }); 100 | const spawnMock = { 101 | stdout: { on: jest.fn() }, 102 | stderr: { on: jest.fn() }, 103 | on: jest.fn(), 104 | }; 105 | 106 | spawn.mockImplementationOnce(() => spawnMock); 107 | // $FlowFixMe: ignore for mocks 108 | delay.mockImplementation(() => { 109 | const spawnMockCalls = spawnMock.stdout.on.mock.calls; 110 | if (spawnMockCalls && spawnMockCalls[0] && spawnMockCalls[0][1]) { 111 | const dataCallback = spawnMock.stdout.on.mock.calls[0][1]; 112 | dataCallback('boot completed'); 113 | } 114 | }); 115 | 116 | const emularor = new AndroidEmulator(name); 117 | 118 | const startPromise = emularor.start(); 119 | 120 | await startPromise; 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/AndroidEmulatorCmd.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | describe('AndroidEmulatorCmd', () => { 4 | beforeEach(() => { 5 | process.env.ANDROID_EMULATOR = ''; 6 | jest.resetModules(); 7 | }); 8 | 9 | it('returns command provided via ANDROID_EMULATOR', () => { 10 | process.env.ANDROID_EMULATOR = 'cmdFrom_ANDROID_EMULATOR'; 11 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 12 | 13 | expect(emulatorCmd).toBe(process.env.ANDROID_EMULATOR); 14 | }); 15 | 16 | it('returns command available in PATH', () => { 17 | process.env.ANDROID_EMULATOR = ''; 18 | jest.mock('../../isCommand', () => () => true); 19 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 20 | 21 | expect(emulatorCmd).toBe('emulator'); 22 | }); 23 | 24 | it('gets command from /Library/Android/sdk/emulator/emulator on mac', () => { 25 | process.env.ANDROID_EMULATOR = ''; 26 | jest.mock('../../isCommand', () => () => false); 27 | jest.mock('../../exec', () => () => 'Darwin'); 28 | const emulatorCmd = require('../AndroidEmulatorCmd').default; 29 | 30 | expect(emulatorCmd 31 | .indexOf('Library/Android/sdk/emulator/emulator') > 0).toBe(true); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/IosSimulator.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | describe('IOSSimulator', () => { 3 | it('initialise', () => {}); 4 | }); 5 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/__snapshots__/AndroidEmulator.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AndroidEmulator initialise emulator 1`] = ` 4 | AndroidEmulator { 5 | "_canStopDevice": false, 6 | "_name": "emulator_name", 7 | } 8 | `; 9 | 10 | exports[`AndroidEmulator start emulator when it is available but not started throws error if not started 1`] = `[Error: Device is not loaded in 30 seconds]`; 11 | 12 | exports[`AndroidEmulator start emulator when it is not available should throw error 1`] = `[Error: Invalid emulator emulator_name]`; 13 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/__snapshots__/deviceProvider.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`deviceProvider provide Android Emulator 1`] = ` 4 | _class { 5 | "_name": "AndroidEmulator", 6 | "deviceName": "test", 7 | } 8 | `; 9 | 10 | exports[`deviceProvider provide AndroidDevice 1`] = ` 11 | _class2 { 12 | "_name": "AndroidDevice", 13 | "deviceName": "test", 14 | } 15 | `; 16 | 17 | exports[`deviceProvider provide iOS device throws error (not implemented) 1`] = `[Error: iOS devices are not supported yet]`; 18 | 19 | exports[`deviceProvider provide iOS simulator 1`] = ` 20 | _class3 { 21 | "_name": "IosSimulator", 22 | "deviceName": "test", 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /src/runner/utils/device/__tests__/deviceProvider.js: -------------------------------------------------------------------------------- 1 | import getDevice from '../deviceProvider'; 2 | 3 | jest.mock('../AndroidEmulator', () => (class { 4 | constructor(name) { 5 | this._name = 'AndroidEmulator'; 6 | this.deviceName = name; 7 | } 8 | })); 9 | jest.mock('../AndroidDevice', () => (class { 10 | constructor(name) { 11 | this._name = 'AndroidDevice'; 12 | this.deviceName = name; 13 | } 14 | })); 15 | jest.mock('../IosSimulator', () => (class { 16 | constructor(name) { 17 | this._name = 'IosSimulator'; 18 | this.deviceName = name; 19 | } 20 | })); 21 | 22 | describe('deviceProvider', () => { 23 | it('provide Android Emulator', () => { 24 | const device = getDevice('test', 'android'); 25 | expect(device).toMatchSnapshot(); 26 | }); 27 | 28 | it('provide AndroidDevice', () => { 29 | const device = getDevice('test', 'android', true); 30 | expect(device).toMatchSnapshot(); 31 | }); 32 | 33 | it('provide iOS simulator', () => { 34 | const device = getDevice('test', 'ios'); 35 | expect(device).toMatchSnapshot(); 36 | }); 37 | 38 | it('provide iOS device throws error (not implemented)', () => { 39 | let error; 40 | let device; 41 | try { 42 | device = getDevice('test', 'ios', true); 43 | } catch (err) { 44 | error = err; 45 | } 46 | 47 | expect(device).toBe(undefined); 48 | expect(error).toMatchSnapshot(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/runner/utils/device/deviceProvider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2019 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import type { DeviceInterface } from './DeviceInterface'; 8 | 9 | import log from '../log'; 10 | import AndroidEmulator from './AndroidEmulator'; 11 | import AndroidDevice from './AndroidDevice'; 12 | import IosSimulator from './IosSimulator'; 13 | 14 | const TAG = 'PIXELS_CATCHER::DEVICE_PROVIDER'; 15 | 16 | export default ( 17 | name: string, 18 | platform: string, 19 | isPhysicalDevice?: boolean, 20 | canStopDevice: boolean = true, 21 | ): DeviceInterface => { 22 | if (platform === 'android') { 23 | return isPhysicalDevice 24 | ? new AndroidDevice(name) 25 | : new AndroidEmulator(name, canStopDevice); 26 | } 27 | 28 | if (!isPhysicalDevice) { 29 | return new IosSimulator(name, canStopDevice); 30 | } 31 | 32 | log.e(TAG, 'iOS devices are not supported yet'); 33 | throw new Error('iOS devices are not supported yet'); 34 | }; 35 | -------------------------------------------------------------------------------- /src/runner/utils/exec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as childProcess from 'child_process'; 8 | 9 | import log from './log'; 10 | 11 | const TAG = 'PIXELS_CATCHER::UTIL_EXEC'; 12 | 13 | export default function exec(cmd: string): string { 14 | let result = ''; 15 | 16 | try { 17 | result = childProcess.execSync(cmd).toString(); 18 | } catch (err) { 19 | log.e(TAG, `Failed to execute [${cmd}], error: [${err instanceof Error ? err.message : 'Unknown error'}]`, err); 20 | } 21 | 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /src/runner/utils/isCommand.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import exec from './exec'; 8 | 9 | function isCommand(cmd: string): boolean { 10 | const out = exec(`whereis ${cmd}`); 11 | 12 | return Boolean(out.trim()); 13 | } 14 | 15 | export default isCommand; 16 | -------------------------------------------------------------------------------- /src/runner/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | const TAG = 'PIXELS_CATCHER::UTIL_LOG'; 8 | const logLevels: { 9 | [key: string]: number 10 | } = { 11 | v: 4, 12 | d: 3, 13 | i: 2, 14 | w: 1, 15 | e: 0, 16 | }; 17 | let activeLevel = logLevels.i; 18 | 19 | const log: { 20 | [key: string]: Function 21 | } = { 22 | v(tag: string, ...args: any) { 23 | if (activeLevel >= logLevels.v) { 24 | global.console.log(`${tag}:`, ...args); 25 | } 26 | }, 27 | 28 | d: (tag: string, ...args: any) => { 29 | if (activeLevel >= logLevels.d) { 30 | global.console.log(`${tag}:`, ...args); 31 | } 32 | }, 33 | 34 | i: (tag: string, ...args: any) => { 35 | if (activeLevel >= logLevels.i) { 36 | global.console.log(`${tag}:`, ...args); 37 | } 38 | }, 39 | 40 | w: (tag: string, ...args: any) => { 41 | if (activeLevel >= logLevels.w) { 42 | global.console.log(`${tag} WARNING:`, ...args); 43 | } 44 | }, 45 | 46 | e: (tag: string, ...args: any) => { 47 | if (activeLevel >= logLevels.e) { 48 | global.console.log(`${tag} ERROR:`, ...args); 49 | } 50 | }, 51 | 52 | setLevel(level: string | undefined = 'i') { 53 | let nextLevel = logLevels[level]; 54 | if (nextLevel === undefined) { 55 | global.console.log(`${TAG} WARNING:`, `Invalid level [${level}]. Supported levels: ${Object.keys(logLevels).join(', ')}`); 56 | nextLevel = logLevels.i; 57 | } 58 | activeLevel = nextLevel; 59 | }, 60 | }; 61 | 62 | export default log; 63 | -------------------------------------------------------------------------------- /src/runner/utils/readConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as fs from 'fs'; 8 | import * as path from 'path'; 9 | 10 | import log from './log'; 11 | 12 | const TAG = 'PIXELS_CATCHER::UTIL_READ_CONFIG'; 13 | const CONFIG_FILE = 'pixels-catcher.json'; 14 | const PACKAGE_JSON_FILE = 'package.json'; 15 | 16 | const readConfigFromPackageJSON = (): any => { 17 | const projectPackageFile = path.join(process.cwd(), PACKAGE_JSON_FILE); 18 | 19 | if (!fs.existsSync(projectPackageFile)) { 20 | log.e(TAG, `Cannot find ${PACKAGE_JSON_FILE} file [${projectPackageFile}]. ` 21 | + 'Check that you started the script from the root of your application'); 22 | process.exit(-1); 23 | } 24 | 25 | const fileContent = fs.readFileSync(projectPackageFile, 'utf8'); 26 | 27 | return JSON.parse(fileContent).PixelsCatcher; 28 | }; 29 | 30 | const readConfigFromFile = (): any => { 31 | const configFile = path.join(process.cwd(), CONFIG_FILE); 32 | 33 | if (!fs.existsSync(configFile)) { 34 | log.w(TAG, `Cannot find [${configFile}] file`); 35 | return undefined; 36 | } 37 | 38 | const fileContent = fs.readFileSync(configFile, 'utf8'); 39 | 40 | return JSON.parse(fileContent); 41 | }; 42 | 43 | export default (): any => { 44 | const pixelsCatcherConfig = readConfigFromPackageJSON() 45 | || readConfigFromFile(); 46 | 47 | if (!pixelsCatcherConfig) { 48 | log.e(TAG, 'Cannot find "PixelsCatcher" in package.json or find ' 49 | + 'pixels-catcher.json file'); 50 | process.exit(-1); 51 | } 52 | 53 | return pixelsCatcherConfig; 54 | }; 55 | -------------------------------------------------------------------------------- /src/runner/utils/timeToSec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Maksym Rusynyk 2018 - present 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const timeToSec = (ms: number): number => { 9 | const sec = ms / 1000; 10 | return Math.round(sec * 1000) / 1000; 11 | }; 12 | 13 | export default timeToSec; 14 | --------------------------------------------------------------------------------