playwright/lib/util.ts (155 lines of code) (raw):
import type { BrowserContext, Cookie, Page } from '@playwright/test';
type Stage = 'code' | 'prod' | 'dev';
type ContentType = 'article' | 'liveblog' | 'front' | 'tagPage';
const normalizeStage = (stage: string): Stage =>
['code', 'prod', 'dev'].includes(stage) ? (stage as Stage) : 'dev';
/**
* Set the stage via environment variable STAGE
* e.g. `STAGE=code pnpm playwright test`
*/
const getStage = (): Stage => {
// TODO check playwright picks up the STAGE env var
const stage = process.env.STAGE;
return normalizeStage(stage?.toLowerCase() ?? 'dev');
};
const hostnames = {
code: 'https://code.dev-theguardian.com',
prod: 'https://www.theguardian.com',
dev: 'http://localhost:3030',
} as const;
const headerBiddingAnalyticsUrl = {
dev: 'http://performance-events.code.dev-guardianapis.com/header-bidding',
code: 'https://performance-events.code.dev-guardianapis.com/header-bidding',
prod: 'http://performance-events.guardianapis.com/header-bidding',
} as const;
const getHost = (stage?: Stage) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive runtime
return hostnames[stage ?? getStage()] ?? hostnames.dev;
};
const getDcrContentType = (
type: ContentType,
): 'Article' | 'Front' | 'TagPage' => {
switch (type) {
case 'front':
return 'Front';
case 'tagPage':
return 'TagPage';
default:
return 'Article';
}
};
/**
* Generate the path for the request to DCR
*/
const getPath = (
stage: Stage,
type: ContentType = 'article',
path: string,
fixtureId: string | undefined,
fixture: Record<string, unknown> | undefined,
) => {
if (stage === 'dev') {
const dcrContentType = getDcrContentType(type);
if (fixtureId) {
return `${dcrContentType}/http://localhost:3031/renderFixtureWithId/${fixtureId}/${path}`;
}
if (fixture) {
const fixtureJson = JSON.stringify(fixture);
const base64Fixture = Buffer.from(fixtureJson).toString('base64');
return `${dcrContentType}/http://localhost:3031/renderFixture/${path}?fixture=${base64Fixture}`;
}
return `${dcrContentType}/https://www.theguardian.com${path}`;
}
return path;
};
/**
* Generate a full URL i.e domain and path
*/
const getTestUrl = ({
stage,
path,
type = 'article',
adtest = 'fixed-puppies-ci',
fixtureId,
fixture,
}: {
stage: Stage;
path: string;
type?: ContentType;
adtest?: string;
fixtureId?: string;
fixture?: Record<string, unknown>;
}) => {
const url = new URL(
getPath(stage, type, path, fixtureId, fixture),
getHost(stage),
);
if (type === 'liveblog') {
url.searchParams.append('live', '1');
}
url.searchParams.append('adtest', adtest);
// force an invalid epic so it is not shown
url.searchParams.append('force-epic', '9999:CONTROL');
return url.toString();
};
// Playwright does not currently have a useful method for removing a cookie, so this workaround is needed.
const clearCookie = async (context: BrowserContext, cookieName: string) => {
const cookies = await context.cookies();
const filteredCookies = cookies.filter(
(cookie: Cookie) => cookie.name !== cookieName,
);
await context.clearCookies();
await context.addCookies(filteredCookies);
};
const waitForSlot = async (page: Page, slot: string, waitForIframe = true) => {
const slotId = `#dfp-ad--${slot}`;
// create a locator for the slot
const slotLocator = page.locator(slotId);
// check that the ad slot is present on the page
await slotLocator.isVisible();
// scroll to it
await slotLocator.scrollIntoViewIfNeeded();
if (waitForIframe) {
// iframe locator
const iframe = page.locator(`${slotId} iframe:first-child`);
// wait for the iframe to be visible
await iframe.waitFor();
}
};
const waitForIsland = async (page: Page, island: string) => {
const islandSelector = `gu-island[name="${island}"]`;
// create a locator for the island
const islandLocator = page.locator(islandSelector);
// check that the island is present on the page
await islandLocator.isVisible();
// scroll to it
await islandLocator.scrollIntoViewIfNeeded();
// wait for it to be hydrated
const hyrdatedIslandSelector = `gu-island[name="${island}"][data-island-status="hydrated"]`;
const hyrdatedIslandLocator = page.locator(hyrdatedIslandSelector);
await hyrdatedIslandLocator.waitFor();
};
const countLiveblogInlineSlots = async (page: Page, isMobile: boolean) => {
const mobileSuffix = isMobile ? '--mobile' : '';
const locator = `#liveblog-body .ad-slot--liveblog-inline${mobileSuffix}`;
return await page.locator(locator).count();
};
const getSlotName = (url: string) => {
const adRequest = new URL(url);
const adRequestParams = adRequest.searchParams;
const prevScp = new URLSearchParams(adRequestParams.get('prev_scp') ?? '');
return prevScp.get('slot') ?? 'unknown';
};
// Warn if any slots are unfilled
const logUnfilledSlots = (page: Page) => {
page.on('response', (response) => {
const url = response.url();
if (url.includes('securepubads.g.doubleclick.net/gampad/ads')) {
const lineItemId = response.headers()['google-lineitem-id'] ?? '';
const creativeId = response.headers()['google-creative-id'] ?? '';
if (
!lineItemId ||
!creativeId ||
lineItemId === '-2' ||
creativeId === '-2'
) {
console.warn(`Unfilled slot: ${getSlotName(url)}`);
}
}
});
};
// Log commercial logs to playwight console
const logCommercial = (page: Page) => {
page.on('console', (msg) => {
const label = JSON.stringify(msg.args()[0]);
if (label.includes('commercial')) {
console.log(msg.args().slice(4).map(String).join(' '));
}
});
};
export {
countLiveblogInlineSlots,
clearCookie,
getStage,
getTestUrl,
waitForIsland,
waitForSlot,
logUnfilledSlots,
logCommercial,
headerBiddingAnalyticsUrl,
};