Detox tests are breaking - react-native

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();
});
});

Related

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

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!

How to use global intercept for all tests in Cypress 6?

I want to test every page of my site (Vue/Nuxt), but API calls should be mocked.
For that there is intercept() function (route() in previous Cypress versions):
homepage.spec.js:
describe('home page', () => {
before(() => {
cy.intercept('**/user-data', { fixture: 'user.json' })
cy.visit('http://localhost:8080')
})
it('renders header, () => {
cy.contains('#header', 'test');
});
// some other tests for homepage
});
Cool, it works, but I have this specific API call (getting logged user info) on every page. I don't want to repeat this intercept code in every test file, for example contact.spec.js. I cannot find a way to set this intercept globally? What is the correct way to mocking axios?

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.

Detox test failed on typeText if text is "long"

Have some interesting problem with detox testing!
All configs are clearly right (cause in another case it wasn't even work, yeah?)
Work with Mocha tests, and yes, it works.
I use redux to work with state and custom components in my main component.
<OpacityInput
idTest="LoginEmailInput"
icon="account"
placeholder="Login..."
value={this.props.email}
onChangeText={this.emailChanged}
/>
So i pass all data by props to component (testID and value for inputs i pass too)...and here is my test (i make two restricted test to catch an error)
it('should find input login', async () => {
await expect(element(by.id('LoginEmailInput'))).toBeVisible()
})
it('should type text login', async () => {
await element(by.id('LoginEmailInput')).typeText('test#test.com')
})
And it is really interesting...if i want to type 'a'(two-three symbol max) - it is ok! Text passing with no problem! But when i change my 'a' to real e-mail...it crash!
I have a strange feeling that detox has only few seconds for typeText, and if it can't so...it crash :)
Here is the error after crash. I used
detox test —loglevel trace
for details. Test on IOS 6

Detox: Testing a React-Native spinner with a stop button

Was wondering if anyone has had a similar issue.
In the app I'm working with, we have a spinner showing downloading content with a stop button in the middle. When the user taps the spinner/stop button, the download is meant to cancel. For reference the spinner/stop button looks like this on iOS:
I'm trying to write an e2e test for this functionality using Detox. It doesn't work using automatic synchronisation as the animation (and the download) keeps the thread running. I've tried using device.disableSynchronization() but I haven't had any success.
Here's my e2e test for reference:
it('should start and then cancel a download from the <My Learning> screen', async() => {
// setup
await device.reloadReactNative()
await expect(element(by.id('feed_screen'))).toBeVisible()
await element(by.id('LearningPlan_Tab')).tap()
await expect(element(by.id('learning-plan-screen'))).toBeVisible()
// Tap the download icon, it will turn into a spinner
await element(by.id('offline_download_c2a')).atIndex(1).tap()
// Alert box appears with Cancel/Download options
await expect(element(by.label('Download')).atIndex(1)).toBeVisible()
await element(by.label('Download')).atIndex(1).tap()
// Ideally this would work, but it doesn't (the above alert box doesn't dismiss properly)
await device.disableSynchronization()
await waitFor(element(by.id('download_spinner_c2a')).atIndex(0)).toBeVisible().withTimeout(5000)
await element(by.id('download_spinner_c2a')).atIndex(0).tap()
await device.enableSynchronization()
await element(by.label('Cancel download')).tap()
await expect(element(by.id('offline_download_c2a')).atIndex(1)).toBeVisible()
})
When this test runs the app still seems to wait for the download to finish. Does anyone know any suggestions on the best way to test this, or if it's even possible?
I've found a way to make it work, though it seems quite flaky:
it('should start and then cancel a download from the <My Learning> screen', async () => {
//...
await device.disableSynchronization()
// This timeout seems to help
await waitFor(element(by.id('download_spinner_c2a')).atIndex(0)).toBeVisible().withTimeout(10000)
await element(by.id('download_spinner_c2a')).atIndex(0).tap()
await device.enableSynchronization()
await element(by.label('Cancel download')).tap()
await expect(element(by.id('offline_download_c2a')).atIndex(1)).toBeVisible()
})