diff --git a/backstop.json b/backstop.json index 4f94cd8..98d6be4 100644 --- a/backstop.json +++ b/backstop.json @@ -7,8 +7,7 @@ "height": 1080 } ], - "scenarios": [ - ], + "scenarios": [], "paths": { "bitmaps_reference": "backstop_data/bitmaps_reference", "bitmaps_test": "backstop_data/bitmaps_test", diff --git a/config/wp.config.sample.ts b/config/wp.config.sample.ts index 7681712..b85fb2e 100755 --- a/config/wp.config.sample.ts +++ b/config/wp.config.sample.ts @@ -15,6 +15,16 @@ const WP_ADMIN_USER = { } as const; +/** + * The default Imagify settings information + * + * @constant + * @type {{ apiKey: string }} + */ +const IMAGIFY_INFOS = { + apiKey: '' +} as const; + /** * Extracted environment variables related to WordPress configuration. * Uses default values if environment variables are not set. @@ -99,5 +109,6 @@ export { WP_SSH_ADDRESS, WP_SSH_KEY, WP_SSH_ROOT_DIR, - SCENARIO_URLS + SCENARIO_URLS, + IMAGIFY_INFOS }; \ No newline at end of file diff --git a/package.json b/package.json index 01666ae..e3eead3 100755 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "lint:fix": "eslint . --ext .ts --fix", "test:e2e": "$npm_package_config_testCommand", "test:smoke": "$npm_package_config_testCommand --tags @smoke", + "test:lcpll": "$npm_package_config_testCommand --tags @lcpll", "test:local": "$npm_package_config_testCommand --tags @local", "test:online": "$npm_package_config_testCommand --tags @online", "test:vr": "$npm_package_config_testCommand --tags @vr", diff --git a/src/features/ll-lcp.feature b/src/features/ll-lcp.feature new file mode 100644 index 0000000..87d3ce6 --- /dev/null +++ b/src/features/ll-lcp.feature @@ -0,0 +1,43 @@ +@lcpll @delaylcp @setup +Feature: Lazyload with LCP + + Background: + Given I am logged in + And plugin is installed 'new_release' + And plugin 'wp-rocket' is activated + When I go to 'wp-admin/options-general.php?page=wprocket#dashboard' + And I save settings 'media' 'lazyloadCssBgImg' + + Scenario: Should Exclude LCP/ATF from Lazyload + And I clear cache + When I log out + And I visit the urls for 'desktop' + When I am logged in + And I clear cache + And I visit the urls and check for lazyload + Then lcp and atf images are not written to LL format + + Scenario: Should exclude next-gen lcp/atf from LL + Given I install plugin 'imagify' + And plugin 'imagify' is activated + When I am logged in + And Imagify is set up + When I log out + And I visit page 'lcp_with_imagify' and check for lcp + When I am logged in + And I save settings 'media' 'lazyloadCssBgImg' + And I clear cache + And I visit the 'lcp_with_imagify' and check lcp-atf are not lazyloaded + Then lcp and atf images are not written to LL format + + Scenario: Should exclude next-gen lcp/atf from LL + When I am logged in + And display next-gen is enabled on imagify + When I log out + And I visit page 'lcp_with_imagify' and check for lcp + When I am logged in + And I save settings 'media' 'lazyloadCssBgImg' + And I clear cache + And I visit the 'lcp_with_imagify' and check lcp-atf are not lazyloaded + Then lcp and atf images are not written to LL format + diff --git a/src/support/steps/general.ts b/src/support/steps/general.ts index bde5458..b512224 100644 --- a/src/support/steps/general.ts +++ b/src/support/steps/general.ts @@ -245,7 +245,8 @@ When('I clear cache', async function (this:ICustomWorld) { await this.utils.gotoWpr(); this.sections.set('dashboard'); - await this.sections.toggle('clearCacheBtn'); + await this.page.locator("text=Clear and preload").last().click(); + //await this.sections.toggle('clearCacheBtn'); await expect(this.page.getByText('WP Rocket: Cache cleared.')).toBeVisible(); }); diff --git a/src/support/steps/imagify.ts b/src/support/steps/imagify.ts new file mode 100644 index 0000000..e167724 --- /dev/null +++ b/src/support/steps/imagify.ts @@ -0,0 +1,31 @@ +import { ICustomWorld } from "../../common/custom-world"; + +import { Given } from '@cucumber/cucumber'; +import { IMAGIFY_INFOS } from "../../../config/wp.config"; +import {expect} from "@playwright/test"; + +Given('Imagify is set up', async function (this: ICustomWorld) { + await this.utils.gotoImagify(); + + // Check if the API key input field exists on the page + const apiKeyInput = await this.page.$('input#api_key'); + + if (apiKeyInput) { + // Fill the API key input field with the API key from the config + await this.page.fill('input#api_key', IMAGIFY_INFOS.apiKey); + // Click the submit button to save the changes + await this.page.click('div.submit.imagify-clearfix input#submit'); + } +}); +Given('display next-gen is enabled on imagify', async function (this: ICustomWorld) { + // Go to Imagify setting page + await this.utils.gotoImagify(); + + // Check the 'Display images in Next-Gen format on the site' checkbox + await this.page.click('label[for="imagify_display_nextgen"]'); + + // Click the submit button to save the changes + await this.page.click('input#submit'); + + await expect(this.page.getByText('Settings saved.')).toBeVisible(); +}); \ No newline at end of file diff --git a/src/support/steps/lcp-beacon-script.ts b/src/support/steps/lcp-beacon-script.ts index cd7508b..6532ddf 100644 --- a/src/support/steps/lcp-beacon-script.ts +++ b/src/support/steps/lcp-beacon-script.ts @@ -8,24 +8,63 @@ * @requires {@link @playwright/test} * @requires {@link @cucumber/cucumber} */ -import { ICustomWorld } from "../../common/custom-world"; -import { expect } from "@playwright/test"; -import { When, Then } from "@cucumber/cucumber"; -import { LcpData, Row } from "../../../utils/types"; - -import { dbQuery, getWPTablePrefix } from "../../../utils/commands"; -import { extractFromStdout } from "../../../utils/helpers"; -import { WP_BASE_URL } from '../../../config/wp.config'; +import {ICustomWorld} from "../../common/custom-world"; +import {expect} from "@playwright/test"; +import {Then, When} from "@cucumber/cucumber"; +import {LcpData, Row, SinglePageLCPImages} from "../../../utils/types"; + +import {dbQuery, getWPTablePrefix} from "../../../utils/commands"; +import {extractFromStdout} from "../../../utils/helpers"; +import {WP_BASE_URL} from '../../../config/wp.config'; import fs from 'fs/promises'; let data: string, truthy: boolean = true, failMsg: string, jsonData: Record, - isDbResultAvailable: boolean = true; + isDbResultAvailable: boolean = true, + lcpLLImages: { [key: string] : { src: string; url: string | boolean; lazyloaded: string | boolean }} = {}, + singlePageLcp : SinglePageLCPImages = {url: '', lcp: '', viewport: ''}; const actual: LcpData = {}; +/** + * Executes step to visit page based on the templates and get check for lazyload. + */ +When('I visit the urls and check for lazyload', async function (this: ICustomWorld) { + const resultFile: string = './src/support/results/expectedResultsDesktop.json'; + + await this.page.setViewportSize({ + width: 1600, + height: 700 + }); + + data = await fs.readFile(resultFile, 'utf8'); + jsonData = JSON.parse(data); + + // Visit page. + for (const key in jsonData) { + if ( jsonData[key].enabled === true ) { + // Visit the page url. + await this.utils.visitPage(key); + + lcpLLImages = await this.page.evaluate((url) => { + const images = document.querySelectorAll('img'), + result = {}; + + Array.from(images).forEach((img) => { + result[url] = { + src: img.getAttribute('src'), + url: url, + lazyloaded: img.classList.contains('lazyloaded') + } + }); + + return result; + }, key); + } + } +}); /** * Executes step to visit page based on the form factor(desktop/mobile) and get the LCP/ATF data from DB. */ @@ -71,7 +110,7 @@ When('I visit the urls for {string}', async function (this: ICustomWorld, formFa await this.page.waitForFunction(() => { const beacon = document.querySelector('[data-name="wpr-wpr-beacon"]'); return beacon && beacon.getAttribute('beacon-completed') === 'true'; - }, { timeout: 900000 }); + }, { timeout: 100000 }); if (formFactor !== 'desktop') { isMobile = 1; @@ -152,18 +191,21 @@ Then('lcp and atf should be as expected for {string}', async function (this: ICu expect(truthy).toBeTruthy(); }); +let lcpImages: Array<{ src: string; fetchpriority: string | boolean; lazyloaded: string | boolean }> = []; + Then('lcp image should have fetchpriority', async function (this: ICustomWorld) { truthy= false; - const imageWithFetchPriority = await this.page.evaluate(() => { + lcpImages = await this.page.evaluate(() => { const images = document.querySelectorAll('img'); return Array.from(images).map(img => ({ src: img.getAttribute('src'), - fetchpriority: img.getAttribute('fetchpriority') || false + fetchpriority: img.getAttribute('fetchpriority') || false, + lazyloaded: img.classList.contains('lazyloaded') })); }); - for (const image of imageWithFetchPriority) { + for (const image of lcpImages) { if(image.src === '/wp-content/rocket-test-data/images/600px-Mapang-test.gif' && image.fetchpriority !== false) { truthy = true } @@ -171,3 +213,115 @@ Then('lcp image should have fetchpriority', async function (this: ICustomWorld) expect(truthy).toBeTruthy(); }); + +Then('lcp and atf images are not written to LL format', async function (this: ICustomWorld) { + // Reset truthy to true here. + truthy = true; + + // Iterate over the data + for (const key in jsonData) { + if (Object.hasOwnProperty.call(jsonData, key) && jsonData[key].enabled === true) { + const expected = jsonData[key]; + // Check for LCP + for (const lcp of expected.lcp) { + // Check if expected lcp is present in actual lcp. + if (lcpLLImages[key].src.includes(lcp) && lcpLLImages[key].lazyloaded) { + truthy = false; + failMsg += `Expected LCP for - ${lcp} for ${lcpLLImages[key].url} is lazyloaded - ${lcpLLImages[key].src}\n\n\n`; + } + } + + // Check for ATF + for (const viewport of expected.viewport) { + if (lcpLLImages[key].src.includes(viewport) && lcpLLImages[key].lazyloaded) { + truthy = false; + failMsg += `Expected Viewport for - ${viewport} for ${lcpLLImages[key].url} is lazyloaded - ${lcpLLImages[key].src} + \n\n\n`; + } + } + } + } + + // Fail test when there is expectation mismatch. + expect(truthy).toBeTruthy(); +}); + +When('I visit the {string} and check lcp-atf are not lazyloaded', async function (this: ICustomWorld, url: string) { + // Reset truthy to true here. + truthy = true; + + await this.page.setViewportSize({ + width: 1600, + height: 700 + }); + + await this.utils.visitPage(url); + + const allImages = await this.page.evaluate((url) => { + const images = document.querySelectorAll('img'), + result = []; + + Array.from(images).forEach((img) => { + result.push({ + src: img.getAttribute('src'), + url: url, + lazyloaded: img.classList.contains('lazyloaded') + }) + }); + + return result; + }, url); + + allImages.forEach((image) => { + if(singlePageLcp.lcp.includes(image.src) && image.lazyloaded ) { + truthy = false; + } + + if(singlePageLcp.viewport.includes(image.src) && image.lazyloaded ) { + truthy = false; + } + }); + + // Fail test when there is expectation mismatch. + expect(truthy).toBeTruthy(); +}); + +/** + * Executes the step to visit page in a specific browser dimension. + */ +When('I visit page {string} and check for lcp', async function (this:ICustomWorld, page) { + + const tablePrefix: string = await getWPTablePrefix(); + + await this.page.setViewportSize({ + width: 1600, + height: 700, + }); + + await this.utils.visitPage(page); + + // Wait the beacon to add an attribute `beacon-complete` to true before fetching from DB. + await this.page.waitForFunction(() => { + const beacon = document.querySelector('[data-name="wpr-wpr-beacon"]'); + return beacon && beacon.getAttribute('beacon-completed') === 'true'; + }, { timeout: 100000 }); + + // Get the LCP/ATF from the DB + const sql = `SELECT lcp, viewport + FROM ${tablePrefix}wpr_above_the_fold + WHERE url LIKE "%${page}%" + AND is_mobile = 0`; + const result = await dbQuery(sql); + const resultFromStdout = await extractFromStdout(result); + + // If no DB result, set assertion var to false, fail msg and skip the loop. + if (!resultFromStdout || resultFromStdout.length === 0) { + isDbResultAvailable = false; + } + + singlePageLcp = { + url: page, + lcp: resultFromStdout[0].lcp, + viewport: resultFromStdout[0].viewport + } +}); \ No newline at end of file diff --git a/utils/commands.ts b/utils/commands.ts index 175c9b5..50a2061 100644 --- a/utils/commands.ts +++ b/utils/commands.ts @@ -239,7 +239,7 @@ export async function activatePlugin(name: string): Promise { /** * Check if plugin is installed * @function - * @name activatePlugin + * @name isPluginInstalled * @async * @param {string} name - The name of the plugin to be checked if installed. * @returns {Promise} - A Promise that resolves when the check is completed. @@ -248,6 +248,18 @@ export async function isPluginInstalled(name: string): Promise { return await wp(`plugin is-installed ${name}`, false); } +/** + * Delete a plugin if exist. + * Note: this is not ideal for wpr or imagify plugins as it doesn't delete DB data which relies on uninstall hook. + * @function + * @name deletePlugin + * @async + * @param {string} name - The name of the plugin to be deleted if installed. + * @returns {Promise} - A Promise that resolves when the check is completed. + */ +export async function deletePlugin(name: string): Promise { + return await wp(`plugin delete ${name}`, false); +} /** * Install a WordPress plugin from a remote zip file using the WP-CLI command. diff --git a/utils/page-utils.ts b/utils/page-utils.ts index 822169a..46ff005 100644 --- a/utils/page-utils.ts +++ b/utils/page-utils.ts @@ -115,6 +115,15 @@ export class PageUtils { await this.page.goto(WP_BASE_URL + '/wp-admin/options-general.php?page=wprocket#dashboard'); } + /** + * Navigates to Imagify settings page. + * + * @return {Promise} + */ + public gotoImagify = async (): Promise => { + await this.page.goto(WP_BASE_URL + '/wp-admin/options-general.php?page=imagify'); + } + /** * Navigates to new post on Wordpress. * diff --git a/utils/types.ts b/utils/types.ts index d03be64..80fb51d 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -111,4 +111,10 @@ export interface LcpData { export interface Row { [key: string]: string +} + +export interface SinglePageLCPImages { + url: string, + lcp: string, + viewport: string } \ No newline at end of file