Writing visual tests that use the Native Mobile Grid

In a previous article, we described how to write an Eyes visual test using the ClassicRunner. When using the ClassicRunner, the Eyes SDK uses real mobile device, Android emulator or iOS simulator to capture screen shots for the checkpoints. The current article explains how to adapt this code so that it uses the Native Mobile Grid. When using the Native Mobile Grid, the Eyes SDK captures a view hierarchy snapshot and other resources and sends it to the Grid server, where the checkpoint image is rendered concurrently for all relevant configurations, along with other checkpoints in your test suite.

Using the Native Mobile Grid considerably reduces test time and also enables you to test multiple mobile devices in a single test run. For a full description of the Native Mobile Grid and what it can do for you, see the article Introduction to the Native Mobile Grid.

Overview

Most aspects of coding Eyes visual tests is the same irrespective of whether you use the Native Mobile Grid or not. The only difference is in how you set up the test suite. This setup consists of several steps:

The first step is typically done as part of the instrumentation process.

The instrumentation process is available for both iOS and Android platforms, to ease the integration of the test application with Applitools.

Applitools collects the application resources by instrumenting a dedicated library (“Grid library”) into the test application, in order to render the image across the other configured devices.

Specifying the use of the Native Mobile Grid

The Java snippet below illustrates how you can easily switch to using the Native Mobile Grid by assigning the runner an instance of a VisualGridRunner class instead of a ClassicRunner class.

public void beforeTestSuite() {
      /* REMOVE
      runner = new ClassicRunner();
      */
      runner = new VisualGridRunner(new RunnerOptions().testConcurrency(10));
      // continued below....
  }

The type of runner you pass to the Eyes constructor when you create it for each test determines whether its checkpoints are rendered using the Native Mobile Grid or not.

The RunnerOptions passed to the VisualGridRunner constructor is used to configure the runner. The example shows the method $runnerOptions.testConcurrency being passed. This option is used to limit the maximum number of Eyes tests that the runner will run simultaneously. Increasing the concurrency value can allow your test suite to run faster, but the maximum number of tests that can run simultaneously depends on your Eyes plan, if concurrency has been allocated at a team level, and what other tests are being run on your account at the same time.

Baselines

By default, when running tests using the Native Mobile Grid, every mobile device configuration uses the baseline implied by the execution environment of that configuration, i.e. the Device’s viewport size and its operating system:

  • When adding a mobile device (e.g. using the Configuration.addMobileDevice method), the Device and Viewport size baseline attributes are defined by the parameters of the method. The Operating system attributes will depend on the device type.

Example

The code below shows a complete example of a test that runs using the Native Mobile Grid.

The examples are for Android and iOS platforms, based on Java Appium and Espresso with Kotlin for Android and XCUI with Swift for iOS.

The code examples below are for demonstration purposes only. In order to be able to execute tests with the Native Mobile Grid, additional prerequisites are required.

Android

Framework: Appium

Language: Java

Add following dependency to the pom.xml file


<dependency>
    <groupId>com.applitools</groupId>
    <artifactId>eyes-appium-java5</artifactId>   
    <version>5.1.0</version>
</dependency>	

Write your test code:


import com.applitools.eyes.appium.Eyes;
import com.applitools.eyes.config.Configuration;
import com.applitools.eyes.visualgrid.model.AndroidDeviceInfo;
import com.applitools.eyes.visualgrid.model.AndroidDeviceName;
import com.applitools.eyes.visualgrid.model.DeviceAndroidVersion;
import com.applitools.eyes.visualgrid.model.ScreenOrientation;
import com.applitools.eyes.visualgrid.services.RunnerOptions;
import com.applitools.eyes.visualgrid.services.VisualGridRunner;
import io.appium.java_client.android.AndroidDriver;
import org.openqa.selenium.remote.DesiredCapabilities;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;

public class BasicDemo {

    public static void main(String[] args) throws 
MalformedURLException {
    VisualGridRunner visualGridRunner = new 
VisualGridRunner(new RunnerOptions().testConcurrency(5));
       Eyes eyes = new Eyes(visualGridRunner);
       eyes.setApiKey("API_KEY_HERE");

       // Configure grid devices to render on
       Configuration configuration = eyes.getConfiguration();
       configuration.addMobileDevice(new 
   AndroidDeviceInfo(AndroidDeviceName.Pixel_4));
        configuration.addMobileDevice(new 
   AndroidDeviceInfo(AndroidDeviceName.Pixel_4_XL, 
   ScreenOrientation.LANDSCAPE));
         configuration.addMobileDevice(new 
   AndroidDeviceInfo(AndroidDeviceName.Pixel_3_XL, 
   ScreenOrientation.LANDSCAPE, DeviceAndroidVersion.LATEST));

          eyes.setConfiguration(configuration);

         // Create an AndroidDriver instance to automate your app
         AndroidDriver driver = startApp();
         try {
        // Visually validate the stickers dialog
        eyes.open(driver, "My Android App", "Java Appium 
	Android UFG Demo");
        eyes.check("Check", Target.window());
        eyes.closeAsync();
     } finally {
        driver.quit();
        eyes.abortAsync();
        visualGridRunner.getAllTestResults();
    }
}

        private static AndroidDriver startApp() throws 
	MalformedURLException {
        DesiredCapabilities capabilities = new 
	DesiredCapabilities();
        capabilities.setCapability("platformName", "Android");
        capabilities.setCapability("deviceName", "Pixel 2");
        String appPath = new File("").getAbsolutePath() + 
	"/src/main/resources/app-debug.apk";
        capabilities.setCapability("app", appPath);
        capabilities.setCapability("platformVersion", "9");
        capabilities.setCapability("automationName", 
	"UiAutomator2");
         capabilities.setCapability("newCommandTimeout", 300);
         capabilities.setCapability("fullReset", false);
         capabilities.setCapability("noReset", true);
         return new AndroidDriver<>(new 
    URL("http://localhost:4723/wd/hub"), capabilities);
	}
}

Framework: Espresso

Language: Kotlin


package com.applitools.android.ufg.espresso
import androidx.test.core.app.ActivityScenario
Import com.applitools.android.ufg.espresso.ui.login.LoginActivity
import com.applitools.eyes.android.common.AndroidDeviceName
 
import com.applitools.eyes.android.common.logger.Logger
 
import
com.applitools.eyes.android.common.logger.StdoutLogHandler
 
import 
com.applitools.eyes.android.components.androidx.AndroidXComponen
tsProvider
import com.applitools.eyes.android.espresso.Eyes
 
import com.applitools.eyes.android.espresso.fluent.Target
 
import 
com.applitools.eyes.android.espresso.visualgrid.RunnerOptions
 
import 
com.applitools.eyes.android.espresso.visualgrid.VisualGridRunner
 
import org.junit.Before
 
import org.junit.Test

				
class KotlinTest {
 
    @Before
 
	fun beforeTest() { 
	    ActivityScenario.launch(LoginActivity::class.java) 
	}
 
	@Test
 
	fun testLogin() {
 
		val logger = Logger()
 
		logger.logHandler = StdoutLogHandler(true)
 
		val options = RunnerOptions()
 
		options.apiKey("<API_KEY_HERE>")
 
		val runner = VisualGridRunner(options)
		val eyes = Eyes(runner)
 
    	eyes.componentsProvider = AndroidXComponentsProvider()
 
	    eyes.configuration = eyes.configuration
 
   .addMobileDevice(AndroidDeviceInfo(AndroidDeviceName.Pixel_4))
 
		   .addMobileDevice(

		       AndroidDeviceInfo(
 
	 	          AndroidDeviceName.Pixel_4_XL
 	           )
		  )
 
		  .addMobileDevice(
 
		      AndroidDeviceInfo(

		         AndroidDeviceName.Pixel_3_XL
 
		      )
 
		 )

		yes.logHandler = logger.logHandler
 
		try {
	
		     eyes.open("AndroidX UFG test app", "Test Login page 
  UFG Kotlin")
 
		     eyes.check(Target.window().withName("Check"))

		     eyes.closeAsync()
 
		} finally {
 
		  runner.allTestResults
 
		  eyes.abortIfNotClosed()
 
		}
 
	}
 
}

iOS

Framework: XCUI

Language: Swift


import XCTest
import EyesXCUI

class UFGDemoTests: XCTestCase {

	static var visualGridRunner = UFGDemoTests.configureRunner()
	static var eyes = UFGDemoTests.configureEyes(runner: visualGridRunner)

	private static func configureRunner() -> VisualGridRunner {
			let options = 
	RunnerOptions().testConcurrency(5).apiKey("API_KEY_HERE")
			return VisualGridRunner(options: options)
	}

	private static func configureEyes(runner: VisualGridRunner) -> 
Eyes {
		let eyes = Eyes(runner: runner)

		let config = Configuration()
		let iPhone8 = IosDeviceInfo(deviceName: .deviceiPhone8)
		let iPhoneProMax = IosDeviceInfo(deviceName: 
	.deviceiPhone12ProMax)
		let iPad = IosDeviceInfo(deviceName: 
	.deviceiPadPro3rdgeneration)
		config.addMobileDevices([iPhone8, iPhoneProMax, iPad])

		eyes.configuration = config

		return eyes
	}

		override class func setUp() {
		let app = 
XCUIApplication(bundleIdentifier:"com.mycompany.myapp")
		app.launchEnvironment["UFG_LIB_FRAMEWORK"] = ""
		app.launch()

		visualGridRunner = UFGDemoTests.configureRunner()
		eyes = UFGDemoTests.configureEyes(runner: 
visualGridRunner)
		eyes.testedApplication = app
		eyes.open(withApplicationName: "My iOS App", testName: 
"eyesXCUI UFG Demo")
		}

		func testMyCheck() throws {
		Self.eyes.check(withTag: "Check", andSettings: 
	Target.window())
		}

		static override func tearDown() {
			eyes.closeAsync()

			let allTestResults = 
	visualGridRunner.getAllTestResultsShouldThrowException(false)
			print(allTestResults)
		}

	}