├── .gitignore ├── .npmignore ├── .babelrc ├── .watchmanconfig ├── sample-app └── CavyDirectory │ └── README.md ├── .github └── dependabot.yml ├── index.js ├── src ├── useCavy.js ├── __test__ │ └── TestHookStore.test.js ├── TestHookStore.js ├── generateTestHook.js ├── Reporter.js ├── hook.js ├── wrap.js ├── Tester.js ├── TestRunner.js └── TestScope.js ├── LICENSE ├── package.json ├── .circleci └── config.yml ├── CODE_OF_CONDUCT.md ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .DS_Store 4 | *.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | sample-app 2 | .circleci/config.yml 3 | .github 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["module:metro-react-native-babel-preset"] 3 | } 4 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": [ 3 | ".git", 4 | "node_modules", 5 | "sample-app/CavyDirectory" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /sample-app/CavyDirectory/README.md: -------------------------------------------------------------------------------- 1 | # Moved 2 | 3 | The sample application now lives at 4 | [pixielabs/cavy-sample-app](https://github.com/pixielabs/cavy-sample-app). 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import hook from './src/hook'; 2 | import Tester from './src/Tester'; 3 | import TestHookStore from './src/TestHookStore'; 4 | import useCavy from './src/useCavy'; 5 | import wrap from './src/wrap'; 6 | 7 | const Cavy = { 8 | hook, 9 | Tester, 10 | TestHookStore, 11 | useCavy, 12 | wrap 13 | }; 14 | 15 | module.exports = Cavy; 16 | -------------------------------------------------------------------------------- /src/useCavy.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { TesterContext } from './Tester'; 3 | import generateTestHook from './generateTestHook'; 4 | 5 | // Public: Call `useCavy()` in a functional component. This returns a function 6 | // that you can pass into inner components' refs to add that component to the 7 | // testHookStore for later use in specs. 8 | // 9 | // Example 10 | // 11 | // import { useCavy } from 'cavy'; 12 | // 13 | // export default () => { 14 | // const generateTestHook = useCavy(); 15 | // return 16 | // } 17 | // 18 | // Returns the ref generating function for use in functional components. 19 | export default function useCavy() { 20 | const testHookStore = useContext(TesterContext); 21 | return generateTestHook(testHookStore); 22 | } 23 | -------------------------------------------------------------------------------- /src/__test__/TestHookStore.test.js: -------------------------------------------------------------------------------- 1 | import TestHookStore from '../TestHookStore' 2 | 3 | describe("TestHookStore", () => { 4 | it("initializes hooks by default", () => { 5 | const testHookStore = new TestHookStore(); 6 | 7 | expect(testHookStore.hooks).toBeDefined(); 8 | }); 9 | 10 | it("can add components", () => { 11 | const testHookStore = new TestHookStore(); 12 | testHookStore.add("key", "value"); 13 | 14 | expect(testHookStore.get("key")).toEqual("value"); 15 | }) 16 | 17 | it("overrides when adding an existing component", () => { 18 | const testHookStore = new TestHookStore(); 19 | testHookStore.add("key", "value1"); 20 | testHookStore.add("key", "value2"); 21 | 22 | expect(testHookStore.get("key")).toEqual("value2"); 23 | }); 24 | 25 | it("can delete components", () => { 26 | const testHookStore = new TestHookStore(); 27 | testHookStore.add("key", "value"); 28 | testHookStore.remove("key"); 29 | 30 | expect(testHookStore.get("key")).toBeUndefined(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Pixie Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cavy", 3 | "version": "4.0.2", 4 | "description": "An integration test framework for React Native.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/pixielabs/cavy.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "react native", 16 | "ios", 17 | "android", 18 | "integration tests", 19 | "testing", 20 | "test", 21 | "specs" 22 | ], 23 | "peerDependencies": { 24 | "react-native": ">= 0.59.0 <= 0.66.0", 25 | "react": "^16.8.3 || ^17.0.0" 26 | }, 27 | "author": "Pixie Labs", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/pixielabs/cavy/issues" 31 | }, 32 | "homepage": "https://github.com/pixielabs/cavy#readme", 33 | "dependencies": { 34 | "hoist-non-react-statics": "^3.3.0", 35 | "prop-types": "^15.5.10" 36 | }, 37 | "devDependencies": { 38 | "@babel/runtime": "^7.4.3", 39 | "babel-jest": "^27.3.1", 40 | "jest": "^27.3.1", 41 | "metro-react-native-babel-preset": "^0.66.2", 42 | "react": "^17.0.2", 43 | "regenerator-runtime": "^0.13.9" 44 | }, 45 | "jest": { 46 | "testPathIgnorePatterns": [ 47 | "/node_modules/", 48 | "/sample-app/", 49 | "/test/CavyTester/" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/TestHookStore.js: -------------------------------------------------------------------------------- 1 | // Public: A TestHookStore stores flattened references to UI components in your 2 | // app that you want to interact with as part of your integration tests. 3 | // 4 | // See Tester.js for an example of instantiating a TestHookStore for use with 5 | // a `` component. 6 | export default class TestHookStore { 7 | constructor() { 8 | this.hooks = {}; 9 | } 10 | 11 | // Internal: Add a new component into the store. If there is an existing 12 | // component with that identifier, replace it. 13 | // 14 | // identifier - String, a unique identifier for this component. To help 15 | // separate out hooked components, use dot namespaces e.g. 16 | // 'MyScene.mycomponent'. 17 | // component - Component returned by React `ref` function. 18 | // 19 | // Returns undefined. 20 | add(identifier, component) { 21 | this.hooks[identifier] = component; 22 | } 23 | 24 | // Internal: Remove a component from the store. 25 | // 26 | // Returns undefined. 27 | remove(identifier) { 28 | delete this.hooks[identifier]; 29 | } 30 | 31 | // Internal: Fetch a component from the store. 32 | // 33 | // Returns the component corresponding to the provided identifier, or 34 | // undefined if it has not been added. 35 | get(identifier) { 36 | return this.hooks[identifier]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | ios: 4 | macos: 5 | xcode: 13.0.0 6 | 7 | steps: 8 | - checkout 9 | 10 | - run: 11 | name: Install React Native dependencies 12 | command: | 13 | brew update 14 | brew install watchman cocoapods yarn || true 15 | brew link --overwrite cocoapods || true 16 | 17 | - run: 18 | name: Install Node dependencies 19 | command: | 20 | npm ci --legacy-peer-deps 21 | 22 | - run: 23 | name: Run unit tests 24 | command: npm test 25 | 26 | - run: 27 | name: Install command line interfaces 28 | command: npm install -g cavy-cli wml 29 | 30 | - run: 31 | name: Clone CavyTester repo 32 | command: 33 | git clone https://github.com/pixielabs/cavy-tester.git ~/cavy-tester 34 | 35 | - run: 36 | name: Install CavyTester dependencies 37 | command: | 38 | cd ~/cavy-tester 39 | yarn 40 | cd ios 41 | pod install 42 | 43 | - run: 44 | name: Link local Cavy version 45 | command: | 46 | (sleep 1; echo -e 'y\n'; sleep 1; echo -e 'y\n') | wml add ~/project ~/cavy-tester/node_modules/cavy 47 | 48 | - run: 49 | name: Build app and run tests 50 | command: | 51 | cd ~/cavy-tester 52 | rm -rf $TMPDIR/react-* $TMPDIR/haste-* $TMPDIR/metro-* 53 | watchman watch-del-all 54 | wml start & 55 | npx react-native start --reset-cache & 56 | cavy run-ios 57 | 58 | workflows: 59 | version: 2 60 | ios: 61 | jobs: 62 | - ios 63 | -------------------------------------------------------------------------------- /src/generateTestHook.js: -------------------------------------------------------------------------------- 1 | // Public: Returns our `generateTestHook` function that in turn returns the ref 2 | // generating function that adds components to the testHookStore. 3 | // 4 | // testHookStore - An instance of a TestHookStore, either from this.context if 5 | // called from within a TesterContext consumer component, or from useContext() 6 | // hook if called from within our own custom useCavy() hook. 7 | // 8 | export default function(testHookStore) { 9 | // Public: Returns a ref generating function that adds the component itself 10 | // to the testHookStore for later use in specs. 11 | // 12 | // identifier - String, the key the component will be stored under in the 13 | // test hook store. 14 | // f - Your own ref generating function or ref (optional). 15 | // 16 | return function generateTestHook(identifier, ref) { 17 | // Returns the component, preserving any user's own ref generating function 18 | // f() or ref attribute created via React.createRef. 19 | // Adds the component to the testHookStore if defined. 20 | 21 | const registerRef = (component) => { 22 | // support for callback refs 23 | if (typeof ref == 'function') { 24 | ref(component); 25 | } 26 | // support for createRef and useRef 27 | if (ref && typeof ref == 'object') { 28 | ref.current = component; 29 | } 30 | } 31 | return (component) => { 32 | if (!testHookStore) { 33 | return registerRef(component) 34 | } 35 | 36 | if (component) { 37 | testHookStore.add(identifier, component); 38 | } else { 39 | testHookStore.remove(identifier); 40 | } 41 | 42 | return registerRef(component) 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/Reporter.js: -------------------------------------------------------------------------------- 1 | // Internal: Reporter is responsible for sending the test results to 2 | // the CLI. 3 | export default class Reporter { 4 | constructor() { 5 | // Set the Reporter type to realtime. 6 | this.type = 'realtime'; 7 | } 8 | 9 | // Internal: Creates a websocket connection to the cavy-cli server. 10 | onStart() { 11 | const url = 'ws://127.0.0.1:8082/'; 12 | this.ws = new WebSocket(url); 13 | } 14 | 15 | // Internal: Send a single test result to cavy-cli over the websocket connection. 16 | send(result) { 17 | if (this.websocketReady()) { 18 | testData = { event: 'singleResult', data: result }; 19 | this.sendData(testData); 20 | } 21 | } 22 | 23 | // Internal: Send report to cavy-cli over the websocket connection. 24 | onFinish(report) { 25 | if (this.websocketReady()) { 26 | testData = { event: 'testingComplete', data: report }; 27 | this.sendData(testData); 28 | } else { 29 | // If cavy-cli is not running, let people know in a friendly way 30 | const message = "Skipping sending test report to cavy-cli - if you'd " + 31 | 'like information on how to set up cavy-cli, check out the README ' + 32 | 'https://github.com/pixielabs/cavy-cli'; 33 | 34 | console.log(message); 35 | } 36 | } 37 | 38 | // Private: Determines whether data can be sent over the websocket. 39 | websocketReady() { 40 | // WebSocket.readyState 1 means the web socket connection is OPEN. 41 | return this.ws.readyState == 1; 42 | } 43 | 44 | // Private: Sends data over the websocket and console logs any errors. 45 | sendData(testData) { 46 | try { 47 | this.ws.send(JSON.stringify(testData)); 48 | if (testData.event == 'testingComplete') { 49 | console.log('Cavy test report successfully sent to cavy-cli'); 50 | } 51 | } catch (e) { 52 | console.group('Error sending test data'); 53 | console.warn(e.message); 54 | console.groupEnd(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/hook.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import hoistNonReactStatic from 'hoist-non-react-statics'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import TestHookStore from './TestHookStore'; 6 | import { TesterContext } from './Tester'; 7 | import generateTestHook from './generateTestHook'; 8 | 9 | // Public: Higher-order React component to factilitate adding hooks to the 10 | // global test hook store. Once you've hooked your main component (see example 11 | // below), you can set an inner component's ref with `this.props.generateTestHook` 12 | // to add it to the testHookStore for later use in a spec. 13 | // 14 | // React will call `generateTestHook` twice during the render lifecycle; once to 15 | // 'unset' the ref, and once to set it. 16 | // 17 | // WrappedComponent - Component to be wrapped, will be passed an initial 18 | // property called 'generateTestHook' which is a function 19 | // generator that will add a component to the testHookStore. 20 | // 21 | // Example 22 | // 23 | // import { hook } from 'cavy'; 24 | // 25 | // class MyComponent extends React.Component { 26 | // 27 | // render() { 28 | // const { generateTestHook } = this.props; 29 | // return ( 30 | // this.textInput = c)} 32 | // ... 33 | // /> 34 | //