Testing helpers are done.

main
Zed A. Shaw 2 years ago
parent 5dfa9ebc6b
commit ded2719236
  1. 139
      lib/testing.js

@ -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 playwright from 'playwright';
import faker from 'faker'; import faker from 'faker';
import random from 'random'; import random from 'random';
@ -12,8 +40,36 @@ const WAIT_TIME = 5000;
const HEADLESS = !process.env.PLAYVIEW; const HEADLESS = !process.env.PLAYVIEW;
const COVERAGE = process.env.NODE_V8_COVERAGE; 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}"]`; 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) => { export const form = async (page, fields, button_id) => {
for(const [key, value] of Object.entries(fields)) { for(const [key, value] of Object.entries(fields)) {
if(typeof value === "boolean") { 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) => { export const playstart = async (url) => {
if(!HEADLESS) console.log("Running playwright in visible mode (not HEADLESS)."); if(!HEADLESS) console.log("Running playwright in visible mode (not HEADLESS).");
if(COVERAGE) console.log(`Coverage reports going to ${COVERAGE}.`); if(COVERAGE) console.log(`Coverage reports going to ${COVERAGE}.`);
@ -48,10 +126,20 @@ export const playstart = async (url) => {
p.on('console', async msg => { p.on('console', async msg => {
console.debug(`CONSOLE ${msg.type()}>>`, msg) console.debug(`CONSOLE ${msg.type()}>>`, msg)
}); });
await p.goto(url); await p.goto(url);
return {browser, context, p}; 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) => { export const playstop = async (browser, p) => {
if(COVERAGE) { if(COVERAGE) {
const coverage = await p.coverage.stopJSCoverage(); const coverage = await p.coverage.stopJSCoverage();
@ -65,6 +153,15 @@ export const playstop = async (browser, p) => {
await browser.close(); 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) => { export const wait = async (p, css_id) => {
try { try {
await p.waitForSelector(css_id, { timeout: WAIT_TIME }); 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) => { export const expect = async (t, p, css_id) => {
await wait(p, css_id); await wait(p, css_id);
@ -83,10 +190,24 @@ export const expect = async (t, p, css_id) => {
return text; 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) => { export const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, 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 = () => { export const random_user = () => {
let email = faker.internet.email(); let email = faker.internet.email();
let rnd = random.int(0, 10 * 1000); 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) => { export const register_user = async (is_admin=false) => {
const rando = random_user(); const rando = random_user();
rando.password_repeat = rando.password; rando.password_repeat = rando.password;
@ -121,7 +250,15 @@ export const register_user = async (is_admin=false) => {
return user; 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) => { export const login = async (t, p, with_user) => {
const user = with_user ? with_user : await register_user(); const user = with_user ? with_user : await register_user();

Loading…
Cancel
Save