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' ;
3
3
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' ;
5
9
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 ] } ` ;
7
129
8
130
/**
9
131
* Loads a page in Playwright and centralises setup
@@ -17,16 +139,8 @@ const loadPage = async ({
17
139
waitUntil = 'domcontentloaded' ,
18
140
region = 'GB' ,
19
141
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 > => {
30
144
await page . addInitScript (
31
145
( args ) => {
32
146
// Set the geo region, defaults to GB
@@ -47,82 +161,54 @@ const loadPage = async ({
47
161
preventSupportBanner,
48
162
} ,
49
163
) ;
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
- : '' ;
57
164
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 ( ) ;
64
166
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 ,
88
184
} ,
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 ) => {
90
197
await route . continue ( {
91
198
method : 'POST' ,
92
199
headers : {
200
+ ...route . request ( ) . headers ( ) ,
93
201
'Content-Type' : 'application/json' ,
94
202
} ,
95
203
postData,
204
+ url : getDcrPostUrl ( path ) ,
96
205
} ) ;
97
206
} ) ;
98
- await loadPage ( { page, path, queryParamsOn : false } ) ;
99
- } ;
100
207
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 } ) ;
121
212
} ;
122
213
123
- export {
124
- BASE_URL ,
125
- fetchAndloadPageWithOverrides ,
126
- loadPage ,
127
- loadPageWithOverrides ,
128
- } ;
214
+ export { loadPage } ;
0 commit comments