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' ;
33import 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