Skip to content

Commit

Permalink
Set up cypress and add campaign and sign up tests
Browse files Browse the repository at this point in the history
  • Loading branch information
matteosb committed Apr 27, 2020
1 parent e92fbdf commit e932766
Show file tree
Hide file tree
Showing 18 changed files with 725 additions and 63 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ coverage/
package-lock.json
CONFIG_FILE.json
scratch/
cypress/screenshots
cypress/videos
22 changes: 22 additions & 0 deletions __test__/cypress/fixtures/test-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default {
users: {
admin1: {
name: "admin1",
email: "[email protected]",
password: "SpokeAdmin1!",
first_name: "Admin1First",
last_name: "Admin1Last",
cell: "5555550000",
role: "OWNER"
},
texter1: {
name: "texter1",
email: "[email protected]",
password: "SpokeTexter1!",
first_name: "Texter1First",
last_name: "Texter1Last",
cell: "5555550001",
role: "TEXTER"
}
}
};
3 changes: 3 additions & 0 deletions __test__/cypress/fixtures/two-contacts.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
first_name,last_name,cell,favorite_color
ContactFirst1,ContactLast1,12025550175,green
ContactFirst2,ContactLast2,12025550176,orange
103 changes: 103 additions & 0 deletions __test__/cypress/integration/basic-campaign-e2e.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import TestData from "../fixtures/test-data";

describe("End-to-end campaign flow", () => {
before(() => {
// ensure texter one exists so they can be assigned
cy.task("createOrUpdateUser", TestData.users.texter1);
});

it("with an assigned texter", () => {
// ADMIN
const campaignTitle = `E2E basic flow ${new Date().getTime()}`;
const campaignDescription = "Basic campaign with assignments";

cy.login("admin1");
cy.visit("/");
cy.get("button[data-test=addCampaign]").click();

// Fill out basics
cy.get("input[data-test=title]").type(campaignTitle);
cy.get("input[data-test=description]").type(campaignDescription);
cy.get("input[data-test=dueBy]").click();

// Very brittle DatePicker interaction to pick the first day of the next month
// Note: newer versions of Material UI appear to have better hooks for integration
// testing.
cy.get(
"body > div:nth-child(5) > div > div:nth-child(1) > div > div > div > div > div:nth-child(2) > div:nth-child(1) > div:nth-child(1) > button:nth-child(3)"
).click();
cy.get("button")
.contains("1")
.click();
// wait for modal to get dismissed, see if there is a better way to do this
cy.wait(200);
cy.get("[data-test=campaignBasicsForm]").submit();

// Upload Contacts
cy.get("#contact-upload").attachFile("two-contacts.csv");
cy.get("button[data-test=submitContactsCsvUpload]").click();

// Assignments
// Note: Material UI v0 AutoComplete component appears to require a click on the element
// later versions should just allow you to hit enter
cy.get("input[data-test=texterSearch]").type("Texter1First");
// see if there is a better way to select the search result
cy.get("body")
.contains("Texter1First Texter1Last")
.click();
cy.get("input[data-test=autoSplit]").click();
cy.get("button[data-test=submitCampaignTextersForm]").click();

// Interaction Steps
cy.get("textarea[data-test=editorInteraction]").click();
cy.get(".DraftEditor-root").type(
"Hi {{}firstName{}} this is {{}texterFirstName{}}, how are you?"
);
cy.get("button[data-test=scriptDone]").click();
cy.get("input[data-test=questionText]").type("How are you?");
cy.get("button[data-test=addResponse]").click();
cy.get("input[data-test=answerOption]").type("Good");
cy.get("textarea[data-test=editorInteraction]")
.eq(1)
.click();
cy.get(".DraftEditor-root").type("Great!");
cy.get("button[data-test=scriptDone]").click();
cy.get("button[data-test=interactionSubmit]").click();
cy.get("button[data-test=startCampaign]").click();
cy.get("div")
.contains("This campaign is running")
.should("exist");

cy.url().then(url => {
const campaignId = url.match(/campaigns\/(\d+)/)[1];
// TEXTER
cy.login("texter1");
cy.visit("/app");
const cardSelector = `div[data-test=assignmentSummary-${campaignId}]`;
cy.get(cardSelector)
.contains(campaignTitle)
.should("exist");
cy.get(cardSelector)
.contains(campaignDescription)
.should("exist");
cy.get(cardSelector)
.find("button[data-test=sendFirstTexts]")
.click();
cy.get("textArea[name=messageText]").then(el => {
expect(el).to.have.text(
"Hi Contactfirst1 this is Texter1first, how are you?"
);
});
cy.get("button[data-test=send]").click();
cy.get("textArea[name=messageText]").then(el => {
expect(el).to.have.text(
"Hi Contactfirst2 this is Texter1first, how are you?"
);
});
cy.get("button[data-test=send]").click();
// Go back to TODOS
cy.wait(200);
cy.url().should("include", "/todos");
});
});
});
37 changes: 37 additions & 0 deletions __test__/cypress/integration/local-auth.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Don't run this
describe("Login with the local passport strategy", () => {
const ts = new Date().getTime();

beforeEach(() => {
cy.visit("/");
});

it("Sign up", () => {
cy.get("#login").click();
cy.get("button[name='signup']").click();
cy.get("input[name='email']").type(`spoke.itest.${ts}@example.com`);
cy.get("input[name='firstName']").type("SignupTestUserFirst");
cy.get("input[name='lastName']").type("SignupTestUserLast");
cy.get("input[name='cell']").type("5555551234");
cy.get("input[name='password']").type("SignupTestUser1!", { delay: 10 });
cy.get("input[name='passwordConfirm']").type("SignupTestUser1!", {
delay: 10
});
cy.get("[data-test=userEditForm]").submit();
// The next page is different depending on whether SUPPRESS_SELF_INVITE is
// set, so we just assert that we are not still on the login page
// the wait is required because cypress doesn't know how long to wait for the url to change
cy.wait(500);
cy.url().then(url => expect(url).not.to.match(/.*login.*/));
});

// sign in as the user
it("Sign in", () => {
cy.get("#login").click();
cy.get("input[name='email']").type(`spoke.itest.${ts}@example.com`);
cy.get("input[name='password']").type("SignupTestUser1!", { delay: 10 });
cy.get("[data-test=userEditForm]").submit();
cy.wait(500);
cy.url().then(url => expect(url).not.to.match(/.*login.*/));
});
});
22 changes: 22 additions & 0 deletions __test__/cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// plugins run in the cypress Node process, not the browser. Define tasks
// operations like seeding the database that can't be run from the browser.
// See: https://on.cypress.io/plugins-guide
require("dotenv").load();
require("babel-register");
require("babel-polyfill");

const makeTasks = require("./tasks").makeTasks;
const utils = require("./utils");

module.exports = async (on, config) => {
if (config.env.SUPPRESS_ORG_CREATION && !config.env.TEST_ORGANIZATION_ID) {
throw new Error(
"Missing TEST_ORGANIZATION_ID and org creation is disabled"
);
}
if (!config.env.TEST_ORGANIZATION_ID) {
config.env.TEST_ORGANIZATION_ID = await utils.getOrCreateTestOrganization();
}

on("task", makeTasks(config));
};
67 changes: 67 additions & 0 deletions __test__/cypress/plugins/tasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { r, User } from "../../../src/server/models";
import AuthHasher from "passport-local-authenticate";

/**
* Make Cypress tasks with access to the config.
*
* https://docs.cypress.io/api/commands/task.html#Syntax
*/
export function makeTasks(config) {
return {
/**
* Create a user and add it to the test organization with the specified role.
*/
createOrUpdateUser: async userData => {
let user = await r
.knex("user")
.where("email", userData.email)
.first();

if (!user) {
// TODO[matteosb]: support Auth0 and consider creating users through
// the API rather than with direct database access, which would be
// better when running against remote envs. Alternatively, we could
// simply not support user creation when running against a remove
// env, similar to SUPPRESS_ORG_CREATION.
user = await new Promise((resolve, reject) => {
AuthHasher.hash(userData.password, async (err, hashed) => {
if (err) reject(err);
const passwordToSave = `localauth|${hashed.salt}|${hashed.hash}`;
const { email, first_name, last_name, cell } = userData;
const u = await User.save({
email,
first_name,
last_name,
cell,
auth0_id: passwordToSave,
is_superadmin: false
});
resolve(u);
});
});
}

const role = await r
.knex("user_organization")
.where({ organization_id: config.env.TEST_ORGANIZATION_ID })
.first();

if (!role) {
await r.knex("user_organization").insert({
user_id: user.id,
organization_id: config.env.TEST_ORGANIZATION_ID,
role: userData.role
});
}

if (role !== userData.role) {
await r
.knex("user_organization")
.where({ organization_id: config.env.TEST_ORGANIZATION_ID })
.update({ role: userData.role });
}

return user.id;
}
};
}
18 changes: 18 additions & 0 deletions __test__/cypress/plugins/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { r } from "../../../src/server/models";
import uuid from "uuid";

const DEFAULT_ORGANIZATION_NAME = "E2E Test Organization";

export async function getOrCreateTestOrganization() {
let org = await r
.knex("organization")
.where("name", DEFAULT_ORGANIZATION_NAME)
.first();
if (!org) {
org = await r
.knex("organization")
.insert({ name: DEFAULT_ORGANIZATION_NAME, uuid: uuid.v4() })
.returning("*");
}
return org.id;
}
21 changes: 21 additions & 0 deletions __test__/cypress/support/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// support/index.js is processed and loaded automatically before your test files.
// this runs in the browser is a good place to define common cypress operations
import "cypress-file-upload";

import TestData from "../fixtures/test-data";

// TODO: support Auth0
Cypress.Commands.add("login", testDataId => {
const userData = TestData.users[testDataId];
if (!userData) {
throw Error(`Unknown test user ${testDataId}`);
}

cy.task("createOrUpdateUser", userData);
cy.request("POST", "/login-callback", {
nextUrl: "/",
authType: "login",
password: userData.password,
email: userData.email
});
});
12 changes: 12 additions & 0 deletions cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"baseUrl": "http://localhost:3000",
"integrationFolder": "__test__/cypress/integration",
"fixturesFolder": "__test__/cypress/fixtures",
"pluginsFile": "__test__/cypress/plugins/index.js",
"supportFile": "__test__/cypress/support/index.js",
"testFiles": "*.test.js",
"env": {
"SUPPRESS_ORG_CREATION": false,
"TEST_ORGANIZATION_ID": null
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@
"babel-jest": "^22.1.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2017": "^6.24.1",
"cypress": "^4.4.1",
"cypress-file-upload": "^4.0.6",
"enzyme": "^3.3.0",
"enzyme-adapter-react-15": "^1.0.5",
"eslint": "2.13.1",
Expand Down
6 changes: 5 additions & 1 deletion src/components/AssignmentSummary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export class AssignmentSummary extends Component {
skippedMessagesCount
} = this.props;
const {
id: campaignId,
title,
description,
hasUnassignedContactsForTexter,
Expand All @@ -132,7 +133,10 @@ export class AssignmentSummary extends Component {
const cardTitleTextColor = setContrastingColor(primaryColor);

return (
<div className={css(styles.container)}>
<div
className={css(styles.container)}
{...dataTest(`assignmentSummary-${campaignId}`)}
>
<Card key={assignment.id}>
<CardTitle
title={title}
Expand Down
1 change: 1 addition & 0 deletions src/components/CampaignBasicsForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default class CampaignBasicsForm extends React.Component {
value={this.formValues()}
onChange={this.props.onChange}
onSubmit={this.props.onSubmit}
{...dataTest("campaignBasicsForm")}
>
<Form.Field
{...dataTest("title")}
Expand Down
2 changes: 2 additions & 0 deletions src/components/CampaignTextersForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ export default class CampaignTextersForm extends React.Component {
filter={filter}
hintText="Search for texters to assign"
dataSource={dataSource}
{...dataTest("texterSearch")}
onNewRequest={value => {
// If you're searching but get no match, value is a string
// representing your search term, but we only want to handle matches
Expand Down Expand Up @@ -571,6 +572,7 @@ export default class CampaignTextersForm extends React.Component {
type="submit"
label={this.props.saveLabel}
disabled={this.props.saveDisabled}
{...dataTest("submitCampaignTextersForm")}
/>
</GSForm>
<Snackbar
Expand Down
1 change: 1 addition & 0 deletions src/components/forms/GSDateField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import DatePicker from "material-ui/DatePicker";
import moment from "moment";
import GSFormField from "./GSFormField";
import { dataTest } from "../../lib/attributes";

export default class GCDateField extends GSFormField {
render() {
Expand Down
2 changes: 2 additions & 0 deletions src/containers/UserEdit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ class UserEdit extends React.Component {
onSubmit={this.handleSave}
defaultValue={user}
className={style}
{...dataTest("userEditForm")}
data
>
<Form.Field label="Email" name="email" {...dataTest("email")} />
{(!authType || authType === "signup") && (
Expand Down
Loading

0 comments on commit e932766

Please sign in to comment.