Commit 26ceac57 authored by Oskar Gustafsson's avatar Oskar Gustafsson Committed by Sam Saccone

Improve test suite robustness. (#1646)

..by waiting for DOM states instead of checking them only once.
parent aa9b051b
'use strict';
var webdriver = require('selenium-webdriver');
var until = require('selenium-webdriver/lib/until');
var idSelectors = true;
var classOrId = idSelectors ? '#' : '.';
var DEFAULT_TIMEOUT = 3000;
var REMOVED_TIMEOUT = 100;
var REMOVE_TEXT_KEY_SEQ = Array(51).join(webdriver.Key.BACK_SPACE + webdriver.Key.DELETE);
// Unique symbols
var ELEMENT_MISSING = Object.freeze({});
var ITEM_HIDDEN_OR_REMOVED = Object.freeze({});
module.exports = function Page(browser) {
// ----------------- utility methods
// CSS ELEMENT SELECTORS
this.tryFindByXpath = function (xpath) {
return browser.findElements(webdriver.By.xpath(xpath));
};
this.getMainSectionCss = function () { return classOrId + 'main'; };
this.findByXpath = function (xpath) {
return browser.findElement(webdriver.By.xpath(xpath));
};
this.getFooterSectionCss = function () { return 'footer' + classOrId + 'footer'; };
this.getTodoListXpath = function () {
return idSelectors ? '//ul[@id="todo-list"]' : '//ul[contains(@class, "todo-list")]';
};
this.getClearCompletedButtonCss = function () { return 'button' + classOrId + 'clear-completed'; };
this.getMainSectionXpath = function () {
return idSelectors ? '//section[@id="main"]' : '//section[contains(@class, "main")]';
};
this.getNewInputCss = function () { return 'input' + classOrId + 'new-todo'; };
this.getFooterSectionXpath = function () {
return idSelectors ? '//footer[@id="footer"]' : '//footer[contains(@class, "footer")]';
};
this.getToggleAllCss = function () { return 'input' + classOrId + 'toggle-all'; };
this.getCompletedButtonXpath = function () {
return idSelectors ? '//button[@id="clear-completed"]' : '//button[contains(@class, "clear-completed")]';
};
this.getItemCountCss = function () { return 'span' + classOrId + 'todo-count'; };
this.getNewInputXpath = function () {
return idSelectors ? '//input[@id="new-todo"]' : '//input[contains(@class,"new-todo")]';
};
this.getFilterCss = function (index) { return classOrId + 'filters li:nth-of-type(' + (index + 1) + ') a'; };
this.getToggleAllXpath = function () {
return idSelectors ? '//input[@id="toggle-all"]' : '//input[contains(@class,"toggle-all")]';
};
this.getSelectedFilterCss = function (index) { return this.getFilterCss(index) + '.selected'; };
this.getCountXpath = function () {
return idSelectors ? '//span[@id="todo-count"]' : '//span[contains(@class, "todo-count")]';
};
this.getFilterAllCss = function () { return this.getFilterCss(0); };
this.getFiltersElementXpath = function () {
return idSelectors ? '//*[@id="filters"]' : '//*[contains(@class, "filters")]';
};
this.getFilterActiveCss = function () { return this.getFilterCss(1); };
this.getFilterXpathByIndex = function (index) {
return this.getFiltersElementXpath() + '/li[' + index + ']/a';
};
this.getFilterCompletedCss = function () { return this.getFilterCss(2); };
this.getSelectedFilterXPathByIndex = function (index) {
return this.getFilterXpathByIndex(index) + '[contains(@class, "selected")]';
};
this.getListCss = function (suffixCss) { return 'ul' + classOrId + 'todo-list' + (suffixCss || ''); };
this.getFilterAllXpath = function () {
return this.getFilterXpathByIndex(1);
this.getListItemCss = function (index, suffixCss, excludeParentSelector) {
suffixCss = (index === undefined ? '' : ':nth-of-type(' + (index + 1) + ')') + (suffixCss || '');
return excludeParentSelector ? 'li' + suffixCss : this.getListCss(' li' + suffixCss);
};
this.getFilterActiveXpath = function () {
return this.getFilterXpathByIndex(2);
};
this.getListItemToggleCss = function (index) { return this.getListItemCss(index, ' input.toggle'); };
this.getFilterCompletedXpath = function () {
return this.getFilterXpathByIndex(3);
};
this.getListItemLabelCss = function (index) { return this.getListItemCss(index, ' label'); };
this.xPathForItemAtIndex = function (index) {
// why is XPath the only language silly enough to be 1-indexed?
return this.getTodoListXpath() + '/li[' + (index + 1) + ']';
};
this.getLastListItemLabelCss = function (index) { return this.getListItemCss(index, ':last-of-type label'); };
// ----------------- navigation methods
this.getListItemInputCss = function (index) { return this.getListItemCss(index, ' input.edit'); };
this.back = function () {
return browser.navigate().back();
this.getEditingListItemInputCss = function () { return this.getListItemCss(undefined, '.editing input.edit'); };
// This CSS selector returns the _last_ element of a list that exactly matches the provided list of completed states
// It is used as a boolean test of the item states
this.getListItemsWithCompletedStatesCss = function (completedStates) {
var suffixCss = ' ' + completedStates.map(function (completed, i) {
return this.getListItemCss(i, completed ? '.completed' : ':not(.completed)', true);
}, this).join(' + ');
return this.getListCss(suffixCss);
};
// ----------------- try / get methods
// PUBLIC SYMBOLS
// unfortunately webdriver does not have a decent API for determining if an
// element exists. The standard approach is to obtain an array of elements
// and test that the length is zero. These methods are used to obtain
// elements which *might* be present in the DOM, hence the try/get name.
this.ITEM_HIDDEN_OR_REMOVED = ITEM_HIDDEN_OR_REMOVED;
this.tryGetMainSectionElement = function () {
return this.tryFindByXpath(this.getMainSectionXpath());
};
// NAVIGATION
this.tryGetFooterElement = function () {
return this.tryFindByXpath(this.getFooterSectionXpath());
this.back = function () {
return browser.navigate().back();
};
this.tryGetClearCompleteButton = function () {
return this.findByXpath(this.getCompletedButtonXpath());
// ELEMENT RETREIVAL
// wait* methods guarantees to return an element, or throw an exception
// get* methods may return nothing at all, or in the case of element lists, an older version of the list
this.getElements = function (css) {
return browser.findElements(webdriver.By.css(css));
};
this.tryGetToggleForItemAtIndex = function (index) {
var xpath = this.xPathForItemAtIndex(index) + '//input[contains(@class,"toggle")]';
return this.findByXpath(xpath);
this.waitForElement = function (css, failMsg, timeout) {
return browser.wait(until.elementLocated(webdriver.By.css(css)), timeout || DEFAULT_TIMEOUT, failMsg);
};
this.tryGetItemLabelAtIndex = function (index) {
return this.findByXpath(this.xPathForItemAtIndex(index) + '//label');
this.waitForFocusedElement = function (css, failMsg) {
return this.waitForElement(css + ':focus', failMsg);
};
// ----------------- DOM element access methods
this.getActiveElement = function () {
return browser.switchTo().activeElement();
this.waitForBlurredElement = function (css, failMsg) {
return this.waitForElement(css + ':not(:focus)', failMsg);
};
this.getFocussedTagName = function () {
return this.getActiveElement().getTagName();
this.waitForListItemCount = function (count) {
var self = this;
return browser.wait(function () {
return self.waitForElement(self.getListCss())
.then(function (listElement) {
return listElement.findElements(webdriver.By.css(self.getListItemCss(undefined, undefined, true)));
})
.then(function (listItems) {
return listItems.length === count;
});
}, DEFAULT_TIMEOUT, 'Expected item list to contain ' + count + ' item' + (count === 1 ? '' : 's'));
};
this.getFocussedElementIdOrClass = function () {
return this.getActiveElement()
.getAttribute(idSelectors ? 'id' : 'class');
this.waitForClearCompleteButton = function () {
return this.waitForElement(this.getClearCompletedButtonCss());
};
this.getEditInputForItemAtIndex = function (index) {
var xpath = this.xPathForItemAtIndex(index) + '//input[contains(@class,"edit")]';
return this.findByXpath(xpath);
this.waitForToggleForItem = function (index) {
return this.waitForElement(this.getListItemToggleCss(index));
};
this.getItemInputField = function () {
return this.findByXpath(this.getNewInputXpath());
this.waitForItemLabel = function (index) {
return this.waitForElement(this.getListItemLabelCss(index));
};
this.getMarkAllCompletedCheckBox = function () {
return this.findByXpath(this.getToggleAllXpath());
this.waitForNewItemInputField = function () {
return this.waitForElement(this.getNewInputCss());
};
this.getItemElements = function () {
return this.tryFindByXpath(this.getTodoListXpath() + '/li');
this.waitForMarkAllCompletedCheckBox = function () {
return this.waitForElement(this.getToggleAllCss());
};
this.getNonCompletedItemElements = function () {
return this.tryFindByXpath(this.getTodoListXpath() + '/li[not(contains(@class,"completed"))]');
this.getListItems = function () {
return this.getElements(this.getListItemCss());
};
this.getItemsCountElement = function () {
return this.findByXpath(this.getCountXpath());
this.waitForVisibility = function (shouldBeVisible, css, failMsg) {
if (shouldBeVisible) {
return this.waitForElement(css, failMsg)
.then(function (element) {
return browser.wait(until.elementIsVisible(element), DEFAULT_TIMEOUT, failMsg);
});
} else {
return this.waitForElement(css, undefined, REMOVED_TIMEOUT)
.catch(function () { return ELEMENT_MISSING; })
.then(function (elementOrError) {
return elementOrError === ELEMENT_MISSING ?
ELEMENT_MISSING : // Returning a value will resolve the promise
browser.wait(until.elementIsNotVisible(elementOrError), DEFAULT_TIMEOUT, failMsg);
});
}
};
this.getItemLabelAtIndex = function (index) {
return this.findByXpath(this.xPathForItemAtIndex(index) + '//label');
this.waitForMainSectionRemovedOrEmpty = function () {
return this.waitForElement(this.getMainSectionCss(), undefined, REMOVED_TIMEOUT)
.catch(function () { return ELEMENT_MISSING; })
.then(function (elementOrError) {
return elementOrError === ELEMENT_MISSING ? ELEMENT_MISSING : this.waitForListItemCount(0);
}.bind(this));
};
this.getItemLabels = function () {
var xpath = this.getTodoListXpath() + '/li//label';
return this.tryFindByXpath(xpath);
this.waitForCheckedStatus = function (shouldBeChecked, failMsg, element) {
var condition = shouldBeChecked ? 'elementIsSelected' : 'elementIsNotSelected';
return browser.wait(until[condition](element), DEFAULT_TIMEOUT, failMsg);
};
this.getVisibleLabelText = function () {
var self = this;
return this.getVisibileLabelIndicies()
.then(function (indicies) {
return webdriver.promise.map(indicies, function (elmIndex) {
var ret;
return browser.wait(function () {
return self.tryGetItemLabelAtIndex(elmIndex).getText()
.then(function (v) {
ret = v;
return true;
})
.thenCatch(function () { return false; });
}, 5000)
.then(function () {
return ret;
});
});
});
this.waitForTextContent = function (text, failMsg, element) {
return browser.wait(until.elementTextIs(element, text), DEFAULT_TIMEOUT, failMsg);
};
this.waitForVisibleElement = function (getElementFn, timeout) {
var foundVisibleElement;
timeout = timeout || 500;
// PAGE ACTIONS
return browser.wait(function () {
foundVisibleElement = getElementFn();
return foundVisibleElement.isDisplayed();
}, timeout)
.then(function () {
return foundVisibleElement;
})
.thenCatch(function (err) {
return false;
});
}
this.getVisibileLabelIndicies = function () {
var self = this;
return this.getItemLabels()
.then(function (elms) {
return elms.map(function (elm, i) {
return i;
});
})
.then(function (elms) {
return webdriver.promise.filter(elms, function (elmIndex) {
return self.waitForVisibleElement(function () {
return self.tryGetItemLabelAtIndex(elmIndex);
});
this.ensureAppIsVisibleAndLoaded = function () {
return this.waitForVisibility(false, this.getFooterSectionCss(), 'Footer is not hidden') // Footer hidden -> app is active
.then(this.waitForElement.bind(this, '.new-todo, #new-todo', 'Could not find new todo input field', undefined))
.then(function (newTodoElement) {
return newTodoElement.getAttribute('id');
})
.then(function (newTodoElementId) {
if (newTodoElementId === 'new-todo') { return; }
idSelectors = false;
classOrId = idSelectors ? '#' : '.';
});
});
};
// ----------------- page actions
this.ensureAppIsVisible = function () {
var self = this;
return browser.wait(function () {
// try to find main element by ID
return browser.isElementPresent(webdriver.By.css('.new-todo'))
.then(function (foundByClass) {
if (foundByClass) {
idSelectors = false;
return true;
}
// try to find main element by CSS class
return browser.isElementPresent(webdriver.By.css('#new-todo'));
});
}, 5000)
.then(function (hasFoundNewTodoElement) {
if (!hasFoundNewTodoElement) {
throw new Error('Unable to find application, did you start your local server?');
}
});
};
this.clickMarkAllCompletedCheckBox = function () {
return this.getMarkAllCompletedCheckBox().then(function (checkbox) {
return checkbox.click();
});
return this.waitForMarkAllCompletedCheckBox().click();
};
this.clickClearCompleteButton = function () {
var self = this;
return self.waitForVisibleElement(function () {
return self.tryGetClearCompleteButton();
})
.then(function (clearCompleteButton) {
return clearCompleteButton.click();
});
return this.waitForVisibility(true, this.getClearCompletedButtonCss(), 'Expected clear completed items button to be visible')
.then(function (clearCompleteButton) {
clearCompleteButton.click();
});
};
this.enterItem = function (itemText) {
var self = this;
return browser.wait(function () {
var textField;
return self.getItemInputField().then(function (itemInputField) {
textField = itemInputField;
return textField.sendKeys(itemText, webdriver.Key.ENTER);
var nItems;
return self.getListItems()
.then(function (items) {
nItems = items.length;
})
.then(function () { return self.getVisibleLabelText(); })
.then(function (labels) {
if (labels.indexOf(itemText.trim()) >= 0) {
return true;
}
return textField.clear().then(function () {
return false;
});
});
}, 5000);
.then(this.waitForNewItemInputField.bind(this))
.then(function (newItemInput) {
return newItemInput.sendKeys(itemText).then(function () { return newItemInput; });
})
.then(function (newItemInput) {
return browser.wait(function () {
// Hit Enter repeatedly until the text goes away
return newItemInput.sendKeys(webdriver.Key.ENTER)
.then(newItemInput.getAttribute.bind(newItemInput, 'value'))
.then(function (newItemInputValue) {
return newItemInputValue.length === 0;
});
}, DEFAULT_TIMEOUT);
})
.then(function () {
return self.waitForElement(self.getLastListItemLabelCss(nItems));
})
.then(this.waitForTextContent.bind(this, itemText.trim(), 'Expected new item label to read ' + itemText.trim()));
};
this.toggleItemAtIndex = function (index) {
return this.tryGetToggleForItemAtIndex(index).click();
return this.waitForToggleForItem(index).click();
};
this.editItemAtIndex = function (index, itemText) {
return this.getEditInputForItemAtIndex(index)
.then(function (itemEditField) {
// send 50 delete keypresses, just to be sure the item text is deleted
var deleteKeyPresses = '';
for (var i = 0; i < 50; i++) {
deleteKeyPresses += webdriver.Key.BACK_SPACE;
deleteKeyPresses += webdriver.Key.DELETE;
}
itemEditField.sendKeys(deleteKeyPresses);
// update the item with the new text.
itemEditField.sendKeys(itemText);
});
return this.waitForElement(this.getListItemInputCss(index))
.then(function (itemEditField) {
return itemEditField.sendKeys(REMOVE_TEXT_KEY_SEQ, itemText);
});
};
this.doubleClickItemAtIndex = function (index) {
return this.getItemLabelAtIndex(index).then(function (itemLabel) {
// double click is not 'natively' supported, so we need to send the
// event direct to the element see:
return this.waitForItemLabel(index).then(function (itemLabel) {
// double click is not 'natively' supported, so we need to send the event direct to the element, see:
// jscs:disable
// http://stackoverflow.com/questions/3982442/selenium-2-webdriver-how-to-double-click-a-table-row-which-opens-a-new-window
// jscs:enable
browser.executeScript('var evt = document.createEvent("MouseEvents");' +
return browser.executeScript('var evt = document.createEvent("MouseEvents");' +
'evt.initMouseEvent("dblclick",true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0,null);' +
'arguments[0].dispatchEvent(evt);', itemLabel);
});
};
this.filterBy = function (selectorFn) {
var self = this;
return browser.wait(function () {
return self.findByXpath(selectorFn()).click()
.then(function () {
return self.findByXpath(selectorFn()).getAttribute('class');
})
.then(function (klass) {
return klass.indexOf('selected') !== -1;
});
}, 5000);
this.filterBy = function (filterCss) {
return this.waitForElement(filterCss)
.click()
.then(this.waitForElement.bind(this, filterCss + '.selected', undefined, undefined));
};
this.filterByActiveItems = function () {
return this.filterBy(this.getFilterActiveXpath.bind(this));
return this.filterBy(this.getFilterActiveCss());
};
this.filterByCompletedItems = function () {
return this.filterBy(this.getFilterCompletedXpath.bind(this));
return this.filterBy(this.getFilterCompletedCss());
};
this.filterByAllItems = function () {
return this.filterBy(this.getFilterAllXpath.bind(this));
return this.filterBy(this.getFilterAllCss());
};
};
'use strict';
var webdriver = require('selenium-webdriver');
var Page = require('./page');
module.exports = function PageLaxMode(browser) {
Page.apply(this, [browser]);
module.exports = function PageLaxMode() {
Page.apply(this, arguments);
this.tryGetMainSectionElement = function () {
return this.tryFindByXpath('//section//section');
this.getMainSectionCss = function () {
return 'section section';
};
this.tryGetFooterElement = function () {
return this.tryFindByXpath('//section//footer');
this.getFooterSectionCss = function () {
return 'section footer';
};
this.getTodoListXpath = function () {
return '(//section/ul | //section/div/ul | //ul[@id="todo-list"])';
this.getListCss = function (suffixCss) {
return [
'section > ul',
'section > div > ul',
'ul#todo-list',
].map(function (listCss) {
return listCss + (suffixCss || '');
}).join(', ');
};
this.getMarkAllCompletedCheckBox = function () {
var xpath = '(//section/input[@type="checkbox"] | //section/*/input[@type="checkbox"] | //input[@id="toggle-all"])';
return browser.findElement(webdriver.By.xpath(xpath));
this.getToggleAllCss = function () {
return [
'section > input[type="checkbox"]',
'section > * > input[type="checkbox"]',
'input#toggle-all',
].join(', ');
};
this.tryGetClearCompleteButton = function () {
var xpath = '(//footer/button | //footer/*/button | //button[@id="clear-completed"])';
return browser.findElements(webdriver.By.xpath(xpath));
this.getClearCompletedButtonCss = function () {
return [
'footer > button',
'footer > * > button',
'button#clear-completed',
].join(', ');
};
this.getItemsCountElement = function () {
var xpath = '(//footer/span | //footer/*/span)';
return browser.findElement(webdriver.By.xpath(xpath));
this.getItemCountCss = function () {
return [
'footer > span',
'footer > * > span',
].join(', ');
};
this.getFilterElements = function () {
return browser.findElements(webdriver.By.xpath('//footer//ul//a'));
this.getFilterCss = function (index) {
return 'footer ul li:nth-child(' + (index + 1) + ') a';
};
this.getItemInputField = function () {
// allow a more generic method for locating the text getItemInputField
var xpath = '(//header/input | //header/*/input | //input[@id="new-todo"])';
return browser.findElement(webdriver.By.xpath(xpath));
this.getNewInputCss = function () {
return [
'header > input',
'header > * > input',
'input#new-todo',
].join(', ');
};
this.tryGetToggleForItemAtIndex = function (index) {
// the specification dictates that the checkbox should have the 'toggle' CSS class. Some implementations deviate from
// this, hence in lax mode we simply look for any checkboxes within the specified 'li'.
var xpath = this.xPathForItemAtIndex(index) + '//input[@type="checkbox"]';
return browser.findElements(webdriver.By.xpath(xpath));
this.getListItemToggleCss = function (index) {
return this.getListItemCss(index, ' input[type="checkbox"]');
};
this.getEditInputForItemAtIndex = function (index) {
// the specification dictates that the input element that allows the user to edit a todo item should have a CSS
// class of 'edit'. In lax mode, we also look for an input of type 'text'.
var xpath = '(' + this.xPathForItemAtIndex(index) + '//input[@type="text"]' + '|' +
this.xPathForItemAtIndex(index) + '//input[contains(@class,"edit")]' + ')';
return browser.findElement(webdriver.By.xpath(xpath));
this.getListItemInputCss = function (index) {
return [
this.getListItemCss(index, ' input.edit'),
this.getListItemCss(index, ' input[type="text"]'),
].join(', ');
};
};
......@@ -8,7 +8,9 @@ var PageLaxMode = require('./pageLaxMode');
var TestOperations = require('./testOperations');
module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMode, browserName) {
test.describe('TodoMVC - ' + frameworkName, function () {
var TODO_ITEM_ONE = 'buy some cheese';
var TODO_ITEM_TWO = 'feed the cat';
var TODO_ITEM_THREE = 'book a doctors appointment';
......@@ -22,7 +24,7 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
.then(function () {
if (done instanceof Function) {
done();
};
}
});
}
......@@ -44,11 +46,11 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
page = laxMode ? new PageLaxMode(browser) : new Page(browser);
testOps = new TestOperations(page);
return page.ensureAppIsVisible()
return page.ensureAppIsVisibleAndLoaded()
.then(function () {
if (done instanceof Function) {
done();
};
}
});
}
......@@ -67,30 +69,15 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
return browser
.quit()
.then(function () {
if (done instanceof Function) {
done();
};
if (done instanceof Function) { done(); }
});
}
if (speedMode) {
test.before(launchBrowser);
test.after(closeBrowser);
test.beforeEach(function (done) {
return page.getItemElements()
.then(function (items) {
if (items.length == 0) { return; }
// find any items that are not complete
page.getNonCompletedItemElements()
.then(function (nonCompleteItems) {
if (nonCompleteItems.length > 0) {
return page.clickMarkAllCompletedCheckBox();
}
})
return page.clickClearCompleteButton();
})
test.afterEach(function (done) {
return browser.executeScript('window.localStorage && localStorage.clear(); location.reload(true);')
.then(function () { done(); });
});
} else {
......@@ -99,38 +86,43 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
printCapturedLogs()
.then(function () {
return closeBrowser(done);
})
});
});
}
test.describe('When page is initially opened', function () {
test.it('should focus on the todo input field', function (done) {
testOps.assertFocussedElement('new-todo')
testOps.assertNewInputFocused()
.then(function () { done(); });
});
});
test.describe('No Todos', function () {
test.it('should hide #main and #footer', function (done) {
testOps.assertItemCount(0);
testOps.assertMainSectionIsHidden();
testOps.assertFooterIsHidden()
testOps.assertMainSectionVisibility(false);
testOps.assertFooterVisibility(false)
.then(function () { done(); });
});
});
test.describe('New Todo', function () {
test.it('should allow me to add todo items', function (done) {
page.enterItem(TODO_ITEM_ONE);
testOps.assertItems([TODO_ITEM_ONE]);
testOps.assertItems([ TODO_ITEM_ONE ]);
page.enterItem(TODO_ITEM_TWO);
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO])
testOps.assertItems([ TODO_ITEM_ONE, TODO_ITEM_TWO ])
.then(function () { done(); });
});
test.it('should clear text input field when an item is added', function (done) {
page.enterItem(TODO_ITEM_ONE);
testOps.assertItemInputFieldText('')
testOps.assertNewItemInputFieldText('')
.then(function () { done(); });
});
......@@ -151,20 +143,20 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
test.it('should show #main and #footer when items added', function (done) {
page.enterItem(TODO_ITEM_ONE);
testOps.assertMainSectionIsVisible();
testOps.assertFooterIsVisible()
testOps.assertMainSectionVisibility(true);
testOps.assertFooterVisibility(true)
.then(function () { done(); });
});
});
test.describe('Mark all as completed', function () {
test.beforeEach(createStandardItems);
test.it('should allow me to mark all items as completed', function (done) {
page.clickMarkAllCompletedCheckBox();
testOps.assertItemAtIndexIsCompleted(0);
testOps.assertItemAtIndexIsCompleted(1);
testOps.assertItemAtIndexIsCompleted(2)
testOps.assertItemCompletedStates([ true, true, true ])
.then(function () { done(); });
});
......@@ -175,7 +167,7 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
page.toggleItemAtIndex(2);
// ensure checkall is in the correct state
testOps.assertCompleteAllIsChecked()
testOps.assertCompleteAllCheckedStatus(true)
.then(function () { done(); });
});
......@@ -183,39 +175,37 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
page.clickMarkAllCompletedCheckBox();
page.clickMarkAllCompletedCheckBox();
testOps.assertItemAtIndexIsNotCompleted(0);
testOps.assertItemAtIndexIsNotCompleted(1);
testOps.assertItemAtIndexIsNotCompleted(2)
testOps.assertItemCompletedStates([ false, false, false ])
.then(function () { done(); });
});
test.it('complete all checkbox should update state when items are completed / cleared', function (done) {
page.clickMarkAllCompletedCheckBox();
testOps.assertCompleteAllIsChecked();
testOps.assertCompleteAllCheckedStatus(true);
// all items are complete, now mark one as not-complete
page.toggleItemAtIndex(0);
testOps.assertCompleteAllIsClear();
testOps.assertCompleteAllCheckedStatus(false);
// now mark as complete, so that once again all items are completed
page.toggleItemAtIndex(0);
testOps.assertCompleteAllIsChecked()
testOps.assertCompleteAllCheckedStatus(true)
.then(function () { done(); });
});
});
test.describe('Item', function () {
test.it('should allow me to mark items as complete', function (done) {
page.enterItem(TODO_ITEM_ONE);
page.enterItem(TODO_ITEM_TWO);
page.toggleItemAtIndex(0);
testOps.assertItemAtIndexIsCompleted(0);
testOps.assertItemAtIndexIsNotCompleted(1);
testOps.assertItemCompletedStates([ true, false ]);
page.toggleItemAtIndex(1);
testOps.assertItemAtIndexIsCompleted(0);
testOps.assertItemAtIndexIsCompleted(1)
testOps.assertItemCompletedStates([ true, true ])
.then(function () { done(); });
});
......@@ -224,17 +214,17 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
page.enterItem(TODO_ITEM_TWO);
page.toggleItemAtIndex(0);
testOps.assertItemAtIndexIsCompleted(0);
testOps.assertItemAtIndexIsNotCompleted(1);
testOps.assertItemCompletedStates([ true, false ]);
page.toggleItemAtIndex(0);
testOps.assertItemAtIndexIsNotCompleted(0);
testOps.assertItemAtIndexIsNotCompleted(1)
testOps.assertItemCompletedStates([ false, false ])
.then(function () { done(); });
});
});
test.describe('Editing', function (done) {
test.describe('Editing', function () {
test.beforeEach(function (done) {
createStandardItems();
page.doubleClickItemAtIndex(1)
......@@ -242,8 +232,8 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
});
test.it('should focus the input', function (done) {
testOps.assertInputFocused();
testOps.assertNewInputNotFocused()
testOps.assertItemInputFocused();
testOps.assertNewInputBlurred() // Unnecessary? The HTML spec dictates that only one element can be focused.
.then(function () { done(); });
});
......@@ -255,7 +245,7 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
test.it('should save edits on enter', function (done) {
page.editItemAtIndex(1, 'buy some sausages' + webdriver.Key.ENTER);
testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE])
testOps.assertItems([ TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE ])
.then(function () { done(); });
});
......@@ -263,30 +253,32 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
page.editItemAtIndex(1, 'buy some sausages');
// click a toggle button so that the blur() event is fired
page.toggleItemAtIndex(0);
testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE])
testOps.assertItems([ TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE ])
.then(function () { done(); });
});
test.it('should trim entered text', function (done) {
page.editItemAtIndex(1, ' buy some sausages ' + webdriver.Key.ENTER);
testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE])
testOps.assertItems([ TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE ])
.then(function () { done(); });
});
test.it('should remove the item if an empty text string was entered', function (done) {
page.editItemAtIndex(1, webdriver.Key.ENTER);
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE])
testOps.assertItems([ TODO_ITEM_ONE, TODO_ITEM_THREE ])
.then(function () { done(); });
});
test.it('should cancel edits on escape', function (done) {
page.editItemAtIndex(1, 'foo' + webdriver.Key.ESCAPE);
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE])
testOps.assertItems([ TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE ])
.then(function () { done(); });
});
});
test.describe('Counter', function () {
test.it('should display the current number of todo items', function (done) {
page.enterItem(TODO_ITEM_ONE);
testOps.assertItemCountText('1 item left');
......@@ -294,9 +286,11 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
testOps.assertItemCountText('2 items left')
.then(function () { done(); });
});
});
test.describe('Clear completed button', function () {
test.beforeEach(createStandardItems);
test.it('should display the correct text', function (done) {
......@@ -309,32 +303,27 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
page.toggleItemAtIndex(1);
page.clickClearCompleteButton();
testOps.assertItemCount(2);
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE])
testOps.assertItems([ TODO_ITEM_ONE, TODO_ITEM_THREE ])
.then(function () { done(); });
});
test.it('should be hidden when there are no items that are completed', function (done) {
page.toggleItemAtIndex(1);
testOps.assertClearCompleteButtonIsVisible();
testOps.assertClearCompleteButtonVisibility(true);
page.clickClearCompleteButton();
testOps.assertClearCompleteButtonIsHidden()
testOps.assertClearCompleteButtonVisibility(false)
.then(function () { done(); });
});
});
test.describe('Persistence', function () {
test.it('should persist its data', function (done) {
function stateTest() {
// wait until things are visible
browser.wait(function () {
return page.getVisibleLabelText().then(function (labels) {
return labels.length > 0;
});
}, 5000);
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO]);
testOps.assertItemAtIndexIsCompleted(1);
return testOps.assertItemAtIndexIsNotCompleted(0);
testOps.assertItemCount(2);
testOps.assertItems([ TODO_ITEM_ONE, TODO_ITEM_TWO ]);
return testOps.assertItemCompletedStates([ false, true ]);
}
// set up state
......@@ -351,15 +340,17 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
stateTest()
.then(function () { done(); });
});
});
test.describe('Routing', function () {
test.beforeEach(createStandardItems);
test.it('should allow me to display active items', function (done) {
page.toggleItemAtIndex(1);
page.filterByActiveItems();
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE])
testOps.assertItems([ TODO_ITEM_ONE, page.ITEM_HIDDEN_OR_REMOVED, TODO_ITEM_THREE ])
.then(function () { return done(); });
});
......@@ -367,18 +358,18 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
page.toggleItemAtIndex(1);
page.filterByActiveItems();
page.filterByCompletedItems();
testOps.assertItems([TODO_ITEM_TWO]);// should show completed items
testOps.assertItems([ page.ITEM_HIDDEN_OR_REMOVED, TODO_ITEM_TWO ]); // should show completed items
page.back(); // then active items
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE]);
testOps.assertItems([ TODO_ITEM_ONE, page.ITEM_HIDDEN_OR_REMOVED, TODO_ITEM_THREE ]);
page.back(); // then all items
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE])
testOps.assertItems([ TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE ])
.then(function () { done(); });
});
test.it('should allow me to display completed items', function (done) {
page.toggleItemAtIndex(1);
page.filterByCompletedItems();
testOps.assertItems([TODO_ITEM_TWO]);
testOps.assertItems([ page.ITEM_HIDDEN_OR_REMOVED, TODO_ITEM_TWO ]);
page.filterByAllItems() // TODO: why
.then(function () { done(); });
});
......@@ -390,7 +381,7 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
page.filterByActiveItems();
page.filterByCompletedItems();
page.filterByAllItems();
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE])
testOps.assertItems([ TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE ])
.then(function () { done(); });
});
......@@ -403,6 +394,9 @@ module.exports.todoMVCTest = function (frameworkName, baseUrl, speedMode, laxMod
testOps.assertFilterAtIndexIsSelected(2)
.then(function () { done(); });
});
});
});
};
'use strict';
var assert = require('assert');
function TestOperations(page) {
// unfortunately webdriver does not have a decent API for determining if an
// element exists. The standard approach is to obtain an array of elements
// and test that the length is zero. In this case the item is hidden if
// it is either not in the DOM, or is in the DOM but not visible.
function testIsHidden(elements, name) {
if (elements.length === 1) {
elements[0].isDisplayed().then(function (isDisplayed) {
assert(!isDisplayed, 'the ' + name + ' element should be hidden');
});
}
}
function testIsVisible(elements, name) {
assert.equal(elements.length, 1);
return elements[0].isDisplayed()
.then(function (isDisplayed) {
assert(isDisplayed, 'the ' + name + ' element should be displayed');
});
}
this.assertNewInputNotFocused = function () {
return page.getFocussedElementIdOrClass()
.then(function (focussedElementIdOrClass) {
assert.equal(focussedElementIdOrClass.indexOf('new-todo'), -1);
});
this.assertItemInputFocused = function () {
return page.waitForFocusedElement(page.getEditingListItemInputCss(), 'Expected the item input to be focused');
};
this.assertInputFocused = function () {
return page.getFocussedTagName()
.then(function (name) {
assert.equal(name, 'input', 'input does not have focus');
});
this.assertNewInputFocused = function () {
return page.waitForFocusedElement(page.getNewInputCss());
};
this.assertFocussedElement = function (expectedIdentifier) {
return page.getFocussedElementIdOrClass()
.then(function (focusedElementIdentifier) {
var failMsg = 'The focused element did not have the expected class or id "' + expectedIdentifier + '"';
assert.notEqual(focusedElementIdentifier.indexOf(expectedIdentifier), -1, failMsg);
});
};
this.assertClearCompleteButtonIsHidden = function () {
return page.tryGetClearCompleteButton()
.then(function (element) {
return testIsHidden(element, 'clear completed items button');
}, function (_error) {
assert(_error.code === 7, 'error accessing clear completed items button, error: ' + _error.message);
});
};
this.assertClearCompleteButtonIsVisible = function () {
return page.waitForVisibleElement(function () {
return page.tryGetClearCompleteButton();
})
.then(function (clearCompleteButton) {
assert(clearCompleteButton, 'the clear completed items button element should be displayed');
});
this.assertNewInputBlurred = function () {
return page.waitForBlurredElement(page.getNewInputCss());
};
this.assertItemCount = function (itemCount) {
return page.getItemElements()
.then(function (toDoItems) {
assert.equal(toDoItems.length, itemCount,
itemCount + ' items expected in the todo list, ' + toDoItems.length + ' items observed');
});
return itemCount === 0 ?
page.waitForMainSectionRemovedOrEmpty() :
page.waitForListItemCount(itemCount);
};
this.assertClearCompleteButtonText = function (buttonText) {
return page.waitForVisibleElement(function () {
return page.tryGetClearCompleteButton();
})
.then(function (clearCompleteButton) {
return clearCompleteButton.getText();
})
.then(function (text) {
return assert.equal(text, buttonText);
});
return page.waitForClearCompleteButton()
.then(page.waitForTextContent.bind(page, buttonText, 'Expected clear button text to be ' + buttonText));
};
this.assertMainSectionIsHidden = function () {
return page.tryGetMainSectionElement()
.then(function (mainSection) {
return testIsHidden(mainSection, 'main');
});
this.assertClearCompleteButtonVisibility = function (shouldBeVisible) {
var failMsg = 'Expected the clear completed items button to be ' + (shouldBeVisible ? 'visible' : 'hidden');
return page.waitForVisibility(shouldBeVisible, page.getClearCompletedButtonCss(), failMsg);
};
this.assertFooterIsHidden = function () {
return page.tryGetFooterElement()
.then(function (footer) {
return testIsHidden(footer, 'footer');
});
this.assertMainSectionVisibility = function (shouldBeVisible) {
var failMsg = 'Expected main section to be ' + (shouldBeVisible ? 'visible' : 'hidden');
return page.waitForVisibility(shouldBeVisible, page.getMainSectionCss(), failMsg);
};
this.assertMainSectionIsVisible = function () {
return page.tryGetMainSectionElement()
.then(function (mainSection) {
return testIsVisible(mainSection, 'main');
});
this.assertFooterVisibility = function (shouldBeVisible) {
var failMsg = 'Expected footer to be ' + (shouldBeVisible ? 'visible' : 'hidden');
return page.waitForVisibility(shouldBeVisible, page.getFooterSectionCss(), failMsg);
};
//TODO: fishy!
this.assertItemToggleIsHidden = function (index) {
return page.tryGetToggleForItemAtIndex(index)
.then(function (toggleItem) {
return testIsHidden(toggleItem, 'item-toggle');
});
return page.waitForVisibility(false, page.getListItemToggleCss(index), 'Expected the item toggle button to be hidden');
};
this.assertItemLabelIsHidden = function (index) {
return page.tryGetItemLabelAtIndex(index)
.then(function (toggleItem) {
return testIsHidden(toggleItem, 'item-label');
});
};
this.assertFooterIsVisible = function () {
return page.tryGetFooterElement()
.then(function (footer) {
return testIsVisible(footer, 'footer');
});
return page.waitForVisibility(false, page.getListItemLabelCss(index), 'Expected the item label to be hidden');
};
this.assertItemInputFieldText = function (text) {
return page.getItemInputField().getText()
.then(function (inputFieldText) {
assert.equal(inputFieldText, text);
});
this.assertNewItemInputFieldText = function (text) {
return page.waitForNewItemInputField()
.then(page.waitForTextContent.bind(page, text, 'Expected the new item input text field contents to be ' + text));
};
this.assertItemText = function (itemIndex, textToAssert) {
return page.getItemLabelAtIndex(itemIndex).getText()
.then(function (text) {
assert.equal(text, textToAssert,
'A todo item with text \'' + textToAssert + '\' was expected at index ' +
itemIndex + ', the text \'' + text + '\' was observed');
});
this.assertItemText = function (itemIndex, text) {
return page.waitForItemLabel(itemIndex)
.then(page.waitForTextContent.bind(page, text, 'Expected the item label to be ' + text));
};
// tests that the list contains the following items, independant of order
this.assertItems = function (textArray) {
return page.getVisibleLabelText()
.then(function (visibleText) {
assert.deepEqual(visibleText.sort(), textArray.sort());
});
};
this.assertItemCountText = function (textToAssert) {
return page.getItemsCountElement().getText()
.then(function (text) {
assert.equal(text.trim(), textToAssert, 'the item count text was incorrect');
return page.getListItems().then(function (items) {
if (items.length < textArray.length) {
// This means that the framework removes rather than hides list items
textArray = textArray.filter(function (item) { return item !== page.ITEM_HIDDEN_OR_REMOVED; });
}
var ret;
textArray.forEach(function (text, i) {
if (text === page.ITEM_HIDDEN_OR_REMOVED) { return; }
var promise = page.waitForTextContent(text, 'Expected item text to be ' + text, items[i]);
ret = ret ? ret.then(promise) : promise;
});
return ret;
});
};
// tests for the presence of the 'completed' CSS class for the item at the given index
this.assertItemAtIndexIsCompleted = function (index) {
return page.getItemElements()
.then(function (toDoItems) {
return toDoItems[index].getAttribute('class');
})
.then(function (cssClass) {
var failMsg = 'the item at index ' + index + ' should have been marked as completed';
assert(cssClass.indexOf('completed') !== -1, failMsg);
});
this.assertItemCountText = function (text) {
return page.waitForElement(page.getItemCountCss())
.then(page.waitForTextContent.bind(page, text, 'Expected item count text to be ' + text));
};
this.assertItemAtIndexIsNotCompleted = function (index) {
return page.getItemElements()
.then(function (toDoItems) {
return toDoItems[index].getAttribute('class');
})
.then(function (cssClass) {
// the maria implementation uses an 'incompleted' CSS class which is redundant
// TODO: this should really be moved into the pageLaxMode
var failMsg = 'the item at index ' + index + ' should not have been marked as completed';
assert(cssClass.indexOf('completed') === -1 || cssClass.indexOf('incompleted') !== -1, failMsg);
});
this.assertItemCompletedStates = function (completedStates) {
return page.waitForElement(
page.getListItemsWithCompletedStatesCss(completedStates),
'Item completed states were incorrect');
};
this.assertFilterAtIndexIsSelected = function (selectedIndex) {
return page.findByXpath(page.getSelectedFilterXPathByIndex(selectedIndex + 1))
.then(function (elm) {
var failMsg = 'the filter / route at index ' + selectedIndex + ' should have been selected';
assert.notEqual(elm, undefined, failMsg);
});
return page.waitForElement(
page.getSelectedFilterCss(selectedIndex),
'Expexted the filter / route at index ' + selectedIndex + ' to be selected');
};
this.assertCompleteAllIsClear = function () {
return page.getMarkAllCompletedCheckBox()
.then(function (markAllCompleted) {
return markAllCompleted.isSelected();
})
.then(function (isSelected) {
assert(!isSelected, 'the mark-all-completed checkbox should be clear');
});
};
this.assertCompleteAllIsChecked = function () {
return page.getMarkAllCompletedCheckBox()
.then(function (markAllCompleted) {
return markAllCompleted.isSelected();
})
.then(function (isSelected) {
assert(isSelected, 'the mark-all-completed checkbox should be checked');
});
this.assertCompleteAllCheckedStatus = function (shouldBeChecked) {
var failMsg = 'Expected the mark-all-completed checkbox to be ' + shouldBeChecked ? 'checked' : 'unchecked';
return page.waitForMarkAllCompletedCheckBox()
.then(page.waitForCheckedStatus.bind(page, shouldBeChecked, failMsg));
};
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment