From 197461471b2bcd2b60aa33cc091e716c4c98e938 Mon Sep 17 00:00:00 2001 From: Anton Lantukh Date: Tue, 3 May 2022 22:00:33 +0200 Subject: [PATCH] fix(e2e): fix watchlist flaky test (#50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add scroll to solve the problem with playlist click Co-authored-by: “Anton <“alantukh@“jwplayer.com> --- .commitlintrc.js | 5 +- docs/developer-guidelines.md | 1 + test-e2e/tests/watch_history_test.ts | 55 +++++----- test-e2e/utils/steps_file.ts | 151 ++++++++++++++------------- 4 files changed, 111 insertions(+), 101 deletions(-) diff --git a/.commitlintrc.js b/.commitlintrc.js index 1e9a175ec..11d561b44 100644 --- a/.commitlintrc.js +++ b/.commitlintrc.js @@ -2,7 +2,9 @@ module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'scope-enum': [ - 2, 'always', [ + 2, + 'always', + [ 'project', 'home', 'playlist', @@ -19,6 +21,7 @@ module.exports = { 'auth', 'menu', 'payment', + 'e2e', ], ], }, diff --git a/docs/developer-guidelines.md b/docs/developer-guidelines.md index 7ebc2048f..d26e8792f 100644 --- a/docs/developer-guidelines.md +++ b/docs/developer-guidelines.md @@ -69,6 +69,7 @@ The allowed scopes are: - auth - menu - payment +- e2e ### Subject diff --git a/test-e2e/tests/watch_history_test.ts b/test-e2e/tests/watch_history_test.ts index 68083efec..b5ab49386 100644 --- a/test-e2e/tests/watch_history_test.ts +++ b/test-e2e/tests/watch_history_test.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; -import constants from "../utils/constants"; +import constants from '../utils/constants'; import LocatorOrString = CodeceptJS.LocatorOrString; @@ -8,11 +8,11 @@ const videoLength = 231; Feature('watch_history - local'); -Before(({I}) => { +Before(({ I }) => { I.useConfig('test--no-cleeng'); }); -Scenario('I can get my watch progress stored (locally)', async ({ I }) => { +Scenario('I can get my watch progress stored (locally)', async ({ I }) => { I.amOnPage(constants.agent327DetailUrl); I.dontSee('Continue watching'); @@ -30,7 +30,7 @@ Scenario('I can continue watching', async ({ I }) => { await checkElapsed(I, 1, 40); }); -Scenario('I can see my watch history on the Home screen', async({ I })=> { +Scenario('I can see my watch history on the Home screen', async ({ I }) => { I.seeCurrentUrlEquals(constants.baseUrl); I.dontSee('Continue watching'); @@ -45,7 +45,7 @@ Scenario('I can see my watch history on the Home screen', async({ I })=> { }); const xpath = '//*[@data-mediaid="continue-watching"]//*[@aria-label="Play Agent 327"]'; - await checkProgress(I, xpath, 200/videoLength * 100); + await checkProgress(I, xpath, (200 / videoLength) * 100); I.click(xpath); await I.waitForPlayerPlaying('Agent 327'); @@ -77,11 +77,11 @@ Scenario('Video removed from continue watching when finished', async ({ I }) => Feature('watch_history - logged in'); -Before(({I}) => { +Before(({ I }) => { I.useConfig('test--accounts'); }); -Scenario('I can get my watch history when logged in', async({ I })=> { +Scenario('I can get my watch history when logged in', async ({ I }) => { I.login(); await playVideo(I, 0); I.see('Start watching'); @@ -91,10 +91,10 @@ Scenario('I can get my watch history when logged in', async({ I })=> { I.see('Continue watching'); I.dontSee('Start watching'); - await checkProgress(I, '//button[contains(., "Continue watching")]', 80 / videoLength * 100, 5, '_progressRail_', '_progress_'); + await checkProgress(I, '//button[contains(., "Continue watching")]', (80 / videoLength) * 100, 5, '_progressRail_', '_progress_'); }); -Scenario('I can get my watch history stored to my account after login', async({ I })=> { +Scenario('I can get my watch history stored to my account after login', async ({ I }) => { I.amOnPage(constants.agent327DetailUrl); I.dontSee('Continue watching'); I.see('Sign up to start watching'); @@ -103,7 +103,7 @@ Scenario('I can get my watch history stored to my account after login', async({ I.amOnPage(constants.agent327DetailUrl); I.dontSee('Start watching'); I.see('Continue watching'); - await checkProgress(I, '//button[contains(., "Continue watching")]', 80 / videoLength * 100, 5, '_progressRail_', '_progress_'); + await checkProgress(I, '//button[contains(., "Continue watching")]', (80 / videoLength) * 100, 5, '_progressRail_', '_progress_'); I.click('Continue watching'); await I.waitForPlayerPlaying('Agent 327'); @@ -111,12 +111,13 @@ Scenario('I can get my watch history stored to my account after login', async({ await checkElapsed(I, 1, 20); }); -Scenario('I can see my watch history on the Home screen when logged in', async({ I })=> { +Scenario('I can see my watch history on the Home screen when logged in', async ({ I }) => { + const xpath = '//*[@data-mediaid="continue-watching"]//*[@aria-label="Play Agent 327"]'; + I.seeCurrentUrlEquals(constants.baseUrl); I.dontSee('Continue watching'); I.login(); - I.see('Continue watching'); await within('div[data-mediaid="continue-watching"]', async () => { @@ -124,18 +125,17 @@ Scenario('I can see my watch history on the Home screen when logged in', async({ I.see('4 min'); }); - const xpath = '//*[@data-mediaid="continue-watching"]//*[@aria-label="Play Agent 327"]'; - await checkProgress(I, xpath, 80/videoLength * 100); + await checkProgress(I, xpath, (80 / videoLength) * 100); + // Automatic scroll leads to click problems for some reasons + I.scrollTo(xpath); I.click(xpath); await I.waitForPlayerPlaying('Agent 327'); - I.click('video'); await checkElapsed(I, 1, 20); I.seeInCurrentUrl('play=1'); }); - async function playVideo(I: CodeceptJS.I, seekTo: number) { I.amOnPage(constants.agent327DetailUrl + '&play=1'); await I.waitForPlayerPlaying('Agent 327'); @@ -146,18 +146,18 @@ async function playVideo(I: CodeceptJS.I, seekTo: number) { } async function checkProgress( - I: CodeceptJS.I, - context: LocatorOrString, - expectedPercent: number, - tolerance: number = 5, - containerClass: string = '_progressContainer', - barClass: string = '_progressBar' + I: CodeceptJS.I, + context: LocatorOrString, + expectedPercent: number, + tolerance: number = 5, + containerClass: string = '_progressContainer', + barClass: string = '_progressBar', ) { - await within(context, async () => { + return within(context, async () => { const containerWidth = await I.grabCssPropertyFrom(`div[class*=${containerClass}]`, 'width'); const progressWidth = await I.grabCssPropertyFrom(`div[class*=${barClass}]`, 'width'); - const percentage = Math.round(100 * pixelsToNumber(progressWidth) / pixelsToNumber(containerWidth)); + const percentage = Math.round((100 * pixelsToNumber(progressWidth)) / pixelsToNumber(containerWidth)); await I.say(`Checking that percentage ${percentage} is between ${expectedPercent - tolerance} and ${expectedPercent + tolerance}`); @@ -177,13 +177,12 @@ function pixelsToNumber(value: string) { async function checkElapsed(I: CodeceptJS.I, expectedMinutes: number, expectedSeconds: number, bufferSeconds: number = 5) { const elapsed = await I.grabTextFrom('[class*=jw-text-elapsed]'); - const [minutes, seconds] = elapsed.split(':').map(item => Number.parseInt(item)); + const [minutes, seconds] = elapsed.split(':').map((item) => Number.parseInt(item)); assert.strictEqual(minutes, expectedMinutes); if (seconds < expectedSeconds || seconds > expectedSeconds + bufferSeconds) { - assert.fail(`Elapsed time of ${minutes}m ${seconds}s is not within ${bufferSeconds} seconds of ${expectedMinutes}m ${expectedSeconds}s`) + assert.fail(`Elapsed time of ${minutes}m ${seconds}s is not within ${bufferSeconds} seconds of ${expectedMinutes}m ${expectedSeconds}s`); } else { assert.ok(expectedSeconds); } - -} \ No newline at end of file +} diff --git a/test-e2e/utils/steps_file.ts b/test-e2e/utils/steps_file.ts index e47031dd2..e08e43052 100644 --- a/test-e2e/utils/steps_file.ts +++ b/test-e2e/utils/steps_file.ts @@ -1,26 +1,33 @@ import * as assert from 'assert'; -import {configFileQueryKey} from "../../src/utils/configOverride"; +import { configFileQueryKey } from '../../src/utils/configOverride'; import constants from './constants'; -import passwordUtils, {LoginContext} from "./password_utils"; +import passwordUtils, { LoginContext } from './password_utils'; declare global { - let jwplayer: () => {getState: () => string}; + let jwplayer: () => { getState: () => string }; } const loaderElement = '[class*=_loadingOverlay]'; -module.exports = function() { +module.exports = function () { return actor({ - useConfig: function(this: CodeceptJS.I, config: 'test--subscription' | 'test--accounts' | 'test--no-cleeng' | 'blender', baseUrl: string = constants.baseUrl) { + useConfig: function ( + this: CodeceptJS.I, + config: 'test--subscription' | 'test--accounts' | 'test--no-cleeng' | 'blender', + baseUrl: string = constants.baseUrl, + ) { const url = new URL(baseUrl); url.searchParams.delete(configFileQueryKey); url.searchParams.append(configFileQueryKey, config); this.amOnPage(url.toString()); }, - login: function (this: CodeceptJS.I, {email, password}: {email: string, password: string} = {email: constants.username, password: constants.password}) { + login: function ( + this: CodeceptJS.I, + { email, password }: { email: string; password: string } = { email: constants.username, password: constants.password }, + ) { this.amOnPage(constants.loginUrl); this.waitForElement('input[name=email]', 10); this.fillField('email', email); @@ -33,10 +40,10 @@ module.exports = function() { return { email, - password - } + password, + }; }, - logout: async function(this: CodeceptJS.I) { + logout: async function (this: CodeceptJS.I) { const isMobile = await this.isMobile(); if (isMobile) { @@ -50,12 +57,11 @@ module.exports = function() { // This function will register the user on the first call and return the context // then assuming context is passed in the next time, will log that same user back in // Use it for tests where you want a new user for the suite, but not for each test - registerOrLogin: function(this: CodeceptJS.I, context?: LoginContext, onRegister?: () => void) { - + registerOrLogin: function (this: CodeceptJS.I, context?: LoginContext, onRegister?: () => void) { if (context) { - this.login({email: context.email, password: context.password}); + this.login({ email: context.email, password: context.password }); } else { - context = {email: passwordUtils.createRandomEmail(), password: passwordUtils.createRandomPassword()}; + context = { email: passwordUtils.createRandomEmail(), password: passwordUtils.createRandomPassword() }; this.amOnPage(`${constants.baseUrl}?u=create-account`); this.waitForElement(constants.registrationFormSelector, 10); @@ -75,11 +81,11 @@ module.exports = function() { return context; }, - submitForm: function(this: CodeceptJS.I, loaderTimeout: number | false = 5) { + submitForm: function (this: CodeceptJS.I, loaderTimeout: number | false = 5) { this.click('button[type="submit"]'); this.waitForLoaderDone(loaderTimeout); }, - waitForLoaderDone: function(this: CodeceptJS.I, timeout: number | false = 5) { + waitForLoaderDone: function (this: CodeceptJS.I, timeout: number | false = 5) { // Specify false when the loader is NOT expected to be shown at all if (timeout === false) { this.dontSeeElement(loaderElement); @@ -87,7 +93,7 @@ module.exports = function() { this.waitForInvisible(loaderElement, timeout); } }, - openMainMenu: async function(this: CodeceptJS.I, isMobile?: boolean) { + openMainMenu: async function (this: CodeceptJS.I, isMobile?: boolean) { isMobile = await this.isMobile(isMobile); if (isMobile) { this.openMenuDrawer(); @@ -100,23 +106,23 @@ module.exports = function() { openMenuDrawer: function (this: CodeceptJS.I) { this.click('div[aria-label="Open menu"]'); }, - openUserMenu: function(this: CodeceptJS.I) { + openUserMenu: function (this: CodeceptJS.I) { this.click('div[aria-label="Open user menu"]'); }, - clickCloseButton: function(this: CodeceptJS.I) { + clickCloseButton: function (this: CodeceptJS.I) { this.click('div[aria-label="Close"]'); }, - seeAll: function(this: CodeceptJS.I, allStrings: string[]) { - allStrings.forEach(s => this.see(s)); + seeAll: function (this: CodeceptJS.I, allStrings: string[]) { + allStrings.forEach((s) => this.see(s)); }, - dontSeeAny: function(this: CodeceptJS.I, allStrings: string[]) { - allStrings.forEach(s => this.dontSee(s)); + dontSeeAny: function (this: CodeceptJS.I, allStrings: string[]) { + allStrings.forEach((s) => this.dontSee(s)); }, - seeValueEquals: async function(this: CodeceptJS.I, value: string, locator: CodeceptJS.LocatorOrString) { + seeValueEquals: async function (this: CodeceptJS.I, value: string, locator: CodeceptJS.LocatorOrString) { assert.equal(await this.grabValueFrom(locator), value); }, - waitForAllInvisible: function(this: CodeceptJS.I, allStrings: string[], timeout: number | undefined = undefined) { - allStrings.forEach(s => this.waitForInvisible(s, timeout)); + waitForAllInvisible: function (this: CodeceptJS.I, allStrings: string[], timeout: number | undefined = undefined) { + allStrings.forEach((s) => this.waitForInvisible(s, timeout)); }, swipeLeft: async function (this: CodeceptJS.I, args) { args.direction = 'left'; @@ -130,46 +136,46 @@ module.exports = function() { await this.executeScript((args) => { const xpath = args.xpath || `//*[text() = "${args.text}"]`; - const points = args.direction === 'left' ? {x1: 100, y1: 1, x2: 50, y2: 1} - : args.direction === 'right' ? {x1: 50, y1: 1, x2: 100, y2: 1} - : args.points; + const points = + args.direction === 'left' ? { x1: 100, y1: 1, x2: 50, y2: 1 } : args.direction === 'right' ? { x1: 50, y1: 1, x2: 100, y2: 1 } : args.points; - const element = document.evaluate(xpath, document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, - null).singleNodeValue; + const element = document.evaluate(xpath, document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue; if (!element) { throw `Could not find element by xpath: "${xpath}"`; } - element.dispatchEvent(new TouchEvent('touchstart', - { - bubbles: true, - touches: [ - new Touch({ - identifier: Date.now(), - target: element, - clientX: points.x1, - clientY: points.y1 - }) - ] - })); - - element.dispatchEvent(new TouchEvent('touchend', - { - bubbles: true, - changedTouches: [ - new Touch({ - identifier: Date.now() + 1, - target: element, - clientX: points.x2, - clientY: points.y2 - }) - ] - })); - + element.dispatchEvent( + new TouchEvent('touchstart', { + bubbles: true, + touches: [ + new Touch({ + identifier: Date.now(), + target: element, + clientX: points.x1, + clientY: points.y1, + }), + ], + }), + ); + + element.dispatchEvent( + new TouchEvent('touchend', { + bubbles: true, + changedTouches: [ + new Touch({ + identifier: Date.now() + 1, + target: element, + clientX: points.x2, + clientY: points.y2, + }), + ], + }), + ); }, args); }, waitForPlayerPlaying: async function (title, tries = 10) { + this.seeElement('div[class*="jwplayer"]'); this.see(title); await this.waitForPlayerState('playing', ['buffering', 'idle', ''], tries); }, @@ -178,7 +184,7 @@ module.exports = function() { // so we have to manually retry (this is because the video can take time to load and the state will be buffering) for (let i = 0; i < tries; i++) { // In theory this expression can be simplified, but without the typeof's codecept throws an error when the value is undefined. - const state = await this.executeScript(() => typeof jwplayer === 'undefined' || typeof jwplayer().getState === 'undefined' ? '' : jwplayer().getState()); + const state = await this.executeScript(() => jwplayer?.()?.getState()); await this.say(`Waiting for Player state. Expected: "${expectedState}", Current: "${state}"`); @@ -199,27 +205,28 @@ module.exports = function() { this.dontSeeElement('div[class*="jwplayer"]'); this.dontSeeElement('video'); // eslint-disable-next-line no-console - assert.equal(await this.executeScript(() => typeof jwplayer === 'undefined' ? undefined : jwplayer().getState), - undefined); + assert.equal(await this.executeScript(() => (typeof jwplayer === 'undefined' ? undefined : jwplayer().getState)), undefined); }, - isMobile: async function(this: CodeceptJS.I) { - return await this.usePlaywrightTo('Get is Mobile', async ({browserContext}) => { - return browserContext._options.isMobile; - }) || false; + isMobile: async function (this: CodeceptJS.I) { + return ( + (await this.usePlaywrightTo('Get is Mobile', async ({ browserContext }) => { + return browserContext._options.isMobile; + })) || false + ); }, - isDesktop: async function(this: CodeceptJS.I) { - return !await this.isMobile(); + isDesktop: async function (this: CodeceptJS.I) { + return !(await this.isMobile()); }, - enableClipboard: async function(this: CodeceptJS.I) { - await this.usePlaywrightTo('Setup the clipboard', async ({browserContext}) => { - await browserContext.grantPermissions(["clipboard-read", "clipboard-write"]); + enableClipboard: async function (this: CodeceptJS.I) { + await this.usePlaywrightTo('Setup the clipboard', async ({ browserContext }) => { + await browserContext.grantPermissions(['clipboard-read', 'clipboard-write']); }); }, - readClipboard: async function(this: CodeceptJS.I) { + readClipboard: async function (this: CodeceptJS.I) { return await this.executeScript(() => navigator.clipboard.readText()); }, - writeClipboard: async function(this: CodeceptJS.I, text: string) { + writeClipboard: async function (this: CodeceptJS.I, text: string) { await this.executeScript((text) => navigator.clipboard.writeText(text), text); - } + }, }); -} \ No newline at end of file +};