How to properly test if a Toast has been shown in react native using native base? - react-native

I am trying to write a test that checks if the screen is showing a Toast with an error message. The test passes, but there is a warning:
console.error
Warning: You called act(async () => ...) without await.
This could lead to unexpected testing behaviour, interleaving multiple act calls
and mixing their scopes. You should - await act(async () => ...);
The screen is working fine, I am just learning how to write tests. This is my test:
it('shows error correctly', async () => {
mockAxios.get.mockRejectedValueOnce(new Error('Async error'))
const { queryByText } = renderWithRedux(<DiscoverScreen />)
await waitFor(() => {
expect(queryByText(ErrorMessages.GeneralErrorToast)).not.toBeNull()
})
await waitForElementToBeRemoved(() => queryByText(ErrorMessages.GeneralErrorToast), { timeout: 5000 })
})
What am I not doing right? Definitely there is an issue with react native testing, because there are problems for certain async querying, especially when you have several of them. I found that here: https://github.com/callstack/react-native-testing-library/issues/379#issuecomment-720734366
I am using native base for showing the Toast, which is using Animated I think. Should I use jest.useFakeTimers() and how?

After researching how the Toast in native base works (this could be done when you open the source code in github - https://github.com/GeekyAnts/NativeBase/blob/master/src/basic/ToastContainer.js), I found that it uses Animated.timing.
So I had to find out how to deal with react native animations in tests. That article had a solution that worked for me: https://medium.com/#joncardasis/react-native-how-to-test-components-implementing-animated-with-jest-8cabb5fc2730
After I added the code in my jest setup file, this is how my test looks:
global.withAnimatedTimeTravelEnabled(async () => {
const { queryByText } = renderedComponent
await waitFor(() => {
expect(queryByText(ErrorMessages.GeneralErrorToast)).not.toBeNull()
})
global.timeTravel(Constants.ErrorToastDelay)
expect(queryByText(ErrorMessages.GeneralErrorToast)).toBeNull()
})
It works and now the test passes with no warnings!
One little adjustment in my jest configuration was also missing. I had to add this:
"testEnvironment": "jsdom"
I hope this could help someone else, too!

Related

Jest + puppeteer best architecture practices

I just entered the world of testing with puppeteer and jest, and I was wondering what the best practice was in terms of folder architecture and logic.
I've never done testing before and I think I'm getting a little lost in the different principles and concepts and how it all fits together.
I learned to do my tests based on the page-object model, so I have classes for each of my pages, but also for each of my modules ( or components ). For example, in my application, the header or the login modal are components.
Then I have a test file per page or per component.
(for example the landingPage.tests.js file, which uses the model of the LandingPage class in the LandingPage.js file)
Here is a concrete example:
I have different login cases and I'd like to test them all. For example I want to test to connect with a "normal" user, for which the process is simply login then password. Then I need to test with a user who has activated 2FA, or with a user from a company that uses SSO.
I first thought about putting my different tests in authentication.tests.js, in different describe blocks, thinking it would open a new tab each time, but it doesn't... I use puppeteer in incognito mode to make sure each tab is an isolated session.
So my questions are:
Where is the best place to do these test suites?
Am I supposed to have test files that "describe" the pages ( for example, the button must be present, such text must be here etc) and also have "scenario type" test file ( a set of contextual actions to a user, like for my different login cases) ?
Here is authentication.tests.js, in which I would like to tests all my different ways of logging in :
import HeaderComponent from "../../../pages/components/HeaderComponent";
import AuthenticationComponent from "../../../pages/components/AuthenticationComponent";
import LandingPage from "../../../pages/landing/LandingPage";
import {
JEST_TIMEOUT,
CREDENTIALS
} from "../../../config";
describe('Component:Authentication', () => {
let headerComponent;
let authenticationComponent;
let landingPage;
beforeAll(async () => {
jest.setTimeout(JEST_TIMEOUT);
headerComponent = new HeaderComponent;
authenticationComponent = new AuthenticationComponent;
landingPage = new LandingPage;
});
describe('Normal login ', () => {
it('should click on login and open modal', async () => {
await landingPage.visit();
await headerComponent.isVisible();
await headerComponent.clickOnLogin();
await authenticationComponent.isVisible();
});
it('should type a normal user email adress and validate', async () => {
await authenticationComponent.typeUsername(CREDENTIALS.normal.username);
await authenticationComponent.clickNext();
});
it('should type the correct password and validate', async () => {
await authenticationComponent.typePassword(CREDENTIALS.normal.password);
await authenticationComponent.clickNext();
});
it('should be logged in', async () => {
await waitForText(page, 'body', 'Success !');
});
});
describe('SSO login ', () => {
// todo ...
});
});
Thank you and sorry if it sounds confusing, like I said I'm trying to figure out how it all fits together.
Regarding the folder structure, Jest will find any files according to the match config, basically anything called *.spec.js or *.test.js. Looks like you know that already.
What that means is the folder structure is completely up to you. Some people like to have the tests for components in the same folders as the components themselves. Personally I prefer to have all the tests in one folder as it makes the project look cleaner.
The other benefit of having all the tests in one folder is that you can then start to distinguish between the types of tests. Component tests check that pure components render and operate as expected. You don't need Puppeteer for this, use snapshots if you're in a React app. Puppeteer is good for integration tests that navigate through so-called 'happy paths', login, signup, add to cart etc., using a headless Chromium browser.
To answer the specific problem you have been having with Jest / Puppeteer on a new page for each test:
//keep a reference to the browser
let browser
//keep a reference to the page
let page
// open puppeteer before all tests
beforeAll(async () => {
browser = await puppeteer.launch()
})
// close puppeteer after all tests
afterAll(async () => {
await browser.close()
})
// Get a new page for each test so that we start fresh.
beforeEach(async () => {
page = await browser.newPage()
})
// Remember to close pages after each test.
afterEach(async () => {
await page.close()
})
describe('Counter', () => {
// "it" blocks go here.
})
Hope that helps a bit.

How to detect screenshots with React Native (both Android and iOS)

I am trying to detect if a user takes a screenshot while using a smartphone app that I have built. I am building my project with React Native.
I have been told that I can possibly prevent screenshots for Android but not for iOS. but can I still detect whether a user attempts to take a screenshot so that I can at least send a warning via Alert?
Thanks in advance
I tried react-native-screenshot-detector but it did not work
you can use this package it supports android&ios screenshot detecting react-native-detector
import {
addScreenshotListener,
removeScreenshotListener,
} from 'react-native-detector';
// ...
React.useEffect(() => {
const userDidScreenshot = () => {
console.log('User took screenshot');
};
const listener = addScreenshotListener(userDidScreenshot);
return () => {
removeScreenshotListener(listener);
};
}, []);
There is no package for it currently.

How to disable React Native YellowBox messages in jest tests

I know how to disable YellowBox warning when running in the device:
YellowBox.ignoreWarnings(ignoredYellowBox);
But I don't know how to silence them in a jest test. There are some deprecation warnings that we cannot deal with atm and they are making our tests very noisy. I'd rather not block every YellowBox warning from our tests, but if necessary, that would work too.
It is a pretty annoying thing. This is what we came up with:
const warnings = [
'Warning: NetInfo has been extracted from react-native core and will be removed in a future release.',
'inside a test was not wrapped in act(...).',
];
const oldError = console.error;
jest.spyOn(console, 'error').mockImplementation((...args) => {
const string = args.join(' ');
if (warnings.some(warning => string.match(warning))) return;
oldError(...args);
});
Add that snippet to your jest setup file, and edit the warnings array as appropriate for your situation.
For future readers, there is a simple way to do this.
jest.spyOn(console, 'warn').mockImplementation(() => {})
jest.spyOn(console, 'error').mockImplementation(() => {})
For TypeScript:
jest.spyOn(console, 'warn').mockImplementation(() => undefined)
jest.spyOn(console, 'error').mockImplementation(() => undefined)
Remember that by using an approach like this you are disabling all console errors and warnings.
If you want to keep other errors and warnings, just use the first answer in this question.

Detox tests are breaking

This is regarding detox e2e tests.
I am running my tests, each under an it('xx', async => { await...});
The tests are scripted in such a way that 1st test would log in, 2nd test would do something on homepage, 3 rd test would navigate from homepage to other pages and so on.
The issue here is, as soon as my first test executes, the app is getting logged out and all the consecutive tests are failing.
But when I include all steps(right from login to the desired functionality) in every test, the suite is working properly.
I would like to know why is this happening. Is there any connection with async function?
One of the gotchas for using Detox is that the sample test specification uses a beforeEach and there is a tendency to copy verbatim examples that we are given, some times missing out things that either need to be removed or should be added.
In this particular case in the beforeEach there is the call await device.reloadReactNative(); this command reloads the device as if you had pressed CMD+R (on iOS) or RR (on Android). This means that items that have been saved to state are lost and the application is pretty much returned to its initial state.
Check your code for the offending line, you can see it in example provided below. If you remove this line then it will stop reloading React Native on your device before each test.
example.spec.js
https://github.com/wix/Detox/blob/master/examples/demo-react-native/e2e/example.spec.js
describe('Example', () => {
beforeEach(async () => {
await device.reloadReactNative(); // <- this is the problem
});
it('should have welcome screen', async () => {
await expect(element(by.id('welcome'))).toBeVisible();
});
it('should show hello screen after tap', async () => {
await element(by.id('hello_button')).tap();
await expect(element(by.text('Hello!!!'))).toBeVisible();
});
it('should show world screen after tap', async () => {
await element(by.id('world_button')).tap();
await expect(element(by.text('World!!!'))).toBeVisible();
});
});

Jest unresolved promise do not fail

Jest docs says:
Unresolved Promises
If a promise doesn't resolve at all, this error might be thrown:
(and so on)
In my case this not happen.
I have this test:
test('detect infinite loop', () => {
expect.assertions(1);
const vastPromise = VastUtils.parseFromUrl(infiniteLoopUrl);
const expectedError =
new VastError(VastErrorCodes.WRAPPER_LIMIT_REACHED);
return expect(vastPromise).rejects.toEqual(expectedError);
});
VastUtils simply fetch an XML located at infiniteLoopUrl, parse it, and if this xml point to another xml, VastUtils follow the link, parse the new xml, merge them and repeat the process.
Now, infiniteLoopUrl point to an XML that refers itself, so it is an infinite loop.
"correctly", the code follow xml link infinitely, and never resolve or reject the promise.
I expect above test fail after a certain timeout, but it didn't.
Someone can help me?
Thanks
EDIT:
I'm trying to reproduce an infinite Promise loop with a smaller example, and this is what i've noticed:
this test correctly FAIL after 5s:
test('Promise2', () => {
const genPromise = (): Promise<void> => {
return new Promise((res) => {
setTimeout(() => {
res();
}, 200);
})
.then(() => {
return genPromise();
});
};
const vastPromise = genPromise();
const expectedError =
new VastError(VastErrorCodes.WRAPPER_LIMIT_REACHED);
return expect(vastPromise).rejects.toEqual(expectedError);
});
This test DO NOT FAIL after 5s (jest remain in an infinite loop)
test('Promise', () => {
const genPromise = (prom: Promise<void>): Promise<void> => {
return prom
.then(() => {
return genPromise(Promise.resolve());
});
};
const vastPromise = genPromise(Promise.resolve());
const expectedError =
new VastError(VastErrorCodes.WRAPPER_LIMIT_REACHED);
return expect(vastPromise).rejects.toEqual(expectedError);
});
Apparently these are similar, but I don't understand the difference that cause the jest infinite loop...
Ok, I've understand the problem.
The cause is the mono thread nature of js.
In the two examples of the edit section, te first one have a timeout so there is a moment whent jest take the control and could check the timeout.
In the second one no, so jest never check the timeout.
In my real case, the problem was the fake server: it was created as:
server = sinon.fakeServer.create({
respondImmediately: true
});
respondImmediately make sinon respond syncroniously, so jest never have the control.
Creating it as:
server = sinon.fakeServer.create({
autoRespond: true
});
sinon respond after 10ms and jest can check the time passing