Skip to content

Commit b28b855

Browse files
authored
Merge branch 'main' into doml/backdrop-filter-fallback
2 parents 659e09a + fb35a0d commit b28b855

File tree

6 files changed

+262
-127
lines changed

6 files changed

+262
-127
lines changed

dotcom-rendering/playwright.config.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { defineConfig, devices } from '@playwright/test';
22

33
const isDev = process.env.NODE_ENV !== 'production';
4+
45
/**
56
* The server port for local development or CI
67
*/
78
export const PORT = isDev ? 3030 : 9000;
9+
export const ORIGIN = `http://localhost:${PORT}`;
810

911
/**
1012
* See https://playwright.dev/docs/test-configuration.
@@ -19,7 +21,7 @@ export default defineConfig({
1921
// Retry on CI only
2022
retries: process.env.CI ? 3 : 1,
2123
// Workers run tests files in parallel
22-
workers: process.env.CI ? 4 : undefined,
24+
workers: process.env.CI ? 4 : 2,
2325
// Reporter to use. See https://playwright.dev/docs/test-reporters
2426
reporter: [['line'], ['html', { open: 'never' }]],
2527
// Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions
@@ -38,12 +40,13 @@ export default defineConfig({
3840
use: { ...devices['Desktop Chrome'] },
3941
},
4042
],
41-
webServer: {
42-
// On CI the server is already started so a no-op
43-
command: isDev ? 'make dev' : ':',
44-
url: `http://localhost:${PORT}`,
45-
reuseExistingServer: true,
46-
stdout: 'pipe',
47-
stderr: 'pipe',
48-
},
43+
webServer: isDev
44+
? {
45+
command: 'make dev',
46+
url: ORIGIN,
47+
reuseExistingServer: true,
48+
stdout: 'pipe',
49+
stderr: 'pipe',
50+
}
51+
: undefined,
4952
});
Lines changed: 165 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,131 @@
1-
import type { Page } from '@playwright/test';
2-
import { PORT } from 'playwright.config';
1+
import type { Cookie, Page } from '@playwright/test';
2+
import { ORIGIN } from '../../playwright.config';
33
import type { FEArticle } from '../../src/frontend/feArticle';
4-
import { validateAsFEArticle } from '../../src/model/validate';
4+
import type { FEFront } from '../../src/frontend/feFront';
5+
import {
6+
validateAsFEArticle,
7+
validateAsFEFront,
8+
} from '../../src/model/validate';
59

6-
const BASE_URL = `http://localhost:${PORT}`;
10+
type LoadPageOptions = {
11+
queryParams?: Record<string, string>;
12+
queryParamsOn?: boolean;
13+
fragment?: `#${string}`;
14+
waitUntil?: 'domcontentloaded' | 'load';
15+
region?: 'GB' | 'US' | 'AU' | 'INT';
16+
preventSupportBanner?: boolean;
17+
overrides?: {
18+
configOverrides?: Record<string, unknown>;
19+
switchOverrides?: Record<string, unknown>;
20+
feFixture?: FEArticle | FEFront;
21+
};
22+
};
23+
24+
type LoadPageParams = {
25+
page: Page;
26+
path: string;
27+
} & LoadPageOptions;
28+
29+
/**
30+
* @param path The path for a DCR endpoint path
31+
* e.g. `/Article/https://www.theguardian.com/world/2025/aug/19/the-big-church-move-sweden-kiruna-kyrka`
32+
* @returns The Frontend URL to fetch the JSON payload
33+
* e.g. `https://www.theguardian.com/world/2025/aug/19/the-big-church-move-sweden-kiruna-kyrka.json`
34+
*/
35+
const getFrontendJsonUrl = (path: string) => {
36+
const secondSlashIndex = path.indexOf('/', 1);
37+
const contentUrl = path.substring(secondSlashIndex + 1);
38+
return `${contentUrl}.json`;
39+
};
40+
41+
/**
42+
* @param path The path for a DCR endpoint path
43+
* e.g. `/Article/https://www.theguardian.com/world/2025/aug/19/the-big-church-move-sweden-kiruna-kyrka`
44+
* @param cookies Cookies to send with the request
45+
* e.g. `GU_EDITION=US`
46+
* @param queryParams Query parameters to append to the request
47+
* e.g. `live=true` for live blogs
48+
* @returns The JSON response from the Frontend URL
49+
*/
50+
const getFrontendJson = async (
51+
path: string,
52+
cookies: Cookie[],
53+
queryParams: LoadPageParams['queryParams'],
54+
): Promise<unknown> => {
55+
try {
56+
const paramsString = `${new URLSearchParams({
57+
dcr: 'true',
58+
...queryParams,
59+
}).toString()}`;
60+
const frontendUrl = `${getFrontendJsonUrl(path)}?${paramsString}`;
61+
const cookie = cookies.map((c) => `${c.name}=${c.value}`).join('; ');
62+
const response = await fetch(frontendUrl, { headers: { cookie } });
63+
if (!response.ok) {
64+
throw new Error(
65+
`Failed to fetch from ${path}: ${response.statusText}`,
66+
);
67+
}
68+
return response.json();
69+
} catch (error) {
70+
throw new Error(
71+
`Error fetching from ${path}: ${
72+
error instanceof Error ? error.message : String(error)
73+
}`,
74+
);
75+
}
76+
};
77+
78+
/**
79+
* Validates the JSON response from the Frontend URL based on the path.
80+
81+
* Add more validation logic here if additional content types are required.
82+
*
83+
* @param path The path for a DCR endpoint, used to determine the content type.
84+
* e.g. `/Article/https://www.theguardian.com/world/2025/aug/19/the-big-church-move-sweden-kiruna-kyrka`
85+
* @param json The JSON response from the Frontend URL
86+
* @returns The validated `FEArticle` or `FEFront` object
87+
*/
88+
const validateJson = (path: string, json: unknown): FEArticle | FEFront => {
89+
if (path.startsWith('/Article')) {
90+
return validateAsFEArticle(json);
91+
} else if (path.startsWith('/Front')) {
92+
return validateAsFEFront(json);
93+
}
94+
throw new Error(`Unsupported URL for validating payload for: ${path}`);
95+
};
96+
97+
/**
98+
* Constructs a DCR URL for a given path and query parameters.
99+
* @param params The parameters for constructing the DCR URL
100+
* @param params.path The path for a DCR endpoint
101+
* @param params.queryParamsOn Whether to append query parameters to the URL
102+
* @param params.queryParams Query parameters to append to the request
103+
* @returns The DCR URL
104+
* e.g. `http://localhost:9000/Article/https://theguardian.com/sport/live/2022/mar/27/west-indies-v-england-third-test-day-four-live?adtest=fixed-puppies-ci&live=true&force-liveblog-epic=true`
105+
*/
106+
const getDcrUrl = ({
107+
path,
108+
queryParamsOn,
109+
queryParams,
110+
}: Pick<LoadPageParams, 'path' | 'queryParamsOn' | 'queryParams'>): string => {
111+
const paramsString = queryParamsOn
112+
? `?${new URLSearchParams({
113+
adtest: 'fixed-puppies-ci',
114+
...queryParams,
115+
}).toString()}`
116+
: '';
117+
return `${ORIGIN}${path}${paramsString}`;
118+
};
119+
120+
/**
121+
* Constructs a DCR POST URL for a given path.
122+
* @param path The path for a DCR endpoint
123+
* e.g. `/Article/https://www.theguardian.com/world/2025/aug/19/the-big-church-move-sweden-kiruna-kyrka`
124+
* @returns The DCR POST URL to send the request to
125+
* e.g. `http://localhost:9000/Article`
126+
* This is used to override the request method to POST in Playwright tests.
127+
*/
128+
const getDcrPostUrl = (path: string) => `${ORIGIN}/${path.split('/')[1]}`;
7129

8130
/**
9131
* Loads a page in Playwright and centralises setup
@@ -17,16 +139,8 @@ const loadPage = async ({
17139
waitUntil = 'domcontentloaded',
18140
region = 'GB',
19141
preventSupportBanner = true,
20-
}: {
21-
page: Page;
22-
path: string;
23-
queryParams?: Record<string, string>;
24-
queryParamsOn?: boolean;
25-
fragment?: `#${string}`;
26-
waitUntil?: 'domcontentloaded' | 'load';
27-
region?: 'GB' | 'US' | 'AU' | 'INT';
28-
preventSupportBanner?: boolean;
29-
}): Promise<void> => {
142+
overrides = {},
143+
}: LoadPageParams): Promise<void> => {
30144
await page.addInitScript(
31145
(args) => {
32146
// Set the geo region, defaults to GB
@@ -47,82 +161,54 @@ const loadPage = async ({
47161
preventSupportBanner,
48162
},
49163
);
50-
// Add an adtest query param to ensure we get a fixed test ad
51-
const paramsString = queryParamsOn
52-
? `?${new URLSearchParams({
53-
adtest: 'fixed-puppies-ci',
54-
...queryParams,
55-
}).toString()}`
56-
: '';
57164

58-
// The default Playwright waitUntil: 'load' ensures all requests have completed
59-
// Use 'domcontentloaded' to speed up tests and prevent hanging requests from timing out tests
60-
await page.goto(`${BASE_URL}${path}${paramsString}${fragment ?? ''}`, {
61-
waitUntil,
62-
});
63-
};
165+
const cookies = await page.context().cookies();
64166

65-
/**
66-
* Create a POST request to the /Article endpoint so we can override config
67-
* and switches in the json sent to DCR
68-
*/
69-
const loadPageWithOverrides = async (
70-
page: Page,
71-
article: FEArticle,
72-
overrides?: {
73-
configOverrides?: Record<string, unknown>;
74-
switchOverrides?: Record<string, unknown>;
75-
},
76-
): Promise<void> => {
77-
const path = `/Article`;
78-
await page.route(`${BASE_URL}${path}`, async (route) => {
79-
const postData = {
80-
...article,
81-
config: {
82-
...article.config,
83-
...overrides?.configOverrides,
84-
switches: {
85-
...article.config.switches,
86-
...overrides?.switchOverrides,
87-
},
167+
// If overrides exist, but no fixture is provided we fetch it from Frontend
168+
const frontendModel = await (overrides.feFixture
169+
? Promise.resolve(overrides.feFixture)
170+
: validateJson(
171+
path,
172+
await getFrontendJson(path, cookies, queryParams),
173+
));
174+
175+
// Apply the config and switch overrides
176+
const postData = {
177+
...frontendModel,
178+
config: {
179+
...frontendModel.config,
180+
...overrides.configOverrides,
181+
switches: {
182+
...frontendModel.config.switches,
183+
...overrides.switchOverrides,
88184
},
89-
};
185+
},
186+
};
187+
188+
const dcrUrl = getDcrUrl({
189+
path,
190+
queryParamsOn,
191+
queryParams,
192+
});
193+
194+
// Override any request matching dcrUrl to use a POST method
195+
// with the overridden payload
196+
await page.route(dcrUrl, async (route) => {
90197
await route.continue({
91198
method: 'POST',
92199
headers: {
200+
...route.request().headers(),
93201
'Content-Type': 'application/json',
94202
},
95203
postData,
204+
url: getDcrPostUrl(path),
96205
});
97206
});
98-
await loadPage({ page, path, queryParamsOn: false });
99-
};
100207

101-
/**
102-
* Fetch the page json from PROD then load it as a POST with overrides
103-
*/
104-
const fetchAndloadPageWithOverrides = async (
105-
page: Page,
106-
url: string,
107-
overrides?: {
108-
configOverrides?: Record<string, unknown>;
109-
switchOverrides?: Record<string, unknown>;
110-
},
111-
): Promise<void> => {
112-
const article = validateAsFEArticle(
113-
await fetch(`${url}.json?dcr`).then((res) => res.json()),
114-
);
115-
await loadPageWithOverrides(page, article, {
116-
configOverrides: overrides?.configOverrides,
117-
switchOverrides: {
118-
...overrides?.switchOverrides,
119-
},
120-
});
208+
// Initiate the page load
209+
// Add the fragment here as Playwright has an issue when matching urls
210+
// with fragments in the page.route handler
211+
await page.goto(`${dcrUrl}${fragment ?? ''}`, { waitUntil });
121212
};
122213

123-
export {
124-
BASE_URL,
125-
fetchAndloadPageWithOverrides,
126-
loadPage,
127-
loadPageWithOverrides,
128-
};
214+
export { loadPage };

0 commit comments

Comments
 (0)