/ *
Helpers for the tests in ` tests/ ` that does things like deal with
[ Playwright ] ( https : //playwright.dev) setup, waiting for things in
/ p a g e s , f i l l n g o u t f o r m s , a n d m a k i n g f a k e u s e r s . F o r a g o o d
example of using almost everything look at ` tests/ui/registration.js ` .
# # # Usage
I like to pull out each function I use separately , then set everything up like this :
` ` ` javascript
import { sleep , expect , tid , playstart , playstop , form , wait } from '../../lib/testing.js' ;
import { base _host } from "../../lib/config.js" ;
import { knex } from '../../lib/ormish.js' ;
test . before ( async t => t . context = await playstart ( ` ${ base _host } /client/#/register/ ` ) ) ;
test . after ( async t => {
knex . destroy ( ) ;
await playstop ( t . context . browser , t . context . p ) ;
} ) ;
` ` `
This gets ` Playwright ` setup to start at the right URL for the test , and makes sure that
it 's stopped with `playstop`. This is important so that coverage works. You' ll also
notice that I 'm manually doing `knex.destroy()`. If you don' t do this then your testing
runs will hang while Node waits for ` knex ` to exit .
* /
import playwright from 'playwright' ;
import faker from 'faker' ;
import random from 'random' ;
import assert from "assert" ;
import { v4 as uuid } from "uuid" ;
import fs from "fs" ;
import url from "url" ;
import path from "path" ;
import { User } from "./models.js" ;
const WAIT _TIME = 5000 ;
const HEADLESS = ! process . env . PLAYVIEW ;
const COVERAGE = process . env . NODE _V8 _COVERAGE ;
/ *
Generates the CSS selector to match the ` data-testid ` for finding HTML
tags in tests .
* /
export const tid = ( name ) => ` [data-testid=" ${ name } "] ` ;
/ *
Fills in a form using the id = "" of each field , then click
the button basedon the ` tid(button_id) ` . For example , here ' s
how the stock registration form is tested :
` ` ` javascript
await form ( p , {
"#email" : user . email ,
"#full_name" : user . full _name ,
"#initials" : "XXX" ,
"#password" : user . password ,
"#password_repeat" : user . password ,
"#tos_agree" : true ,
} , tid ( "register-button" ) ) ;
` ` `
_ _FOOTGUN _ _ : This only works with one form on the page . If you have
multiple forms then it ' ll probably fill in fields randomly based on
the ids .
+ ` page Object ` -- [ Playwright ] ( https : //playwright.dev) page object.
+ ` fields Object ` -- Mapping of "#field-id" to value to type .
+ ` button_id string ` ( _ _ _optional _ _ _ ) -- The name of the button to press to submit ( or anything ) . This is passed to ` tid() ` so do not include the "#" .
* /
export const form = async ( page , fields , button _id ) => {
for ( const [ key , value ] of Object . entries ( fields ) ) {
if ( typeof value === "boolean" ) {
if ( value ) {
await page . check ( key ) ;
} else {
await page . uncheck ( key ) ;
}
} else {
// it's another input to fill
await page . fill ( key , value ) ;
}
}
if ( button _id ) {
await page . click ( button _id ) ;
}
}
/ *
Sets up a [ Playwright ] ( https : //playwright.dev) instance that uses Chromium,
handles HEADLESS or not , starts at a starting URL , and sets up any coverage
requirements .
# # # Headless PLAYVIEW
The code for whether to show the browser window or not is based on if you set
` PLAYVIEW=1 ` environment variables then you ' ll see the browser windows . This
makes debugging easier .
# # # Coverage
Coverage is determined by whether you set ` NODE_V8_COVERAGE=.coverage ` environment variables .
This is specific to Node . js but that 's the only thing I' ve tested . Setting this will
enable coverage information in the _ _browser _ _ , which is important for coverage analysis
during testing . You also need to set this in the back APIs as well , but the ` package.json `
has setups for this too .
+ ` url string ` -- The starting URL for the Playwright instance .
+ ` {browser, context, p: page} ` -- Almost everything you need right away .
* /
export const playstart = async ( url ) => {
if ( ! HEADLESS ) console . log ( "Running playwright in visible mode (not HEADLESS)." ) ;
if ( COVERAGE ) console . log ( ` Coverage reports going to ${ COVERAGE } . ` ) ;
const browser = await playwright [ 'chromium' ] . launch ( { headless : HEADLESS } ) ;
const context = await browser . newContext ( ) ;
const p = await context . newPage ( ) ;
if ( COVERAGE ) {
await p . coverage . startJSCoverage ( ) ;
}
p . on ( 'console' , async msg => {
console . debug ( ` CONSOLE ${ msg . type ( ) } >> ` , msg )
} ) ;
await p . goto ( url ) ;
return { browser , context , p } ;
}
/ *
Properly stops [ Playwright ] ( https : //playwright.dev) if you have coverage enabled (see `playstop`).
There ' s a lot of things you need to do when you stop coverage , so use this or your
coverage will not actually be saved . The file is saved in the directory specified by
` NODE_V8_COVERAGE ` , which is usually ` .coverage ` .
+ ` browser Object ` -- Playwright browser object from ` playstart ` .
+ ` p Object ` -- Playwright page object from ` playstart ` .
* /
export const playstop = async ( browser , p ) => {
if ( COVERAGE ) {
const coverage = await p . coverage . stopJSCoverage ( ) ;
for ( const entry of coverage ) {
entry . url = path . join ( process . cwd ( ) , "public" , url . parse ( entry . url ) . pathname ) ;
}
const v8out = { result : coverage , timestamp : Date . now ( ) } ;
fs . writeFileSync ( ` ${ COVERAGE } / ${ uuid ( ) } .json ` , JSON . stringify ( v8out ) ) ;
}
await browser . close ( ) ;
}
/ *
Waits for the ` css_id ` selector to be visible on the page . Most external
browser controller systems are incredibly annoying because they ' re based on
timing and aysnc waiting for things in the page . This does all the annoying
waiting and logging an error message so you can figure out what ' s going on .
+ ` p Object ` -- Playwright page object .
+ ` css_id string ` -- CSS ID to wait for , use ` tid() ` to use a ` data-testid ` .
* /
export const wait = async ( p , css _id ) => {
try {
await p . waitForSelector ( css _id , { timeout : WAIT _TIME } ) ;
} catch ( error ) {
console . error ( error , ` !!!!!!!!!!!!!!!!!!!!!!!!!!!! Failed waiting for id ${ css _id } ` ) ;
throw error ;
}
}
/ *
Uses the [ Ava ] ( https : //github.com/avajs/ava) test variable `t` to confirm
that a tag with the given ` css_id ` exists . It will wait for this ` css_id ` ,
get the tag for it , then return the ` tag.textContent() ` . You can then do
additional tests to make sure it contains the right content .
+ ` t Object ` -- Ava test object .
+ ` p Object ` -- Playwright page object .
+ ` css_id string ` -- Use a ` tid() ` to target a ` data-testid ` .
* /
export const expect = async ( t , p , css _id ) => {
await wait ( p , css _id ) ;
let tag = await p . $ ( css _id ) ;
let text = tag ? await tag . textContent ( ) : "NULL!" ;
t . true ( text !== null ) ;
return text ;
}
/ *
Due to the timing of requests from the browser to the backend
you sometimes have to wait for data to get stored and things to
happen . This will do that by returning a ` Promise ` that resolves
after a certain ` ms ` miliseconds .
+ ` ms number ` -- Miliseconds to wait , passed to ` setTimeout ` .
+ _ _ _return _ _ _ ` Promise ` -- await on this .
* /
export const sleep = ( ms ) => {
return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
}
/ *
Generate a random user for use in login forms and other things .
+ return { email , full _name , initials , password }
* /
export const random _user = ( ) => {
let email = faker . internet . email ( ) ;
let rnd = random . int ( 0 , 10 * 1000 ) ;
email = email . replace ( '@' , ` ${ rnd } @ ` ) ;
let full _name = faker . name . findName ( ) ;
let initials = full _name . split ( ' ' ) . map ( x => x [ 0 ] ) . join ( '' ) ;
let password = faker . internet . password ( ) ;
assert ( password , "Looks like faker is failing at passwords again." ) ;
return {
email ,
full _name ,
initials ,
password
}
}
/ *
Generates a ` random_user ` and then register it in the database .
It will add the field ` user.raw_password ` to the user that ' s returned
so you can use the password in testing .
+ ` is_admin boolean (false) ` -- Should this user be an admin or not ?
+ _ _ _return _ _ _ ` User ` -- The ` User ` object from ` lib/models.js ` .
* /
export const register _user = async ( is _admin = false ) => {
const rando = random _user ( ) ;
rando . password _repeat = rando . password ;
// copy it to keep the original password
const user = await User . register ( { ... rando } ) ;
if ( is _admin ) {
// make this user an admin
user . admin = true ;
await User . update ( { id : user . id } , { admin : true } ) ;
}
// then replace it so we can use it later
user . raw _password = rando . password ;
return user ;
}
/ *
A helper function for other tests to use logins .
_ _ _FOOTGUN _ _ _ : It must have ` email ` and ` raw_password ` in it for this to work . The ` register_user ` function sets this up for you but if you want to query or craft your own ` User ` you ' ll need to ensure that .
+ ` t Object ` -- Ava testing object .
+ ` p Object ` -- Playwright page object .
+ ` with_user User ` -- A ` User ` object from ` lib/models.js ` .
* /
export const login = async ( t , p , with _user ) => {
const user = with _user ? with _user : await register _user ( ) ;
await expect ( t , p , tid ( "login-form" ) ) ;
// fill out correct form, and redirect to watch
await p . fill ( '#email' , user . email ) ;
await p . fill ( "#password" , user . raw _password ) ;
await p . click ( tid ( 'login-button' ) ) ;
await sleep ( 1000 ) ;
return user ;
}