├── .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 | //
38 | // }
39 | // }
40 | // }
41 | //
42 | // const TestableMyComponent = hook(MyComponent);
43 | // export default TestableMyComponent;
44 | //
45 | // Returns the new component with the ref generating function generateTestHook as a prop.
46 | export default function hook(WrappedComponent) {
47 | const WrapperComponent = class extends Component {
48 | render() {
49 | const testHookStore = this.context;
50 | return (
51 |
55 | )
56 | }
57 | };
58 | // Set the context type.
59 | WrapperComponent.contextType = TesterContext;
60 | // Copy all non-React static methods.
61 | hoistNonReactStatic(WrapperComponent, WrappedComponent);
62 | // Wrap the display name for easy debugging.
63 | WrapperComponent.displayName = `Hook(${getDisplayName(WrappedComponent)})`
64 |
65 | return WrapperComponent;
66 | }
67 |
68 | function getDisplayName(WrappedComponent) {
69 | return WrappedComponent.displayName || WrappedComponent.name || 'Component';
70 | }
71 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at . All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Cavy
6 |
7 | [](https://badge.fury.io/js/cavy) [](https://circleci.com/gh/pixielabs/cavy)
8 |
9 | **Cavy** is a cross-platform, integration test framework for React Native, by
10 | [Pixie Labs](http://pixielabs.io).
11 |
12 | Cavy tests allow you to programmatically interact with deeply nested components
13 | within your application. Write your tests in pure JavaScript and run them on
14 | both Android and iOS.
15 |
16 | Cavy tests look like this:
17 | ```js
18 | export default function(spec) {
19 | spec.describe('A list of the employees', function() {
20 | spec.it('can be filtered by search input', async function() {
21 | await spec.exists('EmployeeList.JimCavy');
22 | await spec.fillIn('SearchBar.TextInput', 'Amy');
23 | await spec.press('Button.FilterSubmit');
24 | await spec.notExists('EmployeeList.JimCavy');
25 | await spec.exists('EmployeeList.AmyTaylor');
26 | });
27 | });
28 | }
29 | ```
30 |
31 | ## 📋 Requirements
32 | - React Native >= 0.59
33 | - React >= 16.8.0
34 |
35 | ## 👶 Getting started
36 | Get set up with Cavy by following our
37 | [installation guide](https://cavy.app/docs/getting-started/installing).
38 |
39 | You might also want to
40 | [check out some articles and watch talks about Cavy](https://cavy.app/media) to
41 | find out a bit more before you write code.
42 |
43 | If you need some inspiration, head over to Cavy's
44 | [sample app](https://github.com/pixielabs/cavy-sample-app), follow the
45 | instructions in the README, and see Cavy in action.
46 |
47 | ## 📘 Documentation
48 | Full documentation and guides for Cavy can be found on our
49 | [website](https://cavy.app).
50 |
51 | ## 🗺️ Development roadmap
52 | Take a look at our public
53 | [Pivotal Tracker](https://www.pivotaltracker.com/n/projects/2447582) to see what
54 | we're currently working on, and what features we plan to add to Cavy next.
55 |
56 | ## 💯 Contributing
57 | When making changes to Cavy, it's useful to have the
58 | [CavyTester](https://github.com/pixielabs/cavy-tester) app running in
59 | development for regression testing.
60 |
61 | Follow the instructions it's own README on how to get the tester app running
62 | against a local version of the Cavy library.
63 |
64 | Here you'll also find instructions on adding new test cases to ensure
65 | your functionality is fully tested. Please do this :)
66 |
67 | Before contributing, please read the [code of conduct](CODE_OF_CONDUCT.md).
68 |
69 | - Check out the latest master to make sure the feature hasn't been implemented
70 | or the bug hasn't been fixed yet.
71 | - Check out the issue tracker to make sure someone already hasn't requested it
72 | and/or contributed it.
73 | - Fork the project.
74 | - Start a feature/bugfix branch.
75 | - Commit and push until you are happy with your contribution.
76 | - Remember to
77 | [submit a PR to DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/cavy)
78 | to update the type definitions if you've changed a function's inputs or outputs.
79 | - Remember to submit a PR to update
80 | [the documentation](https://github.com/pixielabs/cavy-app) if you've changed
81 | how something works.
82 | - Please try not to mess with the package.json, version, or history. If you
83 | want to have your own version, or is otherwise necessary, that is fine, but
84 | please isolate to its own commit so we can cherry-pick around it.
85 |
--------------------------------------------------------------------------------
/src/wrap.js:
--------------------------------------------------------------------------------
1 | import React, { useImperativeHandle, forwardRef } from 'react';
2 | import hoistNonReactStatics from 'hoist-non-react-statics';
3 |
4 | // With a Function Component:
5 | //
6 | // Higher-order component that wraps a Function Component in `forwardRef()`
7 | // and uses `useImperativeHandle` to make the properties of that component
8 | // available via the component ref so that Cavy can interact directly with it
9 | // via the testHookStore.
10 | //
11 | // More information on forwarding refs:
12 | //
13 | //
14 | // More information on `useImperativeHandle`:
15 | //
16 | //
17 | // Example
18 | //
19 | // import React from 'react';
20 | // import { Button } from 'react-native-elements';
21 | // import { useCavy, wrap } from 'cavy';
22 | //
23 | // export default () => {
24 | // const generateTestHook = useCavy();
25 | // const TestableButton = wrap(Button);
26 | //
27 | // return (
28 | //
29 | // )
30 | // };
31 | //
32 | //
33 | //
34 | // With a component like `Text`:
35 | //
36 | // Higher-order component that wraps a native component like `Text`, (neither
37 | // a React Class nor a Function Component), and returns a React Class with
38 | // testable props.
39 | //
40 | // Example:
41 | //
42 | // import React from 'react';
43 | // import { View, Text } from 'react-native';
44 | // import { useCavy, wrap } from 'cavy';
45 | //
46 | // export default ({ data }) => {
47 | // const generateTestHook = useCavy();
48 | // const TestableText = wrap(Text);
49 | //
50 | // return (
51 | //
52 | //
53 | // {data.title}
54 | //
55 | //
56 | // )
57 | // };
58 | //
59 | export default function wrap(Component) {
60 | if (typeof Component === 'function' && isNotReactClass(Component)) {
61 | // `forwardRef` accepts a render function that receives our props and ref.
62 | return forwardRef((props, ref) => {
63 | // It returns the wrapped component after calling `useImperativeHandle`, so
64 | // that our ref can be used to call the inner function component's props.
65 | useImperativeHandle(ref, () => ({ props }));
66 | return Component(props);
67 | });
68 | }
69 |
70 | // For native components like , the RN component itself is a plain
71 | // object, not a function. For these components, the RN renderer returns
72 | // an instance of `ReactNativeFiberHostComponent`, which does not expose any
73 | // props. Users should wrap these components - here we're wrapping them and
74 | // in a class component and exposing testable props.
75 | if (typeof Component == 'object') {
76 | class WrapperComponent extends React.Component {
77 | render() {
78 | return
79 | }
80 | }
81 | // Copy all non-React static methods.
82 | hoistNonReactStatics(WrapperComponent, Component);
83 | // Wrap the display name for easy debugging.
84 | WrapperComponent.displayName = `Wrap(${getDisplayName(Component)})`;
85 | return WrapperComponent;
86 | }
87 |
88 | const message = "Looks like you're passing a class component into `wrap` - " +
89 | "you don't need to do this. Attach a Cavy ref to the component itself."
90 |
91 | console.warn(message);
92 | return Component;
93 | }
94 |
95 | // React Class components are also functions, so we need to perform some extra
96 | // checks here. This code is taken from examples in React source code e.g:
97 | //
98 | // https://github.com/facebook/react/blob/12be8938a5d71ffdc21ee7cf770bf1cb63ae038e/packages/react-refresh/src/ReactFreshRuntime.js#L138
99 | function isNotReactClass(Component) {
100 | return !(Component.prototype && Component.prototype.isReactComponent);
101 | }
102 |
103 | function getDisplayName(Component) {
104 | return Component.displayName || Component.name || 'Component';
105 | }
106 |
--------------------------------------------------------------------------------
/src/Tester.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Children } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { AsyncStorage } from 'react-native';
4 |
5 | import TestHookStore from './TestHookStore';
6 | import TestScope from './TestScope';
7 | import TestRunner from './TestRunner';
8 | import Reporter from './Reporter';
9 |
10 | // Public: Wrap your entire app in Tester to run tests against that app,
11 | // interacting with registered components in your test cases via the Cavy
12 | // helpers (defined in TestScope).
13 | //
14 | // This component wraps your app inside a which ensures
15 | // the testHookStore is in scope when Cavy runs your tests.
16 | //
17 | // store - An instance of TestHookStore.
18 | // specs - An array of spec functions.
19 | // reporter - A function that is called with the test report as an
20 | // argument in place of sending the report to cavy-cli. If
21 | // no reporter prop is present, then Cavy will by default
22 | // send the report to cavy-cli if the test server is running.
23 | // waitTime - An integer representing the time in milliseconds that
24 | // the testing framework should wait for the function
25 | // findComponent() to return the 'hooked' component.
26 | // startDelay - An integer representing the time in milliseconds before
27 | // test execution begins.
28 | // clearAsyncStorage - A boolean to determine whether to clear AsyncStorage
29 | // between each test. Defaults to `false`.
30 | //
31 | // Example
32 | //
33 | // import { Tester, TestHookStore } from 'cavy';
34 | //
35 | // import MyFeatureSpec from './specs/MyFeatureSpec';
36 | // import OtherFeatureSpec from './specs/OtherFeatureSpec';
37 | //
38 | // const testHookStore = new TestHookStore();
39 | //
40 | // export default class AppWrapper extends React.Component {
41 | // // ....
42 | // render() {
43 | // return (
44 | //
45 | //
46 | //
47 | // );
48 | // }
49 | // }
50 | //
51 | export const TesterContext = React.createContext();
52 |
53 | export default class Tester extends Component {
54 | constructor(props, context) {
55 | super(props, context);
56 | this.state = {
57 | key: Math.random()
58 | };
59 | this.testHookStore = props.store;
60 | // Default to sending a test report to cavy-cli if no custom reporter is
61 | // supplied.
62 | if (props.reporter instanceof Function) {
63 | const message = 'Deprecation warning: support for custom function' +
64 | 'reporters will soon be deprecated. Cavy supports custom ' +
65 | 'class based reporters. For more info, see the ' +
66 | 'documentation here: ' +
67 | 'https://cavy.app/docs/guides/writing-custom-reporters';
68 | console.warn(message);
69 | this.reporter = props.reporter;
70 | } else {
71 | reporterClass = props.reporter || Reporter;
72 | this.reporter = new reporterClass;
73 | }
74 | }
75 |
76 | componentDidMount() {
77 | this.runTests();
78 | if (!(this.reporter instanceof Function)
79 | && this.reporter.type == 'realtime' ) {
80 | this.reporter.onStart();
81 | }
82 | }
83 |
84 | // Run all test suites.
85 | async runTests() {
86 | const { specs, waitTime, startDelay, sendReport, only } = this.props;
87 | const testSuites = [];
88 | // Iterate over each suite of specs and create a new TestScope for each.
89 | for (var i = 0; i < specs.length; i++) {
90 | const scope = new TestScope(this, waitTime);
91 | await specs[i](scope);
92 | testSuites.push(scope);
93 | }
94 |
95 | // Instantiate the test runner with the test suites, the reporter to use,
96 | // the startDelay to apply, and the `only` filter to apply.
97 | const runner = new TestRunner(
98 | this,
99 | testSuites,
100 | startDelay,
101 | this.reporter,
102 | sendReport,
103 | only
104 | );
105 |
106 | // Run the tests.
107 | runner.run();
108 | }
109 |
110 | reRender() {
111 | this.setState({key: Math.random()});
112 | }
113 |
114 | // Clear everything from AsyncStorage, warn if anything goes wrong.
115 | async clearAsync() {
116 | if (this.props.clearAsyncStorage) {
117 | try {
118 | await AsyncStorage.getAllKeys().then(AsyncStorage.multiRemove)
119 | } catch(e) {
120 | console.warn("[Cavy] failed to clear AsyncStorage:", e);
121 | }
122 | }
123 | }
124 |
125 | render() {
126 | return (
127 |
128 | {Children.only(this.props.children)}
129 |
130 | );
131 | }
132 | }
133 |
134 | Tester.propTypes = {
135 | clearAsyncStorage: PropTypes.bool,
136 | only: PropTypes.arrayOf(PropTypes.string),
137 | reporter: PropTypes.func,
138 | // Deprecated (see note in TestRunner component)
139 | sendReport: PropTypes.bool,
140 | specs: PropTypes.arrayOf(PropTypes.func),
141 | startDelay: PropTypes.number,
142 | store: PropTypes.instanceOf(TestHookStore),
143 | waitTime: PropTypes.number
144 | };
145 |
146 | Tester.defaultProps = {
147 | clearAsyncStorage: false,
148 | startDelay: 0,
149 | waitTime: 2000
150 | };
151 |
--------------------------------------------------------------------------------
/src/TestRunner.js:
--------------------------------------------------------------------------------
1 | // Internal: TestRunner is responsible for actually running through each
2 | // suite of tests and executing the specs.
3 | //
4 | // It also presents the test results and ensures a report is sent to cavy-cli
5 | // if necessary.
6 | //
7 | // component - the Tester component within which the app is wrapped.
8 | // testSuites - an array of TestScopes, each of which relate to a single suite
9 | // of tests.
10 | // startDelay - length of time in ms that cavy should wait before starting
11 | // tests.
12 | // reporter - the function called with the test report as an argument once
13 | // all tests have finished.
14 | //
15 | export default class TestRunner {
16 | constructor(component, testSuites, startDelay, reporter, sendReport, filter) {
17 | this.component = component;
18 | this.testSuites = testSuites;
19 | this.startDelay = startDelay;
20 | this.reporter = reporter;
21 | // Using the sendReport prop is deprecated - cavy checks whether the
22 | // cavy-cli server is listening and sends a report if true.
23 | this.shouldSendReport = sendReport;
24 | // An array of strings dictating the subset of tagged tests to run, or null
25 | // if all tests should be run.
26 | this.filter = filter;
27 | this.results = [];
28 | this.errorCount = 0;
29 | }
30 |
31 | // Internal: Start tests after optional delay time.
32 | async run() {
33 | if (this.startDelay) { await this.pause(this.startDelay)};
34 | this.runTestSuites();
35 | }
36 |
37 | // Internal: Synchronously runs each test suite one after the other,
38 | // sending a test report to cavy-cli if needed.
39 | async runTestSuites() {
40 | const start = new Date();
41 | console.log(`Cavy test suite started at ${start}.`);
42 |
43 | // Iterate through each suite...
44 | for (let i = 0; i < this.testSuites.length; i++) {
45 | // And then through the suite's test cases...
46 | for (let j = 0; j < this.testSuites[i].testCases.length; j++) {
47 | let scope = this.testSuites[i];
48 | let tag = scope.testCases[j].tag;
49 | // Run the test if:
50 | // 1. The test suite isn't filtered
51 | // 2. The test suite is filtered and test's tag is included
52 | if (!this.filter || (this.filter && this.filter.includes(tag))) {
53 | await this.runTest(scope, scope.testCases[j]);
54 | };
55 | }
56 | }
57 |
58 | const stop = new Date();
59 | const duration = (stop - start) / 1000;
60 | console.log(`Cavy test suite stopped at ${stop}, duration: ${duration} seconds.`);
61 |
62 | // Handle use of deprecated prop `sendReport` and honour previous expected
63 | // behaviour by not reporting results if set to false;
64 | if (this.shouldSendReport != undefined) {
65 | const message = 'Deprecation warning: using the `sendReport` prop is ' +
66 | 'deprecated. By default, Cavy now checks whether the ' +
67 | 'cavy-cli server is running and sends a report if a ' +
68 | 'connection is detected.'
69 | console.warn(message);
70 |
71 | if (!this.shouldSendReport) return;
72 | }
73 |
74 | const fullResults = {
75 | time: duration,
76 | timestamp: start,
77 | testCases: this.results
78 | }
79 |
80 | // Compile the report object.
81 | const report = {
82 | results: this.results,
83 | fullResults: fullResults,
84 | errorCount: this.errorCount,
85 | duration: duration
86 | }
87 |
88 | // Send report to reporter (default is cavy-cli)
89 | if (this.reporter instanceof Function) {
90 | await this.reporter(report);
91 | } else if (this.reporter.type == 'realtime') {
92 | await this.reporter.onFinish(report);
93 | } else if (this.reporter.type == 'deferred') {
94 | await this.reporter.send(report);
95 | } else {
96 | message = 'Could not find a valid reporter. For more ' +
97 | 'information on custom reporters, see the documentation ' +
98 | 'here: https://cavy.app/docs/guides/writing-custom-reporters';
99 | console.log(message);
100 | }
101 | }
102 |
103 | // Internal: Synchronously runs each test case within a test suite, outputting
104 | // on the console if the test passes or fails, and adding to testResult
105 | // array for reporting purposes.
106 | //
107 | // Order of actions:
108 | // 1. Clears AsyncStorage
109 | // 2. Calls a beforeEach function
110 | // 3. Re-renders the app
111 | // 4. Runs the test
112 | async runTest(scope, test) {
113 | const start = new Date();
114 |
115 | await this.component.clearAsync();
116 |
117 | if (scope.beforeEach) { await scope.beforeEach.call(scope) };
118 |
119 | this.component.reRender();
120 |
121 | const { describeLabel, label, f } = test;
122 | const description = `${describeLabel}: ${label}`;
123 |
124 | // Run the test, console logging the result.
125 | try {
126 | await f.call(scope);
127 | const stop = new Date();
128 | const time = (stop - start) / 1000;
129 |
130 | let successMsg = `${description} ✅`;
131 | console.log(successMsg);
132 |
133 | this.results.push({
134 | describeLabel: describeLabel,
135 | description: description,
136 | message: successMsg,
137 | passed: true,
138 | time: time
139 | });
140 |
141 | if (!(this.reporter instanceof Function)
142 | && this.reporter.type == 'realtime' ) {
143 | const result = { message: successMsg, passed: true };
144 | this.reporter.send(result);
145 | }
146 |
147 | } catch (e) {
148 | const stop = new Date();
149 | const time = (stop - start) / 1000;
150 |
151 | let fullErrorMessage = `${description} ❌\n ${e.message}`;
152 | console.warn(fullErrorMessage);
153 |
154 | this.results.push({
155 | describeLabel: describeLabel,
156 | description: description,
157 | message: fullErrorMessage,
158 | errorMessage: e.message,
159 | passed: false,
160 | time: time
161 | });
162 |
163 | if (!(this.reporter instanceof Function)
164 | && this.reporter.type == 'realtime' ) {
165 | const result = { message: fullErrorMessage, passed: false };
166 | this.reporter.send(result);
167 | }
168 |
169 | // Increase error count for reporting.
170 | this.errorCount += 1;
171 | }
172 | }
173 |
174 | // Internal: Pauses the test runner for a length of time.
175 | // Returns a promise.
176 | async pause(time) {
177 | let promise = new Promise((resolve, reject) => {
178 | setTimeout(function() {
179 | resolve();
180 | }, time);
181 | });
182 |
183 | return promise;
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 4.0.2
2 |
3 | - Open up react and react-native peer dependency versions. Thanks
4 | [alexburkowskypolysign](https://github.com/alexburkowskypolysign) for
5 | this.
6 | - Remove sample app from codebase (was ignored by npm anyway, so doesn't
7 | affect package.
8 | - Update CircleCI workflow.
9 |
10 | # 4.0.1
11 |
12 | - Fix bug in `generateTestHook`, preserving the functionality of users' own
13 | refs created using `useRef` and `createRef`. Thanks to
14 | [MrLoh](https://github.com/MrLoh) for the fix.
15 |
16 | # 4.0.0
17 |
18 | **BREAKING** This release includes updates to the default Cavy reporter to
19 | support new cavy-cli features. These changes are backwards compatible within
20 | Cavy itself, and require no changes to your code. However, if you're using
21 | cavy-cli you'll need to upgrade to version cavy-cli 2.0 to continue to run
22 | your tests.
23 |
24 | The updated default reporter now:
25 |
26 | - Opens a websocket connection to cavy-cli when your app boots. cavy-cli waits
27 | for a configurable length of time to receive this connection and exits with an
28 | error code if no connection is made.
29 | - Sends in-progress results to cavy-cli, so that you can see a log of each test
30 | as they pass/fail in realtime.
31 |
32 | This release also:
33 | - Adds a deprecation warning if an old-style reporter is used i.e. a reporter
34 | that is a function taking one argument (the test report object).
35 |
36 | # 3.4.1
37 |
38 | - Remove tester app from package.
39 |
40 | # 3.4.0
41 |
42 | - Add ability to tag and filter tests, allowing you to run a subset of tests per
43 | test run.
44 |
45 | # 3.3.0
46 |
47 | - Add `fullResults` to Cavy's test report object, containing more details about
48 | the test suite and individual test cases e.g. timings and test describe block
49 | labels. Cavy can now support outputting JUnit compatible test reports via
50 | [cavy-cli](https://github.com/pixielabs/cavy-cli).
51 |
52 | # 3.2.1
53 |
54 | - Fix bug in `containsText` helper function, whereby you couldn't test for a
55 | component containing a number. Thank you
56 | [Zooheck](https://github.com/Zooheck)!
57 |
58 | # 3.2.0
59 |
60 | - New `focus` spec helper function. Thanks [Austin](https://github.com/austinpgraham)!
61 |
62 | # 3.1.0
63 |
64 | - Extend `wrap` functionality so that it can also be used turn native components
65 | like `Text` into testable components.
66 | - New `containsText` spec helper function.
67 | - Use [hoist-non-react-statics](https://github.com/mridgway/hoist-non-react-statics)
68 | in `hook` HOC.
69 | - Add a `displayName` to the `hook` HOC for ease of debugging.
70 | - Upgrade React Native in the sample app to 0.59.9.
71 |
72 | # 3.0.0
73 |
74 | - **BREAKING** Fixed issue whereby props were being flattened on `wrap`-ped
75 | function components. This is a breaking change for those users manually
76 | fetching a component and accessing a flattened prop as a workaround. All props
77 | are now accessible through the `props` key as expected. Thanks to
78 | [FLGMwt](https://github.com/FLGMwt) for your help!
79 |
80 | # 2.2.1
81 |
82 | - Fix regression introduced in 2.2.0.
83 |
84 | # 2.2.0
85 |
86 | - Add support for passing refs created via `React.createRef` to `generateTestHook`.
87 |
88 | # 2.1.1
89 |
90 | - Fix confusing messaging when Cavy fails to connect to
91 | [cavy-cli](https://github.com/pixielabs/cavy-cli).
92 |
93 | # 2.1.0
94 |
95 | - Deprecate the `sendReport` prop. By default Cavy checks to see whether
96 | cavy-cli is running and sends the test report if a response is received.
97 | - Add the ability to use a custom `reporter` when running Cavy tests. If
98 | supplied, Cavy will send the test report to the custom reporter rather than
99 | cavy-cli.
100 |
101 | # 2.0.0
102 |
103 | - Add a `beforeEach` function that can be used on a per-spec basis. Thanks to
104 | [PatrickBRT](https://github.com/PatrickBRT) whose work inspired our approach!
105 | - **BREAKING** Clear AsyncStorage and re-render the app before each test runs.
106 | - Cavy no longer resets your app at the end of the test suite.
107 |
108 | # 1.1.0
109 |
110 | - Un-deprecate `wrap` (was deprecated in 0.6.0) and rewrite it using React
111 | Hooks. `wrap` is now the accepted way to test function components, replacing
112 | our previous recommendation to use Recompose's `toClass` (which has been
113 | deprecated in favour of React Hooks). 🎉
114 |
115 | # 1.0.0
116 |
117 | - **BREAKING** Drop official support for React Native < 0.59 and React < 16.8.0.
118 | - Update `` to use the newer Context API introduced in React 16.3.
119 | - Added a custom [React Hook](https://reactjs.org/docs/hooks-intro.html) called
120 | `useCavy()` which can be used to access `generateTestHook` from your
121 | functional components.
122 |
123 | This version brings Cavy in line with how people use React nowadays (moving
124 | towards using functional components). However React Hooks were added in React
125 | Native 0.59 and React 16.8.0, so you will need to upgrade your application to
126 | continue to use Cavy from version 1.0.0 onwards. You can continue to use
127 | 0.6.2 in the meantime.
128 |
129 | If you don't use `useCavy()` Cavy 1.0.0 should work with React Native >= 0.57.5
130 | which was [the earliest version that supported the new Context API](https://github.com/facebook/react-native/issues/21975)
131 | however this is not officially supported.
132 |
133 | # 0.6.2
134 |
135 | - Fix for when `clearAsyncStorage` option is used but there are no entries in
136 | AsyncStorage. Thanks [haikyuu](https://github.com/haikyuu)!
137 |
138 | # 0.6.1
139 |
140 | - Update `babel-presets-env` and `.babelrc`. Thanks
141 | [eriben](https://github.com/eriben).
142 |
143 | # 0.6.0
144 |
145 | - Remove `console.warn` when overwriting a component.
146 | - Add deprecation message when calling `wrap`.
147 |
148 | # 0.5.0
149 |
150 | - Support [cavy-cli](https://github.com/pixielabs/cavy-cli).
151 |
152 | cavy-cli is the next step in Cavy's development. With it, we can start to
153 | support Continuous Integration, conditionally running tests, and a bunch of
154 | other cool stuff. Thanks to [Tyler Pate](https://github.com/TGPSKI) whose
155 | suggestions inspired our approach.
156 |
157 | # 0.4.1
158 |
159 | - Stop using deprecated `PropTypes` and `createClass`. Thanks
160 | [Mohammed Abu-alsaad](https://github.com/mo-solnet)!
161 | - Fix for when using a wrapped component in a shallow render environment.
162 | Thanks [Kye Bracey](https://github.com/Kynosaur)!
163 | - Updated documentation for Create React Native App / Expo.
164 |
165 | # 0.4.0
166 |
167 | - Add optional `startDelay` property to `` which delays the test suite
168 | from beginning once the component is mounted.
169 | - Added a start and end console log line.
170 |
171 | With thanks to [Tyler Pate](https://github.com/TGPSKI) for both of these
172 | features!
173 |
174 | # 0.3.1
175 |
176 | - Tweak to the test failure message.
177 |
178 | # 0.3.0
179 |
180 | - Added the ability to automatically clear the app's entire AsyncStorage
181 | between test cases. By default, behaviour is unchanged (it does not clear
182 | it).
183 |
184 | # 0.2.0
185 |
186 | - Added a `notExists` component assertion.
187 |
188 | # 0.1.0
189 |
190 | - Added a pause function.
191 | - Configurable wait time when finding components.
192 |
193 | # 0.0.2
194 |
195 | - Bug fix to ensure that default props are set for wrapped components.
196 |
197 | # 0.0.1
198 |
199 | - Initial release.
200 |
--------------------------------------------------------------------------------
/src/TestScope.js:
--------------------------------------------------------------------------------
1 | // Custom error returned when Cavy cannot find the component in the
2 | // TestHookStore.
3 | class ComponentNotFoundError extends Error {
4 | constructor(message) {
5 | super(message);
6 | this.name = "ComponentNotFoundError";
7 | }
8 | }
9 |
10 | // Custom error returned when a component has not been wrapped, but should have
11 | // been.
12 | class UnwrappedComponentError extends Error {
13 | constructor(message) {
14 | super(message);
15 | this.name = "UnwrappedComponentError";
16 | }
17 | }
18 |
19 | // Internal: TestScope is responsible for building up testCases to be run by
20 | // the TestRunner, and includes all the functions available when writing these
21 | // specs.
22 | //
23 | // component - the Tester component within which the app is wrapped.
24 | // waitTime - length of time in ms that cavy should wait before giving up on
25 | // finding a component in the testHookStore.
26 | export default class TestScope {
27 | constructor(component, waitTime) {
28 | this.component = component;
29 | this.testHooks = component.testHookStore;
30 | this.testCases = [];
31 | this.waitTime = waitTime;
32 | }
33 |
34 | // Public: Find a component by its test hook identifier. Waits
35 | // this.waitTime for the component to appear before abandoning.
36 | //
37 | // Usually, you'll want to use `exists` instead.
38 | //
39 | // identifier - String, component identifier registered in the test hook store
40 | // via `generateTestHook`.
41 | //
42 | // Example
43 | //
44 | // import { assert } from 'assert';
45 | // const c = await spec.findComponent('MyScene.myComponent');
46 | // assert(c, 'Component is missing');
47 | //
48 | // Returns a promise; use `await` when calling this function. Resolves the
49 | // promise if the component is found, rejects the promise after
50 | // this.waitTime if the component is never found in the test hook
51 | // store.
52 | findComponent(identifier) {
53 | let promise = new Promise((resolve, reject) => {
54 | let startTime = Date.now();
55 | let loop = setInterval(() => {
56 | const component = this.testHooks.get(identifier);
57 | if (component) {
58 | clearInterval(loop);
59 | return resolve(component);
60 | } else {
61 | if (Date.now() - startTime >= this.waitTime) {
62 | reject(
63 | new ComponentNotFoundError(
64 | `Could not find component with identifier ${identifier}`
65 | )
66 | );
67 | clearInterval(loop);
68 | }
69 | }
70 | }, 100);
71 | });
72 |
73 | return promise;
74 | }
75 |
76 | // Public: Build up a group of test cases.
77 | //
78 | // label - Label for these test cases.
79 | // f - Callback function containing your tests cases defined with `it`.
80 | // tag - (Optional) A string tag used to determine whether the group of
81 | // tests should run. Defaults to null.
82 | //
83 | // Example
84 | //
85 | // // specs/MyFeatureSpec.js
86 | // export default function(spec) {
87 | // spec.describe('My Scene', function() {
88 | // spec.it('Has a component', async function() {
89 | // await spec.exists('MyScene.myComponent');
90 | // }, 'focus');
91 | // });
92 | // }
93 | //
94 | // Returns undefined.
95 | describe(label, f, tag = null) {
96 | this.describeLabel = label;
97 | this.tag = tag;
98 | f.call(this);
99 | }
100 |
101 | // Public: Define a test case.
102 | //
103 | // label - Label for this test case. This is combined with the label from
104 | // `describe` when Cavy outputs to the console.
105 | // f - The test case.
106 | // testTag - (Optional) A string tag used to determine whether the individual
107 | // test should run. 'Inherits' a tag from its surrounding describe
108 | // block (if present) or defaults to null.
109 | //
110 | // See example above.
111 | it(label, f, testTag = null) {
112 | const tag = this.tag || testTag;
113 | this.testCases.push({ describeLabel: this.describeLabel, label, f, tag });
114 | }
115 |
116 | // Public: Runs a function before each test case.
117 | //
118 | // f - the function to run
119 | beforeEach(f) {
120 | this.beforeEach = f;
121 | }
122 |
123 | // ACTIONS
124 |
125 | // Public: Fill in a `TextInput`-compatible component with a string value.
126 | // Your component should respond to the property `onChangeText`.
127 | //
128 | // identifier - Identifier for the component.
129 | // str - String to fill in.
130 | //
131 | // Returns a promise, use await when calling this function. Promise will be
132 | // rejected if the component is not found.
133 | async fillIn(identifier, str) {
134 | const component = await this.findComponent(identifier);
135 | component.props.onChangeText(str);
136 | }
137 |
138 | // Public: 'Press' a component (e.g. a ``).
139 | // Your component should respond to the property `onPress`.
140 | //
141 | // identifier - Identifier for the component.
142 | //
143 | // Returns a promise, use await when calling this function. Promise will be
144 | // rejected if the component is not found.
145 | async press(identifier) {
146 | const component = await this.findComponent(identifier);
147 | component.props.onPress();
148 | }
149 |
150 | // Public: 'Focus' a component (e.g. a ``).
151 | // Your component should respond to the property `onFocus`.
152 | //
153 | // identifier - Identifier for the component.
154 | //
155 | // Returns a promise, use await when calling this function. Promise will be
156 | // rejected if the component is not found.
157 | async focus(identifier) {
158 | const component = await this.findComponent(identifier);
159 | component.props.onFocus();
160 | }
161 |
162 | // Public: Pause the test for a specified length of time, perhaps to allow
163 | // time for a request response to be received.
164 | //
165 | // time - Integer length of time to pause for (in milliseconds).
166 | //
167 | // Returns a promise, use await when calling this function.
168 | async pause(time) {
169 | let promise = new Promise((resolve, reject) => {
170 | setTimeout(function() {
171 | resolve();
172 | }, time);
173 | });
174 |
175 | return promise;
176 | }
177 |
178 | // Public: Checks whether a component e.g. contains the text string
179 | // as a child.
180 | //
181 | // identifier - Identifier for the component.
182 | // text - String
183 | //
184 | // Returns a promise, use await when calling this function. Promise will be
185 | // rejected if the component is not found.
186 | async containsText(identifier, text) {
187 | const component = await this.findComponent(identifier);
188 |
189 | if (component.props === undefined) {
190 | const msg = "Cannot read property 'children' of undefined.\n" +
191 | "Are you using `containsText` with a React component?\n" +
192 | "If so, you need to `wrap` the component first.\n" +
193 | "See documentation for Cavy's `wrap` function:" +
194 | "https://cavy.app/docs/api/test-hooks#example-3";
195 |
196 | throw new UnwrappedComponentError(msg);
197 | }
198 |
199 | const stringifiedChildren = component.props.children.includes
200 | ? component.props.children
201 | : String(component.props.children);
202 |
203 | if (!stringifiedChildren.includes(text)) {
204 | throw new Error(`Could not find text ${text}`);
205 | }
206 | }
207 |
208 | // ASSERTIONS
209 |
210 | // Public: Check a component exists.
211 | //
212 | // identifier - Identifier for the component.
213 | //
214 | // Returns a promise, use await when calling this function. Promise will be
215 | // rejected if component is not found, otherwise will be resolved with
216 | // `true`.
217 | async exists(identifier) {
218 | const component = await this.findComponent(identifier);
219 | return !!component;
220 | }
221 |
222 | // Public: Check for the absence of a component. Will potentially halt your
223 | // test for your maximum wait time.
224 | //
225 | // identifier - Identifier for the component.
226 | //
227 | // Returns a promise, use await when calling this function. Promise will be
228 | // rejected if the component is not found.
229 | async notExists(identifier) {
230 | try {
231 | await this.findComponent(identifier);
232 | } catch (e) {
233 | if (e.name == "ComponentNotFoundError") {
234 | return true;
235 | }
236 | throw e;
237 | }
238 | throw new Error(`Component with identifier ${identifier} was present`);
239 | }
240 | }
241 |
--------------------------------------------------------------------------------