Cleanup and tests (#953)
* tslint cleanup * app crash page improvements * fix long lines * remove unused auth code * add more tests * add tslint to scripts and fix deprecationspull/952/head^2
parent
f25241350e
commit
beddbf9c0b
|
@ -16,7 +16,8 @@
|
|||
"webpack": "./node_modules/.bin/webpack-dev-server --config config/webpack.dev.js",
|
||||
"test-slow": "jest --coverage --no-cache -w 4",
|
||||
"test": "jest --no-coverage --cache -w 5",
|
||||
"typecheck": "./node_modules/.bin/tsc --noEmit --jsx preserve"
|
||||
"typecheck": "./node_modules/.bin/tsc --noEmit --jsx preserve",
|
||||
"tslint": "./node_modules/tslint/bin/tslint --project ."
|
||||
},
|
||||
"keywords": [
|
||||
"farmbot"
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {
|
||||
Resource as Res,
|
||||
ResourceName as Name,
|
||||
SpecialStatus
|
||||
SpecialStatus,
|
||||
TaggedResource
|
||||
} from "farmbot";
|
||||
import { generateUuid } from "../resources/util";
|
||||
import { TaggedResource } from "farmbot";
|
||||
|
||||
let ID_COUNTER = 0;
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ const fakeProps = (): AppProps => {
|
|||
axisInversion: { x: false, y: false, z: false },
|
||||
firmwareConfig: undefined,
|
||||
xySwap: false,
|
||||
animate: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ describe("fetchNewDevice", () => {
|
|||
const bot = await fetchNewDevice(auth);
|
||||
expect(bot).toBeInstanceOf(mockFarmbot);
|
||||
// We use this for debugging in local dev env
|
||||
// tslint:disable-next-line:no-any
|
||||
expect((global as any)["current_bot"]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ describe("isSafeError", () => {
|
|||
describe("inferUpdateId", () => {
|
||||
it("it handles failure by returning `*`", () => {
|
||||
expect(inferUpdateId("foo/123/456")).toBe("*");
|
||||
// tslint:disable-next-line:no-any
|
||||
expect(inferUpdateId((true as any))).toBe("*");
|
||||
});
|
||||
|
||||
|
|
|
@ -1,26 +1,10 @@
|
|||
const mockStorj: Dictionary<boolean> = {};
|
||||
|
||||
jest.mock("../session", () => {
|
||||
return {
|
||||
Session: {
|
||||
deprecatedGetBool: (k: string) => {
|
||||
mockStorj[k] = !!mockStorj[k];
|
||||
return mockStorj[k];
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import { Dictionary } from "farmbot";
|
||||
import * as React from "react";
|
||||
import { LoadingPlant } from "../loading_plant";
|
||||
import { shallow } from "enzyme";
|
||||
import { BooleanSetting } from "../session_keys";
|
||||
|
||||
describe("<LoadingPlant/>", () => {
|
||||
it("renders loading text", () => {
|
||||
mockStorj[BooleanSetting.disable_animations] = true;
|
||||
const wrapper = shallow(<LoadingPlant />);
|
||||
const wrapper = shallow(<LoadingPlant animate={false} />);
|
||||
expect(wrapper.find(".loading-plant").length).toEqual(0);
|
||||
expect(wrapper.find(".loading-plant-text").props().y).toEqual(150);
|
||||
expect(wrapper.text()).toContain("Loading");
|
||||
|
@ -28,8 +12,7 @@ describe("<LoadingPlant/>", () => {
|
|||
});
|
||||
|
||||
it("renders loading animation", () => {
|
||||
mockStorj[BooleanSetting.disable_animations] = false;
|
||||
const wrapper = shallow(<LoadingPlant />);
|
||||
const wrapper = shallow(<LoadingPlant animate={true} />);
|
||||
expect(wrapper.find(".loading-plant")).toBeTruthy();
|
||||
const circleProps = wrapper.find(".loading-plant-circle").props();
|
||||
expect(circleProps.r).toEqual(110);
|
||||
|
|
|
@ -11,7 +11,8 @@ import { RouterState, RedirectFunction } from "react-router";
|
|||
async function makeSureTheyAreRoutes(input: typeof topLevelRoutes.childRoutes) {
|
||||
const cb = jest.fn();
|
||||
const all = (input || []);
|
||||
await Promise.all(all.map(route => (route.getComponent || noop)({} as RouterState, cb)));
|
||||
await Promise.all(all.map(route =>
|
||||
(route.getComponent || noop)({} as RouterState, cb)));
|
||||
expect(cb).toHaveBeenCalled();
|
||||
expect(cb).toHaveBeenCalledTimes(all.length);
|
||||
cb.mock.calls.map(x => expect(!!x[1]).toBeTruthy());
|
||||
|
|
|
@ -1,11 +1,33 @@
|
|||
import { fakeWebAppConfig } from "../__test_support__/fake_state/resources";
|
||||
import { fakeState } from "../__test_support__/fake_state";
|
||||
|
||||
const mockConfig = fakeWebAppConfig();
|
||||
jest.mock("../resources/selectors_by_kind", () => ({
|
||||
getWebAppConfig: () => mockConfig
|
||||
}));
|
||||
|
||||
jest.mock("../api/crud", () => ({
|
||||
edit: jest.fn(),
|
||||
save: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockState = fakeState();
|
||||
jest.mock("../redux/store", () => ({
|
||||
store: {
|
||||
dispatch: jest.fn(),
|
||||
getState: () => mockState,
|
||||
}
|
||||
}));
|
||||
|
||||
import {
|
||||
isNumericSetting,
|
||||
isBooleanSetting,
|
||||
safeBooleanSettting,
|
||||
safeNumericSetting,
|
||||
Session
|
||||
Session,
|
||||
} from "../session";
|
||||
import { auth } from "../__test_support__/fake_state/token";
|
||||
import { edit, save } from "../api/crud";
|
||||
|
||||
describe("fetchStoredToken", () => {
|
||||
it("can't fetch token", () => {
|
||||
|
@ -39,9 +61,41 @@ describe("safeBooleanSetting", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("setBool", () => {
|
||||
it("sets bool", () => {
|
||||
Session.setBool("x_axis_inverted", false);
|
||||
expect(edit).toHaveBeenCalledWith(expect.any(Object), {
|
||||
x_axis_inverted: false
|
||||
});
|
||||
expect(save).toHaveBeenCalledWith(mockConfig.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("invertBool", () => {
|
||||
it("inverts bool", () => {
|
||||
Session.invertBool("x_axis_inverted");
|
||||
expect(edit).toHaveBeenCalledWith(expect.any(Object), {
|
||||
x_axis_inverted: true
|
||||
});
|
||||
expect(save).toHaveBeenCalledWith(mockConfig.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("safeNumericSetting", () => {
|
||||
it("safely returns num", () => {
|
||||
expect(() => safeNumericSetting("no")).toThrow();
|
||||
expect(safeNumericSetting("zoom_level")).toBe("zoom_level");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear()", () => {
|
||||
it("clears", () => {
|
||||
localStorage.clear = jest.fn();
|
||||
sessionStorage.clear = jest.fn();
|
||||
window.location.assign = jest.fn();
|
||||
expect(Session.clear()).toEqual(undefined);
|
||||
expect(localStorage.clear).toHaveBeenCalled();
|
||||
expect(sessionStorage.clear).toHaveBeenCalled();
|
||||
expect(window.location.assign).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ jest.mock("farmbot-toastr/dist", () => ({ success: jest.fn() }));
|
|||
import * as React from "react";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { mapStateToProps } from "../state_to_props";
|
||||
import { mount } from "enzyme";
|
||||
import { shallow } from "enzyme";
|
||||
import { Account } from "../index";
|
||||
import { edit } from "../../api/crud";
|
||||
|
||||
|
@ -16,21 +16,20 @@ describe("<Account />", () => {
|
|||
const props = mapStateToProps(fakeState());
|
||||
props.dispatch = jest.fn();
|
||||
|
||||
const el = mount<Account>(<Account {...props} />);
|
||||
expect(() => {
|
||||
(el.instance()).onChange({
|
||||
const el = shallow(<Account {...props} />);
|
||||
expect(() =>
|
||||
el.find("Settings").simulate("change", {
|
||||
currentTarget: {
|
||||
name: "foo",
|
||||
value: "bar"
|
||||
}
|
||||
} as any);
|
||||
}).toThrow();
|
||||
(el.instance() as Account).onChange({
|
||||
})).toThrow();
|
||||
el.find("Settings").simulate("change", {
|
||||
currentTarget: {
|
||||
name: "email",
|
||||
value: "foo@bar.com"
|
||||
}
|
||||
} as any);
|
||||
});
|
||||
expect(props.dispatch).toHaveBeenCalledTimes(1);
|
||||
const expected = edit(props.user, { email: "foo@bar.com" });
|
||||
expect(props.dispatch).toHaveBeenCalledWith(expected);
|
||||
|
@ -39,9 +38,9 @@ describe("<Account />", () => {
|
|||
it("triggers the onSave() event", () => {
|
||||
const props = mapStateToProps(fakeState());
|
||||
props.dispatch = jest.fn(() => Promise.resolve({}));
|
||||
const el = mount<Account>(<Account {...props} />);
|
||||
const el = shallow(<Account {...props} />);
|
||||
|
||||
(el.instance()).onSave();
|
||||
el.find("Settings").simulate("save");
|
||||
expect(props.dispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const mock = {
|
||||
response: {
|
||||
// tslint:disable-next-line:no-any
|
||||
data: (undefined as any) // Mutable
|
||||
}
|
||||
};
|
||||
|
@ -35,6 +36,7 @@ describe("requestAccountExport", () => {
|
|||
|
||||
it("downloads the data synchronously (when API has no email support)", async () => {
|
||||
mock.response.data = {};
|
||||
// tslint:disable-next-line:no-any
|
||||
window.URL = window.URL || ({} as any);
|
||||
window.URL.createObjectURL = jest.fn();
|
||||
window.URL.revokeObjectURL = jest.fn();
|
||||
|
|
|
@ -56,7 +56,8 @@ export class ChangePassword extends React.Component<{}, ChangePWState> {
|
|||
success(t("Your password is changed."), t("Success"));
|
||||
this.clearForm();
|
||||
}, (e) => {
|
||||
error(e ? prettyPrintApiErrors(e) : t("Password change failed."), t("Error"));
|
||||
error(e ? prettyPrintApiErrors(e) : t("Password change failed."),
|
||||
t("Error"));
|
||||
this.clearForm();
|
||||
});
|
||||
|
||||
|
@ -64,7 +65,8 @@ export class ChangePassword extends React.Component<{}, ChangePWState> {
|
|||
const numUniqueValues = uniq(Object.values(this.state.form)).length;
|
||||
switch (numUniqueValues) {
|
||||
case 1:
|
||||
error(t("Provided new and old passwords match. Password not changed."), t("Error"));
|
||||
error(t("Provided new and old passwords match. Password not changed."),
|
||||
t("Error"));
|
||||
this.clearForm();
|
||||
break;
|
||||
case 2:
|
||||
|
|
|
@ -38,7 +38,9 @@ export class DeleteAccount extends
|
|||
</Col>
|
||||
<Col xs={8}>
|
||||
<BlurablePassword
|
||||
onCommit={(e) => { this.setState({ password: e.currentTarget.value }); }} />
|
||||
onCommit={(e) => {
|
||||
this.setState({ password: e.currentTarget.value });
|
||||
}} />
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<button
|
||||
|
|
|
@ -38,6 +38,7 @@ describe("AJAX data tracking", () => {
|
|||
|
||||
it("sets consistency when calling destroy()", () => {
|
||||
const uuid = store.getState().resources.index.byKind.Tool[0];
|
||||
// tslint:disable-next-line:no-any
|
||||
store.dispatch(destroy(uuid) as any);
|
||||
expect(maybeStartTracking).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -47,6 +48,7 @@ describe("AJAX data tracking", () => {
|
|||
x.specialStatus = SpecialStatus.DIRTY;
|
||||
return x;
|
||||
});
|
||||
// tslint:disable-next-line:no-any
|
||||
store.dispatch(saveAll(r) as any);
|
||||
expect(maybeStartTracking).toHaveBeenCalled();
|
||||
const uuids: string[] =
|
||||
|
@ -57,6 +59,7 @@ describe("AJAX data tracking", () => {
|
|||
|
||||
it("sets consistency when calling initSave()", () => {
|
||||
mockBody = resources()[0].body;
|
||||
// tslint:disable-next-line:no-any
|
||||
store.dispatch(initSave(resources()[0]) as any);
|
||||
expect(maybeStartTracking).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -4,34 +4,44 @@ import { Session } from "./session";
|
|||
const STYLE: React.CSSProperties = {
|
||||
border: "2px solid #434343",
|
||||
background: "#a4c2f4",
|
||||
fontSize: "24px",
|
||||
fontSize: "18px",
|
||||
color: "black",
|
||||
display: "block",
|
||||
overflow: "auto"
|
||||
overflow: "auto",
|
||||
padding: "1rem",
|
||||
};
|
||||
|
||||
export function Apology(_: {}) {
|
||||
return <div style={STYLE}>
|
||||
<div>
|
||||
<h1>Page Error</h1>
|
||||
<p>
|
||||
<span>
|
||||
We can't render this part of the page due to an unrecoverable error.
|
||||
Here are some thing you can try:
|
||||
</p>
|
||||
</span>
|
||||
<ol>
|
||||
<li>Perform a "hard refresh" (<strong>CTRL + SHIFT + R</strong> on most machines).</li>
|
||||
<li>
|
||||
Refresh the page.
|
||||
</li>
|
||||
<li>
|
||||
Perform a "hard refresh"
|
||||
(<strong>CTRL + SHIFT + R</strong> on most machines).
|
||||
</li>
|
||||
<li>
|
||||
<span>
|
||||
<a onClick={() => Session.clear()}>
|
||||
Restart the app by clicking here.
|
||||
</a>
|
||||
(You will be logged out of your account.)
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
Send a report to our developer team via the
|
||||
<a href="http://forum.farmbot.org/c/software">FarmBot software
|
||||
forum</a>. Including additional information (such as steps leading up
|
||||
to the error) helps us identify solutions more quickly.
|
||||
<span>
|
||||
Send a report to our developer team via the
|
||||
<a href="http://forum.farmbot.org/c/software">FarmBot software
|
||||
forum</a>. Including additional information (such as steps leading up
|
||||
to the error) helps us identify solutions more quickly.
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,6 @@ import { HotKeys } from "./hotkeys";
|
|||
import { ControlsPopup } from "./controls_popup";
|
||||
import { Content } from "./constants";
|
||||
import { validBotLocationData, validFwConfig } from "./util";
|
||||
import { Session } from "./session";
|
||||
import { BooleanSetting } from "./session_keys";
|
||||
import { getPathArray } from "./history";
|
||||
import { FirmwareConfig } from "./config_storage/firmware_configs";
|
||||
|
@ -44,9 +43,11 @@ export interface AppProps {
|
|||
axisInversion: Record<Xyz, boolean>;
|
||||
xySwap: boolean;
|
||||
firmwareConfig: FirmwareConfig | undefined;
|
||||
animate: boolean;
|
||||
}
|
||||
|
||||
function mapStateToProps(props: Everything): AppProps {
|
||||
const webAppConfigValue = getWebAppConfigValue(() => props);
|
||||
return {
|
||||
timeOffset: maybeGetTimeOffset(props.resources.index),
|
||||
dispatch: props.dispatch,
|
||||
|
@ -56,12 +57,13 @@ function mapStateToProps(props: Everything): AppProps {
|
|||
loaded: props.resources.loaded,
|
||||
consistent: !!(props.bot || {}).consistent,
|
||||
axisInversion: {
|
||||
x: !!Session.deprecatedGetBool(BooleanSetting.x_axis_inverted),
|
||||
y: !!Session.deprecatedGetBool(BooleanSetting.y_axis_inverted),
|
||||
z: !!Session.deprecatedGetBool(BooleanSetting.z_axis_inverted),
|
||||
x: !!webAppConfigValue(BooleanSetting.x_axis_inverted),
|
||||
y: !!webAppConfigValue(BooleanSetting.y_axis_inverted),
|
||||
z: !!webAppConfigValue(BooleanSetting.z_axis_inverted),
|
||||
},
|
||||
xySwap: !!getWebAppConfigValue(() => props)(BooleanSetting.xy_swap),
|
||||
firmwareConfig: validFwConfig(getFirmwareConfig(props.resources.index))
|
||||
xySwap: !!webAppConfigValue(BooleanSetting.xy_swap),
|
||||
firmwareConfig: validFwConfig(getFirmwareConfig(props.resources.index)),
|
||||
animate: !webAppConfigValue(BooleanSetting.disable_animations),
|
||||
};
|
||||
}
|
||||
/** Time at which the app gives up and asks the user to refresh */
|
||||
|
@ -110,7 +112,7 @@ export class App extends React.Component<AppProps, {}> {
|
|||
bot={this.props.bot}
|
||||
dispatch={this.props.dispatch}
|
||||
logs={this.props.logs} />
|
||||
{!syncLoaded && <LoadingPlant />}
|
||||
{!syncLoaded && <LoadingPlant animate={this.props.animate} />}
|
||||
{syncLoaded && this.props.children}
|
||||
{!(["controls", "account", "regimens"].includes(currentPage)) &&
|
||||
<ControlsPopup
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
jest.mock("../../session", () => ({
|
||||
Session: {
|
||||
clear: jest.fn(),
|
||||
deprecatedGetBool: () => true,
|
||||
fetchStoredToken: () => ({}),
|
||||
replaceToken: jest.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock("farmbot-toastr", () => ({
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("axios", () => ({
|
||||
default: {
|
||||
interceptors: {
|
||||
|
@ -36,118 +20,43 @@ jest.mock("../../api/api", () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
jest.mock("../../toast_errors", () => {
|
||||
return { toastErrors: jest.fn() };
|
||||
});
|
||||
jest.mock("../../devices/actions", () => ({
|
||||
fetchReleases: jest.fn(),
|
||||
fetchMinOsFeatureData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../history", () => {
|
||||
return { push: jest.fn() };
|
||||
});
|
||||
|
||||
import { Session } from "../../session";
|
||||
import {
|
||||
logout,
|
||||
requestToken,
|
||||
requestRegistration,
|
||||
didLogin,
|
||||
loginErr,
|
||||
onRegistrationErr,
|
||||
onLogin
|
||||
} from "../actions";
|
||||
import { didLogin } from "../actions";
|
||||
import { Actions } from "../../constants";
|
||||
import { success, error } from "farmbot-toastr";
|
||||
import { API } from "../../api/api";
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { AuthState } from "../interfaces";
|
||||
import { UnsafeError } from "../../interfaces";
|
||||
import { toastErrors } from "../../toast_errors";
|
||||
import { push } from "../../history";
|
||||
import { fetchReleases } from "../../devices/actions";
|
||||
|
||||
const mockToken: AuthState = {
|
||||
const mockToken = (): AuthState => ({
|
||||
token: {
|
||||
encoded: "---",
|
||||
unencoded: { iss: "iss", os_update_server: "os_update_server", jti: "---" }
|
||||
}
|
||||
};
|
||||
|
||||
describe("logout()", () => {
|
||||
it("displays the toast if you are logged out", () => {
|
||||
const result = logout();
|
||||
expect(result.type).toEqual(Actions.LOGOUT);
|
||||
expect(success).toHaveBeenCalledWith("You have been logged out.");
|
||||
expect(Session.clear).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestToken()", () => {
|
||||
it("requests an auth token over HTTP", async () => {
|
||||
const url = "geocities.com";
|
||||
const email = "foo@bar.com";
|
||||
const password = "password123";
|
||||
const result = await requestToken(email, password, url);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith("/api/tokenStub",
|
||||
{ user: { email, password } });
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.data.foo).toEqual("bar");
|
||||
expect(API.setBaseUrl).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestRegistration", () => {
|
||||
it("sends registration to the API", async () => {
|
||||
const inputs = {
|
||||
email: "foo@bar.co",
|
||||
password: "password",
|
||||
password_confirmation: "password",
|
||||
name: "Paul"
|
||||
};
|
||||
const resp = await requestRegistration(inputs.name,
|
||||
inputs.email,
|
||||
inputs.password,
|
||||
inputs.password_confirmation);
|
||||
|
||||
expect(resp.data.foo).toEqual("bar");
|
||||
expect(axios.post).toHaveBeenCalledWith("/api/userStub", { user: inputs });
|
||||
});
|
||||
});
|
||||
|
||||
describe("didLogin()", () => {
|
||||
it("bootstraps the user session", () => {
|
||||
const dispatch = jest.fn();
|
||||
const result = didLogin(mockToken, dispatch);
|
||||
const result = didLogin(mockToken(), dispatch);
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
const { iss } = mockToken.token.unencoded;
|
||||
const { iss } = mockToken().token.unencoded;
|
||||
expect(API.setBaseUrl).toHaveBeenCalledWith(iss);
|
||||
const actions = dispatch.mock.calls.map(x => x && x[0] && x[0].type);
|
||||
expect(actions).toContain(Actions.REPLACE_TOKEN);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loginErr()", () => {
|
||||
it("creates a LOGIN_ERR action", () => {
|
||||
const result = loginErr();
|
||||
expect(result.type).toEqual(Actions.LOGIN_ERROR);
|
||||
expect(error).toHaveBeenCalledWith("Login failed.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("onRegistrationErr()", () => {
|
||||
it("calls toast when needed", () => {
|
||||
const err: UnsafeError = {};
|
||||
onRegistrationErr(jest.fn())(err);
|
||||
expect(toastErrors).toHaveBeenCalledWith(err);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onLogin", () => {
|
||||
it("replaces the session token", () => {
|
||||
it("fetches beta release info", () => {
|
||||
const dispatch = jest.fn();
|
||||
const thunk = onLogin(dispatch);
|
||||
const response: Partial<AxiosResponse<AuthState>> = { data: mockToken };
|
||||
thunk(response as AxiosResponse<AuthState>);
|
||||
expect(Session.replaceToken).toHaveBeenCalledWith(response.data);
|
||||
expect(push).toHaveBeenCalledWith("/app/controls");
|
||||
const mockAuth = mockToken();
|
||||
mockAuth.token.unencoded.beta_os_update_server = "beta_os_update_server";
|
||||
didLogin(mockAuth, dispatch);
|
||||
expect(fetchReleases).toHaveBeenCalledWith("os_update_server");
|
||||
expect(fetchReleases).toHaveBeenCalledWith("beta_os_update_server",
|
||||
{ beta: true });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import { AuthState } from "../interfaces";
|
|||
describe("Auth reducer", () => {
|
||||
function fakeToken(): AuthState {
|
||||
const output: Partial<AuthState> = {
|
||||
// tslint:disable-next-line:no-any
|
||||
token: ({} as any)
|
||||
};
|
||||
return output as AuthState;
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import axios, { AxiosResponse } from "axios";
|
||||
import { t } from "i18next";
|
||||
import { error, success } from "farmbot-toastr";
|
||||
import axios from "axios";
|
||||
import {
|
||||
fetchReleases, fetchMinOsFeatureData, FEATURE_MIN_VERSIONS_URL
|
||||
} from "../devices/actions";
|
||||
import { push } from "../history";
|
||||
import { AuthState } from "./interfaces";
|
||||
import { ReduxAction, Thunk } from "../redux/interfaces";
|
||||
import { ReduxAction } from "../redux/interfaces";
|
||||
import * as Sync from "../sync/actions";
|
||||
import { API } from "../api";
|
||||
import { Session } from "../session";
|
||||
import { UnsafeError } from "../interfaces";
|
||||
import {
|
||||
responseFulfilled,
|
||||
responseRejected,
|
||||
|
@ -18,7 +13,6 @@ import {
|
|||
} from "../interceptors";
|
||||
import { Actions } from "../constants";
|
||||
import { connectDevice } from "../connectivity/connect_device";
|
||||
import { toastErrors } from "../toast_errors";
|
||||
import { getFirstPartyFarmwareList } from "../farmware/actions";
|
||||
|
||||
export function didLogin(authState: AuthState, dispatch: Function) {
|
||||
|
@ -34,30 +28,6 @@ export function didLogin(authState: AuthState, dispatch: Function) {
|
|||
dispatch(connectDevice(authState));
|
||||
}
|
||||
|
||||
// We need to handle OK logins for numerous use cases (Ex: login & registration)
|
||||
export function onLogin(dispatch: Function) {
|
||||
return (response: AxiosResponse<AuthState>) => {
|
||||
const { data } = response;
|
||||
Session.replaceToken(data);
|
||||
didLogin(data, dispatch);
|
||||
push("/app/controls");
|
||||
};
|
||||
}
|
||||
|
||||
export function login(username: string, password: string, url: string): Thunk {
|
||||
return dispatch => {
|
||||
return requestToken(username, password, url).then(
|
||||
onLogin(dispatch),
|
||||
() => dispatch(loginErr())
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function loginErr() {
|
||||
error(t("Login failed."));
|
||||
return { type: Actions.LOGIN_ERROR };
|
||||
}
|
||||
|
||||
/** Very important. Once called, all outbound HTTP requests will
|
||||
* have a JSON Web Token attached to their "Authorization" header,
|
||||
* thereby granting access to the API. */
|
||||
|
@ -70,59 +40,3 @@ export function setToken(auth: AuthState): ReduxAction<AuthState> {
|
|||
payload: auth
|
||||
};
|
||||
}
|
||||
|
||||
/** Sign up for the FarmBot service over AJAX. */
|
||||
export function register(name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
confirmation: string): Thunk {
|
||||
return dispatch => {
|
||||
return requestRegistration(name, email, password, confirmation)
|
||||
.then(onLogin(dispatch), onRegistrationErr(dispatch));
|
||||
};
|
||||
}
|
||||
|
||||
/** Handle user registration errors. */
|
||||
export function onRegistrationErr(_: Function) {
|
||||
return (err: UnsafeError) => toastErrors(err);
|
||||
}
|
||||
|
||||
/** Build a JSON object in preparation for an HTTP POST
|
||||
* to registration endpoint */
|
||||
export function requestRegistration(name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
password_confirmation: string) {
|
||||
|
||||
const form = { user: { email, password, password_confirmation, name } };
|
||||
return axios.post(API.current.usersPath, form);
|
||||
}
|
||||
|
||||
/** Fetch API token if already registered. */
|
||||
export function requestToken(email: string,
|
||||
password: string,
|
||||
url: string) {
|
||||
const payload = { user: { email: email, password: password } };
|
||||
// Set the base URL once here.
|
||||
// It will get set once more when we get the "iss" claim from the JWT.
|
||||
API.setBaseUrl(url);
|
||||
return axios.post(API.current.tokensPath, payload);
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
// When logging out, we pop up a toast message to confirm logout.
|
||||
// Sometimes, LOGOUT is dispatched when the user is already logged out.
|
||||
// In those cases, seeing a logout message may confuse the user.
|
||||
// To circumvent this, we must check if the user had a token.
|
||||
// If there was infact a token, we can safely show the message.
|
||||
if (Session.fetchStoredToken()) {
|
||||
success(t("You have been logged out."));
|
||||
}
|
||||
|
||||
Session.clear();
|
||||
// Technically this is unreachable code:
|
||||
return {
|
||||
type: Actions.LOGOUT,
|
||||
payload: {}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,9 +31,10 @@ jest.mock("../../auth/actions", () => ({
|
|||
setToken: jest.fn()
|
||||
}));
|
||||
|
||||
import { ready } from "../actions";
|
||||
import { setToken } from "../../auth/actions";
|
||||
import { ready, storeToken } from "../actions";
|
||||
import { setToken, didLogin } from "../../auth/actions";
|
||||
import { Session } from "../../session";
|
||||
import { auth } from "../../__test_support__/fake_state/token";
|
||||
|
||||
describe("Actions", () => {
|
||||
it("calls didLogin()", () => {
|
||||
|
@ -53,4 +54,16 @@ describe("Actions", () => {
|
|||
thunk(dispatch, getState);
|
||||
expect(Session.clear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stores token", () => {
|
||||
const old = auth;
|
||||
old.token.unencoded.jti = "old";
|
||||
const dispatch = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
storeToken(old, dispatch)(undefined);
|
||||
expect(setToken).toHaveBeenCalledWith(old);
|
||||
expect(didLogin).toHaveBeenCalledWith(old, dispatch);
|
||||
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining(
|
||||
"Failed to refresh token"));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ export function setBoolViaRedux(key: BooleanConfigKey, val: boolean) {
|
|||
const conf = getWebAppConfig(store.getState().resources.index);
|
||||
if (conf) {
|
||||
store.dispatch(edit(conf, { [key]: val }));
|
||||
// tslint:disable-next-line:no-any
|
||||
store.dispatch(save(conf.uuid) as any);
|
||||
}
|
||||
return val;
|
||||
|
@ -44,6 +45,7 @@ export function setNumViaRedux(key: NumberConfigKey, val: number): number {
|
|||
const conf = getWebAppConfig(store.getState().resources.index);
|
||||
if (conf) {
|
||||
store.dispatch(edit(conf, { [key]: val }));
|
||||
// tslint:disable-next-line:no-any
|
||||
store.dispatch(save(conf.uuid) as any);
|
||||
}
|
||||
return val;
|
||||
|
|
|
@ -182,6 +182,7 @@ describe("onOnline", () => {
|
|||
describe("changeLastClientConnected", () => {
|
||||
it("tells farmbot when the last browser session was opened", () => {
|
||||
const setUserEnv = jest.fn(() => Promise.resolve({}));
|
||||
// tslint:disable-next-line:no-any
|
||||
const fakeFarmbot = { setUserEnv: setUserEnv as any } as Farmbot;
|
||||
changeLastClientConnected(fakeFarmbot)();
|
||||
expect(setUserEnv).toHaveBeenCalledWith(expect.objectContaining({
|
||||
|
|
|
@ -8,6 +8,7 @@ const mockRedux = {
|
|||
jest.mock("../../redux/store", () => mockRedux);
|
||||
jest.mock("lodash", () => {
|
||||
return {
|
||||
// tslint:disable-next-line:no-any
|
||||
set(target: any, key: string, val: any) { target[key] = val; },
|
||||
times: (n: number, iter: Function) => {
|
||||
let n2 = n;
|
||||
|
@ -53,7 +54,7 @@ describe("autoSync", () => {
|
|||
const dispatch = jest.fn();
|
||||
const getState: GetState = jest.fn();
|
||||
const chan = "chanName";
|
||||
const payload = new Buffer([]);
|
||||
const payload = Buffer.from([]);
|
||||
const rmd = routeMqttData(chan, payload);
|
||||
autoSync(dispatch, getState)(chan, payload);
|
||||
expect(handleInbound).toHaveBeenCalledWith(dispatch, getState, rmd);
|
||||
|
|
|
@ -50,12 +50,15 @@ function fakeBot(): Farmbot {
|
|||
}
|
||||
|
||||
function expectStale() {
|
||||
expect(dispatchNetworkDown).toHaveBeenCalledWith("bot.mqtt", undefined, expect.any(String));
|
||||
expect(dispatchNetworkDown)
|
||||
.toHaveBeenCalledWith("bot.mqtt", undefined, expect.any(String));
|
||||
}
|
||||
|
||||
function expectActive() {
|
||||
expect(dispatchNetworkUp).toHaveBeenCalledWith("bot.mqtt", undefined, expect.any(String));
|
||||
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt", undefined, expect.any(String));
|
||||
expect(dispatchNetworkUp)
|
||||
.toHaveBeenCalledWith("bot.mqtt", undefined, expect.any(String));
|
||||
expect(dispatchNetworkUp)
|
||||
.toHaveBeenCalledWith("user.mqtt", undefined, expect.any(String));
|
||||
}
|
||||
|
||||
describe("ping util", () => {
|
||||
|
|
|
@ -3,7 +3,8 @@ import { networkUp, networkDown } from "../actions";
|
|||
|
||||
describe("connectivityReducer", () => {
|
||||
it("goes up", () => {
|
||||
const state = connectivityReducer(DEFAULT_STATE, networkUp("user.mqtt", undefined, "tests"));
|
||||
const state = connectivityReducer(DEFAULT_STATE,
|
||||
networkUp("user.mqtt", undefined, "tests"));
|
||||
expect(state).toBeDefined();
|
||||
const x = state && state["user.mqtt"];
|
||||
if (x) {
|
||||
|
@ -15,7 +16,8 @@ describe("connectivityReducer", () => {
|
|||
});
|
||||
|
||||
it("goes down", () => {
|
||||
const state = connectivityReducer(DEFAULT_STATE, networkDown("user.api", undefined, "tests"));
|
||||
const state = connectivityReducer(DEFAULT_STATE,
|
||||
networkDown("user.api", undefined, "tests"));
|
||||
const x = state && state["user.api"];
|
||||
if (x) {
|
||||
expect(x.state).toBe("down");
|
||||
|
|
|
@ -459,6 +459,10 @@ export namespace Content {
|
|||
that you can provide in support requests to allow FarmBot to look up
|
||||
data relevant to the issue to help us identify the problem.`);
|
||||
|
||||
export const DEVICE_NEVER_SEEN =
|
||||
trim(`The device has never been seen. Most likely,
|
||||
there is a network connectivity issue on the device's end.`);
|
||||
|
||||
// Hardware Settings
|
||||
export const RESTORE_DEFAULT_HARDWARE_SETTINGS =
|
||||
trim(`Restoring hardware parameter defaults will destroy the
|
||||
|
|
|
@ -10,7 +10,9 @@ jest.mock("farmbot-toastr", () => ({ success: jest.fn() }));
|
|||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { DirectionButton, directionDisabled, calculateDistance } from "../direction_button";
|
||||
import {
|
||||
DirectionButton, directionDisabled, calculateDistance
|
||||
} from "../direction_button";
|
||||
import { DirectionButtonProps } from "../interfaces";
|
||||
|
||||
function fakeButtonProps(): DirectionButtonProps {
|
||||
|
|
|
@ -1,30 +1,33 @@
|
|||
import * as React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { MustBeOnline, isBotUp } from "../must_be_online";
|
||||
import { MustBeOnline, isBotUp, MBOProps } from "../must_be_online";
|
||||
|
||||
describe("<MustBeOnline/>", function () {
|
||||
it("Covers content when status is 'unknown'", function () {
|
||||
const elem = <MustBeOnline networkState="down" syncStatus={"sync_now"}>
|
||||
describe("<MustBeOnline/>", () => {
|
||||
const fakeProps = (): MBOProps => ({
|
||||
networkState: "down",
|
||||
syncStatus: "sync_now",
|
||||
});
|
||||
|
||||
it("Covers content when status is 'unknown'", () => {
|
||||
const elem = <MustBeOnline {...fakeProps()}>
|
||||
<span>Covered</span>
|
||||
</MustBeOnline>;
|
||||
const overlay = shallow(elem).find("div");
|
||||
expect(overlay.hasClass("unavailable")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Uncovered when locked open", function () {
|
||||
const elem = <MustBeOnline networkState="down" syncStatus={"sync_now"} lockOpen={true}>
|
||||
<span>Uncovered</span>
|
||||
</MustBeOnline>;
|
||||
const overlay = shallow(elem).find("div");
|
||||
it("is uncovered when locked open", () => {
|
||||
const p = fakeProps();
|
||||
p.lockOpen = true;
|
||||
const overlay = shallow(<MustBeOnline {...p} />).find("div");
|
||||
expect(overlay.hasClass("unavailable")).toBeFalsy();
|
||||
expect(overlay.hasClass("banner")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Doesn't show banner", function () {
|
||||
const elem = <MustBeOnline networkState="down" syncStatus={"sync_now"} hideBanner={true}>
|
||||
<span>Uncovered</span>
|
||||
</MustBeOnline>;
|
||||
const overlay = shallow(elem).find("div");
|
||||
it("doesn't show banner", () => {
|
||||
const p = fakeProps();
|
||||
p.hideBanner = true;
|
||||
const overlay = shallow(<MustBeOnline {...p} />).find("div");
|
||||
expect(overlay.hasClass("unavailable")).toBeTruthy();
|
||||
expect(overlay.hasClass("banner")).toBeFalsy();
|
||||
});
|
||||
|
|
|
@ -46,7 +46,8 @@ describe("<FarmbotOsSettings/>", () => {
|
|||
|
||||
it("fetches OS release notes", async () => {
|
||||
mockReleaseNoteData = { data: "intro\n\n# v6\n\n* note" };
|
||||
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings {...fakeProps()} />);
|
||||
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings
|
||||
{...fakeProps()} />);
|
||||
await expect(axios.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining("RELEASE_NOTES.md"));
|
||||
expect(osSettings.instance().state.osReleaseNotes)
|
||||
|
@ -55,7 +56,8 @@ describe("<FarmbotOsSettings/>", () => {
|
|||
|
||||
it("doesn't fetch OS release notes", async () => {
|
||||
mockReleaseNoteData = { data: "empty notes" };
|
||||
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings {...fakeProps()} />);
|
||||
const osSettings = await mount<FarmbotOsSettings>(<FarmbotOsSettings
|
||||
{...fakeProps()} />);
|
||||
await expect(axios.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining("RELEASE_NOTES.md"));
|
||||
expect(osSettings.instance().state.osReleaseNotes)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { sourceFbosConfigValue, sourceFwConfigValue } from "../source_config_value";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import { fakeFbosConfig, fakeFirmwareConfig } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeFbosConfig, fakeFirmwareConfig
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("sourceFbosConfigValue()", () => {
|
||||
it("returns api value", () => {
|
||||
|
|
|
@ -44,7 +44,7 @@ export class CameraSelection
|
|||
getDevice()
|
||||
.setUserEnv(message)
|
||||
.then(() => {
|
||||
success(t("Successfully configured camera!"),t("Success"));
|
||||
success(t("Successfully configured camera!"), t("Success"));
|
||||
})
|
||||
.catch(() => error(t("An error occurred during configuration.")));
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { t } from "i18next";
|
|||
import * as moment from "moment";
|
||||
import { TaggedDevice } from "farmbot";
|
||||
import { ColWidth } from "../farmbot_os_settings";
|
||||
import { Content } from "../../../constants";
|
||||
|
||||
export interface LastSeenProps {
|
||||
onClick?(): void;
|
||||
|
@ -46,7 +47,7 @@ export class LastSeen extends React.Component<LastSeenProps, {}> {
|
|||
};
|
||||
return t(text, data);
|
||||
} else {
|
||||
return t("The device has never been seen. Most likely, there is a network connectivity issue on the device's end.");
|
||||
return t(Content.DEVICE_NEVER_SEEN);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ import { PinGuardMCUInputGroupProps } from "./interfaces";
|
|||
import { Row, Col } from "../../ui/index";
|
||||
import { settingToggle } from "../actions";
|
||||
import { ToggleButton } from "../../controls/toggle_button";
|
||||
import { isUndefined } from "util";
|
||||
import { t } from "i18next";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
export function PinGuardMCUInputGroup(props: PinGuardMCUInputGroupProps) {
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ import {
|
|||
} from "farmbot";
|
||||
import { ResourceIndex } from "../resources/interfaces";
|
||||
import { WD_ENV } from "../farmware/weed_detector/remote_env/interfaces";
|
||||
import { ConnectionStatus, ConnectionState, NetworkState } from "../connectivity/interfaces";
|
||||
import {
|
||||
ConnectionStatus, ConnectionState, NetworkState
|
||||
} from "../connectivity/interfaces";
|
||||
import { IntegerSize } from "../util";
|
||||
import { WebAppConfig } from "../config_storage/web_app_configs";
|
||||
import { FirmwareConfig } from "../config_storage/firmware_configs";
|
||||
|
|
|
@ -29,7 +29,9 @@ import { error, warning } from "farmbot-toastr";
|
|||
import {
|
||||
fakeResourceIndex
|
||||
} from "../../../sequences/step_tiles/tile_move_absolute/test_helpers";
|
||||
import { PinBindingType, PinBindingSpecialAction } from "farmbot/dist/resources/api_resources";
|
||||
import {
|
||||
PinBindingType, PinBindingSpecialAction
|
||||
} from "farmbot/dist/resources/api_resources";
|
||||
|
||||
describe("<PinBindingInputGroup/>", () => {
|
||||
function fakeProps(): PinBindingInputGroupProps {
|
||||
|
@ -146,7 +148,8 @@ describe("<PinBindingInputGroup/>", () => {
|
|||
});
|
||||
|
||||
it("sets pin", () => {
|
||||
const wrapper = mount<PinBindingInputGroup>(<PinBindingInputGroup {...fakeProps()} />);
|
||||
const wrapper = mount<PinBindingInputGroup>(<PinBindingInputGroup
|
||||
{...fakeProps()} />);
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(undefined);
|
||||
// tslint:disable-next-line:no-any
|
||||
const instance = wrapper.instance() as any;
|
||||
|
@ -163,24 +166,30 @@ describe("<PinBindingInputGroup/>", () => {
|
|||
});
|
||||
|
||||
it("changes pin number", () => {
|
||||
const wrapper = shallow<PinBindingInputGroup>(<PinBindingInputGroup {...fakeProps()} />);
|
||||
const wrapper = shallow<PinBindingInputGroup>(<PinBindingInputGroup
|
||||
{...fakeProps()} />);
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(undefined);
|
||||
wrapper.instance().setSelectedPin(7);
|
||||
expect(wrapper.instance().state.pinNumberInput).toEqual(7);
|
||||
});
|
||||
|
||||
it("changes binding type", () => {
|
||||
const wrapper = shallow<PinBindingInputGroup>(<PinBindingInputGroup {...fakeProps()} />);
|
||||
const wrapper = shallow<PinBindingInputGroup>(<PinBindingInputGroup
|
||||
{...fakeProps()} />);
|
||||
expect(wrapper.instance().state.bindingType).toEqual(PinBindingType.standard);
|
||||
wrapper.instance().setBindingType({ label: "", value: PinBindingType.special });
|
||||
expect(wrapper.instance().state.bindingType).toEqual(PinBindingType.special);
|
||||
});
|
||||
|
||||
it("changes special action", () => {
|
||||
const wrapper = shallow<PinBindingInputGroup>(<PinBindingInputGroup {...fakeProps()} />);
|
||||
const wrapper = shallow<PinBindingInputGroup>(<PinBindingInputGroup
|
||||
{...fakeProps()} />);
|
||||
wrapper.setState({ bindingType: PinBindingType.special });
|
||||
expect(wrapper.instance().state.specialActionInput).toEqual(undefined);
|
||||
wrapper.instance().setSpecialAction({ label: "", value: PinBindingSpecialAction.sync });
|
||||
wrapper.instance().setSpecialAction({
|
||||
label: "",
|
||||
value: PinBindingSpecialAction.sync
|
||||
});
|
||||
expect(wrapper.instance().state.specialActionInput)
|
||||
.toEqual(PinBindingSpecialAction.sync);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { PinBindingType, PinBindingSpecialAction } from "farmbot/dist/resources/api_resources";
|
||||
import {
|
||||
PinBindingType, PinBindingSpecialAction
|
||||
} from "farmbot/dist/resources/api_resources";
|
||||
const mockDevice = {
|
||||
registerGpio: jest.fn(() => { return Promise.resolve(); }),
|
||||
unregisterGpio: jest.fn(() => { return Promise.resolve(); }),
|
||||
|
|
|
@ -22,7 +22,9 @@ import {
|
|||
} from "./list_and_label_support";
|
||||
import { SequenceSelectBox } from "../../sequences/sequence_select_box";
|
||||
import { ResourceIndex } from "../../resources/interfaces";
|
||||
import { PinBindingType, PinBindingSpecialAction } from "farmbot/dist/resources/api_resources";
|
||||
import {
|
||||
PinBindingType, PinBindingSpecialAction
|
||||
} from "farmbot/dist/resources/api_resources";
|
||||
|
||||
export class PinBindingInputGroup
|
||||
extends React.Component<PinBindingInputGroupProps, PinBindingInputGroupState> {
|
||||
|
|
|
@ -36,7 +36,8 @@ export const gpio = [
|
|||
["GND", 21],
|
||||
];
|
||||
|
||||
export class RpiGpioDiagram extends React.Component<RpiGpioDiagramProps, RpiGpioDiagramState> {
|
||||
export class RpiGpioDiagram
|
||||
extends React.Component<RpiGpioDiagramProps, RpiGpioDiagramState> {
|
||||
state: RpiGpioDiagramState = { hoveredPin: undefined };
|
||||
|
||||
hover = (hovered: number | string | undefined) =>
|
||||
|
|
|
@ -16,7 +16,9 @@ jest.mock("farmbot-toastr", () => ({ error: jest.fn() }));
|
|||
|
||||
import { transferOwnership } from "../transfer_ownership";
|
||||
import { getDevice } from "../../../device";
|
||||
import { submitOwnershipChange } from "../../components/fbos_settings/change_ownership_form";
|
||||
import {
|
||||
submitOwnershipChange
|
||||
} from "../../components/fbos_settings/change_ownership_form";
|
||||
import { error } from "farmbot-toastr";
|
||||
import { API } from "../../../api";
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { DataXfer, DataXferIntent, DataXferBase } from "./interfaces";
|
||||
import { uuid as id } from "farmbot";
|
||||
import { SequenceBodyItem as Step } from "farmbot";
|
||||
import { SequenceBodyItem as Step, uuid as id } from "farmbot";
|
||||
import { Everything } from "../interfaces";
|
||||
import { ReduxAction } from "../redux/interfaces";
|
||||
import * as React from "react";
|
||||
|
|
|
@ -1,58 +1,43 @@
|
|||
const mockStorj: Dictionary<boolean> = {};
|
||||
|
||||
jest.mock("../../session", () => {
|
||||
return {
|
||||
Session: {
|
||||
deprecatedGetBool: (k: string) => {
|
||||
mockStorj[k] = !!mockStorj[k];
|
||||
return mockStorj[k];
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import { Dictionary } from "farmbot";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
import { getDefaultAxisLength, getGridSize } from "../index";
|
||||
import { WebAppConfig } from "../../config_storage/web_app_configs";
|
||||
|
||||
describe("getDefaultAxisLength()", () => {
|
||||
it("returns axis lengths", () => {
|
||||
const axes = getDefaultAxisLength();
|
||||
const axes = getDefaultAxisLength(() => false);
|
||||
expect(axes).toEqual({ x: 2900, y: 1400 });
|
||||
});
|
||||
|
||||
it("returns XL axis lengths", () => {
|
||||
mockStorj[BooleanSetting.map_xl] = true;
|
||||
const axes = getDefaultAxisLength();
|
||||
const axes = getDefaultAxisLength(() => true);
|
||||
expect(axes).toEqual({ x: 5900, y: 2900 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getGridSize()", () => {
|
||||
it("returns default grid size", () => {
|
||||
mockStorj[BooleanSetting.map_xl] = false;
|
||||
const grid = getGridSize({
|
||||
x: { value: 100, isDefault: false },
|
||||
y: { value: 200, isDefault: false }
|
||||
});
|
||||
const grid = getGridSize(
|
||||
k => ({ dynamic_map: false, map_xl: false } as WebAppConfig)[k], {
|
||||
x: { value: 100, isDefault: false },
|
||||
y: { value: 200, isDefault: false }
|
||||
});
|
||||
expect(grid).toEqual({ x: 2900, y: 1400 });
|
||||
});
|
||||
|
||||
it("returns XL grid size", () => {
|
||||
mockStorj[BooleanSetting.map_xl] = true;
|
||||
const grid = getGridSize({
|
||||
x: { value: 100, isDefault: false },
|
||||
y: { value: 200, isDefault: false }
|
||||
});
|
||||
const grid = getGridSize(
|
||||
k => ({ dynamic_map: false, map_xl: true } as WebAppConfig)[k], {
|
||||
x: { value: 100, isDefault: false },
|
||||
y: { value: 200, isDefault: false }
|
||||
});
|
||||
expect(grid).toEqual({ x: 5900, y: 2900 });
|
||||
});
|
||||
|
||||
it("returns grid size using bot size", () => {
|
||||
mockStorj[BooleanSetting.dynamic_map] = true;
|
||||
const grid = getGridSize({
|
||||
x: { value: 100, isDefault: false },
|
||||
y: { value: 200, isDefault: false }
|
||||
});
|
||||
const grid = getGridSize(
|
||||
k => ({ dynamic_map: true, map_xl: false } as WebAppConfig)[k], {
|
||||
x: { value: 100, isDefault: false },
|
||||
y: { value: 200, isDefault: false }
|
||||
});
|
||||
expect(grid).toEqual({ x: 100, y: 200 });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { designer } from "../reducer";
|
||||
import { Actions } from "../../constants";
|
||||
import { ReduxAction } from "../../redux/interfaces";
|
||||
import { DesignerState, HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult } from "../interfaces";
|
||||
import {
|
||||
DesignerState, HoveredPlantPayl, CurrentPointPayl, CropLiveSearchResult
|
||||
} from "../interfaces";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
|
||||
describe("designer reducer", () => {
|
||||
|
@ -97,8 +99,9 @@ describe("designer reducer", () => {
|
|||
image: "lettuce"
|
||||
}
|
||||
];
|
||||
const action: ReduxAction<typeof payload> =
|
||||
{ type: Actions.OF_SEARCH_RESULTS_OK, payload };
|
||||
const action: ReduxAction<typeof payload> = {
|
||||
type: Actions.OF_SEARCH_RESULTS_OK, payload
|
||||
};
|
||||
const newState = designer(oldState(), action);
|
||||
expect(newState.cropSearchResults).toEqual(payload);
|
||||
});
|
||||
|
|
|
@ -12,7 +12,9 @@ import * as React from "react";
|
|||
import { mount, shallow } from "enzyme";
|
||||
import { AddFarmEvent } from "../add_farm_event";
|
||||
import { AddEditFarmEventProps } from "../../interfaces";
|
||||
import { fakeFarmEvent, fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeFarmEvent, fakeSequence
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<AddFarmEvent />", () => {
|
||||
function fakeProps(): AddEditFarmEventProps {
|
||||
|
|
|
@ -12,7 +12,9 @@ import * as React from "react";
|
|||
import { mount } from "enzyme";
|
||||
import { EditFarmEvent } from "../edit_farm_event";
|
||||
import { AddEditFarmEventProps } from "../../interfaces";
|
||||
import { fakeFarmEvent, fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeFarmEvent, fakeSequence
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<EditFarmEvent />", () => {
|
||||
function fakeProps(): AddEditFarmEventProps {
|
||||
|
|
|
@ -8,7 +8,9 @@ jest.mock("../../../history", () => ({
|
|||
|
||||
import { mapStateToPropsAddEdit } from "../map_state_to_props_add_edit";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { buildResourceIndex, fakeDevice } from "../../../__test_support__/resource_index_builder";
|
||||
import {
|
||||
buildResourceIndex, fakeDevice
|
||||
} from "../../../__test_support__/resource_index_builder";
|
||||
import {
|
||||
fakeSequence, fakeRegimen, fakeFarmEvent
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
import { betterCompact } from "../../../util";
|
||||
import { TaggedFarmEvent } from "farmbot";
|
||||
|
||||
export function joinFarmEventsToExecutable(input: ResourceIndex): FarmEventWithExecutable[] {
|
||||
export function joinFarmEventsToExecutable(
|
||||
input: ResourceIndex): FarmEventWithExecutable[] {
|
||||
const farmEvents: TaggedFarmEvent[] = selectAllFarmEvents(input);
|
||||
const sequenceById = indexSequenceById(input);
|
||||
const regimenById = indexRegimenById(input);
|
||||
|
|
|
@ -29,7 +29,9 @@ import { EventTimePicker } from "./event_time_picker";
|
|||
import { TzWarning } from "./tz_warning";
|
||||
import { nextRegItemTimes } from "./map_state_to_props";
|
||||
import { first } from "lodash";
|
||||
import { TimeUnit, ExecutableType, FarmEvent } from "farmbot/dist/resources/api_resources";
|
||||
import {
|
||||
TimeUnit, ExecutableType, FarmEvent
|
||||
} from "farmbot/dist/resources/api_resources";
|
||||
|
||||
type FormEvent = React.SyntheticEvent<HTMLInputElement>;
|
||||
export const NEVER: TimeUnit = "never";
|
||||
|
@ -51,7 +53,8 @@ export interface FarmEventViewModel {
|
|||
* by the edit form.
|
||||
* USE CASE EXAMPLE: We have a "date" and "time" field that are created from
|
||||
* a single "start_time" FarmEvent field. */
|
||||
export function destructureFarmEvent(fe: TaggedFarmEvent, timeOffset: number): FarmEventViewModel {
|
||||
export function destructureFarmEvent(
|
||||
fe: TaggedFarmEvent, timeOffset: number): FarmEventViewModel {
|
||||
|
||||
return {
|
||||
startDate: formatDate((fe.body.start_time).toString(), timeOffset),
|
||||
|
|
|
@ -6,7 +6,6 @@ import { mapStateToProps } from "./state_to_props";
|
|||
import { history } from "../history";
|
||||
import { Plants } from "./plants/plant_inventory";
|
||||
import { GardenMapLegend } from "./map/garden_map_legend";
|
||||
import { Session, safeBooleanSettting } from "../session";
|
||||
import { NumericSetting, BooleanSetting } from "../session_keys";
|
||||
import { isUndefined, last } from "lodash";
|
||||
import { AxisNumberProperty, BotSize } from "./map/interfaces";
|
||||
|
@ -14,23 +13,26 @@ import { getBotSize, round } from "./map/util";
|
|||
import { calcZoomLevel, getZoomLevelIndex, saveZoomLevelIndex } from "./map/zoom";
|
||||
import * as moment from "moment";
|
||||
import { DesignerNavTabs } from "./panel_header";
|
||||
import { setWebAppConfigValue, GetWebAppConfigValue } from "../config_storage/actions";
|
||||
|
||||
export const getDefaultAxisLength = (): AxisNumberProperty => {
|
||||
if (Session.deprecatedGetBool(BooleanSetting.map_xl)) {
|
||||
return { x: 5900, y: 2900 };
|
||||
} else {
|
||||
return { x: 2900, y: 1400 };
|
||||
}
|
||||
};
|
||||
export const getDefaultAxisLength =
|
||||
(getConfigValue: GetWebAppConfigValue): AxisNumberProperty => {
|
||||
if (getConfigValue(BooleanSetting.map_xl)) {
|
||||
return { x: 5900, y: 2900 };
|
||||
} else {
|
||||
return { x: 2900, y: 1400 };
|
||||
}
|
||||
};
|
||||
|
||||
export const getGridSize = (botSize: BotSize) => {
|
||||
if (Session.deprecatedGetBool(BooleanSetting.dynamic_map)) {
|
||||
// Render the map size according to device axis length.
|
||||
return { x: round(botSize.x.value), y: round(botSize.y.value) };
|
||||
}
|
||||
// Use a default map size.
|
||||
return getDefaultAxisLength();
|
||||
};
|
||||
export const getGridSize =
|
||||
(getConfigValue: GetWebAppConfigValue, botSize: BotSize) => {
|
||||
if (getConfigValue(BooleanSetting.dynamic_map)) {
|
||||
// Render the map size according to device axis length.
|
||||
return { x: round(botSize.x.value), y: round(botSize.y.value) };
|
||||
}
|
||||
// Use a default map size.
|
||||
return getDefaultAxisLength(getConfigValue);
|
||||
};
|
||||
|
||||
export const gridOffset: AxisNumberProperty = { x: 50, y: 50 };
|
||||
|
||||
|
@ -39,17 +41,17 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
|
||||
initializeSetting =
|
||||
(name: keyof State, defaultValue: boolean): boolean => {
|
||||
const currentValue = Session.deprecatedGetBool(safeBooleanSettting(name));
|
||||
const currentValue = this.props.getConfigValue(name);
|
||||
if (isUndefined(currentValue)) {
|
||||
Session.setBool(safeBooleanSettting(name), defaultValue);
|
||||
this.props.dispatch(setWebAppConfigValue(name, defaultValue));
|
||||
return defaultValue;
|
||||
} else {
|
||||
return currentValue;
|
||||
return !!currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
getBotOriginQuadrant = (): BotOriginQuadrant => {
|
||||
const value = Session.deprecatedGetNum(NumericSetting.bot_origin_quadrant);
|
||||
const value = this.props.getConfigValue(NumericSetting.bot_origin_quadrant);
|
||||
return isBotOriginQuadrant(value) ? value : 2;
|
||||
}
|
||||
|
||||
|
@ -70,13 +72,15 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
}
|
||||
|
||||
toggle = (name: keyof State) => () => {
|
||||
this.setState({ [name]: !this.state[name] });
|
||||
Session.invertBool(safeBooleanSettting(name));
|
||||
const newValue = !this.state[name];
|
||||
this.props.dispatch(setWebAppConfigValue(name, newValue));
|
||||
this.setState({ [name]: newValue });
|
||||
}
|
||||
|
||||
updateBotOriginQuadrant = (payload: BotOriginQuadrant) => () => {
|
||||
this.setState({ bot_origin_quadrant: payload });
|
||||
Session.deprecatedSetNum(NumericSetting.bot_origin_quadrant, payload);
|
||||
this.props.dispatch(setWebAppConfigValue(
|
||||
NumericSetting.bot_origin_quadrant, payload));
|
||||
}
|
||||
|
||||
updateZoomLevel = (zoomIncrement: number) => () => {
|
||||
|
@ -107,7 +111,9 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
} = this.state;
|
||||
|
||||
const botSize = getBotSize(
|
||||
this.props.botMcuParams, this.props.stepsPerMmXY, getDefaultAxisLength());
|
||||
this.props.botMcuParams,
|
||||
this.props.stepsPerMmXY,
|
||||
getDefaultAxisLength(this.props.getConfigValue));
|
||||
|
||||
const stopAtHome = {
|
||||
x: !!this.props.botMcuParams.movement_stop_at_home_x,
|
||||
|
@ -170,7 +176,7 @@ export class FarmDesigner extends React.Component<Props, Partial<State>> {
|
|||
hoveredPlant={this.props.hoveredPlant}
|
||||
zoomLvl={zoom_level}
|
||||
botOriginQuadrant={bot_origin_quadrant}
|
||||
gridSize={getGridSize(botSize)}
|
||||
gridSize={getGridSize(this.props.getConfigValue, botSize)}
|
||||
gridOffset={gridOffset}
|
||||
peripherals={this.props.peripherals}
|
||||
eStopStatus={this.props.eStopStatus}
|
||||
|
|
|
@ -28,7 +28,7 @@ import { ExecutableType, PlantPointer } from "farmbot/dist/resources/api_resourc
|
|||
*/
|
||||
export enum BotOriginQuadrant { ONE = 1, TWO = 2, THREE = 3, FOUR = 4 }
|
||||
|
||||
type Mystery = BotOriginQuadrant | number | undefined;
|
||||
type Mystery = BotOriginQuadrant | number | string | boolean | undefined;
|
||||
export function isBotOriginQuadrant(mystery: Mystery):
|
||||
mystery is BotOriginQuadrant {
|
||||
return isNumber(mystery) && [1, 2, 3, 4].includes(mystery);
|
||||
|
|
|
@ -1,23 +1,8 @@
|
|||
const mockStorj: Dictionary<boolean> = {};
|
||||
|
||||
jest.mock("../../../session", () => {
|
||||
return {
|
||||
Session: {
|
||||
deprecatedGetBool: (k: string) => {
|
||||
mockStorj[k] = !!mockStorj[k];
|
||||
return mockStorj[k];
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import { Dictionary } from "farmbot";
|
||||
import * as React from "react";
|
||||
import { GardenPlant } from "../garden_plant";
|
||||
import { shallow } from "enzyme";
|
||||
import { GardenPlantProps } from "../interfaces";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
import { Actions } from "../../../constants";
|
||||
import { fakeMapTransformProps } from "../../../__test_support__/map_transform_props";
|
||||
|
||||
|
@ -32,13 +17,15 @@ describe("<GardenPlant/>", () => {
|
|||
dispatch: jest.fn(),
|
||||
zoomLvl: 1.8,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
uuid: "plantUuid"
|
||||
uuid: "plantUuid",
|
||||
animate: false,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders plant", () => {
|
||||
mockStorj[BooleanSetting.disable_animations] = true;
|
||||
const wrapper = shallow(<GardenPlant {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
p.animate = false;
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
expect(wrapper.find("image").length).toEqual(1);
|
||||
expect(wrapper.find("image").props().opacity).toEqual(1);
|
||||
expect(wrapper.find("text").length).toEqual(0);
|
||||
|
@ -48,8 +35,9 @@ describe("<GardenPlant/>", () => {
|
|||
});
|
||||
|
||||
it("renders plant animations", () => {
|
||||
mockStorj[BooleanSetting.disable_animations] = false;
|
||||
const wrapper = shallow(<GardenPlant {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
p.animate = true;
|
||||
const wrapper = shallow(<GardenPlant {...p} />);
|
||||
expect(wrapper.find(".soil-cloud").length).toEqual(1);
|
||||
expect(wrapper.find(".animate").length).toEqual(1);
|
||||
});
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { DragHelpersProps } from "./interfaces";
|
||||
import { round, transformXY, getMapSize } from "./util";
|
||||
import { isUndefined } from "util";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { Color } from "../../ui/index";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
enum Alignment {
|
||||
NONE = "not aligned",
|
||||
|
|
|
@ -125,6 +125,11 @@ export class GardenMap extends
|
|||
/** Currently editing a plant? */
|
||||
get isEditing(): boolean { return getMode() === Mode.editPlant; }
|
||||
|
||||
/** Display plant animations? */
|
||||
get animate(): boolean {
|
||||
return !this.props.getConfigValue(BooleanSetting.disable_animations);
|
||||
}
|
||||
|
||||
endDrag = () => {
|
||||
const p = this.getPlant();
|
||||
if (p && this.state.isDragging) {
|
||||
|
@ -402,7 +407,8 @@ export class GardenMap extends
|
|||
zoomLvl={this.props.zoomLvl}
|
||||
activeDragXY={this.state.activeDragXY}
|
||||
activeDragSpread={this.state.activeDragSpread}
|
||||
editing={this.isEditing} />
|
||||
editing={this.isEditing}
|
||||
animate={this.animate} />
|
||||
<PointLayer
|
||||
mapTransformProps={mapTransformProps}
|
||||
visible={!!this.props.showPoints}
|
||||
|
@ -418,7 +424,8 @@ export class GardenMap extends
|
|||
editing={this.isEditing}
|
||||
selectedForDel={this.props.designer.selectedPlants}
|
||||
zoomLvl={this.props.zoomLvl}
|
||||
activeDragXY={this.state.activeDragXY} />
|
||||
activeDragXY={this.state.activeDragXY}
|
||||
animate={this.animate} />
|
||||
<ToolSlotLayer
|
||||
mapTransformProps={mapTransformProps}
|
||||
visible={!!this.props.showFarmbot}
|
||||
|
@ -431,7 +438,8 @@ export class GardenMap extends
|
|||
botSize={this.props.botSize}
|
||||
plantAreaOffset={this.props.gridOffset}
|
||||
peripherals={this.props.peripherals}
|
||||
eStopStatus={this.props.eStopStatus} />
|
||||
eStopStatus={this.props.eStopStatus}
|
||||
getConfigValue={this.props.getConfigValue} />
|
||||
<HoveredPlantLayer
|
||||
visible={!!this.props.showPlants}
|
||||
isEditing={this.isEditing}
|
||||
|
@ -439,7 +447,8 @@ export class GardenMap extends
|
|||
currentPlant={this.getPlant()}
|
||||
designer={this.props.designer}
|
||||
hoveredPlant={this.props.hoveredPlant}
|
||||
dragging={!!this.state.isDragging} />
|
||||
dragging={!!this.state.isDragging}
|
||||
animate={this.animate} />
|
||||
<DragHelperLayer
|
||||
mapTransformProps={mapTransformProps}
|
||||
currentPlant={this.getPlant()}
|
||||
|
|
|
@ -3,8 +3,6 @@ import { GardenPlantProps, GardenPlantState } from "./interfaces";
|
|||
import { cachedCrop, DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
|
||||
import { round, transformXY } from "./util";
|
||||
import { DragHelpers } from "./drag_helpers";
|
||||
import { Session } from "../../session";
|
||||
import { BooleanSetting } from "../../session_keys";
|
||||
import { Color } from "../../ui/index";
|
||||
import { Actions } from "../../constants";
|
||||
|
||||
|
@ -51,13 +49,12 @@ export class GardenPlant extends
|
|||
|
||||
render() {
|
||||
const { selected, dragging, plant, grayscale, mapTransformProps,
|
||||
activeDragXY, zoomLvl } = this.props;
|
||||
activeDragXY, zoomLvl, animate } = this.props;
|
||||
const { id, radius, x, y } = plant.body;
|
||||
const { icon } = this.state;
|
||||
|
||||
const { qx, qy } = transformXY(round(x), round(y), mapTransformProps);
|
||||
const alpha = dragging ? 0.4 : 1.0;
|
||||
const animate = !Session.deprecatedGetBool(BooleanSetting.disable_animations);
|
||||
|
||||
return <g id={"plant-" + id}>
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {
|
||||
TaggedPlantPointer,
|
||||
TaggedGenericPointer
|
||||
TaggedGenericPointer,
|
||||
TaggedCrop
|
||||
} from "farmbot";
|
||||
import { TaggedCrop } from "farmbot";
|
||||
import { State, BotOriginQuadrant } from "../interfaces";
|
||||
import { BotPosition, BotLocationData } from "../../devices/interfaces";
|
||||
import { GetWebAppConfigValue } from "../../config_storage/actions";
|
||||
|
@ -19,6 +19,7 @@ export interface PlantLayerProps {
|
|||
zoomLvl: number;
|
||||
activeDragXY: BotPosition | undefined;
|
||||
selectedForDel: string[] | undefined;
|
||||
animate: boolean;
|
||||
}
|
||||
|
||||
export interface CropSpreadDict {
|
||||
|
@ -59,6 +60,7 @@ export interface GardenPlantProps {
|
|||
activeDragXY: BotPosition | undefined;
|
||||
uuid: string;
|
||||
grayscale: boolean;
|
||||
animate: boolean;
|
||||
}
|
||||
|
||||
export interface GardenPlantState {
|
||||
|
@ -114,6 +116,7 @@ export interface VirtualFarmBotProps {
|
|||
plantAreaOffset: AxisNumberProperty;
|
||||
peripherals: { label: string, value: boolean }[];
|
||||
eStopStatus: boolean;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
}
|
||||
|
||||
export interface FarmBotLayerProps extends VirtualFarmBotProps, BotExtentsProps {
|
||||
|
|
|
@ -21,7 +21,8 @@ describe("<FarmBotLayer/>", () => {
|
|||
},
|
||||
plantAreaOffset: { x: 100, y: 100 },
|
||||
peripherals: [],
|
||||
eStopStatus: false
|
||||
eStopStatus: false,
|
||||
getConfigValue: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ describe("<HoveredPlantLayer/>", () => {
|
|||
hoveredPlant: fakePlant(),
|
||||
isEditing: false,
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
animate: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import * as React from "react";
|
||||
import { ImageLayer, ImageLayerProps } from "../image_layer";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakeImage, fakeWebAppConfig } from "../../../../__test_support__/fake_state/resources";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
import {
|
||||
fakeImage, fakeWebAppConfig
|
||||
} from "../../../../__test_support__/fake_state/resources";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
const mockConfig = fakeWebAppConfig();
|
||||
jest.mock("../../../../resources/selectors", () => {
|
||||
|
|
|
@ -3,14 +3,6 @@ jest.mock("../../../../history", () => ({
|
|||
getPathArray: jest.fn(() => { return mockPath.split("/"); })
|
||||
}));
|
||||
|
||||
jest.mock("../../../../session", () => {
|
||||
return {
|
||||
Session: {
|
||||
deprecatedGetBool: () => { return false; }
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import * as React from "react";
|
||||
import { PlantLayer } from "../plant_layer";
|
||||
import { shallow } from "enzyme";
|
||||
|
@ -31,7 +23,8 @@ describe("<PlantLayer/>", () => {
|
|||
crops: [],
|
||||
dispatch: jest.fn(),
|
||||
zoomLvl: 1,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined }
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
animate: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ describe("<SpreadLayer/>", () => {
|
|||
zoomLvl: 1.8,
|
||||
activeDragXY: { x: undefined, y: undefined, z: undefined },
|
||||
activeDragSpread: undefined,
|
||||
editing: false
|
||||
editing: false,
|
||||
animate: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { FarmBotLayerProps } from "../interfaces";
|
|||
export function FarmBotLayer(props: FarmBotLayerProps) {
|
||||
const {
|
||||
visible, stopAtHome, botSize, plantAreaOffset, mapTransformProps,
|
||||
peripherals, eStopStatus, botLocationData
|
||||
peripherals, eStopStatus, botLocationData, getConfigValue
|
||||
} = props;
|
||||
return visible ? <g id="farmbot-layer">
|
||||
<VirtualFarmBot
|
||||
|
@ -14,7 +14,8 @@ export function FarmBotLayer(props: FarmBotLayerProps) {
|
|||
botLocationData={botLocationData}
|
||||
plantAreaOffset={plantAreaOffset}
|
||||
peripherals={peripherals}
|
||||
eStopStatus={eStopStatus} />
|
||||
eStopStatus={eStopStatus}
|
||||
getConfigValue={getConfigValue} />
|
||||
<BotExtents
|
||||
mapTransformProps={mapTransformProps}
|
||||
stopAtHome={stopAtHome}
|
||||
|
|
|
@ -6,8 +6,6 @@ import { MapTransformProps } from "../interfaces";
|
|||
import { SpreadCircle } from "./spread_layer";
|
||||
import { Circle } from "../circle";
|
||||
import * as _ from "lodash";
|
||||
import { Session } from "../../../session";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
|
||||
/**
|
||||
* For showing the map plant hovered in the plant panel.
|
||||
|
@ -23,6 +21,7 @@ export interface HoveredPlantLayerProps {
|
|||
isEditing: boolean;
|
||||
mapTransformProps: MapTransformProps;
|
||||
dragging: boolean;
|
||||
animate: boolean;
|
||||
}
|
||||
|
||||
export class HoveredPlantLayer extends
|
||||
|
@ -40,7 +39,8 @@ export class HoveredPlantLayer extends
|
|||
|
||||
render() {
|
||||
const {
|
||||
currentPlant, mapTransformProps, dragging, isEditing, visible, designer
|
||||
currentPlant, mapTransformProps, dragging, isEditing, visible, designer,
|
||||
animate
|
||||
} = this.props;
|
||||
const { icon } = designer.hoveredPlant;
|
||||
const hovered = !!icon;
|
||||
|
@ -48,7 +48,6 @@ export class HoveredPlantLayer extends
|
|||
const { qx, qy } = transformXY(round(x), round(y), mapTransformProps);
|
||||
const scaledRadius = currentPlant ? radius : radius * 1.2;
|
||||
const alpha = dragging ? 0.4 : 1.0;
|
||||
const animate = !Session.deprecatedGetBool(BooleanSetting.disable_animations);
|
||||
|
||||
return <g id="hovered-plant-layer">
|
||||
{visible && hovered &&
|
||||
|
@ -59,7 +58,8 @@ export class HoveredPlantLayer extends
|
|||
plant={currentPlant}
|
||||
key={currentPlant.uuid}
|
||||
mapTransformProps={mapTransformProps}
|
||||
selected={false} />
|
||||
selected={false}
|
||||
animate={animate} />
|
||||
|
||||
<Circle
|
||||
className={animate ? "plant-indicator" : ""}
|
||||
|
|
|
@ -21,6 +21,7 @@ export function PlantLayer(props: PlantLayerProps) {
|
|||
selectedForDel,
|
||||
zoomLvl,
|
||||
activeDragXY,
|
||||
animate,
|
||||
} = props;
|
||||
|
||||
crops
|
||||
|
@ -57,7 +58,8 @@ export function PlantLayer(props: PlantLayerProps) {
|
|||
dragging={p.selected && dragging && editing}
|
||||
dispatch={dispatch}
|
||||
zoomLvl={zoomLvl}
|
||||
activeDragXY={activeDragXY} />
|
||||
activeDragXY={activeDragXY}
|
||||
animate={animate} />
|
||||
</Link>;
|
||||
})}
|
||||
</g>;
|
||||
|
|
|
@ -6,8 +6,6 @@ import { cachedCrop } from "../../../open_farm/icons";
|
|||
import { MapTransformProps } from "../interfaces";
|
||||
import { SpreadOverlapHelper } from "../spread_overlap_helper";
|
||||
import { BotPosition } from "../../../devices/interfaces";
|
||||
import { Session } from "../../../session";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
|
||||
export interface SpreadLayerProps {
|
||||
visible: boolean;
|
||||
|
@ -19,11 +17,14 @@ export interface SpreadLayerProps {
|
|||
activeDragXY: BotPosition | undefined;
|
||||
activeDragSpread: number | undefined;
|
||||
editing: boolean;
|
||||
animate: boolean;
|
||||
}
|
||||
|
||||
export function SpreadLayer(props: SpreadLayerProps) {
|
||||
const { plants, visible, mapTransformProps, currentPlant,
|
||||
dragging, zoomLvl, activeDragXY, activeDragSpread, editing } = props;
|
||||
const {
|
||||
plants, visible, mapTransformProps, currentPlant,
|
||||
dragging, zoomLvl, activeDragXY, activeDragSpread, editing, animate
|
||||
} = props;
|
||||
return <g id="spread-layer">
|
||||
<defs>
|
||||
<radialGradient id="SpreadGradient">
|
||||
|
@ -44,7 +45,8 @@ export function SpreadLayer(props: SpreadLayerProps) {
|
|||
plant={p}
|
||||
key={"spread-" + p.uuid}
|
||||
mapTransformProps={mapTransformProps}
|
||||
selected={selected} />}
|
||||
selected={selected}
|
||||
animate={animate} />}
|
||||
<SpreadOverlapHelper
|
||||
key={"overlap-" + p.uuid}
|
||||
dragging={selected && dragging && editing}
|
||||
|
@ -62,6 +64,7 @@ interface SpreadCircleProps {
|
|||
plant: TaggedPlantPointer;
|
||||
mapTransformProps: MapTransformProps;
|
||||
selected: boolean;
|
||||
animate: boolean;
|
||||
}
|
||||
|
||||
interface SpreadCircleState {
|
||||
|
@ -80,9 +83,8 @@ export class SpreadCircle extends
|
|||
|
||||
render() {
|
||||
const { radius, x, y, id } = this.props.plant.body;
|
||||
const { selected, mapTransformProps } = this.props;
|
||||
const { selected, mapTransformProps, animate } = this.props;
|
||||
const { qx, qy } = transformXY(round(x), round(y), mapTransformProps);
|
||||
const animate = !Session.deprecatedGetBool(BooleanSetting.disable_animations);
|
||||
|
||||
return <g id={"spread-" + id}>
|
||||
{!selected &&
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { SpreadOverlapHelperProps } from "./interfaces";
|
||||
import { round, transformXY } from "./util";
|
||||
import { isUndefined } from "util";
|
||||
import { BotPosition } from "../../devices/interfaces";
|
||||
import { cachedCrop } from "../../open_farm/icons";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
enum OverlapColor {
|
||||
NONE = "none",
|
||||
|
|
|
@ -1,22 +1,9 @@
|
|||
const mockStorj: Dictionary<boolean> = {};
|
||||
|
||||
jest.mock("../../../../session", () => {
|
||||
return {
|
||||
Session: {
|
||||
deprecatedGetBool: (k: string) => {
|
||||
mockStorj[k] = !!mockStorj[k];
|
||||
return mockStorj[k];
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import { Dictionary } from "farmbot";
|
||||
import * as React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { BotPeripheralsProps, BotPeripherals } from "../bot_peripherals";
|
||||
import { BooleanSetting } from "../../../../session_keys";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<BotPeripherals/>", () => {
|
||||
function fakeProps(): BotPeripheralsProps {
|
||||
|
@ -24,7 +11,8 @@ describe("<BotPeripherals/>", () => {
|
|||
peripherals: [{ label: "", value: false }],
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 }
|
||||
plantAreaOffset: { x: 100, y: 100 },
|
||||
getConfigValue: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -40,11 +28,11 @@ describe("<BotPeripherals/>", () => {
|
|||
|
||||
function animationToggle(
|
||||
props: BotPeripheralsProps, enabled: number, disabled: number) {
|
||||
mockStorj[BooleanSetting.disable_animations] = false;
|
||||
props.getConfigValue = () => false;
|
||||
const wrapperEnabled = shallow(<BotPeripherals {...props} />);
|
||||
expect(wrapperEnabled.find("use").length).toEqual(enabled);
|
||||
|
||||
mockStorj[BooleanSetting.disable_animations] = true;
|
||||
props.getConfigValue = () => true;
|
||||
const wrapperDisabled = shallow(<BotPeripherals {...props} />);
|
||||
expect(wrapperDisabled.find("use").length).toEqual(disabled);
|
||||
}
|
||||
|
|
|
@ -1,22 +1,10 @@
|
|||
const mockStorj: Dictionary<boolean> = {};
|
||||
|
||||
jest.mock("../../../../session", () => {
|
||||
return {
|
||||
Session: {
|
||||
deprecatedGetBool: (k: string) => {
|
||||
mockStorj[k] = !!mockStorj[k];
|
||||
return mockStorj[k];
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import * as React from "react";
|
||||
import { VirtualFarmBot } from "../index";
|
||||
import { shallow } from "enzyme";
|
||||
import { VirtualFarmBotProps } from "../../interfaces";
|
||||
import { Dictionary } from "farmbot";
|
||||
import { fakeMapTransformProps } from "../../../../__test_support__/map_transform_props";
|
||||
import {
|
||||
fakeMapTransformProps
|
||||
} from "../../../../__test_support__/map_transform_props";
|
||||
|
||||
describe("<VirtualFarmBot/>", () => {
|
||||
function fakeProps(): VirtualFarmBotProps {
|
||||
|
@ -29,25 +17,26 @@ describe("<VirtualFarmBot/>", () => {
|
|||
mapTransformProps: fakeMapTransformProps(),
|
||||
plantAreaOffset: { x: 100, y: 100 },
|
||||
peripherals: [],
|
||||
eStopStatus: false
|
||||
eStopStatus: false,
|
||||
getConfigValue: () => true,
|
||||
};
|
||||
}
|
||||
|
||||
it("shows bot position", () => {
|
||||
const wrapper = shallow(<VirtualFarmBot {...fakeProps()} />);
|
||||
const p = fakeProps();
|
||||
p.getConfigValue = () => false;
|
||||
const wrapper = shallow(<VirtualFarmBot {...p} />);
|
||||
const figures = wrapper.find("BotFigure");
|
||||
expect(figures.length).toEqual(1);
|
||||
expect(figures.last().props().name).toEqual("motor-position");
|
||||
});
|
||||
|
||||
it("shows trail", () => {
|
||||
mockStorj["display_trail"] = true;
|
||||
const wrapper = shallow(<VirtualFarmBot {...fakeProps()} />);
|
||||
expect(wrapper.find("BotTrail").length).toEqual(1);
|
||||
});
|
||||
|
||||
it("shows encoder position", () => {
|
||||
mockStorj["encoder_figure"] = true;
|
||||
const wrapper = shallow(<VirtualFarmBot {...fakeProps()} />);
|
||||
const figures = wrapper.find("BotFigure");
|
||||
expect(figures.length).toEqual(2);
|
||||
|
|
|
@ -3,15 +3,16 @@ import { AxisNumberProperty, MapTransformProps } from "../interfaces";
|
|||
import { getMapSize, transformXY } from "../util";
|
||||
import { BotPosition } from "../../../devices/interfaces";
|
||||
import * as _ from "lodash";
|
||||
import { Session } from "../../../session";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
import { trim } from "../../../util";
|
||||
import { GetWebAppConfigValue } from "../../../config_storage/actions";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
|
||||
export interface BotPeripheralsProps {
|
||||
position: BotPosition;
|
||||
peripherals: { label: string, value: boolean }[];
|
||||
mapTransformProps: MapTransformProps;
|
||||
plantAreaOffset: AxisNumberProperty;
|
||||
getConfigValue: GetWebAppConfigValue;
|
||||
}
|
||||
|
||||
function lightsFigure(
|
||||
|
@ -46,10 +47,9 @@ function lightsFigure(
|
|||
}
|
||||
|
||||
function waterFigure(
|
||||
props: { i: number, cx: number, cy: number }) {
|
||||
const { i, cx, cy } = props;
|
||||
props: { i: number, cx: number, cy: number, animate: boolean }) {
|
||||
const { i, cx, cy, animate } = props;
|
||||
const color = "rgb(11, 83, 148)";
|
||||
const animate = !Session.deprecatedGetBool(BooleanSetting.disable_animations);
|
||||
const copies = animate ? 3 : 1;
|
||||
const animateClass = animate ? "animate" : "";
|
||||
|
||||
|
@ -87,10 +87,9 @@ function waterFigure(
|
|||
}
|
||||
|
||||
function vacuumFigure(
|
||||
props: { i: number, cx: number, cy: number }) {
|
||||
const { i, cx, cy } = props;
|
||||
props: { i: number, cx: number, cy: number, animate: boolean }) {
|
||||
const { i, cx, cy, animate } = props;
|
||||
const color = "black";
|
||||
const animate = !Session.deprecatedGetBool(BooleanSetting.disable_animations);
|
||||
const copies = animate ? 3 : 1;
|
||||
const animateClass = animate ? "animate" : "";
|
||||
|
||||
|
@ -121,11 +120,14 @@ function vacuumFigure(
|
|||
}
|
||||
|
||||
export function BotPeripherals(props: BotPeripheralsProps) {
|
||||
const { peripherals, position, plantAreaOffset, mapTransformProps } = props;
|
||||
const {
|
||||
peripherals, position, plantAreaOffset, mapTransformProps, getConfigValue
|
||||
} = props;
|
||||
const { xySwap } = mapTransformProps;
|
||||
const mapSize = getMapSize(mapTransformProps, plantAreaOffset);
|
||||
const positionQ = transformXY(
|
||||
(position.x || 0), (position.y || 0), mapTransformProps);
|
||||
const animate = !getConfigValue(BooleanSetting.disable_animations);
|
||||
|
||||
return <g className={"virtual-peripherals"}>
|
||||
{peripherals.map((x, i) => {
|
||||
|
@ -141,13 +143,15 @@ export function BotPeripherals(props: BotPeripheralsProps) {
|
|||
return waterFigure({
|
||||
i,
|
||||
cx: positionQ.qx,
|
||||
cy: positionQ.qy
|
||||
cy: positionQ.qy,
|
||||
animate
|
||||
});
|
||||
} else if (x.label.toLowerCase().includes("vacuum") && x.value) {
|
||||
return vacuumFigure({
|
||||
i,
|
||||
cx: positionQ.qx,
|
||||
cy: positionQ.qy
|
||||
cy: positionQ.qy,
|
||||
animate
|
||||
});
|
||||
}
|
||||
})}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { VirtualFarmBotProps } from "../interfaces";
|
||||
import { Session } from "../../../session";
|
||||
import { BooleanSetting } from "../../../session_keys";
|
||||
import { BotFigure } from "./bot_figure";
|
||||
import { BotTrail } from "./bot_trail";
|
||||
|
@ -9,10 +8,10 @@ import { NegativePositionLabel } from "./negative_position_labels";
|
|||
|
||||
export function VirtualFarmBot(props: VirtualFarmBotProps) {
|
||||
const {
|
||||
mapTransformProps, plantAreaOffset, peripherals, eStopStatus
|
||||
mapTransformProps, plantAreaOffset, peripherals, eStopStatus, getConfigValue
|
||||
} = props;
|
||||
const displayTrail = Session.deprecatedGetBool(BooleanSetting.display_trail);
|
||||
const encoderFigure = Session.deprecatedGetBool(BooleanSetting.encoder_figure);
|
||||
const displayTrail = !!getConfigValue(BooleanSetting.display_trail);
|
||||
const encoderFigure = !!getConfigValue(BooleanSetting.encoder_figure);
|
||||
|
||||
return <g id="virtual-farmbot">
|
||||
<NegativePositionLabel
|
||||
|
@ -23,7 +22,8 @@ export function VirtualFarmBot(props: VirtualFarmBotProps) {
|
|||
position={props.botLocationData.position}
|
||||
mapTransformProps={mapTransformProps}
|
||||
plantAreaOffset={plantAreaOffset}
|
||||
peripherals={peripherals} />
|
||||
peripherals={peripherals}
|
||||
getConfigValue={getConfigValue} />
|
||||
<BotFigure name={"motor-position"}
|
||||
position={props.botLocationData.position}
|
||||
mapTransformProps={mapTransformProps}
|
||||
|
|
|
@ -3,9 +3,8 @@ import { Everything } from "../../interfaces";
|
|||
import { EditPlantInfoProps } from "../interfaces";
|
||||
import { maybeFindPlantById } from "../../resources/selectors";
|
||||
import { history } from "../../history";
|
||||
import { TaggedPlantPointer } from "farmbot";
|
||||
import { TaggedPlantPointer, PlantStage } from "farmbot";
|
||||
import * as _ from "lodash";
|
||||
import { PlantStage } from "farmbot";
|
||||
|
||||
export function mapStateToProps(props: Everything): EditPlantInfoProps {
|
||||
const findPlant = (id: string | undefined) => {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import * as _ from "lodash";
|
||||
import { CropLiveSearchResult } from "./interfaces";
|
||||
|
||||
export function findBySlug(crops: CropLiveSearchResult[], slug?: string): CropLiveSearchResult {
|
||||
export function findBySlug(
|
||||
crops: CropLiveSearchResult[], slug?: string): CropLiveSearchResult {
|
||||
const crop = _(crops).find((result) => result.crop.slug === slug);
|
||||
return crop || {
|
||||
crop: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { WeedDetectorConfig } from "../config"
|
||||
import { WeedDetectorConfig } from "../config";
|
||||
|
||||
describe("<WeedDetectorConfig />", () => {
|
||||
it("renders", () => {
|
||||
|
|
|
@ -92,7 +92,7 @@ export const DEFAULT_FORMATTER: Translation = {
|
|||
return val;
|
||||
}
|
||||
},
|
||||
parse: (_, val) => {
|
||||
parse: (__, val) => {
|
||||
try {
|
||||
const b = box(JSON.parse(val));
|
||||
switch (b.kind) {
|
||||
|
|
|
@ -41,6 +41,7 @@ export interface SafeError {
|
|||
}
|
||||
|
||||
/** Prevents hard-to-find NPEs and type errors inside of interceptors. */
|
||||
// tslint:disable-next-line:no-any
|
||||
export function isSafeError(x: SafeError | any): x is SafeError {
|
||||
return !!(
|
||||
(box(x).kind === "object") &&
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import * as React from "react";
|
||||
import { t } from "i18next";
|
||||
import { Session } from "./session";
|
||||
import { BooleanSetting } from "./session_keys";
|
||||
|
||||
export function LoadingPlant() {
|
||||
const animations = !Session.deprecatedGetBool(BooleanSetting.disable_animations);
|
||||
/* tslint:disable:max-line-length */
|
||||
|
||||
export function LoadingPlant({ animate }: { animate: boolean }) {
|
||||
return <div className="loading-plant-div-container">
|
||||
<svg width="300px" height="500px">
|
||||
{animations &&
|
||||
{animate &&
|
||||
<g>
|
||||
<circle
|
||||
className="loading-plant-circle"
|
||||
|
@ -68,8 +67,8 @@ export function LoadingPlant() {
|
|||
</g>
|
||||
</g>}
|
||||
<text
|
||||
className={"loading-plant-text" + (animations ? " animate" : "")}
|
||||
y={animations ? 435 : 150}
|
||||
className={"loading-plant-text" + (animate ? " animate" : "")}
|
||||
y={animate ? 435 : 150}
|
||||
x={150}
|
||||
fontSize={35}
|
||||
textAnchor="middle"
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { TaggedLog } from "farmbot";
|
||||
import { TaggedLog, ConfigurationName, ALLOWED_MESSAGE_TYPES } from "farmbot";
|
||||
import { BotState, SourceFbosConfig } from "../devices/interfaces";
|
||||
import { ConfigurationName, ALLOWED_MESSAGE_TYPES } from "farmbot";
|
||||
|
||||
export interface LogsProps {
|
||||
logs: TaggedLog[];
|
||||
|
|
|
@ -6,7 +6,9 @@ import { Actions } from "../../constants";
|
|||
describe("refilterLogsMiddleware.fn()", () => {
|
||||
it("dispatches when required", () => {
|
||||
const dispatch = jest.fn();
|
||||
// tslint:disable-next-line:no-any
|
||||
const fn = refilterLogsMiddleware.fn({} as any)(dispatch);
|
||||
// tslint:disable-next-line:no-any
|
||||
fn({ type: "any", payload: {} } as any);
|
||||
expect(throttledLogRefresh).not.toHaveBeenCalled();
|
||||
fn({ type: Actions.UPDATE_RESOURCE_OK, payload: { kind: "WebAppConfig" } });
|
||||
|
|
|
@ -14,6 +14,7 @@ describe("revertToEnglishMiddleware", () => {
|
|||
payload: { name: "WebAppConfig", data: { disable_i18n: true } }
|
||||
};
|
||||
expect(revertToEnglish).not.toHaveBeenCalled();
|
||||
// tslint:disable-next-line:no-any
|
||||
revertToEnglishMiddleware.fn({} as any)(dispatch)(action);
|
||||
expect(revertToEnglish).toHaveBeenCalled();
|
||||
expect(revertToEnglishMiddleware.env).toBe("*");
|
||||
|
|
|
@ -17,12 +17,14 @@ describe("unsavedCheck", () => {
|
|||
kind: "WebAppConfig",
|
||||
uuid: "NOT SET HERE!",
|
||||
specialStatus,
|
||||
// tslint:disable-next-line:no-any
|
||||
body: (body as any)
|
||||
};
|
||||
const output = fakeState();
|
||||
output.resources = buildResourceIndex([config]);
|
||||
// `buildResourceIndex` clears specialStatus. Set it again:
|
||||
const uuid = output.resources.index.all[0];
|
||||
// tslint:disable-next-line:no-any
|
||||
(output.resources.index.references[uuid] || {} as any)
|
||||
.specialStatus = specialStatus;
|
||||
return output;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { versionChangeMiddleware } from "../version_tracker_middleware";
|
||||
import { buildResourceIndex, fakeDevice } from "../../__test_support__/resource_index_builder";
|
||||
import {
|
||||
buildResourceIndex, fakeDevice
|
||||
} from "../../__test_support__/resource_index_builder";
|
||||
import { MiddlewareAPI } from "redux";
|
||||
|
||||
describe("version tracker middleware", () => {
|
||||
|
@ -9,11 +11,14 @@ describe("version tracker middleware", () => {
|
|||
window.Rollbar = { configure: jest.fn() };
|
||||
const state = fakeState();
|
||||
state.resources = buildResourceIndex([fakeDevice()]);
|
||||
// tslint:disable-next-line:no-any
|
||||
type Mw = MiddlewareAPI<any>;
|
||||
const fakeStore: Partial<Mw> = {
|
||||
getState: () => state
|
||||
};
|
||||
versionChangeMiddleware.fn(fakeStore as Mw)(jest.fn())({ type: "ANY", payload: {} });
|
||||
versionChangeMiddleware.fn(fakeStore as Mw)(jest.fn())({
|
||||
type: "ANY", payload: {}
|
||||
});
|
||||
expect(window.Rollbar.configure)
|
||||
.toHaveBeenCalledWith({ "payload": { "fbos": "0.0.0" } });
|
||||
window.Rollbar = before;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Dictionary } from "farmbot";
|
|||
|
||||
/** A function that responds to a particular action from within a
|
||||
* generated reducer. */
|
||||
// tslint:disable-next-line:no-any
|
||||
interface ActionHandler<State, Payl = any> {
|
||||
(state: State, action: ReduxAction<Payl>): State;
|
||||
}
|
||||
|
@ -27,6 +28,7 @@ export function generateReducer<State, U = any>(initialState: State,
|
|||
const NOOP: ActionHandler<State> = (s) => s;
|
||||
|
||||
const reducer: GeneratedReducer =
|
||||
// tslint:disable-next-line:no-any
|
||||
((state = initialState, action: ReduxAction<any>): State => {
|
||||
|
||||
// Find the handler in the dictionary, or use the NOOP.
|
||||
|
|
|
@ -33,6 +33,7 @@ export function getMiddleware(env: EnvName) {
|
|||
const middlewareFns = mwConfig
|
||||
.filter(function (mwc) { return (mwc.env === env) || (mwc.env === "*"); })
|
||||
.map((mwc) => mwc.fn);
|
||||
// tslint:disable-next-line:no-any
|
||||
const wow = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
|
||||
const dtCompose = wow && wow({
|
||||
actionsBlacklist: [
|
||||
|
|
|
@ -10,6 +10,7 @@ const WEB_APP_CONFIG: ResourceName = "WebAppConfig";
|
|||
* Middleware function that listens for changes on the `WebAppConfig` resource.
|
||||
* If the resource does change, it will trigger a throttled refresh of all log
|
||||
* resources, downloading the filtered log list as required from the API. */
|
||||
// tslint:disable-next-line:no-any
|
||||
export const fn: Middleware = () => (dispatch) => (action: any) => {
|
||||
const needsRefresh = action
|
||||
&& action.payload
|
||||
|
|
|
@ -18,6 +18,7 @@ const WEB_APP_CONFIG: ResourceName = "WebAppConfig";
|
|||
* possible and then revert to english as soon as we have a chance to read the
|
||||
* value of `web_app_config.disable_i18n`.
|
||||
*/
|
||||
// tslint:disable-next-line:no-any
|
||||
const fn: Middleware = () => (dispatch) => (action: any) => {
|
||||
const isResourceReady = action
|
||||
&& action.type === Actions.RESOURCE_READY
|
||||
|
|
|
@ -5,7 +5,9 @@ import { createRefreshTrigger } from "./create_refresh_trigger";
|
|||
|
||||
const maybeRefresh = createRefreshTrigger();
|
||||
const stateFetchMiddleware: Middleware =
|
||||
// tslint:disable-next-line:no-any
|
||||
(store) => (next) => (action: any) => {
|
||||
// tslint:disable-next-line:no-any
|
||||
const s: Everything = store.getState() as any;
|
||||
maybeRefresh(s.bot.connectivity["bot.mqtt"]);
|
||||
next(action);
|
||||
|
|
|
@ -13,6 +13,7 @@ function dev(): Store {
|
|||
}
|
||||
|
||||
function prod(): Store {
|
||||
// tslint:disable-next-line:no-any
|
||||
return createStore(rootReducer, ({} as any), getMiddleware("production"));
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ function getVersionFromState(state: Everything) {
|
|||
const fn: MW =
|
||||
(store: Store<Everything>) =>
|
||||
(dispatch: Dispatch<Action<object>>) =>
|
||||
// tslint:disable-next-line:no-any
|
||||
(action: any) => {
|
||||
const fbos = getVersionFromState(store.getState());
|
||||
window.Rollbar && window.Rollbar.configure({ payload: { fbos } });
|
||||
|
|
|
@ -87,7 +87,7 @@ export function commitBulkEditor(): Thunk {
|
|||
return error(t("No day(s) selected."));
|
||||
}
|
||||
} else {
|
||||
return error(t("Select a sequence from the dropdown first."),t("Error"));
|
||||
return error(t("Select a sequence from the dropdown first."), t("Error"));
|
||||
}
|
||||
} else {
|
||||
return error(t("Select a regimen first or create one."));
|
||||
|
|
|
@ -15,6 +15,7 @@ describe("write()", () => {
|
|||
const callback = write(input);
|
||||
expect(callback).toBeInstanceOf(Function);
|
||||
const value = "FOO";
|
||||
// tslint:disable-next-line:no-any
|
||||
callback({ currentTarget: { value } } as any);
|
||||
expect(input.dispatch).toHaveBeenCalled();
|
||||
expect(editRegimen).toHaveBeenCalled();
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import * as _ from "lodash";
|
||||
import { Dictionary } from "farmbot";
|
||||
import { Dictionary, TaggedResource } from "farmbot";
|
||||
import { Week, DAYS } from "./bulk_scheduler/interfaces";
|
||||
import { generateReducer } from "../redux/generate_reducer";
|
||||
import { TaggedResource } from "farmbot";
|
||||
import { Actions } from "../constants";
|
||||
|
||||
export interface RegimenState {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { resourceReducer, findByUuid } from "../reducer";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { overwrite, refreshStart, refreshOK, refreshNO } from "../../api/crud";
|
||||
import { SpecialStatus, TaggedSequence, TaggedDevice } from "farmbot";
|
||||
import { SpecialStatus, TaggedSequence, TaggedDevice, ResourceName } from "farmbot";
|
||||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
import { GeneralizedError } from "../actions";
|
||||
import { Actions } from "../../constants";
|
||||
import { fakeResource } from "../../__test_support__/fake_resource";
|
||||
|
||||
describe("resource reducer", () => {
|
||||
it("marks resources as DIRTY when reducing OVERWRITE_RESOURCE", () => {
|
||||
|
@ -25,7 +27,7 @@ describe("resource reducer", () => {
|
|||
it("marks resources as SAVING when reducing REFRESH_RESOURCE_START", () => {
|
||||
const state = fakeState().resources;
|
||||
const uuid = state.index.byKind.Device[0];
|
||||
const device = state.index.references[uuid] as TaggedSequence;
|
||||
const device = state.index.references[uuid] as TaggedDevice;
|
||||
expect(device).toBeTruthy();
|
||||
|
||||
expect(device.kind).toBe("Device");
|
||||
|
@ -48,6 +50,43 @@ describe("resource reducer", () => {
|
|||
const dev4 = afterNo.index.references[uuid] as TaggedDevice;
|
||||
expect(dev4.specialStatus).toBe(SpecialStatus.SAVED);
|
||||
});
|
||||
|
||||
const TEST_RESOURCE_NAMES = [
|
||||
"Crop", "Device", "DiagnosticDump", "FarmEvent", "FarmwareInstallation",
|
||||
"FbosConfig", "FirmwareConfig", "Log", "Peripheral", "PinBinding",
|
||||
"PlantTemplate", "Point", "Regimen", "SavedGarden", "Sensor", "Sequence",
|
||||
];
|
||||
|
||||
it("covers save resource branches", () => {
|
||||
const testResource = (kind: ResourceName) => {
|
||||
|
||||
const state = fakeState().resources;
|
||||
const resource = fakeResource(kind, {});
|
||||
const action = {
|
||||
type: Actions.SAVE_RESOURCE_OK,
|
||||
payload: resource
|
||||
};
|
||||
const newState = resourceReducer(state, action);
|
||||
expect((newState.index.references[resource.uuid] || {})).toEqual(resource);
|
||||
};
|
||||
TEST_RESOURCE_NAMES.map((kind: ResourceName) => testResource(kind));
|
||||
});
|
||||
|
||||
it("covers destroy resource branches", () => {
|
||||
const testResourceDestroy = (kind: ResourceName) => {
|
||||
|
||||
const state = fakeState().resources;
|
||||
const resource = fakeResource(kind, {});
|
||||
const action = {
|
||||
type: Actions.DESTROY_RESOURCE_OK,
|
||||
payload: resource
|
||||
};
|
||||
const newState = resourceReducer(state, action);
|
||||
expect(newState.index.references[resource.uuid]).toEqual(undefined);
|
||||
};
|
||||
TEST_RESOURCE_NAMES.concat(["Image", "SensorReading"])
|
||||
.map((kind: ResourceName) => testResourceDestroy(kind));
|
||||
});
|
||||
});
|
||||
|
||||
describe("findByUuid", () => {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { buildResourceIndex, fakeDevice } from "../../__test_support__/resource_index_builder";
|
||||
import {
|
||||
buildResourceIndex, fakeDevice
|
||||
} from "../../__test_support__/resource_index_builder";
|
||||
import * as Selector from "../selectors";
|
||||
import {
|
||||
resourceReducer,
|
||||
|
|
|
@ -297,8 +297,10 @@ function addToIndex<T>(index: ResourceIndex,
|
|||
kind: ResourceName,
|
||||
body: T,
|
||||
uuid: string) {
|
||||
const tr: TaggedResource =
|
||||
{ kind, body, uuid, specialStatus: SpecialStatus.SAVED } as any;
|
||||
const tr: TaggedResource = {
|
||||
kind, body, uuid, specialStatus: SpecialStatus.SAVED
|
||||
// tslint:disable-next-line:no-any
|
||||
} as any;
|
||||
sanityCheck(tr);
|
||||
index.all.push(tr.uuid);
|
||||
index.byKind[tr.kind].push(tr.uuid);
|
||||
|
|
|
@ -32,7 +32,8 @@ import { error } from "farmbot-toastr";
|
|||
import { joinKindAndId } from "./reducer";
|
||||
import { assertUuid } from "./util";
|
||||
|
||||
const isSaved = <T extends TaggedResource>(t: T) => t.specialStatus === SpecialStatus.SAVED;
|
||||
const isSaved = <T extends TaggedResource>(t: T) =>
|
||||
t.specialStatus === SpecialStatus.SAVED;
|
||||
|
||||
/** Generalized way to stamp out "finder" functions.
|
||||
* Pass in a `ResourceName` and it will add all the relevant checks.
|
||||
|
@ -51,7 +52,8 @@ const uuidFinder = <T extends TaggedResource>(r: T["kind"]) =>
|
|||
}
|
||||
};
|
||||
|
||||
export function findAll<T extends TaggedResource>(index: ResourceIndex, kind: T["kind"]): T[] {
|
||||
export function findAll<T extends TaggedResource>(
|
||||
index: ResourceIndex, kind: T["kind"]): T[] {
|
||||
const results: T[] = [];
|
||||
|
||||
index.byKind[kind].map(function (uuid) {
|
||||
|
@ -74,7 +76,8 @@ export const selectAllSavedGardens = (i: ResourceIndex) =>
|
|||
findAll<TaggedSavedGarden>(i, "SavedGarden");
|
||||
export const selectAllPlantTemplates = (i: ResourceIndex) =>
|
||||
findAll<TaggedPlantTemplate>(i, "PlantTemplate");
|
||||
export const selectAllFarmEvents = (i: ResourceIndex) => findAll<TaggedFarmEvent>(i, "FarmEvent");
|
||||
export const selectAllFarmEvents = (i: ResourceIndex) =>
|
||||
findAll<TaggedFarmEvent>(i, "FarmEvent");
|
||||
export const selectAllImages = (i: ResourceIndex) => findAll<TaggedImage>(i, "Image");
|
||||
export const selectAllLogs = (i: ResourceIndex) => findAll<TaggedLog>(i, "Log");
|
||||
export const selectAllPeripherals =
|
||||
|
@ -91,11 +94,13 @@ export const selectAllToolSlots = (i: ResourceIndex): TaggedToolSlotPointer[] =>
|
|||
|
||||
export const selectAllDiagnosticDumps =
|
||||
(i: ResourceIndex) => findAll<TaggedDiagnosticDump>(i, "DiagnosticDump");
|
||||
export const selectAllRegimens = (i: ResourceIndex) => findAll<TaggedRegimen>(i, "Regimen");
|
||||
export const selectAllRegimens = (i: ResourceIndex) =>
|
||||
findAll<TaggedRegimen>(i, "Regimen");
|
||||
export const selectAllSensors = (i: ResourceIndex) => findAll<TaggedSensor>(i, "Sensor");
|
||||
export const selectAllPinBindings =
|
||||
(i: ResourceIndex) => findAll<TaggedPinBinding>(i, "PinBinding");
|
||||
export const selectAllSequences = (i: ResourceIndex) => findAll<TaggedSequence>(i, "Sequence");
|
||||
export const selectAllSequences = (i: ResourceIndex) =>
|
||||
findAll<TaggedSequence>(i, "Sequence");
|
||||
export const selectAllSensorReadings = (i: ResourceIndex) =>
|
||||
findAll<TaggedSensorReading>(i, "SensorReading");
|
||||
export const selectAllTools = (i: ResourceIndex) => findAll<TaggedTool>(i, "Tool");
|
||||
|
@ -112,14 +117,14 @@ export const getWebAppConfig = (i: ResourceIndex): TaggedWebAppConfig | undefine
|
|||
export const getFirmwareConfig = (i: ResourceIndex): TaggedFirmwareConfig | undefined =>
|
||||
findAll<TaggedFirmwareConfig>(i, "FirmwareConfig")[0];
|
||||
|
||||
export const findByKindAndId =
|
||||
<T extends TaggedResource>(i: ResourceIndex, kind: T["kind"], id: number | undefined): T => {
|
||||
const kni = joinKindAndId(kind, id);
|
||||
const uuid = i.byKindAndId[kni] || bail("Not found: " + kni);
|
||||
const resource = i.references[uuid] || bail("Not found uuid: " + uuid);
|
||||
if (resource.kind === kind) {
|
||||
return resource as T; // Why `as T`?
|
||||
} else {
|
||||
return bail("Impossible! " + uuid);
|
||||
}
|
||||
};
|
||||
export const findByKindAndId = <T extends TaggedResource>(
|
||||
i: ResourceIndex, kind: T["kind"], id: number | undefined): T => {
|
||||
const kni = joinKindAndId(kind, id);
|
||||
const uuid = i.byKindAndId[kni] || bail("Not found: " + kni);
|
||||
const resource = i.references[uuid] || bail("Not found uuid: " + uuid);
|
||||
if (resource.kind === kind) {
|
||||
return resource as T; // Why `as T`?
|
||||
} else {
|
||||
return bail("Impossible! " + uuid);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ function page<T>(path: string,
|
|||
return {
|
||||
path,
|
||||
getComponent(_, cb): void {
|
||||
// tslint:disable-next-line:no-any
|
||||
const ok = (mod: T) => cb(undefined, mod[key] as any);
|
||||
const no = (e: object) => cb(undefined, crashPage(e));
|
||||
/** Whatever you do, make sure this function stays void or you will get a
|
||||
|
|
|
@ -16,6 +16,7 @@ interface RootComponentProps { store: Store; }
|
|||
|
||||
export const attachAppToDom: Callback = () => {
|
||||
attachToRoot(RootComponent, { store: _store });
|
||||
// tslint:disable-next-line:no-any
|
||||
_store.dispatch(ready() as any);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import * as React from "react";
|
|||
import { shallow } from "enzyme";
|
||||
import { InputUnknown } from "../input_unknown";
|
||||
|
||||
/* tslint:disable:no-any */
|
||||
|
||||
describe("<InputUnknown/>", () => {
|
||||
it("is merely a fallback for bad keys", () => {
|
||||
const field: any = "Nope!";
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { NULL_CHOICE, DropDownItem } from "../../../ui/index";
|
||||
import { ResourceIndex } from "../../../resources/interfaces";
|
||||
import { If } from "farmbot";
|
||||
import { isString } from "util";
|
||||
import { findByKindAndId } from "../../../resources/selectors";
|
||||
import { isString } from "lodash";
|
||||
|
||||
interface DisplayLhsProps {
|
||||
currentStep: If;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { TaggedSequence } from "farmbot";
|
||||
import { If } from "farmbot";
|
||||
import { TaggedSequence, If } from "farmbot";
|
||||
import { ResourceIndex } from "../../../resources/interfaces";
|
||||
import { defensiveClone, bail } from "../../../util";
|
||||
import { DropDownItem } from "../../../ui";
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
} from "../../../resources/selectors";
|
||||
import { Point, Tool } from "farmbot/dist";
|
||||
|
||||
export function formatSelectedDropdown(ri: ResourceIndex, ld: LocationData): DropDownItem {
|
||||
export function formatSelectedDropdown(
|
||||
ri: ResourceIndex, ld: LocationData): DropDownItem {
|
||||
switch (ld.kind) {
|
||||
case "tool": return tool(ri, ld);
|
||||
case "point": return point(ri, ld);
|
||||
|
|
|
@ -52,7 +52,8 @@ export function generateList(input: ResourceIndex,
|
|||
const SORT_KEY: keyof DropDownItem = "headingId";
|
||||
const points = selectAllPoints(input)
|
||||
.filter(x => (x.body.pointer_type !== "ToolSlot"));
|
||||
const toolDDI: DropDownItem[] = activeTools(input).map(t => formatTools(t));
|
||||
const toolDDI: DropDownItem[] = activeTools(input)
|
||||
.map(tool => formatTools(tool));
|
||||
return _(points)
|
||||
.map(formatPoint())
|
||||
.concat(toolDDI)
|
||||
|
@ -74,8 +75,8 @@ const formatPoint = () => (p: PointerType): DropDownItem => {
|
|||
};
|
||||
};
|
||||
|
||||
const formatTools = (t: TaggedTool): DropDownItem => {
|
||||
const { id, name } = t.body;
|
||||
const formatTools = (tool: TaggedTool): DropDownItem => {
|
||||
const { id, name } = tool.body;
|
||||
return {
|
||||
label: dropDownName((name || "untitled")),
|
||||
value: "" + id,
|
||||
|
|
|
@ -43,7 +43,8 @@ export function TileReadPin(props: StepParams) {
|
|||
<FBSelect
|
||||
selectedItem={celery2DropDown(pin_number, props.resources)}
|
||||
onChange={setArgsDotPinNumber(props)}
|
||||
list={pinsAsDropDownsReadPin(props.resources, shouldDisplay || (() => false))} />
|
||||
list={pinsAsDropDownsReadPin(props.resources,
|
||||
shouldDisplay || (() => false))} />
|
||||
</Col>
|
||||
<Col xs={6} md={3}>
|
||||
<label>{t("Data Label")}</label>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue