You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

273 lines
9.0 KiB

/*
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';
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;
}