@@ -5,6 +5,7 @@ import * as path from "path";
5
5
import * as puppeteer from "puppeteer" ;
6
6
import { ChildProcess , spawn } from "child_process" ;
7
7
import { logger , field } from "@coder/logger" ;
8
+ import { EventEmitter } from 'events' ;
8
9
9
10
interface IServerOptions {
10
11
host : string ;
@@ -88,6 +89,111 @@ class DeserializedNode {
88
89
}
89
90
}
90
91
92
+ /**
93
+ * Wrapper for puppeteer.Keyboard.
94
+ */
95
+ class TestKeyboard {
96
+ public constructor (
97
+ private readonly page : puppeteer . Page ,
98
+ public readonly onKeyboardEvent : ( fnName : string , state : "before" | "after" ) => Promise < void > ,
99
+ ) { }
100
+
101
+ /**
102
+ * Wrapper for sending individual keyboard up/down events
103
+ * to the page.
104
+ */
105
+ public async press ( key : string , options ?: { text ?: string , delay ?: number } ) : Promise < void > {
106
+ await this . onKeyboardEvent ( "press" , "before" ) ;
107
+ const prom = await this . page . keyboard . press ( key , options ) ;
108
+ await this . onKeyboardEvent ( "press" , "after" ) ;
109
+
110
+ return prom ;
111
+ }
112
+
113
+ /**
114
+ * Wrapper for sending keyboard events to the page.
115
+ */
116
+ public async type ( text : string , options ?: { delay ?: number } ) : Promise < void > {
117
+ await this . onKeyboardEvent ( "type" , "before" ) ;
118
+ const prom = await this . page . keyboard . type ( text , options ) ;
119
+ await this . onKeyboardEvent ( "type" , "after" ) ;
120
+
121
+ return prom ;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Wrapper for puppeteer.Page.
127
+ */
128
+ export class TestPage {
129
+ public readonly keyboard : TestKeyboard ;
130
+ private screenshotCount = 0 ;
131
+
132
+ public constructor ( public readonly rootPage : puppeteer . Page , private readonly tag ?: string ) {
133
+ this . keyboard = new TestKeyboard ( rootPage , async ( fnName , state ) : Promise < void > => {
134
+ await this . screenshot ( `keyboard-${ fnName } _${ state } ` ) ;
135
+ } ) ;
136
+ }
137
+
138
+ /**
139
+ * Saves a screenshot of the headless page in the server's
140
+ * working directory. Useful for debugging.
141
+ *
142
+ * Screenshot path will be in the following format:
143
+ * `<screenshotDir>/[<page-tag>_]<screenshot-number>_<screenshot-name>.jpg`.
144
+ */
145
+ public screenshot ( name : string , options ?: puppeteer . ScreenshotOptions ) : Promise < string | Buffer > {
146
+ options = Object . assign ( { path : path . resolve ( TestServer . puppeteerDir , `./${ this . tag ? `${ this . tag } _` : "" } ${ this . screenshotCount } _${ name } .jpg` ) , fullPage : true } , options ) ;
147
+ const img = this . rootPage . screenshot ( options ) ;
148
+ this . screenshotCount ++ ;
149
+
150
+ return img ;
151
+ }
152
+
153
+ /**
154
+ * 1. Take a screenshot.
155
+ * 2. Run the function.
156
+ * 3. Take a screenshot.
157
+ */
158
+ // tslint:disable-next-line:no-any
159
+ public async recordFunc ( fn : Function , ...args : any [ ] ) : Promise < any > {
160
+ await this . screenshot ( `${ fn . name } _before` ) ;
161
+ const result = await fn . apply ( this . rootPage , args ) ;
162
+ await this . screenshot ( `${ fn . name } _after` ) ;
163
+
164
+ return result ;
165
+ }
166
+
167
+ /**
168
+ * Wrapper for puppeteer.Page.click().
169
+ */
170
+ public click ( selector : string , options ?: puppeteer . ClickOptions ) : Promise < void > {
171
+ return this . recordFunc ( this . rootPage . click , selector , options ) ;
172
+ }
173
+
174
+ public waitFor ( duration : number ) : Promise < void > ;
175
+ public waitFor ( selector : string , options ?: puppeteer . WaitForSelectorOptions ) : Promise < puppeteer . ElementHandle > ;
176
+ public waitFor ( selector : string , options : puppeteer . WaitForSelectorOptionsHidden ) : Promise < puppeteer . ElementHandle | null > ;
177
+ /**
178
+ * Wrapper for puppeteer.Page.waitFor()/puppeteer.Page.waitForSelector().
179
+ */
180
+ public waitFor ( durationOrSelector : number | string , options ?: puppeteer . WaitForSelectorOptionsHidden | puppeteer . WaitForSelectorOptions ) : Promise < puppeteer . ElementHandle | null | void > {
181
+ if ( typeof durationOrSelector === "number" ) {
182
+ return this . recordFunc ( this . rootPage . waitFor , durationOrSelector ) ;
183
+ }
184
+
185
+ return this . recordFunc ( this . rootPage . waitFor , durationOrSelector , options ) ;
186
+ }
187
+
188
+ /**
189
+ * Wrapper for puppeteer.Page.$eval().
190
+ */
191
+ // tslint:disable-next-line:no-any
192
+ public $eval < R > ( selector : string , pageFunction : ( element : Element , ...args : any [ ] ) => R | Promise < R > , ...args : puppeteer . SerializableOrJSHandle [ ] ) : Promise < puppeteer . WrapElementHandle < R > > {
193
+ return this . recordFunc ( this . rootPage . $eval , selector , pageFunction , args ) ;
194
+ }
195
+ }
196
+
91
197
/**
92
198
* Wraps common code for end-to-end testing, like starting up
93
199
* the code-server binary.
@@ -101,6 +207,7 @@ export class TestServer {
101
207
private child : ChildProcess ;
102
208
// The directory to load the IDE with.
103
209
public static readonly workingDir = path . resolve ( __dirname , "../tmp/workspace/" ) ;
210
+ public static readonly puppeteerDir = path . resolve ( TestServer . workingDir , "../puppeteer/" , Date . now ( ) . toString ( ) ) ;
104
211
105
212
public constructor ( opts ?: {
106
213
host ?: string ,
@@ -156,6 +263,9 @@ export class TestServer {
156
263
if ( ! fs . existsSync ( TestServer . workingDir ) ) {
157
264
fs . mkdirSync ( TestServer . workingDir , { recursive : true } ) ;
158
265
}
266
+ if ( ! fs . existsSync ( TestServer . puppeteerDir ) ) {
267
+ fs . mkdirSync ( TestServer . puppeteerDir , { recursive : true } ) ;
268
+ }
159
269
this . child = spawn ( this . options . binaryPath , args , { cwd : TestServer . workingDir } ) ;
160
270
if ( ! this . child . stdout ) {
161
271
await this . dispose ( ) ;
@@ -198,7 +308,7 @@ export class TestServer {
198
308
* to emit the "ide-ready" event. After which, the page
199
309
* should be interactive.
200
310
*/
201
- public async loadPage ( page : puppeteer . Page ) : Promise < puppeteer . Page > {
311
+ public async loadPage ( page : puppeteer . Page , tag ?: string ) : Promise < TestPage > {
202
312
if ( ! page ) {
203
313
throw new Error ( `cannot load page, ${ JSON . stringify ( page ) } ` ) ;
204
314
}
@@ -209,7 +319,7 @@ export class TestServer {
209
319
} ) ;
210
320
} ) ;
211
321
212
- return page ;
322
+ return new TestPage ( page , tag ) ;
213
323
}
214
324
215
325
/**
@@ -249,14 +359,14 @@ export class TestServer {
249
359
* runner context.
250
360
*/
251
361
// tslint:disable-next-line:no-any
252
- public async querySelectorAll ( page : puppeteer . Page , selector : string ) : Promise < Array < DeserializedNode > > {
362
+ public async querySelectorAll ( page : TestPage , selector : string ) : Promise < Array < DeserializedNode > > {
253
363
if ( ! selector ) {
254
364
throw new Error ( "selector undefined" ) ;
255
365
}
256
366
257
367
// tslint:disable-next-line:no-any
258
368
const elements : Array < DeserializedNode > = [ ] ;
259
- const serializedElements = await page . evaluate ( ( selector ) => {
369
+ const serializedElements = await page . rootPage . evaluate ( ( selector ) => {
260
370
// tslint:disable-next-line:no-any
261
371
return new Promise < Array < string > > ( ( res , rej ) : void => {
262
372
const elements = Array . from ( document . querySelectorAll ( selector ) ) ;
@@ -286,7 +396,7 @@ export class TestServer {
286
396
* Get an element on the page. See `TestServer.querySelectorAll`.
287
397
*/
288
398
// tslint:disable-next-line:no-any
289
- public async querySelector ( page : puppeteer . Page , selector : string ) : Promise < DeserializedNode > {
399
+ public async querySelector ( page : TestPage , selector : string ) : Promise < DeserializedNode > {
290
400
if ( ! selector ) {
291
401
throw new Error ( "selector undefined" ) ;
292
402
}
@@ -302,30 +412,17 @@ export class TestServer {
302
412
* See puppeteer docs for key-code definitions:
303
413
* https://github.com/GoogleChrome/puppeteer/blob/master/lib/USKeyboardLayout.js
304
414
*/
305
- public async pressKeyboardCombo ( page : puppeteer . Page , ...keys : string [ ] ) : Promise < void > {
415
+ public async pressKeyboardCombo ( page : TestPage , ...keys : string [ ] ) : Promise < void > {
306
416
if ( ! keys || keys . length === 0 ) {
307
417
throw new Error ( "no keys provided" ) ;
308
418
}
309
419
// Press the keys.
310
420
for ( let i = 0 ; i < keys . length ; i ++ ) {
311
- await page . keyboard . down ( keys [ i ] ) ;
421
+ await page . rootPage . keyboard . down ( keys [ i ] ) ;
312
422
}
313
423
// Release the keys.
314
424
for ( let x = 0 ; x < keys . length ; x ++ ) {
315
- await page . keyboard . up ( keys [ x ] ) ;
316
- }
317
- }
318
-
319
- /**
320
- * Saves a screenshot of the headless page in the server's
321
- * working directory. Useful for debugging.
322
- */
323
- public async screenshot ( page : puppeteer . Page , id : string ) : Promise < void > {
324
- const puppeteerDir = path . resolve ( TestServer . workingDir , "../puppeteer/" ) ;
325
- if ( ! fs . existsSync ( puppeteerDir ) ) {
326
- fs . mkdirSync ( puppeteerDir , { recursive : true } ) ;
425
+ await page . rootPage . keyboard . up ( keys [ x ] ) ;
327
426
}
328
- const screenshotPath = path . resolve ( puppeteerDir , `./screenshot-${ id } .jpg` ) ;
329
- await page . screenshot ( { path : screenshotPath , fullPage : true } ) ;
330
427
}
331
428
}
0 commit comments