@testing-library/react-native 7.2.0 → 9.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/waitFor.js CHANGED
@@ -12,13 +12,16 @@ var _act = _interopRequireDefault(require("./act"));
12
12
 
13
13
  var _errors = require("./helpers/errors");
14
14
 
15
+ var _timers = require("./helpers/timers");
16
+
15
17
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
18
 
17
19
  function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
18
20
 
19
21
  function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
20
22
 
21
- const DEFAULT_TIMEOUT = 4500;
23
+ /* globals jest */
24
+ const DEFAULT_TIMEOUT = 1000;
22
25
  const DEFAULT_INTERVAL = 50;
23
26
 
24
27
  function checkReactVersionAtLeast(major, minor) {
@@ -27,47 +30,170 @@ function checkReactVersionAtLeast(major, minor) {
27
30
  return actualMajor > major || actualMajor === major && actualMinor >= minor;
28
31
  }
29
32
 
30
- function waitForInternal(expectation, options) {
31
- var _options$timeout, _options$interval;
33
+ function waitForInternal(expectation, {
34
+ timeout = DEFAULT_TIMEOUT,
35
+ interval = DEFAULT_INTERVAL,
36
+ stackTraceError
37
+ }) {
38
+ if (typeof expectation !== 'function') {
39
+ throw new TypeError('Received `expectation` arg must be a function');
40
+ } // eslint-disable-next-line no-async-promise-executor
41
+
42
+
43
+ return new Promise(async (resolve, reject) => {
44
+ let lastError, intervalId;
45
+ let finished = false;
46
+ let promiseStatus = 'idle';
47
+ const overallTimeoutTimer = (0, _timers.setTimeout)(handleTimeout, timeout);
48
+ const usingFakeTimers = (0, _timers.jestFakeTimersAreEnabled)();
49
+
50
+ if (usingFakeTimers) {
51
+ checkExpectation(); // this is a dangerous rule to disable because it could lead to an
52
+ // infinite loop. However, eslint isn't smart enough to know that we're
53
+ // setting finished inside `onDone` which will be called when we're done
54
+ // waiting or when we've timed out.
55
+ // eslint-disable-next-line no-unmodified-loop-condition
56
+
57
+ let fakeTimeRemaining = timeout;
58
+
59
+ while (!finished) {
60
+ if (!(0, _timers.jestFakeTimersAreEnabled)()) {
61
+ const error = new Error(`Changed from using fake timers to real timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to real timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`);
62
+
63
+ if (stackTraceError) {
64
+ (0, _errors.copyStackTrace)(error, stackTraceError);
65
+ }
66
+
67
+ reject(error);
68
+ return;
69
+ } // when fake timers are used we want to simulate the interval time passing
70
+
71
+
72
+ if (fakeTimeRemaining <= 0) {
73
+ return;
74
+ } else {
75
+ fakeTimeRemaining -= interval;
76
+ } // we *could* (maybe should?) use `advanceTimersToNextTimer` but it's
77
+ // possible that could make this loop go on forever if someone is using
78
+ // third party code that's setting up recursive timers so rapidly that
79
+ // the user's timer's don't get a chance to resolve. So we'll advance
80
+ // by an interval instead. (We have a test for this case).
81
+
82
+
83
+ jest.advanceTimersByTime(interval); // It's really important that checkExpectation is run *before* we flush
84
+ // in-flight promises. To be honest, I'm not sure why, and I can't quite
85
+ // think of a way to reproduce the problem in a test, but I spent
86
+ // an entire day banging my head against a wall on this.
87
+
88
+ checkExpectation(); // In this rare case, we *need* to wait for in-flight promises
89
+ // to resolve before continuing. We don't need to take advantage
90
+ // of parallelization so we're fine.
91
+ // https://stackoverflow.com/a/59243586/971592
92
+ // eslint-disable-next-line no-await-in-loop
93
+
94
+ await new Promise(resolve => (0, _timers.setImmediate)(resolve));
95
+ }
96
+ } else {
97
+ intervalId = setInterval(checkRealTimersCallback, interval);
98
+ checkExpectation();
99
+ }
100
+
101
+ function onDone(error, result) {
102
+ finished = true;
103
+ (0, _timers.clearTimeout)(overallTimeoutTimer);
32
104
 
33
- const timeout = (_options$timeout = options === null || options === void 0 ? void 0 : options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : DEFAULT_TIMEOUT;
34
- const interval = (_options$interval = options === null || options === void 0 ? void 0 : options.interval) !== null && _options$interval !== void 0 ? _options$interval : DEFAULT_INTERVAL;
35
- const startTime = Date.now(); // Being able to display a useful stack trace requires generating it before doing anything async
105
+ if (!usingFakeTimers) {
106
+ clearInterval(intervalId);
107
+ }
36
108
 
37
- const stackTraceError = new _errors.ErrorWithStack('STACK_TRACE_ERROR', waitFor);
38
- return new Promise((resolve, reject) => {
39
- const rejectOrRerun = error => {
40
- if (Date.now() - startTime >= timeout) {
41
- (0, _errors.copyStackTrace)(error, stackTraceError);
109
+ if (error) {
42
110
  reject(error);
43
- return;
111
+ } else {
112
+ // $FlowIgnore[incompatible-return] error and result are mutually exclusive
113
+ resolve(result);
44
114
  }
115
+ }
116
+
117
+ function checkRealTimersCallback() {
118
+ if ((0, _timers.jestFakeTimersAreEnabled)()) {
119
+ const error = new Error(`Changed from using real timers to fake timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to fake timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`);
45
120
 
46
- setTimeout(runExpectation, interval);
47
- };
121
+ if (stackTraceError) {
122
+ (0, _errors.copyStackTrace)(error, stackTraceError);
123
+ }
124
+
125
+ return reject(error);
126
+ } else {
127
+ return checkExpectation();
128
+ }
129
+ }
130
+
131
+ function checkExpectation() {
132
+ if (promiseStatus === 'pending') return;
48
133
 
49
- function runExpectation() {
50
134
  try {
51
- const result = expectation();
52
- resolve(result);
135
+ const result = expectation(); // $FlowIgnore[incompatible-type]
136
+
137
+ if (typeof (result === null || result === void 0 ? void 0 : result.then) === 'function') {
138
+ promiseStatus = 'pending'; // eslint-disable-next-line promise/catch-or-return
139
+
140
+ result.then(resolvedValue => {
141
+ promiseStatus = 'resolved';
142
+ onDone(null, resolvedValue);
143
+ return;
144
+ }, rejectedValue => {
145
+ promiseStatus = 'rejected';
146
+ lastError = rejectedValue;
147
+ return;
148
+ });
149
+ } else {
150
+ onDone(null, result);
151
+ } // If `callback` throws, wait for the next mutation, interval, or timeout.
152
+
53
153
  } catch (error) {
54
- rejectOrRerun(error);
154
+ // Save the most recent callback error to reject the promise with it in the event of a timeout
155
+ lastError = error;
55
156
  }
56
157
  }
57
158
 
58
- setTimeout(runExpectation, 0);
159
+ function handleTimeout() {
160
+ let error;
161
+
162
+ if (lastError) {
163
+ error = lastError;
164
+
165
+ if (stackTraceError) {
166
+ (0, _errors.copyStackTrace)(error, stackTraceError);
167
+ }
168
+ } else {
169
+ error = new Error('Timed out in waitFor.');
170
+
171
+ if (stackTraceError) {
172
+ (0, _errors.copyStackTrace)(error, stackTraceError);
173
+ }
174
+ }
175
+
176
+ onDone(error, null);
177
+ }
59
178
  });
60
179
  }
61
180
 
62
181
  async function waitFor(expectation, options) {
182
+ // Being able to display a useful stack trace requires generating it before doing anything async
183
+ const stackTraceError = new _errors.ErrorWithStack('STACK_TRACE_ERROR', waitFor);
184
+ const optionsWithStackTrace = {
185
+ stackTraceError,
186
+ ...options
187
+ };
188
+
63
189
  if (!checkReactVersionAtLeast(16, 9)) {
64
- return waitForInternal(expectation, options);
190
+ return waitForInternal(expectation, optionsWithStackTrace);
65
191
  }
66
192
 
67
193
  let result; //$FlowFixMe: `act` has incorrect flow typing
68
194
 
69
195
  await (0, _act.default)(async () => {
70
- result = await waitForInternal(expectation, options);
196
+ result = await waitForInternal(expectation, optionsWithStackTrace);
71
197
  }); //$FlowFixMe: either we have result or `waitFor` threw error
72
198
 
73
199
  return result;
@@ -1,4 +1,5 @@
1
1
  // @flow
2
+ /* globals jest */
2
3
 
3
4
  import * as React from 'react';
4
5
  import act from './act';
@@ -7,8 +8,14 @@ import {
7
8
  throwRemovedFunctionError,
8
9
  copyStackTrace,
9
10
  } from './helpers/errors';
11
+ import {
12
+ setTimeout,
13
+ clearTimeout,
14
+ setImmediate,
15
+ jestFakeTimersAreEnabled,
16
+ } from './helpers/timers';
10
17
 
11
- const DEFAULT_TIMEOUT = 4500;
18
+ const DEFAULT_TIMEOUT = 1000;
12
19
  const DEFAULT_INTERVAL = 50;
13
20
 
14
21
  function checkReactVersionAtLeast(major: number, minor: number): boolean {
@@ -21,36 +28,159 @@ function checkReactVersionAtLeast(major: number, minor: number): boolean {
21
28
  export type WaitForOptions = {
22
29
  timeout?: number,
23
30
  interval?: number,
31
+ stackTraceError?: ErrorWithStack,
24
32
  };
25
33
 
26
34
  function waitForInternal<T>(
27
35
  expectation: () => T,
28
- options?: WaitForOptions
36
+ {
37
+ timeout = DEFAULT_TIMEOUT,
38
+ interval = DEFAULT_INTERVAL,
39
+ stackTraceError,
40
+ }: WaitForOptions
29
41
  ): Promise<T> {
30
- const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
31
- const interval = options?.interval ?? DEFAULT_INTERVAL;
32
- const startTime = Date.now();
33
- // Being able to display a useful stack trace requires generating it before doing anything async
34
- const stackTraceError = new ErrorWithStack('STACK_TRACE_ERROR', waitFor);
42
+ if (typeof expectation !== 'function') {
43
+ throw new TypeError('Received `expectation` arg must be a function');
44
+ }
45
+
46
+ // eslint-disable-next-line no-async-promise-executor
47
+ return new Promise(async (resolve, reject) => {
48
+ let lastError, intervalId;
49
+ let finished = false;
50
+ let promiseStatus = 'idle';
51
+
52
+ const overallTimeoutTimer = setTimeout(handleTimeout, timeout);
53
+
54
+ const usingFakeTimers = jestFakeTimersAreEnabled();
55
+
56
+ if (usingFakeTimers) {
57
+ checkExpectation();
58
+ // this is a dangerous rule to disable because it could lead to an
59
+ // infinite loop. However, eslint isn't smart enough to know that we're
60
+ // setting finished inside `onDone` which will be called when we're done
61
+ // waiting or when we've timed out.
62
+ // eslint-disable-next-line no-unmodified-loop-condition
63
+ let fakeTimeRemaining = timeout;
64
+ while (!finished) {
65
+ if (!jestFakeTimersAreEnabled()) {
66
+ const error = new Error(
67
+ `Changed from using fake timers to real timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to real timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`
68
+ );
69
+ if (stackTraceError) {
70
+ copyStackTrace(error, stackTraceError);
71
+ }
72
+ reject(error);
73
+ return;
74
+ }
75
+
76
+ // when fake timers are used we want to simulate the interval time passing
77
+ if (fakeTimeRemaining <= 0) {
78
+ return;
79
+ } else {
80
+ fakeTimeRemaining -= interval;
81
+ }
82
+
83
+ // we *could* (maybe should?) use `advanceTimersToNextTimer` but it's
84
+ // possible that could make this loop go on forever if someone is using
85
+ // third party code that's setting up recursive timers so rapidly that
86
+ // the user's timer's don't get a chance to resolve. So we'll advance
87
+ // by an interval instead. (We have a test for this case).
88
+ jest.advanceTimersByTime(interval);
89
+
90
+ // It's really important that checkExpectation is run *before* we flush
91
+ // in-flight promises. To be honest, I'm not sure why, and I can't quite
92
+ // think of a way to reproduce the problem in a test, but I spent
93
+ // an entire day banging my head against a wall on this.
94
+ checkExpectation();
95
+
96
+ // In this rare case, we *need* to wait for in-flight promises
97
+ // to resolve before continuing. We don't need to take advantage
98
+ // of parallelization so we're fine.
99
+ // https://stackoverflow.com/a/59243586/971592
100
+ // eslint-disable-next-line no-await-in-loop
101
+ await new Promise((resolve) => setImmediate(resolve));
102
+ }
103
+ } else {
104
+ intervalId = setInterval(checkRealTimersCallback, interval);
105
+ checkExpectation();
106
+ }
107
+
108
+ function onDone(error, result) {
109
+ finished = true;
110
+ clearTimeout(overallTimeoutTimer);
35
111
 
36
- return new Promise((resolve, reject) => {
37
- const rejectOrRerun = (error) => {
38
- if (Date.now() - startTime >= timeout) {
39
- copyStackTrace(error, stackTraceError);
112
+ if (!usingFakeTimers) {
113
+ clearInterval(intervalId);
114
+ }
115
+
116
+ if (error) {
40
117
  reject(error);
41
- return;
118
+ } else {
119
+ // $FlowIgnore[incompatible-return] error and result are mutually exclusive
120
+ resolve(result);
121
+ }
122
+ }
123
+
124
+ function checkRealTimersCallback() {
125
+ if (jestFakeTimersAreEnabled()) {
126
+ const error = new Error(
127
+ `Changed from using real timers to fake timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to fake timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`
128
+ );
129
+ if (stackTraceError) {
130
+ copyStackTrace(error, stackTraceError);
131
+ }
132
+ return reject(error);
133
+ } else {
134
+ return checkExpectation();
42
135
  }
43
- setTimeout(runExpectation, interval);
44
- };
45
- function runExpectation() {
136
+ }
137
+
138
+ function checkExpectation() {
139
+ if (promiseStatus === 'pending') return;
46
140
  try {
47
141
  const result = expectation();
48
- resolve(result);
142
+
143
+ // $FlowIgnore[incompatible-type]
144
+ if (typeof result?.then === 'function') {
145
+ promiseStatus = 'pending';
146
+ // eslint-disable-next-line promise/catch-or-return
147
+ result.then(
148
+ (resolvedValue) => {
149
+ promiseStatus = 'resolved';
150
+ onDone(null, resolvedValue);
151
+ return;
152
+ },
153
+ (rejectedValue) => {
154
+ promiseStatus = 'rejected';
155
+ lastError = rejectedValue;
156
+ return;
157
+ }
158
+ );
159
+ } else {
160
+ onDone(null, result);
161
+ }
162
+ // If `callback` throws, wait for the next mutation, interval, or timeout.
49
163
  } catch (error) {
50
- rejectOrRerun(error);
164
+ // Save the most recent callback error to reject the promise with it in the event of a timeout
165
+ lastError = error;
166
+ }
167
+ }
168
+
169
+ function handleTimeout() {
170
+ let error;
171
+ if (lastError) {
172
+ error = lastError;
173
+ if (stackTraceError) {
174
+ copyStackTrace(error, stackTraceError);
175
+ }
176
+ } else {
177
+ error = new Error('Timed out in waitFor.');
178
+ if (stackTraceError) {
179
+ copyStackTrace(error, stackTraceError);
180
+ }
51
181
  }
182
+ onDone(error, null);
52
183
  }
53
- setTimeout(runExpectation, 0);
54
184
  });
55
185
  }
56
186
 
@@ -58,15 +188,19 @@ export default async function waitFor<T>(
58
188
  expectation: () => T,
59
189
  options?: WaitForOptions
60
190
  ): Promise<T> {
191
+ // Being able to display a useful stack trace requires generating it before doing anything async
192
+ const stackTraceError = new ErrorWithStack('STACK_TRACE_ERROR', waitFor);
193
+ const optionsWithStackTrace = { stackTraceError, ...options };
194
+
61
195
  if (!checkReactVersionAtLeast(16, 9)) {
62
- return waitForInternal(expectation, options);
196
+ return waitForInternal(expectation, optionsWithStackTrace);
63
197
  }
64
198
 
65
199
  let result: T;
66
200
 
67
201
  //$FlowFixMe: `act` has incorrect flow typing
68
202
  await act(async () => {
69
- result = await waitForInternal(expectation, options);
203
+ result = await waitForInternal(expectation, optionsWithStackTrace);
70
204
  });
71
205
 
72
206
  //$FlowFixMe: either we have result or `waitFor` threw error
@@ -0,0 +1,10 @@
1
+ const reactNativePreset = require('react-native/jest-preset');
2
+
3
+ module.exports = {
4
+ ...reactNativePreset,
5
+ // this is needed to make modern fake timers work
6
+ // because the react-native preset overrides global.Promise
7
+ setupFiles: [require.resolve('./save-promise.js')]
8
+ .concat(reactNativePreset.setupFiles)
9
+ .concat([require.resolve('./restore-promise.js')]),
10
+ };
@@ -0,0 +1 @@
1
+ global.Promise = global.RNTL_ORIGINAL_PROMISE;
@@ -0,0 +1 @@
1
+ global.RNTL_ORIGINAL_PROMISE = Promise;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testing-library/react-native",
3
- "version": "7.2.0",
3
+ "version": "9.0.0-alpha.0",
4
4
  "description": "Simple and complete React Native testing utilities that encourage good testing practices.",
5
5
  "main": "build/index.js",
6
6
  "typings": "./typings/index.d.ts",
@@ -20,6 +20,7 @@
20
20
  ],
21
21
  "files": [
22
22
  "build/",
23
+ "jest-preset/",
23
24
  "typings/index.d.ts",
24
25
  "pure.js",
25
26
  "dont-cleanup-after-each.js"
@@ -33,26 +34,26 @@
33
34
  "@babel/preset-react": "^7.9.4",
34
35
  "@callstack/eslint-config": "^10.0.0",
35
36
  "@release-it/conventional-changelog": "^2.0.0",
36
- "@testing-library/jest-native": "~3.4.3",
37
+ "@testing-library/jest-native": "~4.0.2",
37
38
  "@types/react": "^17.0.0",
38
- "@types/react-native": "^0.63.0",
39
+ "@types/react-native": "^0.65.7",
39
40
  "@types/react-test-renderer": "^17.0.0",
40
- "babel-jest": "^26.0.1",
41
+ "babel-jest": "^27.0.0",
41
42
  "conventional-changelog-cli": "^2.0.11",
42
43
  "dedent": "^0.7.0",
43
44
  "eslint": "^7.0.0",
44
45
  "flow-bin": "^0.141.0",
45
46
  "flow-copy-source": "^2.0.9",
46
- "jest": "^26.0.1",
47
- "react": "^17.0.1",
48
- "react-native": "^0.64.0-rc.1",
49
- "react-test-renderer": "^17.0.1",
47
+ "jest": "^27.0.0",
48
+ "react": "^17.0.2",
49
+ "react-native": "^0.66.0",
50
+ "react-test-renderer": "^17.0.2",
50
51
  "release-it": "^14.0.3",
51
52
  "strip-ansi": "^6.0.0",
52
53
  "typescript": "^4.0.2"
53
54
  },
54
55
  "dependencies": {
55
- "pretty-format": "^26.0.1"
56
+ "pretty-format": "^27.0.0"
56
57
  },
57
58
  "peerDependencies": {
58
59
  "react": ">=16.0.0",
@@ -70,11 +71,15 @@
70
71
  "build": "rm -rf build; babel src --out-dir build --ignore 'src/__tests__/*'"
71
72
  },
72
73
  "jest": {
73
- "preset": "react-native",
74
+ "preset": "../jest-preset",
74
75
  "moduleFileExtensions": [
75
76
  "js",
76
77
  "json"
77
78
  ],
78
- "rootDir": "./src"
79
+ "rootDir": "./src",
80
+ "testPathIgnorePatterns": [
81
+ "timerUtils"
82
+ ],
83
+ "testTimeout": 30000
79
84
  }
80
85
  }