Merge branch 'staging' into staging

pull/1390/head
AscendFB 2019-08-24 01:18:56 +02:00 committed by GitHub
commit 2951baeb05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 396 additions and 34 deletions

View File

@ -20,19 +20,19 @@ module CeleryScriptSettingsBag
"BoxLed3" => BoxLed,
"BoxLed4" => BoxLed }
ALLOWED_AXIS = %w(x y z all)
ALLOWED_ASSERTION_TYPES = %w(abort recover abort_recover)
ALLOWED_ASSERTION_TYPES = %w(abort recover abort_recover continue)
ALLOWED_CHANGES = %w(add remove update)
ALLOWED_CHANNEL_NAMES = %w(ticker toast email espeak)
ALLOWED_LHS_STRINGS = [*(0..69)].map { |x| "pin#{x}" }.concat(%w(x y z))
ALLOWED_LHS_TYPES = [String, :named_pin]
ALLOWED_MESSAGE_TYPES = %w(success busy warn error info fun debug)
ALLOWED_MESSAGE_TYPES = %w(assertion busy debug error fun info success warn)
ALLOWED_OPS = %w(< > is not is_undefined)
ALLOWED_PACKAGES = %w(farmbot_os arduino_firmware)
ALLOWED_PIN_MODES = [DIGITAL = 0, ANALOG = 1]
ALLOWED_PIN_TYPES = PIN_TYPE_MAP.keys
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant)
ALLOWED_RESOURCE_TYPE = %w(Device Point Plant ToolSlot GenericPointer)
ALLOWED_RPC_NODES = %w(calibrate change_ownership
ALLOWED_RPC_NODES = %w(assertion calibrate change_ownership
check_updates dump_info emergency_lock
emergency_unlock execute execute_script
factory_reset find_home flash_firmware home

View File

@ -0,0 +1,10 @@
class AddAssertionLogToWebAppConfig < ActiveRecord::Migration[5.2]
safety_assured
def change
add_column :web_app_configs,
:assertion_log,
:integer,
default: 1
end
end

View File

@ -1728,7 +1728,8 @@ CREATE TABLE public.web_app_configs (
confirm_plant_deletion boolean DEFAULT true,
confirm_sequence_deletion boolean DEFAULT true,
discard_unsaved_sequences boolean DEFAULT false,
user_interface_read_only_mode boolean DEFAULT false
user_interface_read_only_mode boolean DEFAULT false,
assertion_log integer DEFAULT 1
);
@ -3268,6 +3269,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20190722160305'),
('20190729134954'),
('20190804194135'),
('20190804194154');
('20190804194154'),
('20190823164837');

View File

@ -271,6 +271,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig {
device_id: idCounter++,
created_at: "2018-01-11T20:20:38.362Z",
updated_at: "2018-01-22T15:32:41.970Z",
assertion_log: 1,
confirm_plant_deletion: true,
confirm_step_deletion: false,
confirm_sequence_deletion: true,

View File

@ -1362,3 +1362,15 @@ ul {
color: $dark_gray;
}
}
textarea {
border: 0;
padding: 6px 8px;
box-shadow: 0 0 10px #ddd;
outline: none!important;
resize: vertical;
}
textarea:focus {
box-shadow: 0 0 10px rgba(0,0,0,.2);
}

View File

@ -65,6 +65,7 @@ export type SourceFwConfig = (config: McuParamName) =>
export type ShouldDisplay = (x: Feature) => boolean;
/** Names of features that use minimum FBOS version checking. */
export enum Feature {
assertion_block = "assertion_block",
named_pins = "named_pins",
sensors = "sensors",
change_ownership = "change_ownership",

View File

@ -1,6 +1,4 @@
jest.mock("react-redux", () => ({
connect: jest.fn()
}));
jest.mock("react-redux", () => ({ connect: jest.fn() }));
let mockPath = "";
jest.mock("../../../history", () => ({
@ -8,10 +6,15 @@ jest.mock("../../../history", () => ({
getPathArray: jest.fn(() => mockPath.split("/"))
}));
jest.mock("../../../api/crud", () => ({
destroy: jest.fn(),
jest.mock("../../../api/crud", () => ({ destroy: jest.fn() }));
let mockDev = false;
jest.mock("../../../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => mockDev }
}));
jest.mock("../../point_groups/actions", () => ({ createGroup: jest.fn() }));
import * as React from "react";
import { mount } from "enzyme";
import { SelectPlants, SelectPlantsProps } from "../select_plants";
@ -19,6 +22,7 @@ import { fakePlant } from "../../../__test_support__/fake_state/resources";
import { Actions } from "../../../constants";
import { clickButton } from "../../../__test_support__/helpers";
import { destroy } from "../../../api/crud";
import { createGroup } from "../../point_groups/actions";
describe("<SelectPlants />", () => {
beforeEach(function () {
@ -106,4 +110,16 @@ describe("<SelectPlants />", () => {
expect(destroy).toHaveBeenCalledWith("plant.1", true);
expect(destroy).toHaveBeenCalledWith("plant.2", true);
});
it("shows other buttons", () => {
mockDev = true;
const wrapper = mount(<SelectPlants {...fakeProps()} />);
expect(wrapper.text()).toContain("Create");
});
it("creates group", () => {
const wrapper = mount(<SelectPlants {...fakeProps()} />);
wrapper.find(".blue").simulate("click");
expect(createGroup).toHaveBeenCalled();
});
});

View File

@ -77,7 +77,7 @@ export class PlantInventoryItem extends
key={plantId}
onMouseEnter={() => toggle("enter")}
onMouseLeave={() => toggle("leave")}
onClick={click} >
onClick={click}>
<img
className="plant-search-item-image"
src={DEFAULT_ICON}
@ -88,6 +88,6 @@ export class PlantInventoryItem extends
<i className="plant-search-item-age">
{daysOld} {t("days old")}
</i>
</div >;
</div>;
}
}

View File

@ -1,6 +1,5 @@
import * as React from "react";
import { history } from "../../history";
import { connect } from "react-redux";
import { Everything } from "../../interfaces";
import { PlantInventoryItem } from "./plant_inventory_item";
@ -14,6 +13,7 @@ import {
} from "./designer_panel";
import { t } from "../../i18next_wrapper";
import { createGroup } from "../point_groups/actions";
import { DevSettings } from "../../account/dev/dev_support";
export function mapStateToProps(props: Everything) {
return {
@ -29,8 +29,6 @@ export interface SelectPlantsProps {
selected: string[];
}
const YOU_SURE = "Are you sure you want to delete {{length}} plants?";
@connect(mapStateToProps)
export class SelectPlants
extends React.Component<SelectPlantsProps, {}> {
@ -47,7 +45,8 @@ export class SelectPlants
destroySelected = (plantUUIDs: string[]) => {
if (plantUUIDs &&
confirm(t(YOU_SURE, { length: plantUUIDs.length }))) {
confirm(t("Are you sure you want to delete {{length}} plants?",
{ length: plantUUIDs.length }))) {
plantUUIDs.map(uuid => {
this
.props
@ -74,17 +73,19 @@ export class SelectPlants
onClick={() => this.props.dispatch(selectPlant(undefined))}>
{t("Select none")}
</button>
<button className="fb-button blue"
onClick={() => createGroup({
points: this.props.selected,
dispatch: this.props.dispatch
})}>
{t("Create group")}
</button>
{/* <button className="fb-button green"
onClick={() => { throw new Error("WIP"); }}>
{t("Create garden")}
</button> */}
{DevSettings.futureFeaturesEnabled() &&
<button className="fb-button blue"
onClick={() => createGroup({
points: this.props.selected,
dispatch: this.props.dispatch
})}>
{t("Create group")}
</button>}
{DevSettings.futureFeaturesEnabled() &&
<button className="fb-button green"
onClick={() => { throw new Error("WIP"); }}>
{t("Create garden")}
</button>}
</div>;
render() {

View File

@ -98,7 +98,14 @@ describe("<Logs />", () => {
it("shows overall filter status", () => {
const wrapper = mount(<Logs {...fakeProps()} />);
wrapper.setState({
success: 3, busy: 3, warn: 3, error: 3, info: 3, fun: 3, debug: 3
assertion: 3,
busy: 3,
debug: 3,
error: 3,
fun: 3,
info: 3,
success: 3,
warn: 3,
});
const filterBtn = wrapper.find("button").first();
expect(filterBtn.text().toLowerCase()).toEqual("filter");

View File

@ -9,7 +9,7 @@ const logTypes = MESSAGE_TYPES;
describe("<LogsFilterMenu />", () => {
const fakeState: LogsState = {
autoscroll: true, success: 1, busy: 1, warn: 1,
autoscroll: true, assertion: 1, success: 1, busy: 1, warn: 1,
error: 1, info: 1, fun: 1, debug: 1
};
@ -36,7 +36,7 @@ describe("<LogsFilterMenu />", () => {
p.toggle = (x) => () => toggle(x);
p.setFilterLevel = (x) => () => setFilterLevel(x);
const wrapper = mount(<LogsFilterMenu {...p} />);
wrapper.find("button").at(2).simulate("click");
wrapper.find("button").at(3).simulate("click");
expect(toggle).toHaveBeenCalledWith(MessageType.success);
});
@ -46,7 +46,7 @@ describe("<LogsFilterMenu />", () => {
const wrapper = mount(<LogsFilterMenu {...fakeProps()} />);
const toggles = wrapper.find("button");
expect(toggles.last().hasClass("green")).toBeTruthy();
expect(toggles.at(2).hasClass("red")).toBeTruthy();
expect(toggles.at(3).hasClass("red")).toBeTruthy();
});
it("bulk toggles filter levels", () => {

View File

@ -49,6 +49,7 @@ export class Logs extends React.Component<LogsProps, Partial<LogsState>> {
info: this.initialize(NumericSetting.info_log, 1),
fun: this.initialize(NumericSetting.fun_log, 1),
debug: this.initialize(NumericSetting.debug_log, 1),
assertion: this.initialize(NumericSetting.assertion_log, 1),
};
/** Toggle display of a log type. Verbosity level 0 hides all, 3 shows all.*/

View File

@ -128,6 +128,20 @@ export function StepButtonCluster(props: StepButtonProps) {
</StepButton>,
];
shouldDisplay(Feature.assertion_block) && ALL_THE_BUTTONS.push(<StepButton
{...commonStepProps}
step={{
kind: "assertion",
args: {
lua: "return 2 + 2 == 4",
_then: { kind: "nothing", args: {} },
assertion_type: "abort_recover",
}
}}
color="purple">
{t("ASSERTION")}
</StepButton>);
shouldDisplay(Feature.mark_as_step) && ALL_THE_BUTTONS.push(<StepButton
{...commonStepProps}
step={{

View File

@ -0,0 +1,65 @@
import React from "react";
import { shallow } from "enzyme";
import { TileAssertion, AssertionStepProps } from "../tile_assertion";
import { Wait } from "farmbot";
import { StepWrapper } from "../../step_ui/step_wrapper";
import { DeepPartial } from "redux";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
import { renderCeleryNode } from "..";
const EMPTY: DeepPartial<AssertionStepProps> = {};
export const fakeAssertProps = (extras = EMPTY): AssertionStepProps => {
const currentSequence = fakeSequence();
const resources = buildResourceIndex().index;
const props: AssertionStepProps = {
currentSequence,
currentStep: {
kind: "assertion",
args: {
lua: "return 2 + 2 == 4",
assertion_type: "continue",
_then: {
kind: "execute",
args: {
sequence_id: currentSequence.body.id || 0
}
}
}
},
dispatch: jest.fn(),
index: 1,
resources,
confirmStepDeletion: false,
};
return { ...props, ...(extras as AssertionStepProps) };
};
describe("renderer", () => {
it("displays the correct component", () => {
const props = fakeAssertProps();
const actual = renderCeleryNode(props);
const expected = <TileAssertion {...props} />;
expect(actual).toEqual(expected);
});
});
describe("<TileAssertion/>", () => {
it("crashes on non-assertion steps", () => {
const p = fakeAssertProps();
const currentStep: Wait =
({ kind: "wait", args: { milliseconds: 1 } });
const boom = () => TileAssertion({ ...p, currentStep });
expect(boom).toThrow("Not an assertion");
});
it("renders default stuff", () => {
const p = fakeAssertProps();
const el = shallow(<TileAssertion {...p} />);
// We test this component's subcomponents in
// isolation- no need to dupliacte tests here.
expect(el.find(StepWrapper).length).toBe(1);
});
});

View File

@ -31,6 +31,7 @@ import { TileSetZero } from "./tile_set_zero";
import { TileCalibrate } from "./tile_calibrate";
import { TileMoveHome } from "./tile_move_home";
import { t } from "../../i18next_wrapper";
import { TileAssertion } from "./tile_assertion";
interface MoveParams {
step: Step;
@ -157,8 +158,8 @@ export function renderCeleryNode(props: StepParams) {
return <TileFirmwareAction {...props} />;
case "sync": case "dump_info": case "power_off": case "read_status":
case "emergency_unlock": case "emergency_lock":
case "install_first_party_farmware":
return <TileSystemAction {...props} />;
case "install_first_party_farmware": return <TileSystemAction {...props} />;
case "assertion": return <TileAssertion {...props} />;
default: return <TileUnknown {...props} />;
}
}

View File

@ -0,0 +1,46 @@
import { StepParams } from "../interfaces";
import React from "react";
import { Row, Col } from "../../ui";
import { StepHeader } from "../step_ui/step_header";
import { StepContent, StepWrapper } from "../step_ui";
import { TypePart } from "./tile_assertion/type_part";
import { LuaPart } from "./tile_assertion/lua_part";
import { SequencePart } from "./tile_assertion/sequence_part";
import { Assertion } from "farmbot/dist/corpus";
export interface AssertionStepProps extends StepParams {
currentStep: Assertion;
}
const CLASS_NAME = "if-step";
const MOVE_THIS_CSS_PLZ = { marginTop: "10px" };
export function TileAssertion(props: StepParams) {
const step = props.currentStep;
if (step.kind !== "assertion") { throw new Error("Not an assertion"); }
const p = props as AssertionStepProps;
return <StepWrapper>
<StepHeader
className={CLASS_NAME}
helpText={""}
currentSequence={p.currentSequence}
currentStep={p.currentStep}
dispatch={p.dispatch}
index={p.index}
confirmStepDeletion={props.confirmStepDeletion} />
<StepContent className={CLASS_NAME}>
<Row>
<Col xs={12}>
<LuaPart {...p} />
</Col>
</Row>
<Row >
<Col xs={6}><div style={MOVE_THIS_CSS_PLZ}> <TypePart {...p} /></div> </Col>
<Col xs={6}><div style={MOVE_THIS_CSS_PLZ}> <SequencePart {...p} /></div> </Col>
</Row>
</StepContent>
</StepWrapper>;
}

View File

@ -0,0 +1,35 @@
import React from "react";
import { LuaPart } from "../lua_part";
import { shallow } from "enzyme";
import { ReduxAction } from "../../../../redux/interfaces";
import { EditResourceParams } from "../../../../api/interfaces";
import { Actions } from "../../../../constants";
import { TaggedSequence } from "farmbot";
import { fakeAssertProps } from "../../__tests__/tile_assertion_test";
describe("<LuaPart/>", () => {
it("renders default verbiage and props", () => {
const p = fakeAssertProps();
const el = shallow(<LuaPart {...p} />);
const fakeEvent =
({ currentTarget: { value: "hello" } });
el.find("textarea").first().simulate("change", fakeEvent);
expect(p.dispatch).toHaveBeenCalled();
const calledWith: ReduxAction<EditResourceParams> | undefined =
(p.dispatch as jest.Mock).mock.calls[0][0];
if (calledWith) {
expect(calledWith.type).toEqual(Actions.OVERWRITE_RESOURCE);
expect(calledWith.payload.uuid).toEqual(p.currentSequence.uuid);
const s = calledWith.payload.update as TaggedSequence["body"];
expect(s).toBeTruthy();
const item = (s.body || [])[1];
if (item.kind === "assertion") {
expect(item.args.lua).toEqual("hello");
} else {
fail();
}
} else {
fail();
}
});
});

View File

@ -0,0 +1,19 @@
import React from "react";
import { shallow } from "enzyme";
import { SequencePart } from "../sequence_part";
import { SequenceSelectBox } from "../../../sequence_select_box";
import { fakeAssertProps } from "../../__tests__/tile_assertion_test";
describe("<SequencePart/>", () => {
it("renders default verbiage and props", () => {
const p = fakeAssertProps();
const el = shallow(<SequencePart {...p} />);
el
.find(SequenceSelectBox)
.simulate("change", { value: 246, label: "y" });
expect(p.dispatch).toHaveBeenCalled();
const calls = (p.dispatch as jest.Mock).mock.calls[0][0];
const { sequence_id } = calls.payload.update.body[1].args._then.args;
expect(sequence_id).toEqual(246);
});
});

View File

@ -0,0 +1,20 @@
import React from "react";
import { shallow } from "enzyme";
import { TypePart } from "../type_part";
import { FBSelect } from "../../../../ui";
import { fakeAssertProps } from "../../__tests__/tile_assertion_test";
describe("<TypePart/>", () => {
it("renders default verbiage and props", () => {
const p = fakeAssertProps();
const el = shallow(<TypePart {...p} />);
el
.find(FBSelect)
.simulate("change", { value: "anything", label: "y" });
expect(p.dispatch).toHaveBeenCalled();
const calls = (p.dispatch as jest.Mock).mock.calls[0][0];
console.log(calls);
const { assertion_type } = calls.payload.update.body[1].args;
expect(assertion_type).toEqual("anything");
});
});

View File

@ -0,0 +1,27 @@
import { editStep } from "../../../api/crud";
import { Assertion } from "farmbot/dist/corpus";
import React from "react";
import { AssertionStepProps } from "../tile_assertion";
export function LuaPart(props: AssertionStepProps) {
const luaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
props.dispatch(editStep({
step: props.currentStep,
index: props.index,
sequence: props.currentSequence,
executor(c: Assertion) {
c.args.lua = e.currentTarget.value;
}
}));
};
const { lua } = props.currentStep.args;
return <div>
<textarea
value={lua}
onChange={luaChange}
style={{
width: "100%",
height: `${((lua.split("\n").length) + 1) * 1.25}em`
}} />
</div>;
}

View File

@ -0,0 +1,33 @@
import { DropDownItem } from "../../../ui/fb_select";
import { editStep } from "../../../api/crud";
import { Assertion } from "farmbot/dist/corpus";
import React from "react";
import { SequenceSelectBox } from "../../sequence_select_box";
import { AssertionStepProps } from "../tile_assertion";
export function SequencePart(props: AssertionStepProps) {
const onChange = (ddi: DropDownItem) => props.dispatch(editStep({
step: props.currentStep,
index: props.index,
sequence: props.currentSequence,
executor(c: Assertion) {
c.args._then = {
kind: "execute",
args: { sequence_id: ddi.value as number }
};
}
}));
let sequenceId: number | undefined;
const { _then } = props.currentStep.args;
if (_then.kind == "execute") {
sequenceId = _then.args.sequence_id;
}
return <span>
<label>Recovery Sequence</label>
<SequenceSelectBox
onChange={onChange}
resources={props.resources}
sequenceId={sequenceId} />
</span>;
}

View File

@ -0,0 +1,32 @@
import React from "react";
import { ALLOWED_ASSERTION_TYPES, Assertion } from "farmbot";
import { DropDownItem, FBSelect } from "../../../ui";
import { editStep } from "../../../api/crud";
import { AssertionStepProps } from "../tile_assertion";
const ASSERTION_TYPES: Record<ALLOWED_ASSERTION_TYPES, DropDownItem> = {
"continue": { label: "Continue", value: "continue" },
"recover": { label: "Recover and continue", value: "recover" },
"abort_recover": { label: "Abort and recover", value: "abort_recover" },
"abort": { label: "Abort", value: "abort" },
};
export function TypePart(props: AssertionStepProps) {
const { assertion_type } = props.currentStep.args;
return <span>
<label>If Test Fails</label>
<FBSelect
selectedItem={ASSERTION_TYPES[assertion_type]}
onChange={(ddi) => {
props.dispatch(editStep({
step: props.currentStep,
index: props.index,
sequence: props.currentSequence,
executor(c: Assertion) {
c.args.assertion_type = ddi.value as ALLOWED_ASSERTION_TYPES;
}
}));
}}
list={Object.values(ASSERTION_TYPES)} />
</span>;
}

View File

@ -55,6 +55,7 @@ export const BooleanSetting: Record<BooleanConfigKey, BooleanConfigKey> = {
export const NumericSetting: Record<NumberConfigKey, NumberConfigKey> = {
/** Logs settings */
assertion_log: "assertion_log",
success_log: "success_log",
busy_log: "busy_log",
warn_log: "warn_log",

View File

@ -45,7 +45,7 @@
"coveralls": "3.0.5",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.14.0",
"farmbot": "8.1.0",
"farmbot": "8.1.6",
"i18next": "17.0.9",
"lodash": "4.17.15",
"markdown-it": "9.0.1",

View File

@ -17,6 +17,24 @@ describe Api::LogsController do
end
describe "#create" do
it "allows `assertion` logs" do
sign_in user
before_count = Log.count
body = {
channels: [],
major_version: 6,
message: "HELLO",
minor_version: 4,
type: "assertion",
verbosity: 1,
x: 0,
y: 0,
z: 0,
}
post :create, body: body.to_json, params: { format: :json }
expect(response.status).to eq(200)
end
it "creates one log (legacy format)" do
sign_in user
before_count = Log.count