package gstests.tutorials;

import org.openqa.selenium.WebDriver;

// Junit imports
import org.junit.Assert;

// assertj swing for OS specific tests
import org.assertj.swing.util.OSFamily;
import org.assertj.swing.util.Platform;

// Selenium
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.interactions.Actions; // for hover-then-click on navigation submenus
import org.openqa.selenium.support.ui.Select; // <select> drop down elements
     // see https://stackoverflow.com/questions/14936825/getting-the-currently-selected-option-as-text-string-for-a-drop-down-in-selenium
import org.openqa.selenium.support.ui.Wait;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;

import org.greenstone.gsdl3.testing.GSBasicTestingUtil;
import static org.greenstone.gsdl3.testing.GSBasicTestingUtil.ENCODING;
import org.greenstone.gsdl3.testing.GSSeleniumUtil;
import org.greenstone.gsdl3.testing.GSSeleniumUtil.DocDisplayStruct;
// static import to use methods and constants without class prefix
import static org.greenstone.gsdl3.testing.GSSeleniumUtil.*;
//import org.greenstone.gsdl3.testing.GSGUITestingUtil; // TODO: import only for now
// static imports - call static functions without class as namespace qualifier
import static org.greenstone.gsdl3.testing.GSGUITestingUtil.PAUSE;
import static org.greenstone.gsdl3.testing.GSGUITestingUtil.OPTIONAL;
import static org.greenstone.gsdl3.testing.GSGUITestingUtil.COMPULSORY;
import static org.greenstone.gsdl3.testing.GSGUITestingUtil.MAX_BUILD_TIMEOUT;
import static org.greenstone.gsdl3.testing.GSGUITestingUtil.ASSERT_FAIL_ON_NO_MATCH;
import static org.greenstone.gsdl3.testing.GSGUITestingUtil.DO_NOT_BAIL_ON_NO_MATCH;

// Java imports
import java.awt.event.InputEvent;
import java.io.*;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.AbstractMap.SimpleEntry;
import java.util.regex.Pattern;
import javax.swing.*;

import org.apache.commons.io.FileUtils;


public class BrowserTest {
    private static final String adminUser = "admin";
    private static String adminPwd = "admin"; // can be customised in GLItest.properties, and will be programmatically replaced with another pwd
    private static final String newAdminPwd = "greenstone";
    static final String custom_username = "me";
    static final String custom_userpwd = "me!";

    // Modes in document editing of a metadata field
    private static final int PREPEND = 1;
    private static final int APPEND = 2;
    private static final int REPLACE = 3;
    
    public static final String mainPageXPath
	= "//div[@id='breadcrumbs']/a[contains(@href, 'library')]";
    
    private static WebDriver _driver = null;
    
    public static void setDriver(WebDriver driver) {
	_driver = driver;
    }

	// Call this to change the initial admin pwd early on to whatever pwd was used during GS3 install
	// As obtained from GLItest.properties
	public static void initAdminPwd(String propertyValue) {
		if(propertyValue != null) {
			adminPwd = propertyValue;
			System.err.println("@@@ Updated admin authentication details");
		} else {
			System.err.println("@@@ Retaining default admin authentication details");
		}
	}

    // https://github.com/SeleniumHQ/selenium/blob/selenium-4.2.0/java/src/org/openqa/selenium/Keys.java#L28
    public static Keys controlOrCommandKey() {
	if(Platform.osFamily() == OSFamily.MAC) {
	    return Keys.COMMAND;
	}
	else {
	    return Keys.CONTROL;
	}
    }

    public static String toLineStart() {
	if(Platform.osFamily() == OSFamily.MAC) {
	    return Keys.chord(Keys.COMMAND, Keys.UP);
	} else {
	    return Keys.chord(Keys.CONTROL, Keys.HOME);
	}
    }

    public static String toLineEnd() {
	if(Platform.osFamily() == OSFamily.MAC) {
	    return Keys.chord(Keys.COMMAND, Keys.DOWN);
	} else {
	    return Keys.chord(Keys.CONTROL, Keys.END);
	}
    }

    
    /*
    // Unfortunately, sometimes a pause using this function goes by lightning fast,
    // not at all pausing for the duration in seconds supplied.
    private static void PAUSE(double seconds) { // Pause(10.5), Pause(0.5), Pause(1) are all fine
	// https://stackoverflow.com/questions/73085999/selenium-webdriverwaitdriver-duration-ofseconds10-launches-java-lang-nosuch
	// Forced to use deprecated version because of version of selenium we have
	// https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/WebDriver.Timeouts.html#implicitlyWait(long,java.util.concurrent.TimeUnit)
	
	_driver.manage().timeouts().implicitlyWait((long)(seconds*1000), TimeUnit.MILLISECONDS);//Duration.ofMillis(100));
	
	}*/


    // Safari has the jsessionid in its URLs, unlike firefox
    public static String removeJSessionID(String url) {
	int startIndex = url.indexOf(";jsessionid=");
	if(startIndex != -1) {
	    // scrub out the ?jsessionid=... part, but capture any section starting ? in the URL that may follow session id
	    int endIndex = url.indexOf("?", startIndex+";jsessionid=".length());
	    if (endIndex != -1) {
		url = url.substring(0, startIndex) + url.substring(endIndex); 
	    } else {
		url = url.substring(0, startIndex);
	    }
	}
	return url;
    }
    
    public static void spotTest(int nthPreview) {
	System.err.println("@@@ BrowserTesting spotTest() - preview test case #: " + nthPreview);

	int numDocs;
	String xPath, errMsg, expected, actual, bookshelfTitle;
	WebElement el;
	String currURL, urlSuffix;
	
	switch(nthPreview) {
	case 1: case 2:
	    System.err.println("@@@ Spot testing lucene-jdbm-demo");
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(11);
	    // browse
	    if(nthPreview == 1) {
		loadClassifierByName(_driver, "Title");
		clickHorizontalClassifier("F"); // will be converted to lowercase internally
		clickDocLink_docTitleContains("Farming snails 2");
		
		loadClassifierByName(_driver, "Subject");
		expandBookshelf(2, "CL2");		
		//expandAllBookshelves("CL2.2.1");
		expandBookshelf(1, "CL2.2"); // Animal Husbandry and Animal Product Processing > Cattle
		clickDocLink_docTitleContains("The Water Buffalo");
		expandDocSectionTitles("Milk"); 
	    } else {
		loadClassifierByName(_driver, "Howto");
		bookshelfTitle = "start a butterfly farm"; // 4.5
		//expandBookshelves(bookshelfTitle);
		// //table//td//a[contains(text(),'start a butterfly farm')]
		xPath = "//table//td//a[contains(text(),'" + bookshelfTitle + "')]";
		clickLink(xPath);
		
		expandDocSectionTitles("2 Butterfly Status and Conservation");
	    }
	    // Search terms are common: cat, then dog
	    searchAndCheckSummaryResults("cat", "all fields", null, 5, 3, "sections");
	    expected = "25 Other Rodents";
	    clickSearchResultDocLink_docTitleContains(expected);
	    currURL = removeJSessionID(_driver.getCurrentUrl());
	    urlSuffix = "lucene-jdbm-demo/document/b17mie.7.11?p.s=TextQuery";
	    ////urlSuffix = ".*lucene-jdbm-demo\\/document\\/b17mie.7.11(;jsessionid=[^\\?]+)?\\?p.s=TextQuery$";
	    //urlSuffix = ".*lucene-jdbm-demo/document/b17mie.7.11(;jsessionid=[^\\?]+)?\\?p.s=TextQuery$";
	    errMsg = "### ASSERT ERROR: section " + expected + " is not at url suffix regex "+urlSuffix;
	    //Assert.assertTrue(errMsg, currURL.matches(urlSuffix));
	    Assert.assertTrue(errMsg, currURL.endsWith(urlSuffix));
	    
	    searchAndCheckSummaryResults("dog", "all fields", null, 12, 12, "sections");
	    expected = "18 Giant Rat";
	    clickSearchResultDocLink_docTitleContains(expected);
	    currURL = removeJSessionID(_driver.getCurrentUrl());
	    urlSuffix = "lucene-jdbm-demo/document/b17mie.7.4?p.s=TextQuery";
	    errMsg = "### ASSERT ERROR: section " + expected + " is not at url suffix "+urlSuffix;
	    Assert.assertTrue(errMsg, currURL.endsWith(urlSuffix));
	    break;
	case 3: case 4:
	    System.err.println("@@@ Spot testing solr-jdbm-demo");
	    GSSeleniumUtil.checkNumDocs(11, "Title", "CL1");

	    // browse
	    if(nthPreview == 3) {

		//clickHorizontalClassifier("F"); // will be converted to lowercase internally
		clickDocLink_docTitleContains("Farming snails 2");
		
		expandDocPage("3.1"); // expand section 3.1 of the doc

		xPath = "//td[@id='headerfb34fe.3']";
		el = getElement(xPath);
		//expected = "\\s*\\d?\\s*Taking care of your snails\\s*";
		expected = "Taking care of your snails";
		// On Safari, text() will includes text of hidden elements unlike with other browsers.
		// https://github.com/SeleniumHQ/selenium/issues/10735
		// "Text method is supposed to render what the user sees on the page. This is a known safari bug."
		actual = el.getText();
		errMsg = String.format("### ASSERT ERROR: Expected section title %s, got %s",
				      expected, actual);
		//Assert.assertTrue(errMsg, actual.matches(expected)); //Assert.assertEquals(errMsg, expected, actual));
		Assert.assertTrue(errMsg, stringContainsText(actual, LITERAL, expected));
		System.err.println("@@@ Section 1.3 title was " + expected + " as expected.");
	    } else {
		loadClassifierByName(_driver, "Organization");
		bookshelfTitle = "EC Courier"; // 3.2
		expandBookshelves(bookshelfTitle);
		checkNumDocsOfBookshelf(3, bookshelfTitle);
		// click 2nd doc under CL3.2 (bookshelf titled EC Courier)
		clickDocLink("CL3.2", 2, POS_DEFAULT_BROWSE_DOC_ICON);
		// On the opened page, open up subsection Dossier > Investing in people
		expandDocSectionTitles("Close-up", "Growing rice");
	    }

	    // https://stackoverflow.com/questions/36647785/scroll-up-the-page-to-the-top-in-selenium
	    //(new Actions(_driver)).sendKeys(Keys.HOME).build().perform();
	    sendKeysToBrowser(Keys.HOME);


	    // TODO: ElementNotInteractableException safari failed to click
	    //https://stackoverflow.com/questions/56261168/why-cant-safaridriver-click-on-a-specific-element-when-chrome-ff-edge-can
	    
	    // Search is common	    
	    String docMatchStr = "%d sections match the query.";
	    String queryMatchStr = "'%s' occurs %d times";

	    //searchAndCheckSummaryResults("cat", "all fields", null, 12, 16, null);
	    GSSeleniumUtil.performQuickSearch(_driver, "cat", "all fields");
	    checkSearchResults(12,
			       String.format(docMatchStr, 12),
			       String.format(queryMatchStr, "cat", 16),
			       null,
			       null);
	    expected = "26 Mouse Deer";
	    clickSearchResultDocLink_docTitleContains(expected);
	    currURL = removeJSessionID(_driver.getCurrentUrl());
	    urlSuffix = "solr-jdbm-demo/document/b17mie.8.1?p.s=TextQuery";	    
	    errMsg = "### ASSERT ERROR: section " + expected + " is not at url suffix "+urlSuffix;
	    Assert.assertTrue(errMsg, currURL.endsWith(urlSuffix));
	    
	    //searchAndCheckSummaryResults("dog", "all fields", null, 24, 36, null);
	    GSSeleniumUtil.performQuickSearch(_driver, "dog", "all fields");
	    checkSearchResults(20,
			       String.format(docMatchStr, 24),
			       String.format(queryMatchStr, "dog", 36),
			       null,
			       null);
	    expected = "34 Black Iguana";
	    clickSearchResultDocLink_docTitleContains(expected);
	    currURL = removeJSessionID(_driver.getCurrentUrl());
	    urlSuffix = "solr-jdbm-demo/document/b17mie.9.2?p.s=TextQuery";
	    errMsg = "### ASSERT ERROR: section " + expected + " is not at url suffix "+urlSuffix;
	    Assert.assertTrue(errMsg, currURL.endsWith(urlSuffix));

	    //searchAndCheckSummaryResults("horse", "all fields", null, 9, 18, null);
	    GSSeleniumUtil.performQuickSearch(_driver, "horse", "all fields");
	    checkSearchResults(9,
			       String.format(docMatchStr, 9),
			       String.format(queryMatchStr, "horse", 18),
			       null,
			       null);
	    expected = "9 Management";
	    clickSearchResultDocLink_docTitleContains(expected);
	    currURL = removeJSessionID(_driver.getCurrentUrl());
	    urlSuffix = "solr-jdbm-demo/document/b21wae.11?p.s=TextQuery";
	    errMsg = "### ASSERT ERROR: section " + expected + " is not at url suffix "+urlSuffix;
	    Assert.assertTrue(errMsg, currURL.endsWith(urlSuffix));

	    break;
	case 5:
	    System.err.println("@@@ Spot testing created test collection");

	    GSSeleniumUtil.checkNumDocs(3, "Title", "CL1");
	    GSSeleniumUtil.checkNumDocs(3, "Source", "CL2");
	    //clickDocLink_anchorContains("CL2", 4, "LOCAL.txt");
	    clickDocLink_docTitleContains("For developers:"); // LOCAL.txt
	    
	    //el = getElement("//div[@id='gs_content']");
	    //expected = "Key things to do:";
	    el = getElement("//div[@id='gs-document-text']");
	    expected = "For developers:";
	    errMsg = "### ASSERT ERROR: Expected page to contain " + expected;
	    Assert.assertTrue(errMsg, el.getText().contains(expected));
	    System.err.println("@@@ LOCAL.txt contained text " + expected + ", as expected.");


	    searchAndCheckSummaryResults("greenstone", "text", null, 33, 3); //30, 2);
	    clickSearchResultDocLink_docTitleContains("Greenstone 3");
	    el = getElement("//div[@id='gs-document-text']"); // refresh for new page
	    expected = "GSDL3";
	    errMsg = "### ASSERT ERROR: Expected page to contain " + expected;
	    Assert.assertTrue(errMsg, el.getText().contains(expected));
	    System.err.println("@@@ LOCAL.txt contained text " + expected + ", as expected.");
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for backdrop");
	    break;
	}
	
    }

    public static void depositorTest(final String COLL) {
	System.err.println("@@@ BrowserTesting depositorTest() - not a preview, pure selenium test");

	// Log in and go to the depositor
	BrowserTest.logInAdmin();
	_driver.get(RunGLITest.getGSBaseURL()+"library");
	clickLinkWithAnchor("Depositor");
	
	// choose collection
	selectInDropDown("//select[@id='colSelect']", COLL);
	
	// START block depositing two files
	
	// Prepare function args for remaining pages of depositor wizard
	// for the first file (a readme file in gli) to deposit
	// COLL already contains README.txt, LOCAL.txt, LICENSE.txt
	// So we ensure we deposit simple files that have *different* filenames
	String filename1 = "READMEfr.txt";
	Collection metaList = new ArrayList(4);
	SimpleEntry<String,String> titleMeta1=new SimpleEntry("dc.Title", "All about Max the Sheepdog");
	metaList.add(titleMeta1);
	metaList.add(new SimpleEntry("dc.Creator", "Sheepdog Max"));
	metaList.add(new SimpleEntry("dc.Description", "Autobiography, life and trials of a sheepdog"));
	metaList.add(new SimpleEntry("dc.Language", "fr")); // test extra meta first
	// as clearing previous meta will test this field was cleared too
	goThroughDepositorWizard(metaList, filename1, false); // no old meta to clear

	// Prepare second gli readme doc to deposit
	String filename2 = "READMEen.txt";
	metaList.clear();
	SimpleEntry<String,String> titleMeta2 = new SimpleEntry("dc.Title", "My fluffy cat friend");
	metaList.add(titleMeta2);
	metaList.add(new SimpleEntry("dc.Creator", "Sheepdog Max"));
	metaList.add(new SimpleEntry("dc.Description", "Biography of best friend"));
	goThroughDepositorWizard(metaList, filename2, true); // clear old meta first

	// END block depositing files
	
	// Now can click the final link (not the same as View Collection link)
	clickLinkWithAnchor("View your collection");

	// Check both docs deposited
	searchAndCheckNumResults(filename1, "filenames", 1);
	searchAndCheckNumResults(titleMeta1.getValue(), "titles", 1);

	searchAndCheckNumResults(filename2, "filenames", 1);
	searchAndCheckNumResults(titleMeta2.getValue(), "titles", 1);

	// The first file should have mention of dc.Language in its metadata.xml
	// The metadata.xml is always in a *folder* with same name as *deposited file*
	String fileFormatStr = System.getenv("GSDL3SRCHOME")
	    +"/web/sites/localsite/collect/"+COLL+"/import/%s/metadata.xml";
	GSBasicTestingUtil.matchText(new File(String.format(fileFormatStr, filename1)),
				     "dc.Language", 1, // first occurrence
				     ASSERT_FAIL_ON_NO_MATCH);

	// The first file *shouldn't* mention dc.Language in its metadata.xml
	if(GSBasicTestingUtil.matchText(
		new File(String.format(fileFormatStr, filename2)),
		"dc.Language", 1, // first occurrence
		DO_NOT_BAIL_ON_NO_MATCH)) {
	    Assert.fail("### ERROR: file2 "+filename2+" added through the depositor "
	       +"to col "+COLL+" contained dc.Language metadata that it shouldn't");
	}
	
    }

    public static void goThroughDepositorWizard(Collection metaList, String filename,
						boolean clearPrevious)
    {	
	// Page 1 of depositor wizard
	clickLinkWithAnchor("Specify Metadata");
	PAUSE(1);
	if(clearPrevious) {
	    getElement("//span[@id='clearSaved']").click();
		waitForPageLoad();
		PAUSE(1);
	    //and text()='Clear all saved data'

	    // Check they're cleared
	    String MD = "md___";
	    String[] fields={"dc.Title", "dc.Creator", "dc.Description"};
	    for(String field : fields) {
		// can be an input or textarea element with expected name attribute
		WebElement input = getElement( //"//input[@name='md___"+field+"']");
		      "//*[self::input or self::textarea][@name='md___"+field+"']");
		    Assert.assertTrue("### ERROR: Field not empty after clearing"
				      + field,
				      input.getText().trim().equals(""));
		    // Need to trim as dc.Description textarea has a space in it, see
		    // de_page1.xsl, perhaps to allow non-self-closing <xsl:text> elem?
	    }
	    
	    // dc.Language is custom, won't exist unless it was added
	    // but if Clear all saved data was pressed, the field would be gone
	    // So check it definitely doesn't exist now, whether ever added or not
	    elementShouldNotExist("//input[@name='md___dc.Language']");
	}
	fillInDepositorMeta(metaList);
	
	// page 2 - filechooser
	clickLinkWithAnchor("Select File");	
	File depositFile = new File(System.getenv("GSDL3SRCHOME")+"/gli/"+filename);
	WebElement fileInput = getElement("//input[@type='file']");	
	// https://www.selenium.dev/documentation/webdriver/elements/file_upload/
	// "Because Selenium cannot interact with the file upload dialog, it
	// provides a way to upload files without opening the dialog. If the
	// element is an input element with type file, you can use the send
	// keys method to send the full path to the file that will be uploaded.
	fileInput.sendKeys(depositFile.getAbsolutePath());
	
	// Check assigned metadata is displayed on this page before proceeding
	String formXpath = "//form[@id='depositorform']";

	Iterator<Map.Entry<String, String>> i = metaList.iterator();
	while(i.hasNext()) {
	    Map.Entry<String, String> entry = i.next();
	    String metaname = entry.getKey();
	    String metaval = entry.getValue();
	    getElement(formXpath+"//td[contains(text(), '"+metaname+":')]");

	    // User entered data on prev wizard page shows up for the browser viewer
	    // in input boxes in current page, but not viewable in browser's
	    // HTML inspector dev tool. These are 'live' elements and for <input>s,
	    // need to do a getAttribute("value") on them to get their 'live' value.
	    // https://stackoverflow.com/questions/7852287/using-selenium-webdriver-to-retrieve-the-value-of-an-html-input
	    WebElement liveElem = getElement(formXpath
			     +"//td/input[@name='md___"+metaname+"']");
	    Assert.assertTrue("### ERROR: depositorform missing display text "+metaval,
			      liveElem.getAttribute("value").contains(metaval));
	    System.err.println("Page displayed meta info "+metaname+": "+metaval);
	}
	
	// page 3 - confirmation
	clickLinkWithAnchor("Confirmation");
	WebElement form = getElement("//form[@id='depositorform']");
	String actualText = form.getText();
	// expected text are on different lines
	Assert.assertTrue(
	  "### ERROR: depositorform didn't contain display text 'Filename:' or '"
	  +filename+"' or 'File size:'",
	  actualText.contains("Filename:") && actualText.contains(filename)
	  && actualText.contains("File size:"));
	System.err.println("Page displayed Filename and File size information");


	// page 4 - building
	clickLinkWithAnchor("Deposit Item");
	new WebDriverWait(_driver, 2*TIMEOUT_SHORT).until(
	  ExpectedConditions.textToBePresentInElementLocated(
		     By.xpath("//div[@id='progressBar']"), "Done!"));
	PAUSE(1); // Allow human to view the result
	
	// page 5 is view collection
	clickLinkWithAnchor("View Collection");
	// clickLinkWithAnchor("View your collection"); // later
    }
    
    public static void fillInDepositorMeta(Collection<Map.Entry<String, String>> metaList) {
	//String inputXpath = "//input[@name='md___%s']";
	// We can match textarea or input element
	// https://stackoverflow.com/questions/37949212/how-to-match-either-one-element-or-another-with-xpath
	String inputXpath = "//*[self::input or self::textarea][@name='md___%s']";
	Iterator<Map.Entry<String, String>> i = metaList.iterator();
	while(i.hasNext()) {
	    Map.Entry<String, String> entry = i.next();
	    String metaname = entry.getKey();
	    String metaval = entry.getValue();
	    WebElement input = findElementByXPath(_driver, String.format(inputXpath, metaname));
	    if(input == null) { // need to add the metaname first before we can
		// add the metavalue
		input = getElement("//input[@id='newMDName']");
		input.sendKeys(metaname);
		getElement("//span[@id='addNewMD']").click();
		// will add the metaname without reloading page
		PAUSE(1);
		input = getElement(String.format(inputXpath, metaname));
	    }
	    input.clear(); // clear extra space in dc.Description textarea
	    input.sendKeys(metaval);
	    PAUSE(0.75);
	}
	PAUSE(1);
    }

    
    public static void userCommentTest(int nthPreview, String COLL, String DOCID)
    {
	System.err.println("@@@ BrowserTesting userCommentTest() - preview number: "+nthPreview);

	final String[] COMMENTS = { "Admin logging in", "Admin was here", "Admin signing off",
				    "Here I am!", "Hello all", "Up, up and away!" };	
	

	// TODO: need to ensure they have editing rights and have toggled doc editing
	_driver.get(RunGLITest.getGSBaseURL()+"library/collection/"+COLL+"/document/"+DOCID);
	waitForPageLoad();
	waitForCommentSectionToDisplay();
	
	switch(nthPreview) {
	    
	case 1: // prerequisite: logged in as admin
	    System.err.println("@@@ Admin to add 3 comments, check they exist, delete middle one");
	    
	    addUserComments(COMMENTS[0], COMMENTS[1], COMMENTS[2]);

	    checkCommentsExist(adminUser, COMMENTS[0], COMMENTS[1], COMMENTS[2]);

	    deleteComments(adminUser, COMMENTS[1]);
	    
	    break;

	case 2: // prerequisite: logged in as custom user, and same COLL and DOCID as in nthPreview=1
	    System.err.println("@@@ " + custom_username + " to add 3 more comments,"
			       + " check all 5 comments so far exist,"
			       + "\n\tThen delete 1st admin comment"
			       + " and 1st and last comments by " + custom_username
			       + "\n\tThen check final 3 remaining comments exist.");
	    
	    addUserComments(COMMENTS[3], COMMENTS[4], COMMENTS[5]);
	    
	    checkCommentsExist(adminUser, COMMENTS[0], COMMENTS[2]);
	    checkCommentsExist(custom_username, COMMENTS[3], COMMENTS[4], COMMENTS[5]);

	    deleteComments(adminUser, COMMENTS[0]);
	    deleteComments(custom_username, COMMENTS[3], COMMENTS[5]);
	    
	    checkCommentsExist(adminUser, COMMENTS[2]);	    
	    checkCommentsExist(custom_username, COMMENTS[4]);
	    break;

	case 3: // after rebuilding collection
	    // check the same comments still exist attached to this document
	    System.err.println("@@@ After rebuilding, checking there's the comment by admin"
			       + " and the comment by " + custom_username);
	    
	    checkCommentsExist(adminUser, COMMENTS[2]);
	    checkCommentsExist(custom_username, COMMENTS[4]);
	    
	    break;
	case 4: // all users logged out
	    System.err.println("@@@ After all users logged out, logging back in as admin,"
			       + " checking 2 comments exist, deleting both and logging out");
	    checkCommentsExist(adminUser, COMMENTS[2]);
	    checkCommentsExist(custom_username, COMMENTS[4]);
	    
	    // click log in link through user comments section and log in as admin
	    getElement("//div[@id='usercommentlink']/a").click(); //[text()='Add Comment']
	    logInAdmin(false); // false to not 1st go to the login page to log in as admin

	    // delete final comments
	    deleteComments(adminUser, COMMENTS[2]);
	    deleteComments(custom_username, COMMENTS[4]);
	    
	    // log off	    
	    getElement("//div[@id='usercommentlogoutlink']/a").click();//[text()='Logout']
	    // it reloads the same page
	    waitForPageLoad();
	    // no more comments left, so not much to load in comment section
	    waitForCommentSectionToDisplay("//div[@id='commentssection']/div[@id='usercomments']");	    
		
	    // There should now be no usercomments left
	    String commentXPath
		="//div[@id='commentssection']/div[@id='usercomments']/div[@class='usercomment']";
	    List<WebElement> commentEls = findElementsByXPath(_driver, commentXPath);
	    Assert.assertTrue("### There should be 0 comments left, yet found "+commentEls.size(),
			      commentEls.size()==0);
	    System.err.println("@@@ No more user comments left");
	    
	    break;
	default:
	    System.err.println("### Unknown preview test number "+nthPreview
			       +" for userCommentTest");
	    break;
	}	
    }

    public static void addUserComments(String ... comments) {
	System.err.println("@@@@ In BrowserTest.addUserComments() - adding: " + Arrays.toString(comments));
	
	String xPath = "//div[@id='commentssection']//form[@id='usercommentform']";
	WebElement submitButton = getElement(xPath + "/input[@id='usercommentSubmitButton']");
	
	//String xPath = "//div[@id='commentssection']//form[@id='usercommentform']//div[@id='commentarea']/textarea";
	for(String comment : comments) {
	    System.err.println("@@@ Going to add user comment: " + comment);
	    WebElement textArea = getElement(xPath + "//div[@id='commentarea']/textarea");
	    scrollElementIntoView(textArea);
	    textArea.clear();
	    textArea.sendKeys(comment);
	    
	    submitButton.click();
	    // https://stackoverflow.com/questions/32971546/how-to-wait-for-input-element-enabled-in-selenium-webdriver
	    // Wait for submit button to be disabled
	    new WebDriverWait(_driver, TIMEOUT_SHORT).until(
		ExpectedConditions.not(ExpectedConditions.elementToBeClickable(submitButton)));
	    // Now we can wait for submit button to be enabled again
	    new WebDriverWait(_driver, 3*TIMEOUT_SHORT).until(
		ExpectedConditions.elementToBeClickable(submitButton));
	    PAUSE(0.5); // Give it time (may need more) to see the submitbutton enabled again
	    System.err.println("@@@ Added user comment: " + comment);
	}
    }
    public static void checkCommentsExist(final String user, String ... comments) {
	System.err.println("@@@@ In BrowserTest.checkCommentsExist() - checking comments: " + Arrays.toString(comments));
	// Comments take some time to appear. Wait for them. Though an array, don't need to wait
	// for final array element to appear (and anyway, we can't know how many elements there are).
	// The xPath to the array of usercomments itself takes this time to be located, so wait for xPath.
	String xPath
	    = "//div[@id='commentssection']/div[@id='usercomments']/div[@class='usercomment']";
	waitForXPath(_driver, xPath);
	scrollElementIntoViewJS("//div[@id='commentssection']");
	
	List<WebElement> commentEls = findElementsByXPath(_driver, xPath);
	Map<String, String> foundComments = new HashMap<String, String>(commentEls.size());
	
	for(WebElement el : commentEls) {
	    // div 0 = name, div 0 = timestamp, div 2 = comment
	    List<WebElement> divs = el.findElements(By.xpath("div"));
	    Assert.assertTrue("### Comment not composed of 3 divs", divs.size()==3);
	    String uname = divs.get(0).getText();
	    if(uname.equals(user)) {
		foundComments.put(divs.get(2).getText(), uname);
	    }
	}

	for(String comment : comments) {
	    Assert.assertTrue("### Comments of user "+user+" did not contain comment:\n"+comment,
			      foundComments.containsKey(comment));
	    System.err.println("@@@ Comment by " + user + " present: " + comment);
	}
    }

    public static void deleteComments(final String user, String ... comments) {
	System.err.println("@@@@ In BrowserTest.deleteComments() - to delete " + Arrays.toString(comments));
	prepForDocEditing(true); // need to ensure enable edit mode is on to delete comments
	// prep for docEditing may reload page, so pass in true to wait for comment section to load
	
	String xPath = "//div[@id='commentssection']";
	//scrollElementIntoViewJS(xPath);//prepFordDocEditing(true) above scrolls comments in view
	String commentXPath = xPath+"/div[@id='usercomments']/div[@class='usercomment']";
	
	// Comments take some time to appear. Wait for them. Though an array, don't need to wait
	// for final array element to appear. commentXPath itself takes time. So wait for commentXPath
	waitForXPath(_driver, commentXPath);
	//PAUSE(2); // Give it some time for the comments to all appear. TODO: how much time? What should we wait on?
	List<WebElement> commentEls = findElementsByXPath(_driver, commentXPath);
	
	if(comments.length > commentEls.size()) {
	    Assert.fail("### ERROR: Asked to delete more comments than are present");
	}

	// Keep a searchable list (map) of all comments we've been asked to delete
	Map<String, String> commentsToDelete = new HashMap<String, String>(comments.length);
	for(String comment : comments) {
	    commentsToDelete.put(comment, "true");
	}

	// Tick all comments by that user that are in the list/map of comments to be deleted
	int countChecked = 0;
	for(WebElement el : commentEls) {
	    // div 0 = checkbox, div 1 = name, div 2 = timestamp, div 3 = comment
	    List<WebElement> children = el.findElements(By.xpath("*"));
	    Assert.assertTrue(
	      "### ERROR: Editable comments must have 4 elements (checkbox input and 3 divs)",
	      children.size()==4);
	    WebElement checkbox = children.get(0); // get the checkbox for this comment
	    String uname = children.get(1).getText();
	    String ucomment = children.get(3).getText();
	    if(uname.equals(user) && commentsToDelete.containsKey(ucomment)) {
		checkbox.click();
		countChecked++;
	    }
	}
	if(countChecked != comments.length) {
	    Assert.fail("### ERROR: Could not find all comments to delete. Found "
			      + countChecked + " instead of expected " + comments.length);
	}

	// Press the delete button to delete all ticked comments
	WebElement deleteButton=getElement(xPath+"//button[@id='del-selected-comments-button']");
	scrollElementIntoView(deleteButton);
	deleteButton.click();
	
	Alert alert = _driver.switchTo().alert();
	Assert.assertEquals("### Unexpected message in delete comments confirmation popup alert",
			    "Are you sure you want to delete the selected user comment(s)?",
			    alert.getText());
	alert.accept(); // for OK	

	WebElement submitButton = getElement(
	     "//form[@id='usercommentform']/input[@id='usercommentSubmitButton']");
	// It's the submit button we need to wait for.
	// Wait for submit button to be disabled
	new WebDriverWait(_driver, TIMEOUT_SHORT).until(
		ExpectedConditions.not(ExpectedConditions.elementToBeClickable(submitButton)));
	// Now we can wait for submit button to be enabled again
	new WebDriverWait(_driver, 3*TIMEOUT_SHORT).until(
		ExpectedConditions.elementToBeClickable(submitButton));
	System.err.println("@@@ Deleted comments: " + Arrays.toString(comments));
	PAUSE(1);
    }    
    public static void waitForCommentSectionToDisplay() {
	// When there *are* comments,
	// The comment form with its submit button appear before user comments are all loaded
	// Therefore the wait is for the comments themselves: their heading appears with them
	String commentSectionHeadingXPath
	    = "//div[@id='usercomments']/div[@class='usercommentheading']";
	waitForCommentSectionToDisplay(commentSectionHeadingXPath);
    }
    // When there are no comments, wait on a different element
    public static void waitForCommentSectionToDisplay(String commentSectionXPath) {
	System.err.println("@@@@ In BrowserTest.waitForCommentSectionToDisplay()");
	
	// Wait for user comment section to be ready	
	new WebDriverWait(_driver, 2*TIMEOUT_SHORT).until(
		  ExpectedConditions.presenceOfElementLocated(By.xpath(commentSectionXPath)));
	scrollElementIntoViewJS(commentSectionXPath); // headings can't receive key input
	// so use the JS version of function
    }
    
    // Pass in either of lucene/solr-jdbm-demo as COLL and docID of doc to edit
    // Then we can change the same metadata on either of these sectioned html
    // demo collections when testing for document editing abilities, and this function is more useful
    // Note solr-jdbm-demo's Title classifier has no horizontal classifiers unlike lucene-jbdm-demo
    public static void documentEditing(int nthPreview, final String COLL, final String DOCID) {
	System.err.println("@@@ BrowserTesting documentEditing() - preview number: "+nthPreview);
	System.err.println("@@@    Document edits performed on collection : "+COLL + " - docID: " + DOCID);
	
	String errMsg;

	// Strings we'll be adding
	final String PINKY = "Pinky";
	final String TOTO = "Toto";
	final String NONO = "Nono";
	final String TOTO_BODY_TEXT = TOTO+" signing off!";
	final String PINKY_SUBSECTION_TITLE = PINKY+" was here!"; // Title for .1
	// Don't use "we're on our way" but "we *are* on our way" because
	// apostrophe is a problem for xPaths where apostrophes are used to bookend strings
	final String NONO_DOC_TITLE = NONO+" we are on our way!"; // dc.Title for toplevel
	final String EXTRA_META_FIELD = "dc.Language";
	final String EXTRA_META_VALUE = "French";
	
	switch(nthPreview) {
	case 1:
	

	// 2. Go directly to lucene-jdbm-demo's document page for
	// "Little Known Asian Animals With a Promising Economic Future"
	// No need to search or browse around to it, as searching and browsing tested elsewhere
	// http://localhost:8383/greenstone3/library/collection/lucene-jdbm-demo/document/b18ase
	_driver.get(RunGLITest.getGSBaseURL()+"library/collection/"+COLL+"/document/"+DOCID);

	// 3. Choose to edit the document content
	prepForDocEditing();
	
	// 4. Make changes for 1st rebuild
	// toplevel doc section header's metadata: add meta
	// first subsection section header's metadata: add meta
	// edit document body of first subsection
	
	toggleDocEditMetadataOfSectionHeader(1, "Edit"); // Edit metadata of 1st sectionHeader (toplevel doc)

	addNewDocMetadata(DOCID, EXTRA_META_FIELD, EXTRA_META_VALUE);
	    
	PAUSE(1);
	toggleDocEditMetadataOfSectionHeader(1, "Hide"); // toplevel section has sectionHeader#1

	// Now the first subsection, "Acknowledgements" in lucene-jdbm-demo doc b18ase
	toggleDocEditMetadataOfSectionHeader(2, "Edit"); // first subsection has sectionHeader#2
	addNewDocMetadata(DOCID+".1", "Title", PINKY_SUBSECTION_TITLE);
	String oldSubsectionTitle = getDocMetaVal(DOCID+".1", "Title", "1");
	
	basicEditDocumentBody(DOCID+".1", TOTO_BODY_TEXT); // prepends

	// TODO: rebuild - check all changes are present: search full text for Toto,
	// and check page for Pinky. Check dc.Language French was added
	saveAndRebuildUntilDone_DocEditing(COLL);

	// Section title we expect is "oldTitle, newTitle", e.g "Acknowledgments, Pinky was here!";
	String expectedSubsectionTitle = oldSubsectionTitle +", "+PINKY_SUBSECTION_TITLE;
	searchTest_onDocEditingRebuild1(TOTO, PINKY, expectedSubsectionTitle);
	// we're returned to the doc
	//_driver.get(RunGLITest.getGSBaseURL()+"library/collection/"+COLL+"/document/"+DOCID);
	
	// check toplevel doc for dc.Language=French and first subsection for dc.Title=Pinky
	prepForDocEditing();
	toggleDocEditMetadataOfSectionHeader(1, "Edit");
	checkDocMetadataExists(DOCID, EXTRA_META_FIELD, EXTRA_META_VALUE, "last()", LITERAL);
	// check first subsection of doc for the Title meta to contain Pinky
	toggleDocEditMetadataOfSectionHeader(2, "Edit");
	checkDocMetadataExists(DOCID+".1", "Title", PINKY, "last()", CONTAINS_PATTERN);
	
	// 5. Make changes for 2nd rebuild
	// delete metadata dc.Language
	// edit metadata (e.g Title of toplevel)
	// Then rebuild and check all the updates and new Title in Title view
	
	// Reload doc page and edit top level doc's metadata: delete and modify existing
	_driver.get(RunGLITest.getGSBaseURL()+"library/collection/"+COLL+"/document/"+DOCID);
	prepForDocEditing();
	toggleDocEditMetadataOfSectionHeader(1, "Edit");

	deleteDocMeta(DOCID, EXTRA_META_FIELD, "last()"); // removes the dc.Language we added
	// prepend NONO_DOC_TITLE to 1st dc.Title meta in DOCID
	modifyDocMeta(DOCID, "dc.Title", NONO_DOC_TITLE, "1", PREPEND);
	saveAndRebuildUntilDone_DocEditing(COLL);

	// 6. 2nd set of preview tests
	// First test should still hold, so repeat it.
	searchTest_onDocEditingRebuild1(TOTO, PINKY, expectedSubsectionTitle);
	// we're returned to the doc
	prepForDocEditing();
	toggleDocEditMetadataOfSectionHeader(1, "Edit");//, OPTIONAL);
	String origVal = getDocMetaVal(DOCID, EXTRA_META_FIELD, "last()");
	errMsg = String.format("### Document %s still contains a field %s with value %s",
				      DOCID, EXTRA_META_FIELD, EXTRA_META_VALUE);
	Assert.assertTrue(errMsg, !origVal.equals(EXTRA_META_VALUE));
	
	System.err.printf("@@@ Doc %s's extra %s field (with value %s) successfully removed.\n",
			  DOCID, EXTRA_META_FIELD, EXTRA_META_VALUE);
	
	loadClassifierByName(_driver, "Title");
	if(COLL.equals("lucene-jdbm-demo")) {
	    clickHorizontalClassifier(NONO_DOC_TITLE.substring(0,1)); // choose letter N
	}
	clickDocLink_docTitleContains(NONO_DOC_TITLE);	
	
	break;
	
	case 2: // after full-rebuild of the collection, so no doc-edits should persist now
	    // as the Document Editor only changes archives and index folders, not import folder
	    
	    //logIn(custom_username, custom_userpwd); // If only running nthPreview case 2
	    
	    expectNoSearchResults(COLL, NONO, "all fields");
	    expectNoSearchResults(COLL, PINKY, "all fields");
	    expectNoSearchResults(COLL, TOTO, "all fields");
	    
	    // double check the same on document page
	    _driver.get(RunGLITest.getGSBaseURL()+"library/collection/"+COLL+"/document/"+DOCID);
	    prepForDocEditing();
	    toggleDocEditMetadataOfSectionHeader(1, "Edit");
	    checkMetaNotPresent(DOCID, EXTRA_META_FIELD, EXTRA_META_VALUE); // already deleted
	    checkMetaNotPresent(DOCID, "dc.Title", NONO_DOC_TITLE);
	    toggleDocEditMetadataOfSectionHeader(1, "Hide");

	    toggleDocEditMetadataOfSectionHeader(2, "Edit");
	    checkMetaNotPresent(DOCID+".1", "Title", PINKY_SUBSECTION_TITLE);
	    boolean found = basicBodySearch_docEditing(DOCID+".1", TOTO);
	    errMsg = "### After full rebuild, doc edits should be gone. Yet string "+TOTO
		+" still found in (rich text editor) section body of "+DOCID+".1";		
	    Assert.assertFalse(errMsg, found);

	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for docEditing");
	    break;
	}
    }

    public static void expectNoSearchResults(String COLL, String query, String level) {
	String noneMatchedStr = "No sections match the query.";
	String queryMatchStr = COLL.equals("solr-jdbm-demo")? null
	    : "'"+query.toLowerCase()+"' occurs 0 times"; // for lucene-jdbm-demo
	GSSeleniumUtil.performQuickSearch(_driver, query, level);
	checkSearchResults(0, noneMatchedStr, queryMatchStr);	
    }
    
    public static void checkMetaNotPresent(String docSectionID, String field, String value) {
	String rowXPath = getDocMetaRowXPathContainingVal(docSectionID, field, value);
	elementShouldNotExist(rowXPath);
    }
    // Hard coded tests
    public static void searchTest_onDocEditingRebuild1(String uniqueTextQuery,
						       String uniqueTitleQuery,
						       String expectedSearchResultFullTitle) {
	// Search text for Toto, index titles for pinky and back on the edit page,
	// check dc.Language=French at toplevel doc
	String oneMatchedStr = "One section matches the query.";
	String queryMatchStr = "'%s' occurs 1 time";
	
	GSSeleniumUtil.performQuickSearch(_driver, uniqueTextQuery, "text");
	checkSearchResults(1,
			   oneMatchedStr,
			   String.format(queryMatchStr, uniqueTextQuery.toLowerCase()));

	GSSeleniumUtil.performQuickSearch(_driver, uniqueTitleQuery, "titles");
	checkSearchResults(1,
			   oneMatchedStr,
			   String.format(queryMatchStr, uniqueTitleQuery.toLowerCase()));

	//String expectedTitle = oldTitle+", "+newTitle; //"Acknowledgments, Pinky was here!";
	checkDocDisplayAt(1, null, POS_DEFAULT_SEARCH_TITLE, expectedSearchResultFullTitle);

	// Goes back to doc page
	clickSearchResultDocLink_docTitleContains(expectedSearchResultFullTitle);
    }
    // Press the Edit Content button - may need to enable it first
    public static void prepForDocEditing() {
	prepForDocEditing(false); // by default, don't need to wait for user comments section
    }
    public static void prepForDocEditing(boolean waitForCommentSection) {
	System.err.println("@@@ prepForDocEditing(): will click button Edit Content");

	boolean pageReloaded = false;
	
	WebElement editContentButton = findElementByXPath(_driver, "//a[@id='editContentButton']");
	if(editContentButton == null) {
	    System.err.println("@@@ Edit mode button needs enabling");
	    chooseUserMenu("Enable edit mode"); // Prerequisite: Should be logged in as a user and on a document page
	    waitForPageLoad(); // the page reloads, so wait for it
	    pageReloaded = true;
	    // Now the button should be present
	    editContentButton = getElement("//a[@id='editContentButton']");
	}
	String expectedEditContentButtonLabel = "hide";
	if(waitForCommentSection) {
		PAUSE(1);
	    // For user comments, we need to ensure the button says Edit content
	    // As then metadata editing tables are hidden, but the user comments editing
	    // controls are available (the controls are hidden metadata tables are showing)
	    expectedEditContentButtonLabel = "edit content";
	}
	// ensure the button doesn't say Hide Editor or, in the case of waiting for user comments,
	// that it doesn't say Edit content
	if(!editContentButton.getText().toLowerCase().contains(expectedEditContentButtonLabel)) {
	    editContentButton.click();
	    waitForPageLoad(); // the page reloads, so wait for it
	    pageReloaded = true;
	} else {
	    System.err.println("@@@ Already in required content editing mode: button labelled "
			       +expectedEditContentButtonLabel);
	}
	
	if(waitForCommentSection && pageReloaded) {
	    waitForCommentSectionToDisplay();
	}
    }
    
    public static void toggleDocEditMetadataOfSectionHeader(int nthSection, String editOrHide) {
	toggleDocEditMetadataOfSectionHeader(nthSection, editOrHide, COMPULSORY);
    }
    
    public static void toggleDocEditMetadataOfSectionHeader(int nthSection, String editOrHide,
							    boolean isOptional) {
	System.err.println("@@@ Toggling Edit meta button on nth sectionHeader "+nthSection
			   +" to "+editOrHide);
	
	//xPath = "//div[@id='gs-document-text']/table[@class='sectionHeader']//td[@class='editMetadataButton']//a//span[text()='Edit metadata']";

	// This xPath gets 2nd sectionHeader's 'Edit metadata' button
	String nthSectionEditMetaButton
	    = String.format("(//div[@id='gs-document-text']//*[@class='sectionHeader'])[%d]//*[@class='editMetadataButton']//a//span[text()='%s metadata']", nthSection, editOrHide);

	// This xPath gets the 2nd 'Edit metadata' button
	// That is, the Edit meta button of the 1st subsection
	//String nthSectionEditMetaButton
	//= String.format("(//div[@id='gs-document-text']/*[@class='sectionHeader']//*[@class='editMetadataButton']//a//span[text()='%s metadata'])[%d]", nthSection, editOrHide);

	if(isOptional) {
	    WebElement editSectionButton = findElementByXPath(_driver, nthSectionEditMetaButton);
	    if(editSectionButton != null) {
		editSectionButton.click();
	    }
	    else {
		System.err.println("*** The "+editOrHide
				   +" button wasn't present, but toggling was optional");
		// TODO: Check the other button label is showing? (Hide vs Edit)
	    }
	} else { // button must exist
	    getElement(nthSectionEditMetaButton).click();	    
	}
	PAUSE(2); // page doesn't reload, but give it some time?
    }

    public static void saveAndRebuildUntilDone_DocEditing(String coll) {
	System.err.println("@@@ About to click saveAndRebuild in document editor, then wait");
	
	getElement("//button[@id='saveAndRebuildButton']").click();

	// A status message indicating that the collection is being built will briefly appear
	// We're done with building when this status message div disappears
	String xPath="//div[@class='statusMessage'][span[contains(text(),'Building collection "
	    +coll+"')]]"; // "Building collection lucene-jdbm-demo ..."
	
	// First we wait until the building status message appears
	new WebDriverWait(_driver, 2*TIMEOUT_SHORT).until(
			    ExpectedConditions.presenceOfElementLocated(By.xpath(xPath)));
	WebElement statusDiv = getElement(xPath);
	
	// Then we can finally wait until it disappears
	new WebDriverWait(_driver, MAX_BUILD_TIMEOUT/3).until(
				       ExpectedConditions.stalenessOf(statusDiv));

	System.err.println("@@@ Doc editor rebuilt collection. Waiting for page reload");
	waitForPageLoad(); // the page reloads after building, so wait a bit
    }
    public static void basicEditDocumentBody(String docSectionID, String prefixStr)
    {
	System.err.println("@@@ About to prefix paragraph to doc "+docSectionID+": "+prefixStr);
	/*
	System.err.print("@@@ About to prefix string to doc "+docSectionID+": "+prefixStr);
	if(appendLine) {
	    System.err.println("<NEWLINE>");
	} else {
	    System.err.println("<SPACE>");
	}
	*/
	// e.g. //div[@id='textb18ase.1' and @contenteditable='true']//p[1]
	// If no paragraphs, then just try //div[@id='textb18ase.1' and @contenteditable='true']

	String xPath = "//div[@id='text"+docSectionID+"' and @contenteditable='true']";
	WebElement el = findElementByXPath(_driver, xPath+"//p[1]");
	
	System.err.println("*** Trying to reach xpath: "+xPath+"//p[1]");	
	if(el == null) {
		System.err.println("*** No paragraph sub element, trying for parent element " + xPath);	
	    el = getElement(xPath);
	}
	// Doesn't work, as we're not allowed to click() or sendKeys() to div and p elements
	// They'll throw an ElementNotInteractableException, "Element is not reachable by keyboard"
	// https://stackoverflow.com/questions/49864965/org-openqa-selenium-elementnotinteractableexception-element-is-not-reachable-by
	
	/*
	el.click();
	// Just start typing here.
	el.sendKeys(prefixStr);
	if(appendLine) {
	    // Could try Keys.ENTER? https://www.selenium.dev/documentation/webdriver/actions_api/keyboard/
	    el.sendKeys(Keys.RETURN);
	} else {
	    el.sendKeys(Keys.SPACE);
	}
	*/
	// Trying javascript (can't try textarea suggestion as this is a div, not a textarea/input)
	// https://stackoverflow.com/questions/40023544/how-to-enter-text-in-rich-text-editor-in-selenium-webdriver
	Assert.assertTrue("### ASSERT ERROR: could not find element at xPath: " + xPath,
		el != null);
	((JavascriptExecutor)_driver).executeScript("arguments[0].innerHTML = '<p>"+prefixStr
						    +"</p>' + arguments[0].innerHTML", el);
	// This doesn't really mimic a user interacting with the Rich Text Editor sadly
	// But we're testing whether the text goes into the collection when rebuilt *through
	// the web editor*. So as long as the text is in the display, it may work
	PAUSE(3);
	System.err.println("*** Attempted to prefix string: "+prefixStr+ " to xPath: " + xPath);
    }
    public static boolean basicBodySearch_docEditing(String docSectionID, String find) {
	System.err.println("@@@ Searching doc "+docSectionID+" for: "+find);
	String xPath = "//div[@id='text"+docSectionID+"' and @contenteditable='true']";
	WebElement el = getElement(xPath);
	//System.err.println("\n***\nRich Text Editor contains text:\n"+el.getText()+"\n****\n");
	return el.getText().contains(find);
    }
    
    // pos is nth occurrence of metadata field with the given field name, e.g 2nd or last() Title
    // row will be returned
    public static String getDocMetaRowXPathContainingVal(String docSectionID, String field, String value) {
	// This gets the matching textarea
	// //table[@id='metab18ase']//tr[td[@class='metaTableCellName' and text()='Title']]/td[@class='metaTableCell']/textarea[contains(text(),'Little Known Asian Animals')]

	// This gets the row
	//table[@id='metab18ase']//tr[td[@class='metaTableCellName' and text()='Title'] and td[@class='metaTableCell']/textarea[contains(text(),'Little Known Asian Animals')]]
	String sectionXPath = String.format("//table[@id='meta%s']", docSectionID);
	String metaRowXPath = sectionXPath+"//tr[td[@class='metaTableCellName' and text()='"+
	    field+"'] and td[@class='metaTableCell']/textarea[contains(text(),'"+value+"')]]";
	
	return metaRowXPath;
    }
    public static String getDocMetaRowXPath(String docSectionID, String field, String pos) {
	// Can get at element in 2 ways. CAPS used to differentiate them, but lowercase in xpath
	//TABLE[@id='METAb18ase']//tr[td[@class='metaTableCellName' and text()='Title']][last()]//textarea[contains(@class, 'metaTableCellArea')]

	// or //DIV[@id='DIVb18ase']//tr[td[@class='metaTableCellName' and text()='Title']][last()]//textarea[contains(@class, 'metaTableCellArea') AND contains(text(), 'Little Known Asian Animals')]
	// We just need to get the entire row
	// Choosing 1st option
	String sectionXPath = String.format("//table[@id='meta%s']", docSectionID);
	String metaRowXPath
	    = sectionXPath+"//tr[td[@class='metaTableCellName' and text()='"+field+"']]["+pos+"]";

	return metaRowXPath;	
    }
    public static void deleteDocMeta(String docSectionID, String field, String pos) {
	
	String removeButtonXPath = getDocMetaRowXPath(docSectionID, field, pos)
	    + "//td[@class='metaTableCellRemove']";
	getElement(removeButtonXPath).click();
    }

    // Returns original value in cell, before modifying the cell with new value. Is this useful?
    public static String modifyDocMeta(String docSectionID, String field, String value, String pos,
				     int MODE) {
	String metavalXPath = getDocMetaRowXPath(docSectionID, field, pos)
	    + "//textarea[contains(@class, 'metaTableCellArea')]";
	WebElement txtArea = getElement(metavalXPath);
	String oldVal = txtArea.getText();
	
	txtArea.click();
	if(MODE == REPLACE) {
	    txtArea.clear();
	} else if(MODE == APPEND) {
	    // Ctrl+end to go to the end of the line and add a space before appending
	    // https://stackoverflow.com/questions/18487159/how-can-i-simultaneously-perform-ctrl-enter-in-selenium-webdriver
	    //txtArea.sendKeys(Keys.chord(Keys.CONTROL, Keys.END));
	    txtArea.sendKeys(toLineEnd());
	    txtArea.sendKeys(" ");
	} else {  // for MODE==PREPEND
	    //txtArea.sendKeys(Keys.chord(Keys.CONTROL, Keys.HOME));
	    txtArea.sendKeys(toLineStart());
	    value += " "; // add a space after the value
	}
	// Now can at last enter the value into the input
	txtArea.sendKeys(value);
	
	return oldVal;
    }    
    
    public static String getDocMetaVal(String docSectionID, String field, String pos) {
	System.err.println("@@@ Getting value of meta "+field+" occurrence "+pos
			   +" of docID "+docSectionID);

	String metavalXPath = getDocMetaRowXPath(docSectionID, field, pos);
	WebElement txtArea = getElement(metavalXPath + "//textarea[contains(@class, 'metaTableCellArea')]");
	return txtArea.getText();
    }
    
    // pos can be null, then this function looks for any matching metadata
    // field within the given docSectionID that matches the given value
    public static void checkDocMetadataExists(String docSectionID, String field, String value,
					      String pos, int MATCH_MODE) {
	System.err.print("@@@ Checking meta exists on docID "+docSectionID+": "
			   +field+" has "+value);
	if(pos != null) {
	    System.err.println("\t at nth occurrence of " + field + ": "+pos);
	    String textcontent = getDocMetaVal(docSectionID, field, pos);
	    stringContainsText(textcontent, MATCH_MODE, value);
		// stringContainsText() already produces a message on success
	} else {
	    String rowXPath = getDocMetaRowXPathContainingVal(docSectionID, field, value);
	    getElement(rowXPath); // assertion will fail if no row with that field and value
	}
    }
    
    public static void addNewDocMetadata(String docSectionID, String field, String value)
    {
	System.err.println("@@@ About to add new doc meta to "+docSectionID+": "+field+"="+value);
	
	// e.g. //div[@id='docb18ase.1']//button[text()='Add new metadata']
	String sectionXPath = String.format("//div[@id='doc%s']", docSectionID);
	String addButtonXPath = sectionXPath + "//button[text()='Add new metadata']";
	
	// We want the input field before the button, to add the metaname
	WebElement input = getElement(addButtonXPath +
	      "/preceding-sibling::input[@type='text' and contains(@title, 'Enter a metadata name')]");
	//System.err.println("*** Trying to reach xpath: " + addButtonXPath +
	//		   "/preceding-sibling::input[@type='text' and contains(@title, 'Enter a metadata name')]");
	scrollElementIntoView(input);
	PAUSE(0.5);
	input.clear();
	input.sendKeys(field);
	getElement(addButtonXPath).click();
	PAUSE(1);

	// To add a metavalue into the input field of the row newly added at the end,
	// first need xPath of this final row. The xpath form is like:
	// "//div[@id='docb18ase.1']//tr[td[@class='metaTableCellName' and text()='Title']][last()]"
	// Xpath to the final, newly added metadata row:
	// https://stackoverflow.com/questions/5537129/php-xpath-selecting-last-matching-element
	String metavalXPath
	    = sectionXPath+"//tr[td[@class='metaTableCellName' and text()='"+field+"']][last()]";
	// Get the input field in the row
	// (//tr[td[@class='metaTableCellName' and text()='dc.Language']])[last()]/td[@class='metaTableCell']/textarea[contains(@class, 'metaTableCellArea')]
	input = getElement(metavalXPath + "//textarea[contains(@class, 'metaTableCellArea')]");
	input.click();
	input.clear();
	input.sendKeys(value);
	PAUSE(1);
    }

    public static void addUser(String uname, String pwd, String ... groups) {
	System.err.println("@@@ Adding user " + uname);
	
	// Going directly to this URL will work whatever interface we're using
	_driver.get(RunGLITest.getGSBaseURL()+"library/admin/AddUser");
	waitForPageLoad();

	String formXpath = "//form[@id='registerForm']";
	
	String inputXpath = formXpath+"//input[%s]";	
	fillInTextField(String.format(inputXpath, "@name='s1.username'"), uname);
	fillInTextField(String.format(inputXpath, "@name='s1.password'"), pwd); //@id='passwordOne'
	fillInTextField(String.format(inputXpath, "@id='passwordTwo'"), pwd);

	Select dropdown_groups = getDropDown(formXpath+"//select[@id='groupSelector']");
	for(String group : groups) {
	    dropdown_groups.selectByVisibleText(group);
	    getElement("//button[@id='addGroupButton']").click();
	}
	
	// get radio button for enabled status
	WebElement status = getElement(formXpath+"//input[@name='s1.status' and @value='true']");
	status.click(); // click the radio button (even though enabled is set by default for now)

	WebElement submitButton = getElement(formXpath+"//input[@id='submitButton']");
	submitButton.click();
    }
    // Expect caller to login and logout the admin user before and after calling
    public static void deleteUser(String uname) {
	System.err.println("@@@ Deleting user " + uname);
	
	// Going directly to this URL will work whatever interface we're using
	_driver.get(RunGLITest.getGSBaseURL()+"library/admin/ListUsers");
	waitForPageLoad();
	
	//table[@id='userListTable']//tr[td[text()='me']]//form[contains(@action, 'PerformDeleteUser')]/input[@type='submit' and @value='Delete']
	String userRowXpath = "//table[@id='userListTable']//tr[td[text()='me']]";
	
	WebElement deleteButton
	    = getElement(userRowXpath
			 +"//form[contains(@action, 'PerformDeleteUser')]/input[@type='submit']");
	deleteButton.click();

	// A confirm alert javascript popup box will appear, must accept
	// https://stackoverflow.com/questions/36407740/click-to-confirm-a-modal-dialog-with-selenium-webdriver
	PAUSE(2);
	Alert alert = _driver.switchTo().alert();
	// assertEquals(errMsg, expected, actual)
	Assert.assertEquals("### Unexpected message in delete user confirmation popup alert",
			    "Are you sure you want to delete user "+uname+"?", alert.getText());
	alert.accept(); // for OK
	waitForPageLoad();//PAUSE(2); // give a chance for the page to reload
    }
    
    public static void simpleHTML() {
	System.err.println("@@@ BrowserTesting simpleHTML() - preview test case #: 1/1");

	final int numDocs = 3;

	GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");
	GSSeleniumUtil.checkNumDocs(numDocs, "Source", "CL2");

	GSSeleniumUtil.loadClassifierByName(_driver, "Title"); // CL1

	// click boleyn.html, unfortunately boleyn.html is inside a <i>. Look for Anne Boleyn
	clickDocLink_anchorContains("CL1", POS_DEFAULT_BROWSE_TITLE, "Anne Boleyn");
	// in page that loads, click on an internal link
	// https://stackoverflow.com/questions/5868439/wait-for-page-load-in-selenium
	clickLink("//a[text()='Katharine of Aragon']");
	String url = _driver.getCurrentUrl();
	// check it's an internal link
	Assert.assertTrue("### ASSERT ERROR: Expected an internal GS3 link: " + url,
			  url.contains("/greenstone3"));

	// Go back to boleyn page and click on external link
	_driver.navigate().back();
	waitForPageLoad();


	// annoyingly, there's a newline between "letters" and "written by Anne"
	clickLink("//a[contains(text(),'letters') and contains(text(),'written by Anne')]");
	
	url = _driver.getCurrentUrl();
	// check it's an external link.
	// TODO: Unfortunately external links are disabled in GS3 by default. How to enable them?
	//Assert.assertFalse("### ASSERT ERROR: Expected external (non-Greenstone) link: " + url,
	//		  url.contains("/greenstone3"));
	// For now, this assertion will work both if external links enabled or disabled:
	Assert.assertTrue("### ASSERT ERROR: Expecting external link to englishhistory.net: "
			  + url, url.contains("englishhistory.net"));
	
    }

    // This function is called when doing browser tests of both the backdrop collection
    // and the two downloadingOverOAI collections (OAICMDdownload and OAIGLIdownload).
    // Test cases 8 and 9 are for the downloadingOverOAI collections.
    public static void backdrop(int nthPreview, Object moreParams) {
	System.err.println("@@@ BrowserTesting backdrop() - preview test case #: " + nthPreview);

	final String browseByTitleLabel = getElement("//ul[@id='gs-nav']/li[1]").getText().trim();
	    // - will be "Browse" for backdrop collection (inherited from image-e base coll),
	    // - versus default CL1 buttonname "Title" for collections in downloadingOverOAI()
	
	final String classifierID = "CL1"; // Browse classifier
	final String descrClassifierID = "CL2";
	final int numDocs = 9;
	String[] descriptions = null; // params
	
	final String[] expectedDocs = { "Bear", "Cat", "Cheetah",
				      "Fish", "Gopher", "Lemur",
				      "Rhino", "Shark", "Tiger" };
	
	final int POS_THUMB_ICON = 2;
	final int POS_DOC_DESCR = 1+POS_THUMB_ICON;
	
	String xPathFormat = "//table[@id='div%s']//table[@id='%s']//tr";
	    // "//table[@id='div%s']/tbody/tr/td/table[@id='%s']/tbody/tr";
	String newXpathFormat = "//table[contains(@id,'div%s')]//tr//table[contains(@id, 'div')][tbody/tr/td["+POS_THUMB_ICON+"]//img[contains(@alt, 'D%d_thumb.gif')]]//tr";
// "//table[contains(@id,'div%s')]//tr[%d]//table[contains(@id, 'div')]//tr";
	
	// NOTE: The xPathFormat string
	//String xPathFormat = "//table[contains(@id,'div%s')]//tr[%d]//table[contains(@id, 'div')]//tr";
	// *seems* more general, like it would work with both backdrop and downloadingOverOAI
	// collections (using classifierID CL1 for backdrop's Browse a.o.t. CL1.1), but it
	// ultimately causes existing tests to fail and we need to use  distinct xPathFormat
	// strings for backdrop vs downloadingOverOAI collections after all.
	// Explanation: The presentation order of docs in the Descriptions classifier ends up
	// different and so produces content mismatches in the final/'title' td cell on testing.
	// Image Cheetah comes before Cat in Descriptions classifier, but after Cat in the
	// Browse/Title classifier.
	// But the general xPathFormat fails for downloadingOverOAI collections' Description
	// classifier (though it works for their Title classifier).
	// Yet all worked out fine using backdrop's original, backdrop-specific xPathFormat for
	// *both* the Browse classifier and Description classifier because we check tablerows
	// in order of subtableID, which gives the same ordering in both classifiers (and is the
	// order we store the titles in expectedDocs[] and descriptions in descriptions[] array).
	// To replicate this ordering-agnostic behaviour for the downloadingOverOAI collection,
	// we need to rely on thumbnail filenames, which are named in the expected order.
	// We can't use a general xpath-format for backdrop and downloadingOverOAI collections
	// because the backdrop collection doesn't have such thumbnail filenames. The process
	// of downloading files over OAI had obtained the files with the new sequential filenames.	
	switch(nthPreview) {
	case 1: // Check 9 docs have imgs in 2nd td and Image Name, Width, Height, Size in 3rd td
	case 2: // same results for 1st and 2nd previews
	    //GSSeleniumUtil.loadClassifierByName(_driver, "Browse"); // CL1

	    GSSeleniumUtil.checkNumDocs(numDocs, browseByTitleLabel, classifierID);
	    
	    String subTableIDprefix = "divD";
	    
	    for(int i = 0; i < numDocs; i++) {
		String subTableID = "divD"+Integer.toString(i);
		//String xPath = "//table[@id='div"+classifierID+"']/tbody/tr/td/table[@id='"+subTableID+"']/tbody/tr";
		String xPath = String.format(xPathFormat, classifierID, subTableID);
		//WebElement bla = GSSeleniumUtil.getElement(xPath);

		// On Safari, the JSessionIDs are coming through in the URLs as  "<image-file-name>;jsessionid=..."
		// So .*Bear.jpg$ doesn't match, but .*Bear.jpg.* will match
		GSSeleniumUtil.attributeMatches(xPath+"/td["+POS_THUMB_ICON+"]/a",
					       "href", ".*"+expectedDocs[i]+".jpg.*");
					       //"href", ".*"+expectedDocs[i]+".jpg$");
		
		GSSeleniumUtil.containsText(xPath+"/td["+POS_DOC_DESCR+"]",
					   "Image Name:"+expectedDocs[i]+".jpg",
					   "Width:",
					   "Height:",
					   "Size:");
		
		// Check it has the expected thumbnailImages referenced and loaded
		WebElement thumbnailImage = GSSeleniumUtil.getElement(xPath + "/td["
								     +POS_THUMB_ICON+"]/a/img");
		GSSeleniumUtil.attributeMatches(thumbnailImage,
					       "src", ".*"+expectedDocs[i]+"_thumb.gif$");
		GSSeleniumUtil.checkImageLoaded(xPath, thumbnailImage);
	    }
	    break;

	case 3: // first 3 docs have Description:descr, all 9 docs now have Title: prefix label
	case 8: // case 8 is when called via downloadingOverOAI();
	    descriptions = (String[])moreParams;
	    
	    GSSeleniumUtil.loadClassifierByName(_driver, browseByTitleLabel); // CL1
	    
	    for(int i = 0; i < numDocs; i++) {		
		String subTableID = "divD"+Integer.toString(i);
		//String xPath = "//table[@id='div"+classifierID+"']/tbody/tr/td/table[@id='"+subTableID+"']/tbody/tr";
		String xPath = (nthPreview == 3)
		    ? String.format(xPathFormat, classifierID, subTableID) // backdrop coll
		    : String.format(newXpathFormat, "CL1", i); // downloadingOverOAI colls
		
		if(i < descriptions.length) { // first 3 Docs should have Description too
		    GSSeleniumUtil.containsPatternAmongLines(xPath+"/td["+POS_DOC_DESCR+"]",
							    "Title: ?"+expectedDocs[i],
							    "Description: ?"+descriptions[i],
							    "Width:",
							    "Height:",
							    "Size:");
		}
	    }
	    break;

	case 4: // thumbnail images should have width attr set to 50
	    GSSeleniumUtil.loadClassifierByName(_driver, browseByTitleLabel); // CL1
	    
	    for(int i = 0; i < numDocs; i++) {
		String subTableID = "divD"+Integer.toString(i);
		
		String xPath = String.format(xPathFormat, classifierID, subTableID);
		WebElement thumbnailImage = GSSeleniumUtil.getElement(xPath + "/td["
								     +POS_THUMB_ICON+"]/a/img");
		GSSeleniumUtil.attributeMatches(thumbnailImage,
					       "src", ".*"+expectedDocs[i]+"_thumb.gif$");
		
		GSSeleniumUtil.attributeMatches(thumbnailImage, "width", "50");
		GSSeleniumUtil.checkImageLoaded(xPath, thumbnailImage);		
	    }
	    break;
	case 5: // check new Description classifier has only 3 docs (Bear, Cat, Cheetah)
	case 9:
	    GSSeleniumUtil.loadClassifierByName(_driver, "Description"); // CL2
	    
	    descriptions = (String[])moreParams; // 3 descriptions
	    
	    GSSeleniumUtil.checkNumDocs(descriptions.length, "Description", descrClassifierID);
	    
	    // Check Description classifier's table contents	    
	    
	    for(int i = 0; i < descriptions.length; i++) {
		String subTableID = "divD"+Integer.toString(i);		
		String xPath = (nthPreview == 5)
		    ? String.format(xPathFormat, descrClassifierID, subTableID) //backdrop coll
		    : String.format(newXpathFormat, descrClassifierID, i); //downloadingOverOAI
		
		GSSeleniumUtil.containsPatternAmongLines(xPath+"/td["+POS_DOC_DESCR+"]",
							"Title: ?"+expectedDocs[i],
							"Description: ?"+descriptions[i],
							"Width:",
							"Height:",
							"Size:");
	    }
	    break;
	case 6:
	    descriptions = (String[])moreParams;
	    
	    //https://stackoverflow.com/questions/40934644/xpath-testing-that-string-ends-with-substring
	    // https://stackoverflow.com/questions/3920957/xpath-query-with-descendant-and-descendant-text-predicates
	    GSSeleniumUtil.checkQueryBoxExists("TextQuery", "s1");

	    // do a search for "Rocky" or Bear
	    GSSeleniumUtil.performQuickSearch(_driver, "Rocky", null);

	    final int numExpectedResults = 1;	    
	    // 1 result, should be the Bear document
	    DocDisplayStruct[] searchResultExpectations = new DocDisplayStruct[1];
	    // tableCellNum 3 should contain Bear title and description
	    searchResultExpectations[0]
		= new DocDisplayStruct(POS_DOC_DESCR,
			 CONTAINS_MULTILINE_PATTERN,
			 new String[] {
			     // Search results do not have Title: and Description
			     // And still only have Image Name:
			     "Image Name: ?Bear.jpg", //"Title: ?"+expectedDocs[0]+"\\.jpg",
			     //"Description: ?.*Rocky.*", //"Description: ?"+descriptions[0],
			     "Width:",
			     "Height:",
			     "Size:"
			 });
	    // On Safari, the JSessionIDs are coming through in the URLs as  "<image-file-name>;jsessionid=..."
	    // So .*Bear.jpg$ doesn't match, but .*Bear.jpg.* will match
	    String[] hrefs = { ".*Bear\\.jpg.*" }; //{ ".*Bear\\.jpg$" };
	    GSSeleniumUtil.checkSearchResults(numExpectedResults,
					     "One document matches the query.",
					     "'Rocky' occurs 1 time",
					     hrefs,
					     searchResultExpectations);
	    break;
	case 7:
	    GSSeleniumUtil.loadClassifierByName(_driver, "Browse"); // CL1
	    //_driver.get(RunGLITest.getGSBaseURL()+"library/collection/backdrop/search/FieldQuery");
	    clickQueryButton("FieldQuery");
	    checkSearchDisplayText("image descriptions");
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for backdrop");
	    break;
	}
    }

    public static void reports(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting reports() - preview test case #: " + nthPreview);

	final int numDocs = 9;
	final int numAuthors = 13;
	
	final String authorClassifierID = "CL2";
	
	switch(nthPreview) {
	case 1:
	    GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");
	    GSSeleniumUtil.checkNumDocs(numDocs, "Source", "CL2");
	    break;
	case 2: // Check Creators (Authors) bookshelf
	    int[] expectedNumDocsInBookShelf = { 2, 1, 2, 1, 2, 1, 1, 2, 1, 1, 1, 1, 5 };
	    checkNumLeafNodesForClassifier(authorClassifierID,
					   "Creator",
					   numAuthors,
					   expectedNumDocsInBookShelf);
	    searchAndCheckNumResults("Witten", "creators", 5);
	    
	    System.err.println("*** Reports: Windows-only test will happen at end of enhancedReports");
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for reports");
	    break;
	}
    }

    public static void formattingReports(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting formattingReports() - preview test case #: " + nthPreview);

	final int numDocs = 9;
	final int numAuthors = 13;
	final String authorClassifierID = "CL2";
	final String authorsLabel = "Creator";
	
	DocDisplayStruct docDisplayFormat = null;
	
	switch(nthPreview) {
	case 1:
	    reports(2);
	    GSSeleniumUtil.performQuickSearch(_driver, "Witten", "creators");
	    // 2nd td icon of each search result should be either book.png or itext.gif
	    checkSearchDocIconsFormat(new SimpleEntry(new Integer(POS_DEFAULT_SEARCH_DOC_ICON), "(book.png|itext.gif)"));
	    break;
	case 2:
	    GSSeleniumUtil.performQuickSearch(_driver, "Witten", "creators");
	    checkSearchDocIconsFormat(new SimpleEntry(new Integer(POS_DEFAULT_SEARCH_DOC_ICON), "(?!book.png|itext.gif)"));
	    break;
	case 3:
	    GSSeleniumUtil.performQuickSearch(_driver, "Witten", "creators");
	    //https://stackoverflow.com/questions/2404010/match-everything-except-for-specified-strings
	    // td 2 should Not contain book.png/itext.gif vs td 3 should contain either
	    checkSearchDocIconsFormat(
				 new SimpleEntry(new Integer(POS_DEFAULT_SEARCH_DOC_ICON), "(?!book.png|itext.gif)"),
				 new SimpleEntry(new Integer(POS_DEFAULT_SEARCH_DOC_ICON+1), "(book.png|itext.gif)")
			       );
	    break;
	case 4:
	    //GSSeleniumUtil.loadClassifierByName(_driver, authorClassifierID);
	    int[] expectedNumDocsInBookShelf = { 2, 1, 2, 1, 2, 1, 1, 2, 1, 1, 1, 1, 5 };
	    // pass true to check num leaf node indicator is showing and showing correct value
	    checkLeafNodesOfClassifier(authorClassifierID,
				       "Creator",
				       numAuthors,
				       expectedNumDocsInBookShelf,
				       true,
				       null);
	    break;
	case 5:
	    GSSeleniumUtil.loadClassifierByName(_driver, authorsLabel);
	    expandBookshelf(1, authorClassifierID);//expandAllBookshelves(authorClassifierID);
	    
	    // 4th table cell should contain the linked title, followed by newline,
	    // then each author name separated by the Separator (comma in this test number 6)
	    // Use CONTAINS_MULTILINE_PATTERN in tests cases 6-8, as the 4th table cell
	    // contains doc title on 1st line and author information over one (case 6, 7)
	    // or more lines (case 8)
	    docDisplayFormat = new DocDisplayStruct(
		    POS_DEFAULT_BROWSE_TITLE, //4
		     CONTAINS_MULTILINE_PATTERN,
		     new String[] {
			 "Ian H. Witten, Rodger J. McNab, Stefan J. Boddie, David Bainbridge"
		     });
	    checkDocDisplayMatches(FIRST_ROW, authorClassifierID, docDisplayFormat);
	    break;
	case 6:
	    GSSeleniumUtil.loadClassifierByName(_driver, authorsLabel);
	    expandBookshelf(1, authorClassifierID);
	   
	    docDisplayFormat = new DocDisplayStruct(
		    POS_DEFAULT_BROWSE_TITLE, // 4
		     CONTAINS_MULTILINE_PATTERN,
		     new String[] {
			 "Ian H. Witten Rodger J. McNab Stefan J. Boddie David Bainbridge"
		     });
	    checkDocDisplayMatches(FIRST_ROW, authorClassifierID, docDisplayFormat);
	    break;
	case 7:
	    GSSeleniumUtil.loadClassifierByName(_driver, authorsLabel);
	    expandBookshelf(1, authorClassifierID);
	    docDisplayFormat = new DocDisplayStruct(
		    POS_DEFAULT_BROWSE_TITLE, //4,
		     CONTAINS_MULTILINE_PATTERN,
		     new String[] {
			 "Ian H. Witten",
			 "Rodger J. McNab",			 
			 "Stefan J. Boddie",
			 "David Bainbridge"
		     });
	    checkDocDisplayMatches(FIRST_ROW, authorClassifierID, docDisplayFormat);	    
	    break;
	case 8:
	    //GSSeleniumUtil.loadClassifierByName(_driver, authorsLabel);
	    //expandAllBookshelves(authorClassifierID);

	    int[] newExpectedNumDocsInBookShelf = { 2, 1, 1, 4 };
	    final int expectedNumBookShelves = newExpectedNumDocsInBookShelf.length;
	    final String[] bookshelfTitles = { "Sally Jo Cunningham",
					     "Michael B. Twidale",
					     "Yong Wang",
					     "Ian H. Witten"};
	    // pass true to check num leaf node indicator is showing and showing correct value
	    checkLeafNodesOfClassifier(authorClassifierID,
				       "Creator",
				       expectedNumBookShelves,
				       newExpectedNumDocsInBookShelf,
				       true,
				       bookshelfTitles);
	    // check final bookshelf (4 docs) displays its authors as expected	    
	    
	    String[][] expectedAuthorsDisplay = new String[4][];
	    expectedAuthorsDisplay[0] = new String[] { "Ian H. Witten", "Zane Bray",
						       "Malika Mahoui", "W.J. Teahan"};
	    expectedAuthorsDisplay[1] = new String[] { "Ian H. Witten", "Stefan Boddie"};
	    expectedAuthorsDisplay[2] = new String[] { "Ian H. Witten", "Rodger J. McNab",
						       "Stefan J. Boddie", "David Bainbridge"};
	    expectedAuthorsDisplay[3] = expectedAuthorsDisplay[2]; //copy

	    DocDisplayStruct[] docDisplayFormats
		= new DocDisplayStruct[expectedAuthorsDisplay.length];

	    for(int i = 0; i < expectedAuthorsDisplay.length; i++) {
		docDisplayFormats[i] = new DocDisplayStruct(
		    POS_DEFAULT_BROWSE_TITLE, //4,
		     CONTAINS_MULTILINE_PATTERN,
		     //new String[] {
		     //"Ian H. Witten Rodger J. McNab Stefan J. Boddie David Bainbridge"
		     //}
		     expectedAuthorsDisplay[i]);
	    }

	    System.err.println("*** formattingReports: Windows-only test will happen at end of enhancedReports");
	    
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for formattingReports");
	    break;
	
	}
    }
    // The Enhanced Word Document Handling tutorial: windows-only (case 1-7) and all-OS (case 8)
    public static void enhancedReports(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting enhancedReports() - preview test case #:" + nthPreview);

	int numDocsInTitleClassifier = 9; // changes
	int numAuthors; // will change
	
	final String authorClassifierID = "CL2";
	final String authorsLabel = "Creator";
	
	final String newDocTitle = "testword.docx";
	
	DocDisplayStruct docDisplayFormat = null;
	
	String xPath;
	String errMsg;
	List<WebElement> elems;
	WebElement docLink = null;
	
	String tocSidePanelXpath = "//div[@id='tableOfContents']//li[contains(@id, 'toc')]//ul";
	String tocBodyXpath = "//p[@class='MsoToc1']";	

	String structuredWordDocXPathFormat = "//table[contains(@id,'divCL')]//tr[td["+POS_DEFAULT_BROWSE_TITLE
	    +"]//text()[contains(.,'%s')]]/td/a[img[contains(@src, 'book.png')]]";

	String tikaImgXPath = "//div[@id='gs-document-text']//img[contains(@id,'Picture 1')]";


	switch(nthPreview) { // check no structure to html versions of word documents
	case 1:
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");
	    // find all word documents and visit them
	    // check that //div[@id='gs-document-text'][//p or div] exists (a gs-doc-txt div with p or div
	    // descendants should exist)
	    // vs how //div[@id='gs-document-text'][//h1 or h2 or h3 or h4 or h5 or h6]//p or div] doesn't
	    // exist (no gs-doc-txt div with headings as descendants should exist

	    int numWordDocs = 4;
	    String flatWordDocXpath = "//table[contains(@id,'divCL')]//tr[td/a/img[contains(@src, 'imsword.gif')]]/td/a[img[contains(@src, 'itext.gif')]]";
	    //flatWordDocXpath = "//table[contains(@id,'divCL')]//td//img[contains(@src, 'imsword.gif')]";

	    // Finds all links to the html versions of word docs
	    List<WebElement> docLinks = findElementsByXPath(_driver, flatWordDocXpath);
	    errMsg = "### ASSERT ERROR: Reports collection's Title classifier doesn't have " + numWordDocs
		+ " but " + docLinks.size() + " word docs.";	    
	    Assert.assertEquals(errMsg, numWordDocs, docLinks.size());
	    System.err.println("@@@ Reports - Title classifier has " + numWordDocs + " .doc word documents");

	    String shouldExistXpath = "//div[@id='gs-document-text'][.//p or .//div]";
	    String shouldNotExistXpath = "//div[@id='gs-document-text'][.//h1 or .//h2 or .//h3 or .//h4 or .//h5 or .//h6]";

	    for(int i = 0; i < numWordDocs; i++) {
		docLinks.get(i).click();
		waitForPageLoad();
		// For each loaded word doc, click on GS3 doc link, and check there are no headings but there are
		// p or div descendants to //div[@id='gs-document-text']
		getElement(shouldExistXpath);
		System.err.println("@@@ word doc "+i+": div[id=gs-document-text] contains div and para elements");
		elementShouldNotExist(shouldNotExistXpath);
		System.err.println("@@@ word doc "+i+": div[id=gs-document-text] has no heading elements");

		// go back
		_driver.navigate().back();
		waitForPageLoad();

		// docLinks array is stale as we visited another page, so get docLinks again
		docLinks = findElementsByXPath(_driver, flatWordDocXpath);
	    }
	    break;

	case 2:
	    // check word03.doc and word06.doc now have a bookshelf icon
	    int numStructuredWordDocs = 2;
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");
	    xPath = "//table[contains(@id,'divCL')]//tr[td/a/img[contains(@src, 'imsword.gif')]]/td/a[img[contains(@src, 'book.png')]]";
	    elems = findElementsByXPath(_driver, xPath);
	    errMsg = "### ASSERT ERROR: Reports collection's Title classifier should now have "
		+ numStructuredWordDocs + " structured word docs, but found there were " + elems.size();
	    Assert.assertEquals(errMsg, numStructuredWordDocs, elems.size());
	    System.err.println("@@@ Title classifier confirms " + numStructuredWordDocs + " *structured* word docs (denoted by book icon)");

	    int numUnstructuredWordDocs = 2;
	    xPath = "//table[contains(@id,'divCL')]//tr[td/a/img[contains(@src, 'imsword.gif')]]/td/a[img[contains(@src, 'itext.gif')]]";
	    elems = findElementsByXPath(_driver, xPath);
	    errMsg = "### ASSERT ERROR: Reports collection's Title classifier should still have "
		+ numUnstructuredWordDocs + " *unstructured* word docs, but found there were " + elems.size();
	    Assert.assertEquals(errMsg, numUnstructuredWordDocs, elems.size());
	    System.err.println("@@@ Title classifier confirms " + numUnstructuredWordDocs + " *unstructured* word docs still (denoted by textfile icon)");
	    break;
	case 3:
	    String[] docTitles = {"word05.doc", "word06.doc"};

	    // word05.doc should also be hierarchically structured now (and have a bookshelf icon)
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");	    

	    for(String docTitle : docTitles) {
		// tests word05.doc uses book.png now too (tests word06 again too, but xPath used to get link too)
		docLink = getElement(String.format(structuredWordDocXPathFormat, docTitle));
		System.err.println("@@@ Titles classifier shows doc " + docTitle + " has a book icon");
		
		// check table of contents for word05 shows more than 1 section
		// And check word06 has a table of contents side-panel too
		docLink.click();
		waitForPageLoad();

		
		elems = findElementsByXPath(_driver, tocSidePanelXpath);
		errMsg = "### ASSERT ERROR: "+docTitle
		    +" should now be structured. Expected >= 1 section but only has " + elems.size();
		Assert.assertTrue(errMsg, elems.size() >= 1);
		System.err.println("@@@ " + docTitle + " has a sidepanel table of contents section");

		if(docTitle.equals("word05.doc")) {
		    // go back
		    _driver.navigate().back();
		    waitForPageLoad();
		} else { // word06.doc: we're still viewing the actual document
		    // test its text body has an extra table of contents section, as it will be removed
		    // for the next test case

		    //String tocBodyXpath
		    //   = "//div[@class='WordSection1']/p//span[text()[contains(., 'Table of Contents')]]";
		    //getElement(tocBodyXpath);		    
		    
		    elems = findElementsByXPath(_driver, tocBodyXpath);

		    int numSectionsInBody = 5;
		    errMsg = "### ASSERT ERROR: " + docTitle + " should have a table of contents in doc body too"
			+ "\n\twith " + numSectionsInBody + " elements (for xpath " + tocBodyXpath
			+ ")\n\tbut has only " + elems.size();
		    Assert.assertEquals(errMsg, numSectionsInBody, elems.size());
		    System.err.println("@@@ Document " + docTitle
				       + " has a table of contents in its body too, with " + numSectionsInBody
				       + " sections.");
		}
	    }
	    break;
	case 4:
	    // Check word06.doc no longer has a TOC in its body by redoing last test of previous test case
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");
	    docLink = getElement(String.format(structuredWordDocXPathFormat, "word06.doc"));	    
	    
	    // Check word06 no longer has table of contents links in its body
	    docLink.click();
	    waitForPageLoad();
	    elems = _driver.findElements(By.xpath(tocBodyXpath)); // findElementsByXPath will return null
	    errMsg = "### ASSERT ERROR: word06 doc body should have 0 table of contents links, but has "
		+ elems.size();
	    Assert.assertEquals(errMsg, 0, elems.size());
	    System.err.println("@@@ word06.doc no longer has table of contents sections in its doc body.");
	    break;
	case 5: // extra final step in basic reports collection
	    int[] expectedNumDocsInBookShelf = { 2, 1, 2, 1, 2, 1, 2, 1,
						 1, 1, 2, 1, 1, 1, 1, 5 };
	    numAuthors = expectedNumDocsInBookShelf.length; // 16
	    final String[] bookshelfTitles = { "David Bainbridge",
					       "Stefan Boddie",
					       "Stefan J. Boddie",
					       "Zane Bray",
					       "Sally Jo Cunningham",
					       "David M. Nichols, Michael B. Twidale",
					       "dg5",
					       "Stuart M. Dillon",
					       "Ian Witten, Stefan Boddie",
					       "Malika Mahoui",
					       "Rodger J. McNab",
					       "David M. Nichols",
					       "W.J. Teahan",
					       "Michael B. Twidale",
					       "Yong Wang",
					       "Ian H. Witten"};	    
	    checkLeafNodesOfClassifier(authorClassifierID,
				       authorsLabel,
				       numAuthors, //expectedNumBookShelves,
				       expectedNumDocsInBookShelf,
				       true, // check leaf node numbering
				       bookshelfTitles);
	    break;
	case 6:
	    int[] newExpectedNumDocsInBookShelf = { 2, 1, 1, 1, 4 };
	    numAuthors = newExpectedNumDocsInBookShelf.length; // 5
	    final String[] newBookshelfTitles =  { "Sally Jo Cunningham",
						   "dg5", // new
						   "Michael B. Twidale",
						   "Yong Wang",
						   "Ian H. Witten"};
	    checkLeafNodesOfClassifier(authorClassifierID,
				       authorsLabel,
				       numAuthors, //expectedNumBookShelves,
				       newExpectedNumDocsInBookShelf,
				       true, // check leaf node numbering
				       newBookshelfTitles);

	    //TODO: GS3 search broken? Searching authors for an author name (e.g. Dillon) produces results
	    // but no results for Author dg5.
	    // Not broken: just need to edit the creators search index to include both dc.Creator and ex.Creator

	    // In preparation for next step
	    GSSeleniumUtil.checkNumDocs(numDocsInTitleClassifier, "Title", "CL1");
	    break;
	case 7: // docx with windows_scripting
	    // Check the new docx was added and has structure and an image
	    // With the new docx file added, there should now be 10 docs under Titles
	    numDocsInTitleClassifier++;
	    GSSeleniumUtil.checkNumDocs(numDocsInTitleClassifier, "Title", "CL1");	    

	    docLink = getElement(String.format(structuredWordDocXPathFormat, newDocTitle));
	    System.err.println("@@@ Titles classifier shows newly added doc "
			       + newDocTitle + " (and it has a book icon).");
	    
	    // check table of contents for new doc shows more than 1 subsection
	    docLink.click();
	    waitForPageLoad();	    
	    
	    elems = findElementsByXPath(_driver, tocSidePanelXpath);
	    errMsg = "### ASSERT ERROR: " + newDocTitle
		+" should be structured. Expected >= 1 section but only has " + elems.size();
	    Assert.assertTrue(errMsg, elems.size() >= 1);
	    System.err.println("@@@ " + newDocTitle + " is sectioned: has a sidepanel with table of contents");

	    // Check the docx converted to HTML has an image (the conversion appears to assign numbered 
	    // ids to them as "Picture [1-n]".
	    getElement(tikaImgXPath);
	    System.err.println("@@@ The HTML version of the docx file " + newDocTitle + " contained an image");
	    break;
	case 8: // docx with UnknownConventerPlugin set to use tika
	    // testword.docx has now been processed by Tika: check text doc icon, doc contains no structure
	    // and no image. Do a search for text however.
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");

	    // This time the greenstone doc icon should not be a book but a text icon
	    String flatDocXFileXPathFormat = structuredWordDocXPathFormat.replace("book.png", "itext.gif");
	    
	    docLink = getElement(String.format(flatDocXFileXPathFormat, newDocTitle));
	    System.err.println("@@@ Titles classifier shows " + newDocTitle + " isn't sectioned: has text icon.");

	    // check there's no table of contents sidepanel this time
	    docLink.click();
	    waitForPageLoad();

	    // Check doc contents: there should be no TOC sidepanel and no image this time
	    errMsg = "### ASSERT ERROR: Tika-processed docx file " + newDocTitle + "should have no %s";

	    elems = _driver.findElements(By.xpath("//div[@id='tableOfContents']"));
	    Assert.assertEquals(String.format(errMsg, "table of contents"), 0, elems.size());
	    System.err.println("@@@ Checking doc contents: " + newDocTitle + " contains no table of contents");

	    elems = _driver.findElements(By.xpath(tikaImgXPath));
	    Assert.assertEquals(String.format(errMsg, "image"), 0, elems.size());
	    System.err.println("@@@ Checking doc contents: " + newDocTitle + " contains no image");
	    
	    searchAndCheckNumResults("sea lion", "text", 1); // testword.docx				 
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for enhancedReports");
	    break;
	}
    }
    public static void pdfCollection(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting pdfCollection() - preview test case #: " + nthPreview);

	final int numDocs = 4;
	final int docLinkTDnum = POS_DEFAULT_BROWSE_DOC_ICON; //2; //td2 of each row is the docLink

	int docNum = -1;
	int pageNum = -1;
	WebElement textEl = null;


	String tocSection_strFormat="//td[@class='pageSliderCol'][1]//span[@class='tocSection%s']";
	String tocSectionNumber = String.format(tocSection_strFormat, "Number");
	String tocSectionTitle = String.format(tocSection_strFormat, "Title");
	String errMsg = "### ASSERT ERROR: tocSection%s should%s be displayed in doc ";
	    
	switch(nthPreview) {
	case 1:
	    GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");
	    GSSeleniumUtil.checkNumDocs(numDocs, "Source", "CL2");
	    searchAndCheckNumResults("bibliography", "text", 1); // pdf03
	    
	    searchAndCheckNumResults("Greenstone", "text", 1); // pdf01
	    searchAndCheckNumResults("FAO", "text", 0); // no docs for image-only pdf05-notext
	    searchAndCheckNumResults("METS", "text", 0); // no results in pdf06-weirdchars
	    //searchAndCheckNumResults("!\" # $ %", "text", 1); // doesn't work searching in GS for
	    // weird char sequence in doc pdf06
	    
	    break;
	case 2:
	    GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");
	    GSSeleniumUtil.checkNumDocs(numDocs, "Source", "CL2");	    
	    // All docs are now image-only, so expect no search results for 1st two docs either
	    searchAndCheckNumResults("bibliography", "text", 0);
	    searchAndCheckNumResults("Greenstone", "text", 0);
	    // we'll do the rest of the testing in case 3 below
	    break;
	case 3:	    
	    GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");
	    // Should have searching back for first 2 docs
	    searchAndCheckNumResults("bibliography", "text", 1); // pdf03
	    searchAndCheckNumResults("Greenstone", "text", 1); // pdf01
	    searchAndCheckNumResults("FAO", "text", 0); // no docs for image-only pdf05-notext
	    searchAndCheckNumResults("METS", "text", 0);
	    
	    // Check 1st doc's 1st page has a PDF page converted to HTML image with text
	    docNum = 1;
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");
	    clickDocLink("CL1", docNum, docLinkTDnum);

	    pageNum = 1;
	    expandDocPage(pageNum);
	    textEl = checkPDFToImageWithTextPage(pageNum, docNum);
	    elemTextEquals(textEl, "Greenstone: A Comprehensive Open-Source");

	    // Sample 5th page for image loaded and image having text
	    pageNum = 5;
	    clickPageinFlatTOC(pageNum);
	    textEl = checkPDFToImageWithTextPage(pageNum, docNum);

	    // Check 2nd doc's 1st page (this time, section 1.1 is page 1)
	    docNum = 2;
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");
	    clickDocLink("CL1", docNum, docLinkTDnum);
	    pageNum = 1;
	    
	    expandDocPage("1.1"); // pages are nested in intermediate bookshelf
	    
	    textEl = checkPDFToImageWithTextPage(pageNum, docNum);
	    elemTextEquals(textEl, "Applications for Bibliometric Research");
	    
	    // Open section 2 page 5
	    clickPageinSectionedTOC(2, 5);	    

	    // actual page loads, check it
	    pageNum = 15; // page 5 of section 2 has pageNum 15
	    textEl = checkPDFToImageWithTextPage(pageNum, docNum);

	    
	    // Check doc3 and doc4 now, they should be image-only:
	    
	    // First check doc 3 page 1
	    docNum = 3;
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");
	    clickDocLink("CL1", docNum, docLinkTDnum);
	    pageNum = 1;
	    //expandDocPage(pageNum); // page1 opened by default as it's not a book but paged doc?
	    String imgXpath = getImgXpathOnImgOnlyPage(pageNum);
	    GSSeleniumUtil.checkImageLoaded(imgXpath, getElement(imgXpath));

	    // Prep for test 4: both tocSectionNumber and tocSectionTitle should exist in doc 3
	    Assert.assertTrue(String.format(errMsg, "Number", " not"),
			      getElement(tocSectionNumber).isDisplayed());
	    Assert.assertTrue(String.format(errMsg, "Title", " not"),
			      getElement(tocSectionTitle).isDisplayed());
	    
	    // test doc 3 page 5 as well
	    // wait for page slider to load
	    //PAUSE(3);
	    pageNum = 5;
	    clickPageInImageSlider(pageNum); //clickLink("//div[contains(@class, 'pageSlider')]//td[@class='pageSliderCol']["+pageNum+"]");	    
	    imgXpath = getImgXpathOnImgOnlyPage(pageNum);
	    GSSeleniumUtil.checkImageLoaded(imgXpath, getElement(imgXpath));

	    // just test doc 4 page 1 to be image only
	    docNum = 4;
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");
	    clickDocLink("CL1", docNum, docLinkTDnum);
	    pageNum = 1;
	    //expandDocPage(pageNum); //page 1 is opened by default
	    imgXpath = getImgXpathOnImgOnlyPage(pageNum);
	    GSSeleniumUtil.checkImageLoaded(imgXpath, getElement(imgXpath));
	    
	    // test doc 3 page 12 as well, to see scrolling in thumbnail slider to the last page
	    // Works but waste of time: ends up testing more of the testing code's scrolling code
	    // instead of testing Greenstone's built collection
	    /*
	    pageNum = 12;
	    clickPageInImageSlider(pageNum); //clickLink("//div[contains(@class, 'pageSlider')]//td[@class='pageSliderCol']["+pageNum+"]");	    
	    imgXpath = getImgXpathOnImgOnlyPage(pageNum);
	    GSSeleniumUtil.checkImageLoaded(imgXpath, getElement(imgXpath));
	    */
	    break;
	case 4:
	    // Check in docc#3 (pdf05) that sectionNumber no longer exists
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");
	    docNum = 3;
	    clickDocLink("CL1", docNum, docLinkTDnum);


	    PAUSE(TIME_FOR_PAGE_LOAD); // TODO: wait for page slider to load
	    Assert.assertFalse(String.format(errMsg + docNum, "Number", ""),
			       getElement(tocSectionNumber).isDisplayed());
	    Assert.assertTrue(String.format(errMsg + docNum, "Title", " not"),
			      getElement(tocSectionTitle).isDisplayed()); // should still display
	    break;
	case 5:
	    // Check in docc#3 (pdf05) that sectionTitle no longer exists
	    // but sectionNumber is back
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");
	    docNum = 3;
	    clickDocLink("CL1", docNum, docLinkTDnum);
	    
	    PAUSE(TIME_FOR_PAGE_LOAD); // TODO: wait for page slider to load
	    Assert.assertTrue(String.format(errMsg + docNum, "Number", "  not"),
			      getElement(tocSectionNumber).isDisplayed()); // should display again
	    Assert.assertFalse(String.format(errMsg + docNum, "Title", ""),
			       getElement(tocSectionTitle).isDisplayed()); // not displaying now
	    break;
	case 6:
	    searchAndCheckNumResults("bibliography", "text", 1);
	    clickSearchResultDocLink(1, 3); // click 1st search result row's 3rd cell (src link)
	    System.err.println("*** TODO: PDF query highlighting not tested");
	    PAUSE(5);
	    break;
	default:	    
	    System.err.println("### Unknown preview test number "
			       + nthPreview + " for pdfcollection");
	    break;
	}
    }
    private static String getImgXpathOnImgOnlyPage(int pageNum) {
	// actually want one div element to start with 'doc' and end with '.<pageNum>'
	// and the second div to start with 'image' and end with '.<pageNum>'
	// But complicated to do startswith/endswith in older XML
	return "//div[contains(@id, 'doc') and contains(@id,'."+pageNum+"')]"
	    +"/div[contains(@id, 'image') and contains(@id, '."+pageNum+"')]/img";
    }
    private static WebElement checkPDFToImageWithTextPage(int pageNum, int docNum)
    {	
	// We're now viewing page pageNum of the doc
	// Check this doc (e.g. at this page) has image with selectable text
	String pageXpath = "//div[@id='page"+pageNum+"']";
	
	String imgXpath = pageXpath + "/div[1]//img[@id='background"+pageNum+"']";
	WebElement pageImage = GSSeleniumUtil.getElement(imgXpath);	    
	GSSeleniumUtil.checkImageLoaded(imgXpath, pageImage);
	
	// For first 2 docs' pageXpath above, its /div1/div[1-n] must have class="txt"
	// for selectable text. Just check div[2]
	String selectableTextXpath = pageXpath + "/div[1]/div[1][@class='txt']";	    
	WebElement textEl = GSSeleniumUtil.getElement(selectableTextXpath);
	// could assert textEl non-null and/or check its text contents	
	if(textEl == null) {
	    Assert.fail("### ASSERT ERROR: no selectable text for pdf "
			+ docNum + "'s image at page " + pageNum);
	    //+ "\n\txpath: " + selectableTextXPath);
	    // getElement already prints this last line out on error
	}
	
	return textEl;
    }
    
    public static void associatedFiles(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting associatedFiles() - preview test case #: " + nthPreview);
	
	final int numDocs = 1;
	final int POS_BROWSE_EQUIVDOC_ICON = POS_DEFAULT_BROWSE_TITLE; // 4
	//final int POS_SHIFTED_BROWSE_TITLE = 1+POS_BROWSE_EQUIVDOC_ICON; // 5

	switch(nthPreview) {
	case 1:
	    GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");
	    GSSeleniumUtil.checkNumDocs(numDocs, "Source", "CL2");
	    // check there's an equivdoc when browsing: 3rd icon (4th td cell) must contain
	    // img src PDF_ICON/"PDF_32.png"
	    GSSeleniumUtil.loadClassifierByName(_driver, "Title");
	    checkBrowseDocIconsFormat("CL1",
		 new SimpleEntry(new Integer(POS_DEFAULT_BROWSE_DOC_ICON), "itext.gif"),
		 new SimpleEntry(new Integer(POS_DEFAULT_BROWSE_SRC_ICON), "imsword.gif"),
		 new SimpleEntry(new Integer(POS_BROWSE_EQUIVDOC_ICON), PDF_ICON));
	    // check no equivdoc yet when searching: 3rd icon (4th td cell) may not contain
	    // img src PDF_ICON/"PDF_32.png".
	    searchAndCheckNumResults("greenstone", "text", 1);
	    // There's no img icon whatever for td[3] and there's no td[4]: td[3] is the title
	    // in the default search format stmt still in use. So testing for img in td[3]
	    // itself will be like a null ptr exception: no such element descendant in td[3]
	    // (and there's no td[4] at all yet).
	    /*
	      checkSearchDocIconsFormat(
		 new SimpleEntry(new Integer(POS_DEFAULT_SEARCH_DOC_ICON), "itext.gif"),
		 new SimpleEntry(new Integer(POS_CUSTOM_SEARCH_SRC_ICON), "imsword.gif"),
		 new SimpleEntry(new Integer(POS_SEARCH_EQUIVDOC_ICON), "(?!"+PDF_ICON+")"));
	    */
	    DocDisplayStruct[] searchResultExpectations = new DocDisplayStruct[1];
	    // tableCellNum 3 should contain Bear title and description
	    searchResultExpectations[0]
		= new DocDisplayStruct(POS_DEFAULT_SEARCH_TITLE,
		 CONTAINS_MULTILINE_PATTERN,
		 new String[] {
		     "Greenstone: A comprehensive open-source digital library software system"
		 });
	    GSSeleniumUtil.checkSearchResults(numDocs,
					     "One document matches the query.",
					     "'greenstone' occurs 21 times in 1 document",
					     null, // not checking hrefs
					     searchResultExpectations);
	    break;
	case 2: // check search now has equivdoc too
	    
	    int POS_CUSTOM_SEARCH_SRC_ICON = 1+POS_DEFAULT_SEARCH_DOC_ICON; //3
	    int POS_SEARCH_EQUIVDOC_ICON = 1+POS_CUSTOM_SEARCH_SRC_ICON; //4
	    //int POS_SHIFTED_SEARCH_TITLE = 1+POS_SEARCH_EQUIVDOC_ICON; //5
	    
	    searchAndCheckNumResults("greenstone", "text", 1);
	    // check there's an equivdoc when browsing: 3rd icon (4th td cell) must contain
	    // img src PDF_ICON/"PDF_32.png"
	    checkSearchDocIconsFormat(
		 new SimpleEntry(new Integer(POS_DEFAULT_SEARCH_DOC_ICON), "itext.gif"),
		 new SimpleEntry(new Integer(POS_CUSTOM_SEARCH_SRC_ICON), "imsword.gif"),
		 new SimpleEntry(new Integer(POS_SEARCH_EQUIVDOC_ICON), PDF_ICON));
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			       + " for assocFiles collection.");
	    break;
	}
    }
    
    public static void tudor(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting tudor() - preview test case #: " + nthPreview);

	final int numDocs = 151; // split over multiple HLists	
	
	switch(nthPreview) {
	case 1:
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs); // checks on coll's main page

	    //count(//table[@id="divCL2.4"]//tr)
	    String[] title_hlist = {"a-l", "m", "pr", "pri", "pri", "q-r", "s", "tu", "tu", "w"};
	    int[] title_hlist_numdocs = { 20, 5, 18, 18, 17, 8, 21, 22, 21, 1 };
	    String[] source_hlist = { "a-b", "c-e", "f-i", "j-l", "m-n", "p-s", "t", "w", "1535"};
	    int[] source_hlist_numdocs = { 22, 22, 21, 21, 14, 24, 24, 2, 1 };
	    
	    loadClassifierByName(_driver, "Title");
	    //GSSeleniumUtil.checkNumDocs(title_hlist_numdocs[0], "Title", "CL1.1");
	    GSSeleniumUtil.checkHorizontalClassifiers("CL1", title_hlist, title_hlist_numdocs);

	    loadClassifierByName(_driver, "Source");
	    //GSSeleniumUtil.checkNumDocs(source_hlist_numdocs[0], "Source", "CL2.1");
	    GSSeleniumUtil.checkHorizontalClassifiers("CL2", source_hlist, source_hlist_numdocs);
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for tudor");
	    break;
	}
    }
    
    public static void tudor2_enhanced(int nthPreview, Object moreParams) {
	System.err.println("@@@ BrowserTesting tudor2_enhanced() - preview test case #: " + nthPreview);

	final int numDocs = 151; // split over multiple HLists	
	final String subjectClassifierID = "CL2";
	
	switch(nthPreview) {
	case 1:
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs); // checks on coll's main page
	    loadClassifierByName(_driver, "Subject");

	    expandBookshelf(1, subjectClassifierID); // opens up CL2.1
	    final String tudorPeriodClassifierID = subjectClassifierID+".1"; // "CL2.1";
	    //expandAllBookshelves(tudorPeriodClassifierID); // 4 nodes under CL2.1

	    int[] expectedNumDocsInBookShelf = { 5, 20, 117, 9 };
	    final int expectedNumBookShelves = expectedNumDocsInBookShelf.length;
	    final String[] bookshelfTitles = { "Citizens", "Monarchs", "Others", "Relatives" };
	    // pass true to check num leaf node indicator is showing and showing correct value
	    checkLeafNodesOfClassifier(tudorPeriodClassifierID,
				       "Tudor period",
				       expectedNumBookShelves,
				       expectedNumDocsInBookShelf,
				       false,
				       bookshelfTitles);
	    break;
	case 2:
	    loadAndWaitForPhindClassifier("Phrase browse");	    
	    searchPhind("king");
	    System.err.println("*** TODO tudor2_enhanced: testing Phind classifier search results not yet implemented");
	    break;
	case 3:
	    searchAndCheckSummaryResults("Mary", "text", "relatives", 365, 7); //365 hits in 7 docs
	    searchAndCheckSummaryResults("Mary", "text", "monarchs", 338, 16);
	    break;
	case 4:
	    String allPartitionSorted = (String)moreParams;
	    // check the new partition index: long name version
	    //checkPartitionExists("citizens,monarchs,others,relatives");
	    checkPartitionExists(allPartitionSorted);
	    break;
	case 5:
	    searchAndCheckSummaryResults("Mary", "text", "citizens", 110, 5);	    
	    searchAndCheckSummaryResults("Mary", "text", "others", 483, 71);
	    searchAndCheckSummaryResults("Mary", "text", "all", 1296, 99);
	            // 1296 is also the sum of all the hits for "Mary" in above statements
		// If we got here, then no assertion failed:
		System.err.println("@@@ Total number of words found for 'Mary' across individual indexes "
			+ "matched 'all' index: " + 1296);
	    break;
	case 6: // check maxdocs option set to 3 had expected effect:
	    final int maxDocs = 3;
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(maxDocs);
	    // doublecheck: check a classifier confirms the number of docs = 3
	    loadClassifierByName(_driver, "Title");
	    GSSeleniumUtil.checkNumDocs(maxDocs, "Title", "CL1");
	    break;
	case 7: // check we're now be back to original number of docs
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for tudor2.");
	    break;
	}
    }

    public static void tudor3_formatting(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting tudor3_formatting() - preview test case #: " + nthPreview);

	final int numDocs = 151; // split over multiple HLists	
	final int numBooks = 5;
	
	final String titleClassifierID = "CL1";
	final String subjectClassifierID = "CL2";
	
	final String hListAtoL_classifierID = titleClassifierID + ".1"; //CL1.1	
	
	final String docTitle = "A discussion of question five from Tudor Quiz: Henry VIII";
	final String srcText = "(quizstuff.html)";
	WebElement row = null;
	String xPath = null;
	
	switch(nthPreview) {	
	case 1: case 2:
	    loadClassifierByName(_driver, "Title");

	    // Note: we want to select the html *row*, <tr> containing the td with matching text:
	    // https://stackoverflow.com/questions/4608097/xpath-to-select-a-table-row-that-has-a-cell-containing-specified-text
	    // The structure is actually div[id='CL1.1']/tr/td/table[id='div...']/**tr**/td/a/i
	    // We want the tr marked **tr** in the above approximate structure
	    // Note: in the part that says tr[td....[contains()]], make sure the starting element
	    // in the first set of [] does not begin with // or even /, as // takes you back
	    // to matching from root element.
	    xPath = "//table[@id='div"+hListAtoL_classifierID
		+"']//tr[td/a/i[contains(text(), '"
		+srcText+"')]]";
		//"//table[@id='div"+hListAtoL_classifierID
		//+"']//tr//table[contains(@id, 'div')]//tr[td/a/i[contains(text(), '"
		//+"(quizstuff.html)')]]";
	    //		
	    row = getElement(xPath);
	    //System.err.println("@@@ row outerHTML: " + row.getAttribute("outerHTML"));
	    checkDocIconsFormatForRow(row, new SimpleEntry(new Integer(2), "itext.gif"));
	    
	    // For case 1, titleTD is in cell 4 and td 3 is empty but takes up space
	    int titleTD = (nthPreview == 1)
		? POS_DEFAULT_BROWSE_TITLE : (POS_DEFAULT_BROWSE_TITLE-1); // ? 4 : 3

	    DocDisplayStruct titleFormat = new DocDisplayStruct(
		     titleTD,
		     CONTAINS_MULTILINE_PATTERN,
		     new String[] {
			 docTitle,
			 srcText
		     });
	    checkDocDisplayOfRowMatches(row, titleFormat);
	    break;
	case 3:
	    loadClassifierByName(_driver, "Subject");
	    expandBookshelves("Tudor period", "Others"); //expandBookshelves("Tudor period", "Citizens");
	    
	    // In the Subjects classifier, the same document denoted by docTitle
	    // should be in a tdCell that contains the exact text of docTitle, no srcText (srcfile)
	    xPath = "//table[@id='div"+subjectClassifierID//+".1"
		//+"']//table[contains(@id, 'div')]" // another table
		+ "']//tr[td/a/text()='"+docTitle+"']";
	    row = findElementByXPath(_driver, xPath);
	    if(row == null) {
		Assert.fail("### ASSERT ERROR: Could not find xPath " + xPath
			    + "\nin Subject classifier (under opened Tudor period > Others)");
			    //+ "\n\trow outerHTML: " + row.getAttribute("outerHTML"));
	    }
	    
	    break;
	case 4:
	    // search for the same document tested in case 1/2
	    GSSeleniumUtil.performQuickSearch(_driver, "\""+docTitle+"\"", "titles");

	    final int numExpectedResults = 1;
	    // 1 result, should be the doc whose exact title we searched for
	    DocDisplayStruct[] searchResultExpectations = new DocDisplayStruct[1];
	    // POS_DEFAULT_SEARCH_TITLE (tdCellNum 3) should contain Bear title and description
	    searchResultExpectations[0]	= new DocDisplayStruct(POS_DEFAULT_SEARCH_TITLE, // 3
							       CONTAINS_MULTILINE_PATTERN,
							       new String[] {
								   docTitle,
								   "Tudor period|Others"
							       });
	    GSSeleniumUtil.checkSearchResults(numExpectedResults,
					     "One document matches the query.",
					     null, // search term breakdown
					     null, // hrefs
					     searchResultExpectations);

	    // check doc icon is in 2nd td cell (1st cell is favourites/star icon)
	    checkSearchDocIconsFormat(new SimpleEntry(new Integer(POS_DEFAULT_SEARCH_DOC_ICON), "itext.gif")); // position=2
	    
	    break;
	case 5: // Test bookshelves start with "Bookshelf title:" and docNodes with "Title:"
	    loadClassifierByName(_driver, "Subject");
	    expandAllBookshelves(subjectClassifierID);
	    expandAllBookshelves(subjectClassifierID+".1"); // expand all bookshelves in next level

	    final String bookshelfIDPrefix = "titleCL";
	    final String docNodeIDPrefix = "divHASH";
	    final int POS_CUSTOM_BROWSE_TITLE = 3;
	    
	    String xpathFormat
		//= "//table[contains(@id, '%s')]//tr/td["+POS_BOOKSHELF_TITLE+"]/b/text()[contains(., '%s')]"; // Exception: org.openqa.selenium.JavascriptException: Cyclic object value
		= "//table[contains(@id, '%s')]//tr/td[%d]/b[contains(text(),'%s')]";
	    xPath = //"//table[contains(@id, 'titleCL')]//tr/td["+POS_BOOKSHELF_TITLE+"]";
		//"//table[contains(@id, 'titleCL')]//tr/td["+POS_BOOKSHELF_TITLE+"]/b/text()[contains(., 'Bookshelf title:')]";
		String.format(xpathFormat, "titleCL", POS_BOOKSHELF_TITLE, "Bookshelf title:");
	    //System.err.println("@@@ xpath: " + xPath);
	    List<WebElement> bookNodeTitleCells = findElementsByXPath(_driver, xPath);
	    Assert.assertEquals(
	       "### Assert Error: expected all "+numBooks+" booknodes to start 'Bookshelf title:'",
	       numBooks, bookNodeTitleCells.size());

	    System.err.println("@@@ All " +numBooks+ " bookshelves start with 'Bookshelf Title:'");
	    
	    xPath = //"//table[contains(@id, 'divHASH')]//tr/td["+POS_CUSTOM_BROWSE_TITLE+"]";
		//"//table[contains(@id, 'divHASH')]//tr/td["+POS_CUSTOM_BROWSE_TITLE+"]/b/text()[contains(., 'Title:')]";
		String.format(xpathFormat, "divHASH", POS_CUSTOM_BROWSE_TITLE, "Title:");
	    //System.err.println("@@@ xpath: " + xPath);
	    List<WebElement> docNodeTitleCells = findElementsByXPath(_driver, xPath);

	    Assert.assertEquals(
		"### Assert Error: expected all "+numDocs+" doc titles to start with 'Title:'",
		numDocs, docNodeTitleCells.size());
	    System.err.println("@@@ All "+numDocs+" doc nodes start with 'Title:'");
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for tudor3.");
	    break;
	}
    }

    public static void tudor4_weblink() {
	System.err.println("@@@ BrowserTesting tudor4_weblink() - preview test case #: 1/1");

	final String titleClassifierID = "CL1";
	
	// CL1 (Title classifier) should have different icons now
	// and these *icons* are external links	

	// doc icon removed, so shift src icon and title position down by 1
	int CUSTOM_POS_BROWSE_SRC_ICON = POS_DEFAULT_BROWSE_SRC_ICON-1;
	int CUSTOM_POS_BROWSE_TITLE = POS_DEFAULT_BROWSE_TITLE-1;
	
	loadClassifierByName(_driver, "Title");
	checkBrowseDocIconsFormat("CL1",
		  new SimpleEntry(new Integer(POS_DEFAULT_BROWSE_DOC_ICON), "iworld.gif"));
	SimpleEntry extLink = new SimpleEntry("href", "http://englishhistory.net/.*");
					      //"http://englishhistory.net/tudor/.*"); // index.html is at same level as tudor folder
	DocDisplayStruct docDisplayFormat = new DocDisplayStruct(
		     CUSTOM_POS_BROWSE_SRC_ICON, //td 2 is webicon and should link to offsite link		     
		     extLink);
	checkDocDisplayMatches(ALL_ROWS, titleClassifierID, docDisplayFormat);

	// check CL1 doc titles still link to greenstone (internal) doc URLs
	SimpleEntry localLink = new SimpleEntry("href", ".*library/collection/tudor/document.*");
	docDisplayFormat = new DocDisplayStruct(
		     CUSTOM_POS_BROWSE_TITLE, //doc title link at td3 should still point to greenstone doc URL
		     localLink);
	checkDocDisplayMatches(ALL_ROWS, titleClassifierID, docDisplayFormat);
    }

    public static void sectionTaggingForHTML() {
	System.err.println("@@@ BrowserTesting sectionTaggingForHTML() - preview test case #: 1/1");

	loadClassifierByName(_driver, "Title");
	// click on "F" hListItem (for document "Farming Snails 1...")
	clickHorizontalClassifier("F"); // will be converted to lowercase internally
	clickDocLink_docTitleContains("Farming snails 1");
	
	String newlyAddedSectionTitle = "Snails are good to eat.";
	
	expandDocPage("1.1"); // expand section 1.1 of the doc (Introduction.1)

	String xPath = "//td[@id='headerfb33fe.1.1']";
	    //"//td[@id='headerfb33fe.1.1' and contains(text(), '"+newlyAddedSectionTitle+"')]";
	WebElement td = getElement(xPath);
	String errMsg = String.format("### ASSERT ERROR: Expected section title %s, got %s",
				      newlyAddedSectionTitle, td.getText());
	Assert.assertEquals(errMsg, newlyAddedSectionTitle, td.getText());
	System.err.println("@@@ Newly added section " + newlyAddedSectionTitle + " found.");
    }

    public static void downloadingTudorFromWeb() {
	System.err.println("@@@ BrowserTesting downloadingFromWeb() - preview test case #: 1/1");

	final int numDocs = Platform.isWindows() ? 17 : 22;
	GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	loadClassifierByName(_driver, "Title");
	
	// 5th row onwards are docs with some content. Click 5th row's doc icon
	clickDocLink("CL1", 5, POS_DEFAULT_BROWSE_DOC_ICON);
	
	WebElement heading = getElement("//h1[@class='entry-title']");

	//No ordering to these documents, so any of the following titles are possible
	String expectedTitle = "(Thomas Cranmer: The First Protestant Archbishop of Canterbury"
	    + "|Robert Dudley, 1st Earl of Leicester"
	    + "|Sir Thomas More: Biography, Facts and Information"
	    + "|Sir Francis Bacon – A Philosopher and Statesman Who Changed the World"
	    + "|Sir Robert Cecil, Earl of Salisbury (1563 – 1612)"
	    + "|Walter Raleigh"
	    + "|Thomas Wolsey: Biography, Portrait, Facts & Information"
	    + "|Margaret Pole"
	    + "|Thomas Culpeper"
	    + "|George Boleyn, Viscount Rochford"
	    + "|Edmund de la Pole"
	    + "|Sir Francis Drake"
	    + "|Mary Boleyn: Biography, Portrait, Facts & Information"
	    + "|Robert Devereux, 2nd Earl of Essex)";
	String errMsg = String.format("### ASSERT ERROR: Expected h1.entry-title %s, got %s",
				      expectedTitle, heading.getText());
	Assert.assertTrue(errMsg, heading.getText().matches(expectedTitle));
	System.err.println("@@@ Newly added section " + expectedTitle + " found.");
    }

    
    public static void bibliographicColl_MARC(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting bibliographicColl_MARC() - preview test case #: " + nthPreview);

	final int numDocs = 234;
	
	//https://stackoverflow.com/questions/23543044/how-to-use-xpath-preceding-sibling-correctly
	final String MARCtitlexPath = "//div[@id='gs-document']/preceding-sibling::span";
	String expectedTitle = null;
	final String subjectClassifierID = "CL2";

	// reused local variables
	List<WebElement> options;
	WebElement el;
	String errMsg;
	    
	switch(nthPreview) {	
	case 1: case 6:
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	    loadClassifierByName(_driver, "Title");
	    
	    // Checking 2 docs: let's try the first doc under letter A
	    clickDocLink("CL1", 1, POS_DEFAULT_BROWSE_TITLE);
	    expectedTitle = "Accuracy bounds for ensembles under 0 - 1 loss / Remco R. Bouckaert.";
	    //checkElementTextEquals("title",
	    //"//preceding-sibling::div[@id='gs-document']", // title
	    //expectedTitle);
	    
	    equalsText(MARCtitlexPath, expectedTitle);
	    System.err.println("@@@ Found 1st doc with title " + expectedTitle);
	    
	    // check another doc: 3rd doc under Com-Cur
	    loadClassifierByName(_driver, "Title");
	    clickHorizontalClassifier("Com-Cur");
	    clickDocLink("CL1", 3, POS_DEFAULT_BROWSE_DOC_ICON);
	    expectedTitle = "Computer concepts without computers : a first course in computer science / by Geoffrey Holmes, Tony C. Smith and William J. Rogers.";
	    equalsText(MARCtitlexPath, expectedTitle);
	    System.err.println("@@@ Found 3rd doc under Com-Cur with title " + expectedTitle);

	    // Search full text index for "graphics"
	    if(nthPreview == 1) {
		searchAndCheckNumResults("graphics", "text", 6);
	    } else if(nthPreview == 6) {
		String[] queries = { "graphics" };
		String[] indexes = { "allfields" }; 
		performFormSearch(_driver, "FieldQuery", queries, indexes, null);
		checkSearchResults(6, null, null, null, null);
	    }
	    expectedTitle = "Tree browsing / by Mark Apperley and Michael Chester.";
	    clickSearchResultDocLink_docTitleContains(expectedTitle);
	    equalsText(MARCtitlexPath, expectedTitle);
	    System.err.println("@@@ Found search result (3rd) for 'graphics' with title "
			       + expectedTitle);


	    // extra test for case 6 - check 1st doc of subjects classifier, CL2.1
	    if(nthPreview == 6) {
		loadClassifierByName(_driver, "Subject");
		expandBookshelf(1, subjectClassifierID+".1"); // CL2.1.1
		clickDocLink_docTitleContains("00000140.nul"); // expect such a doc to exist
	    }
	    
	    break;
	case 2:	
	    loadClassifierByName(_driver, "Subject");
	    //Subjects (CL2): under A-B (CL2.1), 2nd bookshelf (CL2.1.2) contains multiple docs
	    expandBookshelf(2, subjectClassifierID+".1"); // opens up CL2.1.2

	    // pass true to check num leaf node indicator is showing and showing correct value
	    checkLeafNodesOfBookshelf(subjectClassifierID+".1", // CL2.1
				      2, // check CL2.1.2
				      "Agriculture Data processing.", // title of bookshelf
				      5, // expect 5 children
				      null,
				      POS_DEFAULT_BROWSE_TITLE);

	    final String[] bookshelfTitles = {
			"Bayesian network classifiers in Weka / Remco Bouckaert.",
			"A decision tree-based attribute weighting filter for naive Bayes / Mark Hall.",		
			"Experiences with a weighted decision tree learner / by John G. Cleary and Leonard E. Trigg.",
			"Naive Bayes for regression / by Eibe Frank ... \\[et al.\\]."
	    };
	    checkLeafNodesOfBookshelf(subjectClassifierID+".1", // CL2.1
				      -1, // unknown row number, CL2.1.?
				      "Bayesian statistical decision theory.", // title of bookshelf
				      4, // expect 4 children
				      bookshelfTitles,
				      POS_DEFAULT_BROWSE_TITLE);
	    
	    // Check TextQuery button (labelled text search) exists
	    getQueryButton("TextQuery"); // assertion will fail if it doesn't exist
	    
	    // preparation?? for next test: click on Fielded Search button // TODO: unnecessary
	    clickQueryButton("FieldQuery");
	    
	    break;
	case 3:
	    // Check search box no longer exists on main page
	    checkDoesNotExist("//div[@id='quicksearcharea']/form/span[@class='querybox']");
	    // Check textquery button no longer exists
	    checkDoesNotExist("//div[@id='quicksearcharea']//a[contains(@href, '/TextQuery')]");
	    
	    clickQueryButton("FieldQuery");
	    waitForPageLoad();
	    
	    List<WebElement> dropdown_searchInFields
		= findElementsByXPath(_driver, "//td[@class='queryfieldcell']/select[@name='s1.fqf']");	    
	    el = dropdown_searchInFields.get(0);
	    options = el.findElements(By.xpath("option"));
	    Assert.assertEquals("### ASSERT ERROR: >2 fields to search in dropdown", 2, options.size());
	    
	    if(!options.get(0).getText().equals("text")
	       && !options.get(1).getText().equals("titles")) {
		errMsg = String.format("### ASSERT ERROR: fielded search offers %s and %s "
					      + " fields to search in, instead of text and titles",
				       options.get(0).getText(),
				       options.get(1).getText());
		Assert.fail(errMsg);
	    }

	    
	    break;
	case 4:	    
	    clickQueryButton("FieldQuery");
	    waitForPageLoad();

	    // Just get the first matching queryfieldrow
	    el=findElementByXPath(_driver, "//td[@class='queryfieldcell']/select[@name='s1.fqf']");

	    options = el.findElements(By.xpath("option"));
	    Assert.assertEquals("### ASSERT ERROR: There should be 3 fields to search in dropdown", 3, options.size());
	    if(!options.get(2).getText().equals("subjects")) {//if(!options.get(2).getText().equals("_labelSubject_")) {
		errMsg = String.format("### ASSERT ERROR: 3rd field in fielded search is %s"
				       + " and not 'subjects' as expected", // '_labelSubject_'
				       options.get(2).getText());
		Assert.fail(errMsg);
	    }
	    
	    break;
	case 5:
	    // Do actual tests of this case: same number of docs as before
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);

	    // 1. Empty subjects classifier
	    loadClassifierByName(_driver, "Subject"); // CL2
	    checkNumDocs(0, "CL2");
	    
	    // 2. Searching should no longer return results
	    //clickQueryButton("FieldQuery");
	    //waitForPageLoad();
	    //searchAndCheckNumResults("graphics", "text", 0);

	    String[] queries = { "graphics" };
	    String[] indexes = { "text" }; 
	    performFormSearch(_driver, "FieldQuery", queries, indexes, null);
	    checkSearchResults(0, null, null, null, null);

	    // 3. The document display is useless as the linked .nul documents don't exist,
	    // resulting in "Not Found" messages from the server.
	    loadClassifierByName(_driver, "Title"); // CL1
	    //int docNum = 1;
	    //int docLinkTDnum = 1;
	    //clickDocLink("CL1", docNum, docLinkTDnum);
	    clickDocLink_docTitleContains("00000001.nul");

	    // In the doc page that loads, click the link to document view
	    clickLink("//a[text()='00000001.nul']");
	    
	    // In the page that loads, we expect 404 not found message as follows
	    //containsText("//h1", "HTTP Status 404 – Not Found");
	    
	    break;

        //case 6: //See under case 1
	    // classifiers should be back to normal and searching should now work
	    // Almost the same as case 1, so see under case 1
	    //break;	   
	case 7:
	    // For test case 7, POS_BROWSE_TITLE is shifted down by 1
	    // because source icons are removed by now
	    final int CUSTOM_POS_BROWSE_TITLE = POS_DEFAULT_BROWSE_TITLE-1;
		
	    loadClassifierByName(_driver, "Subject");
	    //Subjects (CL2): under A-B (CL2.1), 2nd bookshelf (CL2.1.2) contains multiple docs
	    expandBookshelf(2, subjectClassifierID+".1"); // opens up CL2.1.2

	    // pass true to check num leaf node indicator is showing and showing correct value
	    checkLeafNodesOfBookshelf(subjectClassifierID+".1", // CL2.1
				      2, // check CL2.1.2
				      "Agriculture Data processing.", // title of bookshelf
				      5, // expect 5 children
				      null,
				      CUSTOM_POS_BROWSE_TITLE);

	    // Title display in both classifiers should no longer contain the .nul filenames
		
	    expandBookshelf(1, subjectClassifierID+".1"); // 2.1.1

	    // In test/case 6, we tested that a doc with "00000140.nul" somewhere in its title
	    // existed in the expanded bookshelf. Now that format stmt changes have removed the
	    // source filename from the title display, we test that the same sourcefile name
	    // no longer occurs in any doc title of the the same bookshelf
	    noSuchLeafNodeInBookshelf("CL2.1.1", 1, "00000140.nul", CUSTOM_POS_BROWSE_TITLE);
	    
	    loadClassifierByName(_driver, "Title"); // CL1
	    //clickDocLink_docTitleContains("00000001.nul", false); // false: should not exist
	    noDocTitleContaining("00000001.nul");
	    
	    break;

	case 8:
	    System.err.println("### browser testing MARC case 8 not yet implemented");
	    loadClassifierByName(_driver, "Title"); // CL1
	    // Click on 1st document under horizontal classifier A of Titles classifier,
	    // denoted by title below and then on its document page,
	    // check the doc's title and contents are as given below
	    
	    String title = "Accuracy bounds for ensembles under 0 - 1 loss / Remco R. Bouckaert.";
	    String subject = "Algorithms., Set theory., Machine learning., Data mining.";
	    String publisher = "Hamilton, N.Z. :, Dept. of Computer Science, University of Waikato,";
	    String[] expected = {"Title:", title, "Subject:", subject, "Publisher:", publisher};
	       
	    
	    clickDocLink_docTitleContains(title);

	    // check page contains table with 3 rows x 2 columns, where each td contains:
	    // Title:\s*<title>
	    // Subject:\s*<subject>
	    // Publisher:\s*<publisher>
	    String tdXPath = "//div[@id='gs_content']//table//td";
	    checkTabularPageContents(tdXPath, expected);

	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for bibliographicColl_MARC.");
	    break;
	}
    }

    public static void CDS_ISIS(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting CDS_ISIS() - preview test case #: " + nthPreview);
		
	final int numDocs = 197;
	String errMsg;
	String tdXPath;

	// A doc we'll test often
	String expectedTitle = "Australia. Bureau of Mineral Resources";
	// choosing words that occur in each table cell of this doc's contents
	// (some are exact content)
	String[] expected = {
	    "Year", "[nd]",
	    "Notes", "SOPAC",
	    "Program", "HYDROCARBONS",
	    "Country", "<Pacific Islands>",
	    "Photographer", "Australia"
	};
	
	switch(nthPreview) {
	case 1:
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	    
	    // 1. Titles classifier is completely empty
	    loadClassifierByName(_driver, "Title"); // CL1
	    checkNumDocs(0, "CL1");

	    // 2. filenames classifier: all records come from the same file
	    loadClassifierByName(_driver, "Source"); // CL2

	    // expect all docs with same title 'Untitled' and same source file name 'slide.mst'
	    
	    List<WebElement> docs = findElementsByXPath(_driver, 
		"//td[a[contains(text(), 'Untitled')]/i[contains(text(), 'slide.mst')]]");
	    errMsg = String.format("### ASSERT ERROR: found %d instead of 20 docs in 1st page"
		   + "\n\tof Source classifier with label 'Untitled' and filename 'slide.mst'",
				   docs.size());
	    Assert.assertEquals(errMsg, 20, docs.size());

	    // expect no doc has another source filename than 'slide.mst':
	    docs = _driver.findElements(By.xpath("//td[a/i[not(contains(text(), 'slide.mst'))]]"));
	    errMsg = "### ASSERT ERROR: All filenames in Source classifier should be 'slide.mst'";
	    //+ "\n\t but found %s"; // what if docs.size == 0, this will break
	    Assert.assertEquals(//String.format(errMsg, docs.), // will this break if docs.size==0
				errMsg, 0, docs.size());

	    System.err.println("@@@ Docs under CL2 Source have no other filename but slide.mst"
			       + "\n\t (and they are labelled Untitled)");
	    
	    // expect 10 horizontal classifiers in CL2 (Source classifier), all labelled "SI"
	    String[] source_hlist = new String[10];
	    for(int i = 0; i < source_hlist.length; i++) {
		source_hlist[i] = "Sl";
	    }	    
	    GSSeleniumUtil.checkHorizontalClassifiers("CL2", source_hlist, null);

	    // 3. There is no metadata searching: only text and filenames indexes
	    checkSearchIndexesContain(true, "text", "filenames");
	    break;
	case 2:
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs); // should be the same
	    
	    checkSearchIndexesContain(true, "text", "photographer", "country", "notes");
	    loadClassifierByName(_driver, "Photographer"); // CL1
	    checkNumDocs(1, "CL1.1"); // only 1 doc under horizontal classifier "A"

	    clickDocLink("CL1", 1, POS_DEFAULT_BROWSE_DOC_ICON);
	    
	    tdXPath = "//div[@id='gs-document-text']/table//td";
	    checkTabularPageContents(tdXPath, expected);
	    
	    System.err.println("@@@ First doc under CL1.1 has the expected contents");
	    break;
	case 3:
	    String cdsRecordXPath = "//div[@id='cdsrecord']";
	    
	    // Try searching
	    String anotherExpectedTitle
		= "Col. photographs from the 1994 APEA Conference, Sydney, Australia";
	    searchAndCheckNumResults("Australia", "text", 2);
	    
	    // check titles of both expected docs exist
	    checkSearchResultDocTitleExists(POS_DEFAULT_SEARCH_TITLE, anotherExpectedTitle);
	    System.err.println("@@@ Found expected two search results: one containing title "
			       + anotherExpectedTitle);

	    checkSearchResultDocTitleExists(POS_DEFAULT_SEARCH_TITLE, anotherExpectedTitle);
	    System.err.println("\n\t@@@ Another doc containing title " + expectedTitle);
	    
	    // Search in photographer index for the same term. Click on (sole) expected result
	    searchAndCheckNumResults("Australia", "photographer", 1);
	    clickSearchResultDocLink_docTitleContains(expectedTitle);
	    
	    System.err.println("\n@@@ Checking contents of search result: " + expectedTitle);	    
	    // Different xpath now to test case 2!!
	    tdXPath = "//div[@id='gs_content']//table//td";
	    checkTabularPageContents(tdXPath, expected);
	    
	    checkCDSRecordContents(cdsRecordXPath, expected);
	    System.err.println("@@@ CDS record for the doc has the expected contents.");
	    
	    // Try browsing
	    loadClassifierByName(_driver, "Photographer"); // CL1
	    checkNumDocs(1, "CL1.1"); // only 1 doc under horizontal classifier "A"	    
	    clickDocLink_docTitleContains(expectedTitle); // Now there are proper titles
	    
	    checkTabularPageContents(tdXPath, expected);
	    System.err.println("@@@ First doc under CL1.1 has the expected contents");	    
	    checkCDSRecordContents(cdsRecordXPath, expected);
	    System.err.println("@@@ CDS record for the doc has the expected contents.");

	    // Hide the CDS record
	    clickLink("//a[text()='Show/Hide CDS Record']");
	    
	    WebElement cdsRecord = getElement(cdsRecordXPath);
	    // https://www.browserstack.com/guide/isdisplayed-method-in-selenium
	    if(cdsRecord.isDisplayed()) {
		Assert.fail("### ASSERT ERROR: toggled CDS record show/hide,"
			    + " but record is displaying.");
	    }
	    System.err.println("@@@ Toggled CDS record visibility: checked it's now hidden.");

	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for CDS_ISIS.");
	    break;
	}	
    }
    // local helper function, used more than once but only by cds_isis tutorial
    private static void checkCDSRecordContents(String xPath, String ... expected) {
	WebElement el = findElementByXPath(_driver, xPath);
	String elText = el.getText();
	String errMsg = "### ASSERT ERROR: page content is missing expected string: |%s|";
	for(int i = 0; i < expected.length; i++) {
	    // skip checkinge very even expected string as that's a cell heading
	    // and cell headings are not stored in the CDS record
	    if(i % 2 == 1 && !elText.contains(expected[i])) {
		Assert.fail(String.format(errMsg, expected[i]));
		System.err.println("@@@ Page contains string " + expected[i] + " as expected");
	    }
	}
    }

    public static void multimedia(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting multimedia() - preview test case #: " + nthPreview);

	final int numDocs = 156;
	final int numMidiFiles = 15;
	final String smallBeatlesCollName = "smallbea";
	String errMsg;
	String xpath;
	String browseClassifierID = "CL2";
	// CL2 button name is displayed as "Browse", but xslt contains "browse":
	final String browseClassifier = "Browse";

	// Browse classifier expected values
	final String[] bookshelfTitles = { "Audio", "Discography", "Images", "Lyrics",
					   "MARC", "Supplementary", "Tablature" };
	final int expectedNumBookShelves = bookshelfTitles.length;
	
	// Set of partial icons that are initially set for above bookshelfTitles
	final String[] docIcons = {"imp3.gif", "itext.gif", "_thumb.gif", "itext.gif",
			      "itext.gif", "("+PDF_ICON+"|imsword.gif)", "itext.gif"};
	// Final tests' icons for: "Audio", "Discography", "Images", "Lyrics",
	// "MARC", "Supplementary", "Tablature"
	final String[] newIcons = {"(midi.gif|imp3.gif)", "disc.gif", "_thumb.gif", "lyrics.gif",
			 "marc.gif", "("+PDF_ICON+"|imsword.gif)", "tab.gif"};
	// Matching number of leaves in bookshelfTitles in small beatles and largebea collections
	final int[] expectedNumDocsInBrowseBookshelves = { 4, 31, 10, 20, 50, 9, 17 };
	final int[] largeBeatlesNumDocsInBrowseBookshelves = { 21, 50, 10, 264, 50, 9, 31 };
	
	final String allMyLovingNodeTitle = "All My Loving"; // CL1.1.1
	
	//final String anthologyPrefixTitle = "Anthology";
	
	switch(nthPreview) {
	case 1:
	    // The 15 midi files can't be processed yet
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs - numMidiFiles);
	    // There'd be the default classifiers
	    loadClassifierByName(_driver, "Title");
	    loadClassifierByName(_driver, "Source");
	    
	    break;
	case 2:	    
	    break;
	case 3:
	    // 1. Check the 2 files named magicalmisterytour*.htm now have correctly spelled
	    // titles "magical mystery tour"
	    
	    loadClassifierByName(_driver, "Title");
	    
	    clickHorizontalClassifier("L-P");
	    
	    // filename still misspelled, titles corrected:
	    xpath = "//td[a/i[contains(text(), 'magicalmisterytour')]][a[contains(text(), 'Magical Mystery Tour')]]";
	    List<WebElement> elems = findElementsByXPath(_driver, xpath);
	    errMsg = "### ASSERT ERROR: There should be 2 files named magicalmisterytour*.htm"
		+ " with corrected filename Magical Mystery tour";
	    Assert.assertEquals(errMsg, 2, elems.size());
	    System.err.println("@@@ Found 2 correctly magicalmisterytour*.htm files now "
			       + "\n\tcorrectly titled as 'Magical Mystery Tour'");


	    // 2. Check CL2: check all bookshelf titles (based on dcFormat)
	    // are there by expanding them
	    loadClassifierByName(_driver, browseClassifier); // CL2
	    expandBookshelves(bookshelfTitles);
	    break;
	case 4:
	    loadClassifierByName(_driver, browseClassifier); // CL2	    

	    for(int i = 0; i < bookshelfTitles.length; i++) {
		expandBookshelf(i+1, browseClassifierID); //"CL2"
		checkBrowseDocIconsFormat(browseClassifierID+"."+(i+1),
			  new SimpleEntry(new Integer(POS_DEFAULT_BROWSE_DOC_ICON), docIcons[i]));
		//System.err.println("@@@ CHECKED doc icon for cell " + (i+1) + " matched " + docIcons[i]);
	    }
	    break;
	case 5:
	    // For test case 7, POS_BROWSE_TITLE is shifted down by 1
	    // because source icons are removed by now or replace the doc icon
	    final int CUSTOM_POS_BROWSE_TITLE = POS_DEFAULT_BROWSE_TITLE-1;
	    
	    // check source file names no longer exist
	    loadClassifierByName(_driver, "Title"); // CL1
	    clickHorizontalClassifier("L-P");
	    // L-P is CL1.6
	    //noSuchLeafNodeInBookshelf("CL1.6", 1, "magicalmisterytour.htm", CUSTOM_POS_BROWSE_TITLE);
	    noSuchNodeInClassifier("CL1", "magicalmisterytour.htm", CUSTOM_POS_BROWSE_TITLE);
	    break;
	case 6:
	    loadClassifierByName(_driver, "Title"); // CL1
	    // Now CL1 should use bookshelves. Check All My Loving bookshelf contains 2 leafnodes
	    expandBookshelves(allMyLovingNodeTitle);
	    checkNumDocsOfBookshelf(2, allMyLovingNodeTitle);
	    break;
	case 7: // Check num leaf indicators for both classifiers	    
	    // 1. Check Titles classifier > bookNode "All My Loving" (CL1.1.1)
	    // has num leaf indicator of 2
	    final String firstBookNodeID = "CL1.1.1";
	    loadClassifierByName(_driver, "Title"); // CL1
	    checkNumLeafIndicator(firstBookNodeID, 2);
	    // sanity check that we checked the right node: CL1.1.1 should be "All My Loving"
	    errMsg = String.format("### ASSERT ERROR: 1st booknode of Title classifier %s "
				   + "doesn't contain %s", firstBookNodeID, allMyLovingNodeTitle);
	    Assert.assertEquals(errMsg, allMyLovingNodeTitle, getBookshelfTitle(firstBookNodeID));

	    // 2. Check numleaf indicators for 1st page of Browse (CL2) classifier
	    loadClassifierByName(_driver, browseClassifier);


	    // pass true to check num leaf node indicator is showing and showing correct value
	    checkLeafNodesOfClassifier(browseClassifierID, //"CL2",
				       browseClassifier,
				       expectedNumBookShelves,
				       expectedNumDocsInBrowseBookshelves,
				       true, // check numleaf indicator
				       bookshelfTitles);

	    break;
	case 8:
	    loadAndWaitForPhindClassifier("Phrase browse");	    
	    searchPhind("band");
	    PAUSE(2);
	    System.err.println("*** TODO multimedia: testing Phind classifier search results not yet implemented");
	    break;
	case 9:
	    // check that the about page (the page we start each preview/test case on)
	    // contains the selected image and that it's displayed
	    // Check it has the expected image referenced and loaded

	    // 1. About page image
	    checkAboutPageImg(".*/tile.jpg$");//".*\\/tile.jpg$");

	    // 2. Main page image
	    goToMainLibraryPage();
	    
	    final String collImgXpath="//span[@id='collectionAndGroupLinks']/a[contains(@href, '"
		+smallBeatlesCollName+"/page/about')]/img[@class='collectionLinkImage']";
	    WebElement mainPageCollImage = GSSeleniumUtil.getElement(collImgXpath);
	    
	    GSSeleniumUtil.attributeMatches(mainPageCollImage,
					   "src", ".*/beatlesmm.png$");
	    GSSeleniumUtil.checkImageLoaded(collImgXpath, mainPageCollImage);
	    
	    break;
	case 10:
	    // Now the collection should have the full number of docs at last
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);

	    // Check Browse > Audio bookshelf also has the new number of audio files under it
	    // which should be 15 more than original number
	    loadClassifierByName(_driver, browseClassifier);
	    String audioBookshelf = bookshelfTitles[0];
	    expandBookshelves(audioBookshelf);
	    checkNumDocsOfBookshelf(expectedNumDocsInBrowseBookshelves[0] + numMidiFiles,
				    audioBookshelf);


	    // Test in preparation for case 11
	    loadClassifierByName(_driver, "Title"); // CL1
	    checkNumLeafIndicator(1, "ANTHOLOGY 1");
	    checkNumLeafIndicator(1, "ANTHOLOGY 2");
	    checkNumLeafIndicator(1, "ANTHOLOGY 3");
	    break;
	case 11:
	    // Now the 3 individual "ANTHOLOGY #" items are grouped under bookshelf "ANTHOLOGY"
	    loadClassifierByName(_driver, "Title"); // CL1
	    checkNumLeafIndicator(3, "ANTHOLOGY");

	    // Let's also check A hard day's night
	    clickHorizontalClassifier("C-J");
	    checkNumLeafIndicator(5, "A hard day\'s night");
	    checkNumLeafIndicator(1, "HARD DAY\'S NIGHT");
	    break;
	case 12: // Check the new icons for the different bookshelves under Browse
	    
	    loadClassifierByName(_driver, browseClassifier); // CL2	    

	    // check the *new* doc icons are now present
	    for(int i = 0; i < bookshelfTitles.length; i++) {
		expandBookshelf(i+1, browseClassifierID); //"CL2"
		checkBrowseDocIconsFormat(browseClassifierID+"."+(i+1),
			  new SimpleEntry(new Integer(POS_DEFAULT_BROWSE_DOC_ICON), newIcons[i]));
		//System.err.println("@@@ CHECKED doc icon for cell " + (i+1) + " matched " + docIcons[i]);
	    }
	    break;
	    
	case 13: case 14: // case 14 is lookingAtMultimedia() collection test!
	    // Doesn't mention total docs in collection, has a different About Page img
	    // and no Collage classifier (or Phind)
	    
	    //System.err.println("### Test "+nthPreview+" not yet implemented for multimedia.");

	    int bookshelfTitlePos = POS_BOOKSHELF_TITLE;
	    int numLeafIndicatorPos = POS_NUMLEAF_INDICATOR;
	    if(nthPreview == 13) {
		GSSeleniumUtil.checkNumDocsInCollOnAboutPage(435);
		// Preview lands us on the about page, check its custom image
		checkAboutPageImg(".*/tile.jpg$");		
	    }
	    if(nthPreview == 14) {
		// invisible extra field in bookshelf title rows
		bookshelfTitlePos += 1;
		numLeafIndicatorPos += 1;
	    }
	    
	    // Repeat some earlier tests
	    
	    loadClassifierByName(_driver, "Title"); // CL1
	    // Now CL1 should use bookshelves. Check All My Loving bookshelf contains 2 leafnodes
	    expandBookshelves(allMyLovingNodeTitle);
	    checkNumDocsOfBookshelf(2, allMyLovingNodeTitle);

	    checkNumLeafIndicator(3, "ANTHOLOGY", bookshelfTitlePos, numLeafIndicatorPos);

	    // Hard Day's Night is under H in large beatles collection, not C-J as in small beatles
	    clickHorizontalClassifier("H");
	    checkNumLeafIndicator(5, "A hard day's night", bookshelfTitlePos, numLeafIndicatorPos);
	    checkNumLeafIndicator(1, "HARD DAY'S NIGHT", bookshelfTitlePos, numLeafIndicatorPos);

	    // 2. Check numleaf indicators for 1st page of Browse (CL2) classifier
	    loadClassifierByName(_driver, browseClassifier); // CL2
	    // pass true to check num leaf node indicator is showing and showing correct value
	    checkLeafNodesOfClassifier(browseClassifierID, //"CL2",
				       browseClassifier,
				       expectedNumBookShelves,
				       largeBeatlesNumDocsInBrowseBookshelves,
				       true, // check numleaf indicator
				       bookshelfTitles,
				       bookshelfTitlePos,
				       numLeafIndicatorPos);
	    // check the *new* doc icons are now present
	    for(int i = 0; i < bookshelfTitles.length; i++) {
		//expandBookshelf(i+1, browseClassifierID); //"CL2"
		checkBrowseDocIconsFormat(browseClassifierID+"."+(i+1),
			  new SimpleEntry(new Integer(POS_DEFAULT_BROWSE_DOC_ICON), newIcons[i]));
		//System.err.println("@@@ CHECKED doc icon for cell " + (i+1) + " matched " + docIcons[i]);
	    }

	    if(nthPreview == 13) {
		// TODO: how to test Collage classifier with Selenium? Is it possible?
		// Not checking Phind this time. Collage classifier is untested so far.
		// Can check webswing's canvas element is present on page and loaded?
		loadClassifierByName(_driver, "Collage");
		PAUSE(5); // give the webswing collage application some time to load
		xpath = "//div[@id='webswing-collage']//canvas";
		WebElement collageWScanvas = getElement(xpath);
		// Selenium can't really access canvas elements to check them:
		// https://qxf2.com/blog/selenium-html5-canvas-verify-what-was-drawn/
		// We can't call isImageLoaded() on canvas XPath either as that only works for images,
		// since JavaScript can test the complete attr of an <img> to see if it has loaded
		System.err.println("### Test " + nthPreview
		   + " multimedia: proper Collage browser testing not yet implemented.");
	    }
	    
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview + " for multimedia.");
	    break;
	}	
    }


    // Expects you to be on the about page (default page you land on when previewing a coll) and
    // checks the image is referenced and has loaded. Used by beatles colls of Multimedia tutorials
    private static void checkAboutPageImg(String imgFilePattern) {
	final String aboutImgXpath = "//div[@id='gs_content']/img";
	
	WebElement aboutPageImage = GSSeleniumUtil.getElement(aboutImgXpath);
	GSSeleniumUtil.attributeMatches(aboutPageImage, "src", imgFilePattern);
	GSSeleniumUtil.checkImageLoaded(aboutImgXpath, aboutPageImage);
    }

    public static void scannedImages(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting scannedImages() - preview test case #: " + nthPreview);
		
	String searchablePaperTitle = "Te Whetu o Te Tau";
	String imgOnlyPaperTitle = "Te Waka o Te Iwi";
	
	String generalXPathFormat = "//div[contains(@id, '%s') and contains(@id, '.%s')]";
	// use as: String.format(...XPath, containerType, pageNumber), where
	String docXPathFormat = String.format(generalXPathFormat, "doc", "%d"); // 2nd %s changes to %d pageNum: leave unresolved
	String imgXPathFormat = docXPathFormat+String.format(generalXPathFormat,"image","%d")+"//img";
	//"//div[contains(@id, 'image') and contains(@id, '.%d')]//img";
	String ocrPreElemXPathFormat = docXPathFormat
	    +String.format(generalXPathFormat,"text","%d")+"//pre";
	
	String imgXPath, ocrPreElemXPath;

	int numSearchResults;
	String nDocsMatchedQueryLine = "%d %s match the query.";
	String termOccursNTimesInMDocsOrSectionsLine = "'%s' occurs %d times in %d %s";
	DocDisplayStruct[] searchResultExpectations;
	
	// Used in form searches in final test cases
	String[] queries = { "aroha" };
	String[] indexes = { "text" };
	
	switch(nthPreview) {
	case 1:
	    numSearchResults = 3;
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(5); // 2 x Te Waka, 3 x Te Whetu

	    // 1. Search and inspect a searchable doc (one with text)
	    GSSeleniumUtil.performQuickSearch(_driver, "waka", "text");
	    //searchAndCheckNumResults("waka", "text", numSearchResults);

	    // The only searchable docs should be Te Whetu o Te Tau
	    // whereas Te Waka o Te Iwi are image-only docs, not returned in search results
	    //noDocTitleContaining("Te Waka o Te Iwi");

	    // All 3 results should have the title "Te Whetu o Te Tau"
	    searchResultExpectations = new DocDisplayStruct[numSearchResults];
	    for(int i = 0; i < numSearchResults; i++) {
		searchResultExpectations[i]
		    = new DocDisplayStruct(POS_DEFAULT_SEARCH_TITLE,
					   CONTAINS_MULTILINE_PATTERN,
					   new String[] {
					       searchablePaperTitle // Te Whetu o Te Tau
					   });
	    }
	    GSSeleniumUtil.checkSearchResults(
	     numSearchResults,
	     String.format(nDocsMatchedQueryLine, numSearchResults, "documents"),
	     String.format(termOccursNTimesInMDocsOrSectionsLine,"waka",12,numSearchResults,"documents"),
	     null, // not checking hrefs as random HASH values, not sourcefilenames
	     searchResultExpectations);
	    searchResultExpectations = null;
	    
	    // Test the 1st search result
	    clickSearchResultDocLink(1, POS_DEFAULT_SEARCH_DOC_ICON);
	    
	    // Check all 4 pages in doc
	    int numPages = 4;
	     // a bit of text from each page of doc
	    String[] sampleStr = {"HAERE atu ra e taku aroha",
				  "E hoa e Hare Reweti",
				  "Haere mai e aku potiki",
				  "Ka tu a Kihi i konei ka tuhi"
	    };
	    // Check the contents of each page of this doc: image loaded and some text
	    for(int i = 1; i <= numPages; i++) {
		expandDocPage(i);
		//System.err.println("*** imgXPathFormat: " + imgXPathFormat);
		
		imgXPath = String.format(imgXPathFormat, i, i);
		GSSeleniumUtil.checkImageLoaded(imgXPath, getElement(imgXPath));
		ocrPreElemXPath = String.format(ocrPreElemXPathFormat, i, i);
		
		containsText(ocrPreElemXPath, LITERAL,
			     "TE WHETU O TE TAU.",
			     sampleStr[i-1]);

	    }
	    
	    // 2. Go to titles browser and inspect an image-only doc	    
	    
	    loadClassifierByName(_driver, "Title");
	    clickDocLink_docTitleContains(imgOnlyPaperTitle); // clicks 1st Te Waka o Te Iwi

	    int pageNum = 3;
	    clickPageInImageSlider(pageNum);
	    // only images in this doc, check image
	    imgXPath = String.format(imgXPathFormat, pageNum, pageNum);
	    GSSeleniumUtil.checkImageLoaded(imgXPath, getElement(imgXPath));
	    
	    break;
	case 2:
	    loadClassifierByName(_driver, "Title");

	    // 2 docs under Te Waka o Te Iwi
	    checkLeafNodesOfBookshelf("CL1",
				      -1,
				      imgOnlyPaperTitle,
				      2,
				      new String[]{
					  "(09_1_1.item)",
					  "(09_1_2.item)"
				      },
				      POS_DEFAULT_BROWSE_TITLE);
	    // 3 docs under Te Whetu o Te Tau. Only filenames distinguish them at this stage
	    checkLeafNodesOfBookshelf("CL1",
				      -1,
				      searchablePaperTitle,
				      3,
				      new String[]{
					  "(10_1_1.item)",
					  "(10_1_2.item)",
					  "(10_1_3.item)"
				      },
				      POS_DEFAULT_BROWSE_TITLE);
	    break;

	case 3:
	    // Titles no longer contain source file names to distinguish their titles otherwise
	    // all being Te Waka o Te Iwi/Te Whetu o Te Tau: titles have dates to distinguish them
	    // (and there's no source file name in title cell)
	    
	    loadClassifierByName(_driver, "Title");

	    // num leaf is indicated not in a separated td cell, but in title cell!
	    // And we can't match on exact title!
	    //checkNumLeafIndicator(2, imgOnlyPaperTitle);
	    //checkNumLeafIndicator(3, searchablePaperTitle);

	    int CUSTOM_POS_BROWSE_TITLE = 3; // invisible unused src icon removed, shifting title
	    checkLeafNodesOfBookshelf(
			      "CL1",
			      -1,
			      imgOnlyPaperTitle + " (2)",//numLeaves indicated in title td
			      2, // num leaves
			      new String[]{ // spaces get removed on reloading collection
				  // so make spaces optional in regex
				  "Volume: 1 Number: 1 Date: October 1857".replace(" ", " ?"),
				  "Volume: 1 Number: 2 Date: November 1857".replace(" ", " ?")
			      },
			      CUSTOM_POS_BROWSE_TITLE);

	    checkLeafNodesOfBookshelf(
			      "CL1",
			      -1,
			      searchablePaperTitle + " (3)", // numleaves shown in title TD
			      3, // num leaves
			      new String[]{ // spaces get removed on reloading collection
				  // so make spaces optional in regex
				  "Volume: 1 Number: 1 Date: 01 June 1858".replace(" ", " ?"),
				  "Volume: 1 Number: 2 Date: 01 July 1858".replace(" ", " ?"),
				  "Volume: 1 Number: 3 Date: 01 September 1858".replace(" ", " ?")
			      },
			      CUSTOM_POS_BROWSE_TITLE);
	    break;
	case 4:
	    GSSeleniumUtil.checkNumDocs(5, "Date", "CL2");

	    System.err.println("@@@ Checking Date classifier use formatted dates");	    
	    checkDocDisplay("CL2", "Date", 1, new String[] {"October",
		    FAV_ICON_RE, "itext.gif","", imgOnlyPaperTitle, "October 1857"});
	    checkDocDisplay("CL2", "Date", 2, new String[] {"November",
		    FAV_ICON_RE, "itext.gif","", imgOnlyPaperTitle, "November 1857"});
	    checkDocDisplay("CL2", "Date", 3, new String[] {"June",
		    FAV_ICON_RE, "itext.gif","", searchablePaperTitle, "01 June 1858"});
	    checkDocDisplay("CL2", "Date", 4, new String[] {"July",
		    FAV_ICON_RE, "itext.gif","", searchablePaperTitle, "01 July 1858"});
	    checkDocDisplay("CL2", "Date", 5, new String[] {"September",
		    FAV_ICON_RE,"itext.gif","",searchablePaperTitle,"01 September 1858"});
	    
	    break;
	case 5:
	    // Still 5 docs under Date classifier
	    GSSeleniumUtil.checkNumDocs(5, "Date", "CL2");
	    // Test the display of a sample document has changed, e.g. 3rd doc
	    // No need to test all 5 docs' display
	    System.err.println("@@@ Checking Date classifier now uses internal dates");
	    checkDocDisplay("CL2", "Date", 3, new String[] {"June",
		    FAV_ICON_RE, "itext.gif","", searchablePaperTitle, "18580601"});
	    break;
	case 6:
	    
	    // 1. Search at section/page level for term aroha
	    performFormSearch(_driver,
			      "FieldQuery",
			      queries,
			      indexes,
			      "page");
	    numSearchResults = 9;
	    
	    int[] expectedDocTitles = {10,1,5,11,12,2,9,4,3};
	    searchResultExpectations = new DocDisplayStruct[numSearchResults];
		
	    for(int i = 0; i < numSearchResults; i++) {
		//System.err.println("*** Looking for " + expectedDocTitles[i]);
		searchResultExpectations[i]
		    = new DocDisplayStruct(POS_DEFAULT_SEARCH_TITLE,
					   CONTAINS_MULTILINE_PATTERN,
					   new String[] {
					       Integer.toString(expectedDocTitles[i])
					   });
	    }

	    GSSeleniumUtil.checkSearchResults(
	     numSearchResults,
	     String.format(nDocsMatchedQueryLine, numSearchResults, "sections"),
	     String.format(termOccursNTimesInMDocsOrSectionsLine,"aroha",54,numSearchResults,"sections"),
	     null, // not checking hrefs, because they contain random HASH values and not sourcefilenames
	     null); //searchResultExpectations); // ordering of some docs (2, 11 and 12) is different on Windows
				// where the search term occurs the same number of times in each of these docs
		 
	    searchResultExpectations = null;
	    
	    // 2. Now redo search for term aroha at document/newspaper level
	    performFormSearch(_driver,
			      null/*"FieldQuery" already on fieldquery page*/,
			      queries,
			      indexes,
			      "newspaper");
	    numSearchResults = 3;
	    searchResultExpectations = new DocDisplayStruct[numSearchResults];
	    
	    for(int i = 0; i < numSearchResults; i++) {
		searchResultExpectations[i]
		    = new DocDisplayStruct(POS_DEFAULT_SEARCH_TITLE,
					   CONTAINS_MULTILINE_PATTERN,
					   new String[] {
					       searchablePaperTitle
					   });
	    }
	    GSSeleniumUtil.checkSearchResults(
	     numSearchResults,
	     String.format(nDocsMatchedQueryLine, numSearchResults, "documents"),
	     String.format(termOccursNTimesInMDocsOrSectionsLine,"aroha",54,numSearchResults,"documents"),
	     null, // not checking hrefs as random HASH values, not sourcefilenames
	     searchResultExpectations);
	    
	    break;
	case 7:
	    // Repeat searches for aroha in page vs newspaper
	    // and just check display of a single doc each time (and num of search results)
	    // as only the format statement was changed
	    performFormSearch(_driver, "FieldQuery", queries, indexes, "page");
	    numSearchResults = 9;
	    checkSearchResults(numSearchResults, null, null, null, null);

	    DocDisplayStruct firstResultDisplay = new DocDisplayStruct(
			       POS_DEFAULT_SEARCH_TITLE,
			       CONTAINS_MULTILINE_PATTERN,
			       new String[] {
				   "Te Whetu o Te Tau Volume:1 Number:3 - Page:10",
				   "01 September 1858"
			       });
	    checkDocDisplayMatches(1, firstResultDisplay);

	    // Newspaper level
	    performFormSearch(_driver, null, queries, indexes, "newspaper");
	    numSearchResults = 3;
	    checkSearchResults(numSearchResults, null, null, null, null);

	    firstResultDisplay = new DocDisplayStruct(
			       POS_DEFAULT_SEARCH_TITLE,
			       CONTAINS_MULTILINE_PATTERN,
			       new String[] {
				   "Te Whetu o Te Tau Volume:1 Number:3",
				   "01 September 1858"
			       });
	    checkDocDisplayMatches(1, firstResultDisplay);
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			       + " for BrowserTest.scannedImages().");
	    break;
	}
	
    }
    
   public static void advancedScannedImages(int nthPreview/*, Object moreParams*/) {
       System.err.println("@@@ BrowserTesting advancedScannedImages() - preview test case #: " + nthPreview);

       int numDocs = 6; // at time of test case 1, there'll be an extra doc. At end there'll be 8
       String paperTitle;
       String xpath;
       //int CUSTOM_POS_BROWSE_DOC_ICON = POS_DEFAULT_BROWSE_DOC_ICON - 1;

       // Some reused xpath format strings, same as in scannedImages()
       String generalXPathFormat = "//div[contains(@id, '%s') and contains(@id, '.%s')]";
       
       // 2nd %s in generalXPathFormat changes to %d. This is the pageNum: leave unresolved
       String docXPathFormat = String.format(generalXPathFormat, "doc", "%d"); // 1 x %d
       // For the following 2 Format Strings there are 2 x %d, so we need to pass in 2 x pageNum
       String imgXPathFormat
	   = docXPathFormat+String.format(generalXPathFormat,"image","%d")+"//img";
       String ocrPreElemXPathFormat
	   = docXPathFormat+String.format(generalXPathFormat,"text","%d")+"//pre";
	
       String imgXPath, ocrPreElemXPath;
       int numPages;
       
       switch(nthPreview) {
       case 1: // added a new doc, check it's there and check its pages

	   paperTitle = "Te Haeata 1859-1862";
	   GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);

	   String searchTerm = "marama";

	   // 1. Search and inspect a searchable doc (one with text)
	   //GSSeleniumUtil.performQuickSearch(_driver, "marama", "text");
	   searchAndCheckNumResults(searchTerm, "text", 11);
	   //checkSearchResultDocTitleExists(POS_DEFAULT_SEARCH_TITLE, paperTitle);
	   clickSearchResultDocLink_docTitleContains(paperTitle+" Volume:3 Number:6 - Page:1");
	   PAUSE(TIME_FOR_PAGE_LOAD);
	   // check the search term found in whatever section is opened by default
	   xpath = "//div[@id='gs-document-text']//div[@class='sectionText']/pre";
	   containsText(xpath, searchTerm);

	   // 2. Go to titles browser and inspect an image-only doc
	   loadClassifierByName(_driver, "Title");
	   expandBookshelves(paperTitle + " (1)");
	   //clickDocLink_docTitleContains("Volume:3 Number:6 Date:02 September 1861");
	   clickDocLink("CL1.1", 1, POS_DEFAULT_BROWSE_DOC_ICON); // open 1st doc under CL1.1
	   
	   expandFullDoc();
	   numPages = 4;
	   // a bit of text from each page of doc
	   String[] sampleStr = {"Hei te 5 o nga ra Hua ai te Mamma.",
				 "Engari tenei, kua wai",
				 "Ki te Kai-tuhituhi o te Haeata Waikato NA TE MANIHERA Burmah",
				 "TE TAKIWA o NEHEMIA o TE KARAITI."
	   };
	   // Check the contents of each page of this doc: image loaded and some text
	   for(int i = 1; i <= numPages; i++) {
	       
	       imgXPath = String.format(imgXPathFormat, i, i);
	       GSSeleniumUtil.checkImageLoaded(imgXPath, getElement(imgXPath));
	       ocrPreElemXPath = String.format(ocrPreElemXPathFormat, i, i);
	       
	       containsText(ocrPreElemXPath, LITERAL,
			    //"TE  *HAEATA.",
			    sampleStr[i-1]);
	   }
	   
	   // 3. Now there'll be 6 docs under Date classifier. Check final one is doc just added
	   GSSeleniumUtil.checkNumDocs(numDocs, "Date", "CL2");
	   //System.err.println("@@@ Checking display of new 6th doc under Date classifier");
	   checkDocDisplay("CL2", "Date", 6, new String[] {"", //no new vertical classifier for row
		   FAV_ICON_RE, "itext.gif","", paperTitle, "02 September 1861"});
	   
	   break;
       case 2:
	   // section titles like .1.1 are strings, not digits
	   imgXPathFormat = imgXPathFormat.replace("%d", "%s");
	   ocrPreElemXPathFormat = ocrPreElemXPathFormat.replace("%d", "%s");
	   
	   paperTitle = "Matariki 1881";
	   // 2 more docs added
	   numDocs += 2;
	   GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	   
	   searchTerm = "matariki";
	   searchAndCheckNumResults("matariki", "titles", 2); //2 titles should match

	   performFormSearch(_driver,
			     "FieldQuery",
			     new String[]{"matariki"},
			     new String[]{"text"}, // search in text this time
			     "newspaper"); // search at docs level, not page/section level
	   checkSearchResults(4, null, null, null, null); // 4 docs should contain word matariki

	   loadClassifierByName(_driver, "Title");
	   expandBookshelves(paperTitle + " (2)");
	   clickDocLink("CL1", 2, POS_DEFAULT_BROWSE_DOC_ICON); // sample 2nd doc newly added

	   String abstractSectionTitle = "1.1";
	   expandDocSectionTitles("Abstract"); // parent section Supplementary is already opened	   
	   ocrPreElemXPath = String.format(ocrPreElemXPathFormat, "1.1", "1.1");
	   containsText(ocrPreElemXPath, LITERAL, "Editorial discussing benefits promised");

	   // Check all the newspaper pages of the 2nd newly added doc: image and sample text
	   expandDocSectionTitles("Newspaper pages", "Page One", "Page Two",
				  "Page Three", "Page Four");
	   String newsPaperSectionPrefix = "2.";
	   numPages = 4;
	   // a bit of text from each page of doc
	   String[] sampleStrings = {"Te Kaiwhakaora i nga",
				 "ko ratou, kia mate ko koutou.",
				 "Tomo rawa atu a ia ki roto i taua Aua.",
				 "Kua tae atu hoki kia Te Riihi"
	   };
	   for(int i = 1; i <= numPages; i++) {
	       
	       imgXPath = String.format(imgXPathFormat,
					newsPaperSectionPrefix+i, newsPaperSectionPrefix+i);
	       GSSeleniumUtil.checkImageLoaded(imgXPath, getElement(imgXPath));
	       ocrPreElemXPath = String.format(ocrPreElemXPathFormat,
					       newsPaperSectionPrefix+i, newsPaperSectionPrefix+i);
	       
	       containsText(ocrPreElemXPath, LITERAL, sampleStrings[i-1]);
	       
	   }
	   break;
       default:
	   System.err.println("### Unknown preview test number " + nthPreview
			      + " for BrowserTest.advancedScannedImages().");
	   break;
       }
   }

    public static void OAICollection(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting OAICollection() - preview test case #: " + nthPreview);

	int numDocs = 81;
	String xpath;
	String errMsg;
	WebElement imgEl;	
	
	// total num docs stays consistent for the collection through all this tutorial's tests
	GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	
	switch(nthPreview) {
	case 1:	    
	    int numDocsFirstBucket = 21;	    
	    GSSeleniumUtil.checkNumDocs(numDocsFirstBucket, "Title", "CL1.1");
	    
	    clickDocLink("CL1.1", 1, POS_DEFAULT_BROWSE_SRC_ICON); // thumbnail
	    // check large source image loads
	    xpath = "//body/img";
	    imgEl = getElement(xpath);
	    GSSeleniumUtil.checkImageLoaded(xpath, imgEl);
	    System.err.println("@@@ Clicked on thumbnail and checked full size image referenced/loaded");

	    // Go back to boleyn page and click on external link
	    _driver.navigate().back();
	    waitForPageLoad();

	    // Now choose source classifiers, and click a *document* icon a.o.t thumbnail/srcicon
	    GSSeleniumUtil.checkNumDocs(numDocsFirstBucket, "Source", "CL2.1");
	    clickDocLink("CL2.1", 1, POS_DEFAULT_BROWSE_DOC_ICON);

	    xpath = "//div[@id='gs-document']";
	    // Check no text
	    WebElement textDiv = getElement(xpath+"/div[@id='gs-document-text']");
	    errMsg = "### ASSERT ERROR: div[id=gs-document-text] should contain no text";
	    Assert.assertTrue(errMsg, textDiv.getText().trim().equals(""));
	    System.err.println("@@@ Clicked on doc, checked no text");
	    
	    // Check the screen image is referenced and loaded
	    xpath = xpath + "/div[contains(@id, 'image')]//img[contains(@src, '_screen')]";
	    imgEl = getElement(xpath);
	    GSSeleniumUtil.checkImageLoaded(xpath, imgEl);
	    System.err.println("@@@ Checked screenview image referenced/loaded");
	    break;
	case 2:
	    String subjectsClassifierID = "CL1";
	    // 1. Check Subjects classifier, which has bookshelves
	    loadClassifierByName(_driver, "Subjects");
	    int[] expectedNumDocsInBookShelf = { 27, 7, 15, 12, 20 }; // totals 81 docs
	    final int expectedNumBookShelves = expectedNumDocsInBookShelf.length;
	    final String[] bookshelfTitles = { "JCDL Banquet",
					       "JCDL Day Four",
					       "JCDL Day One",
					       "JCDL Day Three",
					       "JCDL Day Two"};
	    // pass true to check num leaf node indicator is showing and showing correct value
	    checkLeafNodesOfClassifier(subjectsClassifierID,
				       "Subjects",
				       expectedNumBookShelves,
				       expectedNumDocsInBookShelf,
				       false,
				       bookshelfTitles);
	    // Quick check of the new Captions classifier, which doesn't yet have captions
	    GSSeleniumUtil.checkNumDocs(9, "Captions", "CL2.1");//2.1 is A-B, it should have 9 docs
	    break;
	case 3:
	    loadClassifierByName(_driver, "Subjects");
	    // 1. Check the 7 docs in the 2nd bookshelf 
	    expandBookshelves("JCDL Day Four");

	    String[] expectedCaptions = {
		"lunch break",
		"lunch break",
		"lunch break",
		"Jim French and Unmil Karadkar over lunch",
		"Ed Fox seeking assistance from the able volunteers",
		"the most exciting and possibly most spoken about aspect of the conference - wireless networking :)",
		"conference staff hard at work in the office"
	    };

	    DocDisplayStruct[] docDisplayFormats = new DocDisplayStruct[expectedCaptions.length];
	    for(int i = 0; i < expectedCaptions.length; i++) {
		docDisplayFormats[i] = new DocDisplayStruct(
		     4,
		     CONTAINS_MULTILINE_PATTERN,
		     //new String[] {expectedCaptions[i]});
		     expectedCaptions[i]);
	    }

	    // 2. Check captions classifier
	    int CUSTOM_POS_SRC_ICON = 2;
	    int CUSTOM_POS_DOC_TITLE = 3;
	    loadClassifierByName(_driver, "Captions");
	    clickHorizontalClassifier("E-H");	    
	    clickDocLink_docTitleContains("even more conference attendees", CUSTOM_POS_SRC_ICON);
	    
	    // 3. Test searching captions: results will now make sense as captions are displayed
	    searchAndCheckNumResults("cake", null, 1);
	    // Doc with caption '... and version 2 of the slide - a "slide cake"'
	    //clickSearchResultDocLink_docTitleContains("slide cake");
	    checkSearchResultDocTitleExists(CUSTOM_POS_DOC_TITLE, "slide cake");
	    clickSearchResultDocLink(1, CUSTOM_POS_SRC_ICON);
	    
	    String[] expected = {"", // first cell spans 2 cols and is an image, no text
		"Caption", "slide cake",
		"Subject", "JCDL Banquet",
		"Publisher", "JCDL",
		"Rights", "unrestricted"};
	    String tdXPath = "//div[@id='gs_content']//table//td";
	    checkTabularPageContents(tdXPath, expected);

	    // Check the screen image (in the first td cell) and click on its containing anchor
	    xpath = "img[contains(@src, '_screen.')]";
	    tdXPath = "//div[@id='gs_content']//td/a/"+xpath;
	    imgEl = getElement(tdXPath);
	    GSSeleniumUtil.checkImageLoaded(tdXPath, imgEl);

	    // Inspect the <a> containing that img
	    //clickLink("/a[" + xpath + "]"); // don't click on <img>, but the containing <a>
	    WebElement anchor = getElement("//td/a[" + xpath + "]");
	    String externalLink = "http://rocky.dlib.vt.edu/";
	    errMsg = "### ASSERT ERROR: main image doesn't link as expected to " + externalLink;
	    // this link won't work, so don't click on it, just check it's set on the href attr
	    Assert.assertTrue(errMsg, anchor.getAttribute("href").startsWith(externalLink));
	    System.err.println("@@@ Confirmed displayed image links to external (defunct) site "
			       + externalLink);
	    //PAUSE(5);
	    
	    // Click on the clickable link instead. Annoying that some spaces get removed
	    // on reopening the collection
	    //String anchorText = "original 1280x960JPEG available"; // look for it with and without spaces
	    //clickLink("//div[@id='gs_content']//td//a[text()[contains(.,'"+anchorText
	    //+"') or contains(., 'original1280x960JPEG available')]]");

	    clickLink("//div[@id='gs_content']//td//a[text()[contains(.,'original')"
		      + " and contains(., 'available')]]");
	    xpath = "//body/img";
	    imgEl = getElement(xpath);
	    GSSeleniumUtil.checkImageLoaded(xpath, imgEl);
	    System.err.println("@@@ Followed link to locally stored available original image");
	    break;
	default:
	   System.err.println("### Unknown preview test number " + nthPreview
			      + " for BrowserTest.advancedScannedImages().");
	   break;
       }
    }

    public static void downloadingOverOAI() {
	System.err.println("@@@ BrowserTesting downloadingOverOAI() - preview test case #: 1/1");
	
	String[] descriptions = RunGLITest.get_backdrop_descriptions();
	final int POS_THUMB_ICON = 2;
	final int POS_DOC_DESCR = 1+POS_THUMB_ICON;
	
	// Repeat backdrop collection's key tests: downloadingOverOAI collections are formatted
	// largely the same as backdrop, but some improvements to search result display
	GSSeleniumUtil.loadClassifierByName(_driver, "Title"); // CL1 called "Title" unlike in
	                     // backdrop collection which inherits label "Browse" from image-e
	backdrop(8, descriptions);
	backdrop(9, descriptions);

	// Check searching: results are slightly different to BrwoserTest.backdrop(6) test case.
	final int numExpectedResults = 1;
	searchAndCheckNumResults("Rocky", "image descriptions", numExpectedResults);
	//searchAndCheckNumResults("Bear", "titles", numExpectedResults);
	GSSeleniumUtil.performQuickSearch(_driver, "Bear", "titles");
	
	// Either way:
	// 1 result, should be the Bear document
	DocDisplayStruct[] searchResultExpectations = new DocDisplayStruct[1];
	// tableCellNum 3 should contain Bear title and description
	searchResultExpectations[0]
	    = new DocDisplayStruct(POS_DOC_DESCR,
			 CONTAINS_MULTILINE_PATTERN,
			 new String[] {
			     // backdrop's search results do not have Title: and Description
			     // and still only have Image Name:, but not so "downloading
			     // over OAI" tutorial collections
				      
			     "Title: ?Bear", // "Image Name: ?Bear.jpg",
			     "Description: ?"+descriptions[0],//"Description: ?.*Rocky.*",
			     "Width:",
			     "Height:",
			     "Size:"
			 });
	    String[] hrefs = { ".*D0\\.jpg$" };
	    // casefolding is off in this collection, unlike in 'backdrop' coll,
	    // So the search term doesn't come back with titlecased 'Bear' but lowercased
	    // bear to indicate it's case insensitive. Good to check literally check the
	    // expected value again here.
	    GSSeleniumUtil.checkSearchResults(numExpectedResults,
					     "One document matches the query.",
					     "'bear' occurs 1 time",
					     hrefs,
					     searchResultExpectations);
    }

    public static void unknownConverterPluginTutorial(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting unknownConverterPluginTutorial() - preview test case #: "
			   + nthPreview);

	int numDocs = 1;

	String docTitle = "bcReliable PDF Creation";
	String docFileName = "superhero.djvu";
	// At first, we have no icon for djvu yet, and greenstone puts a placeholder there
	String[] docDisplayRow = {"", // no vertical classifier
			  FAV_ICON_RE, "itext.gif","_icondjvu_", docTitle+".*"+docFileName};
	switch(nthPreview) {
	case 1:
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(1);

	    GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");

	    // Check the sole doc displayed under the classifier has all the elements
	    // in the td cells as expected
	    checkDocDisplay("CL1", "Title", 1, docDisplayRow);
	    break;
	case 2:	    
	    String searchTerm = "interoperability";
	    // now we should have a proper icon
	    docDisplayRow[POS_DEFAULT_BROWSE_SRC_ICON] = "idjvu.gif";
	    
	    // There's only the djvu doc in this collection
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);

	    // Check default browsing classifiers: the doc should be present with custom icon
	    GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");
	    checkDocDisplay("CL1", "Title", 1, docDisplayRow);
	    
	    GSSeleniumUtil.checkNumDocs(numDocs, "Source", "CL2");
	    //checkDocDisplay("CL2", "Source", 1, new String[] {"", // can test filename too
	    //	     FAV_ICON_RE, "itext.gif","idjvu.gif", "("+docFileName+")"});
	    checkDocDisplay("CL2", "Source", 1, docDisplayRow);
	    
	    //clickDocLink_docTitleContains(docTitle);


	    // Search for the title, Check the doc is returned
	    searchAndCheckNumResults("PDF", "titles", numDocs);
	    performQuickSearch(_driver, "PDF", "text");
	    GSSeleniumUtil.checkSearchResults(numDocs,
					     "One document matches the query.",
					     "'pdf' occurs 113 times in 1 document",
					     null,
					     null);
	    // Search in full text. The djvudoc should be returned.
	    // Double-check it contains the query term

	    searchAndCheckNumResults(searchTerm, "text", numDocs);
	    // search results don't display filename along with docTitle, just the docTitle
	    docDisplayRow[docDisplayRow.length-1] = docTitle;
	    checkSearchResultsDocDisplay(1, docDisplayRow);
	    clickSearchResultDocLink_docTitleContains(docTitle);
	    
	    // It's case sensitive: doc contains Interoperability not interoperability
	    //containsPattern("//div[@id='gs-document-text']/pre", "interoperability");
	    
	    //This doesn't work, yet works here https://regex101.com/
	    //matchesPattern("//div[@id='gs-document-text']/pre", "(?i).*"+searchTerm+".*");

	    containsText("//div[@id='gs-document-text']/pre", "Interoperability");
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			      + " for BrowserTest.unknownConverterPluginTutorial().");
	   break;
	}
    }
    
    public static void METS() {
	System.err.println("@@@ BrowserTesting METS() - preview test case #: 1/1");

	// Check the rebuilt smallhtm collection. This time can also check the doc display.
    	final int numDocs = 3;
	
	GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");
	String[] docDisplayRow = {"", // no vertical classifier
				   FAV_ICON_RE, "itext.gif","", "Katharine.*(aragon.html)"};
	checkDocDisplay("CL1", "Title", 3, docDisplayRow); // check 3rd doc in CL1
	GSSeleniumUtil.checkNumDocs(numDocs, "Source", "CL2");
    }

    public static void stoneD(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting stoneD() - preview test case #: " + nthPreview);
		
	int numDocs;
	String classifierID = "CL1"; // It's enough to just check Title classifier
	//final String docIconRE = "(itext.gif|book.png)";
	//final String srcIconRE = "(irtf.gif|imsword.gif|"+PDF_ICON+")";
	String[] srcIcons = {"irtf.gif", "imsword.gif", "imsword.gif", PDF_ICON, PDF_ICON, PDF_ICON, "imsword.gif"};
	String[] newSrcIcons = {"irtf.gif", PDF_ICON, PDF_ICON, PDF_ICON, "imsword.gif"};
	final String docTitleRE = ".*\\(\\d\\.[a-z]{3}\\)"; // filename=<digit>.<file-ext>
	String[] docDisplayRow = {"", // no vertical classifier
	  FAV_ICON_RE, "PLACEHOLDER", "PLACEHOLDER", docTitleRE};//FAV_ICON_RE, docIconRE, "PLACEHOLDER", docTitleRE};
	switch(nthPreview) {
	case 1:
	    numDocs = 7;
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	    loadClassifierByName(_driver, "Title");
	    for(int i = 1; i <= numDocs; i++) {
		docDisplayRow[POS_DEFAULT_BROWSE_SRC_ICON] = srcIcons[i-1];
		// book icon for rows 4 to 6
		if(i >=4 && i <= 6) {
		    docDisplayRow[POS_DEFAULT_BROWSE_DOC_ICON] = "book.png";
		} else {
		    docDisplayRow[POS_DEFAULT_BROWSE_DOC_ICON] = "itext.gif";
		}
		checkDocDisplay(classifierID, "Title", i, docDisplayRow);
	    }
	    break;
	case 2:
	    // now 5 docs, but same doc title cell display
	    numDocs = 5;
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	    loadClassifierByName(_driver, "Title");
	    for(int i = 1; i <= numDocs; i++) {
		docDisplayRow[POS_DEFAULT_BROWSE_SRC_ICON] = newSrcIcons[i-1];		
		if(i >=2 && i <= 4) { // now book icons are at rows 2 to 4
		    docDisplayRow[POS_DEFAULT_BROWSE_DOC_ICON] = "book.png";
		} else {
		    docDisplayRow[POS_DEFAULT_BROWSE_DOC_ICON] = "itext.gif";
		}
		checkDocDisplay(classifierID, "Title", i, docDisplayRow);
	    }
	    break;
	case 3:
	    // still 5 docs, but doc title cell display changed for 2nd and 4th row
	    numDocs = 5;
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	    loadClassifierByName(_driver, "Title");
	    for(int i = 1; i <= numDocs; i++) {
		docDisplayRow[POS_DEFAULT_BROWSE_SRC_ICON] = newSrcIcons[i-1];
		if(i >=2 && i <= 4) { // book icons are at rows 2 to 4
		    docDisplayRow[POS_DEFAULT_BROWSE_DOC_ICON] = "book.png";
		} else {
		    docDisplayRow[POS_DEFAULT_BROWSE_DOC_ICON] = "itext.gif";
		}
		// change expected regex of title cell for 2nd and 4th row
		if(i==2 || i==4) {
		    // Expect title cell to also contain "Also available as" with the word icon
		    docDisplayRow[docDisplayRow.length-1] = docTitleRE + ".*"+"Also available as:";
		    checkDocDisplay(classifierID, "Title", i, docDisplayRow);
		    
		    checkBrowseDocIconsFormatAtRow(i, classifierID,
		       new SimpleEntry(new Integer(POS_DEFAULT_BROWSE_TITLE), "imsword.gif"));
		}
	    }

	    String docTitle = "Greenstone: A Comprehensive Open-Source Digital Library System";
	    // Check search result format statement also
	    searchAndCheckNumResults("greenstone", "titles", 1);
	    
	    // Expect title cell to also contain "Also available as" with the word icon
	    String[] searchResultDocDisplayRow = {"", // no vertical classifier
			FAV_ICON_RE, "book.png", docTitle + ".*Also available as:.*"};	    
	    checkSearchResultsDocDisplay(1, searchResultDocDisplayRow);
	    checkSearchDocIconsFormatAtRow(1,
			    new SimpleEntry(new Integer(POS_DEFAULT_SEARCH_TITLE), "imsword.gif"));
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			      + " for BrowserTest.unknownConverterPluginTutorial().");
	   break;
	}
    }

    public static void indexers(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting indexers() - preview test case #: " + nthPreview);

	int numDocs = 11;
	String defaultIndex = "all fields"; // could set to null, and it would default to 1st index
	switch(nthPreview) {
	case 1: // Demo Lucene collection
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	    
	    // Wildcard search in default index of 'all fields'
	    GSSeleniumUtil.performQuickSearch(_driver, "econom*", defaultIndex);
	    // Expect to see output
	    //   183 sections match the query.
	    //   <termList>
	    // where termList is:
	    String termList
		= "economies (38) econometrics (1) economically (25) economics (27) economists (6)"
		+ " economique (1) economy (159) economic (513) economical (6) economist (4)";
	    GSSeleniumUtil.checkSearchResults(20, // count results on first page
	     "183 sections match the query.",
	     termList.replace("(", "\\(").replace(")", "\\)"));// escape ( and ) for regex match

	    // Single letter wildcard search
	    GSSeleniumUtil.performQuickSearch(_driver, "economi??", defaultIndex);
	    termList = "economies (38) economics (27) economist (4)"; // different order if searching in full text
	    GSSeleniumUtil.checkSearchResults(20, // count results on first page
	     "46 sections match the query.",
	     termList.replace("(", "\\(").replace(")", "\\)"));// escape brackets for regex match

	    // Stopword example: the. No results and special message to explain why
	    GSSeleniumUtil.performQuickSearch(_driver, "the", defaultIndex);
	    GSSeleniumUtil.checkSearchResults(0,
	     "No sections match the query.",
	     "The following terms are too common and have been excluded from the search: the");
		
	    break;
	case 2: // Greenstone Demo MGPP collection
	    GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);

	    String[] faoQueries = {"fao", "FAO"};
	    String faoResultLine1 = numDocs + " documents match the query.";
	    String faoResultLine2 = "'%s' occurs 89 times in " + numDocs + " documents";
	    
	    // "By default, searching in collections built with the MGPP indexer
	    // is set to whole word must match and ignore case differences. So searching
	    // econom will return 0 documents. Searching for fao and FAO return the
	    // same result — 89 word counts and 11 matched documents.
	    GSSeleniumUtil.performQuickSearch(_driver, "econom", defaultIndex);
	    GSSeleniumUtil.checkSearchResults(0,
	     "No documents match the query.",
	     "'econom' occurs 0 times");

	    for(String query : faoQueries) {
		GSSeleniumUtil.performQuickSearch(_driver, query, defaultIndex);
		// expect same results as case insensitive by default
		GSSeleniumUtil.checkSearchResults(numDocs, // all 11 docs match
						 faoResultLine1,
						 String.format(faoResultLine2, query));
	    }
	    
	    clickQueryButton("TextQuery");
	    String errMsgFormat = "Expected %s to be set to %s, but was set to %s";
	    // expect stemming off
	    String stemXPath = "//form//select[@name='s1.stem']";
	    
	    //https://stackoverflow.com/questions/14936825/getting-the-currently-selected-option-as-text-string-for-a-drop-down-in-selenium
	    Select stemSelect = getDropDown(stemXPath);
	    Assert.assertEquals(String.format(errMsgFormat, "stemming", "off", "on"),
				"off", stemSelect.getFirstSelectedOption().getText().trim());
	    System.err.println("@@@ Stemming default is indeed OFF");
	    
	    // expect casefolding on
	    String caseXPath = "//form//select[@name='s1.case']";
	    Select caseSelect = getDropDown(caseXPath);
	    Assert.assertEquals(String.format(errMsgFormat, "casefolding", "on", "off"),
				"on", caseSelect.getFirstSelectedOption().getText().trim());
	    System.err.println("@@@ Casefolding default is indeed ON");

	    // Change stemming to on
	    stemSelect.selectByVisibleText("on"); //stemSelect.selectByValue("1");

	    GSSeleniumUtil.performFormSearch(_driver, null, new String[]{"econom"}, null, null);
	    GSSeleniumUtil.checkSearchResults(9,
	     "9 documents match the query.",
	     "'econom' occurs 778 times in 9 documents");


	    clickQueryButton("FieldQuery");
	    // stemming already off on FieldQuery page, but ensuring it's so
	    getDropDown(stemXPath).selectByVisibleText("off");

	    // Turn casefolding off, so searching becomes case sensitive
	    getDropDown(caseXPath).selectByVisibleText("off");

	    // Redo fao vs FAO search
	    GSSeleniumUtil.performFormSearch(_driver, null, new String[]{"fao"}, null, null);
	    GSSeleniumUtil.checkSearchResults(0, // 0 docs should match this time
					     "No documents match the query.",
					     "'fao' occurs 0 times");
	    
	    GSSeleniumUtil.performFormSearch(_driver, null, new String[]{"FAO"}, null, null);
	    // Results as before:	    
	    GSSeleniumUtil.checkSearchResults(numDocs, // all 11 docs match
					     faoResultLine1,
					     String.format(faoResultLine2, "FAO"));

	    // casefolding element went stale (got exception, perhaps because page reloads on
	    // search performed to obtain search results)
	    // so need to obtain the element again before its value can be changed
	    caseSelect = getDropDown(caseXPath);
	    caseSelect.selectByVisibleText("on"); // put casefolding back to default of on


	    // Using hotkeys to search
	    //clickQueryButton("FieldQuery"); // already on FieldQuery page, can't find button for FieldQuery
	    GSSeleniumUtil.performFormSearch(_driver, null, new String[]{"fao#i"}, null, null);
	    // Results as before:
	    GSSeleniumUtil.checkSearchResults(numDocs, // all 11 docs match
					     faoResultLine1,
					     String.format(faoResultLine2, "fao"));

	    // Searching for: econom#si = econom#sc + Econom#sc
	    int numResults = 9;
	    String expectedLine1 = numResults + " documents match the query.";
	    String expectedLine2Format = "'%s' occurs %d times in "+numResults+" documents";
	    GSSeleniumUtil.performFormSearch(_driver, null, new String[]{"econom#si"}, null, null);
	    // Results as before: econom occurs 778 times in 9 docs	    
	    GSSeleniumUtil.checkSearchResults(numResults,
					     expectedLine1,
					     String.format(expectedLine2Format,"econom",778));
	    // 778 = 691+87
	    GSSeleniumUtil.performFormSearch(_driver, null, new String[]{"econom#sc"}, null, null);
	    GSSeleniumUtil.checkSearchResults(numResults,
					     expectedLine1,
					     String.format(expectedLine2Format,"econom",691));
	    
	    GSSeleniumUtil.performFormSearch(_driver, null, new String[]{"Econom#sc"}, null, null);
	    GSSeleniumUtil.checkSearchResults(numResults,
					     expectedLine1,
					     String.format(expectedLine2Format,"Econom",87));
	    
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			      + " for BrowserTest.indexers().");
	   break;
	}
    }

    // Each of the 3 incrementalBuilding collections (incremen, autoincr, 2autoincr) calls each
    // nthPreview test case in this function in sequence.
    public static void incrementalBuilding(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting incrementalBuilding() - preview test case #: " + nthPreview);

	int numDocs = 11;
	String defaultIndex = "all fields"; // could set to null, and it would default to 1st index
	
	switch(nthPreview) {
	case 1: // Search for kouprey: hit in b18ase. Search for groundnuts: no hits.
	    searchAndCheckNumResults("kouprey", null, 1);
	    String[] expectedDocTitleContents1 = {"b18ase"};
	    checkDocDisplayAt(1, null, POS_DEFAULT_SEARCH_TITLE, expectedDocTitleContents1);
	    searchAndCheckNumResults("groundnuts", null, 0);
	    
	    System.err.printf(
	      "@@@ Incr test %d success: b18ase contains 'kouprey', no docs contain 'groundnuts'%n",
	      nthPreview);
	    break;
	case 2: // 'groundnuts' should have a hit in fb33fe
	    searchAndCheckNumResults("groundnuts", null, 1);
	    String[] expectedDocTitleContents2 = {"fb33fe"};	    
	    checkDocDisplayAt(1, null, POS_DEFAULT_SEARCH_TITLE, expectedDocTitleContents2);

	    System.err.printf("@@@ Incr test %d success: doc fb33fe now contains 'kouprey'%n",
			      nthPreview);
	    break;
	case 3: // Neither kouprey nor groundnuts should now be found
	    searchAndCheckNumResults("kouprey", null, 0);
	    searchAndCheckNumResults("groundnuts", null, 0);
	    System.err.printf(
	      "@@@ Incr test %d success: No docs contain either 'kouprey' or 'groundnuts'%n",
	      nthPreview);
	    break;
	case 4: // Search titles for GS3 and search full text for Greenstone
	    searchAndCheckNumResults("GS3", "titles", 1);
	    String[] expectedDocTitleContents3 = {"GS3", "b20cre"};	    
	    checkDocDisplayAt(1, null, POS_DEFAULT_SEARCH_TITLE, expectedDocTitleContents3);

	    // the full text search
	    searchAndCheckNumResults("Greenstone", "text", 1);
	    String[] expectedDocTitleContents4 = {"fb34fe"};
	    checkDocDisplayAt(1, null, POS_DEFAULT_SEARCH_TITLE, expectedDocTitleContents4);
	    // Check document contents contains "Greenstone":
	    clickSearchResultDocLink(1, POS_DEFAULT_SEARCH_DOC_ICON);
	    expandFullDoc();
	    // "//div[@id='gs-document-text']//text()[contains(., 'Greenstone')]",
	    containsText("//div[@id='gs-document-text']", "Greenstone");

	    System.err.printf(
	      "@@@ Incr test %d success: a doc Title now contains 'GS3',"
	      + " another's text now contains 'Greenstone'%n", nthPreview);
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			      + " for BrowserTest.incrementalBuilding().");
	   break;
	}
    }

    public static void imagesGPS(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting imagesGPS() - preview test case #: " + nthPreview);
	
	int numDocs = 15;
	//String xpath;
	//String errMsg;
	//WebElement imgEl;	

	String locationsClassifier = "Locations";
	String locationsClassifierID = "CL3";

	String mapCanvasXpath="//div[@id='map_canvas' and contains(@style, 'visibility: visible')]";
	
	
	int[] expectedNumDocsInBookShelf = { 4, 1, 5, 5 };
	final int expectedNumBookShelves = expectedNumDocsInBookShelf.length;
	final String[] bookshelfTitles = { "Eiffel Tower",
					   "Musée d'Orsay",
					   "Panthéon district",
					   "Parc de Luxembourg"};
	// total num docs stays consistent for the collection through all this tutorial's tests
	GSSeleniumUtil.checkNumDocsInCollOnAboutPage(numDocs);
	
	switch(nthPreview) {
	case 1:
	    GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");	   
	    GSSeleniumUtil.checkNumDocs(numDocs, "Source", "CL2");
	    //GSSeleniumUtil.checkNumDocs(numDocs, "Locations", "CL3");

	    // Check Locations classifier, CL3
	    
	    // pass true to check num leaf node indicator is showing and showing correct value
	    checkLeafNodesOfClassifier(locationsClassifierID,
				       locationsClassifier,
				       expectedNumBookShelves,
				       expectedNumDocsInBookShelf,
				       false, // no num leaf indicator to check
				       bookshelfTitles);
	    break;
	case 2:
	    // This time check for the map displaying and scrolling checkbox to be on
	    GSSeleniumUtil.checkNumDocs(numDocs, "Title", "CL1");
	    getElement(mapCanvasXpath);
	    System.err.println("@@@ Now Title classifier has a div map_canvas element");
	    checkScrollCheckboxOn();
	    
	    GSSeleniumUtil.checkNumDocs(numDocs, "Source", "CL2");
	    getElement(mapCanvasXpath);
	    System.err.println("@@@ Now Source classifier has a div map_canvas element");
	    checkScrollCheckboxOn();
	    
	    // For each bookshelf, go to locations page and click its bookshelf icon
	    // (Not the bookshelf expander icon). This will load the bookshelf in its own
	    // page. Check that the mapcanvas element is there.
	    for(int row = 1; row <= expectedNumBookShelves; row++) {
		loadClassifierByName(_driver, "Locations");
		clickDocLink("CL3", row, POS_BOOKSHELF_ICON);
		if(expectedNumDocsInBookShelf[row-1] > 1) {
		    getElement(mapCanvasXpath);
		    System.err.printf("@@@ Now Locations classifier bookshelf %s has a visible map_canvas, as it contains more than 1 doc", bookshelfTitles[row-1]);
		    checkScrollCheckboxOn();
		} else { // if only one doc in the bookshelf, the map_canvas gets hidden
		    getElement(mapCanvasXpath.replace("visible", "hidden"));
		    System.err.printf("@@@ Now Locations classifier bookshelf %s has a Hidden map_canvas, as it contains 1 doc", bookshelfTitles[row-1]);
		}		
	    }
	    break;
	case 3:
	    searchAndCheckNumResults("Panthéon district", "titles", 5);
	    getElement(mapCanvasXpath);
	    System.err.println("@@@ Now Title classifier has a div map_canvas element");
	    checkScrollCheckboxOn();
	    
	    // Need to find a query for raw search that returns results and then check results:
	    // This raw query string is generated when searching on 'Parc', then clicking rawquery
	    // and looking in the raw query field which then contains the following string.
	    // Results are all 5 Parc de Luxembourg + 2 Pantheon district documents = 7 docs.
	    String rawQuery = "(CS:\"48N86 2E35\") OR (CS:\"48N86 2E34\") OR (CS:\"48N86 2E33\") OR (CS:\"48N86 2E32\") OR (CS:\"48N86 2E31\") OR (CS:\"48N85 2E35\") OR (CS:\"48N85 2E34\") OR (CS:\"48N85 2E33\") OR (CS:\"48N85 2E32\") OR (CS:\"48N85 2E31\") OR (CS:\"48N84 2E35\") OR (CS:\"48N84 2E34\") OR (CS:\"48N84 2E33\") OR (CS:\"48N84 2E32\") OR (CS:\"48N84 2E31\") OR (CS:\"48N83 2E35\") OR (CS:\"48N83 2E34\") OR (CS:\"48N83 2E33\") OR (CS:\"48N83 2E32\") OR (CS:\"48N83 2E31\")";
	    //clickQueryButton("RawQuery");
	    performFormSearch(_driver,
			      "RawQuery",
			      new String[]{rawQuery},
			      null, // index is already the default for rawquery
			      null); // no indexLevel to specify			      
	    checkSearchResults(7, null, null, null, null); // 7 docs must match as described above
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			       + " for BrowserTest.imagesGPS().");
	    break;
	}
    }

    private static void checkScrollCheckboxOn() {
	String scrollCheckboxXpath = "//input[@id='scrollCheckbox']";
	// assertion failure if the scrollCheckbox doesn't exist
	WebElement scrollCheckBox = getElement(scrollCheckboxXpath);
	System.err.println("@@@ scroll checked=" + scrollCheckBox.getAttribute("checked"));
	// Though the Inspector in development tools shows attr checked="checked" set on scrollbox
	// a value of "true" or otherwise is returned for the attribute name "checked".
	boolean isChecked = scrollCheckBox.getAttribute("checked").equals("true");//equals("checked");
	String errMsg = "### ASSERT ERROR: Checkbox to scroll through places was not checked";
	Assert.assertTrue(errMsg, isChecked);
	// If we're past above assertion, the checkbox was checked
	System.err.println("@@@ Checkbox to scroll through places was checked");
    }

    public static void OAIServer(int nthPreview, Object moreParams) {
	System.err.println("@@@ BrowserTesting OAIServer() - preview test case #: " + nthPreview);

	String errMsg;
	String xPath;

	if(nthPreview == 2) {
	    String adminEmail = (String)moreParams;
	    String oaiServerURL = _driver.getCurrentUrl().replace("/library", "/oaiserver?verb=Identify");
	    _driver.get(oaiServerURL);
	    //PAUSE(15);
	    xPath = "//div[@class='results']/table[@class='values']//tr[td[@class='key' and text()='Admin Email']]/td[@class='value']";
	    WebElement e = getElement(xPath);
	    errMsg = String.format("### ASSERT ERROR: value for Admin Email was not %s but |%s|",
				   adminEmail, e.getText());
	    //PAUSE(25);
	    Assert.assertEquals(errMsg, adminEmail, e.getText());
	    System.err.println("@@@ Now the OAI repository's value for Admin Email is set to "
			       + adminEmail);	    
	    return;
	}
	
	// else nthPreview == 1, covered by the rest of this function
	
	// adjust library URL to point to GS3's oaiserver homepage
	String oaiServerURL = _driver.getCurrentUrl().replace("/library", "/oaiserver");
	_driver.get(oaiServerURL);

	// expect badVerb error
	WebElement e = getElement("//p[@class='error']");
	String expected = "Illegal OAI verb";
	errMsg = String.format("### ASSERT ERROR: The error paragraph contains %s instead of %s",
			       e.getText(), expected);
	Assert.assertEquals(errMsg, expected, e.getText()); // expected, actual
	System.err.println("@@@ Page contained the expected error message: " + expected);

	// Test proper page
	_driver.get(oaiServerURL+"?verb=Identify");
	String requestType = "Request was of type %s.";
	xPath = String.format("//body/p[text()='"+requestType+"']", "Identify");
	e = getElement(xPath); // assert failure if xPath can't be found
	System.err.println("@@@ Page confirms Identify request type: "
			 + String.format(requestType, "Identify"));

	// Click on all the basic verb links and spot-test them
	//ul[@class='quicklinks']/li/a[text()='ListSets']
	String xPathPrefix = "//ul[@class='quicklinks']/li/a";
	String[] quickLinkVerbs = {"Identify", "ListRecords (oai_dc)", "ListSets", "ListMetadataFormats", "ListIdentifiers (oai_dc)"};
	// Skip testing Identify, and test rest
	for(int i = 0; i < quickLinkVerbs.length; i++) {
	    xPath = xPathPrefix + "[text()='"+quickLinkVerbs[i]+"']";
	    //e = getElement(xPath);
	    clickLink(xPath);

	    String verbShortName = quickLinkVerbs[i];
	    int space = verbShortName.indexOf(' ');
	    if(space != -1) {
		verbShortName = verbShortName.substring(0, space);
	    }
	    xPath = String.format("//body/p[text()='"+requestType+"']", verbShortName);
	    e = getElement(xPath);
	    System.err.println("@@@ Loaded page confirming OAI request type: "
			     + String.format(requestType, verbShortName));
	    
	    if(verbShortName.equals("Identify")) {
		xPath = "//div[@class='results']/table[@class='values']//tr";
		// get the rows of the *first* values-table in results-div

		String[] keys = {"Repository Name",
				 "Base URL",
				 "Protocol Version",
				 "Earliest Datestamp",
				 "Deleted Record Policy",
				 "Granularity",
				 "Admin Email"
		};
		// "" for fields that don't need checking
		String[] values = {
		    "Greenstone3 OAI repository",
		    oaiServerURL,
		    "2.0",
		    "",
		    "persistent",
		    "YYYY-MM-DDThh:mm:ssZ",
		    ""};

		List<WebElement> rows = e.findElements(By.xpath(xPath));
		checkOAIServerPageRows(rows, keys, values, false);		
	    }
	    else if(verbShortName.equals("ListSets")) {
		// test backdrop and view its record
		//xPath = "//table[@class='values']//tr[1][td[@class='key' and text()='setName']]/td[@class='value' and text()='backdrop']";

		// get the Set table for backdrop collection
		// this xpath will check that the setName=backdrop
		// gets the table where the first row has the 1st and 2nd cells as described:
		String tableXpath = "//table[@class='values'][tbody/tr[1][td[1][@class='key' and text()='setName'] and td[2][@class='value' and text()='backdrop']]]";
		// gets the table where there is a row with the 1st and 2nd cells as described:
		// "//table[@class='values'][tbody/tr[td[1][@class='key' and text()='setName'] and td[2][@class='value' and text()='backdrop']]]"
		// gets the table where the 1st row has a 2nd/value cell containing backdrop: don't care what's in the 1st cell
		// "//table[@class='values'][tbody/tr[1]/td[2][@class='value' and text()='backdrop']]";
		e = getElement(tableXpath);
		System.err.println("@@@@ Found the OAI set for setName (collection) 'backdrop'. Clicking its ListIdentifiers.");

		xPath = "//table[@class='values']//tr[td[@class='key' and text()='setSpec']]/td[@class='value' and contains(text(), 'backdrop')]/a[text()='Identifiers']"; // simplified
		//"//table[@class='values']//tr[3][td[1][@class='key' and text()='setSpec']]/td[2][@class='value' and contains(text(), 'backdrop')]/a[1][text()='Identifiers']";
		//clickLink(xPath); // this xpath relative to the table retrieved in e
		WebElement identifiersAnchor = getElement(xPath);
		identifiersAnchor.click();
		waitForPageLoad();

		// Check sample content of ListIdentifiers page for one doc in backdrop
		String[] keys = {"OAI Identifier", "Datestamp", "setSpec"};
		String[] values = {
		    "oai::backdrop:",
		    "", // Datestamp needs no checking
		    "backdrop"
		};
		// get the first values-table in results-div
		// and get its rows
		xPath = "//div[@class='results']/table[@class='values']//tr";
		//xPath = "//div[@class='results']/table[@class='values'][tbody/tr[1][td[1][@class='key' and text()='"+keys[0]+"']]]/tbody/tr";
		//"(//div[@class='results']/table[@class='values'])[1][tbody/tr[1][td[1][@class='key' and text()='OAI Identifier']]]/tbody/tr"
		List<WebElement> rows = findElementsByXPath(_driver, xPath);
		checkOAIServerPageRows(rows, keys, values, false);

		// Return to ListSets page and click on backdrop again
		_driver.navigate().back();
		waitForPageLoad();
		
		e = getElement(tableXpath); // get the backdrop set values-table again
		System.err.println("@@@@ Back to OAI set for setName (collection) 'backdrop'. Clicking its Records button this time.");
		// The 3rd row should be setSpec, click on Records button
		// Limit search to children of current webelement (values-table) using .// prefix
		xPath = ".//tr[3][td[1][@class='key' and text()='setSpec']]/td[2][@class='value']/a[2][text()='Records']";
		WebElement recordsAnchor = e.findElement(By.xpath(xPath));
		recordsAnchor.click();
		waitForPageLoad();
		xPath = String.format("//body/p[text()='"+requestType+"']", "ListRecords");
		e = getElement(xPath);
		System.err.println("@@@ Loaded page confirming OAI request type: "
			     + String.format(requestType, "ListRecords"));
		System.err.println("@@@ Clicked on Records button for set 'backdrop'");
		
		// Check backdrop collection's Cat.jpg record's dublin core meta
		xPath = "//table[@class='dcdata'][tbody/tr[1][td[1][@class='key' and text()='Title'] and td[2][@class='value' and text()='Cat']]]";
		e = getElement(xPath); // gets that table of dc data for Cat
		// Check the Description meta
		xPath = ".//tr[2][td[1][@class='key' and text()='Description']]/td[2]"; // limit search to children of current node using .//
		WebElement descriptionCell = e.findElement(By.xpath(xPath));
		expected = "Kathy's beloved friend Kouskous";
		errMsg = "### ASSERT ERROR: "
		    + "The 'backdrop' OAI set's record for 'Cat' did not have the description "
		    + expected;
		Assert.assertEquals(errMsg, expected, descriptionCell.getText());

		// See WebElement.findElements():
		// https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/WebElement.html#findElements(org.openqa.selenium.By)
		// limit search to children of current node using .//tr[3]... instead of //tr[3]...
		xPath = ".//tr[td[1][@class='key' and text()='Resource Identifier']]/td[2]";
		List<WebElement> descriptionCells = e.findElements(By.xpath(xPath));
		String[] expVals = {"D1", "Cat.jpg"};
		for(int j = 0; j < expVals.length; j++) {
		    errMsg = "### ASSERT ERROR: " +
			"The backdrop OAI set's record for Cat didn't have Resource Identifier"
			+ expVals[j];
		    Assert.assertTrue(errMsg,
				      descriptionCells.get(j).getText().endsWith(expVals[j]));
		}
	    }
	    else if(verbShortName.equals("ListMetadataFormats")) {
		//PAUSE(5);
		String[] keys = {"metadataPrefix", "metadataNamespace", "schema"};
		String[] values = {
		    "oai_dc",
		    "http://www.openarchives.org/OAI/2.0/oai_dc/",
		    "http://www.openarchives.org/OAI/2.0/oai_dc.xsd"
		};
		xPath = "//table[@class='values'][tbody/tr[1][td[1][@class='key' and text()='metadataPrefix']]]/tbody/tr";
		List<WebElement> rows = e.findElements(By.xpath(xPath));
		
		checkOAIServerPageRows(rows, keys, values, true);
	    }
	    // verbShortName "ListIdentifiers" already checked in greater detail for a particular set, and generally at top level
	}

	// check lucene-jdbm-demo colConfig file contains expected OAI mapping lines

	String[] lines = { "<element name=\"dc:publisher\">",
			   "<mapping elements=\"dls.Organization\"\\s*/>",
			   "</element>" };
	String filepath = System.getenv("GSDL3SRCHOME")
	    + "/web/sites/localsite/collect/lucene-jdbm-demo/etc/collectionConfig.xml";
	if(!GSBasicTestingUtil.fileContainsLines(filepath, lines, ENCODING)) {
	    Assert.fail("### ASSERT ERROR: file " + filepath + " does not contain lines:\n\t"
			+ Arrays.toString(lines));
	}
	System.err.println("@@@ Coll config file " + filepath + " contains OAI lines:\n\t"
			   + Arrays.toString(lines));


	// Set adminEmail, repositoryIdentifier, resumeAfter
	// in file resources/oai/OAIConfig-oaiserver.xml.in
	String adminEmail = (String)moreParams;
	filepath = System.getenv("GSDL3SRCHOME") //+ "/resources/oai/OAIConfig.xml.in";
	    + "/resources/oai/OAIConfig-oaiserver.xml.in";
	File tmpFile = new File(filepath);
	if(!tmpFile.exists()) {
		filepath = System.getenv("GSDL3HOME") + "/WEB-INF/classes/OAIConfig-oaiserver.xml.in";
	}
	String[] findLines = {
	    "<repositoryIdentifier></repositoryIdentifier>",
	    "<adminEmail></adminEmail>",
	    "<resumeAfter>250</resumeAfter>" };
	String[] replacements = {
	    "<repositoryIdentifier>greenstone.org</repositoryIdentifier>",
	    "<adminEmail>"+adminEmail+"</adminEmail>",
	    "<resumeAfter>5</resumeAfter>" };
	GSBasicTestingUtil.replaceLinesInFile(filepath, findLines, replacements, ENCODING);
	
	//filepath = System.getenv("GSDL3SRCHOME") + "/resources/oai/OAIConfig.xml.in";
	//GSBasicTestingUtil.replaceLinesInFile(filepath, findLines, replacements, ENCODING);
    }
    
    private static void checkOAIServerPageRows(List<WebElement> rows, String[] keys,
					      String[] values, boolean exactValue)
    {
	String errMsg;
	WebElement e;
	//System.err.println("*** Num matching rows: " + rows.size());
	for(int j = 0; j < keys.length; j++) {
	    /*
	      xPath = ".//*[td[@class='key' and text()='"+keys[j]+"']]/td[@class='value']";
	      e = rows.get(j).findElement(By.xpath(xPath));
	      System.err.println("*** Tried to find xPath " + xPath);
	      errMsg = String.format("Value for key %s is not expected %s ",
	      keys[j], values[j]);
	      Assert.assertEquals(errMsg, values[j], e.getText());
	    */
	    if(j % 2 == 1) { // odd row check key
		e = rows.get(j).findElement(By.xpath(".//td[@class='key']"));
		errMsg = String.format("Could not find key " + keys[j]);
		Assert.assertEquals(errMsg, keys[j], e.getText());
	    } else { // even row, check value
		e = rows.get(j).findElement(By.xpath(".//td[@class='value']"));
		
		if(exactValue) {
		    errMsg = String.format("### ASSERT ERROR: Value for key " + keys[j]
					   + " was other than expected |" + values[j] + "|");
		    Assert.assertEquals(errMsg, values[j], e.getText());
		} else {
		    errMsg = String.format("### ASSERT ERROR: Value for key " + keys[j]
					   + " did not contain expected |" + values[j] + "|");
		    Assert.assertTrue(errMsg, e.getText().contains(values[j]));
		}
	    }		    
	}    
    }

    public static void fillInTextField(String fieldXpath, String value) {
	WebElement elem = getElement(fieldXpath);
	elem.clear();	    
	elem.sendKeys(value);
	PAUSE(0.5);
    }

    public static void customisingThemes(int nthPreview/*, Object moreParams*/) {
	
	System.err.println("@@@ BrowserTesting customisingThemes() - preview test case #: "
			   + nthPreview);
	
	String xPath;
	String errMsg;
	WebElement elem;
	
	
	switch(nthPreview) {
	case 1:   
	    logIn(BrowserTest.adminUser, BrowserTest.adminPwd);

	    changePwd(BrowserTest.adminPwd, BrowserTest.newAdminPwd); // takes us back to main pg
	    
	    clickLink("//a[contains(@href, 'ListUsers') and contains(text(), 'Administration Page')]");	    
	    PAUSE(1);
	    
	    
	    // Switch themes
	    switchThemeUnderPrefs("Greenstone Custom 2");
	    
	    // It worked visually, but how do we programmatically test the new theme's in effect?
	    break;

	case 2:
	    _driver.get("https://jqueryui.com/themeroller/#!zThemeParams=5d00008000ec05000000000000003d8888d844329a8dfe02723de3e5700bbb34ecf36cdef1e1654faa0015427bdb9eb45ebe89aaede0ec5f0d92417fcc6551ce804ff46bf543167700d2cf4e583f2515147aacb86ffa11c0e588dae72a13c98dc37478199f7eea536e99705016fdb5fcffec4f2af6c404e968ad2cb29f84df71128cc8e40d4b20bcb666fd9f4e0554c5002c60d79ef0f60a1b86892557ff6af9b66712fde45b3cc94d77136f2e5e8f83a9e5280046022099bc7767653c95bf081e03bd909f49918a8624c99b68780d078d83babdb936c3dcef2d47146950ccfc8d78d059650685d1b6a5eee710a8f1b5a2c29a75a0a5028e1efee0fc26486d71b121290e1ca56f344658455ec310971c749803a23f00791a46f9fc8f7a29b4d151f6e3b1d0c3dfe26345bca994324364d5f4605628f4aa8240947eef53c1b99e595206e33d7d913cc60785c9851bc44920d2d04ca7593c403fa04c8e7c012f77ff075dd1352b2583d35e3fa500c89ef2a752cfb92f1b8081df7db71cb4feb4c3e30112097c80d6938937eb4576d0f01ef37aaf6ac8eb4751886aa7b05e0f2c542c97fc4f50425ab3589f325b5257aa0e750fea891a442971976d291b8969da152905d12a5a9faecc23fe9ff852b67dfff3cf4679");
	    PAUSE(3);
	    /*
	    clickLink("//a[contains(text(), 'Header/Toolbar')]");
	    // div id=Header
	    setThemeValue("Header", "//input[@id='bgColorHeader']", "#a23336");	    
	    setThemeOption("Header", "//div[contains(@class, 'texturePicker')]", "//li[@class='glass']");	    
	    setThemeValue("Header", "//input[contains(@class, 'opacity')]", "50");
	    
	    setThemeValue("Header", "//input[@id='borderColorHeader']", "#000000");
	    setThemeValue("Header", "//input[@id='fcHeader']", "#000000");
	    setThemeValue("Header", "//input[@id='iconColorHeader']", "#000000");
	    
	    clickLink("//a[contains(text(), 'Content')]");
	    setThemeValue("Content", "//input[@id='bgColorContent']", "#000000");
	    setThemeOption("Content", "//div[contains(@class, 'texturePicker')]", "//li[@class='inset_soft']");
	    setThemeValue("Content", "//input[contains(@class, 'opacity')]", "25");
	    setThemeValue("Content", "//input[@id='borderColorContent']", "#000000");
	    setThemeValue("Content", "//input[@id='fcContent']", "#c2bcbc");
	    setThemeValue("Content", "//input[@id='iconColorContent']", "#c2bcbc");

	    clickLink("//a[contains(text(), 'Clickable: default state')]");
	    setThemeValue("Default", "//input[@id='bgColorDefault']", "#7e7676");
	    setThemeOption("Default", "//div[contains(@class, 'texturePicker')]", "//li[@class='glass']");
	    setThemeValue("Default", "//input[contains(@class, 'opacity')]", "55");
	    setThemeValue("Default", "//input[@id='borderColorDefault']", "#000000");
	    setThemeValue("Default", "//input[@id='fcDefault']", "#fffff");
	    setThemeValue("Default", "//input[@id='iconColorDefault']", "#ffffff");

	    clickLink("//a[contains(text(), 'Clickable: hover state')]");
	    setThemeValue("Hover", "//input[@id='bgColorHover']", "#303030");
	    setThemeOption("Hover", "//div[contains(@class, 'texturePicker')]", "//li[@class='glass']");
	    setThemeValue("Hover", "//input[contains(@class, 'opacity')]", "75");
	    setThemeValue("Hover", "//input[@id='borderColorHover']", "#000000");
	    setThemeValue("Hover", "//input[@id='fcHover']", "#fffff");
	    setThemeValue("Hover", "//input[@id='iconColorHover']", "#ffffff");
	    
	    clickLink("//a[contains(text(), 'Clickable: active state')]");
	    setThemeValue("Active", "//input[@id='bgColorActive']", "#000000");
	    setThemeOption("Active", "//div[contains(@class, 'texturePicker')]", "//li[@class='glass']");
	    setThemeValue("Active", "//input[contains(@class, 'opacity')]", "65");
	    setThemeValue("Active", "//input[@id='borderColorActive']", "#000000");
	    setThemeValue("Active", "//input[@id='fcActive']", "#fffff");
	    setThemeValue("Active", "//input[@id='iconColorActive']", "#ffffff");

	    // Click outside theme config panel to try and get the config settings to stick
	    elem = findElementByXPath(_driver, "//input[@id='autocomplete']");
	    elem.click();
	    PAUSE(2);
	    */
	    	
	    //clickLink("//a[@id='downloadTheme']"); // pageload times out too fast
	    elem = getElement("//a[@id='downloadTheme']");
	    elem.click();
	    waitForPageLoad(GSSeleniumUtil.TIMEOUT_FOR_PAGE_LOAD, 10);
	    
	    downloadThemeRoller();

	    break;

	case 3:
	    // I think we'd still need to be logged in, so can switch the theme again
	    // TODO: how to test the new theme?
	    switchThemeUnderPrefs("Tutorial Theme");
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			       + " BrowserTest.customisingThemes().");
	    break;
	}
    }

    public static void collectionSpecificTheme(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting collectionSpecifcTheme() - preview test case #: "
			   + nthPreview);
		
	String xPath;
	String errMsg;
	WebElement elem;	
	
	switch(nthPreview) {
	case 1:
	    _driver.get("https://jqueryui.com/themeroller/#!zThemeParams=5d00008000ef05000000000000003d8888d844329a8dfe02723de3e5700bbb34ecf36cdef1e1654faa0015427bdb9eb45ebe89aaede0ec5f0d92417fcc6551ce804ff46bf543167700d2cf4e583f2515147aacb86ffa11c0e588dae72a13c98dc37478199f7eea536e99702a40b46d2aba549a57e4664a9ef6b09019b9385e776f737ae1085d673cdd32f637a5f74687f295fadf1103563afc7737f5e7fc782beba99bde6e7664ec816934f85f5f25e47f5b608163463aa4041e6d94d0ed52e03ca012f0d2ef9b303677cd1400223d0b28771bb06ffa75e99d6d74ef13cad1c8303d9adc7cfdbfb22041c8a87af450b0437b44caee1e6a6855e9642c7c41357ae89ef0b0699fb3dae1b076bc2c59ae09ac8078b9c4419d9ecff7398b0084bffc0a89a654f825cfe276641defd2b321f07be509b3fa8d787afc1ee06d8b8a7d62b8ebe43cdaebed6fa6e80b6a38d617c931dd8b93440d0783e690bf372738aa73541f278dafa427b6ecec53d9696690ade7df72227cea22070ab153ecea7e6d8693dd2ba84f05301353ba1610a658652be4ed86138811bec744851aa9a5ff599867f1fb6996e16754c19407bf0ac19a3f094c83cd6681c5d35ee5c4a50f9f91f088403b8d974ccc3a3f27e79ca3a5973f27cffd7773b8c8aef4ffeca64bb4");
	    PAUSE(3);
	    	
	    //clickLink("//a[@id='downloadTheme']"); // pageload times out too fast
	    elem = getElement("//a[@id='downloadTheme']");
	    elem.click();
	    waitForPageLoad(GSSeleniumUtil.TIMEOUT_FOR_PAGE_LOAD, 10);
	    
	    downloadThemeRoller();
	    break;
	case 2:
	    // TODO: is there any way to test the theme was installed for collection backdrop?
	    System.err.println("@@@ No way to test that the CollectionTheme is installed for"
			       + " the backdrop collection");
	    PAUSE(3);

	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			       + " BrowserTest.collectionSpecificTheme().");
	    break;
	}
    }

    private static void downloadThemeRoller() {
	WebElement elem;
	/*
	//clickLink("//a[@id='downloadTheme']"); // pageload times out too fast
	elem = getElement("//a[@id='downloadTheme']");
	elem.click();
	waitForPageLoad(GSSeleniumUtil.TIMEOUT_FOR_PAGE_LOAD, 10);
	*/
	//PAUSE(3);
	// Look for input element where the parent has child span targeting jQuery version 1.8
	elem = getElement("//input[contains(@id, 'version-1.13')][../span[@class='version-label' and contains(text(), 'for jQuery 1.8')]]");
	elem.click(); // click the radio button
	
	
	// Look for label.toggleAll/input, where parent has child h2=Components
	elem = getElement("(//label[contains(@class, 'toggleAll')][../h2[text()='Components']])/input");
	elem.click(); // click checkbox. Using elem.click as click(xpath) waits for page reload

	// Core section - tick checkbox(es)
	getElement("//input[@id='jquery-patch']").click(); // jquery 1.8+ support
	getElement("//input[@id='keycode']").click();
	// Widgets section - tick checkboxes
	getElement("//input[@id='widgets/datepicker']").click();
	
	// Download
	clickLink("//input[@type='submit' and @value='Download']");

	// TODO: is it possible to try filesystem test that the download exists?
	// But the downloaded zip would have to have the current timestamp
	// so we don't mistake old zips for having finished downloading
	System.err.println("@@@@ Waiting 15s to download...");	
	PAUSE(15);
    }
    
    public static boolean logIn(String username, String pwd) {
	return logIn(username, pwd, true);
    }
    public static boolean logIn(String username, String pwd, boolean goToLoginPage) {
	System.err.print("*** About to log in as " + username);
	// This xPath will work whether the home page is controlled by home.xsl
	// or home-tutorial.xsl
	if(goToLoginPage) {
	    String xPath = "//ul/li//a//.[contains(text(),'Login')]"; // avoid "cyclic object value" error
		waitForXPath(_driver, xPath); //PAUSE(1);
	    // Log in
	    clickLink(xPath);
	}
	String[] authForm = {"username", "password"};
	String inputXpath = "//form[@id='login-form']//input[%s]";
	
	fillInTextField(String.format(inputXpath, "@name='username'"), username);
	fillInTextField(String.format(inputXpath, "@name='password'"), pwd);
	
	clickLink(String.format(inputXpath, "@type='submit' and @value='Login'"));
	waitForPageLoad();//PAUSE(1);
	
	// In the loaded page, check if there's a gs_error element
	WebElement err = findElementByXPath(_driver, "//div[@id='gs_error']");
	if(err == null) {
	    System.err.println("@@@ Logged in as " + username);
	    return true;
	}
	if(!err.getText().contains("Either your username or password was incorrect, please try again.")) {	    
	    System.err.println("Unknown error on logging in: " + err.getText());
	}
	
	return false;
    }
    
    public static void changePwd(String oldPwd, String newPwd) {	
	String topXpath = "//ul[@id='bannerLinks']/li[@id='%s']//a";
	clickLink(String.format(topXpath, "userMenuButton"));
	clickLink("//ul[@id='userMenu']/a/li[text()='Account settings']");
	
	clickLink("//button[@id='changePasswordButton']");
	
	fillInTextField("//tr/td[@id='oldPasswordInputCell']/input", oldPwd);
	fillInTextField("//tr/td[@id='passwordInputCell']/input", newPwd);
	fillInTextField("//tr/td[@id='retypePasswordInputCell']/input", newPwd);
	clickLink("//input[@id='submitButton']");
	PAUSE(1);
	
	// back to main page
	clickLink(mainPageXPath);
	PAUSE(1);
    }

    // returns true if resetting the admin password was possible
    // Not possible if we couldn't even login with the new admin pwd to reset to the old pwd
    public static boolean resetAdminPwd() {
	if(logIn(BrowserTest.adminUser, BrowserTest.newAdminPwd)) {
	    changePwd(BrowserTest.newAdminPwd, BrowserTest.adminPwd);
	    return true;
	}
	return false;
    }

    // Prerequisite: Should be logged in as a user to choose among user menu options
    public static void chooseUserMenu(String menuStr) {
	String topXpath = "//ul[@id='bannerLinks']/li[@id='%s']//a";
	clickLink(String.format(topXpath, "userMenuButton"));

	// Can only use user menus if logged in. e.g. if menuStr=Logout
	// Can only logout if logged in, i.o.w. can't logout if already logged out.
	String menuXpath = "//ul[@id='userMenu']/a/li[text()='"+menuStr+"']";
	WebElement elem = findElementByXPath(_driver, menuXpath);
	if(elem != null) {
	    clickLink(menuXpath);
	}
    }
    
    public static void logout() {
	/*
	String topXpath = "//ul[@id='bannerLinks']/li[@id='%s']//a";
	clickLink(String.format(topXpath, "userMenuButton"));

	// Can only logout if logged in, i.o.w. can't logout if already logged out.
	String logoutXpath = "//ul[@id='userMenu']/a/li[text()='Logout']";
	WebElement elem = findElementByXPath(_driver, logoutXpath);
	if(elem != null) {
	    clickLink(logoutXpath);
	}
	*/
	chooseUserMenu("Logout");
    }
    
    public static void logInAdmin() {
	logIn(BrowserTest.adminUser, BrowserTest.adminPwd, true);
    }
    public static void logInAdmin(boolean goToLoginPage) {
	logIn(BrowserTest.adminUser, BrowserTest.adminPwd, goToLoginPage);
    }
    
    private static void setThemeValue(String sectionID, String relXpath, String value) {
	String xpath = "//div[@id='"+sectionID+"']" + relXpath;
	WebElement elem = findElementByXPath(_driver, xpath);
	//elem.setAttribute("value", value); // not available
	//https://stackoverflow.com/questions/8473024/selenium-can-i-set-any-of-the-attribute-value-of-a-webelement-in-selenium
	JavascriptExecutor js = (JavascriptExecutor)_driver;
	js.executeScript("arguments[0].setAttribute(arguments[1],arguments[2])",
			 elem, "value", value);
	PAUSE(0.25);
	
    }
    private static void setThemeOption(String sectionID, String relXpath, String optionXpath) {
	String xpath = "//div[@id='"+sectionID+"']" + relXpath;
	WebElement elem = findElementByXPath(_driver, xpath);
	elem.click();
	PAUSE(0.25);
	WebElement subElem = findElementByXPath(_driver, xpath + optionXpath);
	((JavascriptExecutor)_driver).executeScript("arguments[0].click()", subElem);
	PAUSE(0.25);
    }
    public static void switchThemeUnderPrefs(String newTheme) {
	// I think we'd still need to be logged in, so can switch the theme again
	clickLink("//ul[@id='bannerLinks']/li/a[span[@id='preferencesButton']]");
	PAUSE(1);
	switchTheme(newTheme);
	clickLink(mainPageXPath);
    }
    
    // expects you to be on the Preferences page
    public static void switchTheme(String newTheme) {
	String submitButton = "//input[@type='submit' and @value='Set preferences']";
	String mainMenuStr = "//div[@id='switcher']//a[span[@class='jquery-ui-switcher-title']]";
	// "//div[@id='switcher']//a[span[@class='jquery-ui-switcher-title' and contains(text(),'%s')]]";
	WebElement mainMenu = findElementByXPath(_driver, mainMenuStr); //findElementByXPath(_driver, String.format(mainMenuStr, "Switch Theme"));
	
	String submenuStr = "//div[@id='switcher']//a[.//text()[contains(.,'%s')]]";
	WebElement subMenu = findElementByXPath(_driver,
						String.format(submenuStr, newTheme));
	
	mainMenu.click();
	// https://stackoverflow.com/questions/40768607/movetoelement-mouse-hovering-function-in-selenium-webdriver-using-java-not-stabl
	((JavascriptExecutor)_driver).executeScript("arguments[0].click()", subMenu);
	clickLink(submitButton);
	
	// Need to repeat selecting and setting the theme a 2nd time for reasons I haven't
	// worked out. Either that, or select the theme to switch to but then don't click
	// the set prefs button, for the new theme to 'stick'.
	// retrieved WebElements are now stale, so redo obtaining them.
	mainMenu = findElementByXPath(_driver, mainMenuStr);
	mainMenu.click();
	PAUSE(0.5);
	subMenu = findElementByXPath(_driver,
				     String.format(submenuStr, newTheme));
	((JavascriptExecutor)_driver).executeScript("arguments[0].click()", subMenu);
	clickLink(submitButton);	    
	PAUSE(0.5);
    }
    
    public static void customisingHomePage(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting customisingHomePage() - preview test case #: "
			   + nthPreview);

	String heading1_xpath = "//body/h1";

	String errorHeading = "The following error occurred:";
	String errMsg;
	WebElement h1;

	String xPath;
	
	switch(nthPreview) {
	case 1:
	    h1 = getElement(heading1_xpath);
	    WebElement p = getElement("//body/p");

	    String expectedError = "Couldn't find and/or load the stylesheet \"pages/home-tutorial.xsl\" for a=p, sa=home, in collection=";
	    errMsg = "### ASSERT ERROR: Page did not contain the expected error "
		+ expectedError;

	    Assert.assertEquals(errMsg, expectedError, p.getText());
	    Assert.assertEquals(errMsg, errorHeading, h1.getText());

	    System.err.println("@@@ Webpage displayed expected error message: " + expectedError);
	    PAUSE(1);
	    break;
	case 2:
	    List<WebElement> elems = _driver.findElements(By.xpath(heading1_xpath));
	    if(elems.size() > 0) { // shouldn't even be the case, but if it is, bail and print error
		errMsg = "### ASSERT ERROR: there should no longer be an error on the library home page. Yet it has a h1 heading and it contains " + elems.get(0).getText();		
		Assert.assertFalse(errMsg, elems.get(0).getText().equals(errorHeading));
		Assert.assertEquals(errMsg, 0, elems.size()); // there shouldn't be any h1 heading
	    }	    
	    System.err.println("@@@ No error heading '"+errorHeading
			       +"' on the library home page any more.");
	    
	    checkCustomisedHomePageTitle("A New Home Page");
	    break;
	case 3:
	    String collTitle = "Demo Collection (Solr)";
	    xPath = "//div[@id='sidebar2']//ul/li/a[text()='"+collTitle+"']";
	    clickLink(xPath); // assertion failure if link doesn't exist
	    PAUSE(1);
	    // Check the collection page that loads matches the expected link
	    xPath = "//div[@id='titlearea']";
	    WebElement titleDiv = getElement(xPath);

	    errMsg = "### ASSERT ERROR: clicked on link for " + collTitle + ",\n"
		+ "but page that loaded has different collection Title: " + titleDiv.getText();
	    Assert.assertEquals(errMsg, collTitle, titleDiv.getText());

	    System.err.println("@@@ Tested collectionsList template: link to collection '"
		       + collTitle + "' existed and directed to collection with that title.");
	    PAUSE(1);
	    break;
	case 4:
	    String query = "economy";
	    //GSSeleniumUtil.performQuickSearch(_driver, query, null);
	    WebElement quickSearchInput = findElementByXPath(_driver,
					     "//div[@id='search']//input[@name='s1.query']");
	    quickSearchInput.clear();
	    quickSearchInput.sendKeys(query);
	    quickSearchInput.sendKeys(Keys.RETURN); //Keys.ENTER // no search button: press Enter
	    waitForPageLoad();
	    PAUSE(3);
	    
	    // We don't know which test collections are already built, but there should
	    // be multiple occurrences of economy.
	    xPath = "//div[@id='matchdocs']";
	    WebElement result = getElement(xPath);
	    // Should not get 'No documents match the query.' but a positive number of results
	    elemContainsText(result, MATCHES_PATTERN, "At least \\d+ documents match the query.");
	    System.err.println("@@@ Got search results for '" + query + "': " + result.getText());
	    PAUSE(1);

	    //Prep for next preview: check unnecessary links under Library Links
	    _driver.navigate().back();
	    waitForPageLoad();
	    String[] links = {"Login", "Account Settings", "Register a new user",
			    "Administration", "Logout", "Help", "Preferences"};

	    // sidebar (not sidebar2) should contain Library Links
	    xPath = "//div[@id='sidebar']//ul/li/a[text()='%s']";
	    System.err.println("@@@ Checking (unnecessary) links present under Library Links...");
	    for(String link : links) {// assert failure if a link doesn't exist
		getElement(String.format(xPath, link));
		System.err.println("\t@@@ Library Link " + link + " present.");
	    }
	    PAUSE(1);
	    break;
	case 5:
	    xPath = "//div[@id='sidebar']//ul/li/a[contains(text(),'%s')]"; // contains() this time
	    BrowserTest.checkLoginLinks(xPath);
	    break;
	case 6:
	    checkCustomisedHomePageTitle("My Greenstone Library");
	    break;
	case 7:
	    checkCustomisedHomePageTitle("The Best Digital Library");
	    
	    break;
	default:
	   System.err.println("### Unknown preview test number " + nthPreview
			       + " for BrowserTest.customisingHomePage().");

	}
    }

    private static void checkLoginLinks(String xPath) {
	
	// Check only necessary links under Library Links section
	    
	// User not logged in. 
	String[] loggedOutLinks = {"Login", "Help", "Preferences"};
	
	System.err.println("@@@ Checking Library Links now contains only "
			   + Arrays.toString(loggedOutLinks));
	for(String link : loggedOutLinks) {// assert failure if a link doesn't exist
	    getElement(String.format(xPath, link));
	    System.err.println("\t@@@ Library Link " + link + " present.");
	}
	PAUSE(1);
	
	// Log in
	clickLink(String.format(xPath, "Login"));
	String[] authForm = {"username", "password"};
	//String inputXpath = "//form[@id='login-form']//input[@name='%s']";
	String inputXpath = "//form[@id='login-form']//input[%s]";
	
	WebElement elem = getElement(String.format(inputXpath, "@name='username'"));
	elem.clear();	    
	elem.sendKeys(BrowserTest.adminUser); //sendKeysSlowly(elem, adminUser);
	PAUSE(0.5);
	elem = getElement(String.format(inputXpath, "@name='password'"));
	elem.clear();	    
	elem.sendKeys(BrowserTest.adminPwd); //sendKeysSlowly(elem, adminPwd);
	PAUSE(0.5);
	//elem = getElement(String.format(inputXpath, "@type='submit' and value='Login'"));
	clickLink(String.format(inputXpath, "@type='submit' and @value='Login'"));
	PAUSE(1);
	
	// Clicking on login should take us back to main page
	
	String[] loggedInLinks = {"Add user", "Administration", "Logged in as: " + adminUser,
				  "Logout", "Help", "Preferences"};
	System.err.println("@@@ Logged in as "+adminUser+". Checking Library Links contains "
			   + Arrays.toString(loggedInLinks));
	for(String link : loggedInLinks) {// assert failure if a link doesn't exist
	    getElement(String.format(xPath, link));
	    System.err.println("\t@@@ Library Link " + link + " present.");
	}
	PAUSE(1);
	
	// Now click on Logout link
	// Do the first test again: check only loggedOutLinks present
	clickLink(String.format(xPath, "Logout"));
	System.err.println("@@@ Logged out. Checking Library Links now again contains just "
			   + Arrays.toString(loggedOutLinks));
	for(String link : loggedOutLinks) {// assert failure if a link doesn't exist
	    getElement(String.format(xPath, link));
	    System.err.println("\t@@@ Library Link " + link + " present.");
	}
	PAUSE(1);
    }
    
    private static void checkCustomisedHomePageTitle(String expectedHeading) {
	// https://stackoverflow.com/questions/8506489/get-page-title-with-selenium-webdriver-using-java
	//WebElement title = getElement("//head/title");
	String errMsg = "### ASSERT ERROR: Library home page's <head><title> wasn't: "
	    +expectedHeading;
	//Assert.assertTrue(errMsg, title.getText().equalsIgnoreCase(expectedHeading));
	Assert.assertTrue(errMsg, _driver.getTitle().equalsIgnoreCase(expectedHeading));
	WebElement h1 = getElement("//body//h1"); // h1 now nested further down into body
	errMsg = "### ASSERT ERROR: Library home page's h1 heading wasn't: " +expectedHeading;
	Assert.assertTrue(errMsg, h1.getText().equalsIgnoreCase(expectedHeading));
	System.err.println("@@@ Library home page <title> and h1 heading confirmed to be: "
			   + expectedHeading);
	PAUSE(1);
    }

    /*
    private static void sendKeysSlowly(WebElement elem, String str) {
	//elem.sendKeys(str);
	for (char ch: str.toCharArray()) {
	    elem.sendKeys(Character.toString(ch));
	    // https://stackoverflow.com/questions/73085999/selenium-webdriverwaitdriver-duration-ofseconds10-launches-java-lang-nosuch
	    // Forced to use deprecated version because of version of selenium we have
	    // https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/WebDriver.Timeouts.html#implicitlyWait(long,java.util.concurrent.TimeUnit)
	    _driver.manage().timeouts().implicitlyWait(100, TimeUnit.MILLISECONDS);//Duration.ofMillis(100));
	}
	
    }
    */

    public static void definingLibraries(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting definingLibraries() - preview test case #: "
			   + nthPreview);
	
	//String errorHeading = "The following error occurred:";
	String errMsg;
	WebElement elem;
	String xPath;
	
	switch(nthPreview) {
	case 1:
	    // crude way to check we're using the halftone-library on the halftone-library
	    // home page we're at, since selenium can't access the <head> elem to check css
	    // points to halftone-library
	    xPath = "//div[@id='title']/a[@id='lib']";
	    elem = getElement(xPath);
	    errMsg = "@@@ ASSERT ERROR: div[id=title]/a[id=lib].href is not: halftone-library";
	    Assert.assertTrue(errMsg, elem.getAttribute("href").endsWith("halftone-library")); 
	    System.err.println("@@@ On halftone-library home page "
			       + "(div[id='title'].href = halftone-library");
	    break;
	case 2:
	    // Home page should look like at end of customisingHomePage tutorial. Just check title
	    checkCustomisedHomePageTitle("The Best Digital Library");
	    // There should only be one collection: 'Demo Collection (Lucene)'
	    String collTitle = "Demo Collection (Lucene)";
	    xPath = "//div[@id='sidebar2']//ul/li/a[text()='%s']";
	    getElement(String.format(xPath, collTitle));
	    System.err.println("@@@ As expected, mysite contains: Demo Collection (Lucene)");
	    // In contrast, no other collections should be present in mysite. Check Solr
	    collTitle = "Demo Collection (Solr)";
	    elementShouldNotExist(String.format(xPath, collTitle));
	    System.err.println("@@@ As expected, mysite does not contain: Demo Collection (Solr)");
	    break;
	case 5: // do a little more before checking the 'rand' library
	    // We enter case 5 not on the rand library home page, but through its
	    // lucene-jdbm-demo collection. Go back to library home page

	    
	    xPath = "//div[@id='title']/a[@id='lib' and contains(@href, 'rand')]";
	    clickLink(xPath); // takes us back to library home page
	    System.err.println("@@@ In 'rand' library collection,"
			       + " found and clicked link to 'rand' library home page");
	    // DO NOT ADD BREAK STATEMENT HERE
	case 3: case 4: // all 3 cases need to check we're previewing the 'rand' library
	    
	    // https://stackoverflow.com/questions/15122864/selenium-wait-until-document-is-ready
	    // Page ready tactics: https://stackoverflow.com/questions/10720325/selenium-webdriver-wait-for-complex-page-with-javascript-to-load/10726369#10726369
	    // navigate refresh: https://www.browserstack.com/guide/refresh-page-selenium
	    // driver get vs navigate https://www.browserstack.com/guide/driver-get-in-selenium
	    // https://www.lambdatest.com/blog/selenium-page-load-strategy/
	    
	    xPath = "//div[@class='headline']";
	    elem = getElement(xPath);
	    String headline = "Select a collection to explore!";
	    errMsg="### ASSERT ERROR: div.class=headline isn't '%s' of althor interface";
	    Assert.assertEquals(errMsg, headline, elem.getText());
	    System.err.println("@@@ rand library home contains althor interface headline: "+headline);
	    
	    xPath = "//div[@class='wrapper']//a[@class='thumb' and contains(@href, 'rand')  and contains(@href, '%s')]/img[contains(@src, 'interfaces/althor')]";
	    getElement(String.format(xPath, "lucene-jdbm-demo")); // should exist
	    System.err.println("@@@ rand library uses althor interface and contains lucene-jdbm-demo");
	    elementShouldNotExist(String.format(xPath, "solr-jdbm-demo"));
	    System.err.println("@@@ As expected, rand library uses mysite,"
			       + " so doesn't contain collection solr-jdbm-demo");	    
	    
	    break;
	    /*    
	case 6:
	    PAUSE(3);
	    System.err.println("### TODO: BrowserTest.definingLibraries "+nthPreview+": preview");
	    break;
	    */
	default:
	   System.err.println("### Unknown preview test number " + nthPreview
			       + " for BrowserTest.definingLibraries().");

	}
    }

    public static void designingANewInterface2(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting designingANewInterface2() - preview test case #: "
			   + nthPreview);
	//String errorHeading = "The following error occurred:";
	String errMsg;
	WebElement elem;
	String xPath;
	String expected;

	String heading1_xpath = "//body/h1";
	String errorHeading = "The following error occurred:";
	List<WebElement> elems;
	
	switch(nthPreview) {
	case 1:
	    // Not Jenny's new home page, but the home page of the original default interface
	    //getElement("//div[@id='banner']/img[contains(@src, 'ngunguru.jpg')]");
	    //System.err.println("@@@ As expected: New Home Page tutorial banner still exists.");
	    System.err.println("@@@ Checking library 'golden' still looks like default interface"
			       + "\n\tby looking for //div[@id='topArea']...");
	    getElement("//div[@id='topArea']");
	    System.err.println("@@@ As expected: New Home Page tutorial banner still exists.");
	    break;
	case 2:
	    elem = getElement("//div[@id='gs_content']");
	    expected = "HOME PAGE CONTENT GOES HERE";
	    errMsg = "### ASSERT ERROR: home page does not say '" + expected + "'";
	    Assert.assertEquals(errMsg, expected, elem.getText());
	    System.err.println("@@@ Home page states expected: " + expected);
	    break;
	case 3:

	    WebElement h1 = getElement(heading1_xpath);
	    WebElement p = getElement("//body/p");
	    
	    String expectedError = "XMLTransformer.transform(Doc, Doc, HashMap, Doc) couldn't load stylesheet";
	    errMsg = "### ASSERT ERROR: Page did not contain the expected error "
		+ expectedError;

	    Assert.assertEquals(errMsg, expectedError, p.getText());
	    Assert.assertEquals(errMsg, errorHeading, h1.getText());

	    System.err.println("@@@ Webpage displayed expected error message: " + expectedError);
	    PAUSE(1);
	    break;
	case 4:
	    elems = _driver.findElements(By.xpath(heading1_xpath));
	    if(elems.size() > 0) { // shouldn't even be the case, but if it is, bail and print error
		errMsg = "### ASSERT ERROR: there should no longer be an error on the library home page. Yet it has a h1 heading and it contains " + elems.get(0).getText();		
		Assert.assertFalse(errMsg, elems.get(0).getText().equals(errorHeading));
		Assert.assertEquals(errMsg, 0, elems.size()); // there shouldn't be any h1 heading
	    }	    
	    System.err.println("@@@ No error heading '"+errorHeading
			       +"' on the library home page any more.");

	    // Find some expected elements in the new look of the page:
	    xPath = "//div[@class='content']/div[@id='featured_slide']/ul[@id='featurednews']/li/img[contains(@src, 'perrin/images/default.jpg')]";
	    elems = findElementsByXPath(_driver, xPath);
	    errMsg = "### ASSERT ERROR: Could not find expected new structure for collections:"
		+ "\n\t" + xPath;
	    Assert.assertTrue(errMsg, elems.size() > 0);
	    System.err.println("@@@ Confirmed new home page with default image for all collections");
	    break;
	case 5:
	    // ImageSlider: only one panel not greyed out in devtools at a time
	    List<WebElement> panels = _driver.findElements(By.xpath("//div[@class='panel']"));

	    
	    Wait<WebDriver> wait = new WebDriverWait(_driver, Duration.ofSeconds(2));
	    System.err.println("@@@ Number of panels for collections: " + panels.size());

	    WebElement panel = wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath(
		"(//div[@class='panel']//a[contains(@href, '/page/about')])")));
	
	    System.err.println("@@@ collection image slider href: " + panel.getAttribute("href"));
	    String oldHref = panel.getAttribute("href");

	    // Displayed/visible panels change every 5 or so seconds. So check after 8 seconds
	    // that the same panel is no longer visible
	    wait = new WebDriverWait(_driver, Duration.ofSeconds(8));
	    wait.until(ExpectedConditions.invisibilityOf(panel));
	
	    System.err.println("@@@ collection image slider href: " + panel.getAttribute("href"));
	    System.err.println("@@@ Confirmed coll slider works: 1 div.panel displayed at a time");
	    break;
	case 6:
	    elems = _driver.findElements(By.xpath("//div[@class='column']/ul[@class='latestnews']/li"));
	    errMsg = "### ASSERT ERROR: Highlighted items placeholder should have 3 items, but has"
		+ elems.size();
	    Assert.assertEquals(errMsg, 3, elems.size());
	    System.err.println("@@@ Highlighted items placeholder section has expected 3 items");
	    break;
	case 7:
	    elems = _driver.findElements(By.xpath("//div[@id='hpage_cats']/div"));
	    errMsg = "### ASSERT ERROR: No collections listed under div#hpage_cats"
		+ elems.size();
	    Assert.assertTrue(errMsg, elems.size() > 0);
	    System.err.println("@@@ collectionsList is now displayed (div#hpage_cats is present)");

	    // Check demo collection exists and has description
	    getElement("//div[@id='hpage_cats']//h2/a[contains(text(),'Demo Collection (Lucene)')]");
	    getElement("//div[@id='hpage_cats']//p[contains(text(),'This is a demonstration collection')]");
	    System.err.println("@@@ Demo collection present with title and description");
	    
	    break;
	case 8:
	    System.err.println("@@@ BrowserTest.designingANewInterface2()"
			       + " Test case 8 not yet implemented but not essential either:"
			       + "\n\tJust copying in a header.xsl into the perrin interface"
			       + "\n\tshould have had no effect (css to be changed in next test.");
	    break;
	case 9:
	    //errMsg = "### ASSERT ERROR: update to CSS links should have changed page drastically";
	    xPath = "/html/head/link[contains(@href, 'interfaces/perrin/styles%s')]";
	    elems = _driver.findElements(By.xpath(String.format(xPath, "")));
	    errMsg = "### ASSERT ERROR: 2 of perrin interface's css files should be linked in now."
		+ " Not " + elems.size();
	    
	    Assert.assertEquals(errMsg, 2, elems.size());
	    System.err.println("@@@ 2 css files from the perrin folder are now linked in");
	    getElement(String.format(xPath, "/gs3-core-min.css"));
	    getElement(String.format(xPath, "/layout.css"));
	    System.err.println("\t@@@ These are the expected gs3-core-min.css and layout.css");

	    elementShouldNotExist(String.format(xPath, "/core.css"));
	    System.err.println("\t@@@ Confirmed no linking to a core.css in perrin folder");

	    // prep for next test
	    getElement("//ul[@id='bannerLinks']");
	    System.err.println("@@@ ul#bannerLinks still present, to be removed by new main.xsl");
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			       + " for BrowserTest.designingANewInterface2().");
	    
	}
    }

    public static void designingANewInterface3(int nthPreview/*, Object moreParams*/) {
	System.err.println("@@@ BrowserTesting designingANewInterface3() - preview test case #: "
			   + nthPreview);
	
	//String errorHeading = "The following error occurred:";
	String errMsg;
	WebElement elem;
	String xPath;
	String expected;

	String heading1_xpath = "//body/h1";
	String errorHeading = "The following error occurred:";
	List<WebElement> elems;
	
	switch(nthPreview) {
	case 1:
	    elementShouldNotExist("//ul[@id='bannerLinks']");
	    System.err.println("@@@ ul#bannerLinks removed now perrin's gained a custom main.xsl");
	    
	    break;
	case 2:
	    // Test footer is present on library home page and a collection page
	    String footerStr = //"Copyright ©" + " 2013 - All Rights Reserved";
			" 2013 - All Rights Reserved";
	    xPath = "//div[@class='wrapper col8']";
	    elem = getElement(xPath);
	    errMsg = "### ASSERT ERROR: Footer on main page didn't contain '"
		+ footerStr + "'\n\tbut: " + elem.getText();
	    Assert.assertTrue(errMsg, elem.getText().contains(footerStr));//elem.getText().matches(footerStr)) and containsPatternAmongLines(xpath, footerStr) don't work
	    System.err.println("@@@ Footer present on home page and contains: " + footerStr);

	    // Repeat the same tests for a collection about page
	    clickLink("//div[@id='hpage_cats']/div/h2/a[contains(text(),'Demo Collection (Lucene)')]");
	    elem = getElement(xPath);
	    Assert.assertTrue(errMsg, elem.getText().contains(footerStr));
	    System.err.println("@@@ Same footer also present on demo collection page");
	    
	    break;
	case 3:
	    System.err.println("@@@ BrowserTest.designingANewInterface()"
			       +" Test case 12 not yet implemented but is it needed?"
			       +"\n\tImage slider, Highlighted Items already moved away from edges"
			       +"\n\twhen a custom main.xsl was added to the perrin interface.");
	    break;
	case 4:
	    String query = "economy";
	    //GSSeleniumUtil.performQuickSearch(_driver, query, null);
	    WebElement quickSearchInput = findElementByXPath(_driver,
					     "//div[@id='search']//input[@name='s1.query']");
	    quickSearchInput.clear();
	    quickSearchInput.sendKeys(query);
	    quickSearchInput.sendKeys(Keys.RETURN); //Keys.ENTER // no search button: press Enter
	    waitForPageLoad();
	    PAUSE(3);
	    
	    // There should be multiple occurrences of economy.
	    xPath = "//div[@id='matchdocs']";
	    WebElement result = getElement(xPath);
	    // Should not get 'No documents match the query.' but a positive number of results
	    elemContainsText(result, MATCHES_PATTERN, "At least \\d+ documents match the query.");
	    System.err.println("@@@ Got search results for '" + query + "': " + result.getText());
	    PAUSE(1);

	    // Click on lucene-jdbm-demo result of Cross collection search.
	    // In Cross Coll Search results:
	    // Position 1 contains nothing visible, Pos 2 contains the search doc icon,
	    // Pos 3 contains the title of section and Pos 4 contains the collection name.
	    xPath = "//tr[td[3]/text()[contains(., 'West Africa')]][td[4]//text()[contains(., 'Demo Collection (Lucene)')]]/td["+POS_DEFAULT_SEARCH_DOC_ICON+"]/a";

	    
	    // On Mac, the xpath is all wrong and is out of bounds for clicking.
	    // Manually trying in the browser, it ought to be all indices offset by +1:
	    // //tr[td[4]/text()[contains(., 'West Africa')]][td[5]//text()[contains(., 'Demo Collection (Lucene)')]]/td[3]/a
	    // yet this does not work when testing. It's not even found, not just
	    // out of bounds for clicking. What does work is the 1st actual doc link we want to click:
	    //if(Platform.osFamily() == OSFamily.MAC) {
		xPath = "//a[contains(@href,'lucene-jdbm-demo/document/ec160e.5.1.12')]";
	    //}
	    // On Windows and now Mac too (both cases attempted on firefox), clicking on the link didn't work
	    //if(Platform.isWindows() || Platform.osFamily() == OSFamily.MAC) {
			WebElement linkEl = getElement(xPath);
			JavascriptExecutor executor = (JavascriptExecutor)_driver;
			executor.executeScript("arguments[0].click();", linkEl);
			waitForPageLoad();
			PAUSE(2);			
	    //} else { // Linux
			//clickLink(xPath);
	    // }
		
	    // Check docTitle is as expected
	    expected = //"The Courier - N°160 - Nov - Dec 1996 - Dossier Habitat - Country reports: Fiji , Tonga";
			"160 - Nov - Dec 1996 - Dossier Habitat - Country reports: Fiji , Tonga";

	    elem = getElement("//p[contains(@class, 'sectionHeader')]/span");
	    errMsg = "### ASSERT ERROR: document title is not expected:\n\t" + expected
		+ "\n\tBut: " + elem.getText();
	    Assert.assertTrue(errMsg, elem.getText().contains(expected));
	    System.err.println("@@@ A search result for 'economy' led to expected"
			       + " lucene-jdbm-demo collection doc:\n\t" + expected);


		Actions actions;
		// On Windows (firefox) and Mac (firefox) too now: need to scroll up AND then
		// locate and click on the document (particularly
		// element click on div class="wrapper col2", as clicking on div class="wrapper" was insufficient)
		// in order for clicking on Search > Text Search hereafter to work.				
		// https://stackoverflow.com/questions/36647785/scroll-up-the-page-to-the-top-in-selenium
		//if(Platform.isWindows() || Platform.osFamily() == OSFamily.MAC) {
			actions = new Actions(_driver);
			actions.sendKeys(Keys.HOME).build().perform();
			PAUSE(1);
			//WebElement searchEl = _driver.findElement(By.xpath("//div[@id='topnav']/ul/li/a[text()='Search']"));
			//((JavascriptExecutor) _driver).executeScript("arguments[0].scrollIntoView(true);", searchEl);
			//PAUSE(0.5);
			WebElement doc = _driver.findElement(By.xpath("//div[contains(@class,'wrapper') and contains(@class, 'col2')]"));//"//div[@class='wrapper']//div[@class='paramLabel'][1]"));//"//div[@class='wrapper']"));
			doc.click();
			PAUSE(0.5);
		//}
		
	    // Now on document page, we should be able to do a collection-level search
	    // This allows us to test the navigation bar.
		// Need to hover over Search > click Text Search:	    
	    // https://www.browserstack.com/guide/mouse-hover-in-selenium
		
	    // Locating the Main Menu (Parent element)
	    String navXPath = "//div[@id='topnav']/ul/li";
	    WebElement mainMenu = findElementByXPath(_driver, navXPath+"/a[text()='Search']");
	    //Instantiating Actions class
	    actions = new Actions(_driver);	    
	    //Hovering on main menu
	    actions.moveToElement(mainMenu);
	    // Locating the element from Sub Menu
	    WebElement subMenu = findElementByXPath(_driver,
						    navXPath+"/ul/li/a[text()='Text Search']");
	    //To mouseover on sub menu
	    actions.moveToElement(subMenu);
	    //build()- used to compile all the actions into a single step 
	    actions.click().build().perform();

	    // Greenstone has stored the search query ('economy'), so just click Search button
	    // But there's 2 search buttons, get the 2nd one:
	    PAUSE(2);
	    WebElement searchBox = getElement("//div[@id='gs_content']//input[@name='s1.query']");
	    searchBox.clear();
	    searchBox.sendKeys(query); // searching for economy again, but just in this collection
	    clickLink("//div[@id='gs_content']//input[@type='submit' and @value='Search']");
	    checkSearchResults(20, // num results on first page
			       "72 sections match the query.",
			       "'economy' occurs 159 times in 72 sections");

	    // Finally go back to the home page, where there should be no menu
	    clickLink(navXPath + "/a[text()='Home']");

	    // Now on the main page, //div[@id='topnav']/ul exists, but no li inside it
	    elementShouldNotExist(navXPath);
	    System.err.println("@@@ Main page confirmed to not yet have navigation menus.");
	    
	    break;
	case 5:
	    xPath = "//div[@id='header']";	    
	    expected = "The Best Digital Library";
	    elem = getElement(xPath);
	    errMsg = "### ASSERT ERROR: %s page title wasn't "+expected+" but "+elem.getText();
	    Assert.assertTrue(String.format(errMsg, "library home"),
			      elem.getText().contains(expected));
	    System.err.println("@@@ Library home page title was as expected "+ expected);
	    
	    // Click a collection page, e.g. backdrop
	    clickLink("//div[@id='hpage_cats']/div/h2/a[contains(text(),'backdrop')]");
	    // repeat main title test
	    elem = getElement(xPath);
	    Assert.assertTrue(String.format(errMsg, "backdrop collection"),
			      elem.getText().contains(expected));
	    System.err.println("@@@ Backdrop collection main title was as expected "+ expected);
	    // Now check subtitle of backdrop collection
	    expected = "backdrop";
	    elem = getElement(xPath+"//p");
	    Assert.assertTrue(String.format(errMsg, "backdrop collection sub"), // subtitle
			      elem.getText().contains(expected));
	    System.err.println("@@@ Backdrop collection sub title was as expected "+ expected);
	    break;
	case 6:
	    // repeat test from earlier tutorial checking login/help/preferences links when
	    // logged in vs logged out
	    xPath = "//div[@id='topline']//ul/li/a[contains(text(),'%s')]"; // contains() this time
	    BrowserTest.checkLoginLinks(xPath);

	    // TODO: optionally can also click on Preferences and Help and check them
	    
	    break;
	default:
	    System.err.println("### Unknown preview test number " + nthPreview
			       + " for BrowserTest.designingANewInterface3().");
	    
	}
    }
}


