mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-02-25 05:44:13 -05:00
test: fix join flow test flakiness
- wait for navigation to complete so we don't accidentally grab an element on the page we're navigating away from, leading to a stale reference - wait for focus to move into a selected input field so we don't accidentally grab the `validation-message` attached to the old input box (the "username" box is selected by default on the join flow page)
This commit is contained in:
parent
8682c4a9f4
commit
06a65d60e6
3 changed files with 80 additions and 51 deletions
|
@ -5,7 +5,8 @@ const SeleniumHelper = require('./selenium-helpers.js');
|
|||
const {
|
||||
clickText,
|
||||
buildDriver,
|
||||
findText
|
||||
findText,
|
||||
waitUntilDocumentReady
|
||||
} = new SeleniumHelper();
|
||||
|
||||
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
|
@ -28,14 +29,9 @@ describe('www-integration footer links', () => {
|
|||
|
||||
// ==== About Scratch column ====
|
||||
|
||||
const pageLoadComplete = () =>
|
||||
driver.wait(async () => {
|
||||
return await driver.executeScript('return document.readyState;') === 'complete';
|
||||
});
|
||||
|
||||
test('click About Scratch link', async () => {
|
||||
await clickText('About Scratch');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/about\/?$/);
|
||||
|
@ -43,7 +39,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click For Parents link', async () => {
|
||||
await clickText('For Parents');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/parents\/?$/);
|
||||
|
@ -51,7 +47,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click For Educators link', async () => {
|
||||
await clickText('For Educators');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/educators\/?$/);
|
||||
|
@ -59,7 +55,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click For Developers link', async () => {
|
||||
await clickText('For Developers');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/developers\/?$/);
|
||||
|
@ -69,7 +65,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click Community Guidelines link', async () => {
|
||||
await clickText('Community Guidelines');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/community_guidelines\/?$/);
|
||||
|
@ -77,7 +73,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click Discussion Forums link', async () => {
|
||||
await clickText('Discussion Forums');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/discuss\/?$/);
|
||||
|
@ -85,7 +81,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click Statistics link', async () => {
|
||||
await clickText('Statistics');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/statistics\/?$/);
|
||||
|
@ -95,7 +91,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click Ideas link', async () => {
|
||||
await clickText('Ideas');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/ideas\/?$/);
|
||||
|
@ -103,7 +99,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click FAQ link', async () => {
|
||||
await clickText('FAQ');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/faq\/?$/);
|
||||
|
@ -111,7 +107,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click Download link', async () => {
|
||||
await clickText('Download');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/download\/?$/);
|
||||
|
@ -119,7 +115,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click Contact Us link', async () => {
|
||||
await clickText('Contact Us');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/contact-us\/?$/);
|
||||
|
@ -129,7 +125,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click Terms of Use link', async () => {
|
||||
await clickText('Terms of Use');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/terms_of_use\/?$/);
|
||||
|
@ -137,7 +133,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click Privacy Policy link', async () => {
|
||||
await clickText('Privacy Policy');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/privacy_policy\/?$/);
|
||||
|
@ -145,7 +141,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click Cookies link', async () => {
|
||||
await clickText('Cookies');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/cookies\/?$/);
|
||||
|
@ -158,7 +154,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click DMCA link', async () => {
|
||||
await clickText('DMCA');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/DMCA\/?$/);
|
||||
|
@ -168,7 +164,7 @@ describe('www-integration footer links', () => {
|
|||
|
||||
test('click Scratch Conference link', async () => {
|
||||
await clickText('Scratch Conference');
|
||||
await pageLoadComplete();
|
||||
await waitUntilDocumentReady();
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/scratch-conference\/?$/);
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
const SeleniumHelper = require('./selenium-helpers.js');
|
||||
|
||||
const {
|
||||
findByXpath,
|
||||
buildDriver,
|
||||
clickXpath,
|
||||
buildDriver
|
||||
findByXpath,
|
||||
navigate,
|
||||
waitUntilDocumentReady
|
||||
} = new SeleniumHelper();
|
||||
|
||||
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
|
@ -18,72 +20,77 @@ let driver;
|
|||
describe('www-integration join flow', () => {
|
||||
beforeAll(async () => {
|
||||
driver = await buildDriver('www-integration join flow');
|
||||
await driver.get(rootUrl);
|
||||
});
|
||||
|
||||
afterAll(async () => await driver.quit());
|
||||
|
||||
beforeEach(async () => {
|
||||
await driver.get(rootUrl);
|
||||
await clickXpath('//a[@class="registrationLink"]');
|
||||
await navigate(rootUrl); // navigate to home page
|
||||
await clickXpath('//a[@class="registrationLink"]'); // navigate to join page
|
||||
await waitUntilDocumentReady();
|
||||
});
|
||||
|
||||
test('click Join opens join modal', async () => {
|
||||
let joinModal = await findByXpath('//div[@class = "join-flow-outer-content"]');
|
||||
let modalVisible = await joinModal.isDisplayed();
|
||||
await expect(modalVisible).toBe(true);
|
||||
expect(modalVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('username validation message appears', async () => {
|
||||
await clickXpath('//input[contains(@name, "username")]');
|
||||
const clickedInput = await clickXpath('//input[contains(@name, "username")]');
|
||||
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', clickedInput));
|
||||
let message = await findByXpath('//div[contains(@class, "validation-message")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toEqual('Don\'t use your real name');
|
||||
|
||||
expect(messageText).toEqual('Don\'t use your real name');
|
||||
});
|
||||
|
||||
test('password validation message appears', async () => {
|
||||
await clickXpath('//input[contains(@name, "password")]');
|
||||
const clickedInput = await clickXpath('//input[contains(@name, "password")]');
|
||||
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', clickedInput));
|
||||
let message = await findByXpath('//div[contains(@class, "validation-message")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toContain('Write it down so you remember.');
|
||||
expect(messageText).toContain('Write it down so you remember.');
|
||||
});
|
||||
|
||||
test('password validation message appears', async () => {
|
||||
await clickXpath('//input[contains(@name, "passwordConfirm")]');
|
||||
test('passwordConfirm validation message appears', async () => {
|
||||
const clickedInput = await clickXpath('//input[contains(@name, "passwordConfirm")]');
|
||||
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', clickedInput));
|
||||
let message = await findByXpath('//div[contains(@class, "validation-message")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toEqual('Type password again');
|
||||
expect(messageText).toEqual('Type password again');
|
||||
});
|
||||
|
||||
test('username validation: too short', async () => {
|
||||
let textInput = await findByXpath('//input[contains(@name, "username")]');
|
||||
await textInput.click();
|
||||
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', textInput));
|
||||
await textInput.sendKeys('ab');
|
||||
await clickXpath('//div[@class = "join-flow-outer-content"]');
|
||||
let message = await findByXpath('//div[contains(@class, "validation-error")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toContain('Must be 3 letters or longer');
|
||||
expect(messageText).toContain('Must be 3 letters or longer');
|
||||
});
|
||||
|
||||
test('username validation: username taken', async () => {
|
||||
let textInput = await findByXpath('//input[contains(@name, "username")]');
|
||||
await textInput.click();
|
||||
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', textInput));
|
||||
await textInput.sendKeys(takenUsername);
|
||||
await clickXpath('//div[@class = "join-flow-outer-content"]');
|
||||
let message = await findByXpath('//div[contains(@class, "validation-error")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toContain('Username taken.');
|
||||
expect(messageText).toContain('Username taken.');
|
||||
});
|
||||
|
||||
test('username validation: bad word', async () => {
|
||||
let textInput = await findByXpath('//input[contains(@name, "username")]');
|
||||
await textInput.click();
|
||||
await driver.wait(() => driver.executeScript('return document.activeElement == arguments[0]', textInput));
|
||||
// Should be caught by the filter
|
||||
await textInput.sendKeys('xxxxxxxxx');
|
||||
await clickXpath('//div[@class = "join-flow-outer-content"]');
|
||||
let message = await findByXpath('//div[contains(@class, "validation-error")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toContain('Username not allowed');
|
||||
expect(messageText).toContain('Username not allowed');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,8 +33,10 @@ class SeleniumHelper {
|
|||
'getLogs',
|
||||
'getSauceDriver',
|
||||
'isSignedIn',
|
||||
'navigate',
|
||||
'signIn',
|
||||
'urlMatches',
|
||||
'waitUntilDocumentReady',
|
||||
'waitUntilGone'
|
||||
]);
|
||||
}
|
||||
|
@ -108,6 +110,25 @@ class SeleniumHelper {
|
|||
return Key[keyName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the document is ready (i.e. the document.readyState is 'complete')
|
||||
* @returns {Promise} A promise that resolves when the document is ready
|
||||
*/
|
||||
waitUntilDocumentReady () {
|
||||
return this.driver.wait(async () =>
|
||||
await this.driver.executeScript('return document.readyState;') === 'complete'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the given URL and wait until the document is ready
|
||||
* @param {string} url The URL to navigate to.
|
||||
* @returns {Promise} A promise that resolves when the document is ready
|
||||
*/
|
||||
navigate (url) {
|
||||
return this.driver.get(url).then(() => this.waitUntilDocumentReady());
|
||||
}
|
||||
|
||||
findByXpath (xpath, timeoutMessage = `findByXpath timed out for path: ${xpath}`) {
|
||||
return this.driver.wait(until.elementLocated(By.xpath(xpath)), DEFAULT_TIMEOUT_MILLISECONDS, timeoutMessage)
|
||||
.then(el => (
|
||||
|
@ -122,31 +143,35 @@ class SeleniumHelper {
|
|||
|
||||
async waitUntilClickable (xpath, allowScrolling = true) {
|
||||
return await this.driver.wait(async () => {
|
||||
let elementAtPath = await this.findByXpath(xpath);
|
||||
const elementAtPath = await this.findByXpath(xpath);
|
||||
if (!elementAtPath) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (allowScrolling) {
|
||||
await this.driver.executeScript(
|
||||
const info = await this.driver.executeScript(
|
||||
`
|
||||
const info = {};
|
||||
const element = arguments[0];
|
||||
const boundingRect = element.getBoundingClientRect();
|
||||
boundingRect.windowWidth = window.innerWidth;
|
||||
boundingRect.windowHeight = window.innerHeight;
|
||||
if (boundingRect.top < 0 || boundingRect.bottom > window.innerHeight ||
|
||||
boundingRect.left < 0 || boundingRect.right > window.innerWidth)
|
||||
{
|
||||
boundingRect.scrollIntoView = true;
|
||||
element.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block:'nearest',
|
||||
inline: 'nearest'
|
||||
});
|
||||
info.didScroll = true;
|
||||
}
|
||||
return info;
|
||||
`,
|
||||
elementAtPath
|
||||
);
|
||||
if (info.didScroll) {
|
||||
// try again after the scroll completes
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const elementAtPoint = await this.driver.executeScript(
|
||||
|
@ -160,7 +185,7 @@ class SeleniumHelper {
|
|||
elementAtPath
|
||||
);
|
||||
if (!elementAtPoint) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
// If we ask to click on a button and Selenium finds an image on the button, or vice versa, that's OK.
|
||||
// It doesn't have to be an exact match.
|
||||
|
@ -170,21 +195,22 @@ class SeleniumHelper {
|
|||
elementAtPoint
|
||||
);
|
||||
if (!match) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (!await elementAtPath.isDisplayed()) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (!await elementAtPath.isEnabled()) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
return elementAtPath;
|
||||
});
|
||||
}
|
||||
|
||||
async clickXpath (xpath) {
|
||||
const element = await this.waitUntilClickable(xpath);
|
||||
async clickXpath (xpath, allowScrolling = true) {
|
||||
const element = await this.waitUntilClickable(xpath, allowScrolling);
|
||||
element.click();
|
||||
return element;
|
||||
}
|
||||
|
||||
clickText (text) {
|
||||
|
|
Loading…
Reference in a new issue