This is the code that runs https://bandolier.learnjsthehardway.com/ for you to review. It uses the https://git.learnjsthehardway.com/learn-javascript-the-hard-way/bandolier-template to create the documentation for the project.
https://bandolier.learnjsthehardway.com/
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
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;
|
|
}
|
|
|