1
\$\begingroup\$

I have a volunteer registration website that I wrote some end-to-end tests for in Selenium. Selenium is of course software that lets you automate clicking around a browser, which makes it great for testing websites.

This is my first time working with Selenium. I got some tests working and they are very helpful. But the code feels verbose and disorganized. I think as I add tests this is going to spiral out of control and become too verbose. Looking for ideas for improvement.

Notes/Questions:

  • Should I install some kind of library/framework that gets rid of some of this low level code?
  • I'm using a pattern of "log in, set settings, log out, run tests, log in, verify tests" for each test, to allow each test to run independently. Is this the best approach?
  • I'm surprised Selenium doesn't have implicit wait built in. iMacros (another E2E testing software I've used) has this... it wouldn't start clicking around until the page was fully loaded. In Selenium, it seems if you do a driver.get() then immediately try to do stuff, you get an error. I solved this by putting a driver.wait() in a function that I always call before everything else.
  • I'm surprised Selenium doesn't detect HTTP errors (e.g. 401, 403, 404) and PHP errors automatically. iMacros did this and it was really nice. I tried to write some code to do this in the visitPage() function. In an ideal world we would also fail on JavaScript errors, and do HTML validation.
  • I decided to create a cookie to turn off the website sending email confirmations. Other than that, everything is the normal code paths, so the test should be pretty thorough.

I will eventually throw this in a Docker container instead of using driver.get(), and I will eventually set this up in GitHub Actions CI, but for now this will have to do.

Anyway here's the code:

enter image description here

// global
var testString1;
var testString2;
var delay = 5000; // ms for implicit waits
var driver;
var devOrWWW = 'dev';

var secrets = [];
secrets['httpwd'] = '';
secrets['consoleUsername'] = '';
secrets['consolePassword'] = '';
secrets['deleteCompanySecurityKey'] = '';

async function visitPage(driver, uri) {
    // if URL, convert to URI
    uri = uri.replace('https://dev.titanvolunteers.com/', '');

    // Log into development version of the website. Note that these files need to be up-to-date. Don't forget to upload your changes via FTP.
    if ( devOrWWW === 'dev' ) {
        await driver.get(`https://dev:${secrets['httpwd']}@dev.titanvolunteers.com/` + uri);
    } else {
        await driver.get("https://www.titanvolunteers.com/" + uri);
    }

    // The below line serves two purposes: 1) fail on any PHP error, and 2) make sure the page is loaded before trying to click stuff, making it so we don't need to use .wait() anywhere else
    assert(await hasPageText(driver, "A PHP Error was encountered") === false, 'PHP error');

    // stuff from ci_dev/application/views/errors/html
    assert(await hasPageText(driver, "404 Page Not Found") === false, '404 error');
    assert(await hasPageText(driver, "403 Permission Error") === false, '403 error');
    assert(await hasPageText(driver, "An uncaught Exception was encountered") === false, 'PHP uncaught exception');
    assert(await hasPageText(driver, "A Database Error Occurred") === false, 'Database error');

    // Turn off email sending while testing. Too spammy. There is code in PHP that reads this cookie and turns off emails.
    await driver.manage().addCookie({name: 'no-emails', value: 'true'});
}

async function logIn(driver) {
    await visitPage(driver, "general/log_in");
    await driver.findElement(By.name("username")).sendKeys(secrets['consoleUsername']) // this is a non-admin account, which is good
    await driver.findElement(By.name("password")).sendKeys(secrets['consolePassword'])
    await driver.findElement(By.css(".sign_up_button")).click()
}

async function logOut(driver) {
    await visitPage(driver, "general/log_out");
}

async function click(driver, css) {
    await driver.findElement(By.css(css)).click();
}

async function typeIntoForm(driver, htmlName, str) {
    await driver.findElement(By.name(htmlName)).clear();
    await driver.findElement(By.name(htmlName)).sendKeys(str);
}

async function hasPageText(driver, str) {
    let bodyHTML = await driver.wait(until.elementLocated(By.css('body')), delay).getText();
    return bodyHTML.includes(str); // can't chain .includes() for some reason. can't do By.css('html') for some reason.
}

async function tickCheckBox(driver, htmlName) {
    let el = await driver.findElement(By.name(htmlName));
    if ( ! await el.isSelected() ) {
        await el.click();
    }
}

async function untickCheckBox(driver, htmlName) {
    let el = await driver.findElement(By.name(htmlName));
    if ( await el.isSelected() ) {
        await el.click();
    }
}

/** If you try to use driver.findElement() instead of driver.findElements(), it throws an exception. If you try to do assert(! driver.findElements()), it won't work because [] is not falsy in JavaScript. */
async function elementDoesNotExist(driver, by) {
    return await driver.findElements(by) === [];
}

async function elementExists(driver, by) {
    return await driver.findElements(by) !== [];
}

async function setUpEventWithDefaultPreferences(driver) {
    await logIn(driver);
    await visitPage(driver, "managers/edit_event/480-Selenium-Tests");

    // Recommend turning on
    await tickCheckBox(driver, 'race_send_vol_list_to_group_leaders');
    await tickCheckBox(driver, 'race_limit_group_member_shifts');
    await tickCheckBox(driver, 'race_reserve_group_spots');
    await tickCheckBox(driver, 'race_groups_one_shift_per_day');
    await tickCheckBox(driver, 'race_north_american_phone_number_validation');

    // Recommend turning off
    await tickCheckBox(driver, 'race_hide_from_public_event_list'); // turn this one on, rest off
    await untickCheckBox(driver, 'race_use_hard_close');
    await untickCheckBox(driver, 'race_email_vc_when_group_signs_up');
    await untickCheckBox(driver, 'race_reminder_emails_hide_estimate');
    await untickCheckBox(driver, 'race_hide_need_to_open_warning');

    // Waiver - blanking this will turn off the signature and e-signature fields, so this is basically a "show signature" setting
    await driver.findElement(By.name('race_waiver')).clear();
    await driver.findElement(By.name('race_waiver')).sendKeys("Putting text here to activate the signature and parent signature fields.");

    // submit
    await driver.findElement(By.css(`[value="Save Changes"]`)).click();

    // navigate back to the edit event page, check that all the settings are correct
    await visitPage(driver, "managers/edit_event/480-Selenium-Tests");
    assert(await driver.findElement(By.name('race_send_vol_list_to_group_leaders')).isSelected(), 'Edit Event: unexpected setting 1');
    assert(await driver.findElement(By.name('race_limit_group_member_shifts')).isSelected(), 'Edit Event: unexpected setting 2');
    assert(await driver.findElement(By.name('race_reserve_group_spots')).isSelected(), 'Edit Event: unexpected setting 3');
    assert(await driver.findElement(By.name('race_groups_one_shift_per_day')).isSelected(), 'Edit Event: unexpected setting 4');
    assert(await driver.findElement(By.name('race_north_american_phone_number_validation')).isSelected(), 'Edit Event: unexpected setting 5');
    assert(await driver.findElement(By.name('race_hide_from_public_event_list')).isSelected(), 'Edit Event: unexpected setting 6');
    assert(! await driver.findElement(By.name('race_use_hard_close')).isSelected(), 'Edit Event: unexpected setting 7');
    assert(! await driver.findElement(By.name('race_email_vc_when_group_signs_up')).isSelected(), 'Edit Event: unexpected setting 8');
    assert(! await driver.findElement(By.name('race_reminder_emails_hide_estimate')).isSelected(), 'Edit Event: unexpected setting 9');
    assert(! await driver.findElement(By.name('race_hide_need_to_open_warning')).isSelected(), 'Edit Event: unexpected setting 10');
    assert(await driver.findElement(By.name('race_waiver')).getText() === 'Putting text here to activate the signature and parent signature fields.', 'Edit Event: unexpected setting 11');

    // log out
    await logOut(driver);
}

async function deleteCurrentCompany(driver) {
    await visitPage(driver, "https://dev.titanvolunteers.com/managers/delete_current_company/");
    await driver.findElement(By.name("security_key")).sendKeys(secrets['deleteCompanySecurityKey']);
    await driver.findElement(By.css(`[type="submit"]`)).click();
}

const { Builder, By, Key, until, logging } = require('selenium-webdriver');
const assert = require('assert');
const { doesNotMatch } = require('assert');

describe('https://dev.titanvolunteers.com/', function() {
    this.timeout(500000); // essential to raise this, since default is to cut off your tests after 2 seconds. this does not like to be inside of other functions. current longest test is 120 seconds.

    before(async function() {
        testString1 = Math.random().toString() + '-Iñtërnâtiônàlizætiøn';
        testString2 = Math.random().toString() + '-Iñtërnâtiônàlizætiøn';
        driver = await new Builder().forBrowser('chrome').build();
        await driver.manage().window().maximize(); // helpful with auto screenshots
        //await driver.manage().window().setRect({ width: 1552, height: 840 })
        // await driver.manage().setTimeouts({implicit: 2000}); // not working
    });

    beforeEach(async function() {
        await driver.manage().deleteAllCookies(); // delete all cookies before each test. selenium uses a fresh browser, so cookie deletions won't affect your main browser
    });

    afterEach(async function() {
        // reuse same browser for now, faster
    });

    after(async function() {
        await driver.quit(); // closes the browser
    });

    describe('volunteers/sign_up_individual', function() {
        it('should process a signup', async function() {
            await setUpEventWithDefaultPreferences(driver);

            await visitPage(driver, "volunteers/sign_up_individual/480-Selenium-Public-Page-Tests");
            await driver.findElement(By.name("shift_id[4068]")).click();
            await driver.findElement(By.name("volunteer_email")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_email2")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_firstname")).sendKeys(testString1)
            await driver.findElement(By.name("volunteer_lastname")).sendKeys("Jones")
            await driver.findElement(By.name("volunteer_phone")).sendKeys("555-555-5555")
            const dropdown = await driver.findElement(By.name("volunteer_shirt_size"))
            await dropdown.findElement(By.xpath("//option[. = 'Small']")).click()
            await driver.findElement(By.name("spam_check")).sendKeys("30")
            await driver.findElement(By.name("volunteer_group_name")).sendKeys("Test Key Club")
            await driver.findElement(By.name("volunteer_group_leader")).click()
            await driver.findElement(By.name('volunteer_esig')).sendKeys("Signature goes here");
            await driver.findElement(By.css(".sign_up_button")).click()

            // log in and check volunteer list, verify the signup went through
            await logIn(driver);
            await visitPage(driver, "managers/volunteers_by_signup_date/480-Selenium-Public-Page-Tests?page=1");
            await driver.findElement(By.linkText(testString1)).click();

            // delete the volunteer
            await driver.findElement(By.css(`[name="additional_action"][value="delete"]`)).click();
            await driver.findElement(By.css(`[value="Save Changes"]`)).click();

            // confirm volunteer deleted
            await visitPage(driver, "managers/volunteers_by_signup_date/480-Selenium-Public-Page-Tests?page=1");
            assert(elementDoesNotExist(driver, By.linkText(testString1)));
        });

        // good to have a test that tests for failure, in addition to the test for success test above
        it('should display form validation error if spam check text box is not 30', async function() {
            await setUpEventWithDefaultPreferences(driver);

            await visitPage(driver, "volunteers/sign_up_individual/480-Selenium-Public-Page-Tests");
            await driver.findElement(By.name("shift_id[4068]")).click();
            await driver.findElement(By.name("volunteer_email")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_email2")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_firstname")).sendKeys(testString1)
            await driver.findElement(By.name("volunteer_lastname")).sendKeys("Jones")
            await driver.findElement(By.name("volunteer_phone")).sendKeys("555-555-5555");
            const dropdown = await driver.findElement(By.name("volunteer_shirt_size"))
            await dropdown.findElement(By.xpath("//option[. = 'Small']")).click()
            await driver.findElement(By.name("spam_check")).sendKeys("29")
            await driver.findElement(By.name("volunteer_group_name")).sendKeys("Test Key Club")
            await driver.findElement(By.name("volunteer_group_leader")).click()
            await driver.findElement(By.name('volunteer_esig')).sendKeys("Signature goes here");
            await driver.findElement(By.css(".sign_up_button")).click()

            // make sure it throws the spam check validation error
            assert(await hasPageText(driver, "The Spam Check field must be one of: 30.") === true, 'Spam check failure message not detected');
        });

        it('should display form validation error if international phone number given with phone # validation turned ON', async function() {
            await setUpEventWithDefaultPreferences(driver);

            await visitPage(driver, "volunteers/sign_up_individual/480-Selenium-Public-Page-Tests");
            await driver.findElement(By.name("shift_id[4068]")).click();
            await driver.findElement(By.name("volunteer_email")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_email2")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_firstname")).sendKeys(testString1)
            await driver.findElement(By.name("volunteer_lastname")).sendKeys("Jones")
            await driver.findElement(By.name("volunteer_phone")).sendKeys("07825654199");
            const dropdown = await driver.findElement(By.name("volunteer_shirt_size"))
            await dropdown.findElement(By.xpath("//option[. = 'Small']")).click()
            await driver.findElement(By.name("spam_check")).sendKeys("30")
            await driver.findElement(By.name("volunteer_group_name")).sendKeys("Test Key Club")
            await driver.findElement(By.name("volunteer_group_leader")).click()
            await driver.findElement(By.name('volunteer_esig')).sendKeys("Signature goes here");
            await driver.findElement(By.css(".sign_up_button")).click()

            // make sure it throws the spam check validation error
            assert(await hasPageText(driver, "Cell Phone must be in the format 111-111-1111 x111") === true);
        });

        it('should NOT display form validation error if international phone number given with phone # validation turned OFF', async function() {
            await setUpEventWithDefaultPreferences(driver);

            await logIn(driver);
            await visitPage(driver, "managers/edit_event/480-Selenium-Tests");
            await untickCheckBox(driver, 'race_north_american_phone_number_validation');
            await driver.findElement(By.css(`[value="Save Changes"]`)).click();
            await logOut(driver);

            await visitPage(driver, "volunteers/sign_up_individual/480-Selenium-Public-Page-Tests");
            await driver.findElement(By.name("shift_id[4068]")).click();
            await driver.findElement(By.name("volunteer_email")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_email2")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_firstname")).sendKeys(testString1)
            await driver.findElement(By.name("volunteer_lastname")).sendKeys("Jones")
            await driver.findElement(By.name("volunteer_phone")).sendKeys("07825654199");
            const dropdown = await driver.findElement(By.name("volunteer_shirt_size"))
            await dropdown.findElement(By.xpath("//option[. = 'Small']")).click()
            await driver.findElement(By.name("spam_check")).sendKeys("30")
            await driver.findElement(By.name("volunteer_group_name")).sendKeys("Test Key Club")
            await driver.findElement(By.name("volunteer_group_leader")).click()
            await driver.findElement(By.name('volunteer_esig')).sendKeys("Signature goes here");
            await driver.findElement(By.css(".sign_up_button")).click()

            assert(await hasPageText(driver, "Cell Phone must be in the format 111-111-1111 x111") === false);

            await logIn(driver);
            await visitPage(driver, "managers/volunteers_by_signup_date/480-Selenium-Public-Page-Tests?page=1");
            await driver.findElement(By.linkText(testString1)).click();
            await driver.findElement(By.css(`[name="additional_action"][value="delete"]`)).click();
            await driver.findElement(By.css(`[value="Save Changes"]`)).click();
        });
    });

    describe('volunteers/sign_up_group_leader', function() {
        it('should process a signup', async function() {
            await setUpEventWithDefaultPreferences(driver);

            await visitPage(driver, "volunteers/sign_up_group_leader/480-Selenium-Public-Page-Tests");
            await driver.findElement(By.name("shift_id[4068]")).sendKeys("10");
            await driver.findElement(By.name("group_name")).sendKeys(testString1)
            await driver.findElement(By.name("volunteer_email")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_email2")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_firstname")).sendKeys(testString2)
            await driver.findElement(By.name("volunteer_lastname")).sendKeys("Jones")
            await driver.findElement(By.name("volunteer_phone")).sendKeys("555-555-5555")
            const dropdown = await driver.findElement(By.name("volunteer_shirt_size"))
            await dropdown.findElement(By.xpath("//option[. = 'Small']")).click()
            await driver.findElement(By.name("spam_check")).sendKeys("30")
            await driver.findElement(By.name('volunteer_esig')).sendKeys("Signature goes here");
            await driver.findElement(By.css(".sign_up_button")).click()

            // log in and check group list, verify the signup went through
            await logIn(driver);
            await visitPage(driver, "managers/group_report/480-Selenium-Tests");
            await driver.findElement(By.linkText(testString1)).click();

            // delete the group
            await driver.findElement(By.name("delete_group")).click();
            await driver.findElement(By.name("delete_group_members")).click();
            await driver.findElement(By.css(`[value="Edit Group"]`)).click();

            // confirm group deleted
            await visitPage(driver, "managers/group_report/480-Selenium-Tests");
            assert(elementDoesNotExist(driver, By.linkText(testString1)));

            // confirm volunteer deleted
            await visitPage(driver, "managers/volunteers_by_signup_date/480-Selenium-Public-Page-Tests?page=1");
            assert(elementDoesNotExist(driver, By.linkText(testString2)));
        });
    });

    describe('volunteers/sign_up_group_member', function() {
        it('should process a signup', async function() {
            await setUpEventWithDefaultPreferences(driver);

            // log in, add a group, log out
            await logIn(driver);
            await visitPage(driver, "managers/add_group/480-Selenium-Tests");
            await typeIntoForm(driver, 'group_name', testString1);
            await typeIntoForm(driver, 'shift_id[4068]', '10');
            await driver.findElement(By.css(`[value="Add Group"]`)).click();

            // confirm group created
            await visitPage(driver, "managers/group_report/480-Selenium-Tests");
            assert(elementExists(driver, By.linkText(testString1)), 'managers/add_group failed');

            await logOut(driver);

            // sign up group member
            await visitPage(driver, "volunteers/sign_up_individual/480-Selenium-Public-Page-Tests");
            const dropdown = await driver.findElement(By.id("group_picker"));
            await dropdown.findElement(By.xpath(`//option[contains(.,'${testString1}')]`)).click();
            await tickCheckBox(driver, 'shift_id[4068]');
            await driver.findElement(By.name("volunteer_email")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_email2")).sendKeys("[email protected]")
            await driver.findElement(By.name("volunteer_firstname")).sendKeys(testString2)
            await driver.findElement(By.name("volunteer_lastname")).sendKeys("Jones")
            await driver.findElement(By.name("volunteer_phone")).sendKeys("555-555-5555");
            const dropdown2 = await driver.findElement(By.name("volunteer_shirt_size"))
            await dropdown2.findElement(By.xpath("//option[. = 'Small']")).click()
            await driver.findElement(By.name("spam_check")).sendKeys("30")
            await driver.findElement(By.name('volunteer_esig')).sendKeys("Signature goes here");
            await driver.findElement(By.css(".sign_up_button")).click()

            // verify volunteer
            await logIn(driver);
            await visitPage(driver, "managers/volunteers_by_signup_date/480-Selenium-Public-Page-Tests?page=1");
            await driver.findElement(By.linkText(testString2)).click();

            // verify group
            await visitPage(driver, "managers/group_report/480-Selenium-Tests");
            await driver.findElement(By.linkText(testString1)).click();

            // delete group
            await driver.findElement(By.name("delete_group")).click();
            await driver.findElement(By.name("delete_group_members")).click();
            await driver.findElement(By.css(`[value="Edit Group"]`)).click();

            // confirm group deleted
            await visitPage(driver, "managers/group_report/480-Selenium-Tests");
            assert(elementDoesNotExist(driver, By.linkText(testString1)));

            // confirm volunteer deleted (deleted during group delete process)
            await visitPage(driver, "managers/volunteers_by_signup_date/480-Selenium-Public-Page-Tests?page=1");
            assert(elementDoesNotExist(driver, By.linkText(testString2)));
        });
    });

    describe('entire website', function() {
        it('should visit every page via HTTP GET (not POST), and not find any PHP or 404 errors', async function() {
            // logged out
            await visitPage(driver, "/");
            await visitPage(driver, "https://dev.titanvolunteers.com/volunteers/view_event_list");
            await visitPage(driver, "https://dev.titanvolunteers.com/general/screenshots");
            await visitPage(driver, "https://dev.titanvolunteers.com/general/sign_up_company");
            await visitPage(driver, "https://dev.titanvolunteers.com/general/tips");
            await visitPage(driver, "https://dev.titanvolunteers.com/general/log_in");
            await visitPage(driver, "https://dev.titanvolunteers.com/general/about_us");
            await visitPage(driver, "https://dev.titanvolunteers.com/general/contact_us");
            await visitPage(driver, "https://dev.titanvolunteers.com/volunteers/sign_up_individual/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/volunteers/sign_up_group_leader/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/general/password_reset_begin");
            await visitPage(driver, "https://dev.titanvolunteers.com/volunteers/verification_letter/480-Selenium-Tests?code=9a54e609");
            await visitPage(driver, "https://dev.titanvolunteers.com/general/paypal_listener");

            // logged in
            await logIn(driver);

            // company menu
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/company_report/5-Test-Company");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/add_event/5-Test-Company");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/buy_more");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/master_volunteer_list/5-Test-Company");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/volunteer_search_company/5-Test-Company");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/blacklist/5-Test-Company");
            await visitPage(driver, "https://dev.titanvolunteers.com/volunteers/shift_info/4068-1-Test-Shift-000?code=0849ef0f");

            // shifts menu
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/volunteer_report/480-Selenium-Tests");
                await visitPage(driver, "https://dev.titanvolunteers.com/managers/volunteer_report/480-Selenium-Tests?printer_friendly=1&notes=0");
                await visitPage(driver, "https://dev.titanvolunteers.com/managers/volunteer_report/480-Selenium-Tests?excel=1");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/volunteer_report_simple/480-Selenium-Tests");
                await visitPage(driver, "https://dev.titanvolunteers.com/managers/volunteer_report_simple/480-Selenium-Tests?printer_friendly=1");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/add_shifts/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/edit_shifts/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/merge_shifts/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/shift_info_links/480-Selenium-Tests");

            // group menu
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/group_report/480-Selenium-Tests");
                await visitPage(driver, "https://dev.titanvolunteers.com/managers/group_report/480-Selenium-Tests?printer_friendly=1");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/add_group/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/email_group_reminders/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/text_group_reminders/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/mass_edit_volunteers_by_group_name/480-Selenium-Tests");

            // event menu
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/edit_event/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/delete_event/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/duplicate_event/480-Selenium-Tests");

            // volunteer menu
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/volunteers_by_signup_date/480-Selenium-Tests?page=1");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/volunteer_search/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/add_volunteer/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/export_to_excel/480-Selenium-Tests");

            // email menu
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/email_group_reminders/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/email_vol_instructions/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/email_list/480-Selenium-Tests");

            // printouts menu
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/sign_in_sheets/480-Selenium-Tests");
                await visitPage(driver, "https://dev.titanvolunteers.com/managers/sign_in_sheets_print/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/single_page_waiver/480-Selenium-Tests");
                await visitPage(driver, "https://dev.titanvolunteers.com/managers/single_page_waiver_print/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/alpha_list_tool_gbs_configure/480-Selenium-Tests");
                await visitPage(driver, "https://dev.titanvolunteers.com/managers/alpha_list_tool_gbs_view/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/alpha_list_tool_gbg_configure/480-Selenium-Tests");
                await visitPage(driver, "https://dev.titanvolunteers.com/managers/alpha_list_tool_gbg_view/480-Selenium-Tests");
            
            // misc menu
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/post_race_report/480-Selenium-Tests");
                await visitPage(driver, "https://dev.titanvolunteers.com/managers/post_race_report/480-Selenium-Tests?printer_friendly=1");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/edit_post_race_data/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/shift_list_as_text/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/shift_list_as_table/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/verification_letter/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/edit_templates/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/signup_sources/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/signup_graph/480-Selenium-Tests");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/invoice_generator/480-Selenium-Tests");

            await visitPage(driver, "https://dev.titanvolunteers.com/managers/paypal_successful");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/paypal_unsuccessful");
            await visitPage(driver, "https://dev.titanvolunteers.com/managers/paypal");

            await logOut(driver);
        });
    });

    describe('general/sign_up_company', function() {
        it('should process a signup', async function() {
            await visitPage(driver, "https://dev.titanvolunteers.com/general/sign_up_company");
            await driver.findElement(By.name("manager_email")).sendKeys("[email protected]")
            await driver.findElement(By.name("manager_password")).sendKeys("ToughPassword#1");
            await driver.findElement(By.name("manager_firstname")).sendKeys("Selenium");
            await driver.findElement(By.name("manager_lastname")).sendKeys("Tests");
            await driver.findElement(By.name("manager_phone")).sendKeys("555-555-5555");
            await driver.findElement(By.name("company_website_url")).sendKeys("www.test.com");
            await driver.findElement(By.name("i_understand_1")).click();
            await driver.findElement(By.name("i_understand_2")).click();
            await driver.findElement(By.css(`[value="Create Your Account"]`)).click();

            // Account creation successful page
            await driver.findElement(By.linkText('Click here')).click();

            // Shift report
            await driver.findElement(By.linkText('Control Panel (Selenium Tests)')).click();

            // Company report

            await deleteCurrentCompany(driver);
        });

        it('should allow international phone numbers', async function() {
            await visitPage(driver, "https://dev.titanvolunteers.com/general/sign_up_company");
            await driver.findElement(By.name("manager_email")).sendKeys("[email protected]")
            await driver.findElement(By.name("manager_password")).sendKeys("ToughPassword#1");
            await driver.findElement(By.name("manager_firstname")).sendKeys("Selenium");
            await driver.findElement(By.name("manager_lastname")).sendKeys("Tests");
            await driver.findElement(By.name("manager_phone")).sendKeys("07825654199");
            await driver.findElement(By.name("company_website_url")).sendKeys("www.test.com");
            await driver.findElement(By.name("i_understand_1")).click();
            await driver.findElement(By.name("i_understand_2")).click();
            await driver.findElement(By.css(`[value="Create Your Account"]`)).click();

            // Account creation successful page
            await driver.findElement(By.linkText('Click here')).click();

            // Shift report
            await driver.findElement(By.linkText('Control Panel (Selenium Tests)')).click();

            // Company report

            await deleteCurrentCompany(driver);
        });
    });
});

Thanks for your time.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

This is rough, and paraphrasing, but I think you could benefit from some wrappers around the verbosity of the Selenium API.

I've borrowed here from what is often referred to as the 'Screenplay' design pattern, which was popularised by Serenity/JS, where you can dig more into the design pattern. Whether you want to go the whole hog and adopt a framework probably depends on the scale of the testing task. I think I've outlined below that its actually quite a simple pattern, but I guess the devil is in the details.

In essense, an Actor represents the entity which is 'doing' the work, injected with the tool(s) it needs (in this case the Selenium driver instance). Screenplay encapsulates these in 'Abilities' but here for the sake of simplicity I'm just passing the driver directly.

We then wrap our By.name|css calls in simple functions so that we aren't so reliant on avoiding typos in strings, and abstracting what the specific locator strategy is for a given DOM element (useful if refactoring later).

Each 'action' is also represented by a higher order function, each passed as a separate argument to a function that maps them all into promises and wraps the whole array in a Promise.

Hopefully you can also apply this to the validations, the Screenplay pattern uses the concept of 'Questions' to encapsulate requests (such as the content of a DOM element) which can then be paired with 'Expectations' which perform some validation on the result.

The intention here is to abstract the work to be done from 'how' its performed, so that the test case is much easy to reason.

An alternative is the Page Object Model which creates a helper class 'per' target page but I always found that a lot more boiler plate and not so reusable. If your codebase has good naming conventions, you should be able to see that, for example, locators, actions and questions are reusable across multiple pages.

class Actor {
    constructor(driver) {
        this.driver = driver
    }

    async attemptsTo(...actions) {
        return Promise.all(actions.map(action => action(this.driver)))
    }
}

function visit(url) {
    return (driver) => visitPage(driver, "https://dev.titanvolunteers.com/general/sign_up_company")
}

const managerEmail = () => By.name("manager_email")
const managerPassword = () => By.name("manager_password")
const managerFirstname = () => By.name("manager_firstname")
const managerLastname = () => By.name("manager_lastname")
const managerPhone = () => By.name("manager_phone")
const companyWebsiteUrl = () => By.name("company_website_url")
const iUnderstand1 = () => By.name("i_understand_1")
const iUnderstand2 = () => By.name("i_understand_2")
const createYourAccount = () => By.css(`[value="Create Your Account"]`)

const sendKeys = (locator, keys) => (driver) => driver.findElement(locator).sendKeys(keys)
const click = (locator) => (driver) => driver.findElement(locator).click()

(async () => {

    // Imagine this is the body of a test

    const driver = {}   // whatever setup this needs

    const actor = new Actor(driver)

    // Depending on your test framework you might want to return the 
    // Promise, await on it, perform some assertions etc, as you see fit.

    await actor.attemptsTo(
        visit("https://dev.titanvolunteers.com/general/sign_up_company"),
        sendKeys(managerEmail(), "[email protected]"),
        sendKeys(managerPassword(), "ToughPassword#1"),
        sendKeys(managerFirstname(), "Selenium"),
        sendKeys(managerLastname(), "Tests"),
        sendKeys(managerPhone(), "07825654199"),
        sendKeys(companyWebsiteUrl(),"www.test.com"),
        click(iUnderstand1()),
        click(iUnderstand2()),
        click(createYourAccount()),
    )
})()
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.