|
|
|
@ -1,3 +1,31 @@ |
|
|
|
|
/* |
|
|
|
|
Helpers for the tests in `tests/` that does things like deal with |
|
|
|
|
[Playwright](https://playwright.dev) setup, waiting for things in
|
|
|
|
|
/pages, fillng out forms, and making fake users. For a good |
|
|
|
|
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'; |
|
|
|
@ -12,8 +40,36 @@ 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") { |
|
|
|
@ -33,6 +89,28 @@ export const form = async (page, fields, 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}.`); |
|
|
|
@ -48,10 +126,20 @@ export const playstart = async (url) => { |
|
|
|
|
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(); |
|
|
|
@ -65,6 +153,15 @@ export const playstop = async (browser, p) => { |
|
|
|
|
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 }); |
|
|
|
@ -74,6 +171,16 @@ export const wait = async (p, css_id) => { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* |
|
|
|
|
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); |
|
|
|
|
|
|
|
|
@ -83,10 +190,24 @@ export const expect = async (t, p, css_id) => { |
|
|
|
|
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); |
|
|
|
@ -105,6 +226,14 @@ export const random_user = () => { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* |
|
|
|
|
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; |
|
|
|
@ -121,7 +250,15 @@ export const register_user = async (is_admin=false) => { |
|
|
|
|
return user; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* A helper function for other tests to use logins. */ |
|
|
|
|
/* |
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
|