diff --git a/wlauto/workloads/youtube/__init__.py b/wlauto/workloads/youtube/__init__.py new file mode 100755 index 00000000..bef245df --- /dev/null +++ b/wlauto/workloads/youtube/__init__.py @@ -0,0 +1,90 @@ +# Copyright 2014-2016 ARM Limited +# +# 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. +# + +from wlauto import AndroidUxPerfWorkload, Parameter +from wlauto.exceptions import WorkloadError + + +class Youtube(AndroidUxPerfWorkload): + + name = 'youtube' + package = 'com.google.android.youtube' + min_apk_version = '11.19.56' + max_apk_version = None # works with latest (11.33.58) at time of publishing this + activity = '' + description = ''' + A workload to perform standard productivity tasks within YouTube. + + The workload plays a video from the app, determined by the ``video_source`` parameter. + While the video is playing, a some common actions are done such as video seeking, pausing + playback and navigating the comments section. + + Test description: + The ``video_source`` parameter determines where the video to be played will be found + in the app. Possible values are ``search``, ``home``, ``my_videos``, and ``trending``. + -A. search - Goes to the search view, does a search for the given term, and plays the + first video in the results. The parameter ``search_term`` must also be provided + in the agenda for this to work. This is the default mode. + -B. home - Scrolls down once on the app's home page to avoid ads (if present, would be + first video), then select and plays the video that appears at the top of the list. + -C. my_videos - Goes to the 'My Videos' section of the user's account page and plays a + video from there. The user must have at least one uploaded video for this to work. + -D. trending - Goes to the 'Trending Videos' section of the app, and plays the first + video in the trending videos list. + + For the selected video source, the following test steps are performed: + 1. Navigate to the general app settings page to disable autoplay. This improves test + stability and predictability by preventing screen transition to load a new video + while in the middle of the test. + 2. Select the video from the source specified above, and dismiss any potential embedded + advert that may pop-up before the actual video. + 3. Let the video play for a few seconds, pause it, then resume. + 4. Expand the info card that shows video metadata, then collapse it again. + 5. Scroll down to the end of related videos and comments under the info card, and then + back up to the start. A maximum of 5 swipe actions is performed in either direction. + ''' + + parameters = [ + Parameter('video_source', kind=str, default='search', + allowed_values=['home', 'my_videos', 'search', 'trending'], + description=''' + Determines where to play the video from. This can either be from the + YouTube home, my videos section, trending videos or found in search. + '''), + Parameter('search_term', kind=str, + default='Big Buck Bunny 60fps 4K - Official Blender Foundation Short Film', + description=''' + The search term to use when ``video_source`` is set to ``search``. Ignored otherwise. + '''), + ] + + view = [ + package + '/com.google.android.apps.youtube.app.WatchWhileActivity', + package + '/com.google.android.apps.youtube.app.honeycomb.SettingsActivity', + ] + + requires_network = True + + def __init__(self, device, **kwargs): + super(Youtube, self).__init__(device, **kwargs) + self.run_timeout = 300 + + def validate(self): + if self.video_source == 'search' and not self.search_term: + raise WorkloadError("Param 'search_term' must be specified when video source is 'search'") + + self.uiauto_params['package'] = self.package + self.uiauto_params['video_source'] = self.video_source + self.uiauto_params['search_term'] = self.search_term.replace(' ', '0space0') diff --git a/wlauto/workloads/youtube/com.arm.wlauto.uiauto.youtube.jar b/wlauto/workloads/youtube/com.arm.wlauto.uiauto.youtube.jar new file mode 100644 index 00000000..218135c5 Binary files /dev/null and b/wlauto/workloads/youtube/com.arm.wlauto.uiauto.youtube.jar differ diff --git a/wlauto/workloads/youtube/uiauto/build.sh b/wlauto/workloads/youtube/uiauto/build.sh new file mode 100755 index 00000000..c7d05cfc --- /dev/null +++ b/wlauto/workloads/youtube/uiauto/build.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# CD into build dir if possible - allows building from any directory +script_path='.' +if `readlink -f $0 &>/dev/null`; then + script_path=`readlink -f $0 2>/dev/null` +fi +script_dir=`dirname $script_path` +cd $script_dir + +# Ensure build.xml exists before starting +if [[ ! -f build.xml ]]; then + echo 'Ant build.xml file not found! Check that you are in the right directory.' + exit 9 +fi + +# Copy base classes from wlauto dist +class_dir=bin/classes/com/arm/wlauto/uiauto +base_classes=`python -c "import os, wlauto; print os.path.join(os.path.dirname(wlauto.__file__), 'common', 'android', '*.class')"` +mkdir -p $class_dir +cp $base_classes $class_dir + +# Build and return appropriate exit code if failed +ant build +exit_code=$? +if [[ $exit_code -ne 0 ]]; then + echo "ERROR: 'ant build' exited with code $exit_code" + exit $exit_code +fi + +# If successful move JAR file to workload folder (overwrite previous) +package=com.arm.wlauto.uiauto.youtube.jar +rm -f ../$package +if [[ -f bin/$package ]]; then + cp bin/$package .. +else + echo 'ERROR: UiAutomator JAR could not be found!' + exit 9 +fi diff --git a/wlauto/workloads/youtube/uiauto/build.xml b/wlauto/workloads/youtube/uiauto/build.xml new file mode 100644 index 00000000..b0e91680 --- /dev/null +++ b/wlauto/workloads/youtube/uiauto/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wlauto/workloads/youtube/uiauto/project.properties b/wlauto/workloads/youtube/uiauto/project.properties new file mode 100644 index 00000000..ce39f2d0 --- /dev/null +++ b/wlauto/workloads/youtube/uiauto/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-18 diff --git a/wlauto/workloads/youtube/uiauto/src/com/arm/wlauto/uiauto/UiAutomation.java b/wlauto/workloads/youtube/uiauto/src/com/arm/wlauto/uiauto/UiAutomation.java new file mode 100644 index 00000000..339bd0bf --- /dev/null +++ b/wlauto/workloads/youtube/uiauto/src/com/arm/wlauto/uiauto/UiAutomation.java @@ -0,0 +1,239 @@ +/* Copyright 2014-2016 ARM Limited + * + * 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.arm.wlauto.uiauto.youtube; + +import android.os.Bundle; +import android.os.SystemClock; +import android.util.Log; + +// Import the uiautomator libraries +import com.android.uiautomator.core.UiObject; +import com.android.uiautomator.core.UiScrollable; +import com.android.uiautomator.core.UiSelector; + +import com.arm.wlauto.uiauto.UxPerfUiAutomation; + +import static com.arm.wlauto.uiauto.BaseUiAutomation.FindByCriteria.BY_ID; +import static com.arm.wlauto.uiauto.BaseUiAutomation.FindByCriteria.BY_TEXT; +import static com.arm.wlauto.uiauto.BaseUiAutomation.FindByCriteria.BY_DESC; + +public class UiAutomation extends UxPerfUiAutomation { + + public static final String CLASS_BUTTON = "android.widget.Button"; + public static final String CLASS_TEXT_VIEW = "android.widget.TextView"; + + public static final int WAIT_TIMEOUT_1SEC = 1000; + public static final int WAIT_TIMEOUT_5SEC = 5000; + public static final int VIDEO_SLEEP_SECONDS = 3; + public static final int LIST_SWIPE_COUNT = 5; + public static final String SOURCE_MY_VIDEOS = "my_videos"; + public static final String SOURCE_SEARCH = "search"; + public static final String SOURCE_TRENDING = "trending"; + + protected ActionLogger logger; + protected Bundle parameters; + protected String packageName; + protected String packageID; + protected String searchTerm; + + public void runUiAutomation() throws Exception { + parameters = getParams(); + packageName = parameters.getString("package"); + packageID = packageName + ":id/"; + searchTerm = parameters.getString("search_term"); + if (searchTerm != null) { + searchTerm = searchTerm.replaceAll("0space0", " "); + } + + setScreenOrientation(ScreenOrientation.NATURAL); + clearFirstRunDialogues(); + disableAutoplay(); + testPlayVideo(parameters.getString("video_source"), searchTerm); + unsetScreenOrientation(); + } + + public void clearFirstRunDialogues() throws Exception { + UiObject laterButton = new UiObject(new UiSelector().textContains("Later").className(CLASS_TEXT_VIEW)); + if (laterButton.waitForExists(WAIT_TIMEOUT_1SEC)) { + laterButton.click(); + } + UiObject cancelButton = new UiObject(new UiSelector().textContains("Cancel").className(CLASS_BUTTON)); + if (cancelButton.waitForExists(WAIT_TIMEOUT_1SEC)) { + cancelButton.click(); + } + UiObject skipButton = new UiObject(new UiSelector().textContains("Skip").className(CLASS_TEXT_VIEW)); + if (skipButton.waitForExists(WAIT_TIMEOUT_1SEC)) { + skipButton.click(); + } + UiObject gotItButton = new UiObject(new UiSelector().textContains("Got it").className(CLASS_BUTTON)); + if (gotItButton.waitForExists(WAIT_TIMEOUT_1SEC)) { + gotItButton.click(); + } + } + + public void disableAutoplay() throws Exception { + clickUiObject(BY_DESC, "More options"); + startMeasurements("goto_settings"); + clickUiObject(BY_TEXT, "Settings", true); + endMeasurements("goto_settings"); + startMeasurements("goto_settings_general"); + clickUiObject(BY_TEXT, "General", true); + endMeasurements("goto_settings_general"); + + // Don't fail fatally if autoplay toggle cannot be found + UiObject autoplayToggle = new UiObject(new UiSelector().textContains("Autoplay")); + if (autoplayToggle.waitForExists(WAIT_TIMEOUT_1SEC)) { + autoplayToggle.click(); + } + getUiDevice().pressBack(); + + // Tablet devices use a split with General in the left pane and Autoplay in the right so no + // need to click back twice + UiObject generalButton = new UiObject(new UiSelector().textContains("General").className(CLASS_TEXT_VIEW)); + if (generalButton.exists()) { + getUiDevice().pressBack(); + } + } + + public void testPlayVideo(String source, String searchTerm) throws Exception { + if (SOURCE_MY_VIDEOS.equalsIgnoreCase(source)) { + startMeasurements("goto_account"); + clickUiObject(BY_DESC, "Account"); + endMeasurements("goto_account"); + startMeasurements("goto_my_videos"); + clickUiObject(BY_TEXT, "My Videos", true); + endMeasurements("goto_my_videos"); + + startMeasurements("play_from_my_videos"); + clickUiObject(BY_ID, packageID + "thumbnail", true); + endMeasurements("play_from_my_videos"); + + } else if (SOURCE_SEARCH.equalsIgnoreCase(source)) { + startMeasurements("goto_search"); + clickUiObject(BY_DESC, "Search"); + endMeasurements("goto_search"); + + startMeasurements("search_video"); + UiObject textField = getUiObjectByResourceId(packageID + "search_edit_text"); + textField.setText(searchTerm); + endMeasurements("search_video"); + + getUiDevice().pressEnter(); + startMeasurements("play_from_search"); + // If a video exists whose title contains the exact search term, then play it + // Otherwise click the first video in the search results + UiObject thumbnail = new UiObject(new UiSelector().resourceId(packageID + "thumbnail")); + UiObject matchedVideo = thumbnail.getFromParent(new UiSelector().textContains(searchTerm)); + if (matchedVideo.exists()) { + matchedVideo.clickAndWaitForNewWindow(); + } else { + thumbnail.clickAndWaitForNewWindow(); + } + endMeasurements("play_from_search"); + + } else if (SOURCE_TRENDING.equalsIgnoreCase(source)) { + startMeasurements("goto_trending"); + clickUiObject(BY_DESC, "Trending"); + endMeasurements("goto_trending"); + + startMeasurements("play_from_trending"); + clickUiObject(BY_ID, packageID + "thumbnail", true); + endMeasurements("play_from_trending"); + + } else { // homepage videos + UiScrollable list = new UiScrollable(new UiSelector().resourceId(packageID + "results")); + if (list.exists()) { + list.scrollForward(); + } + startMeasurements("play_from_home"); + clickUiObject(BY_ID, packageID + "thumbnail", true); + endMeasurements("play_from_home"); + } + + dismissAdvert(); + checkPlayerError(); + pausePlayVideo(); + checkVideoInfo(); + scrollRelated(); + } + + public void dismissAdvert() throws Exception { + UiObject advert = new UiObject(new UiSelector().textContains("Visit advertiser")); + if (advert.exists()) { + UiObject skip = new UiObject(new UiSelector().textContains("Skip ad")); + if (skip.waitForExists(WAIT_TIMEOUT_5SEC)) { + skip.click(); + sleep(VIDEO_SLEEP_SECONDS); + } + } + } + + public void checkPlayerError() throws Exception { + UiObject playerError = new UiObject(new UiSelector().resourceId(packageID + "player_error_view")); + UiObject tapToRetry = new UiObject(new UiSelector().textContains("Tap to retry")); + if (playerError.waitForExists(WAIT_TIMEOUT_1SEC) || tapToRetry.waitForExists(WAIT_TIMEOUT_1SEC)) { + throw new RuntimeException("Video player encountered an error and cannot continue."); + } + } + + public void pausePlayVideo() throws Exception { + UiObject player = getUiObjectByResourceId(packageID + "player_fragment_container"); + sleep(VIDEO_SLEEP_SECONDS); + repeatClickUiObject(player, 2, 100); + sleep(1); // pause the video momentarily + player.click(); + startMeasurements("player_video_windowed"); + sleep(VIDEO_SLEEP_SECONDS); + endMeasurements("player_video_windowed"); + } + + public void checkVideoInfo() throws Exception { + UiObject expandButton = new UiObject(new UiSelector().resourceId(packageID + "expand_button")); + if (!expandButton.waitForExists(WAIT_TIMEOUT_1SEC)) { + return; + } + // Expand video info + expandButton.click(); + SystemClock.sleep(500); // short delay to simulate user action + expandButton.click(); + } + + public void scrollRelated() throws Exception { + // ListView of related videos and (maybe) comments + UiScrollable list = new UiScrollable(new UiSelector().resourceId(packageID + "watch_list")); + if (list.isScrollable()) { + startMeasurements("watch_list_fling_down"); + list.flingToEnd(LIST_SWIPE_COUNT); + endMeasurements("watch_list_fling_down"); + + startMeasurements("watch_list_fling_up"); + list.flingToBeginning(LIST_SWIPE_COUNT); + endMeasurements("watch_list_fling_up"); + } + // After flinging, give the window enough time to settle down before + // the next step, or else UiAutomator fails to find views in time + sleep(VIDEO_SLEEP_SECONDS); + } + + protected void startMeasurements(String testTag) throws Exception { + logger = new ActionLogger(testTag, parameters); + logger.start(); + } + + protected void endMeasurements(String testTag) throws Exception { + logger.stop(); + } +}