Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into ag/hide-branched-sc…
Browse files Browse the repository at this point in the history
…ripts
  • Loading branch information
agreenspan24 committed Sep 10, 2021
2 parents 49a4160 + 9df3984 commit d9605a0
Show file tree
Hide file tree
Showing 167 changed files with 12,678 additions and 4,130 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/cypress-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
- name: Cypress run
uses: cypress-io/github-action@v2
env:
DEBUG: '@cypress/github-action'
NODE_ENV: test
PORT: 3001
OUTPUT_DIR: ./build
Expand All @@ -40,6 +41,8 @@ jobs:
SESSION_SECRET: secret
DEFAULT_SERVICE: fakeservice
JOBS_SAME_PROCESS: 1
JOBS_SYNC: 1
TASKS_SYNC: 1
PASSPORT_STRATEGY: local
PHONE_INVENTORY: 1
with:
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
10
12
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ARG BUILDER_IMAGE=node:10.15
ARG RUNTIME_IMAGE=node:10.15-alpine
ARG BUILDER_IMAGE=node:12.22
ARG RUNTIME_IMAGE=node:12.22-alpine
ARG PHONE_NUMBER_COUNTRY=US

FROM ${BUILDER_IMAGE} as builder
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ Spoke is an open source text-distribution tool for organizations to mobilize sup

Spoke was created by Saikat Chakrabarti and Sheena Pakanati, and is now maintained by MoveOn.org.

The latest version is [10.2](https://github.com/MoveOnOrg/Spoke/tree/v10.2) (see [release notes](https://github.com/MoveOnOrg/Spoke/blob/main/docs/RELEASE_NOTES.md#v102))
The latest version is [11.0](https://github.com/MoveOnOrg/Spoke/tree/v11.0) (see [release notes](https://github.com/MoveOnOrg/Spoke/blob/main/docs/RELEASE_NOTES.md#v110))


## Setting up Spoke


The easiest way to get started is with Heroku. You can also learn about Spoke through the [texter](https://youtu.be/EqE1UDvKGco) and [admin](https://youtu.be/PTMykMX8gII) video demos or in the explanation on [how to decide if Spoke is right for you.](/docs/EXPLANATION_DECIDING_ON_SPOKE.md)

For developers, please see our recomendations for [deploying locally for development](/docs/HOWTO_DEVELOPMENT_LOCAL_SETUP.md).
For developers, please see our recommendations for [deploying locally for development](/docs/HOWTO_DEVELOPMENT_LOCAL_SETUP.md).

Want to know more?
[Click here to visit the Spoke Documentation microsite!](https://moveonorg.github.io/Spoke/)
Expand All @@ -24,28 +24,28 @@ Want to know more?
### Quick Start with Heroku
This version of Spoke suitable for testing and, potentially, for small campaigns. This won't cost any money and will not support production(aka large-scale) usage. It's a great way to practice deploying Spoke or see it in action.

<a href="https://heroku.com/deploy?template=https://github.com/MoveOnOrg/Spoke/tree/v10.2">
<a href="https://heroku.com/deploy?template=https://github.com/MoveOnOrg/Spoke/tree/v11.0">

<img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy">
</a>

Follow up instructions located [here](/docs/HOWTO_HEROKU_DEPLOY.md).


**NOTE:** You can upgrade this deployment later for use in a production setting, but keep in mind you will need to migrate data from any prior campaigns. Thus it is best to upgrade before you start any live campaigns. This will cost ~$75 ($25 dyno + $50 postgres) a month and should be suitable for production level usage for most organizations. We recommend that if you plan to use Spoke at scale that you use [this link to deploy with a production infrastructure from the start!](https://heroku.com/deploy?template=https://github.com/MoveOnOrg/Spoke/tree/heroku-button-paid)
**NOTE:** You can upgrade this deployment later for use in a production setting, but keep in mind you will need to migrate data from any prior campaigns. Thus it is best to upgrade before you start any live campaigns. This will cost ~$75 ($25 dyno + $50 postgres) a month and should be suitable for production level usage for most organizations. We recommend that if you plan to use Spoke at scale that you use [this link to deploy with a production infrastructure from the start!](https://heroku.com/deploy?template=https://github.com/MoveOnOrg/Spoke/tree/heroku-button-paid)

Please let us know if you deployed by filling out this form [here](https://act.moveon.org/survey/tech/)


### Other Options for Production Use
### Other Options for Production Use

You can also [deploy on AWS Lambda.](/docs/HOWTO_DEPLOYING_AWS_LAMBDA.md) which is a lot cheaper than Heroku at scale, but requires considerably more technical knowledge to deploy and maintain. We recommend this option for large scale campaigns with tech resources.
You can also [deploy on AWS Lambda.](docs/HOWTO_DEPLOYING_AWS_LAMBDA.md) which is a lot cheaper than Heroku at scale, but requires considerably more technical knowledge to deploy and maintain. We recommend this option for large scale campaigns with tech resources.

Additional guidance:
- [Choosing a set-up for production](/docs/EXPLANATION_CHOOSE_A_SETUP.md)
- [How to hire someone to install Spoke](/docs/HOWTO_HIRE_SOMEONE_TO_INSTALL_SPOKE.md)
- [Choosing a set-up for production](docs/EXPLANATION_CHOOSE_A_SETUP.md)
- [How to hire someone to install Spoke](docs/HOWTO_HIRE_SOMEONE_TO_INSTALL_SPOKE.md)
- [Option for minimalist deployment](docs/HOWTO_MINIMALIST_DEPLOY.md)

# License

Spoke is licensed under the MIT license.
Spoke is licensed under the [GPL3 license with a special author attribution requirement](LICENSE).
68 changes: 51 additions & 17 deletions __test__/components/AssignmentTexter/Survey.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from "react";
import { shallow } from "enzyme";
import Accordion from "@material-ui/core/Accordion";
import AccordionSummary from "@material-ui/core/AccordionSummary";
import MenuItem from "@material-ui/core/MenuItem";
import ListItem from "@material-ui/core/ListItem";
import Survey from "../../../src/components/AssignmentTexter/Survey";

describe("Survey component", () => {
Expand All @@ -19,53 +21,85 @@ describe("Survey component", () => {
},
{
value: "Foo is a mineral",
interactionStepId: 3,
interactionStepId: 4,
nextInteractionStep: { script: "bar" }
},
{
value: "Foo is a vegetable",
interactionStepId: 3,
interactionStepId: 5,
nextInteractionStep: { script: "fizz" }
}
],
filteredAnswerOptions: [
{
value: "Foo is a mineral",
interactionStepId: 3,
interactionStepId: 4,
nextInteractionStep: { script: "bar" }
}
]
}
};
const interactionSteps = [currentInteractionStep];
const secondStep = {
id: 4,
script: "bar",
question: {
text: "Is Foo bigger than a breadbox?",
answerOptions: [
{
value: "Yes",
interactionStepId: "100",
nextInteractionStep: {
id: "100",
script: "Is it human-made?"
}
}
]
}
};
const wrapper1 = shallow(
<Survey
questionResponses={questionResponses}
interactionSteps={[currentInteractionStep]}
currentInteractionStep={currentInteractionStep}
/>
);

const wrapper = shallow(
const wrapper2 = shallow(
<Survey
questionResponses={questionResponses}
interactionSteps={interactionSteps}
interactionSteps={[currentInteractionStep, secondStep]}
currentInteractionStep={currentInteractionStep}
/>
);

test("Accordion started open with correct text", () => {
const accordion = wrapper.find(Accordion);
const accordionSummary = wrapper.find(AccordionSummary);
const cardHeader = wrapper.find("CardHeader");
const cardText = wrapper.find("CardText").at(0);
test("Accordion started closed with correct text", () => {
const accordion = wrapper2.find(Accordion);
const accordionSummary = wrapper2.find(AccordionSummary);
const menuItem = wrapper2.find(MenuItem).at(1); // 2nd for mineral
const listItems = wrapper2.find(ListItem);

expect(accordion.prop("expanded")).toBe(true);
expect(accordionSummary.prop("children")).toContain("Current question");
expect(accordion.prop("expanded")).toBe(false);
expect(accordionSummary.prop("children")).toContain("All questions");
expect(menuItem.prop("value")).toContain("Foo is a mineral");
// filtered list includes mineral
expect(
listItems.findWhere(x => x.prop("value") === "Foo is a mineral").length
).toBe(1);
// filtered list does NOT include vegetable
expect(
listItems.findWhere(x => x.prop("value") === "Foo is a vegetable").length
).toBe(0);
});

test("handleExpandChange Function", () => {
expect(wrapper.state().showAllQuestions).toEqual(false);
wrapper.instance().handleExpandChange(true);
expect(wrapper.state().showAllQuestions).toEqual(true);
expect(wrapper1.state().showAllQuestions).toEqual(false);
wrapper1.instance().handleExpandChange(true);
expect(wrapper1.state().showAllQuestions).toEqual(true);
});

test("getNextScript Function", () => {
expect(
wrapper.instance().getNextScript({
wrapper1.instance().getNextScript({
interactionStep: currentInteractionStep,
answerIndex: 0
})
Expand Down
26 changes: 16 additions & 10 deletions __test__/cypress/integration/basic-campaign-e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,32 +42,34 @@ describe("End-to-end campaign flow", () => {
// Selectors are fairly brittle, consider upgrading material-ui for easier test interaction

// Open picker by focusing input
cy.get("[data-test=dueBy] input").click();
cy.get("[data-test=dueBy] input").type("1");
// Click next month (>)
cy.get("div.MuiPickersCalendarHeader-switchHeader > button:nth-child(3)")
.first()
.click();

cy.task("log", "cyLOG basic-campaign-e2e-test 1");
// Click first of the month
cy.get(".MuiPickersCalendar-week button:not(.MuiPickersDay-hidden)")
.eq(3)
.click();

cy.task("log", "cyLOG basic-campaign-e2e-test 2");
// Click okay on calendar
/*
cy.get(".MuiDialogActions-root button")
.eq(1)
.click();

*/
cy.task("log", "cyLOG basic-campaign-e2e-test 3");
// Wait for modal to close then submit
// TODO: use cy.waitUntil() instead of wait()
cy.wait(400);
cy.get("[data-test=campaignBasicsForm]").submit();

cy.task("log", "cyLOG basic-campaign-e2e-test 4");
// Upload Contacts
cy.get("#contact-upload").attachFile("two-contacts.csv"), { force: true };
cy.wait(400);
cy.get("button[data-test=submitContactsCsvUpload]").click();

cy.task("log", "cyLOG basic-campaign-e2e-test 5");
// Assignments
// Note: Material UI v0 AutoComplete component appears to require a click on the element
// later versions should just allow you to hit enter
Expand All @@ -84,7 +86,9 @@ describe("End-to-end campaign flow", () => {
cy.wait(400);

// Interaction Steps
cy.get("[data-test=editorInteraction] input").click();
// the editorInteraction selector might seem overly precise
// -- the problem is that multiline fields have two textareas, one hidden
cy.get("[data-test=editorInteraction] textarea[name=script]").click();
cy.wait(400);
cy.get(".DraftEditor-root").type(
"Hi {{}firstName{}} this is {{}texterFirstName{}}, how are you?"
Expand All @@ -93,7 +97,7 @@ describe("End-to-end campaign flow", () => {
cy.get("[data-test=questionText] input").type("How are you?");
cy.get("button[data-test=addResponse]").click();
cy.get("[data-test=answerOption] input").type("Good");
cy.get("[data-test=editorInteraction] input")
cy.get("[data-test=editorInteraction] textarea[name=script]")
.eq(1)
.click();
cy.get(".DraftEditor-root").type("Great!");
Expand Down Expand Up @@ -122,12 +126,12 @@ describe("End-to-end campaign flow", () => {
.find("button[data-test=sendFirstTexts]")
.click();
cy.get("[name=messageText]").then(els => {
console.log(els[0]);
console.log("name=messageText", els[0]);
expect(els[0].value).to.match(
/Hi ContactFirst(\d) this is TexterFirst, how are you\?/
);
});

cy.task("log", "cyLOG basic-campaign-e2e-test 6");
cy.get("button[data-test=send]")
.eq(0)
.click();
Expand All @@ -144,7 +148,9 @@ describe("End-to-end campaign flow", () => {

// Shows we're done and click back to /todos
cy.get("body").contains("You've messaged all your assigned contacts.");
cy.task("log", "cyLOG basic-campaign-e2e-test 7");
cy.get("button:contains(Back To Todos)").click();
cy.task("log", "cyLOG basic-campaign-e2e-test 8");
cy.waitUntil(() => cy.url().then(url => url.match(/\/todos$/)));
});
});
Expand Down
5 changes: 5 additions & 0 deletions __test__/cypress/plugins/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export function defineTasks(on, config) {
});
user.password = userInfo.password;
return user;
},

log(message, arg1, arg2) {
console.log("LOG", message, arg1, arg2);
return null;
}
});

Expand Down
9 changes: 7 additions & 2 deletions __test__/extensions/action-handlers/ngpvan-action.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,7 @@ describe("ngpvn-action", () => {
describe("when the contact has a VanPhoneId", () => {
beforeEach(async () => {
contact = {
cell: '+15555550990',
custom_fields: JSON.stringify({
VanID: "8675309",
VanPhoneId: "789"
Expand All @@ -1133,13 +1134,17 @@ describe("ngpvn-action", () => {

body = {
canvassContext: {
phoneId: "789"
phoneId: "789",
phone: {
dialingPrefix: '1',
phoneNumber: '555-555-0990'
}
},
willVote: true
};
});

it("calls the people endpoint and includes VanPhoneId in the canvass context", async () => {
it("calls the people endpoint and includes VanPhoneId and phone in the canvass context", async () => {
const postPeopleCanvassResponsesNock = nock(
"https://api.securevan.com:443",
{
Expand Down
52 changes: 50 additions & 2 deletions __test__/extensions/contact-loaders/ngpvan/ngpvan.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ describe("ngpvan", () => {
}
})
.get(
`/v4/savedLists?$top=&maxPeopleCount=${process.env.NGP_VAN_MAXIMUM_LIST_SIZE}`
`/v4/savedLists?$top=100&maxPeopleCount=${process.env.NGP_VAN_MAXIMUM_LIST_SIZE}`
)
.reply(200, {
items: listItems,
Expand All @@ -168,6 +168,42 @@ describe("ngpvan", () => {
getSavedListsNock.done();
});

it("gets extra when more than 100 count", async () => {
const savedListsNock = nock(`${fakeNgpVanBaseApiUrl}:443`, {
encodedQueryParams: true,
reqheaders: {
authorization: "Basic c3Bva2U6dG9wc2VjcmV0fDA="
}
});

const getSavedListsNock = savedListsNock
.get(
`/v4/savedLists?$top=100&maxPeopleCount=${process.env.NGP_VAN_MAXIMUM_LIST_SIZE}`
)
.reply(200, {
items: listItems.slice(0, 4),
nextPageLink: null,
count: 101
});

const getExtraSavedListsNock = savedListsNock
.get(
`/v4/savedLists?$top=100&maxPeopleCount=${process.env.NGP_VAN_MAXIMUM_LIST_SIZE}&$skip=1`
)
.reply(200, {
items: listItems.slice(4),
nextPageLink: null,
count: 1
});

const savedListsResponse = await getClientChoiceData();

expect(JSON.parse(savedListsResponse.data).items).toEqual(listItems);
expect(savedListsResponse.expiresSeconds).toEqual(30);
getSavedListsNock.done();
getExtraSavedListsNock.done();
});

describe("when there is an error retrieving the list", () => {
it("returns what we expect", async () => {
const getSavedListsNock = nock(`${fakeNgpVanBaseApiUrl}:443`, {
Expand All @@ -177,7 +213,7 @@ describe("ngpvan", () => {
}
})
.get(
`/v4/savedLists?$top=&maxPeopleCount=${process.env.NGP_VAN_MAXIMUM_LIST_SIZE}`
`/v4/savedLists?$top=100&maxPeopleCount=${process.env.NGP_VAN_MAXIMUM_LIST_SIZE}`
)
.reply(404);

Expand Down Expand Up @@ -1376,6 +1412,18 @@ describe("ngpvan", () => {
expect(autocomplete.html()).toContain("Select a list to import");
});

it("populates as empty when client choice data errors", async () => {
const props = {
...commonProps,
clientChoiceData: JSON.stringify({ error: "error occurred" })
};

wrapper = shallow(<CampaignContactsForm {...props} />);
component = wrapper.instance();
const autocomplete = wrapper.find(Autocomplete);
expect(autocomplete.props().options).toEqual([]);
});

describe("when lastResult indicates a success", () => {
beforeEach(async () => {
StyleSheetTestUtils.suppressStyleInjection();
Expand Down
Loading

0 comments on commit d9605a0

Please sign in to comment.