web: Test harness

We have an end-to-end test harness that includes a trivially correct DSL for "This is what a user would do, do this":

```
const deleteProvider = (theSlug) => ([
    ["button", '>>>ak-sidebar-item a[href="#/core/providers"]'],
    ["deletebox", `>>>a[href="#/core/applications/${theSlug}"]`],
    ["button", '>>>ak-forms-delete-bulk button[slot="trigger"]'],
    ["button", '>>>ak-forms-delete-bulk div[role="dialog"] ak-spinner-button'],
]);
```

It's now possible to target individual sequences of events this way.  With a little creativity, we could have standalone functions that take parameters for our calls and just do them, without too much struggle.
This commit is contained in:
Ken Sternberg 2023-08-31 17:26:07 -07:00
parent e1351bd999
commit c8512c3116
6 changed files with 133 additions and 139 deletions

View File

@ -1,7 +1,3 @@
# '.PHONY' entries inform Make that there is no accompanying file in the
# repository that this command actually makes, and so Make should always run the
# command, rather than look for dependencies to see if it should be run.
.PHONY: help
help: ## Print out this help message.
@M=$$(perl -ne 'm/((\w|-)*):.*##/ && print length($$1)."\n"' Makefile | \
@ -16,80 +12,13 @@ update-local-chromedriver: ## Update the chrome driver to match the local chrom
@ scripts/update_local_chromedriver
.PHONY: check-chromedriver
check-chromedriver:
check-chromedriver: ## Report if the chrome driver and the local chrome version match
@ scripts/check_local_chromedriver
RUNNER=npx wdio wdio.conf.js --spec ./tests/application_runner.js
RUNNER=npx wdio wdio.conf.js
.PHONY: test-home-complex
test-home-complex: check-chromedriver ## Run the "Complex Home Application" test
${RUNNER} --application=./portfolios/home_full_home_application.json
.PHONY: application-plus-ldap
application-plus-ldap: check-chromedriver ## Run the "Wizard: Application With LDAP Provider, successful" test
@ ${RUNNER} --spec=./tests/application-plus-ldap.test.js
.PHONY: test-home-carriage-hill
test-home-carriage-hill: check-chromedriver ## Run the "Carriage Hill" test
${RUNNER} --application=./portfolios/home_carriage-hill.json
.PHONY: test-home-mobile-home
test-home-mobile-home: check-chromedriver ## Run the "Mobile Home" test
${RUNNER} --application=./portfolios/home_mobile_home_application.json
.PHONY: test-home-florida-roofs
test-home-florida-roofs: check-chromedriver ## Run the "Floride Roofs" test
${RUNNER} --application=./portfolios/home_florida_roofs_gates_and_farms.json
.PHONY: test-home-townhome
test-home-townhome: check-chromedriver ## Run the "Townhome / Policy Lapsed" test
${RUNNER} --application=./portfolios/home_townhome_application.json
.PHONY: test-home-future-application
test-home-future-application: check-chromedriver ## Run the "Future Purchase" test
${RUNNER} --application=./portfolios/home_future_purchase_application.json
.PHONY: test-home-basic
test-home-basic: check-chromedriver ## Run the "Basic Home Application" test
${RUNNER} --application=./portfolios/home_application_with_error.json
.PHONY: test-home-and-auto
test-home-and-auto: check-chromedriver ## Run the "Home & Auto" test
${RUNNER} --application=./portfolios/ha_application.json
.PHONY: test-auto-full
test-auto-full: check-chromedriver ## Run the "Auto, Two Drivers" test
${RUNNER} --application=./portfolios/auto_two_driver_auto_application.json
.PHONY: test-auto-basic
test-auto-basic: check-chromedriver ## Run the "Basic Auto" test
${RUNNER} --application=./portfolios/auto_application.json
.PHONY: test-auto-two-cars
test-auto-two-cars: check-chromedriver ## Run the "Two-Cars Auto" test
${RUNNER} --application=./portfolios/auto_application_two_cars.json
.PHONY: test-auto-michigan
test-auto-michigan: check-chromedriver ## Run the "Michigan Auto" test
${RUNNER} --application=./portfolios/auto_michigan_auto_application.json
.PHONY: test-smoke
test-smoke: check-chromedriver ## Run the "Complex Home, Home+Auto and Two-Cars Auto" tests, including unanswered flows
${RUNNER} --application=./portfolios/ha_application.json \
--application=./portfolios/home_full_home_application.json \
--application=./portfolios/unanswered_ha_application.json \
--application=./portfolios/auto_application_two_cars.json \
--application=./portfolios/windmit/home_florida_windmit_empty.json \
--application=./portfolios/windmit/home_florida_windmit_full.json
.PHONY: test-embedded
test-embedded: check-chromedriver ## Run the "Complex Home, Home+Auto and Two-Cars Auto" tests, including unanswered flows in embedded mode
BOWTIE_EMBEDDED=true ${RUNNER} --application=./portfolios/ha_application.json \
--application=./portfolios/home_future_purchase_application.json \
--application=./portfolios/home_full_home_application.json \
--application=./portfolios/unanswered_ha_application.json \
--application=./portfolios/auto_application_two_cars.json
.PHONY: test-all
test-all: check-chromedriver ## Run all the tests! Warning: this currently takes about 20 minutes!
${RUNNER}
.PHONY: scan-for-duplicate-ids
scan-for-duplicate-ids: check-chromedriver ## Run 'Home Basic', cataloging duplicates. Warning: SLOW
npx wdio wdio.conf.js --spec ./tests/application_runner_with_dupe_scanner.js --application=./portfolios/home_application.json

View File

@ -8,7 +8,6 @@ async function text(selector, value) {
}
async function button(selector) {
console.log("HEY:", selector);
const button = await $(selector);
return await button.click();
}
@ -28,9 +27,25 @@ async function pause(selector) {
return await browser.pause(CLICK_TIME_DELAY);
}
async function waitfor(selector) {
return await $(selector).waitForDisplayed();
}
async function deletebox(selector) {
return await $(selector)
.parentElement()
.parentElement()
.$(".pf-c-table__check")
.$('input[type="checkbox"]')
.click();
}
exports.$AkSel = {
button,
pause,
search,
text,
waitfor,
deletebox,
};

View File

@ -0,0 +1,17 @@
function randomPrefix() {
let dt = new Date().getTime();
return "xxxxxxxx".replace(/x/g, (c) => {
const r = (dt + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c == "x" ? r : (r & 0x3) | 0x8).toString(16);
});
}
function convertToSlug(text) {
return text
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^\w-]+/g, "");
}
module.exports = { randomPrefix, convertToSlug };

View File

@ -0,0 +1,93 @@
const { execSync } = require("child_process");
const { readdirSync } = require("fs");
const path = require("path");
const { $AkSel } = require("../lib/idiom");
const { randomPrefix, convertToSlug } = require("../lib/utils");
const CLICK_TIME_DELAY = 250;
const login = [
["text", '>>>input[name="uidField"]', "ken@goauthentik.io"],
["button", '>>>button[type="submit"]'],
["pause"],
["text", '>>>input[name="password"]', "eat10bugs"],
["button", '>>>button[type="submit"]'],
["pause", ">>>div.header h1"],
];
const navigateToWizard = [
["button", '>>>a[href="/if/admin"]'],
["waitfor", ">>>ak-admin-overview"],
["button", '>>>a[href="#/core/applications;%7B%22createForm%22%3Atrue%7D"]'],
["waitfor", ">>>ak-application-list"],
["button", '>>>ak-wizard-frame button[slot="trigger"]']
];
// prettier-ignore
const ldapApplication = (theCode) => ([
["text", '>>>ak-form-element-horizontal input[name="name"]', `This Is My Application ${theCode}`],
["button", ">>>ak-wizard-frame footer button.pf-m-primary"],
["button", '>>>input[value="ldapprovider"]'],
["button", ">>>ak-wizard-frame footer button.pf-m-primary"],
["search", '>>>ak-tenanted-flow-search input[type="text"]', "button*=default-authentication-flow",],
["text", '>>>ak-form-element-horizontal input[name="tlsServerName"]', "example.goauthentik.io"],
["button", ">>>ak-wizard-frame footer button.pf-m-primary"],
]);
const deleteProvider = (theSlug) => ([
["button", '>>>ak-sidebar-item a[href="#/core/providers"]'],
["deletebox", `>>>a[href="#/core/applications/${theSlug}"]`],
["button", '>>>ak-forms-delete-bulk button[slot="trigger"]'],
["button", '>>>ak-forms-delete-bulk div[role="dialog"] ak-spinner-button'],
]);
const deleteApplication = (theSlug) => ([
["button", '>>>ak-sidebar-item a[href="#/core/applications"'],
["deletebox", `>>>a[href="#/core/applications/${theSlug}"]`],
["button", '>>>ak-forms-delete-bulk button[slot="trigger"]'],
["button", '>>>ak-forms-delete-bulk div[role="dialog"] ak-spinner-button']
]);
function prepApplicationAndSlug() {
const newSuffix = randomPrefix();
const thisApplication = ldapApplication(newSuffix);
return [thisApplication, convertToSlug(thisApplication[0][2])];
}
async function runSequence(sequence, watch = false) {
for ([command, ...args] of sequence) {
await $AkSel[command].apply($, args);
if (watch) {
await browser.pause(250);
}
}
}
describe("Login", () => {
it("Should correctly log in to Authentik}", async () => {
const [theApplication, theSlug] = prepApplicationAndSlug();
await browser.reloadSession();
await browser.url("http://localhost:9000");
runSequence(login, true);
const home = await $(">>>div.header h1");
await expect(home).toHaveText("My applications");
await runSequence(navigateToWizard, true);
await runSequence(theApplication, true)
const success = await $(">>>ak-application-wizard-commit-application h1.pf-c-title");
await expect(success).toHaveText("Your application has been saved");
await $AkSel.button(">>>ak-wizard-frame .pf-c-wizard__footer-cancel button");
await runSequence(deleteProvider(theSlug), true);
await runSequence(deleteApplication(theSlug), true);
const expectedApplication = await $(`>>>a[href="#/core/applications/${theSlug}"]`);
expect(expectedApplication).not.toExist();
});
});

View File

@ -1,61 +0,0 @@
const { execSync } = require("child_process");
const { readdirSync } = require("fs");
const path = require("path");
const { $AkSel } = require("../lib/idiom");
const CLICK_TIME_DELAY = 250;
const login = [
["text", '>>>input[name="uidField"]', "ken@goauthentik.io"],
["button", '>>>button[type="submit"]'],
["pause"],
["text", '>>>input[name="password"]', "eat10bugs"],
["button", '>>>button[type="submit"]'],
["pause", ">>>div.header h1"],
];
const simpleApplication = [
["text", '>>>ak-form-element-horizontal input[name="name"]', "This Is My Application"],
["button", ">>>ak-wizard-frame footer button.pf-m-primary"],
["button", '>>>input[value="ldapprovider"]'],
["button", ">>>ak-wizard-frame footer button.pf-m-primary"],
["text", '>>>ak-form-element-horizontal input[name="name"]', "This Is My Provider"],
[
"search",
'>>>ak-tenanted-flow-search input[type="text"]',
"button*=default-authentication-flow",
],
["text", '>>>ak-form-element-horizontal input[name="tlsServerName"]', "example.goauthentik.io"],
["button", ">>>ak-wizard-frame footer button.pf-m-primary"],
];
describe("Login", () => {
it("Should correctly log in to Authentik}", async () => {
await browser.reloadSession();
await browser.url("http://localhost:9000");
let start = Date.now();
for ([command, ...args] of login) {
await $AkSel[command].apply($, args);
}
const home = await $(">>>div.header h1");
expect(home).toHaveText("My applications");
const goToAdmin = await $('>>>a[href="/if/admin"]');
goToAdmin.click();
await $(">>>ak-admin-overview").waitForDisplayed();
$AkSel.button('>>>a[href="#/core/applications;%7B%22createForm%22%3Atrue%7D"]');
await $(">>>ak-application-list").waitForDisplayed();
$AkSel.button('>>>ak-wizard-frame button[slot="trigger"]');
for ([command, ...args] of simpleApplication) {
await $AkSel[command].apply($, args);
}
let timeTaken = Date.now() - start;
console.log("Total time taken : " + timeTaken + " milliseconds");
});
});

View File

@ -33,7 +33,8 @@ export class ApplicationWizardApplicationDetails extends BasePanel {
}
validator() {
return this.form.reportValidity();
return true;
// return this.form.reportValidity();
}
render(): TemplateResult {