when inactive", () => {
+ const props: AddButtonProps = { active: false, click: jest.fn() };
+ const wrapper = mount(
);
+ expect(wrapper.html()).toEqual("
");
+ });
+});
diff --git a/webpack/regimens/bulk_scheduler/__tests__/index_test.tsx b/webpack/regimens/bulk_scheduler/__tests__/index_test.tsx
new file mode 100644
index 000000000..fe6bdf0de
--- /dev/null
+++ b/webpack/regimens/bulk_scheduler/__tests__/index_test.tsx
@@ -0,0 +1,87 @@
+import * as React from "react";
+import { mount, shallow } from "enzyme";
+import { BulkSchedulerWidget } from "../index";
+import { BulkEditorProps } from "../interfaces";
+import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
+import { Actions } from "../../../constants";
+import { fakeSequence } from "../../../__test_support__/fake_state/resources";
+
+describe("
", () => {
+ const weeks = [{
+ days:
+ {
+ day1: true,
+ day2: false,
+ day3: false,
+ day4: false,
+ day5: false,
+ day6: false,
+ day7: false
+ }
+ }];
+
+ function fakeProps(): BulkEditorProps {
+ const sequence = fakeSequence();
+ sequence.body.name = "Fake Sequence";
+ return {
+ selectedSequence: sequence,
+ dailyOffsetMs: 3600000,
+ weeks,
+ sequences: [fakeSequence(), fakeSequence()],
+ resources: buildResourceIndex([]).index,
+ dispatch: jest.fn()
+ };
+ }
+
+ it("renders with sequence selected", () => {
+ const wrapper = mount(
);
+ const buttons = wrapper.find("button");
+ expect(buttons.length).toEqual(6);
+ ["Scheduler", "Sequence", "Fake Sequence", "Time",
+ "Days", "Week 1", "1234567"].map(string =>
+ expect(wrapper.text()).toContain(string));
+ });
+
+ it("renders without sequence selected", () => {
+ const p = fakeProps();
+ p.selectedSequence = undefined;
+ const wrapper = mount(
);
+ ["Sequence", "None", "Time"].map(string =>
+ expect(wrapper.text()).toContain(string));
+ });
+
+ it("changes time", () => {
+ const p = fakeProps();
+ p.dispatch = jest.fn();
+ const wrapper = shallow(
);
+ const timeInput = wrapper.find("BlurableInput").first();
+ expect(timeInput.props().value).toEqual("01:00");
+ timeInput.simulate("commit", { currentTarget: { value: "02:00" } });
+ expect(p.dispatch).toHaveBeenCalledWith({
+ payload: 7200000,
+ type: Actions.SET_TIME_OFFSET
+ });
+ });
+
+ it("changes sequence", () => {
+ const p = fakeProps();
+ p.dispatch = jest.fn();
+ const wrapper = shallow(
);
+ const sequenceInput = wrapper.find("FBSelect").first();
+ sequenceInput.simulate("change", { value: "sequences" });
+ expect(p.dispatch).toHaveBeenCalledWith({
+ payload: "sequences",
+ type: Actions.SET_SEQUENCE
+ });
+ });
+
+ it("doesn't change sequence", () => {
+ const p = fakeProps();
+ p.dispatch = jest.fn();
+ const wrapper = shallow(
);
+ const sequenceInput = wrapper.find("FBSelect").first();
+ const change = () => sequenceInput.simulate("change", { value: 4 });
+ expect(change).toThrowError("WARNING: Not a sequence UUID.");
+ expect(p.dispatch).not.toHaveBeenCalled();
+ });
+});
diff --git a/webpack/regimens/bulk_scheduler/__tests__/week_grid_test.tsx b/webpack/regimens/bulk_scheduler/__tests__/week_grid_test.tsx
new file mode 100644
index 000000000..a4cd360b8
--- /dev/null
+++ b/webpack/regimens/bulk_scheduler/__tests__/week_grid_test.tsx
@@ -0,0 +1,58 @@
+import * as React from "react";
+import { mount } from "enzyme";
+import { WeekGrid } from "../week_grid";
+import { WeekGridProps } from "../interfaces";
+import { Actions } from "../../../constants";
+
+describe("
", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const weeks = [{
+ days:
+ {
+ day1: true,
+ day2: false,
+ day3: false,
+ day4: false,
+ day5: false,
+ day6: false,
+ day7: false
+ }
+ }];
+
+ it("renders", () => {
+ const props: WeekGridProps = { weeks, dispatch: jest.fn() };
+ const wrapper = mount(
);
+ const buttons = wrapper.find("button");
+ expect(buttons.length).toEqual(4);
+ ["Days", "Week 1", "1234567"].map(string =>
+ expect(wrapper.text()).toContain(string));
+ });
+
+ function checkAction(position: number, text: string, type: Actions) {
+ const props: WeekGridProps = { weeks, dispatch: jest.fn() };
+ const wrapper = mount(
);
+ const button = wrapper.find("button").at(position);
+ expect(button.text().toLowerCase()).toContain(text.toLowerCase());
+ button.simulate("click");
+ expect(props.dispatch).toHaveBeenCalledWith({ type, payload: undefined });
+ }
+
+ it("adds week", () => {
+ checkAction(0, "Week", Actions.PUSH_WEEK);
+ });
+
+ it("removes week", () => {
+ checkAction(1, "Week", Actions.POP_WEEK);
+ });
+
+ it("selects all days", () => {
+ checkAction(2, "Deselect All", Actions.DESELECT_ALL_DAYS);
+ });
+
+ it("deselects all days", () => {
+ checkAction(3, "Select All", Actions.SELECT_ALL_DAYS);
+ });
+});
diff --git a/webpack/regimens/bulk_scheduler/actions.ts b/webpack/regimens/bulk_scheduler/actions.ts
index d4f01a31e..14b28ecc2 100644
--- a/webpack/regimens/bulk_scheduler/actions.ts
+++ b/webpack/regimens/bulk_scheduler/actions.ts
@@ -73,9 +73,11 @@ export function commitBulkEditor(): Thunk {
// Proceed only if they selected a sequence from the drop down.
if (selectedSequenceUUID) {
const seq = findSequence(res.index, selectedSequenceUUID).body;
- const regimenItems = groupRegimenItemsByWeek(weeks, dailyOffsetMs, seq);
+ const regimenItems = weeks.length > 0
+ ? groupRegimenItemsByWeek(weeks, dailyOffsetMs, seq)
+ : undefined;
// Proceed only if days are selcted in the scheduler.
- if (regimenItems.length > 0) {
+ if (regimenItems && regimenItems.length > 0) {
const reg = findRegimen(res.index, currentRegimen);
const update = defensiveClone(reg).body;
update.regimen_items = update.regimen_items.concat(regimenItems);
diff --git a/webpack/regimens/editor/__tests__/active_editor_test.tsx b/webpack/regimens/editor/__tests__/active_editor_test.tsx
new file mode 100644
index 000000000..3ec9644fe
--- /dev/null
+++ b/webpack/regimens/editor/__tests__/active_editor_test.tsx
@@ -0,0 +1,48 @@
+import * as React from "react";
+import { mount } from "enzyme";
+import { ActiveEditor } from "../active_editor";
+import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
+import { ActiveEditorProps } from "../interfaces";
+import { Actions } from "../../../constants";
+
+describe("
", () => {
+ const props: ActiveEditorProps = {
+ dispatch: jest.fn(),
+ regimen: fakeRegimen(),
+ calendar: [{
+ day: "1",
+ items: [{
+ name: "Item 0",
+ color: "red",
+ hhmm: "10:00",
+ sortKey: 0,
+ day: 1,
+ dispatch: jest.fn(),
+ regimen: fakeRegimen(),
+ item: {
+ sequence_id: 0, time_offset: 1000
+ }
+ }]
+ }]
+ };
+
+ it("renders", () => {
+ const wrapper = mount(
);
+ ["Day", "Item 0", "10:00"].map(string =>
+ expect(wrapper.text()).toContain(string));
+ });
+
+ it("removes regimen item", () => {
+ const wrapper = mount(
);
+ wrapper.find("i").simulate("click");
+ expect(props.dispatch).toHaveBeenCalledWith({
+ payload: {
+ update: {
+ color: "red", name: "Foo", regimen_items: []
+ },
+ uuid: "regimens.1.17"
+ },
+ type: Actions.OVERWRITE_RESOURCE
+ });
+ });
+});
diff --git a/webpack/regimens/editor/__tests__/index_test.tsx b/webpack/regimens/editor/__tests__/index_test.tsx
new file mode 100644
index 000000000..70c4b4689
--- /dev/null
+++ b/webpack/regimens/editor/__tests__/index_test.tsx
@@ -0,0 +1,84 @@
+import { fakeState } from "../../../__test_support__/fake_state";
+const mockState = fakeState;
+const mockDestroy = jest.fn();
+const mockSave = jest.fn();
+jest.mock("../../../api/crud", () => ({
+ getState: mockState,
+ destroy: mockDestroy,
+ save: mockSave
+}));
+
+import * as React from "react";
+import { mount } from "enzyme";
+import { RegimenEditorWidget } from "../index";
+import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
+import { RegimenEditorWidgetProps } from "../interfaces";
+import { auth } from "../../../__test_support__/fake_state/token";
+import { bot } from "../../../__test_support__/fake_state/bot";
+
+describe("
", () => {
+ beforeEach(function () {
+ jest.clearAllMocks();
+ });
+
+ function fakeProps(): RegimenEditorWidgetProps {
+ return {
+ dispatch: jest.fn(),
+ auth,
+ bot,
+ current: fakeRegimen(),
+ calendar: [{
+ day: "1",
+ items: [{
+ name: "Item 0",
+ color: "red",
+ hhmm: "10:00",
+ sortKey: 0,
+ day: 1,
+ dispatch: jest.fn(),
+ regimen: fakeRegimen(),
+ item: {
+ sequence_id: 0, time_offset: 1000
+ }
+ }]
+ }]
+ };
+ }
+
+ it("active editor", () => {
+ const wrapper = mount(
);
+ ["Regimen Editor", "Delete", "Item 0", "10:00"].map(string =>
+ expect(wrapper.text()).toContain(string));
+ });
+
+ it("empty editor", () => {
+ const props = fakeProps();
+ props.current = undefined;
+ const wrapper = mount(
);
+ ["Regimen Editor", "No Regimen selected."].map(string =>
+ expect(wrapper.text()).toContain(string));
+ });
+
+ it("error: not logged in", () => {
+ const props = fakeProps();
+ props.auth = undefined;
+ const wrapper = () => mount(
);
+ expect(wrapper).toThrowError("Must log in first");
+ });
+
+ it("deletes regimen", () => {
+ const wrapper = mount(
);
+ const deleteButton = wrapper.find("button").at(2);
+ expect(deleteButton.text()).toContain("Delete");
+ deleteButton.simulate("click");
+ expect(mockDestroy).toHaveBeenCalledWith("regimens.6.22");
+ });
+
+ it("saves regimen", () => {
+ const wrapper = mount(
);
+ const saveeButton = wrapper.find("button").at(0);
+ expect(saveeButton.text()).toContain("Save");
+ saveeButton.simulate("click");
+ expect(mockSave).toHaveBeenCalledWith("regimens.8.24");
+ });
+});
diff --git a/webpack/regimens/editor/__tests__/regimen_name_input.tsx b/webpack/regimens/editor/__tests__/regimen_name_input.tsx
new file mode 100644
index 000000000..094543a71
--- /dev/null
+++ b/webpack/regimens/editor/__tests__/regimen_name_input.tsx
@@ -0,0 +1,22 @@
+jest.mock("../../actions", () => ({ editRegimen: jest.fn() }));
+
+import { write } from "../regimen_name_input";
+import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
+import { editRegimen } from "../../actions";
+
+describe("write()", () => {
+ it("crashes without a regimen", () => {
+ const input = { regimen: undefined, dispatch: jest.fn() };
+ expect(() => write(input)).toThrowError();
+ });
+
+ it("calls dispatch", () => {
+ const input = { regimen: fakeRegimen(), dispatch: jest.fn() };
+ const callback = write(input);
+ expect(callback).toBeInstanceOf(Function);
+ const value = "FOO";
+ callback({ currentTarget: { value } } as any);
+ expect(input.dispatch).toHaveBeenCalled();
+ expect(editRegimen).toHaveBeenCalled();
+ });
+});
diff --git a/webpack/regimens/editor/regimen_name_input.tsx b/webpack/regimens/editor/regimen_name_input.tsx
index 43ed20779..144ef3e2f 100644
--- a/webpack/regimens/editor/regimen_name_input.tsx
+++ b/webpack/regimens/editor/regimen_name_input.tsx
@@ -5,7 +5,7 @@ import { ColorPicker } from "../../ui";
import { Row, Col } from "../../ui/index";
import { editRegimen } from "../actions";
-function write({ dispatch, regimen }: RegimenProps):
+export function write({ dispatch, regimen }: RegimenProps):
React.EventHandler
> {
if (regimen) {
return (event: React.FormEvent) => {
diff --git a/webpack/regimens/list/__tests__/index_test.tsx b/webpack/regimens/list/__tests__/index_test.tsx
new file mode 100644
index 000000000..5d000647c
--- /dev/null
+++ b/webpack/regimens/list/__tests__/index_test.tsx
@@ -0,0 +1,29 @@
+import * as React from "react";
+import { mount, shallow } from "enzyme";
+import { RegimensList } from "../index";
+import { RegimensListProps } from "../../interfaces";
+import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
+
+describe("", () => {
+ function fakeProps(): RegimensListProps {
+ const regimen = fakeRegimen();
+ regimen.body.name = "Fake Regimen";
+ return {
+ dispatch: jest.fn(),
+ regimens: [regimen, regimen],
+ regimen: undefined
+ };
+ }
+ it("renders", () => {
+ const wrapper = mount();
+ expect(wrapper.text()).toContain("Regimens");
+ expect(wrapper.text()).toContain("Fake Regimen Fake Regimen");
+ });
+
+ it("sets search term", () => {
+ const wrapper = shallow();
+ wrapper.find("input").simulate("change",
+ { currentTarget: { value: "term" } });
+ expect(wrapper.state().searchTerm).toEqual("term");
+ });
+});
diff --git a/webpack/sequences/__tests__/sequences_list_test.tsx b/webpack/sequences/__tests__/sequences_list_test.tsx
index 039dcbeb4..8195a90a7 100644
--- a/webpack/sequences/__tests__/sequences_list_test.tsx
+++ b/webpack/sequences/__tests__/sequences_list_test.tsx
@@ -17,7 +17,7 @@ describe("", () => {
sequences={[fakeSequence1, fakeSequence2]} />);
expect(wrapper.find("input").first().props().placeholder)
.toContain("Search Sequences");
- expect(wrapper.text()).toContain("Sequence 1");
- expect(wrapper.text()).toContain("Sequence 2");
+ ["Sequence 1", "Sequence 2"].map(string =>
+ expect(wrapper.text()).toContain(string));
});
});
diff --git a/webpack/sync/actions.ts b/webpack/sync/actions.ts
index 68ab209ed..9f2c93636 100644
--- a/webpack/sync/actions.ts
+++ b/webpack/sync/actions.ts
@@ -32,7 +32,7 @@ export function fetchSyncData(dispatch: Function) {
type, payload: { name, data: r.data }
}), fail);
- const fail = () => warning("Please try refreshing the page.",
+ const fail = () => warning("Please try refreshing the page or logging in again.",
"Error downloading data");
fetch("users", API.current.usersPath);
@@ -47,16 +47,3 @@ export function fetchSyncData(dispatch: Function) {
fetch("sequences", API.current.sequencesPath);
fetch("tools", API.current.toolsPath);
}
-
-export function fetchSyncDataOk(payload: {}) {
- return {
- type: "FETCH_SYNC_OK", payload
- };
-}
-
-export function fetchSyncDataNo(err: Error) {
- return {
- type: "FETCH_SYNC_NO",
- payload: {}
- };
-}
diff --git a/webpack/tools/__tests__/index_test.tsx b/webpack/tools/__tests__/index_test.tsx
new file mode 100644
index 000000000..9824df59e
--- /dev/null
+++ b/webpack/tools/__tests__/index_test.tsx
@@ -0,0 +1,39 @@
+jest.mock("react-redux", () => ({
+ connect: jest.fn()
+}));
+
+import * as React from "react";
+import { mount } from "enzyme";
+import { Tools } from "../index";
+import { Props } from "../interfaces";
+import { fakeToolSlot, fakeTool } from "../../__test_support__/fake_state/resources";
+
+describe("", () => {
+ function fakeProps(): Props {
+ return {
+ toolSlots: [],
+ tools: [fakeTool()],
+ getToolSlots: () => [fakeToolSlot()],
+ getToolOptions: () => [],
+ getChosenToolOption: () => { return { label: "None", value: "" }; },
+ getToolByToolSlotUUID: () => fakeTool(),
+ changeToolSlot: jest.fn(),
+ isActive: () => true,
+ dispatch: jest.fn(),
+ botPosition: { x: undefined, y: undefined, z: undefined }
+ };
+ }
+
+ it("renders", () => {
+ const wrapper = mount();
+ const txt = wrapper.text();
+ const strings = [
+ "ToolBay 1",
+ "SlotXYZ",
+ "Tool1101010Foo",
+ "Tools",
+ "Tool NameStatus",
+ "Fooactive"];
+ strings.map(string => expect(txt).toContain(string));
+ });
+});
diff --git a/webpack/tools/components/tool_list.tsx b/webpack/tools/components/tool_list.tsx
index a15845d68..0d8f16d68 100644
--- a/webpack/tools/components/tool_list.tsx
+++ b/webpack/tools/components/tool_list.tsx
@@ -28,7 +28,7 @@ export class ToolList extends React.Component {
{tools.map((tool: TaggedTool) => {
- return
+ return
{tool.body.name || "Name not found"}
diff --git a/webpack/tools/components/toolbay_list.tsx b/webpack/tools/components/toolbay_list.tsx
index 796f66434..416f19398 100644
--- a/webpack/tools/components/toolbay_list.tsx
+++ b/webpack/tools/components/toolbay_list.tsx
@@ -24,7 +24,7 @@ export class ToolBayList extends React.Component {
{getToolSlots().map((slot: TaggedToolSlotPointer, index: number) => {
const tool = getToolByToolSlotUUID(slot.uuid);
const name = (tool && tool.body.name) || "None";
- return
+ return
diff --git a/webpack/util.ts b/webpack/util.ts
index ceeb8fd03..003f81ebe 100644
--- a/webpack/util.ts
+++ b/webpack/util.ts
@@ -461,5 +461,5 @@ export function withTimeout(ms: number, promise: Promise) {
});
// Returns a race between our timeout and the passed in promise
- return Promise.race([promise, timeout]);
+ return Promise.race([promise, timeout]) as Promise;
}
diff --git a/webpack/verification_support.ts b/webpack/verification_support.ts
index a534cbbc6..1a39dd216 100644
--- a/webpack/verification_support.ts
+++ b/webpack/verification_support.ts
@@ -1,9 +1,20 @@
import { getParam, HttpData } from "./util";
-import axios from "axios";
+import axios, { AxiosResponse } from "axios";
import { API } from "./api/api";
import { Session } from "./session";
import { AuthState } from "./auth/interfaces";
+/** Keep track of this in rollbar to prevent global registration failures. */
+export const ALREADY_VERIFIED =
+ `
+ You are already verified. We will now forward you to the main application.
+
+
+ If you are still unable to access the app, try logging in again or
+ asking for help on the FarmBot Forum.
+
`;
+const ALREADY_VERIFIED_MSG = "TRIED TO RE-VERIFY";
+
export const FAILURE_PAGE =
`
We were unable to verify your account.
@@ -17,7 +28,15 @@ export const FAILURE_MSG = "USER VERIFICATION FAILED!";
/** Function called when the Frontend verifies its registration token.
* IF YOU BREAK THIS FUNCTION, YOU BREAK *ALL* NEW USER REGISTRATIONS. */
-export const verify = async () => { try { attempt(); } catch (e) { fail(); } };
+export const verify = async () => {
+ try {
+ console.log("TODO: Make sure this thing actually uses `await`." +
+ " function won't work without await");
+ await attempt();
+ } catch (e) {
+ fail(e);
+ }
+};
export async function attempt() {
API.setBaseUrl(API.fetchBrowserLocation());
@@ -28,7 +47,23 @@ export async function attempt() {
window.location.href = API.current.baseUrl + "/app/controls";
}
-export function fail() {
- document.write(FAILURE_PAGE);
- throw new Error(FAILURE_MSG);
+interface AxiosError extends Error {
+ response?: AxiosResponse | undefined; // Need to be extra cautious here.
}
+
+export function fail(err: AxiosError | undefined) {
+ switch (err && err.response && err.response.status) {
+ case 409:
+ alreadyVerified();
+ break;
+ default:
+ document.write(FAILURE_PAGE);
+ throw new Error(FAILURE_MSG);
+ }
+}
+
+const alreadyVerified = () => {
+ window.location.href = "/app/controls";
+ document.write(ALREADY_VERIFIED);
+ throw new Error(ALREADY_VERIFIED_MSG);
+};
diff --git a/yarn.lock b/yarn.lock
index 9fcaf1ec1..08c5b16c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1972,9 +1972,9 @@ farmbot-toastr@^1.0.0, farmbot-toastr@^1.0.3:
farmbot-toastr "^1.0.0"
typescript "^2.3.4"
-farmbot@5.0.1-rc12:
- version "5.0.1-rc12"
- resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-5.0.1-rc12.tgz#3746018e2d42657ece67ff2af23b507d604ceb0d"
+farmbot@5.0.1-rc13:
+ version "5.0.1-rc13"
+ resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-5.0.1-rc13.tgz#473366b179eb967d9f1265952334b6195de7b1ef"
dependencies:
mqtt "^1.7.4"
typescript "^2.4.2"