/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.tests; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.gecko.Actions; import org.mozilla.gecko.Assert; import org.mozilla.gecko.FennecMochitestAssert; public abstract class SessionTest extends BaseTest { protected Navigation mNavigation; @Override public void setUp() throws Exception { super.setUp(); mNavigation = new Navigation(mDevice); } /** * A generic session object representing a collection of items that has a * selected index. */ protected abstract class SessionObject { private final int mIndex; private final T[] mItems; public SessionObject(int index, T... items) { mIndex = index; mItems = items; } public int getIndex() { return mIndex; } public T[] getItems() { return mItems; } } protected class PageInfo { private final String url; private final String title; public PageInfo(String key) { if (key.startsWith("about:")) { url = key; } else { url = getPage(key); } title = key; } } protected class SessionTab extends SessionObject { public SessionTab(int index, PageInfo... items) { super(index, items); } } protected class Session extends SessionObject { public Session(int index, SessionTab... items) { super(index, items); } } /** * Walker for visiting items in a browser-like navigation order. */ protected abstract class NavigationWalker { private final T[] mItems; private final int mIndex; public NavigationWalker(SessionObject obj) { mItems = obj.getItems(); mIndex = obj.getIndex(); } /** * Walks over the list of items, calling the onItem() callback for each. * * The selected item is the first item visited. Each item after the * selected item is then visited in ascending index order. Finally, the * list is iterated in reverse, and each item before the selected item * is visited in descending index order. */ public void walk() { onItem(mItems[mIndex], mIndex); for (int i = mIndex + 1; i < mItems.length; i++) { goForward(); onItem(mItems[i], i); } if (mIndex > 0) { for (int i = mItems.length - 2; i >= 0; i--) { goBack(); if (i < mIndex) { onItem(mItems[i], i); } } } } /** * Callback when an item is visited during a walk. * * Only one callback is executed per item. */ public abstract void onItem(T item, int currentIndex); /** * Callback executed for each back step of the walk. */ public void goBack() {} /** * Callback executed for each forward step of the walk. */ public void goForward() {} } /** * Loads a set of tabs in the browser specified by the given session. * * @param session Session to load */ protected void loadSessionTabs(Session session) { // Verify initial about:home tab verifyTabCount(1); verifyUrl(mStringHelper.ABOUT_HOME_URL); SessionTab[] tabs = session.getItems(); for (int i = 0; i < tabs.length; i++) { final SessionTab tab = tabs[i]; final PageInfo[] pages = tab.getItems(); // New tabs always start with about:home, so make sure about:home // is always the first entry. mAsserter.is(pages[0].url, mStringHelper.ABOUT_HOME_URL, "first page in tab is " + mStringHelper.ABOUT_HOME_URL); // If this is the first tab, the tab already exists, so no need to // create a new one. Otherwise, create a new tab if we're loading // the first the first page in the set. if (i > 0) { addTab(); } for (int j = 1; j < pages.length; j++) { Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow"); loadUrl(pages[j].url); pageShowExpecter.blockForEvent(); pageShowExpecter.unregisterListener(); } final int index = tab.getIndex(); for (int j = pages.length - 1; j > index; j--) { mNavigation.back(); } } selectTabAt(session.getIndex()); } /** * Verifies that the set of open tabs matches the given session. * * @param session Session to verify */ protected void verifySessionTabs(Session session) { verifyTabCount(session.getItems().length); (new NavigationWalker(session) { boolean mFirstTabVisited; @Override public void onItem(SessionTab tab, int currentIndex) { // The first tab to check should already be selected at startup if (mFirstTabVisited) { selectTabAt(currentIndex); } else { mFirstTabVisited = true; } (new NavigationWalker(tab) { @Override public void onItem(PageInfo page, int currentIndex) { final String text; if (mStringHelper.ABOUT_HOME_URL.equals(page.url)) { text = mStringHelper.TITLE_PLACE_HOLDER; } else if (page.url.startsWith(URL_HTTP_PREFIX)) { text = page.url.substring(URL_HTTP_PREFIX.length()); } else { text = page.url; } waitForText(text); verifyUrlBarTitle(page.url); } @Override public void goBack() { mNavigation.back(); } @Override public void goForward() { mNavigation.forward(); } }).walk(); } }).walk(); } /** * Gets session restore JSON corresponding to the open session. * * The JSON format follows the format used in Gecko for session restore and * should be interchangeable with the Gecko's generated sessionstore.js. * * @param session Session to serialize * @return JSON string of session */ protected String buildSessionJSON(Session session) { final SessionTab[] sessionTabs = session.getItems(); String sessionString = null; try { final JSONArray tabs = new JSONArray(); for (int i = 0; i < sessionTabs.length; i++) { final JSONObject tab = new JSONObject(); final JSONArray entries = new JSONArray(); final SessionTab sessionTab = sessionTabs[i]; final PageInfo[] pages = sessionTab.getItems(); for (int j = 0; j < pages.length; j++) { final PageInfo page = pages[j]; final JSONObject entry = new JSONObject(); entry.put("url", page.url); entry.put("title", page.title); entries.put(entry); } tab.put("entries", entries); tab.put("index", sessionTab.getIndex() + 1); tabs.put(tab); } JSONObject window = new JSONObject(); window.put("tabs", tabs); window.put("selected", session.getIndex() + 1); sessionString = new JSONObject().put("windows", new JSONArray().put(window)).toString(); } catch (JSONException e) { mAsserter.ok(false, "JSON exception", getStackTraceString(e)); } return sessionString; } /** * @see SessionTest#verifySessionJSON(Session, String, Assert) */ protected void verifySessionJSON(Session session, String sessionString) { verifySessionJSON(session, sessionString, mAsserter); } /** * Verifies a session JSON string against the given session. * * @param session Session to verify against * @param sessionString JSON string to verify * @param asserter Assert class to use during verification */ protected void verifySessionJSON(Session session, String sessionString, Assert asserter) { final SessionTab[] sessionTabs = session.getItems(); try { final JSONObject window = new JSONObject(sessionString).getJSONArray("windows").getJSONObject(0); final JSONArray tabs = window.getJSONArray("tabs"); final int optSelected = window.optInt("selected", -1); asserter.is(optSelected, session.getIndex() + 1, "selected tab matches"); for (int i = 0; i < tabs.length(); i++) { final JSONObject tab = tabs.getJSONObject(i); final int index = tab.getInt("index"); final JSONArray entries = tab.getJSONArray("entries"); final SessionTab sessionTab = sessionTabs[i]; final PageInfo[] pages = sessionTab.getItems(); asserter.is(index, sessionTab.getIndex() + 1, "selected page index matches"); for (int j = 0; j < entries.length(); j++) { final JSONObject entry = entries.getJSONObject(j); final String url = entry.getString("url"); final String title = entry.optString("title"); final PageInfo page = pages[j]; asserter.is(url, page.url, "URL in JSON matches session URL"); if (!page.url.startsWith("about:")) { asserter.is(title, page.title, "title in JSON matches session title"); } } } } catch (JSONException e) { asserter.ok(false, "JSON exception", getStackTraceString(e)); } } /** * Exception thrown by NonFatalAsserter for assertion failures. */ public static class AssertException extends RuntimeException { public AssertException(String msg) { super(msg); } } /** * Asserter that throws an AssertException on failure instead of aborting * the test. * * This can be used in methods called via waitForCondition() where an assertion * might not immediately succeed. */ public class NonFatalAsserter extends FennecMochitestAssert { @Override public void ok(boolean condition, String name, String diag) { if (!condition) { String details = (diag == null ? "" : " | " + diag); throw new AssertException("Assertion failed: " + name + details); } mAsserter.ok(condition, name, diag); } } /** * Gets a URL for a dynamically-generated page. * * The page will have a URL unique to the given ID, and the page's title * will match the given ID. * * @param id ID used to generate page URL * @return URL of the page */ protected String getPage(String id) { return getAbsoluteUrl("/robocop/robocop_dynamic.sjs?id=" + id); } protected String readProfileFile(String filename) { try { return readFile(new File(mProfile, filename)); } catch (IOException e) { mAsserter.ok(false, "Error reading" + filename, getStackTraceString(e)); } return null; } protected void writeProfileFile(String filename, String data) { try { writeFile(new File(mProfile, filename), data); } catch (IOException e) { mAsserter.ok(false, "Error writing to " + filename, getStackTraceString(e)); } } private String readFile(File target) throws IOException { if (!target.exists()) { return null; } FileReader fr = new FileReader(target); try { StringBuffer sb = new StringBuffer(); char[] buf = new char[8192]; int read = fr.read(buf); while (read >= 0) { sb.append(buf, 0, read); read = fr.read(buf); } return sb.toString(); } finally { fr.close(); } } private void writeFile(File target, String data) throws IOException { FileWriter writer = new FileWriter(target); try { writer.write(data); } finally { writer.close(); } } }