Losing webdriverio session when testing electron app restart using spectron - electron

I'm using spectron to run integration tests against my electron app. Everything is working fine apart from attempting to test that app settings are persisted properly between app restarts.
While running tests, my app starts up with new temporary userData directory for every test which ensures that the tests are isolated. This means the the persistence testing needs to ideally occur in a single test and to achieve this I have to restart the app in the middle of the test. There's an app.restart method so this has got to be supported right?
I'm using the following spectron test code:
// save some settings here
await app.restart();
await app.client.waitUntilWindowLoaded()
// do some more checking to ensure the app is fully loaded
// check the settings here
However I'm getting the following error:
Error: waitUntilWindowLoaded Promise was rejected with the following reason:
Error: A session id is required for this command but wasn't found in the response payload
What's the correct way to do this? I've also tried stopping the Application instance and starting a new one with similar results.

This appears to work
// save some settings here
await app.stop();
app = new Application({ ... });
await app.start();
await app.client.waitUntilWindowLoaded();
// do some more checking to ensure the app is fully loaded
// check the settings here

Following snippsets works,
import test from 'ava'
import util from '../util'
test(async t => {
// This runs after each test and other test hooks, even if they failed
let app = util.createApp()
app = await util.waitForLoad(app, t)
await app.restart()
await app.client.waitUntilWindowLoaded()
// app = await util.waitForLoad(app, t)
})
works with, "spectron": "^3.5.0"

Related

Sending commands to the web app in electron renderer process since contextIsolation changes

I understand that contextIsolation changes are introduced for security purposes, and i read about contextBridge which exposes custom API to the webapp running in the renderer process so web app can control electron app in which is runing.
electron preload script
const contextBridge = require("electron").contextBridge;
contextBridge.exposeInMainWorld("electronApi", {
'doSomething' : function () {
// some code to execute
}
});
web app
window.electronApi.doSomething();
This is perfectly clear and i understand why is this done this way.
However, i do not understand how can communication work the other way, so how can electron execute web app commands? Let's take following example, web app has window.someWebAppMethod defined and electron should execute it
web app
window.someWebAppMethod = function () {
// do somehing web app related
}
electron preload script
window.someWebAppMethod()
^^ this does not work because of contextIsolation which was whole point of contextIsolation, but i still need to have a certain way of triggering web app commands from electron. Most obvious reason is let's say i have electron main menu with command labeled "Open Quick Jump" which should tell the web app loaded in rendered process to execute method which will show the "Quick Jump" function of the web app.
Maybe i'm missing something painfully obvious, but i'd still appreciate any help i can get.
Thanks
I found a way of doing this. Not sure it's obvious or if it is secure enough, but here it is:
electron preload script
const contextBridge = require("electron").contextBridge;
let doSomethingInWebApp = null;
contextBridge.exposeInMainWorld("electronApi", {
'exposeDoSomethingInWebApp' : function (callback) {
doSomethingInWebApp = callback;
}
});
web app
if (window.electronApi && window.electronApi.exposeDoSomethingInWebApp) {
window.electronApi.exposeDoSomethingInWebApp(function () {
// execute whatever you need to execute in webapp
});
}
electron preload script
if (doSomethingInWebApp) {
// execute previously defined custom behavior in web app
doSomethingInWebApp();
}
So it's quite simple and it works.

AWS Transcribe stuck in getTranscriptionJob in iOS

I'm trying to use AWS Transcribe in an iOS app using the aws-sdk-ios. The app starts a transcription job and I can see the job on the AWS console. But the app can't list the jobs, or get a specific job, because it gets stuck in the request to getTranscriptionJob or listTranscriptionJobs, as these requests never complete (I added a print statement and a breakpoint inside the completion block, and it never prints, nor reaches the breakpoint).
I uploaded to GitHub a sample single-view app demonstrating the problem. You'll need an AWS account or IAM user with full permissions on S3 and Transcribe. Insert that account's keys and S3 bucket in ViewController.swift in the appropriate variables.
https://github.com/joaomarceloods/AWSTranscribeBug
I need help. Is this a bug, or am I doing something wrong?
Swift, iOS 13.2, CocoaPods, AWSCore 2.12.1, AWSTranscribe 2.12.1
Most important snippet:
/// `getTranscriptionJob` repeatedly until the status is no longer `inProgress`.
/// However, `getTranscriptionJob` never completes.
var transcriptionInProgress = true
while transcriptionInProgress {
print("getTranscriptionJob")
transcribe.getTranscriptionJob(request).continueWith { task -> Any? in
print("getTranscriptionJob never completes...")
let transcriptionJob = task.result?.transcriptionJob
transcriptionInProgress = transcriptionJob?.transcriptionJobStatus == .inProgress
return nil
}.waitUntilFinished()
}
print("...after the getTranscriptionJob")
I found a solution.
I still don't understand why getTranscriptionJob freezes, but it will execute normally if you run it on DispatchQueue.global():
.continueWith(executor: AWSExecutor(dispatchQueue: DispatchQueue.global())) {
Sample code diff: https://github.com/joaomarceloods/AWSTranscribeBug/commit/98e43af553413ed2bbd0c3f96e259139a991303e
Reference: https://aws-amplify.github.io/docs/ios/how-to-ios-asynchrounous-tasks#executing-a-block-on-the-main-thread-with-awstask

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.

Reset app state between InstrumentationTestCase runs

One of my QA engineers is supporting an app with a fairly large codebase and a lot of different SharedPreferences files. He came to me the other day asking how to reset the application state between test runs, as if it had been uninstalled-reinstalled.
It doesn't look like that's supported by Espresso (which he is using) nor by the Android test framework natively, so I'm not sure what to tell him. Having a native method to clear all the different SharedPreferences files would be a pretty brittle solution.
How can one reset the application state during instrumentation?
Current espresso doesn't provide any mechanism to reset application state. But for each aspect (pref, db, files, permissions) exist a solution.
Initial you must avoid that espresso starts your activity automatically so you have enough time to reset.
#Rule
public ActivityTestRule<Activity> activityTestRule = new ActivityTestRule<>(Activity.class, false, false);
And later start your activity with
activityTestRule.launchActivity(null)
For reseting preferences you can use following snippet (before starting your activity)
File root = InstrumentationRegistry.getTargetContext().getFilesDir().getParentFile();
String[] sharedPreferencesFileNames = new File(root, "shared_prefs").list();
for (String fileName : sharedPreferencesFileNames) {
InstrumentationRegistry.getTargetContext().getSharedPreferences(fileName.replace(".xml", ""), Context.MODE_PRIVATE).edit().clear().commit();
}
You can reset preferences after starting your activity too. But then the activity may have already read the preferences.
Your application class is only started once and already started before you can reset preferences.
I have started to write an library which should make testing more simple with espresso and uiautomator. This includes tooling for reseting application data. https://github.com/nenick/espresso-macchiato See for example EspAppDataTool with the methods for clearing preferences, databases, cached files and stored files.
Improving on #nenick's solution, encapsulate the state clearing behavior in a custom ActivityTestRule. If you do this, you can allow the test to continue to launch the activity automatically without intervention from you. With a custom ActivityTestRule, the activity is already in the desired state when it launches for the test.
Rules are particularly useful because they're not tied to any specific test class, so can be easily reused within any test class or any project.
Below is one I implemented to ensure that the app is signed out when the activity launches, per test. Some tests, when they failed, were leaving the app in a signed in state. This would then cause later tests to also fail because the later ones assumed they would need to sign in, but the app would already be signed in.
public class SignedOutActivityTestRule<T extends Activity> extends ActivityTestRule<T> {
public SignedOutActivityTestRule(Class<T> activityClass) {
super(activityClass);
}
#Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
InstrumentationRegistry.getTargetContext()
.getSharedPreferences(
Authentication.SHARED_PREFERENCES_NAME,
Context.MODE_PRIVATE)
.edit()
.remove(Authentication.KEY_SECRET)
.remove(Authentication.KEY_USER_ID)
.apply();
}
}
you can try add this to gradle:
android {
...
defaultConfig {
...
testInstrumentationRunnerArguments clearPackageData: 'true'
}
}
refer to https://developer.android.com/training/testing/junit-runner
To remove all shared state from your device's CPU and memory after each test, use the clearPackageData flag.

Caching issue in uiautomator

I have an android app. I am doing automated testing of this app using uiautomator.
Before doing any processing I need to login the app. But at first time it store the data in cache and login automatically every time when I launch.
I want application should be logged in every time with filled credential.
Is there any way to stop this caching by using uiautomator api.
For those of you who wants to clear your target app under test, while running UiAutomator, you could probably try the method I wrote below. This only works with API level 18 or greater.
http://developer.android.com/reference/android/app/UiAutomation.html#executeShellCommand(java.lang.String)
public static void clearData (String packageName) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.executeShellCommand("pm clear " + packageName)
.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
You can give below command before you start your tests, this would clear you application data. Then start your tests !
adb shell pm clear yourPackageName
In your app you can manually first clear the data or disable the "Save my credential" option in the app. So every time when starting the app it will ask for the credentials again.
Or else you can directly write it in command prompt before running the test case.
adb shell pm clear yourPackageName

Resources