11const path = require ( "node:path" ) ;
22const os = require ( "node:os" ) ;
33const fs = require ( "node:fs" ) ;
4- const { once } = require ( "node:events" ) ;
5- const jsdom = require ( "jsdom" ) ;
4+ const { chromium } = require ( "playwright" ) ;
65
76// global absolute root path
87global . root_path = path . resolve ( `${ __dirname } /../../../` ) ;
@@ -17,8 +16,67 @@ const sampleCss = [
1716 " top: 100%;" ,
1817 "}"
1918] ;
20- var indexData = [ ] ;
21- var cssData = [ ] ;
19+ let indexData = "" ;
20+ let cssData = "" ;
21+
22+ let browser ;
23+ let context ;
24+ let page ;
25+
26+ /**
27+ * Ensure Playwright browser and context are available.
28+ * @returns {Promise<void> }
29+ */
30+ async function ensureContext ( ) {
31+ if ( ! browser ) {
32+ browser = await chromium . launch ( { headless : true } ) ;
33+ }
34+ if ( ! context ) {
35+ context = await browser . newContext ( ) ;
36+ }
37+ }
38+
39+ /**
40+ * Open a fresh page pointing to the provided url.
41+ * @param {string } url target url
42+ * @returns {Promise<import('playwright').Page> } initialized page instance
43+ */
44+ async function openPage ( url ) {
45+ await ensureContext ( ) ;
46+ if ( page ) {
47+ await page . close ( ) ;
48+ }
49+ page = await context . newPage ( ) ;
50+ await page . goto ( url , { waitUntil : "load" } ) ;
51+ return page ;
52+ }
53+
54+ /**
55+ * Close page, context and browser if they exist.
56+ * @returns {Promise<void> }
57+ */
58+ async function closeBrowser ( ) {
59+ if ( page ) {
60+ await page . close ( ) ;
61+ page = null ;
62+ }
63+ if ( context ) {
64+ await context . close ( ) ;
65+ context = null ;
66+ }
67+ if ( browser ) {
68+ await browser . close ( ) ;
69+ browser = null ;
70+ }
71+ }
72+
73+ exports . getPage = ( ) => {
74+ if ( ! page ) {
75+ throw new Error ( "Playwright page is not initialized. Call getDocument() first." ) ;
76+ }
77+ return page ;
78+ } ;
79+
2280
2381exports . startApplication = async ( configFilename , exec ) => {
2482 vi . resetModules ( ) ;
@@ -36,7 +94,7 @@ exports.startApplication = async (configFilename, exec) => {
3694 } ) ;
3795
3896 if ( global . app ) {
39- await this . stopApplication ( ) ;
97+ await exports . stopApplication ( ) ;
4098 }
4199
42100 // Use fixed port 8080 (tests run sequentially, no conflicts)
@@ -65,114 +123,125 @@ exports.startApplication = async (configFilename, exec) => {
65123} ;
66124
67125exports . stopApplication = async ( waitTime = 100 ) => {
126+ await closeBrowser ( ) ;
127+
68128 if ( ! global . app ) {
69- if ( global . window ) {
70- global . window . close ( ) ;
71- delete global . window ;
72- }
73129 delete global . testPort ;
74130 return Promise . resolve ( ) ;
75131 }
76132
77- // Stop server first
78133 await global . app . stop ( ) ;
79134 delete global . app ;
80135 delete global . testPort ;
81136
82137 // Wait for any pending async operations to complete before closing DOM
83138 await new Promise ( ( resolve ) => setTimeout ( resolve , waitTime ) ) ;
84-
85- if ( global . window ) {
86- // Close window after async operations have settled
87- global . window . close ( ) ;
88- delete global . window ;
89- delete global . document ;
90- }
91139} ;
92140
93141exports . getDocument = async ( ) => {
94142 const port = global . testPort || config . port || 8080 ;
95- // JSDOM requires localhost instead of 0.0.0.0 for URL resolution
96143 const address = config . address === "0.0.0.0" ? "localhost" : config . address || "localhost" ;
97144 const url = `http://${ address } :${ port } ` ;
98145
99- const dom = await jsdom . JSDOM . fromURL ( url , { resources : "usable" , runScripts : "dangerously" } ) ;
146+ await openPage ( url ) ;
147+ } ;
100148
101- dom . window . name = "jsdom" ;
102- global . window = dom . window ;
103- global . document = dom . window . document ;
104- // Some modules access navigator.*, so provide a minimal stub for JSDOM-based tests.
105- global . navigator = {
106- useragent : "node.js"
107- } ;
108- dom . window . fetch = fetch ;
149+ exports . waitForElement = async ( selector , ignoreValue = "" , timeout = 0 ) => {
150+ const currentPage = exports . getPage ( ) ;
151+ const locator = currentPage . locator ( selector ) ;
152+ const effectiveTimeout = timeout && timeout > 0 ? timeout : 30000 ;
153+ const deadline = Date . now ( ) + effectiveTimeout ;
109154
110- // fromURL() resolves when HTML is loaded, but with resources: "usable",
111- // external scripts load asynchronously. Wait for the load event to ensure scripts are executed.
112- if ( dom . window . document . readyState !== "complete" ) {
113- await once ( dom . window , "load" ) ;
155+ while ( Date . now ( ) <= deadline ) {
156+ const count = await locator . count ( ) ;
157+ if ( count > 0 ) {
158+ if ( ! ignoreValue ) {
159+ return locator . first ( ) ;
160+ }
161+ const text = await locator . first ( ) . textContent ( ) ;
162+ if ( ! text || ! text . includes ( ignoreValue ) ) {
163+ return locator . first ( ) ;
164+ }
165+ }
166+ await currentPage . waitForTimeout ( 100 ) ;
114167 }
168+
169+ return null ;
115170} ;
116171
117- exports . waitForElement = ( selector , ignoreValue = "" , timeout = 0 ) => {
118- return new Promise ( ( resolve ) => {
119- let oldVal = "dummy12345" ;
120- let element = null ;
121- const interval = setInterval ( ( ) => {
122- element = document . querySelector ( selector ) ;
123- if ( element ) {
124- let newVal = element . textContent ;
125- if ( newVal === oldVal ) {
126- clearInterval ( interval ) ;
127- resolve ( element ) ;
128- } else {
129- if ( ignoreValue === "" ) {
130- oldVal = newVal ;
131- } else {
132- if ( ! newVal . includes ( ignoreValue ) ) oldVal = newVal ;
133- }
134- }
172+ exports . waitForAllElements = async ( selector , timeout = 30000 ) => {
173+ const currentPage = exports . getPage ( ) ;
174+ const locator = currentPage . locator ( selector ) ;
175+ const effectiveTimeout = timeout && timeout > 0 ? timeout : 30000 ;
176+ const deadline = Date . now ( ) + effectiveTimeout ;
177+
178+ while ( Date . now ( ) <= deadline ) {
179+ const count = await locator . count ( ) ;
180+ if ( count > 0 ) {
181+ const elements = [ ] ;
182+ for ( let i = 0 ; i < count ; i ++ ) {
183+ elements . push ( locator . nth ( i ) ) ;
135184 }
136- } , 100 ) ;
137- if ( timeout !== 0 ) {
138- setTimeout ( ( ) => {
139- if ( interval ) clearInterval ( interval ) ;
140- resolve ( null ) ;
141- } , timeout ) ;
185+ return elements ;
142186 }
143- } ) ;
187+ await currentPage . waitForTimeout ( 100 ) ;
188+ }
189+
190+ return [ ] ;
144191} ;
145192
146- exports . waitForAllElements = ( selector ) => {
147- return new Promise ( ( resolve ) => {
148- let oldVal = 999999 ;
149- const interval = setInterval ( ( ) => {
150- const element = document . querySelectorAll ( selector ) ;
151- if ( element ) {
152- let newVal = element . length ;
153- if ( newVal === oldVal ) {
154- clearInterval ( interval ) ;
155- resolve ( element ) ;
156- } else {
157- if ( newVal !== 0 ) oldVal = newVal ;
158- }
159- }
160- } , 100 ) ;
161- } ) ;
193+ exports . testMatch = async ( selector , regex ) => {
194+ await exports . expectTextContent ( selector , { matches : regex } ) ;
195+ return true ;
162196} ;
163197
164- exports . testMatch = async ( element , regex ) => {
165- const elem = await this . waitForElement ( element ) ;
166- expect ( elem ) . not . toBeNull ( ) ;
167- expect ( elem . textContent ) . toMatch ( regex ) ;
198+ exports . querySelector = async ( selector ) => {
199+ const locator = exports . getPage ( ) . locator ( selector ) ;
200+ return ( await locator . count ( ) ) > 0 ? locator . first ( ) : null ;
201+ } ;
202+
203+ exports . querySelectorAll = async ( selector ) => {
204+ const locator = exports . getPage ( ) . locator ( selector ) ;
205+ const count = await locator . count ( ) ;
206+ const elements = [ ] ;
207+ for ( let i = 0 ; i < count ; i ++ ) {
208+ elements . push ( locator . nth ( i ) ) ;
209+ }
210+ return elements ;
211+ } ;
212+
213+ exports . expectTextContent = async ( target , expectation ) => {
214+ if ( ! expectation || ( expectation . equals === undefined && expectation . contains === undefined && expectation . matches === undefined ) ) {
215+ throw new Error ( "expectTextContent expects an object with equals, contains, or matches" ) ;
216+ }
217+
218+ let locator = target ;
219+ if ( typeof target === "string" ) {
220+ locator = await exports . waitForElement ( target ) ;
221+ }
222+
223+ expect ( locator ) . not . toBeNull ( ) ;
224+ if ( ! locator ) {
225+ const description = typeof target === "string" ? target : "supplied locator" ;
226+ throw new Error ( `No element found for ${ description } ` ) ;
227+ }
228+
229+ const textPromise = locator . textContent ( ) ;
230+ if ( expectation . equals !== undefined ) {
231+ await expect ( textPromise ) . resolves . toBe ( expectation . equals ) ;
232+ } else if ( expectation . contains !== undefined ) {
233+ await expect ( textPromise ) . resolves . toContain ( expectation . contains ) ;
234+ } else {
235+ await expect ( textPromise ) . resolves . toMatch ( expectation . matches ) ;
236+ }
168237 return true ;
169238} ;
170239
171240exports . fixupIndex = async ( ) => {
172241 // read and save the git level index file
173242 indexData = ( await fs . promises . readFile ( indexFile ) ) . toString ( ) ;
174243 // make lines of the content
175- let workIndexLines = indexData . split ( os . EOL ) ;
244+ const workIndexLines = indexData . split ( os . EOL ) ;
176245 // loop thru the lines to find place to insert new region
177246 for ( let l in workIndexLines ) {
178247 if ( workIndexLines [ l ] . includes ( "region top right" ) ) {
@@ -191,7 +260,7 @@ exports.fixupIndex = async () => {
191260
192261exports . restoreIndex = async ( ) => {
193262 // if we read in data
194- if ( indexData . length > 1 ) {
263+ if ( indexData . length > 0 ) {
195264 //write out saved index.html
196265 await fs . promises . writeFile ( indexFile , indexData , { flush : true } ) ;
197266 // write out saved custom.css
0 commit comments