com.tascape.qa.th.android.driver.App Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2015 - 2016 Nebula Bay.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tascape.qa.th.android.driver;
import com.android.uiautomator.stub.IUiCollection;
import com.android.uiautomator.stub.IUiDevice;
import com.android.uiautomator.stub.IUiObject;
import com.android.uiautomator.stub.IUiScrollable;
import com.android.uiautomator.stub.Rect;
import com.google.common.collect.Lists;
import com.tascape.qa.th.Utils;
import com.tascape.qa.th.android.model.UIANode;
import com.tascape.qa.th.android.model.WindowHierarchy;
import com.tascape.qa.th.driver.EntityDriver;
import com.tascape.qa.th.exception.EntityDriverException;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author linsong wang
*/
@SuppressWarnings("ProtectedField")
public abstract class App extends EntityDriver {
private static final Logger LOG = LoggerFactory.getLogger(App.class);
public static final String SYSPROP_APK_PATH = "qa.th.android.APK_PATH";
public static final int NUMBER_OF_HOME_PAGE = 10;
protected UiAutomatorDevice device;
protected IUiDevice uiDevice;
protected IUiObject uiObject;
protected IUiCollection uiCollection;
protected IUiScrollable uiScrollable;
protected String version;
public abstract String getPackageName();
public abstract int getLaunchDelayMillis();
@Override
public String getVersion() {
if (StringUtils.isBlank(version)) {
try {
version = device.getAppVersion(getPackageName());
} catch (IOException | EntityDriverException ex) {
LOG.warn(ex.getMessage());
version = "";
}
}
return version;
}
public UiAutomatorDevice getDevice() {
return device;
}
public void setDevice(UiAutomatorDevice device) {
this.device = device;
}
public void fetchUiaStubs() {
uiDevice = device.getUiDevice();
uiObject = device.getUiObject();
uiCollection = device.getUiCollection();
uiScrollable = device.getUiScrollable();
}
public void launch() throws IOException, InterruptedException {
this.launch(true);
}
public void launch(boolean killExisting) throws IOException, InterruptedException {
if (StringUtils.isBlank(this.getPackageName())) {
return;
}
if (killExisting) {
device.getAdb().shell(Lists.newArrayList("am", "force-stop", this.getPackageName()));
}
device.waitForIdle();
device.getAdb().shell(Lists.newArrayList("monkey", "-p", this.getPackageName(), "1"));
Utils.sleep(this.getLaunchDelayMillis(), "wait for app to launch");
}
public void launchFromUi(boolean killExisting) throws IOException, InterruptedException {
if (killExisting) {
device.getAdb().shell(Lists.newArrayList("am", "force-stop", this.getPackageName()));
}
device.backToHome();
device.waitForIdle();
if (device.descriptionExists("Apps")) {
device.clickByDescription("Apps");
}
String name = getName();
if (!device.textExists(name)) {
int w = device.getScreenDimension().width;
int h = device.getScreenDimension().height;
device.dragHorizontally(w * NUMBER_OF_HOME_PAGE);
for (int i = 0; i < NUMBER_OF_HOME_PAGE; i++) {
if (device.textExists(name)) {
break;
} else {
LOG.debug("swipe to next screen");
device.swipe(w / 2, h / 2, 0, h / 2, 5);
device.takeDeviceScreenshot();
}
}
}
device.clickByText(name);
Utils.sleep(this.getLaunchDelayMillis(), "wait for app to launch");
device.waitForIdle();
}
/**
* The method starts a GUI to let an user inspect element tree and take screenshot when the user is interacting
* with the app-under-test manually. Please make sure to set timeout long enough for manual interaction.
*
* @param timeoutMinutes timeout in minutes to fail the manual steps
*
* @throws Exception if case of error
*/
public void interactManually(int timeoutMinutes) throws Exception {
LOG.info("Start manual UI interaction");
long end = System.currentTimeMillis() + timeoutMinutes * 60000L;
AtomicBoolean visible = new AtomicBoolean(true);
AtomicBoolean pass = new AtomicBoolean(false);
String tName = Thread.currentThread().getName() + "m";
SwingUtilities.invokeLater(() -> {
JDialog jd = new JDialog((JFrame) null, "Manual Device UI Interaction - " + device.getProductDetail());
jd.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
JPanel jpContent = new JPanel(new BorderLayout());
jd.setContentPane(jpContent);
jpContent.setPreferredSize(new Dimension(1088, 828));
jpContent.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
JPanel jpInfo = new JPanel();
jpContent.add(jpInfo, BorderLayout.PAGE_START);
jpInfo.setLayout(new BorderLayout());
{
JButton jb = new JButton("PASS");
jb.setForeground(Color.green.darker());
jb.setFont(jb.getFont().deriveFont(Font.BOLD));
jpInfo.add(jb, BorderLayout.LINE_START);
jb.addActionListener(event -> {
pass.set(true);
jd.dispose();
visible.set(false);
});
}
{
JButton jb = new JButton("FAIL");
jb.setForeground(Color.red);
jb.setFont(jb.getFont().deriveFont(Font.BOLD));
jpInfo.add(jb, BorderLayout.LINE_END);
jb.addActionListener(event -> {
pass.set(false);
jd.dispose();
visible.set(false);
});
}
JLabel jlTimeout = new JLabel("xxx seconds left", SwingConstants.CENTER);
jpInfo.add(jlTimeout, BorderLayout.CENTER);
jpInfo.add(jlTimeout, BorderLayout.CENTER);
new SwingWorker() {
@Override
protected Long doInBackground() throws Exception {
while (System.currentTimeMillis() < end) {
Thread.sleep(1000);
long left = (end - System.currentTimeMillis()) / 1000;
this.publish(left);
}
return 0L;
}
@Override
protected void process(List chunks) {
Long l = chunks.get(chunks.size() - 1);
jlTimeout.setText(l + " seconds left");
if (l < 850) {
jlTimeout.setForeground(Color.red);
}
}
}.execute();
JPanel jpResponse = new JPanel(new BorderLayout());
JPanel jpProgress = new JPanel(new BorderLayout());
jpResponse.add(jpProgress, BorderLayout.PAGE_START);
JTextArea jtaJson = new JTextArea();
jtaJson.setEditable(false);
jtaJson.setTabSize(4);
Font font = jtaJson.getFont();
jtaJson.setFont(new Font("Courier New", font.getStyle(), font.getSize()));
JTree jtView = new JTree();
JTabbedPane jtp = new JTabbedPane();
jtp.add("tree", new JScrollPane(jtView));
jtp.add("json", new JScrollPane(jtaJson));
jpResponse.add(jtp, BorderLayout.CENTER);
JPanel jpScreen = new JPanel();
jpScreen.setMinimumSize(new Dimension(200, 200));
jpScreen.setLayout(new BoxLayout(jpScreen, BoxLayout.PAGE_AXIS));
JScrollPane jsp1 = new JScrollPane(jpScreen);
jpResponse.add(jsp1, BorderLayout.LINE_START);
JPanel jpJs = new JPanel(new BorderLayout());
JTextArea jtaJs = new JTextArea();
jpJs.add(new JScrollPane(jtaJs), BorderLayout.CENTER);
JSplitPane jSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, jpResponse, jpJs);
jSplitPane.setResizeWeight(0.88);
jpContent.add(jSplitPane, BorderLayout.CENTER);
JPanel jpLog = new JPanel();
jpLog.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));
jpLog.setLayout(new BoxLayout(jpLog, BoxLayout.LINE_AXIS));
JCheckBox jcbTap = new JCheckBox("Enable Click", null, false);
jpLog.add(jcbTap);
jpLog.add(Box.createHorizontalStrut(8));
JButton jbLogUi = new JButton("Log Screen");
jpResponse.add(jpLog, BorderLayout.PAGE_END);
{
jpLog.add(jbLogUi);
jbLogUi.addActionListener((ActionEvent event) -> {
jtaJson.setText("waiting for screenshot...");
Thread t = new Thread(tName) {
@Override
public void run() {
LOG.debug("\n\n");
try {
WindowHierarchy wh = device.loadWindowHierarchy();
jtView.setModel(getModel(wh));
jtaJson.setText("");
jtaJson.append(wh.root.toJson().toString(2));
jtaJson.append("\n");
File png = device.takeDeviceScreenshot();
BufferedImage image = ImageIO.read(png);
int w = device.getDisplayWidth();
int h = device.getDisplayHeight();
BufferedImage resizedImg = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = resizedImg.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(image, 0, 0, w, h, null);
g2.dispose();
JLabel jLabel = new JLabel(new ImageIcon(resizedImg));
jpScreen.removeAll();
jsp1.setPreferredSize(new Dimension(w + 30, h));
jpScreen.add(jLabel);
jLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
LOG.debug("clicked at {},{}", e.getPoint().getX(), e.getPoint().getY());
if (jcbTap.isSelected()) {
device.click(e.getPoint().x, e.getPoint().y);
device.waitForIdle();
jbLogUi.doClick();
}
}
});
} catch (Exception ex) {
LOG.error("Cannot log screen", ex);
jtaJson.append("Cannot log screen");
}
jtaJson.append("\n\n\n");
LOG.debug("\n\n");
jd.setSize(jd.getBounds().width + 1, jd.getBounds().height + 1);
jd.setSize(jd.getBounds().width - 1, jd.getBounds().height - 1);
}
};
t.start();
});
}
jpLog.add(Box.createHorizontalStrut(38));
{
JButton jbLogMsg = new JButton("Log Message");
jpLog.add(jbLogMsg);
JTextField jtMsg = new JTextField(10);
jpLog.add(jtMsg);
jtMsg.addFocusListener(new FocusListener() {
@Override
public void focusLost(final FocusEvent pE) {
}
@Override
public void focusGained(final FocusEvent pE) {
jtMsg.selectAll();
}
});
jtMsg.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(java.awt.event.KeyEvent e) {
if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ENTER) {
jbLogMsg.doClick();
}
}
});
jbLogMsg.addActionListener(event -> {
Thread t = new Thread(tName) {
@Override
public void run() {
String msg = jtMsg.getText();
if (StringUtils.isNotBlank(msg)) {
LOG.info("{}", msg);
jtMsg.selectAll();
}
}
};
t.start();
try {
t.join();
} catch (InterruptedException ex) {
LOG.error("Cannot take screenshot", ex);
}
jtMsg.requestFocus();
});
}
jpLog.add(Box.createHorizontalStrut(38));
{
JButton jbClear = new JButton("Clear");
jpLog.add(jbClear);
jbClear.addActionListener(event -> {
jtaJson.setText("");
});
}
JPanel jpAction = new JPanel();
jpContent.add(jpAction, BorderLayout.PAGE_END);
jpAction.setLayout(new BoxLayout(jpAction, BoxLayout.LINE_AXIS));
jpJs.add(jpAction, BorderLayout.PAGE_END);
jd.pack();
jd.setVisible(true);
jd.setLocationRelativeTo(null);
jbLogUi.doClick();
});
while (visible.get()) {
if (System.currentTimeMillis() > end) {
LOG.error("Manual UI interaction timeout");
break;
}
Thread.sleep(500);
}
if (pass.get()) {
LOG.info("Manual UI Interaction returns PASS");
} else {
Assert.fail("Manual UI Interaction returns FAIL");
}
}
private TreeModel getModel(WindowHierarchy wh) {
DefaultMutableTreeNode rootNode = createNode(wh.root);
DefaultTreeModel treeModel = new DefaultTreeModel(rootNode);
return treeModel;
}
private DefaultMutableTreeNode createNode(UIANode uiNode) {
Rect bounds = uiNode.getBounds();
String s = uiNode.getKlass() + " " + uiNode.getContentDescription() + " "
+ String.format("[%d,%d][%d,%d]", bounds.left, bounds.top, bounds.right, bounds.bottom);
DefaultMutableTreeNode tNode = new DefaultMutableTreeNode(s);
tNode.setUserObject(uiNode);
for (UIANode n : uiNode.nodes()) {
tNode.add(createNode(n));
}
return tNode;
}
}