StudioWebDiag.groovy: updated to check for an ‘Error:’ before looking for results in a table, and the Error need not be the first thing listed; after each query test, it now deletes all data from REPLICATED_TABLE, as well as PARTITIONED_TABLE; also increased the range of the try/finally, to catch most test failures; and added debug print (triggered whenever a test fails, or optionally for all tests), and wait options, so you can see the results go by slowly.
StudioWebDiag.groovy: updated to check for an ‘Error:’ before looking
for results in a table, and the Error need not be the first thing
listed; after each query test, it now deletes all data from
REPLICATED_TABLE, as well as PARTITIONED_TABLE; also increased the range
of the try/finally, to catch most test failures; and added debug print
(triggered whenever a test fails, or optionally for all tests), and wait
options, so you can see the results go by slowly.
/* This file is part of VoltDB.
 * Copyright (C) 2008-2014 VoltDB Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * 'Software'), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

import geb.*
import geb.spock.*
import geb.driver.CachingDriverFactory
import org.openqa.selenium.interactions.Actions
import spock.lang.*
import groovy.json.*


class StudioWebPage extends Page {
    static url = StudioWebDiag.USE_NEW_UI ? 'http://admin:voltdb@stag.deerwalk.com/mockup/voltdb_monitor/studio/index.htm'
                                          : 'file://localhost/' + new File('../../../src/frontend/org/voltdb/studio/index.htm').getCanonicalPath()
    static at = { title == 'VoltDB Web Studio' }
}

class StudioWebDiag extends GebReportingSpec {
    static final boolean USE_NEW_UI = false;
    static final boolean DEBUG_PRINT = false;
    // Set these > 0 only for debugging, so you can see results go by slowly:
    static final int WAIT_SECS_AFTER_EACH_TEST = 0;
    static final int WAIT_SECS_AFTER_DELETE = 0;

    //STATUS MESSAGES
    static String FAILURE = USE_NEW_UI ? 'Query Result' : 'Query error'
    static String SUCCESS = USE_NEW_UI ? 'Query Result' : 'Query Duration:'
    static String ERROR = 'Error:' //all errors are handled server-side

    //FILES
    static String elemFileName = USE_NEW_UI ? 'elemsNewUI.txt' : 'elems.txt'
    @Shared def elementFile = new File('src/test/resources/' + elemFileName)
    @Shared def procedureFile = new File('src/test/resources/storedProcs.txt')
    @Shared def sqlFile = new File('src/test/resources/sql.txt')
    @Shared def correctReadme = new File('src/test/resources/readme.htm')
    @Shared def chkReadme = new File('../../../src/frontend/org/voltdb/studio/readme.htm')
  
    //SHARED VARIABLES
    @Shared def slurper = new JsonSlurper()
    @Shared def response //stores expected response so that it may be accessed by all helper methods
    
    @Shared def sqlList = []
    @Shared def correctElements = []
    @Shared def correctProcedures = []
    @Shared def correctReadmeLines = []
    @Shared def chkReadmeLines = []
    
    @Shared def fileListPairs = [
        [elementFile, correctElements],
        [procedureFile, correctProcedures],
        [sqlFile, sqlList],
        [correctReadme, correctReadmeLines],
        [chkReadme, chkReadmeLines]]
    
    
    def setupSpec() {
        //Move file contents to memory
        def primeList = {file,list -> if(file.size() != 0) {
             file.eachLine {line -> if (!line.trim().startsWith('#')){
                list.add(line)}}
            }
        }
        fileListPairs.each{ primeList(*it) }
    }

    def 'To StudioWeb'() {
        setup: 'Open Studio web'
        to StudioWebPage

        expect: 'to be on studio web page'
        at StudioWebPage
    }

    def 'Contains important elements'(){
        setup: 'Get elements from page'
        def elesToCheck = $('[id]:not(canvas)').collect {"${ it.attr('id') }"}
        //important elements identified by ID. List of id's returned
        //ignores canvas element generated by firefoxDriver

        expect: 'compare page contents to file contents and expect them to be the same'
        if (correctElements.isEmpty()){
            correctElements = elesToCheck
            assert false , 'ERROR: No elements expected. On cleanUp, contents of elementFile will be set to elements found on page'
        }else {assert elesToCheck == correctElements}
    }

    def 'Connect to server'(){
        if (USE_NEW_UI) {
            return
        }

        when: 'connect button is clicked'
        $('span.connect').click()

        then: 'test connection'
        $('span.ui-button-text', text: 'Test Connection').click()

        then: 'wait for server response'
        waitFor {$('td.validateTips').text().contains('successful') | $('td.validateTips').text().contains('Unable')}

        then: 'get server state'
        def state = $('td.validateTips').text()

        when: 'if server available'
        state.contains('successful')

        then: 'connect to server'
        $('span.ui-button-text', text: 'OK').click() //OK button
    }

    def 'Check readMe file'(){
        expect: 'them to be the same'
        correctReadmeLines == chkReadmeLines & correctReadmeLines.size() != 0
    }

    def 'System Stored Procedures'(){
        if (USE_NEW_UI) {
            return
        }

        setup: 'make stored procedures visible'
        expandFolds()

        and: 'obtain their locations and make list of their text'
        def procsToCheck = $('#localhost_8080__Admin_sp').find('ul', 0).children().children('span')*.text()

        and: 'expect them to be the same (no extras, none missing)'
        if (correctProcedures.isEmpty()){
            correctProcedures = procsToCheck
            assert false , 'ERROR: No procedures expected. On cleanUp, contents of ProcedureFile will be set to procedures found on page'
        }else {assert procsToCheck == correctProcedures}
    }

    //TEST SQL Queries
    @Unroll //performs this method for each item in testName
    def '#testName'(){
            boolean testFailed = true
            def statusField = null
            def resultField = null
            def inputField = null
            
        try{
            setup: 'open new query'
            response = resTmp
            $('#new-query').click()
            inputField = $('#worktabs').find('div', 0).find('textarea')
            if (USE_NEW_UI) {
                inputField = $('#theQueryText')
                response.result = [Contestant_name:[null, 'Lorem_Ipsum', 'Lorem_Dolor', 'Ipsum_Dolor', 'Sit_Amet', 'Lorem_Ipsum_Dolor', 'Ipsum_Dolor_Sit'],
                                   Contestant_number:[null, 20, 21, 22, 23, 24, 25],
                                   Total_votes:[null, 15002, 18002, 19003, 22809, 28402, 32456] ]
            }

            and: 'execute SQL query'
            inputField.value(input)
            if (USE_NEW_UI) {
                $('.btnStudio').click()
            } else {
                $('#execute-query').click()
            }

            and: 'obtain response status and result'
            statusField = $('#worktabs').find('span.status')
            resultField = $('#worktabs').find('div.resultbar').children('div')
            if (USE_NEW_UI) {
                statusField = $('.icon-queryResult')
                resultField = $('#resultHtml')
            }
            // TODO: may want to add a wait here, to make sure (new) values
            // are visible in statusField & resultField, since this fails very
            // occasionally, apparently do to a timing issue

            when: 'ensure appropriate status and result are displayed'
            assert checkResponse(statusField, resultField)
            testFailed = false

        } finally {
            if (testFailed || DEBUG_PRINT) {
                println "\n\n" + testName + (testFailed ? " FAILED:\n" : " passed:\n") + inputField?.value()
                println "Status field: " + statusField?.text()
                println "Result field:\n" + resultField?.text()
            }
            if (WAIT_SECS_AFTER_EACH_TEST > 0) {
                Thread.sleep(WAIT_SECS_AFTER_EACH_TEST * 1000);
            }
            // TODO: update this section, to use new UI (when available)
            then: 'clear all tables'
            if (inputField) {
                inputField.value('DELETE FROM partitioned_table;\nDELETE FROM replicated_table;')
                $('#execute-query').click()
            }
            if (WAIT_SECS_AFTER_DELETE > 0) {
                Thread.sleep(WAIT_SECS_AFTER_DELETE * 1000);
            }

            and: 'close query'
            $('#worktabs').find('ul').find('li', 0).find('span').click()

            and: 'ensure query was closed'
            assert $('#worktabs').children('div').isEmpty()
        }

        where: "list of inputs to test and expected responses"
        line << sqlList
        iter = slurper.parseText(line)
        testName = iter.testName
        input = iter.sqlCmd
        resTmp = iter.response
    }


    def 'Disconnect from server'(){
        if (USE_NEW_UI) {
            return
        }

        setup: 'Actions interface to enable right click'
        def actions = new Actions(this.getDriver())

        and: 'find element in sidebar to right click'
        def clickMe = $('#dbbrowser').children('li').firstElement()

        when: 'context menu is brought up'
        actions.contextClick(clickMe).build().perform()

        then: 'find disconnect option'
        def disconn = $('ul', id: 'objectbrowsermenu').find('li.disconnect').find('a')

        when: 'disconnect is visible'
        disconn.isDisplayed()

        then: 'click to disconnect from server'
        disconn.click()

        and: 'expect server content to be unavailable'
        assert $('ul#dbbrowser.treeview').children().isEmpty()
    }

    def cleanupSpec(){

        //any empty files will have new list of procedures and elements added.
        def newBaseLine = {file,list -> if(file.size() == 0){
            file.withWriter {out -> list.each {out.writeLine(it)}}}
        }
        [[elementFile,correctElements],[procedureFile,correctProcedures]].each {newBaseLine(*it)}
    }

    def checkResponse(def statusField, def resultField){

        //check status--false if bad status
        waitFor(){statusField.text() != null}
        if(response.status == 'SUCCESS' | response.status == 'FAILURE'){
            def status = response.status == 'SUCCESS' ? SUCCESS : FAILURE
            assert statusField.text() =~ /^$status/
        }else{return false}

        //check result
        if(resultField.text() =~ /$ERROR/){
            return response.result == 'ERROR'
        }else if(resultField.children().is('table')) {
            makeAndCheckTable(resultField.find('table').last())
        }else{return false} //result should only be a table or an error message
    }

    def makeAndCheckTable(def tableLoc){

        def table = [:]
        def columns = tableLoc.find('thead').find('th')*.text()
        if (USE_NEW_UI) {
            columns = tableLoc.find('tr').find('th')*.text()
        }
        def rows = tableLoc.find('tbody').find('tr')

        //make table
        def colNum = 0
        def makeCol = { index,rowset -> rowset.collect { row -> row.find('td',index).text() } }
        columns = columns.collect {it.toLowerCase()}
        columns.each {table.put(it,makeCol(colNum++, rows))}

        //list table for debug purposes
        // def listColumn = { System.out.println("DEBUG: " + response.result."$it" + " : " + table[it]) }
        // columns.each(listColumn)

        //check table
        // In the original version of this code, this check was much simpler:
        // def checkColumn = {assert response.result."$it" == table[it]}
        // but when the code was dusted off for 4.0 testing, nulls were failing this check
        // by getting returned as a singleton list containing an empty string.
        // So, without really understanding "what changed?" (since presumably this test used to pass),
        // we relaxed the test.
        def checkColumn = {
            def expected_column = response.result."$it"
            def table_column = table[it]
            if (expected_column == null) {
                assert table_column.size() == (USE_NEW_UI ? 7 : 1)
                assert table_column[0] == (USE_NEW_UI ? null : "")
            } else {
                assert expected_column == table_column
            }
        }
        columns.each(checkColumn)
    }

    void expandFolds(){
        def fold1 = $('span', text: 'Programmability')
        def fold2 = fold1.siblings().find('li',0).children('div')
        def fold3 = fold2.siblings().find('li',0).children('div')
        [fold1, fold2, fold3]*.click() //open each folder
    }
}

/* This file is part of VoltDB.
 * Copyright (C) 2008-2014 VoltDB Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * 'Software'), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

import geb.*
import geb.spock.*
import geb.driver.CachingDriverFactory
import org.openqa.selenium.interactions.Actions
import spock.lang.*
import groovy.json.*


class StudioWebPage extends Page {
    static url = StudioWebDiag.USE_NEW_UI ? 'http://admin:voltdb@stag.deerwalk.com/mockup/voltdb_monitor/studio/index.htm'
                                          : 'file://localhost/' + new File('../../../src/frontend/org/voltdb/studio/index.htm').getCanonicalPath()
    static at = { title == 'VoltDB Web Studio' }
}

class StudioWebDiag extends GebReportingSpec {
    static final boolean USE_NEW_UI = false;

    //STATUS MESSAGES
    static String FAILURE = USE_NEW_UI ? 'Query Result' : 'Query error'
    static String SUCCESS = USE_NEW_UI ? 'Query Result' : 'Query Duration:'
    static String ERROR = 'Error:' //all errors are handled server-side

    //FILES
    static String elemFileName = USE_NEW_UI ? 'elemsNewUI.txt' : 'elems.txt'
    @Shared def elementFile = new File('src/test/resources/' + elemFileName)
    @Shared def procedureFile = new File('src/test/resources/storedProcs.txt')
    @Shared def sqlFile = new File('src/test/resources/sql.txt')
    @Shared def correctReadme = new File('src/test/resources/readme.htm')
    @Shared def chkReadme = new File('../../../src/frontend/org/voltdb/studio/readme.htm')
  
    //SHARED VARIABLES
    @Shared def slurper = new JsonSlurper()
    @Shared def response //stores expected response so that it may be accessed by all helper methods
    
    @Shared def sqlList = []
    @Shared def correctElements = []
    @Shared def correctProcedures = []
    @Shared def correctReadmeLines = []
    @Shared def chkReadmeLines = []
    
    @Shared def fileListPairs = [
        [elementFile, correctElements],
        [procedureFile, correctProcedures],
        [sqlFile, sqlList],
        [correctReadme, correctReadmeLines],
        [chkReadme, chkReadmeLines]]
    
    
    def setupSpec() {
        //Move file contents to memory
        def primeList = {file,list -> if(file.size() != 0) {
             file.eachLine {line -> if (!line.trim().startsWith('#')){
                list.add(line)}}
            }
        }
        fileListPairs.each{ primeList(*it) }
    }

    def 'To StudioWeb'() {
        setup: 'Open Studio web'
        to StudioWebPage

        expect: 'to be on studio web page'
        at StudioWebPage
    }

    def 'Contains important elements'(){
        setup: 'Get elements from page'
        def elesToCheck = $('[id]:not(canvas)').collect {"${ it.attr('id') }"}
        //important elements identified by ID. List of id's returned
        //ignores canvas element generated by firefoxDriver

        expect: 'compare page contents to file contents and expect them to be the same'
        if (correctElements.isEmpty()){
            correctElements = elesToCheck
            assert false , 'ERROR: No elements expected. On cleanUp, contents of elementFile will be set to elements found on page'
        }else {assert elesToCheck == correctElements}
    }

    def 'Connect to server'(){
        if (USE_NEW_UI) {
            return
        }

        when: 'connect button is clicked'
        $('span.connect').click()

        then: 'test connection'
        $('span.ui-button-text', text: 'Test Connection').click()

        then: 'wait for server response'
        waitFor {$('td.validateTips').text().contains('successful') | $('td.validateTips').text().contains('Unable')}

        then: 'get server state'
        def state = $('td.validateTips').text()

        when: 'if server available'
        state.contains('successful')

        then: 'connect to server'
        $('span.ui-button-text', text: 'OK').click() //OK button
    }

    def 'Check readMe file'(){
        expect: 'them to be the same'
        correctReadmeLines == chkReadmeLines & correctReadmeLines.size() != 0
    }

    def 'System Stored Procedures'(){
        if (USE_NEW_UI) {
            return
        }

        setup: 'make stored procedures visible'
        expandFolds()

        and: 'obtain their locations and make list of their text'
        def procsToCheck = $('#localhost_8080__Admin_sp').find('ul', 0).children().children('span')*.text()

        and: 'expect them to be the same (no extras, none missing)'
        if (correctProcedures.isEmpty()){
            correctProcedures = procsToCheck
            assert false , 'ERROR: No procedures expected. On cleanUp, contents of ProcedureFile will be set to procedures found on page'
        }else {assert procsToCheck == correctProcedures}
    }

    //TEST SQL Queries
    @Unroll //performs this method for each item in testName
    def '#testName'(){

            setup: 'open new query'
            response = resTmp
            $('#new-query').click()
            def inputField = $('#worktabs').find('div', 0).find('textarea')
            if (USE_NEW_UI) {
                inputField = $('#theQueryText')
                response.result = [Contestant_name:[null, 'Lorem_Ipsum', 'Lorem_Dolor', 'Ipsum_Dolor', 'Sit_Amet', 'Lorem_Ipsum_Dolor', 'Ipsum_Dolor_Sit'],
                                   Contestant_number:[null, 20, 21, 22, 23, 24, 25],
                                   Total_votes:[null, 15002, 18002, 19003, 22809, 28402, 32456] ]
            }

            and: 'execute SQL query'
            inputField.value(input)
            if (USE_NEW_UI) {
                $('.btnStudio').click()
            } else {
                $('#execute-query').click()
            }

            and: 'obtain response status and result'
            def statusField = $('#worktabs').find('span.status')
            def resultField = $('#worktabs').find('div.resultbar').children('div')
            if (USE_NEW_UI) {
                statusField = $('.icon-queryResult')
                resultField = $('#resultHtml')
            }
            // TODO: may want to add a wait here, to make sure (new) values
            // are visible in statusField & resultField, since this fails very
            // occasionally, apparently do to a timing issue

        try{
            when: 'ensure appropriate status and result are displayed'
            assert checkResponse(statusField, resultField)

        } finally {
            // TODO: update this section, to use new UI (when available)
            then: 'clear all tables'
            inputField.value('DELETE FROM partitioned_table')
            $('#execute-query').click()

            and: 'close query'
            $('#worktabs').find('ul').find('li', 0).find('span').click()

            and: 'ensure query was closed'
            assert $('#worktabs').children('div').isEmpty()
        }

        where: "list of inputs to test and expected responses"
        line << sqlList
        iter = slurper.parseText(line)
        testName = iter.testName
        input = iter.sqlCmd
        resTmp = iter.response
    }


    def 'Disconnect from server'(){
        if (USE_NEW_UI) {
            return
        }

        setup: 'Actions interface to enable right click'
        def actions = new Actions(this.getDriver())

        and: 'find element in sidebar to right click'
        def clickMe = $('#dbbrowser').children('li').firstElement()

        when: 'context menu is brought up'
        actions.contextClick(clickMe).build().perform()

        then: 'find disconnect option'
        def disconn = $('ul', id: 'objectbrowsermenu').find('li.disconnect').find('a')

        when: 'disconnect is visible'
        disconn.isDisplayed()

        then: 'click to disconnect from server'
        disconn.click()

        and: 'expect server content to be unavailable'
        assert $('ul#dbbrowser.treeview').children().isEmpty()
    }

    def cleanupSpec(){

        //any empty files will have new list of procedures and elements added.
        def newBaseLine = {file,list -> if(file.size() == 0){
            file.withWriter {out -> list.each {out.writeLine(it)}}}
        }
        [[elementFile,correctElements],[procedureFile,correctProcedures]].each {newBaseLine(*it)}
    }

    def checkResponse(def statusField, def resultField){

        //check status--false if bad status
        waitFor(){statusField.text() != null}
        if(response.status == 'SUCCESS' | response.status == 'FAILURE'){
            def status = response.status == 'SUCCESS' ? SUCCESS : FAILURE
            assert statusField.text() =~ /^$status/
        }else{return false}

        //check result
        if(resultField.children().is('table')){
            makeAndCheckTable(resultField.find('table').last())
        }else if(resultField.text() =~ /^$ERROR/) {return response.result == 'ERROR'
        }else{return false} //result should only be a table or an error message
    }

    def makeAndCheckTable(def tableLoc){

        def table = [:]
        def columns = tableLoc.find('thead').find('th')*.text()
        if (USE_NEW_UI) {
            columns = tableLoc.find('tr').find('th')*.text()
        }
        def rows = tableLoc.find('tbody').find('tr')

        //make table
        def colNum = 0
        def makeCol = { index,rowset -> rowset.collect { row -> row.find('td',index).text() } }
        columns = columns.collect {it.toLowerCase()}
        columns.each {table.put(it,makeCol(colNum++, rows))}

        //list table for debug purposes
        // def listColumn = { System.out.println("DEBUG: " + response.result."$it" + " : " + table[it]) }
        // columns.each(listColumn)

        //check table
        // In the original version of this code, this check was much simpler:
        // def checkColumn = {assert response.result."$it" == table[it]}
        // but when the code was dusted off for 4.0 testing, nulls were failing this check
        // by getting returned as a singleton list containing an empty string.
        // So, without really understanding "what changed?" (since presumably this test used to pass),
        // we relaxed the test.
        def checkColumn = {
            def expected_column = response.result."$it"
            def table_column = table[it]
            if (expected_column == null) {
                assert table_column.size() == (USE_NEW_UI ? 7 : 1)
                assert table_column[0] == (USE_NEW_UI ? null : "")
            } else {
                assert expected_column == table_column
            }
        }
        columns.each(checkColumn)
    }

    void expandFolds(){
        def fold1 = $('span', text: 'Programmability')
        def fold2 = fold1.siblings().find('li',0).children('div')
        def fold3 = fold2.siblings().find('li',0).children('div')
        [fold1, fold2, fold3]*.click() //open each folder
    }
}
