React Native Detox - Local and CI have different outcomes - ios

I am having strange issue with React Native Detox testing when being ran on local and CI environments.
Following is the piece of code being ran on both of the environments:
import { E2E_IDS } from './constants';
import {
executeBeforeEachTest,
tapById,
testAccount,
typeTextById,
waitForId,
waitForText,
} from './utils';
describe('User email sign in flow test', () => {
beforeEach(executeBeforeEachTest);
it('should login with provided user credentials successfully', async () => {
await tapById(E2E_IDS.SIGN_IN);
await waitForText('Welcome back');
await tapById(E2E_IDS.SIGN_IN_VIA_EMAIL);
await typeTextById(E2E_IDS.SIGN_IN_EMAIL, testAccount.USER);
await typeTextById(E2E_IDS.SIGN_IN_PASSWORD, testAccount.PASS);
await tapById(E2E_IDS.SIGN_IN_LOGIN_BUTTON);
await waitForText('You have no classes yet.');
});
});
On my local mac machine, e2e tests runs fine as expected.
On Github CI mac machine, e2e fails because single tap on SIGN_IN_LOGIN_BUTTON is not enough, if I do the following:
await tapById(E2E_IDS.SIGN_IN_LOGIN_BUTTON, 2);
If we tap the button twice, then it passes the test. I wonder why the first tap is not being acknowledged by the CI machine, that we had to do twice.
If anyone could help narrow down the root cause of this behaviour that would be great.

The last issue was quite tricky, it was actually a UX bug. So, when user types their email and password in the login form, the keyboard doesn't drop when we tap on the login button, because we had a keyboard overlay which is blocking the login button being tapped. To circumvent this issue, we need to add keyboardShouldPersistTaps='handled' to the top level scrollView which will trigger down the tap events to it's child which will result in closing the keyboard plus the tap to right element, all in one go.
It was strange why this didn't happen on my local simulator, perhaps, we can toggle the keyboard on/off which didn't help me to narrow down the issue at hand.
Why 2 taps?
Because, first one was to drop the keyboard (inactive state) then the next one was to tap on the login button.

Related

Splash screen prevent from hiding after ejecting expo

Recently I have ejected my project from expo to dare and do all the necessary things like pod install and all.
When I run my project Its runs fine but as soon as I click on any modal or any other button its don't show any error logs and shows Splashscreen and it's not hiding the code which should be executed in the use effect is not excuting. Sometimes it's said.
'SplashScreen.show' has already been called for given view controller.
It works well on the real device but the splash screen not hiding on the simulator.
I am stuck in for the last 3 days and do all the necessary things. Check almost all the questions on StackOverflow regarding this.
I have also try below code but it doesn't work.
import * as SplashScreen from 'expo-splash-screen';
useEffect(() => {
console.log('A');
setTimeout(async () => {
console.log('B');
await SplashScreen.hideAsync();
}, 10000);
}, []);
useEffect(async () => {
await SplashScreen.hideAsync();
}, []);
I have tried almost everything and all the things were working perfectly before ejecting;

Testing timeout-related tests, such as Popups/Tooltips etc with Detox

I've beed using detox for a while, but after upgrade to 17.5.+ I have started facing an issue with Popups testing. I haven't find useful info on a stack overflow and on detox issues.
Problem here is the following: I have a test to check if correct popup is shown. It looks like:
it('expect to see "Answer correct" popup', async () => {
await openQuestionnaire('theory_questionnaire_learn_button');
await scroll('questionnaire_scroll_view', 150);
await element(by.id('select_1_button')).tap();
await element(by.id('questionnaire_answer_button')).tap();
// #ts-ignore
await expect(element(by.id('questionnaire_simple_popup'))).toBeVisible();
// #ts-ignore
await expect(element(by.id('questionnaire_simple_popup'))).toHaveLabel('correct');
await delay(500);
// #ts-ignore
await expect(element(by.id('questionnaire_simple_popup'))).toNotExist();
});
Background for this: after questionnaire_answer_button is tapped, popup appears. It is visible for 500ms and then it disappears. For this 500ms I'm using setTimeout({ () => dismiss() }, 500).
Popup is visible on an emulator, but my test fails with Test Failed: No elements found for “MATCHER(identifier: == “questionnaire_simple_popup”)”
Checking hierarchy I haven't find this element there. Maybe someone else have already faced this and know a solution?
If the identifier is not found, it means the identifier has not been properly set with the native view’s accessibility identifier. Check with the popup developer that testID are properly forwarded to the native views.
Ok, what have I found out after several tries. It start working after increasing timeout from 500ms to 3000ms. 2500 was still failing, 2750 haven't tried. Regarding matchers. by.id and by.text works as well.
So the reason probably was a timeout duration.

Know the trigger when user has pressed cancel buttons during In-App purchase

I have integrated IAP functionality in my flutter app. I wants to know when user has clicked the "Cancel" button on IAP popup.
There are many solution found here but that all code is in native.
It will be very helpful If I get the trigger of cancel button click in flutter iOS.
On the cancel button click I need to do other functionalities.
I have followed the IAP integration code from this link:
https://fireship.io/lessons/flutter-inapp-purchases/
and the IAP popup is shown as default popup not a custom.
Please give some suggestion. Any help would be appreciated.
Thank you.
I created a loop to look for failed transactions and then finish them accordingly.
I am still experimenting with where to do this to try to avoid a user error. I am also going to try to do a similar process in the stream listener that processes purchases.
There are StoreKit Wrappers included with IAP plugin:
Note: I looked at SK info for XCode and it seems fine to do this with a failed transaction. They said it is a 'no no' to do it to a pending one.
Future<Null> _removeFailedPendingPurchases() async {
debugPrint(':::: SWIPE WAR ::::: _removeFailedPendingPurchases()');
final SKPaymentQueueWrapper paymentWrapper = SKPaymentQueueWrapper();
final List<SKPaymentTransactionWrapper> transactions =
await paymentWrapper.transactions();
debugPrint(
':::: SWIPE WAR ::::: transactions ${transactions.length} ${transactions.toString()}');
for (SKPaymentTransactionWrapper transaction in transactions) {
debugPrint('TRANSACTION everything : ${transaction.toString()}');
debugPrint('TRANSACTION STATE : ${transaction.transactionState}');
if (transaction.transactionState ==
SKPaymentTransactionStateWrapper.failed)
await paymentWrapper.finishTransaction(transaction);
}
return;
}
When you press cancel button the PurchaseDetails.status will be equal to PurchaseStatus.error. So you check it:
if (purchaseDetails.status == PurchaseStatus.error) {
//Change UI state to reflect this problem
}

ReactJs PWA not updating on iOS

I'm building a ReactJs PWA but I'm having trouble detecting updates on iOS.
On Android everything is working great so I'm wondering if all of this is related to iOS support for PWAs or if my implementation of the service worker is not good.
Here's what I've done so far:
Build process and hosting
My app is built using webpack and hosted on AWS. Most of the files (js/css) are built with some hash in their name, generated from their content. For those which aren't (app manifest, index.html, sw.js), I made sure that AWS serves them with some Cache-Control headers preventing any cache. Everything is served over https.
Service Worker
I kept this one as simple as possible : I didn't add any cache rules except precache for my app-shell:
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
Service-worker registration
Registration of the service worker occurs in the main ReactJs App component, in the componentDidMount() lifecycle hook:
componentDidMount() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
reg.onupdatefound = () => {
this.newWorker = reg.installing;
this.newWorker.onstatechange = () => {
if (this.newWorker.state === 'installed') {
if (reg.active) {
// a version of the SW is already up and running
/*
code omitted: displays a snackbar to the user to manually trigger
activation of the new SW. This will be done by calling skipWaiting()
then reloading the page
*/
} else {
// first service worker registration, do nothing
}
}
};
};
});
}
}
Service worker lifecycle management
According to the Google documentation about service workers, a new version of the service worker should be detected when navigating to an in-scope page. But as a single-page application, there is no hard navigation happening once the app has been loaded.
The workaround I found for this is to hook into react-router and listen for route changes, then manually ask the registered service worker to update itself :
const history = createBrowserHistory(); // from 'history' node package
history.listen(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.getRegistration()
.then((reg) => {
if (!reg) {
return null;
}
reg.update();
});
}
});
Actual behavior
Throwing a bunch of alert() everywhere in the code showed above, this is what I observe :
When opening the pwa for the first time after adding it to the homescreen, the service worker is registered as expected, on Android and iOS
While keeping the app opened, I deploy a new version on AWS. Navigating in the app triggers the manual update thanks to my history listener. The new version is found, installed in the background. Then my snackbar is displayed and I can trigger the switch to the new SW.
Now I close the app and deploy a new version on AWS. When opening the app again :
On Android the update is found immediately as Android reloads the page
iOS does not, so I need to navigate within the app for my history listener to trigger the search for an update. When doing so, the update is found
After this, for both OS, my snackbar is displayed and I can trigger the switch to the new SW
Now I close the app and turn off the phones. After deploying a new version, I start them again and open the app :
On Android, just like before, the page is reloaded which detects the update, then the snackbar is displayed, etc..
On iOS, I navigate within the app and my listener triggers the search for an update. But this time, the new version is never found and my onupdatefound event handler is never triggered
Reading this post on Medium from Maximiliano Firtman, it seems that iOS 12.2 has brought a new lifecycle for PWAs. According to him, when the app stays idle for a long time or during a reboot of the device, the app state is killed, as well as the page.
I'm wondering if this could be the root cause of my problem here, but I was not able to find anyone having the same trouble so far.
So after a lot of digging and investigation, I finally found out what was my problem.
From what I was able to observe, I think there is a little difference in the way Android and iOS handle PWAs lifecycle, as well as service workers.
On Android, when starting the app after a reboot, it looks like starting the app and searching an update of the service worker (thanks to the hard navigation occuring when reloading the page) are 2 tasks done in parallel. By doing that, the app have enough time to subscribe to the already existing service worker and define a onupdatefound() handler before the new version of the service worker is found.
On the other hand with iOS, it seems that when you start the app after a reboot of the device (or after not using it for a long period, see Medium article linked in the main topic), iOS triggers the search for an update before starting your app. And if an update is found, it will be installed and and enter its 'waiting' status before the app is actually started. This is probably what happens when the splashscreen is displayed...
So in the end, when your app finally starts and you subscribe to the already existing service worker to define your onupdatefound() handler, the update has already been installed and is waiting to take control of the clients.
So here is my final code to register the service worker :
componentDidMount() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
if (reg.waiting) {
// a new version is already waiting to take control
this.newWorker = reg.waiting;
/*
code omitted: displays a snackbar to the user to manually trigger
activation of the new SW. This will be done by calling skipWaiting()
then reloading the page
*/
}
// handler for updates occuring while the app is running, either actively or in the background
reg.onupdatefound = () => {
this.newWorker = reg.installing;
this.newWorker.onstatechange = () => {
if (this.newWorker.state === 'installed') {
if (reg.active) {
// a version of the SW already has control over the app
/*
same code omitted
*/
} else {
// very first service worker registration, do nothing
}
}
};
};
});
}
}
Note :
I also got rid of my listener on history that I used to trigger the search for an update on every route change, as it seemed overkill.
Now I rely on the Page Visibility API to trigger this search every time the app gets the focus :
// this function is called in the service worker registration promise, providing the ServiceWorkerRegistration instance
const registerPwaOpeningHandler = (reg) => {
let hidden;
let visibilityChange;
if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
hidden = 'hidden';
visibilityChange = 'visibilitychange';
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden';
visibilityChange = 'msvisibilitychange';
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden';
visibilityChange = 'webkitvisibilitychange';
}
window.document.addEventListener(visibilityChange, () => {
if (!document[hidden]) {
// manually force detection of a potential update when the pwa is opened
reg.update();
}
});
return reg;
};
As noted by Speckles (thanks for saving me the headache), iOS installs the new SW before launching the app. So the SW doesn't get a chance to catch the 'installing' state.
Work-around: check if the registration is in the waiting state then handle it.
I've made an (untested) example of handling this. - a mod to the default CRA SW.

Why does this Spectron code block my Electron app after the first click?

This is the first time I'm trying to create automated tests for an Electron app using Spectron. It might be my rusty knowledge of async programming but I don't know why the code below is misbehaving:
it ('should allow me to create an account', function() {
return app.client
.waitUntilWindowLoaded()
.waitForExist('//a[text()="Create Free Account"]')
.click('//a[text()="Create Free Account"]')
.waitForExist('//button[text()="Create Account"]')
.setValue('#Email', "test#test.com")
.setValue('#Password', "Password1!")
.click('//button[text()="Create Account"]')
.waitForExist('//p[contains(text(),"Almost done.")]')
});
The test seems to get as far as the first click(), then it should wait for the App to request a new page, eventually displaying a "Create Account" button. However, for some reason, the app itself seems to block at this point. I know the click is occurring. When I try it manually, the app behaves properly.
Mark
I think you need to wait till the next page loads
Also chain
.pause(3*1000)
after clicking the button
(Or)
.waitUntilWindowLoaded(3*1000)

Resources