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 deprecations
pull/952/head^2
Gabriel Burnworth 2018-08-08 07:44:12 -07:00 committed by Rick Carlino
parent f25241350e
commit beddbf9c0b
107 changed files with 528 additions and 528 deletions

View File

@ -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"

View File

@ -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;

View File

@ -29,6 +29,7 @@ const fakeProps = (): AppProps => {
axisInversion: { x: false, y: false, z: false },
firmwareConfig: undefined,
xySwap: false,
animate: false,
};
};

View File

@ -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();
});
});

View File

@ -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("*");
});

View File

@ -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);

View File

@ -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());

View File

@ -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();
});
});

View File

@ -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);
});
});

View File

@ -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();

View File

@ -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:

View File

@ -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

View File

@ -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();
});

View File

@ -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>
&nbsp;(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&nbsp;
<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>

View File

@ -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

View File

@ -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 });
});
});

View File

@ -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;

View File

@ -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: {}
};
}

View File

@ -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"));
});
});

View File

@ -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;

View File

@ -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({

View File

@ -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);

View File

@ -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", () => {

View File

@ -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");

View File

@ -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

View File

@ -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 {

View File

@ -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();
});

View File

@ -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)

View File

@ -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", () => {

View File

@ -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.")));
}

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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";

View File

@ -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);
});

View File

@ -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(); }),

View File

@ -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> {

View File

@ -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) =>

View File

@ -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";

View File

@ -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";

View File

@ -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 });
});
});

View File

@ -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);
});

View File

@ -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 {

View File

@ -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 {

View File

@ -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";

View File

@ -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);

View File

@ -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),

View File

@ -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}

View File

@ -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);

View File

@ -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);
});

View File

@ -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",

View File

@ -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()}

View File

@ -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}>

View File

@ -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 {

View File

@ -21,7 +21,8 @@ describe("<FarmBotLayer/>", () => {
},
plantAreaOffset: { x: 100, y: 100 },
peripherals: [],
eStopStatus: false
eStopStatus: false,
getConfigValue: jest.fn(),
};
}

View File

@ -25,6 +25,7 @@ describe("<HoveredPlantLayer/>", () => {
hoveredPlant: fakePlant(),
isEditing: false,
mapTransformProps: fakeMapTransformProps(),
animate: false,
};
}

View File

@ -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", () => {

View File

@ -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,
};
}

View File

@ -15,7 +15,8 @@ describe("<SpreadLayer/>", () => {
zoomLvl: 1.8,
activeDragXY: { x: undefined, y: undefined, z: undefined },
activeDragSpread: undefined,
editing: false
editing: false,
animate: false,
};
}

View File

@ -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}

View File

@ -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" : ""}

View File

@ -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>;

View File

@ -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 &&

View File

@ -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",

View File

@ -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);
}

View File

@ -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);

View File

@ -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
});
}
})}

View File

@ -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}

View File

@ -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) => {

View File

@ -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: {

View File

@ -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", () => {

View File

@ -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) {

View File

@ -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") &&

View File

@ -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"

View File

@ -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[];

View File

@ -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" } });

View File

@ -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("*");

View File

@ -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;

View File

@ -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;

View File

@ -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.

View File

@ -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: [

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -13,6 +13,7 @@ function dev(): Store {
}
function prod(): Store {
// tslint:disable-next-line:no-any
return createStore(rootReducer, ({} as any), getMiddleware("production"));
}

View File

@ -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 } });

View File

@ -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."));

View File

@ -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();

View File

@ -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 {

View File

@ -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", () => {

View File

@ -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,

View File

@ -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);

View File

@ -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);
}
};

View File

@ -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

View File

@ -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);
};

View File

@ -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!";

View File

@ -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;

View File

@ -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";

View File

@ -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);

View File

@ -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,

View File

@ -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