package gstests.tutorials;

/*
Ctrl+Shift+A TO ABORT A RUNNING GUI-TEST CYCLE (won't abort Selenium/browser tests)

Using AssertJ Swing to do Java GUI testing (of swing classes)
- Obtain from: https://search.maven.org/search?q=assertj-swing-junit
- Documentation: https://joel-costigliola.github.io/assertj/assertj-swing.html

Alternatives to AssertJSwing (googled: automate java interface testing) suggested at
https://sqa.stackexchange.com/questions/18554/open-source-tools-for-automation-of-java-gui-application-testing

Event Dispatch Thread (EDT) pages:
- https://joel-costigliola.github.io/assertj/assertj-swing-edt.html
- https://web.archive.org/web/20120526191520/http://alexruiz.developerblogs.com/?p=160
- https://web.archive.org/web/20130218063544/http://weblogs.java.net/blog/alexfromsun/archive/2006/02/debugging_swing.html
- https://stackoverflow.com/questions/2829364/java-difference-between-swingworker-and-swingutilities-invokelater

Got AssertJ Swing from Maven Central Repository:
https://search.maven.org/search?q=assertj-swing-junit
-> http://central.maven.org/maven2/org/assertj/assertj-core/3.8.0/
More jar files: http://repo1.maven.org/maven2/org/assertj/
- http://repo1.maven.org/maven2/org/assertj/assertj-swing/3.8.0/
- http://central.maven.org/maven2/org/assertj/assertj-core/3.8.0/

API:
https://joel-costigliola.github.io/assertj/swing/api/index.html

JUNIT:
- https://junit.org/junit4/faq.html#atests_2
   How do I use a test fixture?
- https://junit.org/junit4/faq.html#organize_3
   How can I run setUp() and tearDown() code once for all of my tests?

*/

// Junit imports
import org.junit.AfterClass;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

// GLI imports
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.GathererProg; // main GLI class we'll be testing
import org.greenstone.gatherer.Dictionary; // access to display strings
import org.greenstone.gatherer.util.SafeProcess;

// Java GUI testing with AssertJ Swing
import org.assertj.swing.junit.testcase.AssertJSwingJUnitTestCase;
import org.assertj.swing.fixture.*;
import org.assertj.swing.core.*; // Robot, Settings
import org.assertj.swing.data.Index;
import org.assertj.swing.util.OSFamily;
import org.assertj.swing.util.Platform;
import org.assertj.swing.timing.Timeout;

// Selenium
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
//import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.safari.SafariDriver;

//import org.openqa.selenium.Alert;
////import org.openqa.selenium.support.ui.Wait;
//import org.openqa.selenium.support.ui.WebDriverWait;
//import org.openqa.selenium.support.ui.ExpectedConditions;

// Helper classes for selenium and AssertJ Swing tests
import org.greenstone.gsdl3.testing.GSSeleniumUtil;
import org.greenstone.gsdl3.testing.GSGUITestingUtil;
import org.greenstone.gsdl3.testing.GSBasicTestingUtil;
import static org.greenstone.gsdl3.testing.GSBasicTestingUtil.ENCODING;

// Java imports
import java.awt.event.InputEvent;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
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;

//import java.io.InputStream;
//import java.io.IOException;

// static imports - call static functions without class as namespace qualifier
import static org.greenstone.gsdl3.testing.GSGUITestingUtil.*;

/**
 * Class that will eventually go through all the Greenstone3 tutorials by running GLI and GEMS.
 *
 * Don't need to have a tearDown() that will do window.cleanUp as the test class inherits
 * from AssertJSwingJUnitTestCase, so that it does the teardown cleanup automatically.
 * https://joel-costigliola.github.io/assertj/assertj-swing-getting-started.html
 * https://joel-costigliola.github.io/assertj/assertj-swing-basics.html
 */
public class RunGLITest extends AssertJSwingJUnitTestCase {
    // For aborting running test suite, with Ctrl+Shift+A by default
    // https://joel-costigliola.github.io/assertj/assertj-swing-running.html
    private static EmergencyAbortListener abortListener = EmergencyAbortListener.registerInToolkit();

    // https://www.browserstack.com/guide/run-selenium-tests-on-safari-using-safaridriver
    static {
	// https://stackoverflow.com/questions/9907492/how-to-get-firefox-working-with-selenium-webdriver-on-mac-osx
	// https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/firefox/FirefoxBinary.html
	if(Platform.osFamily() == OSFamily.MAC) { //if(Platform.isMacintosh()||Platform.isOSX()) {
	    
	    //FirefoxOptions options = new FirefoxOptions()
	    //	.setBinary("/Volumes/Macintosh HD/Applications/Firefox.app/Contents/MacOS/firefox");
		
	    // https://stackoverflow.com/questions/32175170/firefox-is-showing-cannot-find-firefox-binary-in-path-error-in-mac-os
	    System.setProperty("webdriver.firefox.bin","/Volumes/Macintosh HD/Applications/Firefox.app/Contents/MacOS/firefox");
	}

	// Still can't get mac to do any remote controlling of the mouse/keyboard
	// https://github.com/assertj/assertj-swing/issues/224
    }

    // Set up browser driver used by Selenium portion of tests
    private static WebDriver _driver = new FirefoxDriver();
    //private static WebDriver _driver = (Platform.isMacintosh() || Platform.isOSX()) ?
    //new SafariDriver() : new FirefoxDriver();

    
    private FrameFixture window;

    private static Properties props;
    
    public static final String ANT_COMMAND = Platform.isWindows() ? "ant.bat" : "ant";

    public static boolean USE_GSI_TO_RESTART_SERVER = false; // false: uses CMDLINE server restart

    public static final boolean HACK_DONOT_ICONIFY_GLI = true;

    
    // Changing Exit SecurityManager is now unused as GLI now doesn't System.exit in testing mode
    // https://stackoverflow.com/questions/50815329/close-application-after-test-without-test-failure-using-assertj
    // https://stackoverflow.com/questions/23139321/testing-java-gui-application-closing-window-during-test-ends-test-suite
    // https://stackoverflow.com/questions/5401281/preventing-system-exit-from-api
    
    @BeforeClass
    public static void initOnce() {
	// to prevent system.exit from causing issues
	//org.assertj.swing.security.NoExitSecurityManagerInstaller.installNoExitSecurityManager();

	if(System.getenv("GSDL3SRCHOME") == null) {
	    System.err.println("@@@ GS3 environment not set. Terminating tests.");
	    System.exit(-1);
	}

	props = new Properties();
	loadProperties();
    }
    
    // Selenium
    @Before
    public void init()
    {
		if(Platform.isWindows()) {
			PAUSE(10);
		}
	//https://stackoverflow.com/questions/38676719/selenium-using-java-the-path-to-the-driver-executable-must-be-set-by-the-webdr
	// GS3's build.xml would have set the webdriver.gecko.driver path System.Property to
	// the location of Firefox' geckodriver executable when launching this test class already.
	// So now we can continue to just do:
	_driver.get(System.getProperty("SERVERURL"));

	// Robot is the Assertj Swing object used to find GUI elements for us
	// IMPORTANT, note the call to 'robot()': must use the Robot from AssertJSwingJUnitTestCase
	// So we shouldn't instantiate our own robot, as only one can be in use at a time
	GSGUITestingUtil.setRobot(robot());

	// For selenium testing
	GSSeleniumUtil.setDriver(_driver);
	BrowserTest.setDriver(_driver);
    }
    

    // assertj-swing
    // As init() above, onSetUp() gets called before every test method too,
    // but is overridden from assertj-swing base class
    @Override
    protected void onSetUp() {
	System.err.println("#### onSetUp called - running GLI");
	// Launch GLI and then get a ref to the launched app window:
	runGLI();
	
	// IMPORTANT, note the call to 'robot()': must use the Robot from AssertJSwingJUnitTestCase
	//window = findFrame("GUIManager").using(robot());
	window = GSGUITestingUtil.getGLIApplicationWindow(robot());
	
	Settings robotSettings = robot().settings();
	System.err.println("@@@ Robot settings - visibility timeout: " + robotSettings.timeoutToBeVisible());
	robotSettings.dragDelay(250);
	robotSettings.dropDelay(250);	
	//robotSettings.idleTimeOut(250);

    }
    
    
    //@Test
  public void testGLIRunning() {
      // waiting a few seconds for window, so we can see it
      PAUSE(2);
      makeGLIWindowActive();
      
      System.err.println("@@@ First test: GLI Running");
      
      // steal any collection lock that may or may not presents itself within param seconds
      stealAnyCollectionLock(1);
      
      String expectedWindowTitle = Gatherer.PROGRAM_NAME;

            
      String gatherPaneLabel = Dictionary.get("GUI.Gather");
      System.err.println("@@@ Expecting label: " + gatherPaneLabel);
      
      System.err.println("@@@ Second test: that Gather panel is selected and has right title");
      
      JTabbedPaneFixture tab = window.tabbedPane("GUIManager.tab_pane");
      
      
      tab.requireSelectedTab(Index.atIndex(1));
      tab.requireTitle(gatherPaneLabel, Index.atIndex(1));
      
      // attempt to switch to enrich pane, uses static methods of GSGUITestingUtil
      // through static import of that class
      switchToPane(DOWNLOAD_PANE);

      // For testing, want GLI to be in librarian mode by default
      changeUserMode("librarian");
            
      switchToPane(GATHER_PANE);
      
      loadCollection("lucene-jdbm-demo");

      // wait a couple of seconds again?
      PAUSE(2);      
      closeCollection();
      
      createCollection_manual("pinky", "Pinky was here", null);
      closeCollection(); // collection must be closed in order to be deleted
      deleteCollection("pinky");
      // wait a few of seconds again?
      PAUSE(2);
      
      exportCollection("GreenstoneMETS", "lucene-jdbm-demo");
      
      // Reset GLI to expert mode for regular GLI use
      changeUserMode("expert");
      
      PAUSE(2);      
      changeUserMode("librarian"); // change it back for other tests
      
      switchToPane(GATHER_PANE);      
      
      //exitGLI(); //System.exit() on GLI won't allow quit to be called on
      // Selenium's web _driver, why? Because System.exit exited the JUnit tests too.
      // Fixed in GLI: now, not only if applet, but also if testing, GLI will only
      // Dispose On Close, not additionally call System.exit(). So exitGLI() works
      // But only call this once per test or remember to launch GLI after every exitGLI.
      // Now we exitGLI in tutorial_tests() so don't call it here.
      
      PAUSE(3);
  }


    @Test
    public void tutorial_tests() {

	//_driver.manage().window().setSize(new org.openqa.selenium.Dimension(1300, 1400));
	
	// At this point in the code Safari is in focus, but automated tests want to click in GLI.
	// SafariDriver doesn't allow clicking and actual keypresses, the automation appears
	// to be emulation of the same signals sent by Selenium.
	// When automated clicks meant to go to GLI are sent to the Safari window because it's
	// the application that has focus, SafariDriver thinks a human is instigating the
	// clicks and that this implies the human wants to probably end automated tests and
	// assume control of browser. So Safari produces an option dialog, informing the user
	// that the browser is in the mode to do automated tasks and asking if the user wants
	// to Continue the automation Session or take control. We can't automate clicking
	// in the options dialog, and the actual solution is as simple as making sure GLI
	// is the active window at this point of the testing code.
	// The actual problem described above became clear when I read the following after
	// googling why this popup dialog kept appearing:
	// https://stackoverflow.com/questions/51397823/safari-driver-asking-every-time-to-enable-allow-remote-automation-in-selenium
	// "I'm only shown the "This Safari window is remotely controlled by an automated test." message if I try to interact with the safari window using my mouse/keyboard. If I don't click on the window, the automation script continues to execute without problems. What's the version of Safari on your machine?"
	// https://developer.apple.com/documentation/webkit/testing-with-webdriver-in-safari
	// https://stackoverflow.com/questions/37802673/allow-javascript-from-apple-events-in-safari-through-terminal-mac/39323358#39323358
	// https://stackoverflow.com/questions/41629592/macos-sierra-how-to-enable-allow-remote-automation-using-command-line
	// https://stackoverflow.com/questions/57617400/selenium-java-in-safari-switch-to-new-tab-is-not-working
	// https://stackoverflow.com/questions/51397823/safari-driver-asking-every-time-to-enable-allow-remote-automation-in-selenium
	// https://www.lambdatest.com/blog/selenium-safaridriver-macos/
	// https://stackoverflow.com/questions/62246240/disable-automation-warning-in-safari-when-using-selenium
	makeGLIWindowActive(); // Ensure GLI is active, taking focus away from Safari
	
	// Preliminaries
	// waiting a few seconds for window, so we can see GLI running
	//PAUSE(2);

	// steal any collection lock that may or may not present itself within param seconds
	stealAnyCollectionLock(1);
	// wait for optional loading of previous collection to finish
	waitForAnyProgressPopupToGoAway(Dictionary.get("CollectionManager.Loading_Collection"),
					MAX_WAIT_TIMEOUT_SECONDS);
	//PAUSE(3);
	
	switchToPane(GATHER_PANE);
	PAUSE(3); // Need some time before we start making selections in the workspace tree

	//changeUserMode("librarian");
	List switchOffWarnings = new ArrayList(1);
	switchOffWarnings.add(Dictionary.get("NoPluginExpectedToProcessFile.Title"));
	setPrefs("librarian",  
		 null, // general tab
		 null, // connection tab
		 switchOffWarnings);
	switchOffWarnings.clear();
	
	//switchToPane(GATHER_PANE);
	
	// dummy tests
	//testGLIRunning();

	// Test phasing out pauses waiting for creating/loading coll progress popups to disappear
	// Start a new collection within the Librarian Interface:
	//createCollection("Bla", "", null);
	//closeCollection();
	//deleteCollection("Bla");
	//PAUSE(2); // don't need pause now we have waitForAnyProgressPopupToGoAway()
	
	//loadCollection("Bla");//loadCollection("smallhtm");
	//closeCollection();
	//String dragNodePath = openWorkspacePath(WKS_TOP_FOLDER_LOCAL, props.get("samplefiles.treepath").toString()+"/simple_html/html_files", SELECT);

	// First tutorials
	
	turnOnDragNDrop();

	// Drag n drop can be on or off for spotTest, depositorTest and docEditing
	// tests demo collections and creating a very basic collection
	// This is the spot test I do of final release binaries
	final String NEWCOL = "testnew";
	
	spotTest(NEWCOL);
	//deleteCollection(NEWCOL);
	depositorTest(NEWCOL); // builds on NEWCOL collection
	
	documentEditing(); // dragNDrop can be on or off when documentEditing(), as we don't use it in this test	
	
	// dragNdropTest() can now go first or last and is independent of all others (it creates the
	// sample_files workspace shortcut for itself, as it does nothing noticeable if it already exists)
	dragNdropTest();
	simpleHTML();
	
	// For all subsequent tutorials, turn off drag N drop since that has been sufficiently tested
	// and my usage of assertj swing's single file dragNdrop to do multi selection dragNdrop is buggy (no
	// reflection on GLI's dragNdrop abilities, which are fine). If dragNdrop tests above passed, GLI's
	// dragNdrop works as always. If the above tests failed, GLI's dragNdrop has so far always been
	// fine still, and it's rather my buggy/non-robust usage of assertj swing's single file dragNdrop
	// to do multi selection dragNdrop that failed.
	turnOffDragNDrop();
	
	backdrop();
	
	imagesGPS(); // need google key to do proper browser (selenium) testing, but gli's fine
	
	reports();	
	formattingReports();
	
	// The Enhanced Word Document Handling tutorial
	if(Platform.isWindows()) {
	    enhancedReportsTut_windowsScriptingAndTika();
	} else { // do only tika processing docx part of the enhanced reports collection on non-Windows OS
	    enhancedReportsTut_tika();
	}

	pdfCollection();

	associatedFiles();
	
	tudor();
	tudor2_enhanced();
	tudor3_formatting();	
	tudor4_webLink();
	
	sectionTaggingForHTML();

	downloadingTudorFromWeb();

	bibliographicColl_MARC();

	CDS_ISIS();

	lookingAtMultimedia();
	//undoCustomisationTutorials();//undoCustomisingHomePage();
	//undoCustomisingThemes();
	multimedia();

	scannedImages();
	advancedScannedImages();
	
	OAICollection();
	
	undoUnknownConverterPlugTut();
	unknownConverterPluginTutorial();
	
	// downloadingOverOAI needs server to have restarted after creating backdrop collection
	// so do this tutorial after unknownConverterPluginTutorial() as that does a server restart
	downloadingOverOAI(); //downloadingOverOAI_browserTestingOnly();

	METS();
	stoneD();

	gs_to_dspace_cmdl(); // no browser testing

	indexers();

	incrementalBuilding();
	
	undoOAIServerTutorialChanges();
	OAIserver();
	
	undoCustomisingThemes(); // can be run regardless of if customisingThemes() was ever run
	customisingThemes(); // puts admin pwd and default theme back at the end
	undoCollectionTheme(); // can be run even if collectionSpecificTheme() was never yet run
	collectionSpecificTheme();
	
	// customisation tutorials, must be done in sequence as some customisation-tut-tests
	// build on assumptions of previous customisation tutorials having been done
	
	// first undo customisation tutorials (undoes happen in reverse order and reverse tut order)
	boolean success = undoCustomisationTutorials(); // no effect if customisation tuts were never run
	//undoDesigningANewInterface();
	if(!success) {	    
	    Assert.fail(
		"### ASSERT ERROR: Can't run customisation tutorial tests due to above reported"
		+ "\nissues when attempting to prep for them with undoCustomisationTutorials()");
	}
	
	// Can at last attempt to run customisation tutorials in sequence
	refreshServerAndPreview();
	customisingHomePage();
	definingLibraries();
	
	//undoDesigningANewInterface();
	designingANewInterface1();
	designingANewInterface2();
	designingANewInterface3();

	
	GEMStutorial(); // no browser testing
		
	exitGLI(); // needed, to save user choices regarding warning popups
    }

    // Selenium
    // called once and only once: to quit the firefox driver geckodriver
    @AfterClass
    public static void destroy()
    {
	abortListener.unregister(); // aborting assertJ-swing GUI tests
	// https://www.browserstack.com/guide/run-selenium-tests-on-safari-using-safaridriver
	// https://stackoverflow.com/questions/15067107/difference-between-webdriver-dispose-close-and-quit
	_driver.quit();
	//After exiting your application uninstall the no exit security manager
	//org.assertj.swing.security.NoExitSecurityManagerInstaller.installNoExitSecurityManager().uninstall();
    }

    /****************************** TUTORIAL FUNCTIONS  *******************************/
    // Tutorial functions: regular methods called by JUnit at-Test methods in sequence
    // to ensure sequential ordering of tests
    public void simpleHTML() {
	System.err.println("@@@ Tutorial 1: simple HTML");
	//loadCollection("smallhtm");
	//PAUSE(2);
	/*
	stealAnyCollectionLock(1);
	switchToPane(GATHER_PANE);
	changeUserMode("expert");
	//openWorkspacePath(WKS_TOP_FOLDER_LOCAL, props.get("samplefiles.treepath").toString());

	
	loadCollection("lucene-jdbm-demo");
	// wait a couple of seconds for loading to be done
	PAUSE(2);
	openCollectionPath("b17mie/b17mie.htm", CLICK);
	PAUSE(3);
	openWorkspacePath(WKS_TOP_FOLDER_LOCAL, props.get("samplefiles.treepath").toString());	
	*/

	// Start a new collection within the Librarian Interface:
	createCollection_manual("Small HTML Collection",
			 "A small collection of HTML pages.",
			 null);
	
	String dragNodePath = openWorkspacePath(WKS_TOP_FOLDER_LOCAL, props.get("samplefiles.treepath").toString()+"/simple_html/html_files", SELECT);

	String collPath = dragNDrop(dragNodePath,
				    "",
				    new Runnable() {
					public void run() {
					    closePopup(OPTIONAL, Dictionary.get("NoPluginExpectedToProcessFile.Title"), Dictionary.get("General.OK"), TICK_CHECKBOX);				    
					}
				    });

	
	openCollectionPath("html_files", DOUBLE_CLICK);
	//PAUSE(2);
	
	//changeUserMode("librarian");
	//loadCollection("smallhtm");
	
	switchToPane(CREATE_PANE);
	//PAUSE(2);
	//buildCollection();
	//previewCollectionWhenReady(MAX_BUILD_TIMEOUT);
	buildCollectionTillReady(MAX_BUILD_TIMEOUT);	
	//closePopup("Collection Creation Result", "OK", DO_NOT_TICK_CHECKBOX);
	//closePopup(OPTIONAL, Dictionary.get("CollectionBuilt.Title"), Dictionary.get("General.OK"), TICK_CHECKBOX);
	closePopup(OPTIONAL, Dictionary.get("CollectionBuilt.Title"), Dictionary.get("General.OK"), TICK_CHECKBOX);	
	String previewURL = previewCollection();
	_driver.get(previewURL);

	BrowserTest.simpleHTML();
	
	PAUSE(1.5);
	makeGLIWindowActive();
	PAUSE(1);
	
	switchToPane(ENRICH_PANE);	
	closeCollection();


	String path = openWorkspacePath(WKS_TOP_FOLDER_LOCAL, props.get("samplefiles.treepath").toString(),
					RIGHT_CLICK, false); // false: not for dragndrop but just file selection
	PAUSE(1);
	selectContextMenu(Dictionary.get("MappingPrompt.Map")); // Create Shortcut
	closePopup(Dictionary.get("MappingPrompt.Title"), Dictionary.get("General.OK")); // accept shortcut name

	collapseWorkspacePath(WKS_TOP_FOLDER_LOCAL);
	PAUSE(3);
    }

    // The quick spot testing of binaries: lucene-jdbm-demo, solr-jdbm-demo, and new test coll
    public void spotTest(final String NEWCOL) {
	System.err.println("@@@ Test Overview: Preview pre-built lucene-jdbm-demo to test searching, browsing and document view."
			   + "\n   Repeat testing lucene-jdbm-demo after rebuilding it."
			   + "\n   Test pre-built then the rebuilt solr-jdbm-demo collection similarly."
			   + "\n   Finally, a very basic collection (" + NEWCOL + ") is created, 3 text files are added in to it"
			   + "\n   and the collection is built and previewed to test the basic operations (search, browse, doc view).");
	
	loadCollection("lucene-jdbm-demo");
	switchToPane(CREATE_PANE);

	// build if not pre-built. Preview either way
	if(!isPreviewButtonEnabled()) {
	    // lucene-jdbm-demo ought to be pre-built in a GS3 binary
	    File srcCode = new File(System.getenv("GSDL3SRCHOME"), "src");
	    if(!srcCode.exists()) {
		//Assert.fail("### This appears to be a binary installation, yet lucene-jbdm-demo collection wasn't pre-built!");
		System.err.println("\n\n### This appears to be a binary install,\n"
		   + "### yet the lucene-jbdm-demo collection wasn't pre-built!\n\n");
	    }
	    createBuildPreviewCycle(new Runnable() {
		    public void run() {
			BrowserTest.spotTest(1); // preview test 1
		    }
		});
	} else { // lucene-jdbm-demo is pre-built, so just preview
	    switchToPane(FORMAT_PANE);
	    quickFormatPreviewCycle(new Runnable() {
		    public void run() {
			BrowserTest.spotTest(1); // search and browse lucene-jdbm-demo (still just preview 1)
		    }
		});
	    // after rebuilding, redo same searches and do more browsing of lucene-jdbm-demo
	    createBuildPreviewCycle(new Runnable() {
		    public void run() {
			BrowserTest.spotTest(2);
		    }
		});
	}
	
	
	loadCollection("solr-jdbm-demo");
	// test basic searching and browsing of solr-jdbm-demo
	switchToPane(FORMAT_PANE);
	quickFormatPreviewCycle(new Runnable() {
		    public void run() {
			BrowserTest.spotTest(3);
		    }
		});	
	
	// after rebuilding, redo same searches and do more browsing of solr-jdbm-demo
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.spotTest(4);
		}
	    });
	
	
	// Very basic test of creating a new collection: can we create a new coll, build it,
	// then search and browse it successfully?
		
	switchToPane(GATHER_PANE);
	createCollection(NEWCOL, null, null);
	
	String collectionPath = "";
	String workspaceFolder = "";
	workspaceFolder = openWorkspacePath(WKS_TOP_FOLDER_LOCAL, toWorkspaceTreePath(System.getenv("GSDL3SRCHOME")),
					    EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/LICENSE.txt", workspaceFolder+"/LOCAL.txt",
				     collectionPath);
	dragNDrop(workspaceFolder+"/README.txt", collectionPath);
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.spotTest(5); // test basic searching and browsing of new col
		}
	    });
	closeCollection();
	
	
	// For quick testing of NEWCOL
	/*
	loadCollection(NEWCOL);
	switchToPane(FORMAT_PANE);
	quickFormatPreviewCycle(new Runnable() {
		    public void run() {
			BrowserTest.spotTest(5);
		    }
	    });
	*/
	//

    }

    public void depositorTest(final String COL) {
	System.err.println("@@@ Test Overview: depositorTest() will use the Depositor to add 2 docs along with metadata to col:"
			   + "\n\t'" + COL + "', rebuilding and previewing to check for the newly added document each time."
			   + "\n\tOne of the documents has a new metadata field added then checked for during preview.");
	minimiseGLI();
	focusBrowser();	
	
	BrowserTest.depositorTest(COL);
	
	// Back in GLI, can get rid of collection
	normaliseGLI();//makeGLIWindowActive();
	//switchToPane(GATHER_PANE);
	PAUSE(1);
	//closeCollection(); // it wasn't open in GLI, spotTest() already closed coll
	deleteCollection(COL);
    }

    public boolean undoDocEditing(String uc_coll, String uc_docid) {
	boolean success = false;
	System.err.println("@@@ In undoDocEditing");
	
	File storedMetaXML = new File(System.getenv("GSDL3SRCHOME")
		      +"/ext/testing/src/tmp/meta_"+uc_coll+"_"+uc_docid+".xml");
	File metaFile = new File(System.getenv("GSDL3HOME")
		 +"/sites/localsite/collect/"+uc_coll+"/import/"+uc_docid+"/metadata.xml");
	if(storedMetaXML.exists()) {
	    // restore web\sites\localsite\COLL\import\DOCID\metadata.xml
	    try {
		FileUtils.copyFile(storedMetaXML, metaFile);// can create dir, will overwrite file
		// delete from testing/tmp
		GSBasicTestingUtil.delete(storedMetaXML.getAbsolutePath());
		// Need to rebuild and reactivate collection
		if(Platform.isWindows()) {
		    String[] cmd = {"perl", "-S", "full-rebuild.pl", uc_coll};
		    runCommand(cmd);
		} else {
		    String[] cmd = {"full-rebuild.pl", uc_coll};
		    runCommand(cmd);
		}
	    } catch(Exception e){
		Assert.fail("### ERROR: undoDocEditing()--error restoring metadata.xml for doc "
			    + uc_docid + " of collection " + uc_coll
			    + "\n\tfrom " + storedMetaXML + "\n\tto " + metaFile);
	    }
	} else {
	    try {
		System.err.println("@@@ Backing up " + metaFile + "\n\tto " + storedMetaXML);
		FileUtils.copyFile(metaFile, storedMetaXML);
	    } catch(Exception e){
		Assert.fail("### ERROR: undoDocEditing()--error restoring metadata.xml for doc "
			    + uc_docid + " of collection " + uc_coll
			    + "\n\tfrom " + metaFile + "\n\tto " + storedMetaXML);
	    }
	}

	success = true;
	return success;
    }
	
    public void documentEditing() {
	System.err.println("@@@ Test Overview: the documentEditing() test will have admin add a custom user."
			   + "\n   The document editing operations will then be performed as that user."
			   + "\n   At test end, the user will be deleted again.");
	System.err.println("@@@ This test also combines user comments testing when logged in as admin and as the custom user");

	int exitValue = -1;
	
	// The coll and doc for user comment testing
	final String uc_coll = "lucene-jdbm-demo";
	final String uc_docid = "b22bue";

	// Turn on user comments feature
	loadCollection(uc_coll);
	switchToPane(FORMAT_PANE);
	PAUSE(1);
	formatFeature("display");
	PAUSE(3);
	// Enable user comments for this tutorial, iff not already done so
	//String tutorial = "documentEditing";
	//TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	if(!formatFeatureTextContains("allowUserComments")) {
	    //tutorialEdits.getEdit(editcount++).execute();	    
	    tutorials.get("documentEditing").getEdit(0).execute();	    
	}

	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    // previewing will make the above format change active
		}
	    });
	
	//int editcount = 1; // always 1 at this point, in case there'll be more TutorialEdits
	// added to the documentEditing() "tutorial" in the tutorial-edits.yaml file in future
	closeCollection();
	//minimiseGLI(); // happens below
	//focusBrowser();
	
	// Back up metadata.xml file for the doc of the collection where user comments
	// are to be added/deleted

	// Back up the user comment doc's metadata.xml, as we want to put it back at end of test
	undoDocEditing(uc_coll, uc_docid);
	
	// 1. Deactivate recaptcha (if not already deactivated, e.g. due to previous test failure)
	File siteConfigFile = new File(System.getenv("GSDL3HOME")
				       +"/sites/localsite/siteConfig.xml");
	String activeRecaptchaLine = "<recaptcha name=\"operations\" value=\"Register,AddUser\"/>";
	String inactiveRecaptchaLine = "<!--" + activeRecaptchaLine + "-->";

	// If the file didn't get into a state with the inactiveRecaptchaLine already in it
	// then deactivate the active recaptcha line (will fail if active captcha line is missing)
	if(!GSBasicTestingUtil.fileContainsLines(siteConfigFile.getAbsolutePath(),
		new String[]{inactiveRecaptchaLine})) { // inactive recaptcha line not yet in the file, so it's active
	    // replace 1st occurrence and true to append new line, false for no newline
	    System.err.println("@@@ DEactivating recaptcha (edited siteConfig.xml) requires restart.");
	    GSBasicTestingUtil.replaceText(siteConfigFile, activeRecaptchaLine,
			   inactiveRecaptchaLine, 1, ASSERT_FAIL_ON_NO_MATCH, false);
	    exitValue = commandLineServerRestart();

	    if(exitValue != 0) {
		System.err.println("### ERROR in documentEditing():\n\t"
		   + "Restart error after modifying siteConfig.xml to DEactivate recaptcha.");
	    } else {
		System.err.println("@@@ Server restarted.");
		_driver.get(RunGLITest.getGSBaseURL()+"library"); //ensure server finished restart
	    }
	} else {
	    System.err.println("*** Recaptcha already deactivated in " + siteConfigFile);
	}
	
	// 2.a Login as admin, add/test user comments, then create custom user and log out admin.

	// log in as admin, add a new user, logout, login as new user
	// while logged in as admin, add 3 comments, check they're there and delete one, check the 2 first comments remain
	// while logged in as new user, add 3 comments, check they're there. Then delete two and one by admin, check 3 comments remain
	minimiseGLI();
	focusBrowser();
	BrowserTest.logInAdmin();
	BrowserTest.userCommentTest(1, uc_coll, uc_docid);
	
	BrowserTest.addUser(BrowserTest.custom_username, BrowserTest.custom_userpwd, "all-collections-editor");
	BrowserTest.logout();

	// 2.b Then login as custom user and add/test some more user comments as custom user
	BrowserTest.logIn(BrowserTest.custom_username, BrowserTest.custom_userpwd);
	System.err.println("@@@ Done. Logged on as user " + BrowserTest.custom_username);
	BrowserTest.userCommentTest(2, uc_coll, uc_docid);	
	
	normaliseGLI();
	
	// 3. Start the testing
	// a. Test the collection document
	final String[] COLLS = {"lucene-jdbm-demo"};//, "solr-jdbm-demo"};
	final String[] DOCIDS = {"b18ase"};//, "fb33fe"};
	//final String[] COLLS = {"lucene-jdbm-demo", "solr-jdbm-demo"};
	//final String[] DOCIDS = {"b18ase", "fb33fe"};

	for(int i = 0; i < COLLS.length; i++) {
	    final String COLL = COLLS[i];
	    final String DOCID = DOCIDS[i];
	    loadCollection(COLL, 3);
	switchToPane(FORMAT_PANE);
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.documentEditing(1, COLL, DOCID);
		}
	    });

	// c. Finished document editing, reset the collection by rebuilding and testing it's reset
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    System.err.println("@@@ Rebuilt lucene-jdbm-demo after document editing to reset all its text and metadata");
		    BrowserTest.documentEditing(2, COLL, DOCID);
		}
	    });
	closeCollection();
	}
	
	// If testing just user comments, comment out block containing points 3a - 3c above
	// and activate just this commented out section:
	/*
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    System.err.println("@@@ Rebuilt lucene-jdbm-demo after adding user comments");
		}
	    });
	closeCollection();
	*/
	
	// 4. logout custom user, login as admin and delete custom user
	// Then as admin do some more user comment testing
	minimiseGLI();
	focusBrowser();
	BrowserTest.logout(); // logout custom user
	System.err.println("@@@ User " + BrowserTest.custom_username + " logged out again.");
	
	// delete custom_username
	BrowserTest.logInAdmin();
	BrowserTest.deleteUser(BrowserTest.custom_username);

	BrowserTest.userCommentTest(3, uc_coll, uc_docid);
	BrowserTest.logout();
	// admin logged out, do final user comment test
	BrowserTest.userCommentTest(4, uc_coll, uc_docid);

	// Put the metadata.xml file of the doc tested for user comments back, since it got
	// slightly modified (gained empty XML elements), despite all comments being deleted
	String tmpDir = System.getenv("GSDL3SRCHOME")+"/ext/testing/src/tmp";
	String docImportDir = System.getenv("GSDL3HOME")
		 +"/sites/localsite/collect/"+uc_coll+"/import/"+uc_docid;

	// moveFileTo() overwrites any existing destfile
	GSBasicTestingUtil.moveFileTo(tmpDir, docImportDir,
				      "meta", // common file prefix
				      "_"+uc_coll+"_"+uc_docid+".xml", // fromSuffix
				      "data.xml"); //destSuffix
	PAUSE(2);	
	
	// 4. Reactivate recaptcha	
	System.err.println("@@@ REactivating recaptcha (edited siteConfig.xml) requires restart.");
	GSBasicTestingUtil.replaceText(siteConfigFile, inactiveRecaptchaLine, activeRecaptchaLine,
				       1, ASSERT_FAIL_ON_NO_MATCH, false);
	exitValue = commandLineServerRestart();
	if(exitValue != 0) {
	    System.err.println("### ERROR in documentEditing():\n\t"
	       + "Restart error after modifying siteConfig.xml to REactivate recaptcha.");
	} else {
	    System.err.println("@@@ Server restarted.");
	}
	
	normaliseGLI();
    }
    
    // Test some more dragging and dropping, before turning it off and using file-copy
    /**
     * We test all the dragging and dropping from GLI Gather pane's Workspace/Collection view into Collection
     * view here. This is because, although GLI's own drag and drop is robust (but we still want to test it),
     * we cheat in the testing code to make assertj swing give a semblance of multi-file drag and drop
     * though it only really implements single file drag and drop. The cheat is not robust however, so we
     * don't want our tutorial tests to fail just because automated testing's cheat implementation of
     * dragging and dropping in GLI isn't working. We're not testing the automated testing code's workaround
     * to do drag and drop in GLI works, but that GLI's own drag and drop functionality works.
     * So If this function works fine, it proves GLI's drag and drop works, but if this function fails at 
     * any point, it doesn't mean GLI's drag and drop is broken: it is likely to be our workaround solution
     * to GLI drag and drop that tripped as it sometimes does.
     * If testing fails here, it fails fast and with a try catch we could actually proceed with the rest of
     * the actual automated testing of the tutorials. This means the tutorials themselves don't need to use
     * drag and drop but can use the more trustworthy background file copying of files from the file system
     * (workspace view) into the new/open collection's import folder (collection view), followed by a refresh
     * of the Gather pane's Collection View to avoid the brittle automated testing drag n drop functioning,
     * since the latter anyway doesn't reflect poorly on GLI's abilities.
     *
     * Further details: assertj swing's JTreeFixture only really allows dragging and dropping one treenode,
     * not multiple. But I worked around things in the automated testing code to get the semblance of multiple
     * contiguously selected treenodes to drag and drop as follows: drag is effected on just the last of the
     * contiguously selected nodes, and dragging this then gives the *effect* of dragging and dropping the
     * entire selection when it goes right.
     * But it can go wrong. Difference with GLI: there is a wobble when dragging at the last selected node
     * in a multi-file selection that no amount of changing timings in assertj swing Settings() or calls
     * to PAUSE() at various times during the drag n drop operation seems to remove. Whether perhaps because
     * of this wobble, or otherwise, the multi-file drag and drop work around implemented with assertj
     * swing's JTreeFixture doesn't always work. At least, not for the way I've implemented it for multiple
     * (contiguously) selected files. Even implementing the drag n drop in a loop of maximum n (5) attempts
     * if the target files don't exist in the destination collection tree view doesn't always guarantee that
     * a drag n drop will work.
     * But such a sporadically occurring failure will result in an assertj swing assertion failure which
     * will then terminate all tutorial tests. We obviously don't want our tests to fail merely because
     * our *testing* implementation to exercise GLI's drag n drop isn't robust when GLI's own drag and drop is
     * fine and normal. Remember we're testing that GLI's own operations/implementation work, not testing
     * that our custom testing code to automate GLI's behaviour is working.
     */
    public void dragNdropTest() {
	String sampleFilesPath = (String)props.get("samplefiles.treepath");
	String collectionPath = "";
	String workspaceFolder = "";
	
	// Create sample files shortcut in the workspace view
	
	workspaceFolder = openWorkspacePath(WKS_TOP_FOLDER_LOCAL, props.get("samplefiles.treepath").toString(),
					    RIGHT_CLICK, false); // false: not for dragndrop but file selection
	PAUSE(1);
	selectContextMenu(Dictionary.get("MappingPrompt.Map")); // Create Shortcut
	closePopup(Dictionary.get("MappingPrompt.Title"), Dictionary.get("General.OK")); // accept shortcut name
	//collapseWorkspacePath(WKS_TOP_FOLDER_LOCAL);

	createCollection("dragdrop test",
			 "Testing single and multi files, auto add/skip plugin suggestions",
			 null);

	// Test copying from workspace shortcut (samplefiles) and an AddPlugin and DoNotAddPlugin
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "/dspace", EXPAND);
	dragNDrop(workspaceFolder+"/1", // 1 folder copy's enough to trigger AddPlugin suggestions
		  //dragNDropContiguousSelection(WORKSPACE_VIEW,
		  //	     workspaceFolder+"/1",
		  //	     workspaceFolder+"/5",
		  collectionPath,
		  new Runnable() {
		      public void run() {			  
			  // Don't add LOMPlugin
			  closeAddPluginPopup_doNotAdd(COMPULSORY);
			  // Add DSpacePlugin
			  closeAddPluginPopup_add(COMPULSORY);			  
		      }
		  });

	// Test copy from Local workspace folder and multi file copy: contiguous-single-contiguous
	workspaceFolder = openWorkspacePath(WKS_TOP_FOLDER_LOCAL, props.get("samplefiles.treepath").toString()+"/images", EXPAND);	
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/Bear.jpg", workspaceFolder+"/Gopher.jpg",
				     collectionPath);
	dragNDrop(workspaceFolder+"/Lemur.jpg", collectionPath);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/Rhino.jpg", workspaceFolder+"/Tiger.jpg",
				     collectionPath);
	collapseWorkspacePath(WKS_TOP_FOLDER_LOCAL);

	// test copying from Documents in Greenstone Collections folder and OPTIONAL addPlugin
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_DOCS_GS, "/localsite/Demo Collection (Lucene) [lucene-jdbm-demo]", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/b17mie",
				     workspaceFolder+"/wb34te",
				     collectionPath,
				     new Runnable() {
					 public void run() {
					     // Skip merging metadata: the dialog appears twice. NOT if basing coll on lucene-jdbm-demo
					     for(int i = 0; i < 2; i++) {
					         closePopupByName(COMPULSORY, "ModalDialog.MetadataImportMappingPrompt", Dictionary.get("MIMP.Ignore"));
					     }
					     
					     // Let GLI add the ImagePlugin
					     closeAddPluginPopup_add(OPTIONAL);
					 }
				     });

	// Test copying docs from the Workspace's Downloads shortcut
	// First put some files into Greenstone's DOWNLOADS folder
	GSBasicTestingUtil.copyToDirectory(
	   sampleFilesPath+"/beatles/advbeat_large/images",
	   topGLIFolder_toLocalPath_map.get(WKS_TOP_FOLDER_DOWNLOADS));
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_DOWNLOADS, "images", EXPAND);
	dragNDrop(workspaceFolder, collectionPath);

	// FINALLY, test dragging and dropping within CollectionView
	collectionPath = "images";	
	dragNDropContiguousSelection(COLLECTION_VIEW,
				     "Bear.jpg", "Tiger.jpg",
				     collectionPath);

	// Delete the dummy test collection
	closeCollection(); // collection must be closed in order to be deleted
	deleteCollection("dragdrop");
	
	/*
	// Nothing ensures that clicking on the Remove Shortcut rightclick menu be consistently effective
	// It is effective on rare occasion. And when it's not, we strangely end up with two identical
	// shortcuts, and then when subsequent tutorials need to use the shortcut, testing fails with the
	// error that there are 2 identically named shortcuts, being unable to click one.
	// Fortunately, we can keep creating a workspace shortcut to the same path and yet only one
	// shortcut to that path ever exists in GLI.
	System.err.println("### ABOUT TO REMOVE SHORTCUT");
	// Remove sample files shortcut
	workspaceFolder = openWorkspacePath(WKS_TOP_FOLDER_SAMPLEFILES, "", RIGHT_CLICK, false);
	selectContextMenu(Dictionary.get("MappingPrompt.Unmap"));
	PAUSE(3);
	*/
    }
    
    public static String[] get_backdrop_descriptions() {
	String[] descriptions = { "Bear in the Rocky Mountains",
				  "Kathy's beloved friend Kouskous",
				  "Fastest land mammal" };
	return descriptions;
    }

    public void backdrop() {
	//loadCollection("backdrop");
	
	String[] descriptions = get_backdrop_descriptions();
	
	System.err.println("@@@ Tutorial 2: backdrop");	
	
	String image_e_coll = props.get("samplefiles.treepath")+"/images/"+"image-e";
	File newFolder = GSBasicTestingUtil.copyIntoCollect(image_e_coll);
	System.err.println("Trying to create the folder " + newFolder);
	PAUSE(2);
	String errMsg = String.format("### ASSERT ERROR: %s doesn't exist", newFolder);
	Assert.assertTrue(errMsg, newFolder.exists());
	System.err.println("Successfully created folder " + newFolder);
	
	// Start a new collection within the Librarian Interface:
	createCollection("backdrop",
			 "A simple image collection",
			 "Simple image collection");
	
	
	//PAUSE(3);
	
	// Multiple selection drag and drop works iff Shift-Selecting a contiguous set of files.
	// 
	// Shift+Ctrl select works to make a tailored multiple file selection, but dragging
	// and dropping such a complex selection from JTreeFixture fails: JTreeFixture can only
	// drag from a single node and selects it first, which deselects the previous selection.
	// So Ctrl on a single treenode drag (to first add the treenode to an existing selection)
	// selects and deselects the node. A ghost drag movement happens but nothing gets dragged.
	// So using Ctrl is anathema to multi-selection dropping. I can only Shift on start
	// and Drag on end of a contiguous selection to then get dragging to work on the entire
	// selection and the drop happens. With any Ctrl to deselect and select some further
	// items, the final item does not DRAG but gets deselected and so any drag-and-drop fails.
	// Ctrl+Shift will not even select anything for dragging.
	// Using JTreeFixture.selectPaths() also fails as I can't tell it to drag on the last
	// path to select. Any drag() call thereafter deselects any previous selection again.

	//String workspaceFolderPath = WKS_TOP_FOLDER_LOCAL+props.get("samplefiles.treepath").toString()+"images";
	String workspaceFolder = openWorkspacePath(WKS_TOP_FOLDER_LOCAL, props.get("samplefiles.treepath").toString()+"/images", EXPAND);
	String collectionPath = "";
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/Bear.jpg", workspaceFolder+"/Gopher.jpg",
				     collectionPath);
	dragNDrop(workspaceFolder+"/Lemur.jpg", collectionPath);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/Rhino.jpg", workspaceFolder+"/Tiger.jpg",
				     collectionPath);		
	
	createBuildPreviewCycle(new Runnable() {	
	//switchToPane(FORMAT_PANE); quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.backdrop(1, null); // preview test 1 of backdrop
		}
	    });
	
	// which also tries to make the GLI main window the active one again, instead of the
	// browser. https://stackoverflow.com/questions/4005491/how-to-activate-window-in-java
	//makeGLIWindowActive();
	//PAUSE(1);	
	
	switchToPane(ENRICH_PANE);	
	//PAUSE(2);	
	//openCollectionPath("Bear.jpg", CLICK);
	changeToMetadataSet("Dublin Core Metadata Element Set");
	
	setMetaByTyping("Bear.jpg", "dc.Title", "Bear");	
	setMeta("Cat.jpg", "dc.Title", "Cat");
	setMeta("Cheetah.jpg", "dc.Title", "Cheetah");
	addMeta("Bear.jpg", "dc.Description", descriptions[0]); //"Bear in the Rocky Mountains");
	addMeta("Cat.jpg", "dc.Description", descriptions[1]); //"Kathy's beloved friend Kouskous");
	addMeta("Cheetah.jpg", "dc.Description", descriptions[2]); //"Fastest land mammal");
	
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.backdrop(2, null);
		}
	    });
	
	switchToPane(FORMAT_PANE);
	formatFeature("browse");
	PAUSE(1);
	
	/*
	String lines = 
	    "<td valign=\"top\">"+
	    "<gsf:displayText name=\"ImageName\"/>:<gsf:metadata name=\"Image\"/><br/>"+
	    "<gsf:displayText name=\"Width\"/>:<gsf:metadata name=\"ImageWidth\"/><br/>"+
	    "<gsf:displayText name=\"Height\"/>:<gsf:metadata name=\"ImageHeight\"/><br/>"+
	    "<gsf:displayText name=\"Size\"/>:<gsf:metadata name=\"ImageSize\"/>"+
	    "</td>";	
	requireFormatFeatureText(lines); //matchFormatFeatureText(lines, Edit.FIRST_OCCURRENCE);

	
	//replaceFormatFeatureText_manual("<gsf:displayText name=\"ImageName\"/>:<gsf:metadata name=\"Image\"/><br/>",
	//			 "Title:<gsf:metadata name=\"dc.Title\"/><br/>"
	//			 +"Description:<gsf:metadata name=\"dc.Description\"/><br/>"
	//			 );
	
	replaceExactFormatFeatureText_manual("<gsf:displayText name=\"ImageName\"/>:<gsf:metadata name=\"Image\"/><br/>",
				 "Title:<gsf:metadata name=\"dc.Title\"/><br/>"
				 );
	insertFormatFeatureText("Description:<gsf:metadata name=\"dc.Description\"/><br/>");
	
	//replaceFormatFeatureText("<gsf:displayText name=\"ImageName\"/>:<gsf:metadata name=\"Image\"/><br/>",
	//			 "Title:<gsf:metadata name=\"dc.Title\"/><br/>"
	//			 +"Description:<gsf:metadata name=\"dc.Description\"/><br/>"
	//			 );
	
	PAUSE(1);
	previewCollectionViaFormat();
	PAUSE(1.5);
	makeGLIWindowActive();	
	PAUSE(1);
	*/

	// The following (along with the tutorial-edits.YAML file) replaces above
	String tutorial = "backdrop";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	// check a required format stmt exists
	tutorialEdits.getEdit(editcount++).execute();
	Edit formatEdit = tutorialEdits.getEdit(editcount++).print();
	// Do a manual entry replacement of the text found.
	// Don't call execute() as it runs the automatic paste string methods
	replaceFormatFeatureText_manual(formatEdit.getFind(),
					formatEdit.getText(),
					formatEdit.getOccurrence());
	// Test our method to insert at cursor, instead of doing a find and replace/insert after
	//tutorialEdits.getEdit(editcount++).executeAndPreview();
	formatEdit = tutorialEdits.getEdit(editcount++).print();
	insertFormatFeatureText(formatEdit.getText());
	
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.backdrop(3, descriptions);
		}
	    });	
	
	switchToPane(DESIGN_PANE);
	//PAUSE(3);
	selectPlugin("ImagePlugin", CLICK);
	PAUSE(1);
	Collection configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("thumbnailsize", "50"));
	configureSelectedPluginOrClassifier(configOptions);
	configOptions.clear();
	configOptions = null;

	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.backdrop(4, null);
		}
	    });	
	
	switchToPane(DESIGN_PANE);
	//PAUSE(3);
	selectPlugin("ImagePlugin", DOUBLE_CLICK);
	PAUSE(1);
	List options = new ArrayList(1);
	options.add("thumbnailsize");
	switchOffConfigOptions(options);
	options.clear();
	options = null;
	
	chooseBrowsingClassifiers();
	configOptions = new ArrayList(2);
	configOptions.add(new SimpleEntry("metadata", "dc.Description"));
	configOptions.add(new SimpleEntry("partition_type_within_level", "none"));
	addClassifier("List", configOptions);
	configOptions.clear();
	configOptions = null;
	
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.backdrop(5, descriptions);
		}
	    });	

	switchToPane(DESIGN_PANE);
	//PAUSE(3);
	addSearchIndex("dc.Description");

	// TESTING utility functions related to search indexes
	//String origIndexes = "dc.Description"; //String[] origIndexes = {"dc.Description"};
	//String[] replacements = {"dc.Creator", "dc.Title"}; // GLI combines indexes alphabetically
	//editSearchIndex(origIndexes, replacements, null);// this method has been changed
	//addSearchIndex("dc.Description");
	//String[] removeIndexes = {"dc.Creator,dc.Title", "dc.Description"};
	//removeSearchIndexes(removeIndexes);

	//PAUSE(2);
	
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.backdrop(6, descriptions);
		}
	    });
	
	switchToPane(FORMAT_PANE);
	//PAUSE(3);
	Collection searchDisplayItems = new ArrayList(1);
	searchDisplayItems.add(new SimpleEntry("Index: dc.Description", "image descriptions"));
	//searchDisplayItems.add(new SimpleEntry("document", "doc"));
	formatSearch(searchDisplayItems);
	
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    //window.iconify();
		    BrowserTest.backdrop(7, null);
		    //window.normalize();
		}
	    });
	
    }

    public void imagesGPS() {
	//loadCollection("Images-GPS");
	
	String collectionPath = "";
	String workspaceFolder;	
	Collection configOptions;
	
	String tutorial = "imagesGPS";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	createCollection("Images-GPS", "An image collection with GPS metadata", null);
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "/images_gps", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/eiffel-tower",
				     workspaceFolder+"/pantheon-district",
				     collectionPath);
	
	switchToPane(ENRICH_PANE);
	// setting folder level metadata
	setMeta("eiffel-tower", "dc.Title", "Eiffel Tower");	
	closePopup(OPTIONAL,
		   Dictionary.get("DirectoryLevelMetadata.Title"),
		   Dictionary.get("General.OK"),
		   TICK_CHECKBOX);
	setMeta("luxembourg-parc", "dc.Title", "Parc de Luxembourg");
	setMeta("museum-d_orsay", "dc.Title", "Musée d'Orsay");
	setMeta("pantheon-district", "dc.Title", "Panthéon district");

	switchToPane(DESIGN_PANE);
	chooseBrowsingClassifiers();
	configOptions = new ArrayList(2);
	configOptions.add(new SimpleEntry("metadata", "dc.Title"));
	configOptions.add(new SimpleEntry("buttonname", "locations"));
	addClassifier("AZCompactList", configOptions);
	configOptions.clear();
	configOptions = null;
	
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.imagesGPS(1);
		}
	    });

	
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();	
	addPlugin("EmbeddedMetadataPlugin", null);
	// move it up to just under GreenstoneXMLPlugin
	movePluginOrClassifierNearTo(
		     Pattern.compile(" EmbeddedMetadataPlugin.*"),
		     Pattern.compile(" GreenstoneXMLPlugin.*"),
		     UP);
	
	switchToPane(CREATE_PANE);
	buildCollectionTillReady(MAX_BUILD_TIMEOUT);
	
	switchToPane(ENRICH_PANE);
	String[] metanames = //{ "ex.Latitude", "ex.Longitude", "ex.LatShort", "ex.LngShort"};
	    { "ex.Coordinate", "ex.CoordShort"};
	checkDocHasMetaAssignedFor("eiffel-tower/IMG_20120725_182044.jpg", metanames);

	switchToPane(DESIGN_PANE);
	addSearchIndex("ex.CoordShort");
	addSearchIndex("ex.Coordinate");
	
	switchToPane(CREATE_PANE);
	buildCollectionTillReady(MAX_BUILD_TIMEOUT);
	
	switchToPane(FORMAT_PANE);
	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.imagesGPS(2);
		}
	    });	
	
	appendSearchType("raw");
	
	formatFeature("display");
	tutorialEdits.getEdit(editcount++).execute();
	formatFeature("search");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.imagesGPS(3);
		}
	    });
    }
    
	/*
	Collection meta=new ArrayList(2);
	  // Reuse the following to add aggregate metadata to each document
	meta.add(new SimpleEntry("dc.Title", ""));
	meta.add(new SimpleEntry("dc.Creator", new String[]{""}));
	addAllMeta("", meta);
	meta.clear();
	*/

    // TODO: Implement setting File Associations in GLI
    public void reports() {
	//loadCollection("reports");
	
	createCollection("reports", "A collection of Word and PDF files", null);
	
	String workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "Word_and_PDF/Documents", EXPAND);
	    //openWorkspacePath(WKS_TOP_FOLDER_LOCAL, props.get("samplefiles.treepath").toString()+"/Word_and_PDF/Documents", EXPAND);
	String collectionPath = "";
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/cluster.ps", workspaceFolder+"/word06.doc",
				     collectionPath);
	PAUSE(3);
	
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.reports(1);
		}
	    });
	
	// Set file associations
	Collection extCmdPairs = new ArrayList(2);
	if(Platform.isLinux()) {
		extCmdPairs.add(new SimpleEntry("doc", "xdg-open %1"));//"soffice %1"));//
		//extCmdPairs.add(new SimpleEntry("pinky", "xdg-open %1"));
	} else if(Platform.isWindows()) {
		extCmdPairs.add(new SimpleEntry("doc", "cmd.exe /c start \"\" \"%1\""));
	} else { // assume mac
		extCmdPairs.add(new SimpleEntry("doc", "open %1"));
	}
	fileAssociations(extCmdPairs);
	
	switchToPane(ENRICH_PANE);	
	setMeta("word03.doc", "dc.Title", "Greenstone: A comprehensive open-source digital library software system");
	

	String[] authors = {"Ian H. Witten", "Rodger J. McNab", "Stefan J. Boddie", "David Bainbridge" };
	addMeta("word03.doc", "dc.Creator", authors);

	// Testing setting meta from previously set values
	setMetaFromPrevSetMeta("pdf01.pdf", "dc.Title", "Greenstone: A comprehensive open-source digital library software system");
	addMetaValsFromPrevSetValues("pdf01.pdf", "dc.Creator", authors);
	

	// Test adding aggregate meta
	Collection meta=new ArrayList(2); // Setting meta for 2 metanames: dc.Title and dc.Creator
	
	//meta.add(new SimpleEntry("dc.Title", "Clustering with finite data from semi-parametric mixture distributions")); // use ex.Title
	meta.add(new SimpleEntry("dc.Creator", new String[]{"Yong Wang", "Ian H. Witten"}));
	addAllMeta("cluster.ps", meta);
	meta.clear();
	
	meta.add(new SimpleEntry("dc.Title", "Using language models for generic entity extraction"));
	meta.add(new SimpleEntry("dc.Creator", new String[]{"Ian H. Witten", "Zane Bray", "Malika Mahoui", "W.J. Teahan"}));
	addAllMeta("langmodl.ps", meta);
	meta.clear();

	meta.add(new SimpleEntry("dc.Title", "Applications for Bibliometric Research in the Emerging Digital Libraries"));
	meta.add(new SimpleEntry("dc.Creator", "Sally Jo Cunningham"));
	addAllMeta("pdf03.pdf", meta);
	meta.clear();

	//meta.add(new SimpleEntry("dc.Title", "Authorship patterns in   Information Systems")); // use ex.Title
	meta.add(new SimpleEntry("dc.Creator", new String[]{"Sally Jo Cunningham", "Stuart M. Dillon"}));
	addAllMeta("rtf01.rtf", meta);
	meta.clear();


	//addMeta("word01.doc", "dc.Title", "1997-00 Listing of Working Papers"); // use ex.Title


	//meta.add(new SimpleEntry("dc.Title", "Greenstone Digital Library Installer's Guide")); // use ex.Title
	meta.add(new SimpleEntry("dc.Creator", new String[]{"Ian H. Witten", "Stefan Boddie"}));


	addAllMeta("word05.doc", meta);
	meta.clear();

	//meta.add(new SimpleEntry("dc.Title", "Computational sense: The role of technology in the education of digital librarians")); // use ex.Title
	meta.add(new SimpleEntry("dc.Creator", new String[]{"Michael B. Twidale", "David M. Nichols"}));


	addAllMeta("word06.doc", meta);
	meta.clear();	

	meta = null;
	
	switchToPane(DESIGN_PANE);
	String[] pluginsToRemove = {"ZIPPlugin", "TextPlugin", "HTMLPlugin", "EmailPlugin", "PowerPointPlugin", "ExcelPlugin", "ImagePlugin", "ISISPlug", "NulPlugin", "OAIPlugin"};
	removePluginsByName(pluginsToRemove);
	

	removeSearchIndexes("ex.Source");
	addSearchIndex("dc.Creator");

	//removeClassifiersByName(new String[] {"ex.Source"});
	removeClassifiersByName("ex.Source");
	
	Collection configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("metadata", "dc.Creator"));
	addClassifier("AZCompactList", configOptions);
	configOptions.clear();
	
	switchToPane(CREATE_PANE);
	buildCollectionTillReady(MAX_BUILD_TIMEOUT); //createBuildPreviewCycle();
	
	switchToPane(FORMAT_PANE);
	//PAUSE(3);
	Collection searchDisplayItems = new ArrayList(1);
	searchDisplayItems.add(new SimpleEntry("Index: dc.Creator", "creators"));
	formatSearch(searchDisplayItems);
	
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.reports(2);
		}
	    });

	configOptions = null;
	
	closeCollection();
		
    }


    public void formattingReports() {
	
	loadCollection("reports");
	//PAUSE(6);
	switchToPane(FORMAT_PANE);
	
	int editcount = 0;

	String tutorial = "formattingReports";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	
	// FORMAT STATEMENTS TESTS
	// simplifying browse and globalformat statements, no visible changes
	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute();
	PAUSE(1);	
	formatFeature("global");
	PAUSE(1);
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.formattingReports(1); // should be same as BrowserTest.reports(2)
		        // but also check greenstone doc icons are there
		}
	    });
	
	// next 2 edits are part of 1 step
	formatFeature("search");
	PAUSE(1);
	tutorialEdits.getEdit(editcount++).execute();
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.formattingReports(2); // check only src icons are there
		}
	    });

	// Add in both doc and src icons to the search format statement
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.formattingReports(3); // check src and doc icons are there
		}
	    });
	
	// display numleafdocs info
	formatFeature("browse");	
	PAUSE(1);
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.formattingReports(4); // check num leaf indicator info
		}
	    });
	
	// display creator metadata, comma separated
	tutorialEdits.getEdit(editcount++).execute();
	
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.formattingReports(5); // check author meta is comma-separated
		}
	    });
	
	// creator metadata space separated
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.formattingReports(6); // check author meta is space-separated
		}
	    });
	// creator newline separated
	tutorialEdits.getEdit(editcount++).execute();
	
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.formattingReports(7); // check author meta is newline-separated
		}
	    });
	
	
	switchToPane(DESIGN_PANE);	
	chooseBrowsingClassifiers(); // optional when editing, TODO: always optional/compulsory?
	Collection configOptions = new ArrayList(2);
	configOptions.add(new SimpleEntry("firstvalueonly", null));
	editClassifierByPattern(".*AZCompactList .*-metadata .*dc.Creator.*", configOptions);
	configOptions.clear();
	configOptions = null;	
	
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.formattingReports(8); // check author meta is newline-separated
		}
	    });
	
	closeCollection();	
    }

    // The Enhanced Word Document Handling tutorial (enhanced reports) is handled by different functions below,
    // depending on if testing on Windows or other OS. Though the proper test is Word is installed on Win, I'm
    // assuming Windows test machines will have it installed and that therefore Windows implies Word's present.

    // Only for windows - uses windows scripting
    public void enhancedReportsTut_windowsScriptingAndTika() {
	loadCollection("reports");
	Collection configOptions;
	List options;

	switchToPane(FORMAT_PANE);
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.enhancedReports(1); // check no structure to html versions of word documents
		}
	    });

	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("windows_scripting", null));
	configurePlugin("WordPlugin", configOptions);
	configOptions = null;

	chooseSearchIndexes();
	addIndexingLevel("section");

	// Disabling alerts from Word, so that automated testing can proceed without manual intervention
	// to close its popups, is now taken care of by the combination of svn commits r39488 and r39489:
	// 1. gsConvert.pl now invoking newdoc2html.vbs (formerly called docx2html.vbs) to process not just docx
	// but also, when windows_scripting is on, all .doc and .rtf
	// 2. Modification of docx2html.vbs to set Word Application's DisableAlerts=False
	// and not just making Word invisible

	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.enhancedReports(2);
		}
	    });

	// ensure we're in the librarian mode
	changeUserMode("librarian");
	
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	configOptions = new ArrayList(4);
	configOptions.add(new SimpleEntry("level1_header", "(ChapterTitle|AppendixTitle)"));
	configOptions.add(new SimpleEntry("level2_header", "SectionHeading"));
	configOptions.add(new SimpleEntry("level3_header", "SubsectionHeading"));
	configOptions.add(new SimpleEntry("title_header", "ManualTitle"));			  
	configurePlugin("WordPlugin", configOptions);
	configOptions = null;
	
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.enhancedReports(3);
		}
	    });

	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	configOptions = new ArrayList(2);
	configOptions.add(new SimpleEntry("delete_toc", null));
	configOptions.add(new SimpleEntry("toc_header", "(MsoToc1|MsoToc2|MsoToc3|MsoTof|TOA)"));
	configurePlugin("WordPlugin", configOptions);
	configOptions = null;
	
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.enhancedReports(4);
		}
	    });

	switchToPane(ENRICH_PANE);
	//checkDocHasNoMetaAssignedFor("word05.doc", DC_SUBJECT);
	//checkDocHasNoMetaAssignedFor("word06.doc", DC_SUBJECT);
	checkDocLacksMetanameFields("word05.doc", "ex.Creator", "ex.Subject");
	checkDocLacksMetanameFields("word06.doc", "ex.Creator", "ex.Subject");

	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("metadata_fields", "Title,Author<Creator>,Subject,Keywords<Subject>"));
	configurePlugin("WordPlugin", configOptions);
	configOptions = null;

	switchToPane(CREATE_PANE);
	buildCollectionTillReady(MAX_BUILD_TIMEOUT);

	switchToPane(ENRICH_PANE);
	checkDocHasMetaAssignedFor("word05.doc", "ex.Creator", "ex.Subject");
	checkDocHasMetaAssignedFor("word06.doc", "ex.Creator", "ex.Subject");
	
	
	// After windowsScripting tutorial is nearly finished (before its UnknownConverterPlugin section),
	// the reports collection is in the correct
	// state for us to return to the basic reports tutorial and complete its final step
	switchToPane(DESIGN_PANE);
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("metadata", "dc.Creator,ex.Creator"));
	configOptions.add(new SimpleEntry("allvalues", null)); // test allvalues option
	editClassifierByName("AZCompactList", configOptions);
	configOptions.clear();
	configOptions = null;

	// Have to undo the work in formattingReports collection for browser test (and will redo it later)
	selectClassifier("AZCompactList", DOUBLE_CLICK);
	options = new ArrayList(1);
	options.add("firstvalueonly");
	switchOffConfigOptions(options);
	options.clear();
	options = null;

	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.enhancedReports(5);
		}
	    });
	
	// Take off allvalues option and put back firstvalueonly option
	switchToPane(DESIGN_PANE);	
	chooseBrowsingClassifiers();
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("firstvalueonly", null));
	editClassifierByName("AZCompactList", configOptions);
	configOptions.clear();
	configOptions = null;

	selectClassifier("AZCompactList", DOUBLE_CLICK);
	options = new ArrayList(1);
	options.add("allvalues");
	switchOffConfigOptions(options);
	options.clear();
	options = null;

	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    // The reports collection is now in the right state to do the final step
		    // of the Formatting Reports tutorial
		    BrowserTest.enhancedReports(6);
		}
	    });

	// Do the Unknown Converter Plugin steps of the enhanced Reports collection
	addDocxFileAndUnknownConverterPluginWithTika();
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		// Check the new docx was added and has structure and an image
		    BrowserTest.enhancedReports(7);
		}
	    });
	
	// Turn off windows_scripting (on windows) for the tika part of the tutorial
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	selectPlugin("WordPlugin", DOUBLE_CLICK);
	options = new ArrayList(1);
	options.add("windows_scripting");
	switchOffConfigOptions(options);
	options.clear();
	options = null;

	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    // Check the docx has no structure (and is linked by a text doc icon instead of book.png)
		    // and contains no image
		    BrowserTest.enhancedReports(8);
		}
	    });
	closeCollection();
    }

    // The part of the Enhanced Reports tutorial that non-Windows machines still need to test:
    // UnknownConverterPlugin with Apache Tika processing the docx file
    public void enhancedReportsTut_tika() {
	loadCollection("reports");
	
	addDocxFileAndUnknownConverterPluginWithTika();

	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    // Check the docx has no structure (and is linked by a text doc icon instead of book.png)
		    // and contains no image
		    BrowserTest.enhancedReports(8);
		}
	    });
	
	/*
	  switchToPane(FORMAT_PANE);
	  quickFormatPreviewCycle(new Runnable() {	
	  public void run() {
	  BrowserTest.enhancedReports(8);
	  }
	  });
	  */
	closeCollection();
    }
    private void addDocxFileAndUnknownConverterPluginWithTika() {
	switchToPane(GATHER_PANE);
	String collectionPath = "";
	String workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "Word_and_PDF/extra_docx", EXPAND);
	dragNDrop(workspaceFolder+"/testword.docx", collectionPath);
	
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	// Move UnknownConverterPlugin down past WordPlugin to just above PostScriptPlugin
	movePluginOrClassifierNearTo(
		     Pattern.compile(" UnknownConverterPlugin.*"),
		     Pattern.compile(" PostScriptPlugin.*"),
		     DOWN);
    }

    // TODO: quotes are not appearing around notext... in plugin configuration value
    // CAN'T FIX: Happens only when the collection has been re-opened and is true in GLI
    // itself, not just in GLI run for automated testing.
    public void pdfCollection() {
	//loadCollection("PDF collection");

	Collection configOptions = null;

	String collectionPath = "";	
	
	createCollection("PDF collection", "Enhanced PDF handling", null);
	
	String workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "Word_and_PDF/Documents", EXPAND);	
	dragNDropContiguousSelection(WORKSPACE_VIEW,
	     workspaceFolder+"/pdf01.pdf", workspaceFolder+"/pdf03.pdf",
	     collectionPath);
	
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "Word_and_PDF/difficult_pdf", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
	     workspaceFolder+"/pdf05-notext.pdf", workspaceFolder+"/pdf06-weirdchars.pdf",
	     collectionPath);

	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.pdfCollection(1);
		}
	    });
	
	// check expected config options are set
	switchToPane(DESIGN_PANE);	
	
	selectPlugin("PDFv2Plugin", DOUBLE_CLICK);
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("convert_to", "paged_pretty_html"));
	checkSelectedPluginOrClassifierConfig(configOptions);
	configOptions.clear();
	
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("convert_to", "pagedimg_jpg"));
	configurePlugin("PDFv2Plugin", configOptions);
	configOptions.clear();
	
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.pdfCollection(2);
		}
	    });
	
	switchToPane(GATHER_PANE);
	//String path = openCollectionPath("", RIGHT_CLICK);
	//selectContextMenu(Dictionary.get("CollectionPopupMenu.New_Folder"));
	//propertyComp.pressAndReleaseKey(KeyPressInfo.keyCode(java.awt.event.KeyEvent.VK_TAB));
	newCollectionFolder("", "notext");

        //String
	collectionPath = "notext";	
	dragNDropContiguousSelection(COLLECTION_VIEW,
				     "pdf05-notext.pdf", "pdf06-weirdchars.pdf",
				     collectionPath);
	
	switchToPane(DESIGN_PANE);	
	addPlugin("PDFv2Plugin", null); // new plugin added and selected	
	//selectPlugin(Pattern.compile(" PDFv2Plugin\\s+-convert_to\\s+pagedimg_jpg.*"), SELECT);
	//moveSelectedPluginOrClassifierUp(10);

	// Reconfigure the earlier PDFv2Plugin
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("process_exp", "\"notext.*\\.pdf\""));
	configurePlugin(Pattern.compile(".*PDFv2Plugin\\s+-convert_to\\s+pagedimg_jpg.*"),
			configOptions);

	//Optional: moving it up to just under the pagedimg/notext-folder PDFv2Plugin
	movePluginOrClassifierNearTo(
		     Pattern.compile(" PDFv2Plugin\\s+-convert_to\\s+paged_pretty_html.*"),
		     Pattern.compile(" PDFv2Plugin\\s+-convert_to\\s+pagedimg_jpg.*"),//notext
		     UP);
	
	createBuildPreviewCycle(new Runnable() {
		public void run() {
		    // do a combined test of features of all previews of this collection
		    // to this present preview
		    BrowserTest.pdfCollection(3);
		}
	    });
	
	// format statements
	switchToPane(FORMAT_PANE);
	String tutorial = "pdfCollection";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	formatFeature("display");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(HACK_DONOT_ICONIFY_GLI, new Runnable() {
	 	public void run() {
	 	    BrowserTest.pdfCollection(4);
	 	}
	    });
	
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.pdfCollection(5);
	 	}
	    });

	formatFeature("search");
	tutorialEdits.getEdit(editcount++).execute();	
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
		    // can't really test PDF contents highlighting, but can search and open a PDF
	 	    BrowserTest.pdfCollection(6);
	 	}
	    });
    }

    public void associatedFiles() {
	//loadCollection("Associated Files");
	
	createCollection("Associated Files",
			 "Combining different versions of the same document together",
			 null);

	String collectionPath = "";
	String workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "Word_and_PDF/Documents", EXPAND);	
	dragNDrop(workspaceFolder+"/pdf01.pdf", collectionPath);
	dragNDrop(workspaceFolder+"/word03.doc", collectionPath);

	renameCollFileOrFolder("pdf01.pdf", "greenstone1.pdf");
	renameCollFileOrFolder("word03.doc", "greenstone1.doc");
	
	switchToPane(ENRICH_PANE);
	shiftMultiSelect(COLLECTION_VIEW,
			 "greenstone1.doc",
			 "greenstone1.pdf",
			 CLICK);
	
	setMetaOnCurrentDocsSelection("dc.Title",
	      "Greenstone: A comprehensive open-source digital library software system");
	
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	//selectPlugin("WordPlugin", DOUBLE_CLICK);
	Collection configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("associate_ext", "pdf"));
	configurePlugin("WordPlugin", configOptions);
	configOptions.clear();
	
	//createBuildPreviewCycle();	
	switchToPane(CREATE_PANE);
	buildCollectionTillReady(MAX_BUILD_TIMEOUT);
	String[] expectedOutputLines
	    = { "1 document was considered for processing:",
		"1 document was processed and included in the collection."};
	checkBuildLogContains(expectedOutputLines);
	
	// format statements
	switchToPane(FORMAT_PANE);
	String tutorial = "associatedFiles";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.associatedFiles(1);
	 	}
	    });

	formatFeature("search");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.associatedFiles(2);
	 	}
	    });
    }

    public void tudor() {
	//loadCollection("tudor");
	
	createCollection("tudor", "A large collection of HTML files", null);

	String collectionPath = "";
	String workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "tudor", EXPAND);
	dragNDrop(workspaceFolder+"/englishhistory.net", collectionPath);

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor(1);
	 	}
	    });
	
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	Collection configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("metadata_fields", "Title,Author,Page_topic,Content"));
	configurePlugin("HTMLPlugin", configOptions);
	configOptions.clear();

	//createBuildPreviewCycle();
	switchToPane(CREATE_PANE);
	buildCollectionTillReady(MAX_BUILD_TIMEOUT);
	// some sanity checking as we're not previewing
	String[] expectedOutputLines
	    = { "151 documents were considered for processing",
		"151 documents were processed and included in the collection"};
	checkBuildLogContains(expectedOutputLines);
	
	switchToPane(ENRICH_PANE);
	String[] metanames = { "ex.Title", "ex.Author", "ex.Page_topic", "ex.Content" };
	checkDocHasMetaAssignedFor("englishhistory.net/tudor/monarchs/aragon.html", metanames);
	checkDocHasMetaAssignedFor("englishhistory.net/tudor/citizens/brandon.html", metanames);
	checkDocHasMetaAssignedFor("englishhistory.net/tudor/relative/janegrey.html", metanames);
	
	switchToPane(GATHER_PANE);	
	checkViewFilterAgainstCollPath("HTM & HTML", "englishhistory.net/tudor");
	checkViewFilterAgainstCollPath("Images", "englishhistory.net/tudor");
	showFiles("All Files"); // put collection view filter back to showing All Files

	PAUSE(2);

    }
    public void tudor2_enhanced() {
	loadCollection("tudor");
	Collection configOptions = null;
	
	switchToPane(ENRICH_PANE);	
	setMeta("englishhistory.net/tudor/citizens", DC_SUBJECT, "Tudor period|Citizens");	
	closePopup(OPTIONAL,
		   Dictionary.get("DirectoryLevelMetadata.Title"),
		   Dictionary.get("General.OK"),
		   TICK_CHECKBOX);
	setMeta("englishhistory.net/tudor/monarchs", DC_SUBJECT, "Tudor period|Monarchs");
	setMeta("englishhistory.net/tudor/relative", DC_SUBJECT, "Tudor period|Relatives");

	shiftMultiSelect(COLLECTION_VIEW,
			 "englishhistory.net/tudor/1500s.gif",
			 "englishhistory.net/tudorrosemain.jpg",
			 CLICK);
	setMetaOnCurrentDocsSelection(DC_SUBJECT, "Tudor period|Others");

	switchToPane(DESIGN_PANE);
	chooseBrowsingClassifiers();
	configOptions = new ArrayList(2);
	configOptions.add(new SimpleEntry("metadata", DC_SUBJECT));
	addClassifier("Hierarchy", configOptions);
	configOptions.clear();
	
	// there's a space at the start of the classifier name, so allow for that
	removeClassifiersByPattern("\\s*List -metadata ex.Source .*");
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor2_enhanced(1, null);
	 	}
	    });

	switchToPane(DESIGN_PANE);
	chooseBrowsingClassifiers();
	addClassifier("Phind", null);
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor2_enhanced(2, null);
	 	}
	    });

	switchToPane(DESIGN_PANE);
	choosePartitionIndexes(DEFINE_FILTERS);

	String flagsField = null;
	defineFilters("monarchs", DC_SUBJECT, "Monarchs", flagsField);
	defineFilters("relatives", DC_SUBJECT, "Relatives", flagsField);
	defineFilters("citizens", DC_SUBJECT, "Citizens", flagsField);
	defineFilters("others", DC_SUBJECT, "Others", flagsField);

	choosePartitionIndexes(ASSIGN_PARTITIONS);
	//assignPartitions(new String[]{"monarchs", "relatives", "citizens", "others", "all"});
	assignPartitions("monarchs", "relatives", "citizens", "others");
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor2_enhanced(3, null);
	 	}
	    });
	
	switchToPane(DESIGN_PANE);
	choosePartitionIndexes(ASSIGN_PARTITIONS);	
	assignPartitions("ALL");//new String[]{"ALL"}); // special keyword

	String allPartitionSorted = "citizens,monarchs,others,relatives";
	movePartitionToTop(allPartitionSorted);

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor2_enhanced(4, allPartitionSorted);
	 	}
	    });
	
	//String allPartitionSorted = "citizens,monarchs,others,relatives";

	switchToPane(FORMAT_PANE);
	Collection searchDisplayItems = new ArrayList(1);
	searchDisplayItems.add(new SimpleEntry("Partition: " + allPartitionSorted, "all"));
	formatSearch(searchDisplayItems);

	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor2_enhanced(5, null);
	 	}
	    });
	
	switchToPane(CREATE_PANE);
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("maxdocs", "3"));
	configureBuildOptions(configOptions);
	configOptions.clear();

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor2_enhanced(6, null);
	 	}
	    });

	List options = new ArrayList(1);
	options.add("maxdocs");
	switchOffBuildOptions(options);
	options.clear();

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor2_enhanced(7, null);
	 	}
	    });
    }
    
    public void tudor3_formatting() {
	loadCollection("tudor");

	// check display of browse format stmt is as expected before changing it
	switchToPane(FORMAT_PANE);	
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor3_formatting(1);
	 	}
	    });
	
	// format statements
	String tutorial = "tudor3_formatting";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor3_formatting(2); // *almost* the same test
	 	}
	    });	
	
	String formatToEdit = "CL2 Hierarchy -metadata dc.Subject";
	addFormatFeatureStartsWith(formatToEdit);

	// Go to another section of the Format pane so that the newly added format stmt
	// will have been automatically restructured to have the right/expected structure
	//goToFormatGeneral();	
	//formatFeature(formatToEdit);
	
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor3_formatting(3);
		    // Make sure title classifier is as before by re-running test#2
		    System.err.println("@@@@ Re-running tudor3_formatting test 2 to ensure "
				       + "\n\tTitle classifier doc display is unchanged after "
				       + "\n\tediting Subject classifier's format statement:");
		    BrowserTest.tudor3_formatting(2);
	 	}
	    });	
	
	formatFeature("search");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor3_formatting(4);		   
	 	}
	    });
	
	// 2 related edits to modify the bookshelf display for CL2 dc.Subject hierarchy classifier
	formatFeature(formatToEdit);
	tutorialEdits.getEdit(editcount++).execute();
	tutorialEdits.getEdit(editcount++).execute();	
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor3_formatting(5);
	 	}
	    });
	//PAUSE(2);
	
    }

    public void tudor4_webLink() {
	loadCollection("tudor");
	
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	Collection configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("file_is_url", null));
	configurePlugin("HTMLPlugin", configOptions);
	configOptions.clear();

	switchToPane(FORMAT_PANE);	
	formatFeature("browse");
	TutorialEdits tutorialEdits = tutorials.get("tudor4_webLink");
	tutorialEdits.getEdit(0).execute();
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.tudor4_weblink();
	 	}
	    });	
    }
    
    public void sectionTaggingForHTML() {
	//loadCollection("lucene-jdbm-demo");
	
	String pathToFile = (String)props.get("scriptsAndPatches.folder") + "fb33fe.htm";
	    //System.getenv("GSDL3SRCHOME") + "/ext/testing/src/scripts_and_patches/fb33fe.htm";
	
	GSBasicTestingUtil.copyIntoCollection(pathToFile, "lucene-jdbm-demo", "import/fb33fe");
	loadCollection("lucene-jdbm-demo");

	switchToPane(DESIGN_PANE);
	Collection configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("description_tags", null));
	selectPlugin("HTMLPlugin", DOUBLE_CLICK);
	checkSelectedPluginOrClassifierConfig(configOptions);
	configOptions.clear();
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.sectionTaggingForHTML();
	 	}
	    });
    }
    
	private void prepareForDownloading() {
		// On Mac, need the Prefs > Connection tab > No_Check_Certificate tickbox ticked
		if(Platform.osFamily() == OSFamily.MAC) {
			Collection connOptions = new ArrayList(2);
			// 1st set site as it restricts servlets, so set servlets 2nd		
			connOptions.add(new SimpleEntry(Dictionary.get("Preferences.Connection.No_Check_Certificate"), null));

			setPrefs(null, // mode (librarian/expert/library assistant)
				 null, // general tab
				 null, // collDirPath in connection tab
				 connOptions, // connection tab		 
				 null); // switchOffWarnings tab
			connOptions = null;
		}
	}
	
    public void downloadingTudorFromWeb() {
	//loadCollection("webtudor");
	
	createCollection("webtudor", "Downloading files from the web", null);
	
	prepareForDownloading();
	
	switchToPane(DOWNLOAD_PANE);
	
	clearDownloadCache();
	
	Collection configOptions = new ArrayList(4);
	configOptions.add(new SimpleEntry("Source URL", "https://englishhistory.net/tudor/citizens/"));
	configOptions.add(new SimpleEntry("Download Depth", "1"));
	configOptions.add(new SimpleEntry("Only files below URL", null));
	configOptions.add(new SimpleEntry("Only files within site", null));
	
	Map proxyOpts = getProxyOpts();
	download(Dictionary.get("DOWNLOAD.MODE.WebDownload"), configOptions,
		 proxyOpts, MAX_WAIT_DOWNLOAD_SECONDS);
	
	configOptions.clear();
	configOptions = null;
	if(proxyOpts != null) {
	    proxyOpts.clear();
	    proxyOpts = null;
	}
	//PAUSE(5);
	switchToPane(GATHER_PANE);
	// Copy files across from "Downloaded Files" GLI workspace shortcut!
	String collectionPath = "";
	String workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_DOWNLOADS, "englishhistory.net", EXPAND);
	dragNDrop(workspaceFolder, collectionPath);
	//closePopup(OPTIONAL, Dictionary.get("NoPluginExpectedToProcessFile.Title"), Dictionary.get("General.OK"), TICK_CHECKBOX);
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.downloadingTudorFromWeb();
	 	}
	    });
    }
    
    public void bibliographicColl_MARC() {
	//loadCollection("Papers Bibliography");
	
	Collection configOptions;
	Collection searchDisplayItems;
	
	createCollection("Papers Bibliography",
			 "a collection of example MARC records of the working papers "
			 +"published at the Computer Science Department, Waikato University",
			 null);

	String collectionPath = "";
	String workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "marc", EXPAND);	
	dragNDrop(workspaceFolder+"/CMSwp-all.marc",
		  collectionPath,
		  new Runnable() {
		      public void run() {
			  //PAUSE(2);
			  // accept optional suggestion to add MARCPlugin
			  closeAddPluginPopup_add(OPTIONAL);
		      }
		  });
	
	switchToPane(DESIGN_PANE);
	autoAddPluginIfDragNDropOff(OPTIONAL, "MARCPlugin");
	chooseBrowsingClassifiers();
	removeClassifiersByPattern("\\s*List -metadata ex.Source .*");
	
	chooseSearchIndexes();
	removeSearchIndexes("ex.Source");
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.bibliographicColl_MARC(1);
	 	}
	    });	
	
	switchToPane(DESIGN_PANE);
	chooseBrowsingClassifiers();
	configOptions = new ArrayList(2);
	configOptions.add(new SimpleEntry("metadata", DC_SUBJECT));
	configOptions.add(new SimpleEntry("sort", "dc.Title")); // sort to get consistent expected result order when browser testing
	addClassifier("AZCompactList", configOptions);
	configOptions.clear();

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.bibliographicColl_MARC(2);
	 	}
	    });	
	
	switchToPane(FORMAT_PANE);
	//formatFeature("searchType");
	setSearchType("simpleform");
	
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.bibliographicColl_MARC(3);
	 	}
	    });
	
	switchToPane(DESIGN_PANE);
	addSearchIndex(DC_SUBJECT);
		
	switchToPane(FORMAT_PANE);
	
	searchDisplayItems = new ArrayList(1);
	searchDisplayItems.add(new SimpleEntry("Index: dc.Subject", "subjects"));
	formatSearch(searchDisplayItems);	
	searchDisplayItems.clear();
	searchDisplayItems = null;
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.bibliographicColl_MARC(4);
	 	}
	    });

	
	switchToPane(GATHER_PANE);	
	simpleExplodeMetadataDatabase("CMSwp-all.marc");
	closePopup(Dictionary.get("ExplodeMetadataPrompt.Successful_Title"),
		   Dictionary.get("General.OK"));

	switchToPane(DESIGN_PANE);
	removePluginsByName("MARCPlugin");
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.bibliographicColl_MARC(5);
	 	}
	    });
	
	// No need to look for global format MATCH, as we do a REPLACE of the same
	// expected global format statement later
	
	switchToPane(DESIGN_PANE);
	String origIndexes = "dc.Title,ex.dc.Title,ex.Title";
	String[] replacements = {"exp.Title"}; // GLI combines indexes alphabetically
	String[] skipUnticking = {"ex.dc.Title"}; // doesn't actually exist in list of assigned indexes
	editSearchIndex(origIndexes, replacements, skipUnticking);
	
	removeSearchIndexes(DC_SUBJECT);
	addSearchIndex("exp.Subject");
	removeSearchIndexes("text");	
	
	addCombinedSearchIndex(); //adds and selects combined index
	// null signifies current selected index, which is the combined index
	moveIndexToTop(null);//moveSelectedDesignControlToTop(); // move new combined index to top
	setAsDefaultIndex(null); //makeSelectedDesignControlDefault();

	chooseBrowsingClassifiers();
	configOptions.add(new SimpleEntry("metadata", "exp.Title"));
	editClassifierByPattern("\\s*List -metadata dc.Title,ex.Title .*", configOptions);
	configOptions.clear();
	
	configOptions.add(new SimpleEntry("metadata", "exp.Subject"));
	editClassifierByPattern("\\s*AZCompactList -metadata dc.Subject.*", configOptions);
	configOptions.clear();
	configOptions = null;
	
	switchToPane(FORMAT_PANE);
	searchDisplayItems = new ArrayList(2);
	searchDisplayItems.add(new SimpleEntry("Index: exp.Title", "titles"));
	searchDisplayItems.add(new SimpleEntry("Index: exp.Subject", "subjects"));
	formatSearch(searchDisplayItems);
	searchDisplayItems.clear();
	searchDisplayItems = null;
	
	//PAUSE(2);	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.bibliographicColl_MARC(6);
	 	}
	    });
	
	switchToPane(FORMAT_PANE);
	String tutorial = "bibliographicColl_MARC";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;

	formatFeature("global");
	tutorialEdits.getEdit(editcount++).execute();

	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute();
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.bibliographicColl_MARC(7);
	 	}
	    });
	
	formatFeature("display");
	tutorialEdits.getEdit(editcount++).execute();
	tutorialEdits.getEdit(editcount++).execute();

	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.bibliographicColl_MARC(8);
		}
	    });
    }

    public void CDS_ISIS() {
	//loadCollection("ISIS Collection");
	
	createCollection("ISIS Collection", null, null);
	String collectionPath = "";
	String workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "isis", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/slide.fdt", workspaceFolder+"/slide.xrf",
				     collectionPath);

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.CDS_ISIS(1);
		}
	    });
	
	switchToPane(DESIGN_PANE);
	// regex should help identify the Title search index
	removeSearchIndexes("ex.Source", "Title");

	// Thankfully addSearchIndex doesn't take regexes but exact strings for indexnames
	addSearchIndex("ex.Photographer^all");
	addSearchIndex("ex.Country^all");
	addSearchIndex("ex.Notes^all");
	
	switchToPane(FORMAT_PANE);
	Collection searchDisplayItems = new ArrayList(3);
	// Escape caret mark for regex
	searchDisplayItems.add(new SimpleEntry("Index: Photographer\\^all", "photographer"));
	searchDisplayItems.add(new SimpleEntry("Index: Country\\^all", "country"));
	searchDisplayItems.add(new SimpleEntry("Index: Notes\\^all", "notes"));
	formatSearch(searchDisplayItems);
	
	switchToPane(DESIGN_PANE);
	removeClassifiersByName("dc.Title,ex.Title");
	removeClassifiersByName("ex.Source");

	Collection configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("metadata", "ex.Photographer"));
	addClassifier("List", configOptions);
	configOptions.clear();

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.CDS_ISIS(2);
		}
	    });
	
	switchToPane(FORMAT_PANE);
	String tutorial = "CDS_ISIS";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute();
	formatFeature("search");
	tutorialEdits.getEdit(editcount++).execute();

	formatFeature("display");
	tutorialEdits.getEdit(editcount++).execute();
	tutorialEdits.getEdit(editcount++).execute();
	
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.CDS_ISIS(3);
		}
	    });	
    }

    public void lookingAtMultimedia() {
	String sampleFilesPath = (String)props.get("samplefiles.treepath");
	try {
	    GSBasicTestingUtil.copyToDirectory(
					       new File(sampleFilesPath+"/beatles/advbeat_large"),
					       GSBasicTestingUtil.getCollectFolder());
	    File indexDir = GSBasicTestingUtil.getCollectionFolder("/advbeat_large/index");
	    FileUtils.deleteDirectory(indexDir);
	    File etcDir = GSBasicTestingUtil.getCollectionFolder("/advbeat_large/etc");
	    File colcfg = new File(etcDir, "collect.cfg");
	    colcfg.renameTo(new File(etcDir, "collect.cfg.bak"));
	} catch(Exception e) {
	    e.printStackTrace();
	    System.err.println("### Exception during Looking at Multimedia Collection test: "
			       + e.getMessage());
	}

	loadCollection("advbeat_large");
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
		    // redo much of the final test for the multimedia coll,
		    // with some changes and adjustments
	 	    BrowserTest.multimedia(14);

		}
	    });
    }
    
    public void multimedia() {
	//loadCollection("small beatles");
	String collectionPath = "";
	String workspaceFolder;
	String sampleFilesPath = (String)props.get("samplefiles.treepath");
	Collection configOptions;

	String tutorial = "multimedia";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	createCollection("small beatles", "A small beatles collection", null);
	collectionPath = "";
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "beatles/advbeat_small", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/discography",
				     workspaceFolder+"/beatles_midi.zip",
				     collectionPath,
				     new Runnable() {
					 public void run() {
					     // Add plugin for MP3 and MARC		
					     closeAddPluginPopup_add(COMPULSORY);
					     closeAddPluginPopup_add(COMPULSORY);
					 }
				     });	

	PAUSE(5);
	refreshFolderView();
	PAUSE(2);

	switchToPane(ENRICH_PANE);
	// Check it's an unbuilt collection: no ex. meta nor any meta assigned
	checkDocHasNoMetaYet("images/Beatles_memorabilia.jpg", true);
	System.err.println("@@@ Confirmed no meta assigned - unbuilt collection");

	autoAddPluginIfDragNDropOff(COMPULSORY, "MP3Plugin", "MARCPlugin");
	    
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(1);
		}
	    });
	
	switchToPane(ENRICH_PANE);
	String collPath = "discography/www.thebeatlesarchive.com/discography";
	//openCollectionPath(collPath, EXPAND);
	//shiftMultiSelect(COLLECTION_VIEW,
	//		 collPath+"/magicalmisterytour.htm",
	//		 collPath+"/magicalmisterytourUS.htm",
	//		 CLICK);
	shiftMultiSelectFilesInFolder(COLLECTION_VIEW,
				      collPath,
				      "magicalmisterytour.htm",
				      "magicalmisterytourUS.htm",
				      CLICK);
	setMetaOnCurrentDocsSelection("dc.Title", "Magical Mystery Tour");

	//createBuildPreviewCycle(); // test 2 was merged with 3 to reduce num building cycles

	switchToPane(ENRICH_PANE);
	setMeta("discography", "dc.Format", "Discography");
	closePopup(OPTIONAL,
		   Dictionary.get("DirectoryLevelMetadata.Title"),
		   Dictionary.get("General.OK"),
		   TICK_CHECKBOX);
	setMeta("html_lyrics", "dc.Format", "Lyrics");
	setMeta("images", "dc.Format", "Images");
	setMeta("marc", "dc.Format", "MARC");
	setMeta("mp3", "dc.Format", "Audio");
	setMeta("tablature_txt", "dc.Format", "Tablature");
	setMeta("wordpdf", "dc.Format", "Supplementary");
	

	switchToPane(DESIGN_PANE);
	removeClassifiersByName("ex.Source");
	configOptions = new ArrayList(5);
	configOptions.add(new SimpleEntry("metadata", "dc.Format"));
	configOptions.add(new SimpleEntry("bookshelf_type", "always"));
	configOptions.add(new SimpleEntry("partition_type_within_level", "none"));
	configOptions.add(new SimpleEntry("sort_leaf_nodes_using", "ex.Title"));
	configOptions.add(new SimpleEntry("buttonname", "browse"));
	addClassifier("List", configOptions);
	configOptions.clear();

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(3);
		}
	    });
	
	switchToPane(FORMAT_PANE);

	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(HACK_DONOT_ICONIFY_GLI, new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(4);
		}
	    });
	tutorialEdits.getEdit(editcount++).execute();	
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(5);
		}
	    });

	switchToPane(DESIGN_PANE);
	configOptions = new ArrayList(2);
	removeClassifiersByName("List -metadata dc.Title,ex.Title");	
	configOptions.add(new SimpleEntry("metadata", "dc.Title,ex.Title"));
	addClassifier("AZCompactList", configOptions);
	moveSelectedPluginOrClassifierToTop();
	configOptions.clear();	
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(6);
		}
	    });
	
	switchToPane(FORMAT_PANE);
	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(7);
		}
	    });

	// phind
	switchToPane(DESIGN_PANE);	
	addClassifier("Phind", null);
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(8);
		}
	    });
	
	switchToPane(FORMAT_PANE);
	formatGeneralAboutPageIcon(sampleFilesPath + "/beatles/advbeat_large/images/tile.jpg");
	formatGeneralHomePageIcon(sampleFilesPath
				   + "/beatles/advbeat_large/images/beatlesmm.png");
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(9);
		}
	    });

	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();	
	configOptions = new ArrayList(3);
	configOptions.add(new SimpleEntry("process_extension", "mid"));
	configOptions.add(new SimpleEntry("file_format", "MIDI"));
	configOptions.add(new SimpleEntry("mime_type", "audio/midi"));
	addPlugin("UnknownPlugin", configOptions);
	configOptions.clear();
	
	// Doing the correct thing *before* building: setting dc.Format on midi zip file
	switchToPane(ENRICH_PANE);
	setMeta("beatles_midi.zip", "dc.Format", "Audio");
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(10);
		}
	    });
	
	switchToPane(DESIGN_PANE);
	configOptions = new ArrayList(2);
	configOptions.add(new SimpleEntry("removesuffix", "(?i)(\\s+\\d+)|(\\s+[[:punct:]].*)"));
	editClassifierByPattern(".*AZCompactList .*-metadata .*dc.Title,ex.Title.*",
				configOptions);
	configOptions.clear();
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(11);
		}
	    });

	closeCollection();	

	// copy across icons
	GSBasicTestingUtil.copyToDirectory(
	   new File(sampleFilesPath+"/beatles/advbeat_large/images"),
		//new File(GSBasicTestingUtil.getCollectFolder()+"/smallbea"));
		GSBasicTestingUtil.getCollectionFolder("smallbea"));
	
	loadCollection("small beatles");
	
	switchToPane(FORMAT_PANE);	
	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(12);
		}
	    });
	
	closeCollection();

	// Create the large beatles collection and add collageClassifier
	createCollection("large beatles", "Large beatles collection", "small beatles");
	collectionPath = "";
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "beatles/advbeat_large/import", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/discography",
				     workspaceFolder+"/beatles_midi.zip",
				     collectionPath,
				     new Runnable() {
					 public void run() {
					     // Allow file copy some time before checking if files
					     // exist in collection folder for reattempting
					     // drag-n-drop if missing
					     PAUSE(5);
					 }
				     });
	PAUSE(7);
	refreshFolderView();
	PAUSE(2);
	// only on drag n dropping beatles_midi.zip does it inherit metadata from base collection (dc.Format=Audio)
	if(!DRAG_N_DROP_ON) {
	    switchToPane(ENRICH_PANE);
	    setMeta("beatles_midi.zip", "dc.Format", "Audio");
	    //checkDocHasMetaAssignedFor("beatles_midi.zip", "dc.Format");
	}
	
	switchToPane(FORMAT_PANE);
	formatGeneralAboutPageIcon(sampleFilesPath + "/beatles/advbeat_large/images/tile.jpg");
	formatGeneralHomePageIcon(sampleFilesPath
				   + "/beatles/advbeat_large/images/beatlesmm.png");
	
	//createBuildPreviewCycle();

	switchToPane(DESIGN_PANE);
	addClassifier("Collage", null);
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(13);
		}
	    });
	
	/*
	// If just doing the browser test for the large beatles collection:
	loadCollection("large beatles");
	switchToPane(FORMAT_PANE);
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.multimedia(13);
		}
	    });
	*/
    }

    public void scannedImages() {
	//loadCollection("Paged Images");
	
	String collectionPath = "";
	String workspaceFolder;	
	Collection configOptions;
	
	String tutorial = "scannedImages";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;

	createCollection("Paged Images", "A collection sourced from an excerpt of Niupepa documents", null);	
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "/niupepa/sample_items", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/09",
				     workspaceFolder+"/10",
				     collectionPath,
				     new Runnable() {
					 public void run() {
					     // Allow it to add the PagedImagePlugin
					     closeAddPluginPopup_add(COMPULSORY);
					 }
				     });	
	
	autoAddPluginIfDragNDropOff(COMPULSORY, "PagedImagePlugin");
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.scannedImages(1);
		}
	    });
	
	switchToPane(DESIGN_PANE);
	removeClassifiersByName("ex.Source");

	configOptions = new ArrayList(2);	
	configOptions.add(new SimpleEntry("bookshelf_type", "always"));
	configOptions.add(new SimpleEntry("sort_leaf_nodes_using", "ex.Volume|ex.Number"));
	editClassifierByPattern(".*-metadata .*dc.Title,ex.Title.*", configOptions);
	configOptions.clear();

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.scannedImages(2);
		}
	    });	
	
	switchToPane(FORMAT_PANE);	
	String formatToEdit = "CL1 List -metadata dc.Title,ex.Title";
	addFormatFeatureStartsWith(formatToEdit);
	// Go to another section of the Format pane so that the newly added format stmt
	// will have been automatically restructured to have the right/expected structure
	//goToFormatGeneral();
	//formatFeature(formatToEdit);
	tutorialEdits.getEdit(editcount++).execute();
	tutorialEdits.getEdit(editcount++).execute();	
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.scannedImages(3);
		}
	    });
	
	//switchToPane(DESIGN_PANE);
	//configOptions = new ArrayList(1);	
	//configOptions.add(new SimpleEntry("sort_leaf_nodes_using", "ex.Volume|ex.Number"));
	//editClassifierByPattern(".*List .*-metadata .*dc.Title,ex.Title.*", configOptions);	
	//configOptions.clear();
	//createBuildPreviewCycle();
	
	switchToPane(DESIGN_PANE);
	chooseBrowsingClassifiers();
	addClassifier("DateList", null);
	
	switchToPane(FORMAT_PANE);
	addFormatFeatureContains("DateList");
	tutorialEdits.getEdit(editcount++).execute();
	formatFeature("global");
	tutorialEdits.getEdit(editcount++).execute();
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.scannedImages(4);
		}
	    });	
	
	switchToPane(FORMAT_PANE);
	formatFeature("global");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.scannedImages(5);
		}
	    });
	
	formatFeatureUndo();

	
	switchToPane(DESIGN_PANE);
	chooseSearchIndexes();
	//removeSearchIndexes("ex.Source");
	addIndexingLevel("section");
	setDefaultIndexingLevel("section");
	
	switchToPane(FORMAT_PANE);
	Collection searchDisplayItems = new ArrayList(1);
	searchDisplayItems.add(new SimpleEntry("Level: document", "newspaper"));
	searchDisplayItems.add(new SimpleEntry("Level: section", "page"));
	formatSearch(searchDisplayItems);

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
		    BrowserTest.scannedImages(4); // Test the formatFeatureUndo() above worked
	 	    BrowserTest.scannedImages(6); // Test the actual changes for this preview
		}
	    });

	switchToPane(FORMAT_PANE);
	formatFeature("search");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.scannedImages(7);
		}
	    });
    }
    public void advancedScannedImages() {
	loadCollection("Paged Images");
	
	String collectionPath = "";
	String workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "/niupepa/new_papers", EXPAND);

	dragNDrop(workspaceFolder+"/12", collectionPath);
	
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "/niupepa/formats", EXPAND);
	
	openCollectionPath("12", EXPAND);
	dragNDrop(workspaceFolder+"/12_3_6.item", "12");

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.advancedScannedImages(1);
		}
	    });

	switchToPane(GATHER_PANE);
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "/niupepa/new_papers", EXPAND);
	dragNDrop(workspaceFolder+"/xml", collectionPath);

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.advancedScannedImages(2);
		}
	    });
    }

    public void OAICollection()
    {
	//loadCollection("OAI Service Provider");
	
	String collectionPath = "";
	String workspaceFolder;	
	Collection configOptions;
	
	String tutorial = "OAICollection";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	createCollection("OAI Service Provider", "A collection of XML records produced by the OAI-PMH protocol", null);

	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "/oai/sample_small", EXPAND);
	dragNDrop(workspaceFolder+"/oai",
		  collectionPath,
		  new Runnable() {
		      public void run() {
			  // Optional: Allow GLI to add the OAIPlugin.
			  // This likely won't appear, as nowadays
			  // OAIPlugin appears to be in the default pipeline
			  closeAddPluginPopup_add(OPTIONAL);
		      }
		  });
	
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	autoAddPluginIfDragNDropOff(OPTIONAL, "OAIPlugin");
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("document_field", "ex.dc.Identifier"));
	editPluginByName("OAIPlugin", configOptions);
	configOptions.clear();

	configOptions.add(new SimpleEntry("screenviewsize", "300"));
	editPluginByName("ImagePlugin", configOptions);
	configOptions.clear();
	configOptions = null;

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.OAICollection(1);
		}
	    });

	switchToPane(DESIGN_PANE);
	chooseBrowsingClassifiers(); // optional
	removeClassifiersByPattern("\\s*List -metadata ex.Source .*",
				   "\\s*List -metadata dc.Title,ex.Title .*");
	
	configOptions = new ArrayList(4);
	configOptions.add(new SimpleEntry("metadata", "ex.dc.Subject"));
	configOptions.add(new SimpleEntry("buttonname", "Subjects"));
	addClassifier("AZCompactList", configOptions);
	configOptions.clear();
	
	configOptions.add(new SimpleEntry("metadata", "ex.dc.Description"));
	configOptions.add(new SimpleEntry("mingroup", "2"));
	configOptions.add(new SimpleEntry("mincompact", "1"));
	configOptions.add(new SimpleEntry("maxcompact", "10"));
	configOptions.add(new SimpleEntry("buttonname", "Captions"));
	addClassifier("AZCompactList", configOptions);
	configOptions.clear();
	configOptions = null;

	chooseSearchIndexes();
	removeSearchIndexes("text",
			    "dc.Title,ex.dc.Title,ex.Title",
			    "ex.Source");
	addSearchIndex("ex.dc.Description");

	switchToPane(FORMAT_PANE);
	Collection searchDisplayItems = new ArrayList(1);
	searchDisplayItems.add(new SimpleEntry("Index: ex.dc.Description", "descriptions"));
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.OAICollection(2);
		}
	    });
	
	switchToPane(FORMAT_PANE);
	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute(); // docNode and classifierNode
	formatFeature("search");
	tutorialEdits.getEdit(editcount++).execute(); // docNode
	formatFeature("display");
	tutorialEdits.getEdit(editcount++).execute(); // documentHeading
	tutorialEdits.getEdit(editcount++).execute(); // documentContent
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.OAICollection(3);
		}
	    });
    }

    public boolean undoUnknownConverterPlugTut() {
	//closeCollection();
	
	System.err.println("@@@ UNDOing UnknownConverterPluging tutorial changes...");
	boolean success = true;

	String djvulibre_ext = "djvulibre";
	if(Platform.isWindows()) {
	    djvulibre_ext = "DjVuLibre";
            commandLineServerStop(); // stop server to avoid file locking problems when deleting coll
	}
	
	success = success && GSBasicTestingUtil.delete(System.getenv("GSDL3HOME")
			       +"/sites/localsite/collect/djvucoll");
	success = success && GSBasicTestingUtil.delete(System.getenv("GSDL3HOME")
			       +"/interfaces/default/images/idjvu.gif");
	success = success && GSBasicTestingUtil.delete(System.getenv("GSDL3SRCHOME")
						       +"/ext/"+djvulibre_ext);

	// restore web\sites\localsite\siteConfig.xml
	try {
	    File storedSiteCfg = new File(System.getenv("GSDL3SRCHOME")
					  +"/ext/testing/src/tmp/siteConfig.xml");
	    if(storedSiteCfg.exists()) {
		File destFile = new File(System.getenv("GSDL3HOME")
				    +"/sites/localsite/siteConfig.xml");
		FileUtils.copyFile(storedSiteCfg, destFile);// can create dir, will overwrite file
		// delete from testing/tmp
		GSBasicTestingUtil.delete(storedSiteCfg.getAbsolutePath());

		// Now that siteConfig.xml has been updated, we need to restart the server
		int exitValue = commandLineServerRestart();

		if(exitValue != 0) {
		    System.err.println("### ERROR undoUnknownConverterPlugTut(): "
				       + "Restart error after restoring siteConfig.xml");
		}
	    } else if(Platform.isWindows()) {
		commandLineServerStart(); // start server again on Windows after stopping it to avoid file locking problems when deleting coll
	    }
	} catch(Exception e){
	    Assert.fail("### ERROR: undoUnknownConverterPlugTut()--error restoring siteConfig.xml"
			+ "\n\tfrom ext/testing/src/tmp to web/sites/localsite");
	}
	return success;
    }

    /**
     * The following must be the case to run this tutorial successfully:
     * 1. There should be no collection called djvucoll in web\sites\localsite\collect
     * 2. The following line must be absent from web\sites\localsite\siteConfig.xml and the server must have been restarted after the change
     *       <replace macro="_icondjvu_" scope="metadata" text="&lt;img src='interfaces/default/images/idjvu.gif' border='0'/&gt;" resolve="false"/>
     * 3. web\interfaces\default\images must not contain the file idjvu.gif
     * 4. There should be no folder called DjVuLibre in GSDL3SRCHOME\ext
     * Because the above are all changes this tutorial will make, and cannot be in place before this tutorial runs.
     * TODO: Should this tutorial first undo all the above before running the rest of the tutorial, so it can always be run on any GS3
     * installation and not always require a fresh GS3 installation?
     */
    public void unknownConverterPluginTutorial() { // DjVu example at present
	//loadCollection("DjVu Collection");
	
	String collectionPath = "";
	String workspaceFolder;	
	Collection configOptions;
	
	String tutorial = "unknownConverterPluginTutorial";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;

	createCollection("DjVu Collection", "", null);

	//workspaceFolder = openWorkspacePath(
	//	   WKS_TOP_FOLDER_SAMPLEFILES, "/djvu", EXPAND);
	//dragNDrop(workspaceFolder+"/superhero.djvu", collectionPath);

	// Test refreshFolderView after system file copy
	// instead of the usual way of dragging and dropping files:
	
	String superhero_djvu = props.get("samplefiles.treepath")+"/djvu/"+"superhero.djvu";
	File newFolder = GSBasicTestingUtil.copyIntoCollection(superhero_djvu, "djvucoll", "import");
	PAUSE(2);
	refreshFolderView();	

	String pathToScript
	    = (String)props.get("scriptsAndPatches.folder") + "get_djvulibre";//+scriptSuffix;
	//= System.getenv("GSDL3SRCHOME") + "/ext/testing/src/get_djvulibre.sh";
	String djvuLibreTxtRelPath; //= System.getenv("GSDL3SRCHOME")+"/ext/djvulibre/bin/djvutxt";

	// Windows appears to be fine with forward slashes now or even mixed slashes in paths,
	// even when launching a Java Process with full paths to executable and parameters.
	if(Platform.isWindows()) {
	    pathToScript += ".bat";
	    djvuLibreTxtRelPath = "/ext/DjVuLibre/djvutxt.exe";
	} else if(Platform.osFamily()==OSFamily.MAC) {//if(Platform.isMacintosh()||Platform.isOSX()) {
	    pathToScript += "_mac.sh";
	    djvuLibreTxtRelPath = "/ext/djvulibre/Contents/MacOS/djvutxt";
	} else { // if(Platform.isLinux()) { // assume some linux/other unix 
	    pathToScript += ".sh";
	    djvuLibreTxtRelPath = "/ext/djvulibre/bin/djvutxt";
	}

	// Install djvulibre into ext folder if it doesn't yet exist there
	File djvuLibreTxt = new File(System.getenv("GSDL3SRCHOME")+djvuLibreTxtRelPath);
	if(!djvuLibreTxt.exists()) {
	    System.err.println("@@@ "+djvuLibreTxt+" doesn't exist. Running script to get it.");
	    runCommand(new String[]{pathToScript});
	}

	String errMsg
	    = String.format("### ASSERT ERROR: executable %s doesn't exist",
			    System.getenv("GSDL3SRCHOME")+djvuLibreTxtRelPath);
	Assert.assertTrue(errMsg, djvuLibreTxt.exists());
	
	String infile = System.getenv("GSDL3HOME") +
	    "/sites/localsite/collect/djvucoll/import/superhero.djvu";
	String outfile = System.getenv("GSDL3SRCHOME") + File.separator + "superhero.txt";
	String[] cmd = { System.getenv("GSDL3SRCHOME")+djvuLibreTxtRelPath, infile, outfile };
	backgroundRunCommand(cmd);	
	
	errMsg
	    = String.format("### ASSERT ERROR: expected output file %s doesn't exist", outfile);
	Assert.assertTrue(errMsg, (new File(outfile)).exists());

	
	//loadCollection("DjVu Collection");
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();	
	configOptions = new ArrayList(5);
	configOptions.add(new SimpleEntry("exec_cmd",
	  "\\\"%%GSDL3SRCHOME"+djvuLibreTxtRelPath+"\\\" %%INPUT_FILE %%OUTPUT"));
	  //"\"%%GSDL3SRCHOME/ext/djvulibre/bin/djvutxt\" %%INPUT_FILE %%OUTPUT")); // TODO:????
	// But Windows finds following acceptable (and it may be OK that it doesn't work on linux,
	// as support for spaces in GS3 install path was implemented for Windows):
	// \"%%GSDL3SRCHOME/ext/DjVuLibre/djvutxt.exe\" %%INPUT_FILE %%OUTPUT
	configOptions.add(new SimpleEntry("convert_to", "text"));
	configOptions.add(new SimpleEntry("mime_type", "image/vnd.djvu"));
	configOptions.add(new SimpleEntry("process_extension", "djvu"));
	configOptions.add(new SimpleEntry("srcicon", "icondjvu"));
	addPlugin("UnknownConverterPlugin", configOptions);
	configOptions.clear();
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.unknownConverterPluginTutorial(1);
		}
	    });

	String djvuIcon = props.get("samplefiles.treepath")+"/djvu/"+"idjvu.gif";
	GSBasicTestingUtil.copyToDirectory(djvuIcon,
		   System.getenv("GSDL3HOME")+"/interfaces/default/images");
	
	// First back up siteConfig.xml before modifying it
	String siteConfig = System.getenv("GSDL3HOME")+"/sites/localsite/siteConfig.xml";
	File siteCfgFile = new File(siteConfig);
	try {
	    File destFile = new File(System.getenv("GSDL3SRCHOME")
				     +"/ext/testing/src/tmp/siteConfig.xml");
	    FileUtils.copyFile(siteCfgFile, destFile);	
	} catch(Exception e){
	    Assert.fail("### ERROR: unknownConverterPluginTutorial() - error backing up file\n\t"
			+ siteCfgFile + " to ext/testing/src/tmp");
	}
	
	// In siteConfig.xml, insert into replaceList: <replaceList  id="gs2-standard">	
	
	String insertion = "<replace macro=\"_icondjvu_\" scope=\"metadata\" text=\"&lt;img src='interfaces/default/images/idjvu.gif' border='0'/&gt;\" resolve=\"false\"/>";
			
	// https://stackoverflow.com/questions/15559359/insert-line-after-match-using-sed
	// For later: https://unix.stackexchange.com/questions/26284/how-can-i-use-sed-to-replace-a-multi-line-string
	//String[] cmd_args = { "sed", "-e", "'/<replaceList  id=/a\\'", "-e", "'"+insertion+"'", siteConfig};
	//backgroundRunCommand(cmd_args);	

	//https://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/FileUtils.html#readFileToString(java.io.File)
	
	long start_time = System.currentTimeMillis();
	try {
	    List<String> lines = FileUtils.readLines(siteCfgFile, ENCODING); // closes the file
	    
	    List<String> insertions = new ArrayList<String>(1);
	    insertions.add(insertion);
	    List<String> newlines = GSBasicTestingUtil.insertLinesAfter("<replaceList id=\"gs2-standard\"",
		     lines,
		     insertions,
		     "<replace macro=\"_icondjvu_\""); // not inserted if this line present
	    if(newlines.size() != lines.size()) {
		//exitGLI();
		
		FileUtils.writeLines(siteCfgFile, ENCODING, newlines, GSBasicTestingUtil.NEWLINE);

		System.err.println("@@@ Restarting GS3 server after modifying siteConfig.xml...");
		
		int exitValue = commandLineServerRestart();
		/*
		int exitValue = -1;
		if(Platform.osFamily() == OSFamily.MAC) {
		    exitValue = backgroundRunCommand(new String[] {
			    System.getenv("GSDL3SRCHOME")+"/ant-restart-with-exts.sh"});
		} else {
		    exitValue = commandLineServerRestart();
		}
		*/
		/*
		runGLI();
		
		window = GSGUITestingUtil.getGLIApplicationWindow(robot(), 10);
		//if(window == null) {
		//    System.err.println("### Window not found");
		//}		
		//GSGUITestingUtil.setRobot(robot());
		PAUSE(2);
		makeGLIWindowActive();
		stealAnyCollectionLock(1);
		PAUSE(2);
		switchToPane(DOWNLOAD_PANE);
		PAUSE(1);
		switchToPane(GATHER_PANE);
		//loadCollection("DjVu Collection");
		*/
	    } else {
		System.err.println("### Line insertion didn't happen - same number of lines");
	    }
	} catch(Exception ex) {
	    ex.printStackTrace();
	    System.err.println("### Error reading/writing file " + siteConfig);
	}
	
	switchToPane(FORMAT_PANE);
	//quickFormatPreviewCycle();
	formatFeature("search");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.unknownConverterPluginTutorial(2);
		}
	    });

	// https://stackoverflow.com/questions/4532202/how-to-calculate-the-time-difference-between-two-events-in-java
	long end_time = System.currentTimeMillis();
	float time_diff_seconds = (end_time - start_time) / 1000;
	System.err.println("@@@ Duration " + time_diff_seconds);

	closeCollection();
    }

    // This function was for developing the browser tests for downloadingOverOAI
    // It assumes the collections built over by downloadingOverOAI() already exist
    // and just runs the browser testing portion.
	// It's not used at present
    public void downloadingOverOAI_browserTestingOnly() {	
		
	String[] colls = {"OAICMDdownload","OAIGLIdownload"};
	for(String coll : colls) {
	    loadCollection(coll);
	    switchToPane(FORMAT_PANE);
	    quickFormatPreviewCycle(new Runnable() {
		    public void run() {
			BrowserTest.downloadingOverOAI();
		    }
		});
	}
    }
    
    public void downloadingOverOAI() {
	// Need backdrop collection available as OAI set on the OAIserver, for which we need to restart the GS3 server
	//commandLineServerRestart();
	
	String gsBaseURL = (String)props.get("gs.base.url");
	String downloadType = Dictionary.get("DOWNLOAD.MODE.OAIDownload");
	
	stealAnyCollectionLock(1);
	// wait for optional loading of previous collection to finish
	waitForAnyProgressPopupToGoAway(Dictionary.get("CollectionManager.Loading_Collection"),
					MAX_WAIT_TIMEOUT_SECONDS);

	// can't make GLI active without clicking on some pane, even some time spent (like
	// clearing the cache here, as is *needed* for repeating this tutorial) helps.
	switchToPane(DOWNLOAD_PANE);
	// clear Cache from OAI section instead of web section
	//clearDownloadCache();
	openDownloadType(downloadType);
	clearCache();
	
	// Need GS3's library page loaded to be able to download from its oaiserver page
	//backgroundRunCommand(new String[]{"firefox", "--new-tab", "--url", gsBaseURL+"library"});
	_driver.get(gsBaseURL+"library");
	makeGLIWindowActive();
	
	// (1) First do the downloading over command line version of the tutorial
	
	// Create the collection first, because on Windows there's some issue when this is done
	// after backgroundRunCommand()/before createOAIDownloadCollection() whereby the created
	// collection is not ready by the time the Gather pane is clicked and then the 
	// refreshFolderView() fails because we're not in the Gather pane.
	createCollection("OAICMDdownload", "Docs downloaded from command line", null);
	
	//switchToPane(DOWNLOAD_PANE); // can't make GLI active without clicking on some pane
	//clearDownloadCache();
	
	String pathToScript
	    = (String)props.get("scriptsAndPatches.folder") + "oai_cmdline_download";
	    //System.getenv("GSDL3SRCHOME") + "/ext/testing/src/oai_cmdline_download";
	pathToScript += Platform.isWindows() ? ".bat" : ".sh";

	// using backgroundRunCommand i.p.o. runCommand as the script finishes too fast
	// to see in terminal anyway
	backgroundRunCommand(new String[]{pathToScript, gsBaseURL});
	
	// createCollection("OAICMDdownload", "Docs downloaded from command line", null); // Moved up due to Win issues
	createOAIDownloadCollection();
	closeCollection();

	// (2) Now do the downloading through GLI version of the tutorial
	prepareForDownloading();
	switchToPane(DOWNLOAD_PANE);
	// clear Cache from OAI section
	openDownloadType(downloadType);
	clearCache(); //clearDownloadCache();
	
	System.err.println("@@@ base URL " + gsBaseURL);
	System.err.println("@@@ downloadType " + downloadType);	

	// check serverInfo for oaiserver page
	Collection configOptions = new ArrayList(6);
	configOptions.add(new SimpleEntry("Source URL", gsBaseURL+"oaiserver"));
	//openDownloadType(downloadType);
	configurePanel(window, EDIT_MODE, configOptions);
	Map proxyOpts = getProxyOpts();
	serverInfo(proxyOpts);
	
	configOptions.clear();
	configOptions.add(new SimpleEntry("Metadata prefix", null));
	configOptions.add(new SimpleEntry("Restrict to set", "backdrop"));
	configOptions.add(new SimpleEntry("Get document", null));
	configOptions.add(new SimpleEntry("Only include file types", "jpg,doc,pdf,ppt"));
	configOptions.add(new SimpleEntry("Max records", "10"));

	download(downloadType, configOptions, proxyOpts, MAX_WAIT_DOWNLOAD_SECONDS);
	
	configOptions.clear();
	configOptions = null;
	if(proxyOpts != null) {
	    proxyOpts.clear();
	    proxyOpts = null;
	}
	
	createCollection("OAIGLIdownload", "Docs downloaded using GLI", null);

	createOAIDownloadCollection();
	
    }

    private void createOAIDownloadCollection() {
	String collectionPath = "";
	String workspaceFolder;
	Collection configOptions;
	
	String tutorial = "OAIDownload";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	switchToPane(GATHER_PANE);
	// Copy folder "localhost" across from "Downloaded Files" GLI workspace shortcut!
	
	workspaceFolder = openWorkspacePath(WKS_TOP_FOLDER_DOWNLOADS, "localhost", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/D0.jpg",
				     workspaceFolder+"/D8.oai",
				     collectionPath);
	
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("no_cover_image", null));
	editPluginByName("OAIPlugin", configOptions);
	//closePopup(OPTIONAL, Dictionary.get("NoPluginExpectedToProcessFile.Title"), Dictionary.get("General.OK"), TICK_CHECKBOX);
	
	switchToPane(CREATE_PANE);
	buildCollectionTillReady(MAX_BUILD_TIMEOUT); //createBuildPreviewCycle();	

	switchToPane(ENRICH_PANE);
	String[] metanames = { "ex.dc.Title", "ex.dc.Description", "ex.dc.Identifier"};
	checkDocHasMetaAssignedFor("D0.jpg", metanames);


	// The rest below is just making this collection look like the backdrop collection
	// (but further also corrects classifier node and formats the search format statement)
	switchToPane(DESIGN_PANE);
	chooseBrowsingClassifiers();
	removeClassifiersByName("ex.Source");
	configOptions = new ArrayList(3);
	configOptions.add(new SimpleEntry("metadata", "ex.dc.Description"));
	configOptions.add(new SimpleEntry("partition_type_within_level", "none"));
	configOptions.add(new SimpleEntry("buttonname", "Description"));
	addClassifier("List", configOptions);
	configOptions.clear();
	configOptions = null;
	
	removeSearchIndexes("text", "ex.Source");
	addSearchIndex("ex.dc.Description");
	
	switchToPane(FORMAT_PANE);	
	Collection searchDisplayItems = new ArrayList(1);
	searchDisplayItems.add(new SimpleEntry("Index: ex.dc.Description", "image descriptions"));
	formatSearch(searchDisplayItems);

	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute();
	formatFeature("search");
	tutorialEdits.getEdit(editcount++).execute();

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
		    // whether the collection is built through cmdline scripts or GLI,
		    // the browser testing part for such collections is the same
	 	    BrowserTest.downloadingOverOAI();
		}
	    });
    }

    public void METS() {
	String collFolderName = "smallhtm";
	
	loadCollection("Small HTML Collection");
	changeUserMode("expert");
	
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	removePluginsByName("GreenstoneXMLPlugin");
	addPlugin("GreenstoneMETSPlugin", null);
	// Move METS plug up to just under ZipPlug
	movePluginOrClassifierNearTo(
		     Pattern.compile(" GreenstoneMETSPlugin.*"),
		     Pattern.compile(" ZIPPlugin.*"),
		     UP);
	
	switchToPane(CREATE_PANE);
	
	Collection configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("saveas", "GreenstoneMETS"));
	configureImportOptions(configOptions);
	configOptions.clear();
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.METS();
		}
	    });
	
	changeUserMode("librarian");
	
	String sampleLocation = "archives/HASH01c1.dir/";
	System.err.println("Checking a docmets.xml and doctxt.xml exist in sample location "
			   + sampleLocation);
	String errMsg
		= String.format("### ASSERT ERROR: %s didn't exist in %s/%s.",
				"doc(mets/txt).xml", collFolderName, sampleLocation);
	Assert.assertTrue(errMsg, GSBasicTestingUtil.checkCollectionFileExists(collFolderName,
				       sampleLocation + "docmets.xml"));
	Assert.assertTrue(errMsg, GSBasicTestingUtil.checkCollectionFileExists(collFolderName,
				       sampleLocation + "doctxt.xml"));
	/*
	switchToPane(FORMAT_PANE);
	quickFormatPreviewCycle(new Runnable() {
		public void run() {		    
		    BrowserTest.METS();
		}
	    });
	*/
    }
    
    public void stoneD() {
	//loadCollection("StoneD");
	
	String collectionPath = "";
	String workspaceFolder;	
	Collection configOptions;
	
	String tutorial = "stoneD";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;	

	createCollection("StoneD", "Moving a collection from DSpace to Greenstone", null);

	//switchToPane(DESIGN_PANE);
	//chooseDocumentPlugins();
	//addPlugin("DSpacePlugin", null);
	//movePluginOrClassifierNearTo(
	//	     Pattern.compile(" DSpacePlugin.*"),
	//	     Pattern.compile(" ZIPPlugin.*"),
	//	     UP);
	//
	//switchToPane(GATHER_PANE);
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "/dspace", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/1",
				     workspaceFolder+"/5",
				     collectionPath,
				     new Runnable() {
					 public void run() {
					     // TODO: problem if drag and drop failed. Must think.
					     
					     // Don't add LOMPlugin
					     closeAddPluginPopup_doNotAdd(COMPULSORY);
					     // Add DSpacePlugin
					     closeAddPluginPopup_add(COMPULSORY);
					     // Ignore LOMPlugin suggestion for remaining 4 docs
					     for(int i = 0; i <= 4; i++) {
						 closeAddPluginPopup_doNotAdd(OPTIONAL);
					     }
					 }
				     });	

	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	autoAddPluginIfDragNDropOff(COMPULSORY, "DSpacePlugin");
	movePluginOrClassifierNearTo(
		     Pattern.compile(" DSpacePlugin.*"),
		     Pattern.compile(" ZIPPlugin.*"),
		     UP);	
	
	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.stoneD(1);
		}
	    });
	   
	switchToPane(DESIGN_PANE);
	chooseDocumentPlugins();
	configOptions = new ArrayList(1);
	configOptions.add(new SimpleEntry("first_inorder_ext", "pdf,doc,rtf"));
	editPluginByName("DSpacePlugin", configOptions);
	configOptions.clear();
	configOptions = null;

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.stoneD(2);
		}
	    });

	switchToPane(DESIGN_PANE);
	chooseSearchIndexes();
	removeSearchIndexes("ex.Source");
	addSearchIndex("ex.dc.Contributor");

	switchToPane(FORMAT_PANE);	
	Collection searchDisplayItems = new ArrayList(1);
	searchDisplayItems.add(new SimpleEntry("Index: ex.dc.Contributor", "contributors"));
	formatSearch(searchDisplayItems);

	switchToPane(DESIGN_PANE);	
	chooseBrowsingClassifiers();
	configOptions = new ArrayList(4);
	configOptions.add(new SimpleEntry("metadata", "ex.dc.Contributor"));
	configOptions.add(new SimpleEntry("bookshelf_type", "always"));
	configOptions.add(new SimpleEntry("partition_type_within_level", "none"));
	configOptions.add(new SimpleEntry("buttonname", "Contributors"));
	editClassifierByPattern(".*List .*-metadata .*ex.Source.*", configOptions);
	configOptions.clear();
	configOptions = null;	
	
	switchToPane(FORMAT_PANE);
	formatFeature("browse");
	tutorialEdits.getEdit(editcount++).execute(); // add equivdoc to default browse docNode
	
	String formatToEdit = "CL2 List -metadata ex.dc.Contributor";
	addFormatFeatureStartsWith(formatToEdit);
	tutorialEdits.getEdit(editcount++).execute(); // CL2, editing documentNode
	tutorialEdits.getEdit(editcount++).execute(); // still CL2, edit classifierNode
	tutorialEdits.getEdit(editcount++).execute(); // add equivdoc to CL2's docNode

	formatFeature("search");
	tutorialEdits.getEdit(editcount++).execute(); // add equivdoc to search docNode

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.stoneD(3);
		}
	    });
    }

    public void gs_to_dspace_cmdl() {
	// Minimise GLI and move browser out of the way to view terminal
	// As this tutorial is purely commandline. Don't use runCommand() as it will
	// maximise GLI too soon (as soon as script given to runCommand finishes)
	// whereas we want to inspect contents of export folder after that.
	minimiseGLI(); //window.iconify();
	GSSeleniumUtil.shrinkBrowser();
	
	String collFolderName = "stoned";
	
	String pathToScript
	    = (String)props.get("scriptsAndPatches.folder") + "gs_to_dspace_cmdline_tut";
	    //= System.getenv("GSDL3SRCHOME") + "/ext/testing/src/gs_to_dspace_cmdline_tut";
	pathToScript += Platform.isWindows() ? ".bat" : ".sh";

	// backgroundRunCommand, because we already got GLI and browser out of the way
	backgroundRunCommand(new String[]{pathToScript});
	
	String errMsg = String.format(
	      "### ASSERT ERROR: collection folder %s/export didn't get created.",collFolderName);
	Assert.assertTrue(errMsg, GSBasicTestingUtil.checkCollectionFolderExists(collFolderName,
				       "export"));
	
	File exportFolder = new File(GSBasicTestingUtil.getCollectionFolder(collFolderName),
				     "export");
	File[] files = exportFolder.listFiles();
	for(File file : files) {
	    System.err.println("@@@ Found DSpace exported folder: " + file.getName());
	    String errMsg1
		= String.format("### ASSERT ERROR: %s was missing in %s/export/%s.",
				"contents", collFolderName, file.getName());
	    String errMsg2
		= String.format("### ASSERT ERROR: %s was missing in %s/export/%s.",
				"dublin_core.xml", collFolderName, file.getName());

	    // Each exported subfolder should contain a "contents" and "dublin_core.xml" file
	    File contentsFile = new File(file, "contents");
	    File dcFile = new File(file, "dublin_core.xml");
	    Assert.assertTrue(errMsg1, contentsFile.exists());
	    Assert.assertTrue(errMsg2, dcFile.exists());
	}
	// If we got here, no assertion failures
	System.err.println("@@@ Each exported DSpace folder in " + collFolderName
			   + " contained the expected contents and dublin_core.xml files.");

	PAUSE(2); // Allow time to eyeball terminal output

	// Restore browser and GLI
	GSSeleniumUtil.restoreBrowser();
	/*
	if(Platform.osFamily() == OSFamily.MAC) {
	    makeGLIWindowActive(); // TODO: test if this is enough for all OS
	} else {
	    window.normalize(); // want GLI back to normal, non-minimised state
	}
	*/
	normaliseGLI(); // want GLI back to normal, non-minimised state
    }

	public boolean undoOAIServerTutorialChanges() {
		System.err.println("@@@ UNDOing any OAIServer tutorial changes...");
		/*
		int exitValue = commandLineServerStop();	
		if(exitValue != 0) {
			System.err.println("### ERROR OAIServer() test: "
					   + "error during GS3 Server command line stop.");
		}*/
		// Undo state of /WEB-INF/classes/OAIConfig-oaiserver.xml.in, put it back to orig
		//GSBasicTestingUtil.copy(System.getenv("GSDL3SRCHOME") + "/resources/oai/OAIConfig.xml.in",
		//	System.getenv("GSDL3HOME") + "/web/WEB-INF/classes/"); // will overwrite
		
		// Why do I have to delete it first for the new version copied over to take effect? Even if embedded
		// within an ant stop and (re)start, it still doesn't work if I don't delete the destination file.
		boolean deleteSuccess = GSBasicTestingUtil.delete(System.getenv("GSDL3HOME") + "/WEB-INF/classes/OAIConfig-oaiserver.xml");
		Assert.assertTrue(
			"### Failed to delete file web/WEB-INF/classes/OAIConfig-oaiserver.xml",
			deleteSuccess);
			
		boolean moveSuccess = GSBasicTestingUtil.moveFile(System.getenv("GSDL3SRCHOME")+"/resources/oai",
					   "OAIConfig-oaiserver", ".bak1", ".xml.in"); // overwrite existing .xml.in
		Assert.assertTrue(
			"### Failed to move file resources/oai/OAIConfig-oaiserver.xml.in from .bak1", moveSuccess);
			
		boolean copySuccess = GSBasicTestingUtil.copyFile(
			System.getenv("GSDL3SRCHOME") + "/resources/oai/OAIConfig-oaiserver.xml.in",
			System.getenv("GSDL3HOME") + "/WEB-INF/classes/OAIConfig-oaiserver.xml.in",
			true); // true to overwrite if file exists
		Assert.assertTrue(
			"### Failed file copy from resources/oai/OAIConfig.xml.in to web/WEB-INF/classes/OAIConfig-oaiserver.xml.in",
			copySuccess);
		
		/*int exitValue = commandLineServerRestart();
		
		if(exitValue != 0) {
			System.err.println("### ERROR OAIServer() test: "
					   + "error during GS3 Server command line start.");
		}*/
		return deleteSuccess && moveSuccess && copySuccess;
	}

    public void OAIserver() {

	// make backup of resources/oai/OAIConfig-oaiserver.xml.in iff not already present,
	// to be able to undo this tutorial later
	try {
	    GSBasicTestingUtil.copyFile(System.getenv("GSDL3SRCHOME")+"/resources/oai",
					"OAIConfig-oaiserver", ".xml.in", ".bak1", false);
	} catch(Exception e) {
	    e.printStackTrace();
	    System.err.println("### OAIserver(): Error when backing up resources/oai/OAIConfig-oaiserver.xml.in");
	}

	minimiseGLI();
	
	PAUSE(1);

	runGSI();
	FrameFixture gsi_win = GSGUITestingUtil.getGSIApplicationWindow(robot(), 10);
	if(gsi_win == null) {
	    System.err.println("### GSI window not found");
	    System.exit(-1);
	}
	PAUSE(2);
	//GSI_settings();

	String adminEmail = (String)props.get("admin.email");
	
	GSI_enterRestartLibrary(new Runnable() {
	 	public void run() {		    
	 	    BrowserTest.OAIServer(1, adminEmail);
		    //PAUSE(25);
		}
	    });

	// "The modifications to OAIConfig-oaiserver.xml.in need to be brought into effect,
	// so quit and relaunch the Greenstone 3 server application if it is already running."
	//PAUSE(5);
	/*
	exitGSI();	
	PAUSE(5); // give server some time to stop. TODO: make it a conditional pause
	// using wget on the main page to determine server has stopped before restarting?	
	runGSI();
	gsi_win = GSGUITestingUtil.getGSIApplicationWindow(robot(), 10);
	if(gsi_win == null) {
	    System.err.println("### GSI window not found");
	    System.exit(-1);
	}
	PAUSE(2);
	*/
	// After changes to org.greenstone.util.RunTarget and
	// org.greenstone.server.Server3/BaseServer, to run ant commands from GSDL3SRCHOME
	// (so no more errors about missing build.xml), the following now works by itself
	// for automated testing even without having to first exitGSI() and runGSI() again.
	// 
	// OAI Config file would have been updated if there were no assertion failures
	// So restart the server. Then check the adminEmail is now shown on verb=identify page
	GSI_enterRestartLibrary(new Runnable() {
	 	public void run() {
	 	    BrowserTest.OAIServer(2, adminEmail);
		}
	    });

	exitGSI();
	PAUSE(1);

	int exitValue = commandLineServerRestart();
	
	if(exitValue != 0) {
	    System.err.println("### ERROR OAIServer() test: "
			       + "error during GS3 Server command line restart.");
	}
	
	normaliseGLI();
	PAUSE(2);
    }
    
    public void GEMStutorial() {
	//exitGLI(); // fine (on linux at least) to leave GLI running in the background
	
	PAUSE(1);
	System.err.println("@@@ UNDOing any GEMS tutorials changes...");
	boolean success = true;
	File mdsFile = new File(System.getenv("GSDL3SRCHOME")+"/gli/metadata/my.mds");
	if(mdsFile.exists()) {
	    success = success && GSBasicTestingUtil.delete(mdsFile.getAbsolutePath());
	}
	
	long start_time = System.currentTimeMillis();
	runGEMS();
	FrameFixture gems_win = GSGUITestingUtil.getGEMSApplicationWindow(robot(), 10);
	if(gems_win == null) {
	    System.err.println("### GEMS window not found");
	    System.exit(-1);
	}		
	//GSGUITestingUtil.setRobot(robot());
	PAUSE(2);

	// https://stackoverflow.com/questions/4532202/how-to-calculate-the-time-difference-between-two-events-in-java
	long end_time = System.currentTimeMillis();
	float time_diff_seconds = (end_time - start_time) / 1000;
	System.err.println("@@@ Duration " + time_diff_seconds);

	String mdset_title = "My Metadata Set";
	newMetadataSet(mdset_title, "my", null,
		       "Development Library Subset Example Metadata");
	//openMetadataSet(mdset_title);
	addMetadataSetElement(mdset_title, "Category");
	exitGEMS(true); //true to save before exit
	
	//window = getGLIApplicationWindow(robot, 10);
    }

    public void indexers() {
	turnOnDragNDrop();
	//loadCollection("Demo Lucene");
	
	String collectionPath = "";
	String workspaceFolder;
	
	createCollection("Demo Lucene", "Uses default Lucene indexer", "lucene-jdbm-demo");

	// copy from Documents in Greenstone Collections folder!
	workspaceFolder = openWorkspacePath(
	   WKS_TOP_FOLDER_DOCS_GS,
	   "/localsite/Demo Collection (Lucene) [lucene-jdbm-demo]",
	   EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/b17mie",
				     workspaceFolder+"/wb34te",
				     collectionPath,
				     new Runnable() {
					 public void run() {
					     // Skip merging metadata: the dialog appears twice. NOT if basing coll on lucene-jdbm-demo
					     //for(int i = 0; i < 2; i++) {
					     //    closePopupByName(COMPULSORY, "ModalDialog.MetadataImportMappingPrompt", Dictionary.get("MIMP.Ignore"));
					     //}
					     
					     // Let GLI add the ImagePlugin
					     closeAddPluginPopup_add(COMPULSORY);
					 }
				     });

	
	
	switchToPane(ENRICH_PANE);
	String[] metanames = { "dc.Title", DC_SUBJECT, "dc.Resource Identifier", "dc.Language" };
	String[] folders = { "b17mie", "b18ase", "b20cre", "b21wae", "b22bue", "ec158e",
			     "ec159e", "ec160e", "fb33fe", "fb34fe", "wb34te" };
	System.err.println("@@@@ Checking that folders: " + Arrays.toString(folders)
			   + "\n\t contain meta for: " + Arrays.toString(metanames));
	for(String folder : folders) {
	    checkDocHasMetaAssignedFor(folder, metanames);
	}
	
	switchToPane(DESIGN_PANE);
	autoAddPluginIfDragNDropOff(COMPULSORY, "ImagePlugin");
	chooseSearchIndexes();
	checkIndexerInUse("Lucene");

	createBuildPreviewCycle(new Runnable() {
	 	public void run() {
	 	    BrowserTest.indexers(1);
		}
	    });


	// MGPP indexer
	//loadCollection("Demo MGPP");
	
	createCollection("Greenstone Demo MGPP", "Uses the MGPP indexer", "lucene-jdbm-demo");

	// copy from Documents in Greenstone Collections folder!
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_DOCS_GS, "/localsite/Demo Collection (Lucene) [lucene-jdbm-demo]", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/b17mie",
				     workspaceFolder+"/wb34te",
				     collectionPath,
				     new Runnable() {
					 public void run() {
					     // Let GLI add the ImagePlugin
					     closeAddPluginPopup_add(COMPULSORY);
					 }
				     });
	
	switchToPane(DESIGN_PANE);
	autoAddPluginIfDragNDropOff(COMPULSORY, "ImagePlugin");
	chooseSearchIndexes();
	checkIndexerInUse("Lucene");
	changeToIndexer("MGPP");

	checkSearchIndexOptionsTicked("stem", "casefold", "accentfold");
	addIndexingLevel("section");
	setDefaultIndexingLevel("document");
	
	createBuildPreviewCycle(new Runnable() {
		    public void run() {
			BrowserTest.indexers(2);
		    }
		});
	
	// If just Browser testing:
	/*
	loadCollection("Demo Lucene");
	switchToPane(FORMAT_PANE);
	quickFormatPreviewCycle(new Runnable() {
		public void run() {		    
		    BrowserTest.indexers(1);
		}
	    });
	
	loadCollection("Demo MGPP");
	switchToPane(FORMAT_PANE);
	quickFormatPreviewCycle(new Runnable() {
		public void run() {		    
		    BrowserTest.indexers(2);
		}
	    });
	*/
	turnOffDragNDrop();
    }


    public void incrementalBuilding() {
	// 3 incremental building collections: same previewing/browsert tests, but slightly
	// diff cmd line scripts. These differences are handled by incremental_building_tut.sh
	
	createCollection("Incremental With Manifests",
			 "Incremental building from the command line",
			 "lucene-jdbm-demo");
	testIncrementalCollection("incremen");

	
	createCollection("Auto incremental",
			 "Incremental building without manifests",
			 "lucene-jdbm-demo");
	testIncrementalCollection("autoincr");

	
	// TODO: need to run 2-in-1 incremental import *and* build scripts for
	// the 2nd auto incremental collection	
	createCollection("2 Auto incremental",
			 "Incremental building without manifests with a single build script",
			 "lucene-jdbm-demo");
	testIncrementalCollection("2autoinc");
    }

    public void testIncrementalCollection(String collFolderName) {
	String collectionPath = "";
	String workspaceFolder;
	
	workspaceFolder = openWorkspacePath(
		   WKS_TOP_FOLDER_SAMPLEFILES, "/incr_build/import", EXPAND);
	dragNDropContiguousSelection(WORKSPACE_VIEW,
				     workspaceFolder+"/b17mie",
				     workspaceFolder+"/b20cre",
				     collectionPath,
				     new Runnable() {
					 public void run() {
					     // Let GLI add the ImagePlugin
					     closeAddPluginPopup_add(COMPULSORY);
					 }
				     });
	
	switchToPane(DESIGN_PANE);
	autoAddPluginIfDragNDropOff(COMPULSORY, "ImagePlugin");
	chooseSearchIndexes();
	//PAUSE(1);
	setDefaultIndexingLevel("document");
	//PAUSE(1);
	// Do *not* build in GLI!
	closeCollection();

	// minimise GLI
	minimiseGLI(); //window.iconify();

	
	int exitValue = -1;
	String pathToScript
	    = (String)props.get("scriptsAndPatches.folder") + "incremental_building_tut";
	    //System.getenv("GSDL3SRCHOME") + "/ext/testing/src/incremental_building_tut";
	pathToScript += Platform.isWindows() ? ".bat" : ".sh";
	
	// each incremental building collection that we call this function on (incremen,
	// autoincr, 2autoincr) has 4 preview tests
	final int numPreviews = 4;

	String gsBaseURL = (String)props.get("gs.base.url"); // "...://localhost:8383/greenstone3/"
	final String previewURLprefix = gsBaseURL+"/library/collection/";
	
	String sampleFilesPath = (String)props.get("samplefiles.treepath");
	//if(Platform.isWindows()) {
	//	sampleFilesPath = sampleFilesPath.replace("/", "\\");
	//}
	
	for(int prepForPreview = 1; prepForPreview <= numPreviews; prepForPreview++) {
	    // Shrink browser to view terminal
	    GSSeleniumUtil.shrinkBrowser();
	    
	    exitValue = runCommand_withoutExitValCheck(new String[]{
		    pathToScript,
		    collFolderName,
		    sampleFilesPath,//sampleFilesPath.replace("\"", ""),
		    Integer.toString(prepForPreview)});
	    // The script will return -1 or 1-4.
	    // Check the exit value here: an exit value of -1 is the error.
	    // exit values of 1-4 is expected as they represent the next (nth) preview
	    // the cmdline script has prepared us for.
	    if(exitValue < 0) {
		System.err.println("*** Script " + pathToScript + " exitted abnormally with exit value " + exitValue);
		break;
	    }

	    // put browser back, to view browser testing
	    GSSeleniumUtil.restoreBrowser();
	    
	    // else successful exit value: do the preview test
	    _driver.get(previewURLprefix+collFolderName); //open the collection in browser
	    BrowserTest.incrementalBuilding(exitValue); //exitval should be same as prepForPreview
	    PAUSE(1.5); // let us have a look at result before shrinking browser again
	}

	String errMsg = "Error running script " + pathToScript
	    + "\nLook for error messages above starting with ###\n";
	Assert.assertTrue(errMsg, exitValue >= 0);
	// Else Assert test succeeded:
	System.err.println("@@@@ Incremental building collection " + collFolderName
			   + " tested to completion successfully");
	
	normaliseGLI(); // when we want GLI back to normal, non-minimised state
    }

    // After changing admin pwd and theme, this function now puts admin pwd
    // and default theme back at the end
    public void customisingThemes() {
	String tutorial = "customisingThemes";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	String sampleFilesPath = (String)props.get("samplefiles.treepath");

	// back up default/pref.xsl	
	File prefXSL = new File(System.getenv("GSDL3HOME")
				+"/interfaces/default/transform/pages/pref.xsl");
	try {
	    File destFile = new File(System.getenv("GSDL3SRCHOME")+"/ext/testing/src/tmp/pref.xsl");
	    FileUtils.copyFile(prefXSL, destFile);	
	} catch(Exception e){
	    Assert.fail("### ERROR: customisingThemes() - error backing up file\n\t"
			+ prefXSL + " to ext/testing/src/tmp");
	}
	
	// Go to the default library home page to do some logging in and theme switching
	refreshServerAndPreview();
	BrowserTest.customisingThemes(1);
	
	// Configure the jqueryUI theme and download it
	refreshServerAndPreview("http://jqueryui.com/themeroller/");
	BrowserTest.customisingThemes(2);
	
	unpackDownloadedThemeAs("TutorialTheme",
				System.getenv("GSDL3HOME")+"/interfaces/default/style/themes");
	
		
	// edit pref.xsl to add TutorialTheme
	tutorialEdits.getEdit(editcount++).execute();
	
	GSBasicTestingUtil.copyToDirectory(sampleFilesPath+"/downloads/TutorialTheme.png",
		   System.getenv("GSDL3HOME")+"/interfaces/default/style/images");
	
	refreshServerAndPreview();
	// If just testing customisingThemes(3), then login as admin (with new pwd) first
	//BrowserTest.logIn(BrowserTest.adminUser, BrowserTest.newAdminPwd);
	BrowserTest.customisingThemes(3);

	PAUSE(3); // no way to test the new theme, TutorialTheme

	// Put it all back
	BrowserTest.logout();
	BrowserTest.resetAdminPwd();
	BrowserTest.switchThemeUnderPrefs("Greenstone Default");
	BrowserTest.logout();
	
	makeGLIWindowActive();
    }
    
    public void collectionSpecificTheme() {
	String tutorial = "collectionSpecificTheme";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	String sampleFilesPath = (String)props.get("samplefiles.treepath");


	// back up backdropCollConfig
	File backdropCollConfig = new File(System.getenv("GSDL3HOME")
			       +"/sites/localsite/collect/backdrop/etc/collectionConfig.xml");
	try {
	    
	    File destFile = new File(System.getenv("GSDL3SRCHOME")
				     +"/ext/testing/src/tmp/backdropCollCfg.xml");
	    FileUtils.copyFile(backdropCollConfig, destFile);
	} catch(Exception e){
	    Assert.fail("### ERROR: collectionSpecificTheme() - error backing up file\n\t"
			+ backdropCollConfig + " to ext/testing/src/tmp");
	}
    
	refreshServerAndPreview("http://jqueryui.com/themeroller/");
	// Go to JQuery UI Themeroller, create and download the collection-specific theme
	BrowserTest.collectionSpecificTheme(1);
	
	unpackDownloadedThemeAs("CollectionTheme",
		System.getenv("GSDL3HOME")+"/sites/localsite/collect/backdrop/style");

	// Now in GLI, need to edit format statement of backdrop coll to use CollectionTheme
	makeGLIWindowActive();
	loadCollection("backdrop");
	switchToPane(FORMAT_PANE);
	formatFeature("global");
	tutorialEdits.getEdit(editcount++).execute();
	quickFormatPreviewCycle(new Runnable() {
		public void run() {
		    BrowserTest.collectionSpecificTheme(2); // TODO: no way to test theme installed
		}
	    });
    }

    private void unpackDownloadedThemeAs(String themeFolderName, String destDir) {
	
	String systemUsername = (String)props.get("system.username");
	File downloadsFolder = null;
	
	if(Platform.isLinux()) {
	    // https://askubuntu.com/questions/608320/where-is-my-download-folder
	    downloadsFolder = new File("/home/"+systemUsername+"/Downloads");	    
	} else if(Platform.isWindows()) {
		//System.err.println("### Windows: Extraction of themes zip file not yet implemented.");
		// https://stackoverflow.com/questions/27460275/get-the-windows-download-folders-path-download-shell-folder-in-a-batch-file#71496374
		downloadsFolder = new File(System.getenv("userprofile") + "/Downloads");	    
	} else {
	    //System.err.println("### Mac: Extraction of themes zip file not yet implemented.");
	    downloadsFolder = new File("/Users/"+systemUsername+"/Downloads");
	}

	if(downloadsFolder == null || !downloadsFolder.exists()) {
	    Assert.fail("### Could not determine Downloads folder for OS "+Platform.osFamily());
	} else {
	    File[] jquery_ui_zips = downloadsFolder.listFiles(new FilenameFilter() {
		    public boolean accept(File dir, String name) {
			if(name.startsWith("jquery-ui") && name.endsWith(".zip")) {
			    return true;
			}
			return false;
		    }
		});
	    // if there's more than one jquery-ui zip downloaded, sort by name
	    // then reverse sort by last modified (newer/larger timestamps first)
	    Arrays.sort(jquery_ui_zips); // sorts by name
	    Arrays.sort(jquery_ui_zips, new Comparator<File>() {
		    public int compare(File f1, File f2) {
			// sort f1 first if it has the same or greater timestamp
			return (f1.lastModified() >= f2.lastModified()) ? -1 : 1;
		    }
		});
	

	    // Now we can just grab the first file in the sorted list and unzip it
	    File themeFolder = new File(downloadsFolder + "/" + themeFolderName);
	    try {
		//GSBasicTestingUtil.unzip(jquery_ui_zips[0], themeFolder);
		GSBasicTestingUtil.unzip(jquery_ui_zips[0], downloadsFolder);

		// Change the name of the extraction dir to themeFolderName
		// The latest jquery ui zip may have (num).zip suffix, but
		// the folder inside it will not have (num) suffix, which is important
		// for the renaming step
		File outDir = new File(downloadsFolder,
				       //jquery_ui_zips[0].getName().replace(".zip", ""));
				       jquery_ui_zips[0].getName().replaceFirst("(\\(\\d+\\))*.zip", ""));
		if(!outDir.renameTo(themeFolder)) {
		    throw new Exception("Folder rename failed: from"+outDir+"\n\tto:"+themeFolder);
		}

		// moveToDirectory is the better choice, as undoCustomisingThemes() can then work
		// Else, with a copy here, we'd need to delete the downloads/themeFolderName dir
		// when undoing the themes tut, for which need to work out Downloads dir again
		//GSBasicTestingUtil.copyToDirectory(themeFolder,
		//   new File(System.getenv("GSDL3HOME")+"/interfaces/default/style/themes"));
		FileUtils.moveToDirectory(themeFolder, new File(destDir), true); // create dir
		
	    } catch(Exception e){
		Assert.fail("### ERROR: Error unzipping "+jquery_ui_zips[0]+"\n\t"+e.getMessage());
	    }
	}
    }
    
    /* To undo the changes made by this tutorial:
     * 1. sites/localsite/siteConfig.xml : The Best Digital Library -> My Greenstone Library
     * 2. interfaces/default/interfaceConfig.xml : home-tutorial.xsl -> home.xsl
     * 3. interfaces/default/transform/pages: can remove home-tutorial.xsl
     * 4. interfaces/default/style/themes: can remove tutorialbliss folder
     * 5. Remember to restart Greenstone after above changes to undo this tutorial:
     * gs3-svn> ./ant-logreset-restart-with-exts.sh
     */
    public void customisingHomePage() {
	
	minimiseGLI();
	USE_GSI_TO_RESTART_SERVER = false;	
	
	String sampleFilesPath = (String)props.get("samplefiles.treepath"); // fwd-slash version
	
	String gsBaseURL = (String)props.get("gs.base.url");
	final String LIBRARY_HOME_PAGE = gsBaseURL + "library";
	
	String tutorial = "customisingHomePage";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;

	// make backups of siteConfig.xml and interfaceConfig.xml *iff* they don't exist already
	// to be able to undo this tutorial later
	try {
	    GSBasicTestingUtil.copyFile(System.getenv("GSDL3HOME")+"/interfaces/default",
					"interfaceConfig", ".xml", ".bak1", false);
	    GSBasicTestingUtil.copyFile(System.getenv("GSDL3HOME")+"/sites/localsite",
					"siteConfig", ".xml", ".bak1", false);
	} catch(Exception e) {
	    e.printStackTrace();
	    System.err.println("### customisingHomePage(): Error when attempting to back up"
			       + " default/interfaceConfig.xml and localsite/siteConfig.xml");
	}
	
	/*if(GSGUITestingUtil.gsi_window == null) {
	    runGSI();
	    GSGUITestingUtil.getGSIApplicationWindow(robot(), 10);
	    GSI_enterRestartLibrary(null);
	    }*/

	// Single replacement in interfaceConfig.xml
	/*
	String xmlFile = System.getenv("GSDL3HOME")+"/interfaces/default/interfaceConfig.xml";
	String[] before = { "<subaction name=\"home\" xslt=\"pages/home.xsl\"/>" };
	String[] after = { "<subaction name=\"home\" xslt=\"pages/home-tutorial.xsl\"/>" };
	GSBasicTestingUtil.replaceLinesInFile(xmlFile, before, after, ENCODING);
	*/	

	tutorialEdits.getEdit(editcount++).execute();	
	//GSI_enterRestartLibrary(null);
	serverRestartAndPreview(LIBRARY_HOME_PAGE); //serverRestart();	
	BrowserTest.customisingHomePage(1);//BrowserTest.customisingHomePage(1,LIBRARY_HOME_PAGE);
	
	GSBasicTestingUtil.copyToDirectory(sampleFilesPath+"/custom/tutorialbliss",
			System.getenv("GSDL3HOME")+"/interfaces/default/style/themes");
	GSBasicTestingUtil.copyToDirectory(sampleFilesPath+"/custom/home-tutorial.xsl",
			System.getenv("GSDL3HOME")+"/interfaces/default/transform/pages");
	
	refreshServerAndPreview(LIBRARY_HOME_PAGE); //serverRestart();	
	BrowserTest.customisingHomePage(2);//BrowserTest.customisingHomePage(2,LIBRARY_HOME_PAGE);
	
	
	tutorialEdits.getEdit(editcount++).execute(); // insert collectionsList template
	tutorialEdits.getEdit(editcount++).execute(); // call collectionsList template
	refreshServerAndPreview(LIBRARY_HOME_PAGE);
	BrowserTest.customisingHomePage(3);
	
	
	tutorialEdits.getEdit(editcount++).execute(); // insert searchBox template
	tutorialEdits.getEdit(editcount++).execute(); // call searchBox template
	refreshServerAndPreview(LIBRARY_HOME_PAGE);	
	BrowserTest.customisingHomePage(4);
	
	
	tutorialEdits.getEdit(editcount++).execute(); // insert searchBox template
	tutorialEdits.getEdit(editcount++).execute(); // call searchBox template
	refreshServerAndPreview(LIBRARY_HOME_PAGE);
	BrowserTest.customisingHomePage(5);
	
	
	tutorialEdits.getEdit(editcount++).execute(); // Replace all occurrences of homepage title
	refreshServerAndPreview(LIBRARY_HOME_PAGE);	
	BrowserTest.customisingHomePage(6);
	
	tutorialEdits.getEdit(editcount++).execute(); // Update siteConfig.xml with new title
	serverRestartAndPreview(LIBRARY_HOME_PAGE); // needs server restart
	//PAUSE(2); // give it some time more before checking page title? moved to server restart
	BrowserTest.customisingHomePage(7);
	
	normaliseGLI();//makeGLIWindowActive();
    }

    /*
     * To undo this tutorial:
     * 1. rm -rf web/sites/mysite/
     * 2. Remove the final 2 library definitions and servlet mappings from servlets.xml.in
     * (for libraries 'library2' and 'rand' added by this tutorial)
     * 3. Can rm -rf web/interfaces/althor
     * 4. Remember to restart Greenstone after above changes to undo this tutorial:
     * gs3-svn> ./ant-logreset-restart-with-exts.sh
     * 
     * This function already puts the servlet settings in GSI and GLI > Connection tab back at end.
     */
    public void definingLibraries() {	
	minimiseGLI();
	
	// make backup of servlets.xml.in iff not already present,
	// to be able to undo this tutorial later
	try {
	    GSBasicTestingUtil.copyFile(System.getenv("GSDL3SRCHOME")+"/resources/web",
					"servlets", ".xml.in", ".bak1", false);
	} catch(Exception e) {
	    e.printStackTrace();
	    System.err.println("### definingLibraries(): Error when backing up servlets.xml.in.");
	}
	String sampleFilesPath = (String)props.get("samplefiles.treepath"); // fwd-slash version
	String gsBaseURL = (String)props.get("gs.base.url");
	//final String LIBRARY_HOME_PAGE = gsBaseURL + "library";
	
	final String LIBRARY2_HOME_PAGE = gsBaseURL + "library2";
	final String HALFTONE_LIBRARY = gsBaseURL + "halftone-library";
	final String RAND_LIBRARY = gsBaseURL + "rand";

	String tutorial = "definingLibraries";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;

	// Check halftone-library exists
	refreshServerAndPreview(HALFTONE_LIBRARY);
	BrowserTest.definingLibraries(1);

	// Check (MATCH text) that gs3/resources/web/servlets.xml.in contains expected lines
	// specifying Interface and Site for Libraries 'library' and 'halftone-library'
	// and the servlet-mapping for 'library'
	tutorialEdits.getEdit(editcount++).execute(); // default library "library" definition
	tutorialEdits.getEdit(editcount++).execute(); // part 2 of above
	tutorialEdits.getEdit(editcount++).execute(); // halftone-library definition
	tutorialEdits.getEdit(editcount++).execute(); // servlet-mapping for library
	
	//"FileUtils.copyToDirectory: This method copies the contents
	//of the specified source file to a file of the same name in
	//the specified destination directory. The destination
	//directory is created if it does not exist. If the
	//destination file exists, then this method will overwrite it."
	// This is great, as mysite/collect needs to be created and siteConfig copied into it
	
	//File srcFile = new File(System.getenv("GSDL3HOME") + "/sites/localsite/collect/siteConfig.xml");
	//File destDir = new File(System.getenv("GSDL3HOME") + "/sites/mysite/collect");
	//FileUtils.copyFileToDirectory(srcFile, destDir);
	
	GSBasicTestingUtil.copyToDirectory(
		   System.getenv("GSDL3HOME") + "/sites/localsite/siteConfig.xml",
		   System.getenv("GSDL3HOME") + "/sites/mysite"); // will be created
	GSBasicTestingUtil.copyToDirectory(
		   System.getenv("GSDL3HOME") + "/sites/localsite/collect/lucene-jdbm-demo",
		   System.getenv("GSDL3HOME") + "/sites/mysite/collect"); // will be created
	// Edit servlets.xml.in to define library2 to use mysite and default interface:
	tutorialEdits.getEdit(editcount++).execute();
	serverRestartAndPreview(LIBRARY2_HOME_PAGE);
	//refreshServerAndPreview(LIBRARY2_HOME_PAGE);
	BrowserTest.definingLibraries(2);
	
	// copy sampleFiles' "althor" interface into interfaces folder
	GSBasicTestingUtil.copyToDirectory(sampleFilesPath+"/libraries/althor",
					   System.getenv("GSDL3HOME") + "/interfaces");
	// Edit servlets.xml.in to define library rand to use mysite and althor interface:
	tutorialEdits.getEdit(editcount++).execute();
	serverRestartAndPreview(RAND_LIBRARY);
	BrowserTest.definingLibraries(3);

	// on Mac, must shrinkBrowser() to make GSI servlet app active to access its Settings, then restoreBrowser()
	GSSeleniumUtil.shrinkBrowser();
	FrameFixture gsi_win = getGSI();
	//GSI_settings();
	//makeGSIWindowActive();
	GSI_setServlet("rand");
	//GSSeleniumUtil.restoreBrowser(); // can't press GSI's enter library when browser restored
	                                  // so moved restoreBrowser() into GSI_enterRestartLibrary
	GSI_enterRestartLibrary(new Runnable() {
	 	public void run() {
	 	    BrowserTest.definingLibraries(4);
		}
	    });
	PAUSE(2);
	minimiseGSI(); // allow the browser to get focus when doing further browser testing
	//exitGSI(); // don't exit yet, as we need to undo the servlet change at the end.
	/*
	if(Platform.osFamily() == OSFamily.MAC) {
	    normaliseGLI();
	} else {
	    makeGLIWindowActive();
	}*/
	normaliseGLI();
	window.moveToFront();
	//makeGLIWindowActive();
	switchToPane(GATHER_PANE); // Ensure GLI is in focus before setting GLI prefs
	                  // needed on Mac to return focus from browser to GLI
	
	Collection connOptions = new ArrayList(2);
	// 1st set site as it restricts servlets, so set servlets 2nd
	String siteName = "mysite";
	//connOptions.add(new SimpleEntry("PAUSE", "5")); //allow time to load
	connOptions.add(new SimpleEntry(Dictionary.get("Preferences.Connection.Site"), siteName));
	//connOptions.add(new SimpleEntry("PAUSE", "5")); //allow time to load servlets for this site
	connOptions.add(new SimpleEntry(Dictionary.get("Preferences.Connection.Servlet"), "rand"));	

	setPrefs(null, // mode (librarian/expert/library assistant)
		 null, // general tab
		 System.getenv("GSDL3HOME")+"/sites/"+siteName+"/collect",
		 connOptions, // connection tab		 
		 null); // switchOffWarnings tab
	connOptions = null;
	
	// Try it out by previewing the solitary collection (lucene) in rand library
	// that uses "mysite" site
	loadCollection("lucene-jdbm-demo");
	switchToPane(FORMAT_PANE);
	quickFormatPreviewCycle(new Runnable() {
	 	public void run() {		    
	 	    BrowserTest.definingLibraries(5);
		}
	    });


	// CLEAN UP: now put GSI and GLI back to default servlet
	
	//makeGLIWindowActive();
	siteName = "localsite";
	connOptions = new ArrayList(2);
	// set site then servlets
	connOptions.add(new SimpleEntry(Dictionary.get("Preferences.Connection.Site"), siteName));
	//connOptions.add(new SimpleEntry("PAUSE", "1.5")); //allow time to load servlets for this site
	connOptions.add(new SimpleEntry(Dictionary.get("Preferences.Connection.Servlet"), "library"));	

	setPrefs(null, // mode (librarian/expert/library assistant)
		 null, // general tab
		 System.getenv("GSDL3HOME")+"/sites/"+siteName+"/collect",
		 connOptions, // connection tab		 
		 null); // switchOffWarnings tab
	GSSeleniumUtil.shrinkBrowser();
	gsi_win = getGSI();
	GSI_setServlet("library");
	GSI_enterRestartLibrary(null);
	PAUSE(2);	
	exitGSI();
	GSSeleniumUtil.restoreBrowser();
	makeGLIWindowActive();
    }
    
    /*
     * To undo the designingANewInterface<1-3> set of tutorials:
     * 1. Remove the library definition for 'golden' and its servlet mapping from servlets.xml.in
     * 2. Can rm -rf web/interfaces/perrin
     * 3. Remember to restart Greenstone after above changes to undo this tutorial:
     * gs3-svn> ./ant-logreset-restart-with-exts.sh
     */
    public void designingANewInterface1() {
	// TUTORIAL: Designing A New Interface 1
	
	minimiseGLI();
	GSSeleniumUtil.shrinkBrowser();
	
	// GS3/resources/web/servlets.xml.in is already backed up in servlets.bak1 at same level
	// So in theory we don't need to prepare for that undo.
	// However, passing false here means it won't do the copy (won't overwrite) if target file
	// already exists. So it doesn't hurt to attempt backup here.
	// Further, if we just run this customisationt tutorial, we need to backup
	// servlets.xml.in before we modify it.
	try {
	    GSBasicTestingUtil.copyFile(System.getenv("GSDL3SRCHOME")+"/resources/web",
					"servlets", ".xml.in", ".bak1", false);
	} catch(Exception e) {
	    e.printStackTrace();
	    System.err.println("### designingANewInterface(): Error backing up servlets.xml.in.");
	}
	
	String sampleFilesPath = (String)props.get("samplefiles.treepath"); // fwd-slash version
	String gsBaseURL = (String)props.get("gs.base.url");
	final String GOLDEN_LIBRARY = gsBaseURL + "golden";
	
	String tutorial = "designingANewInterface1";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;

	String perrinPath = System.getenv("GSDL3HOME") + "/interfaces/perrin";

	// Copy custom interfaceConfig.xml into perrin then make it inherit from default interface
	GSBasicTestingUtil.copyToDirectory(
			   sampleFilesPath+"/interfaces/aybara/interfaceConfig.xml",
			   perrinPath); // creates perrin and copies the file into it
	tutorialEdits.getEdit(editcount++).execute(); // inherit from default interface

	// unzip samplefiles/interfaces/news-magazine.zip into samplefiles/interfaces
	String newsMagazinePath = sampleFilesPath+"/interfaces/news-magazine";
	File newsMagazineDir = new File(newsMagazinePath);
	if(!newsMagazineDir.exists()) {
	    try {
		GSBasicTestingUtil.unzip(newsMagazinePath+".zip", sampleFilesPath+"/interfaces/");
	    } catch(Exception e) {
		e.printStackTrace();
		System.err.println("### designingANewInterface(): Unable to extract "
				   + newsMagazineDir);
	    }
	}
	
	// copy file and 3 folders in samplefiles/interfaces/news-magazine(/layout) into perrin
	// The news-magazine folder is the readymade interface that Jenny's tutorial works with	
	String[] locations = {"layout/styles","layout/scripts",//"layout/images",
		      //"license.txt"
		      "licence.txt"}; // renamed in interfaces/news-magazine vs online version
	for(String path : locations) {
	    GSBasicTestingUtil.copyToDirectory(sampleFilesPath+"/interfaces/news-magazine/"+path,
					       perrinPath);
	}
	String[] jsFiles = {"jquery.easing.1.3.js", //"jquery.galleryview.2.1.1.min.js",
			    "jquery.galleryview.setup.js", "jquery.timers.1.2.js"};
	// Move perrin/scripts/galleryviewthemes/<javascriptFiles.js> into perrin/scripts/.
	for(String jsFile : jsFiles) {
	    File f = new File(perrinPath+"/scripts/galleryviewthemes/"+jsFile);
	    try {
		// param false: no need to create dirs (shouldn't have to create "scripts" dir)
		FileUtils.moveFileToDirectory(f, new File(perrinPath, "scripts"), false);
	    } catch(IOException ioe) {
		ioe.printStackTrace();
		System.err.println("### Exception moving file "+f+" to interfaces/perrin/scripts");
	    }
	}

	
	// Move perrin/scripts/galleryviewthemes/themes into perrin/images then rename
	// the copied /perrin/images/themes folder into 'galleryviewthemes'
	//FileUtils.moveDirectoryToDirectory(perrinPath + "/scripts/galleryviewthemes/themes",
	//				   perrinPath + "/images/galleryviewthemes");
	// Can delete scripts/galleryviewthemes folder
	//GSBasicTestingUtil.delete(perrinPath + "/scripts/galleryviewthemes");

	// Next step not needed: we'll copy samplefiles/interfaces/aybara/images into perrin
	// which contains the files we need and nothing we don't
	// delete all the images in perrin/images folder (not the html file or subfolder)
	//String[] gifFiles = {"facebook.gif", "rss.gif", "twitter.gif"};
	//for(String gifFile : gifFiles) {
	//    GSBasicTestingUtil.delete(perrinPath + "/images/" + gifFile);
	//}

	
	// remove line from perrin/styles/layout.css
	tutorialEdits.getEdit(editcount++).execute();

	// copy file and 3 folders from sample-files/aybara into perrin
	String[] files = {//"styles",
	    "images", "transform", "index-GS3.html" };
	for(String path : files) {
	    GSBasicTestingUtil.copyToDirectory(sampleFilesPath+"/interfaces/aybara/"+path,
					       perrinPath);
	}

	
	// Do these steps here to avoid the windows popup asking if merging folders is acceptable
	GSBasicTestingUtil.copyToDirectory(
			   sampleFilesPath+"/interfaces/aybara/styles/gs3-core-min.css",
			   perrinPath+"/styles");
	// Next steps not needed: as samplefiles/interfaces/aybara/images
	// now contains galleryviewthemes, see commit message for
	// https://trac.greenstone.org/log/documentation/trunk/tutorial_sample_files/interfaces/aybara/images/galleryviewthemes?rev=39523&verbose=on#
	/*
	// Move perrin/scripts/galleryviewthemes/themes into perrin/images then rename
	// the copied 'themes' folder into 'galleryviewthemes'
	// ALT: need to use moveToDirectory() followed by rename
	try {
	    FileUtils.moveDirectory(new File(perrinPath+"/scripts/galleryviewthemes/themes"),
				    new File(perrinPath + "/images/galleryviewthemes"));
	} catch(IOException ioe) {
	    System.err.println("### Exception moving dir perrin/scripts/galleryviewthemes/themes"+
			       " as perrin/images/galleryviewthemes");
	}
	// Can delete scripts/galleryviewthemes folder
	GSBasicTestingUtil.delete(perrinPath + "/scripts/galleryviewthemes");
	*/

	// remove line in perrin/styles/featured_slide.css
	tutorialEdits.getEdit(editcount++).execute();
	
	// Avoid Windows overwrite popup by not copying old jquery.galleryview.2.1.1.min.js above
	// We copy just the new version, and we do it now:
	String[] newJSFiles = {"jquery.galleryview.2.1.1.min.js", "jquery.galleryview-2.1.1.js"};
	for(String jsFile : newJSFiles) {
	    GSBasicTestingUtil.copyToDirectory(sampleFilesPath+"/interfaces/"+jsFile,
					       perrinPath + "/scripts");
	}

	// Preview perrinPath/index-GS3.html. Note that it's a local file, not on GS3 server
	// https://stackoverflow.com/questions/17972885/how-to-use-selenium-webdriver-on-local-webpage-on-my-pc-instead-of-one-located

	GSSeleniumUtil.restoreBrowser(); // Important to bring selenium-driven browser back
	File localPage = new File(perrinPath, "index-GS3.html");
	refreshServerAndPreview("file://"+localPage.getPath()); //_driver.get(localPage.getPath());
	PAUSE(3);
	// TODO: Any tests for previewing this static index file?

	makeGLIWindowActive();
    }

    public void designingANewInterface2() {
	minimiseGLI();
	//GSSeleniumUtil.shrinkBrowser();
	
	String perrinPath = System.getenv("GSDL3HOME") + "/interfaces/perrin";
	String sampleFilesPath = (String)props.get("samplefiles.treepath"); // fwd-slash version
	String gsBaseURL = (String)props.get("gs.base.url");
	final String GOLDEN_LIBRARY = gsBaseURL + "golden";
	
	String tutorial = "designingANewInterface2";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	// TUTORIAL: Designing A New Interface 2	
	serverRestartAndPreview(GOLDEN_LIBRARY);
	BrowserTest.designingANewInterface2(1);

	// HOME.XSL
	GSBasicTestingUtil.copyToDirectory(
			   sampleFilesPath+"/interfaces/aybara/home.xsl",
			   perrinPath+"/transform/pages");	
	refreshServerAndPreview(GOLDEN_LIBRARY);
	BrowserTest.designingANewInterface2(2);
	
	// Make an edit that deliberately causes an error (referring to non-existent template)
	tutorialEdits.getEdit(editcount++).execute();	
	refreshServerAndPreview(GOLDEN_LIBRARY);
	BrowserTest.designingANewInterface2(3);	

	// Add in missing collSlider template - error should be gone
	tutorialEdits.getEdit(editcount++).execute();
	refreshServerAndPreview(GOLDEN_LIBRARY);	
	BrowserTest.designingANewInterface2(4);
	

	// template to turn list items into image slider
	tutorialEdits.getEdit(editcount++).execute();	
	refreshServerAndPreview(GOLDEN_LIBRARY);	
	BrowserTest.designingANewInterface2(5);
	
	// placeholder columns with content
	tutorialEdits.getEdit(editcount++).execute();	
	refreshServerAndPreview(GOLDEN_LIBRARY);	
	BrowserTest.designingANewInterface2(6);
	

	// Define then call the collectionsList template
	tutorialEdits.getEdit(editcount++).execute();
	tutorialEdits.getEdit(editcount++).execute();
	refreshServerAndPreview(GOLDEN_LIBRARY);	
	BrowserTest.designingANewInterface2(7);
	

	// HEADER.XSL
	GSBasicTestingUtil.copyToDirectory(
			   sampleFilesPath+"/interfaces/aybara/header.xsl",
			   perrinPath+"/transform/layouts");
	refreshServerAndPreview(GOLDEN_LIBRARY); // nothing should have changed
	BrowserTest.designingANewInterface2(8);

	// Look and feel should change drastically
	tutorialEdits.getEdit(editcount++).execute();
	refreshServerAndPreview(GOLDEN_LIBRARY);
	BrowserTest.designingANewInterface2(9);

	makeGLIWindowActive();
    }
    
    public void designingANewInterface3() {
	minimiseGLI();
	//GSSeleniumUtil.shrinkBrowser();
	
	String perrinPath = System.getenv("GSDL3HOME") + "/interfaces/perrin";
	String sampleFilesPath = (String)props.get("samplefiles.treepath"); // fwd-slash version
	String gsBaseURL = (String)props.get("gs.base.url");
	final String GOLDEN_LIBRARY = gsBaseURL + "golden";
	
	String tutorial = "designingANewInterface3";
	TutorialEdits tutorialEdits = tutorials.get(tutorial);
	int editcount = 0;
	
	// MAIN.XSL
	GSBasicTestingUtil.copyToDirectory(
			   sampleFilesPath+"/interfaces/aybara/main.xsl",
			   perrinPath+"/transform/layouts");
	refreshServerAndPreview(GOLDEN_LIBRARY);
	BrowserTest.designingANewInterface3(1);	
	
	// tut edit num 8: adding the footer
	tutorialEdits.getEdit(editcount++).execute();
	refreshServerAndPreview(GOLDEN_LIBRARY);
	BrowserTest.designingANewInterface3(2);
	
	
	// Move image slider and highlighted items from edge
	tutorialEdits.getEdit(editcount++).execute();
	refreshServerAndPreview(GOLDEN_LIBRARY);
	BrowserTest.designingANewInterface3(3); // just adding in main.xsl already fixed this?
	
	
	// Navigation bar and search box steps combined - defining and calling templates
	tutorialEdits.getEdit(editcount++).execute();
	tutorialEdits.getEdit(editcount++).execute();
	refreshServerAndPreview(GOLDEN_LIBRARY);
	BrowserTest.designingANewInterface3(4);
	

	// add header to display library title. And coll title on coll page
	tutorialEdits.getEdit(editcount++).execute();
	refreshServerAndPreview(GOLDEN_LIBRARY);
	BrowserTest.designingANewInterface3(5);
	

	// Add top bar with links for logging in, help and preferences pages
	tutorialEdits.getEdit(editcount++).execute(); // call loginLinks template	
	tutorialEdits.getEdit(editcount++).execute(); // define loginLinks template	
	refreshServerAndPreview(GOLDEN_LIBRARY);
	BrowserTest.designingANewInterface3(6);
	
	makeGLIWindowActive();
    }

    // undoCustomisingThemes() can now be run even if customisingThemes() was never run before
    public boolean undoCustomisingThemes() {
	System.err.println("@@@ UNDOing any Themes tutorials changes...");
	boolean success = true;
	
	success = success && GSBasicTestingUtil.delete(System.getenv("GSDL3HOME")
			       +"/interfaces/default/style/themes/TutorialTheme");
	success = success && GSBasicTestingUtil.delete(System.getenv("GSDL3HOME")
			       +"/interfaces/default/style/images/TutorialTheme.png");
	success = success && GSBasicTestingUtil.delete(System.getenv("GSDL3HOME")
			       +"/interfaces/default/style/images/TutorialTheme.png");

	try {
	    File storedPrefXSL = new File(System.getenv("GSDL3SRCHOME")
					  +"/ext/testing/src/tmp/pref.xsl");
	    if(storedPrefXSL.exists()) {
		File destFile = new File(System.getenv("GSDL3HOME")
				    +"/interfaces/default/transform/pages/pref.xsl");
		FileUtils.copyFile(storedPrefXSL, destFile); // can create dir, will overwrite file
		
		// delete from testing/tmp
		GSBasicTestingUtil.delete(storedPrefXSL.getAbsolutePath());
	    }
	} catch(Exception e){
	    Assert.fail("### ERROR: undoCustomisingThemes() - error restoring pref.xsl"
			+ "\n\tfrom ext/testing/src/tmp to default/transform/pages");
	}
	refreshServerAndPreview();
	
	if(BrowserTest.resetAdminPwd()) {
	    BrowserTest.switchThemeUnderPrefs("Greenstone Default");
	    BrowserTest.logout();
	}
	return success;
    }

    public void undoCollectionTheme() {
	try {
	    // delete the installed CollecitonTheme folder from backdrop coll if it exists
	    File collectionTheme = new File(System.getenv("GSDL3HOME")
			    + "/sites/localsite/collect/backdrop/style/CollectionTheme");
	    if(collectionTheme.exists()) {
		GSBasicTestingUtil.delete(collectionTheme.getAbsolutePath());
	    }

	    // restore backed up backdrop collectionConfig file
	    File backdropCollConfig = new File(System.getenv("GSDL3SRCHOME")
					     +"/ext/testing/src/tmp/backdropCollCfg.xml");
	    if(backdropCollConfig.exists()) {
		File destFile = new File(System.getenv("GSDL3HOME")
			 +"/sites/localsite/collect/backdrop/etc/collectionConfig.xml");
		FileUtils.copyFile(backdropCollConfig, destFile); // can create dir, overwrites file
		// delete from testing/tmp
		GSBasicTestingUtil.delete(backdropCollConfig.getAbsolutePath());	    
	    }
	} catch(Exception e){
	    Assert.fail("### ERROR: undoCollectionTheme() - error restoring backdropCollCfg.xml "
			+ " from ext/testing/src/tmp");
	}
    }
    
    // Calling this (should have) no effect if customisation tutorials were never run
    // Ensure this remains the case if modifiying this function and those it calls.
    public boolean undoCustomisationTutorials() {
	System.err.println("@@@ UNDOing any customisation tutorials changes in reverse order...");
	
	if(Platform.isWindows()) {
	    commandLineServerStop(); // stop server to avoid file locking problems
	}

	// undo in reverse tutorial order
	boolean success
	    = undoDesigningANewInterface();
	success = success   
	    && undoDefiningLibraries();
	success = success
	    && undoCustomisingHomePage();
	
	if(Platform.isWindows()) {
	    commandLineServerStart();
	} else {
	    commandLineServerRestart();
	}
	return success;
    }

    // undoing any customisation tutorial should have no effect if its customisation tutorial
    // was never run
    public boolean undoCustomisingHomePage() {
	System.err.println("@@@ UNDOing any changes for customisingHomePage tutorial test");
	boolean success = true;

	success = success
	    && GSBasicTestingUtil.moveFile(System.getenv("GSDL3HOME")+"/interfaces/default",
					   "interfaceConfig", ".bak1", ".xml");
	success = success
	    && GSBasicTestingUtil.moveFile(System.getenv("GSDL3HOME")+"/sites/localsite",
					   "siteConfig", ".bak1", ".xml");
	success = success
	    && GSBasicTestingUtil.delete(System.getenv("GSDL3HOME")
					 +"/interfaces/default/style/themes/tutorialbliss");
	success = success
	    && GSBasicTestingUtil.delete(System.getenv("GSDL3HOME")
			 +"/interfaces/default/transform/pages/home-tutorial.xsl");
	if(!success) {

	    System.err.println("### One or more undo operations in undoCustomisingHomePage() failed.");
	}
	return success;
    }
    public boolean undoDefiningLibraries() {
	System.err.println("@@@ UNDOing any changes for the definingLibraries tutorial test");
	boolean success = true;
	
	success = success
	    && GSBasicTestingUtil.moveFile(
					   System.getenv("GSDL3SRCHOME")+"/resources/web",
					 "servlets", ".bak1", ".xml.in");
	success = success
	    && GSBasicTestingUtil.delete(System.getenv("GSDL3HOME") + "/sites/mysite");
	success = success
	    && GSBasicTestingUtil.delete(System.getenv("GSDL3HOME") + "/interfaces/althor");
	
	if(!success) {
	    System.err.println("### One or more undo operations in undoDefiningLibraries() failed.");
	}

	return success;
    }

    public boolean undoDesigningANewInterface() {	
	boolean success = GSBasicTestingUtil.delete(System.getenv("GSDL3HOME") + "/interfaces/perrin");
	// servlets.bak1 will be put back as servlets.xml.in by undoDefiningLibraries
	// It doesn't hurt doing it here, however: as it will only happen if bak1 exists.
	success = success
	    && GSBasicTestingUtil.moveFile(
					   System.getenv("GSDL3SRCHOME")+"/resources/web",
					 "servlets", ".bak1", ".xml.in");
	return success;
    }
    /******************************* UTILITY FUNCTIONS ********************************/

    // backgroundRunCommand is when you don't need to view the contents of the terminal
    // and don't mind that the selenium-driven browser and/or GLI are obscuring the terminal
    public static int backgroundRunCommand(String[] cmd_args) {
	int exitValue = runCommand_withoutExitValCheck(cmd_args);
	if(exitValue != 0) {
	    System.err.println("*** Script " + cmd_args[0] + " exitted abnormally with exit value " + exitValue);
	}
	return exitValue;
    }
    public static int backgroundRunCommand(String[] cmd_args, String[] envp, String launchDir) {
	int exitValue = runCommand_withoutExitValCheck(cmd_args, envp, launchDir);
	if(exitValue != 0) {
	    System.err.println("*** Script " + cmd_args[0] + " exitted abnormally with exit value " + exitValue);
	}
	return exitValue;
    }
    // Same as backgroundRunCommand, but minimises GLI and the selenium driven browser before
    // running the command and afterwards restores both the testing browser and GLI
    // This allows us to view what's going on in the terminal if we're present
    public static int runCommand(String[] cmd_args, String[] envp, String launchDir) {
	PAUSE(1); // some spacing, to eyeball that things are fine in the currently open window

	minimiseGLI(); // minimise GLI

	// minimise the selenium-managed browser to see terminal
	// https://stackoverflow.com/questions/42647058/how-to-minimize-the-browser-window-in-selenium-webdriver-3
	//_driver.manage().window().minimize(); // only in JavaScript
	GSSeleniumUtil.shrinkBrowser();
	
	int exitValue = -1;
	if(launchDir == null) {
	    exitValue = backgroundRunCommand(cmd_args);
	} else {
	    exitValue = backgroundRunCommand(cmd_args, envp, launchDir);
	}

	// restore the selenium-managed browser and GLI to its former size
	GSSeleniumUtil.restoreBrowser(); //_driver.manage().window().maximize();
	//PAUSE(1);
	makeGLIWindowActive(); // normaliseGLI(); // also works. Want GLI back to normal, non-minimised state
	
	return exitValue;
    }
    // the most frequently used version
    public static int runCommand(String[] cmd_args) {
	return runCommand(cmd_args, null, null);
    }

    // Same as runCommand, but won't print out an error message if the exitvalue is other than 0
    // Call this when you can have custom exit values and will print error messages on
    // undesirable exit values yourself
    public static int runCommand_withoutExitValCheck(String[] cmd_args) {
	return runCommand_withoutExitValCheck(cmd_args, null, null);
    }
    public static int runCommand_withoutExitValCheck(String[] cmd_args, String[] envp, String launchDir) {
	System.err.println("@@@ Running command: " + Arrays.toString(cmd_args));
	SafeProcess process;
	if(launchDir != null) {
	    if(envp != null) {
		System.err.println("\t@@@ with env params: " + Arrays.toString(envp));	    
	    }
	    System.err.println("\t@@@ in launchDir: " + launchDir);
	    process = new SafeProcess(cmd_args, null, new File(launchDir));
	} else {
	    process = new SafeProcess(cmd_args);
	}
	SafeProcess.LineByLineHandler processOutLineHandler
	    = new ScriptOutputHandler(SafeProcess.STDOUT);
	SafeProcess.LineByLineHandler processErrLineHandler
	    = new ScriptOutputHandler(SafeProcess.STDERR);	
	process.setExceptionHandler(new SafeProcess.ExceptionHandler() {
		public void gotException(Exception e) {
		    String msg = "Got exception running script.";
		    System.err.println(msg);
		    e.printStackTrace();
		}
	    }); 
	int exitValue = process.runProcess(processOutLineHandler, processErrLineHandler); // use default procIn handling
		
	process = null;

	return exitValue;
    }
    
    // @return can be null if no proxy params set up in the GLItest.properties file
    public Map getProxyOpts() {
	Map proxyOpts = null;	
	if(props.get("http.proxyhost") != null || props.get("https.proxyhost") != null) {
	    proxyOpts = new HashMap<String,String>(6);

	    if(props.get("http.proxyhost") != null) {
		proxyOpts.put("http_host",(String)props.get("http.proxyhost"));
		proxyOpts.put("http_port",(String)props.get("http.proxyport"));
	    }
	    if(props.get("https.proxyhost") != null) {
		proxyOpts.put("https_host",(String)props.get("https.proxyhost"));
		proxyOpts.put("https_port",(String)props.get("https.proxyport"));
	    }
	    proxyOpts.put("un", (String)props.get("proxy.un"));
	    proxyOpts.put("pw", (String)props.get("proxy.pw"));
	}
	return proxyOpts;
    }
    
    public static class ScriptOutputHandler extends SafeProcess.LineByLineHandler {
	public ScriptOutputHandler(int src) {
	    super(src); // will set this.source to STDERR or STDOUT
	}
	
	public void gotLine(String line) {
	    System.err.println(SafeProcess.streamToString(this.source) + "> " + line);
	}
	public void gotException(Exception e) {
	    String msg = "Got exception processing script's " + SafeProcess.streamToString(this.source) + " stream.";
	    System.err.println(msg);
	    e.printStackTrace();
	}
    }

    public static String getGSBaseURL() {
	String gsBaseURL = (String)props.get("gs.base.url");
	return gsBaseURL;
    }
    
    public static void loadProperties() {
	//System.err.println("@@@ Classpath: " + System.getProperty("java.class.path"));
	
	// try with resources >= Java 7	
	// "A try-with-resources statement can have catch and finally
	// blocks just like an ordinary try statement. In a
	// try-with-resources statement, any catch or finally block is
	// run after the resources declared have been closed."
	// https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
	// https://stackoverflow.com/questions/8285595/reading-properties-file-in-java
	try (InputStream istream = Thread.currentThread().getContextClassLoader().getResourceAsStream("GLItest.properties")) {
	    //FileInputStream fstream = new FileInputStream("GLItest.properties")) {
	     props.load(istream);
	     System.err.println("@@@ sampleFilesPath in GLI.properties: " + props.getOrDefault("samplefiles.path", ""));
	} catch(Exception e) {
	    e.printStackTrace();
	    System.err.println("Could not find GLItest.properties in classpath (in testing/lib) with samplefiles.path set");
	    System.exit(-1);
	}

	File location = new File((String)props.getOrDefault("samplefiles.path", ""));
	String toplevelGS = System.getenv("GSDL3SRCHOME");
	if(toplevelGS == null) { // assume GS2?
	    toplevelGS = System.getenv("GSDLHOME");
	}
	if(!location.exists() || !location.isDirectory()) {
	    System.err.println("*** No sample files directory at " + location.getAbsolutePath()
			       + ".\nTrying fallback inside GS installation.");
	    location = new File(toplevelGS, "sample_files");
	    if(!location.exists() || !location.isDirectory()) {
		System.err.println("*** No sample files folder in GS install either at "
				   + location.getAbsolutePath() + ".\nWill svn check out.");
		int result = svnCheckout(
			 "https://svn.greenstone.org/documentation/trunk/tutorial_sample_files",
			 "sample_files",
			 toplevelGS);
		
		if(result != 0) {
		    System.err.println("### Failed to svn checkout sample_files. Terminating.");
		    System.exit(-1);
		} else {
		    // samplefiles.path can use fwd or backward slashes
		    // (backslashes to be doubled if manually typed out in file)
		    props.setProperty("samplefiles.path", location.getAbsolutePath());
		}
	    }
	}
	String sampleFilesPath = location.getAbsolutePath();
	System.err.println("@@@ Using sampleFilesPath: " + sampleFilesPath);

	String sampleFilesTreePath = sampleFilesPath;
	// Ensure trailing OS-specific slash
	//if(!sampleFilesTreePath.endsWith(File.separator)) {
	//    sampleFilesTreePath = sampleFilesTreePath + File.separator;
	//    props.setProperty("samplefiles.path", sampleFilesTreePath);
	//}

	// JTreeFixtures denote paths with /
	//sampleFilesTreePath = sampleFilesTreePath.replace('\\', '/');
	sampleFilesTreePath = sampleFilesTreePath.replace(File.separator, "/"); // On windows, change single backslash ('\\') to '/'
	props.setProperty("samplefiles.treepath", sampleFilesTreePath);
	GSGUITestingUtil.setSampleFilesFolder(sampleFilesTreePath);
	
	String username = (String)props.get("system.username");
	GSGUITestingUtil.setUser(username);
	GSGUITestingUtil.storePathEquivsForTopGLIFolders();
	
	// Look for any custom pwd for admin
	// if gs3.admin.pwd not set in GLItest.properties, initAdminPwd() will deal with any null value
	BrowserTest.initAdminPwd(props.getProperty("gs3.admin.pwd"));
	
	String scriptsFolder
	    = System.getenv("GSDL3SRCHOME") + "/ext/testing/src/scripts_and_patches/";
	props.setProperty("scriptsAndPatches.folder", scriptsFolder);
	
	String tutEditsFile = (String)props.get("tutorialedits.yamlfile");
	File f = new File(tutEditsFile);
	System.err.println("@@@ Attempting to load YAML file: " + f.getAbsolutePath());
	readYAML(tutEditsFile);
    }

    public static int svnCheckout(String svnURL, String asFolder, String inDir)
    {
	//File destDir = new File(inDir);
	//if(!destDir.exists()) {
	//    destDir.mkdirs();
	//}
	String[] svnCmd = {"svn", "co", svnURL, asFolder};
	int exitVal = backgroundRunCommand(svnCmd, null, inDir);
	if(exitVal == 0) {
	    File tmpFile = new File(inDir, asFolder);
	    if(!tmpFile.exists()) {
		exitVal = -1;
	    }
	}
	return exitVal;
    }
    
    // We don't care about closing tabs in the Selenium automated browser, as it just
    // reuses the same tab, which is (so far) fine. If needed, see:
    // https://www.lambdatest.com/blog/selenium-close-tab/
    // https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/firefox/FirefoxDriver.html
    // https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/remote/RemoteWebDriver.html
    // Working with regular firefox, closing tabs would need to be from commandline
    // and, if such a command is possible, is OS dependent
    // https://superuser.com/questions/583246/can-i-close-firefox-browser-tab-or-firefox-browser-from-ubuntu-terminal
    // https://askubuntu.com/questions/295584/close-current-tab-firefox-using-terminal
    // https://unix.stackexchange.com/questions/709509/does-firefox-have-a-command-line-flag-to-close-the-application
    
    public static void createBuildPreviewCycle(Runnable browserTestCode) {
	createBuildPreviewCycle(true, browserTestCode);
    }
    public static void createBuildPreviewCycle() {
	createBuildPreviewCycle(true, null);
    }
    // Need a createBuildPreview() quick cycle too
    // These previewCycles, besides (building and) updating browser URL, also iconify GLI at start
    // and restore GLI at end so we can see the browser testing happening, assuming the browser was
    // opened and had been located behind GLI
    public static void createBuildPreviewCycle(boolean preview, Runnable browserTestCode) {
	if(!preview) {
	    return;
	}
	PAUSE(0.5);
	switchToPane(CREATE_PANE);
	//PAUSE(2);	
	//buildCollectionTillReady(MAX_BUILD_TIMEOUT);
	//String previewURL = previewCollection();
	String previewURL = buildAndPreviewCollection(MAX_BUILD_TIMEOUT);
	System.err.println("@@@ Preview URL: " + previewURL);

	// minimise GLI so we can see the browser testing running in the browser
	minimiseGLI(); //does a window.iconify();
	_driver.get(previewURL);

	//PAUSE(1.5);
	if(browserTestCode != null) {
	    System.err.println("*************************************");
	    focusBrowser();
	    browserTestCode.run();
	    System.err.println("*************************************");
	}
	
	PAUSE(1.5);
	// Try to make the GLI main window the active one again, instead of the browser
	// https://stackoverflow.com/questions/4005491/how-to-activate-window-in-java
	normaliseGLI();
	/*
	if(Platform.osFamily() == OSFamily.MAC) {
	    normaliseGLI();
	} else {
	    makeGLIWindowActive();
	}*/
	PAUSE(1);
    }

    public static void quickFormatPreviewCycle() {
	quickFormatPreviewCycle(false, null);
    }
    public static void quickFormatPreviewCycle(Runnable browserTestCode) {
	quickFormatPreviewCycle(false, browserTestCode);
    }
    //    public static void quickFormatPreviewCycle(boolean skip, Runnable browserTestCode) {
    public static void quickFormatPreviewCycle(boolean hack, Runnable browserTestCode) {
	//if(!skip) {
	    PAUSE(1);
	
	    String previewURL = previewCollectionViaFormat();
	    System.err.println("@@@ Quick preview URL: " + previewURL);

	    if(hack != HACK_DONOT_ICONIFY_GLI) { // if(!hack) {
		// minimise GLI so we can see the browser testing running in the browser
		minimiseGLI(); //does a window.iconify();
	    }

	    _driver.get(previewURL);
	    
	    //PAUSE(1.5);
	    if(browserTestCode != null) {
		System.err.println("*************************************");
		focusBrowser();
		browserTestCode.run();
		System.err.println("*************************************");
	    }
	    
	    PAUSE(1.5);
		normaliseGLI();
		/*
	    if(Platform.osFamily() == OSFamily.MAC) {
		normaliseGLI();
	    } else {
		makeGLIWindowActive();
	    }*/
	    PAUSE(1);
	    // Call to makeGLIWindowActive() above already normalises GLI window
	    //window.normalize(); // when we want GLI back to normal, non-minimised state
	    //}
    }

    // Needed for Safari. Funnctionality may be optional (untested) for Firefox and other
    // browsers. For now this function will only have any effect for Safari.
    // It's called before any BrowserTesting code is run ("browserTestCode.run()" in this file)
    // and ensures that the browser, for now Safari only, has focus. Firefox didn't need it
    // but may be fine with it. I'm just 
    public static void focusBrowser() {	
	
	// The following will probably work for other browsers, but as it works without for
	// Firefox (and the rest?), is it faster to skip this step for non-safari browsers?


	// The problem was as expected by ITS Mac expert Mr Richard: he noticed that Safari
	// wasn't getting the focus and this may be why clicking on Selenium Webelements
	// (even though the element exists and is found, being non-null) doesn't work.	
	// The solution is to focus the browser (it's necessary for Safari, and not
	// necessary for Firedfox) and is suggested along with the problem description at
	// https://stackoverflow.com/questions/77388720/automation-testing-with-selenium-click-doesnt-works-on-new-safari-17-ios-sonoma
	// https://www.selenium.dev/documentation/webdriver/interactions/windows/
	if(_driver instanceof SafariDriver) {
	    // This method has the effect of ensuring the focus on the Safari browser
	    // Specifically, it switches the focus to the 1st browser window in case there
	    // are multiple open.
	    //Object[] windowHandles=_driver.getWindowHandles().toArray();
	    //_driver.switchTo().window((String)windowHandles[0]);
	    _driver.switchTo().window(_driver.getWindowHandles().iterator().next());
	}
	// On Firefox, when testing on linux, above switchTo() call produced the error
	// org.openqa.selenium.InvalidArgumentException: Expected "handle" to be a string, got [object Undefined] undefined
	// So just doing the above for Safari. Edge wholly untested. Other combinations of
	// using the switchTo() line with Firefox on Win/Mac untested, but all worked without
	// the line.
    }

    // getGSI will launch GSI if not launched yet and return its FrameFixture
    // getGSI() variants are not static functions but methods, as they need to use robot() member
    public FrameFixture getGSI(boolean enterRestartLibrary, Runnable browserTestCode) {
	FrameFixture gsi_win;
	if(GSGUITestingUtil.gsi_window == null) {
	    runGSI();
	    
	    //GSI_enterRestartLibrary(null);
	}
	gsi_win = GSGUITestingUtil.getGSIApplicationWindow(robot(), 10);

	normaliseGSI(); // needed on mac along with shrunk/minimized browser to bring GSI to fore
	gsi_win.moveToFront();

	// https://coderanch.com/t/584473/java/JDialog-accessing-title-bar
	
	if(enterRestartLibrary) {
	    GSI_enterRestartLibrary(browserTestCode);
	}
	return gsi_win;
    }
    public FrameFixture getGSI() {
	return getGSI(false, null);
    }
    
    public static void GSI_enterRestartLibrary(Runnable browserTestCode) {	
	enterOrRestartLibraryAndWait(MAX_WAIT_SERVER_RESTART_SECONDS);
	//minimiseGSI(); //enterOrRestartLibraryAndWait(MAX_WAIT_SERVER_RESTART_SECONDS);
	String previewURL = getGSILibraryURL();
	// https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/WebDriver.html#get(java.lang.String)
	//WebDriver.get(): "this is done using an HTTP POST operation,
	// and the method will block until the load is complete (with the
	// default 'page load strategy'. ...)"
	// So we needn't wait any further to start browser testing once WebDriver.get() returns
	GSSeleniumUtil.restoreBrowser();
	_driver.get(previewURL);
	GSSeleniumUtil.waitForPageLoad();
	
	// GSI already gets minimised when enter library is pressed
	//PAUSE(2); // give it some more time for library to show up
	if(browserTestCode != null) {
	    focusBrowser();
	    browserTestCode.run();
	}
	
	normaliseGSI();
	PAUSE(1);
    }
    public static int commandLineServerStart() {
	return commandLineServerCmd(true, "start");
    }
    public static int commandLineServerStart(boolean isFailureFatal) {
	return commandLineServerCmd(isFailureFatal, "start");
    }
    public static int commandLineServerStop() {
	return commandLineServerCmd(true, "stop");
    }
    public static int commandLineServerStop(boolean isFailureFatal) {
	return commandLineServerCmd(isFailureFatal, "stop");
    }
    public static int commandLineServerRestart() {
	return commandLineServerCmd(true, "restart");
    }
    public static int commandLineServerRestart(boolean isFailureFatal) {
	return commandLineServerCmd(isFailureFatal, "restart");
    }
    // returns exitvalue of running the server restart/stop/start command
    public static int commandLineServerCmd(boolean failureIsFatal, String cmd) {	
	int exitValue = 0;
	long start_time = System.currentTimeMillis();

	// TODO: now that we have windows batch scripts of the ant-logreset-restart-with-exts
	// and other ant-with-exts scripts, we can probably generalise this to always call the
	// OS specific script ant-logreset-restart-with-exts and not "ant restart" for windows.
	// But first need to test ant-logreset-restart-with-exts works with testing for windows.
	/*
	// On Windows, running the unix bash script ant-logreset-restart-with-exts.sh (which can run on
	// Windows from the command line OK) conflicts with using Java Process: it produces exception
	// "IOException:cannot run program <scriptname> (even when run in the launchdirectory).
	// CreateProcess error=193, %1 is not a valid Win32 application"
	// The same happens when Process is used to run "ant restart" (works on Windows cmdline),
	// but that's because the ant executable is literally a .sh script too when inspected.
	// What works is "ant.bat restart".
	// https://stackoverflow.com/questions/19621838/createprocess-error-2-the-system-cannot-find-the-file-specified
	if(Platform.isWindows()) {
	    exitValue = runCommand(
		   new String[]{ ANT_COMMAND, "restart"}, // works, but doesn't do as much as ant- scripts
		   //new String[]{System.getenv("GSDL3SRCHOME")+"/ant-logreset-restart-with-exts.bat"},//1
		   //new String[]{System.getenv("GSDL3SRCHOME")+"\\ant-logreset-restart-with-exts.bat"},//1
	       
		   //new String[]{System.getenv("GSDL3SRCHOME")+"\\ant-logreset-restart-with-exts.sh"},//2
		   
		   null,
		   System.getenv("GSDL3SRCHOME"));
	    // [1] Both appear to work (variant using / seems to be better) until after server
	    // restart: then browser window can't be brought back automatically. I have to
	    // manually get it back and then things move a little.
	    // [2] Doesn't work: creating SafeProcess causes CreateProcess error=193 described above
	    // Used suggestion at https://stackoverflow.com/questions/25511402/java-io-ioexception-cannot-run-program-createprocess-error-193-1-is-not-a?rq=3
	    // To create a batch file that runs a .sh file: the .bat can be launched with Java Process
	    // Next doesn't work as ant-logreset-restart-with-exts needs to be run from GSDL3SRCHOME
	    //runCommand(new String[] {
	    //    System.getenv("GSDL3SRCHOME")+"\\ant-logreset-restart-with-exts.bat"});
	    
	} else {
	    exitValue = runCommand(new String[] {
		    System.getenv("GSDL3SRCHOME")+"/ant-logreset-restart-with-exts.sh"});
	}
	*/

	// OLD WAY THAT WORKED
	/*
	if(Platform.isWindows()) {
	    exitValue = runCommand( // see commandLineServerRestart(boolean)
				   new String[]{ ANT_COMMAND, cmd },
				   null,
				   System.getenv("GSDL3SRCHOME"));
	} else {
	    String command = cmd;
	    if(cmd.equals("restart")) {
		cmd = "logreset-restart";
	    }
	    exitValue = runCommand(new String[] {
		    System.getenv("GSDL3SRCHOME")+"/ant-"+command+"-with-exts.sh"});
	}
	*/
	
	if(cmd.equals("restart")) {
		cmd = "logreset-restart";
	}
	String command = cmd;
	if(Platform.isWindows()) {
		command = "\\ant-"+command+"-with-exts.bat";
	} else {
		command = "/ant-"+command+"-with-exts.sh";
	}
	/*exitValue = runCommand( // see commandLineServerRestart(boolean)
				   new String[]{ "."+command },
				   null,
				   System.getenv("GSDL3SRCHOME"));
				   */
	exitValue = runCommand(new String[] {
		    System.getenv("GSDL3SRCHOME")+command});
		
	PAUSE(1);
	System.err.println("@@@ Done requesting Greenstone server " + cmd + ".");

	long end_time = System.currentTimeMillis();
	float time_diff_seconds = (end_time - start_time) / 1000;
	System.err.println("@@@ " + cmd + " duration " + time_diff_seconds);
	
	if(exitValue != 0) {
	    String msg = "Server didn't "+cmd+" properly. Exitted with " + exitValue;
	    if(failureIsFatal) {
		Assert.fail("### ASSERT ERROR: " + msg);
	    }
	    // if server not stop/start/restarting isn't fatal, still log the issue to std error
	    System.err.println("### " + msg);
	}	
	return exitValue;
    }

    
    // Set this to the kind of server restart we want to do
    // The GSI server restart working was already tested in the OAIServer tutorial
    // And it's faster to just restart the server from the command line, which
    // suffices for tutorials that are testing things other than GSI.
    public static int serverRestart() {
	minimiseGLI();
	int result = 0;
	/*
	if(USE_GSI_TO_RESTART_SERVER) {
	    if(GSGUITestingUtil.gsi_window == null) {
		runGSI();
		GSGUITestingUtil.getGSIApplicationWindow(robot(), 10);
		//GSI_enterRestartLibrary(null);
	    }
	    GSI_enterRestartLibrary(null);
	} else {
	    result = commandLineServerRestart();
	}
	*/
	result = commandLineServerRestart();
	return result;
    }
    public static int serverRestartAndPreview() {
	return serverRestartAndPreview(null, null);	
    }
    public static int serverRestartAndPreview(String previewURL) {
	return serverRestartAndPreview(previewURL, null);
    }    
    public static int serverRestartAndPreview(String previewURL, Runnable browserTestCode) {
	int result = serverRestart();
	PAUSE(2); // give it some more time
	refreshServerAndPreview(previewURL, browserTestCode);
	
	return result;
    }

    public static void refreshServerAndPreview() {
	refreshServerAndPreview(null, null);	
    }
    public static void refreshServerAndPreview(String previewURL) {
	refreshServerAndPreview(previewURL, null);
    }
    public static void refreshServerAndPreview(String previewURL, Runnable browserTestCode) {
	minimiseGLI();
	if(previewURL == null) {
	    String gsBaseURL = (String)props.get("gs.base.url");
	    final String LIBRARY_HOME_PAGE = gsBaseURL + "library";
	    previewURL = LIBRARY_HOME_PAGE;
	}
	_driver.get(previewURL);
	
	if(browserTestCode != null) {
	    System.err.println("*************************************");
	    focusBrowser();
	    browserTestCode.run();
	    System.err.println("*************************************");
	}
    }

	public static String toWorkspaceTreePath(String path) {
		// JTreeFixtures denote paths with /
		//String treePath = path.replace('\\', '/'); // WRONG
		String treePath = path.replace(File.separator, "/"); // On windows, change single backslash ('\\') to '/'
		return treePath;
	}
}
