folders ui tests

pull/1639/head
gabrielburnworth 2019-12-23 14:38:48 -08:00
parent 8d5218f67c
commit 9c3340be56
19 changed files with 765 additions and 130 deletions

View File

@ -2,15 +2,12 @@ import {
TaggedResource,
SpecialStatus,
ResourceName,
TaggedSequence
TaggedSequence,
} from "farmbot";
import {
isTaggedResource,
} from "../resources/tagged_resources";
import {
GetState,
ReduxAction
} from "../redux/interfaces";
import { GetState, ReduxAction } from "../redux/interfaces";
import { API } from "./index";
import axios from "axios";
import {
@ -18,7 +15,7 @@ import {
destroyOK,
destroyNO,
GeneralizedError,
saveOK
saveOK,
} from "../resources/actions";
import { UnsafeError } from "../interfaces";
import { defensiveClone, unpackUUID } from "../util";
@ -284,7 +281,7 @@ export function urlFor(tag: ResourceName) {
User: API.current.usersPath,
WebAppConfig: API.current.webAppConfigPath,
WebcamFeed: API.current.webcamFeedPath,
Folder: API.current.foldersPath
Folder: API.current.foldersPath,
};
const url = OPTIONS[tag];
if (url) {

View File

@ -15,9 +15,6 @@
}
}
.float-right { float: right; }
.float-left { float: left; }
.hardware-widget {
.bp3-popover-wrapper {
float: right;

View File

@ -230,14 +230,16 @@
}
.folders-panel {
height: calc(100vh - 15rem);
overflow-y: auto;
overflow-x: hidden;
margin-left: -30px;
margin-right: -20px;
@media screen and (max-width: 767px) {
margin-left: -15px;
}
.non-empty-state {
height: calc(100vh - 15rem);
overflow-y: auto;
overflow-x: hidden;
}
.panel-top {
margin-left: 1rem !important;
button {
@ -282,8 +284,8 @@
padding-left: 2rem;
padding-right: 2rem;
transition: height 0.5s ease-out,
padding-top 0.5s ease-out,
padding-bottom 0.5s ease-out;
padding-top 0.5s ease-out,
padding-bottom 0.5s ease-out;
transition-delay: 0.4s;
color: $gray;
font-weight: bold;
@ -292,8 +294,8 @@
cursor: pointer;
&.visible {
transition: height 0.3s ease-in,
padding-top 0.3s ease-in,
padding-bottom 0.3s ease-in;
padding-top 0.3s ease-in,
padding-bottom 0.3s ease-in;
transition-delay: 0.2s;
height: 3rem;
padding-top: 0.5rem;
@ -322,8 +324,8 @@
border-left: 4px solid $dark_gray;
}
.fa-chevron-down, .fa-chevron-right {
z-index: 2;
position: absolute;
z-index: 2;
width: 3rem;
font-size: 1.1rem;
}
@ -391,8 +393,8 @@
}
}
.input {
width: 90%;
margin: 0.3rem;
width: 90%;
}
}
}
@ -426,8 +428,8 @@
}
padding-left: 3rem;
.saucer, .icon-saucer {
top: 0.55rem;
position: relative;
top: 0.55rem;
margin: auto;
margin-top: 0.6rem;
}

View File

@ -4,6 +4,7 @@ import { Everything } from "../interfaces";
import { ReduxAction } from "../redux/interfaces";
import * as React from "react";
import { Actions } from "../constants";
import { UUID } from "../resources/interfaces";
export const STEP_DATATRANSFER_IDENTIFER = "farmbot/sequence-step";
/** SIDE EFFECT-Y!! Stores a step into store.draggable.dataTransfer and
@ -12,7 +13,8 @@ export const STEP_DATATRANSFER_IDENTIFER = "farmbot/sequence-step";
export function stepPut(value: Step,
ev: React.DragEvent<HTMLElement>,
intent: DataXferIntent,
draggerId: number):
draggerId: number,
resourceUuid?: UUID):
ReduxAction<DataXferBase> {
const uuid = id();
ev.dataTransfer.setData(STEP_DATATRANSFER_IDENTIFER, uuid);
@ -22,7 +24,8 @@ export function stepPut(value: Step,
intent,
uuid,
value,
draggerId
draggerId,
resourceUuid,
}
};
}

View File

@ -1,4 +1,5 @@
import { SequenceBodyItem as Step } from "farmbot";
import { UUID } from "../resources/interfaces";
/** An entry in the data transfer table. Used to transfer data from a "draggable"
* to a "dropable". For type safety, this is a "tagged union". See Typescript
@ -17,6 +18,8 @@ export interface DataXferBase {
/** "why" the drag/drop event took place (tagged union- See Typescript
* documentation for more information). */
intent: DataXferIntent;
/** Optional resource UUID. */
resourceUuid?: UUID;
}
/** Data transfer payload used when moving a *new* step into an existing step */
@ -51,4 +54,5 @@ export interface StepDraggerProps {
intent: DataXferIntent;
children?: React.ReactNode;
draggerId: number;
resourceUuid?: UUID;
}

View File

@ -2,6 +2,7 @@ import * as React from "react";
import { stepPut } from "./actions";
import { SequenceBodyItem as Step } from "farmbot";
import { DataXferIntent, StepDraggerProps } from "./interfaces";
import { UUID } from "../resources/interfaces";
/** Magic number to indicate that the draggerId was not provided or can't be
* known. */
@ -21,22 +22,21 @@ export const NULL_DRAGGER_ID = 0xCAFEF00D;
export const stepDragEventHandler = (dispatch: Function,
step: Step,
intent: DataXferIntent,
draggerId: number) => {
draggerId: number,
resourceUuid?: UUID) => {
return (ev: React.DragEvent<HTMLElement>) => {
dispatch(stepPut(step, ev, intent, draggerId));
dispatch(stepPut(step, ev, intent, draggerId, resourceUuid));
};
};
export function StepDragger({ dispatch,
step,
children,
intent,
draggerId }: StepDraggerProps) {
export function StepDragger(props: StepDraggerProps) {
const { dispatch, step, children, intent, draggerId, resourceUuid } = props;
return <div className="step-dragger"
onDragStart={stepDragEventHandler(dispatch,
step,
intent,
draggerId)}>
draggerId,
resourceUuid)}>
{children}
</div>;
}

View File

@ -1,3 +1,11 @@
const mockStepGetResult = {
value: { kind: "execute", args: { sequence_id: 1 } },
resourceUuid: "",
};
jest.mock("../../draggable/actions", () => ({
stepGet: jest.fn(() => () => mockStepGetResult),
}));
import { FolderNode } from "../constants";
import { ingest } from "../data_transfer";
import {
@ -11,7 +19,9 @@ import {
toggleFolderOpenState,
toggleFolderEditState,
toggleAll,
moveSequence
moveSequence,
dropSequence,
sequenceEditMaybeSave,
} from "../actions";
import { sample } from "lodash";
import { cloneAndClimb, climb } from "../climb";
@ -24,6 +34,8 @@ import { save, edit, init, initSave, destroy } from "../../api/crud";
import { setActiveSequenceByName } from "../../sequences/set_active_sequence_by_name";
import { push } from "../../history";
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { stepGet } from "../../draggable/actions";
import { SpecialStatus } from "farmbot";
/** A set of fake Folder resources used exclusively for testing purposes.
```
@ -76,7 +88,7 @@ const mockState: DeepPartial<Everything> =
jest.mock("../../redux/store", () => {
return {
store: {
dispatch: jest.fn(),
dispatch: jest.fn(x => typeof x === "function" && x()),
getState: jest.fn(() => mockState)
}
};
@ -146,10 +158,6 @@ export const TEST_GRAPH = ingest({
}
});
describe("deletion of folders", () => {
test.todo("can't delete populated folders");
});
describe("expand/collapse all", () => {
const halfOpen = cloneAndClimb(TEST_GRAPH, (node) => {
node.open = !sample([true, false]);
@ -209,6 +217,16 @@ describe("createFolder", () => {
parent_id: 0
});
});
it("saves a new folder without inputs", () => {
createFolder();
expect(store.dispatch).toHaveReturnedTimes(1);
expect(initSave).toHaveBeenCalledWith("Folder", {
color: "gray",
name: "New Folder",
parent_id: 0
});
});
});
describe("deleteFolder", () => {
@ -259,6 +277,24 @@ describe("toggleAll", () => {
});
});
describe("sequenceEditMaybeSave()", () => {
it("saves", () => {
const sequence = fakeSequence();
sequence.specialStatus = SpecialStatus.SAVED;
sequenceEditMaybeSave(sequence, {});
expect(edit).toHaveBeenCalled();
expect(save).toHaveBeenCalledWith(sequence.uuid);
});
it("doesn't save", () => {
const sequence = fakeSequence();
sequence.specialStatus = SpecialStatus.DIRTY;
sequenceEditMaybeSave(sequence, {});
expect(edit).toHaveBeenCalled();
expect(save).not.toHaveBeenCalled();
});
});
describe("moveSequence", () => {
it("silently fails when given bad UUIDs", () => {
const uuid = "a.b.c";
@ -276,3 +312,30 @@ describe("moveSequence", () => {
expect(save).toHaveBeenCalledWith(uuid);
});
});
describe("dropSequence()", () => {
const fakeDragEvent = ({
dataTransfer: { getData: () => "fakeKey" }
} as unknown as React.DragEvent<HTMLElement>);
it("updates folder_id", () => {
dropSequence(1)(fakeDragEvent);
expect(stepGet).toHaveBeenCalledWith("fakeKey");
expect(edit).toHaveBeenCalledWith(mockSequence, { folder_id: 1 });
});
it("handles missing sequence", () => {
mockStepGetResult.value.args.sequence_id = -1;
dropSequence(1)(fakeDragEvent);
expect(stepGet).toHaveBeenCalledWith("fakeKey");
expect(edit).not.toHaveBeenCalled();
});
it("gets sequence by UUID", () => {
mockStepGetResult.value.args.sequence_id = -1;
mockStepGetResult.resourceUuid = mockSequence.uuid;
dropSequence(1)(fakeDragEvent);
expect(stepGet).toHaveBeenCalledWith("fakeKey");
expect(edit).toHaveBeenCalledWith(mockSequence, { folder_id: 1 });
});
});

View File

@ -1,7 +1,73 @@
jest.mock("../actions", () => ({
updateSearchTerm: jest.fn(),
toggleAll: jest.fn(),
moveSequence: jest.fn(),
dropSequence: jest.fn(() => jest.fn()),
sequenceEditMaybeSave: jest.fn(),
deleteFolder: jest.fn(),
toggleFolderEditState: jest.fn(),
createFolder: jest.fn(),
addNewSequenceToFolder: jest.fn(),
setFolderName: jest.fn(),
toggleFolderOpenState: jest.fn(),
setFolderColor: jest.fn(),
}));
let mockPath = "";
jest.mock("../../history", () => ({
history: { getCurrentLocation: () => ({ pathname: mockPath }) }
}));
import * as React from "react";
import { mount } from "enzyme";
import { Folders } from "../component";
import { FolderProps } from "../constants";
import { mount, shallow } from "enzyme";
import {
Folders, FolderPanelTop, SequenceDropArea, FolderNameEditor,
FolderButtonCluster, FolderListItem, FolderNameInput,
} from "../component";
import {
FolderProps, FolderPanelTopProps, SequenceDropAreaProps, FolderNodeProps,
FolderNodeInitial, FolderButtonClusterProps, FolderItemProps,
FolderNameInputProps,
FolderNodeMedial,
FolderNodeTerminal,
} from "../constants";
import {
updateSearchTerm, toggleAll, moveSequence, dropSequence,
sequenceEditMaybeSave,
deleteFolder,
toggleFolderEditState,
createFolder,
addNewSequenceToFolder,
setFolderName,
toggleFolderOpenState,
setFolderColor,
} from "../actions";
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { SpecialStatus, Color } from "farmbot";
const fakeRootFolder = (): FolderNodeInitial => ({
kind: "initial",
children: [],
id: 1,
name: "my folder",
content: [],
color: "gray",
open: true,
editing: false,
});
const fakeFolderNode = (): FolderNodeMedial => {
const folder = fakeRootFolder() as unknown as FolderNodeMedial;
folder.kind = "medial";
return folder;
};
const fakeTerminalFolder = (): FolderNodeTerminal => {
const folder = fakeRootFolder() as unknown as FolderNodeTerminal;
folder.children = undefined;
folder.kind = "terminal";
return folder;
};
describe("<Folders />", () => {
const fakeProps = (): FolderProps => ({
@ -21,4 +87,460 @@ describe("<Folders />", () => {
const wrapper = mount<Folders>(<Folders {...p} />);
expect(wrapper.text()).toContain("No Sequences.");
});
it("renders sequences outside of folders", () => {
const p = fakeProps();
p.rootFolder.folders[0] = fakeRootFolder();
const sequence = fakeSequence();
p.sequences = { [sequence.uuid]: sequence };
sequence.body.name = "my sequence";
p.rootFolder.noFolder = [sequence.uuid];
const wrapper = mount<Folders>(<Folders {...p} />);
expect(wrapper.text()).toContain("my sequence");
});
it("renders empty folder", () => {
const p = fakeProps();
p.rootFolder.folders[0] = fakeRootFolder();
const wrapper = mount<Folders>(<Folders {...p} />);
expect(wrapper.text()).toContain("my folder");
});
it("renders sequences in folder", () => {
const p = fakeProps();
const sequence = fakeSequence();
sequence.body.name = "my sequence";
p.sequences = { [sequence.uuid]: sequence };
const folder = fakeRootFolder();
folder.content = [sequence.uuid];
p.rootFolder.folders[0] = folder;
const wrapper = mount<Folders>(<Folders {...p} />);
expect(wrapper.text()).toContain("my sequence");
});
it("renders folders in folder", () => {
const p = fakeProps();
const folder = fakeRootFolder();
const childFolder = fakeFolderNode();
childFolder.name = "deeper folder";
folder.children = [childFolder];
p.rootFolder.folders[0] = folder;
const wrapper = mount<Folders>(<Folders {...p} />);
expect(wrapper.text()).toContain("deeper folder");
});
it("renders terminal folder", () => {
const p = fakeProps();
const folder = fakeRootFolder();
folder.name = "folder";
const childFolder = fakeFolderNode();
childFolder.name = "deeper folder";
const terminalFolder = fakeTerminalFolder();
terminalFolder.name = "deepest folder";
childFolder.children = [terminalFolder];
folder.children = [childFolder];
p.rootFolder.folders[0] = folder;
const wrapper = mount<Folders>(<Folders {...p} />);
["folder", "deeper folder", "deepest folder"].map(string =>
expect(wrapper.text()).toContain(string));
});
it("toggles all folders", () => {
const wrapper = mount<Folders>(<Folders {...fakeProps()} />);
expect(wrapper.state().toggleDirection).toEqual(false);
wrapper.instance().toggleAll();
expect(toggleAll).toHaveBeenCalledWith(false);
expect(wrapper.state().toggleDirection).toEqual(true);
});
it("starts sequence move", () => {
const wrapper = mount<Folders>(<Folders {...fakeProps()} />);
expect(wrapper.state().movedSequenceUuid).toEqual(undefined);
wrapper.instance().startSequenceMove("fakeUuid");
expect(wrapper.state().movedSequenceUuid).toEqual("fakeUuid");
expect(wrapper.state().stashedUuid).toEqual(undefined);
});
const toggleMoveTest = (p: {
prev: string | undefined,
current: string | undefined,
arg: string | undefined,
new: string | undefined
}) => {
const wrapper = mount<Folders>(<Folders {...fakeProps()} />);
wrapper.setState({ movedSequenceUuid: p.current, stashedUuid: p.prev });
wrapper.instance().toggleSequenceMove(p.arg);
expect(wrapper.state().movedSequenceUuid).toEqual(p.new);
};
it("toggle sequence move: on", () => {
toggleMoveTest({
prev: undefined, current: undefined, arg: "fakeUuid", new: "fakeUuid"
});
toggleMoveTest({
prev: undefined, current: "oldFakeUuid", arg: "fakeUuid", new: "fakeUuid"
});
});
it("toggle sequence move: off", () => {
toggleMoveTest({
prev: undefined, current: undefined, arg: undefined, new: undefined
});
toggleMoveTest({
prev: "fakeUuid", current: "fakeUuid", arg: undefined, new: undefined
});
toggleMoveTest({
prev: "fakeUuid", current: undefined, arg: "fakeUuid", new: undefined
});
toggleMoveTest({
prev: "fakeUuid", current: "fakeUuid", arg: "fakeUuid", new: undefined
});
});
it("ends sequence move", () => {
const wrapper = mount<Folders>(<Folders {...fakeProps()} />);
wrapper.setState({ movedSequenceUuid: "fakeUuid" });
wrapper.instance().endSequenceMove(1);
expect(moveSequence).toHaveBeenCalledWith("fakeUuid", 1);
expect(wrapper.state().movedSequenceUuid).toEqual(undefined);
});
it("ends sequence move: undefined", () => {
const wrapper = mount<Folders>(<Folders {...fakeProps()} />);
wrapper.setState({ movedSequenceUuid: undefined });
wrapper.instance().endSequenceMove(1);
expect(moveSequence).toHaveBeenCalledWith("", 1);
expect(wrapper.state().movedSequenceUuid).toEqual(undefined);
});
});
describe("<FolderListItem />", () => {
const fakeProps = (): FolderItemProps => ({
startSequenceMove: jest.fn(),
toggleSequenceMove: jest.fn(),
sequence: fakeSequence(),
movedSequenceUuid: undefined,
dispatch: jest.fn(),
variableData: undefined,
inUse: false,
});
it("renders", () => {
const p = fakeProps();
p.sequence.body.name = "my sequence";
const wrapper = mount(<FolderListItem {...p} />);
expect(wrapper.text()).toContain("my sequence");
expect(wrapper.find("li").hasClass("move-source")).toBeFalsy();
expect(wrapper.find("li").hasClass("active")).toBeFalsy();
});
it("renders: move in progress", () => {
const p = fakeProps();
p.movedSequenceUuid = p.sequence.uuid;
const wrapper = mount(<FolderListItem {...p} />);
expect(wrapper.find("li").hasClass("move-source")).toBeTruthy();
});
it("renders: active", () => {
const p = fakeProps();
p.sequence.body.name = "sequence";
mockPath = "/app/sequences/sequence";
const wrapper = mount(<FolderListItem {...p} />);
expect(wrapper.find("li").hasClass("active")).toBeTruthy();
});
it("renders: unsaved", () => {
const p = fakeProps();
p.sequence.body.name = "my sequence";
p.sequence.specialStatus = SpecialStatus.DIRTY;
const wrapper = mount(<FolderListItem {...p} />);
expect(wrapper.text()).toContain("my sequence*");
});
it("renders: in use", () => {
const p = fakeProps();
p.inUse = true;
const wrapper = mount(<FolderListItem {...p} />);
expect(wrapper.find(".in-use").length).toEqual(1);
});
it("changes color", () => {
const p = fakeProps();
p.sequence.body.id = undefined;
p.sequence.body.name = "";
p.sequence.body.color = "" as Color;
const wrapper = shallow(<FolderListItem {...p} />);
wrapper.find("ColorPicker").simulate("change", "green");
expect(sequenceEditMaybeSave).toHaveBeenCalledWith(p.sequence, {
color: "green"
});
});
it("starts sequence move", () => {
const p = fakeProps();
const wrapper = shallow(<FolderListItem {...p} />);
wrapper.find(".fa-bars").simulate("mouseDown");
expect(p.startSequenceMove).toHaveBeenCalledWith(p.sequence.uuid);
});
it("toggles sequence move", () => {
const p = fakeProps();
const wrapper = shallow(<FolderListItem {...p} />);
wrapper.find(".fa-bars").simulate("mouseUp");
expect(p.toggleSequenceMove).toHaveBeenCalledWith(p.sequence.uuid);
});
});
describe("<FolderButtonCluster />", () => {
const fakeProps = (): FolderButtonClusterProps => ({
node: fakeRootFolder(),
close: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<FolderButtonCluster {...fakeProps()} />);
expect(wrapper.find("button").length).toEqual(4);
});
it("deletes folder", () => {
const p = fakeProps();
p.node.id = 1;
const wrapper = mount(<FolderButtonCluster {...p} />);
wrapper.find("button").at(0).simulate("click");
expect(deleteFolder).toHaveBeenCalledWith(1);
});
it("edits folder", () => {
const p = fakeProps();
p.node.id = 1;
const wrapper = mount(<FolderButtonCluster {...p} />);
wrapper.find("button").at(1).simulate("click");
expect(p.close).toHaveBeenCalled();
expect(toggleFolderEditState).toHaveBeenCalledWith(1);
});
it("creates new folder", () => {
const p = fakeProps();
p.node.id = 1;
const wrapper = mount(<FolderButtonCluster {...p} />);
wrapper.find("button").at(2).simulate("click");
expect(p.close).toHaveBeenCalled();
expect(createFolder).toHaveBeenCalledWith({ parent_id: p.node.id });
});
it("creates new sequence", () => {
const p = fakeProps();
p.node.id = 1;
const wrapper = mount(<FolderButtonCluster {...p} />);
wrapper.find("button").at(3).simulate("click");
expect(p.close).toHaveBeenCalled();
expect(addNewSequenceToFolder).toHaveBeenCalledWith(1);
});
});
describe("<FolderNameInput />", () => {
const fakeProps = (): FolderNameInputProps => ({
node: fakeFolderNode(),
});
it("edits folder name", () => {
const p = fakeProps();
p.node.editing = true;
const wrapper = shallow(<FolderNameInput {...p} />);
wrapper.find("BlurableInput").simulate("commit", {
currentTarget: { value: "new name" }
});
expect(setFolderName).toHaveBeenCalledWith(p.node.id, "new name");
});
it("closes folder name input", () => {
const p = fakeProps();
p.node.editing = true;
const wrapper = shallow(<FolderNameInput {...p} />);
wrapper.find("button").simulate("click");
expect(toggleFolderEditState).toHaveBeenCalledWith(p.node.id);
});
});
describe("<FolderNameEditor />", () => {
const fakeProps = (): FolderNodeProps => ({
node: fakeRootFolder(),
sequences: {},
movedSequenceUuid: undefined,
startSequenceMove: jest.fn(),
toggleSequenceMove: jest.fn(),
onMoveEnd: jest.fn(),
dispatch: jest.fn(),
resourceUsage: {},
sequenceMetas: {},
});
it("renders", () => {
const p = fakeProps();
const wrapper = mount(<FolderNameEditor {...p} />);
expect(wrapper.text()).toContain("my folder");
expect(wrapper.find(".fa-ellipsis-v").hasClass("open")).toBeFalsy();
expect(wrapper.find(".fa-chevron-down").length).toEqual(1);
expect(wrapper.find(".fa-chevron-right").length).toEqual(0);
expect(wrapper.find(".folder-name-input").length).toEqual(0);
});
it("renders: settings open", () => {
const p = fakeProps();
const wrapper = mount<FolderNameEditor>(<FolderNameEditor {...p} />);
wrapper.setState({ settingsOpen: true });
expect(wrapper.find(".fa-ellipsis-v").hasClass("open")).toBeTruthy();
});
it("renders: folder closed", () => {
const p = fakeProps();
p.node.open = false;
const wrapper = mount(<FolderNameEditor {...p} />);
expect(wrapper.find(".fa-chevron-down").length).toEqual(0);
expect(wrapper.find(".fa-chevron-right").length).toEqual(1);
});
it("renders: editing", () => {
const p = fakeProps();
p.node.editing = true;
const wrapper = mount(<FolderNameEditor {...p} />);
expect(wrapper.find(".folder-name-input").length).toEqual(1);
});
it("closes folder", () => {
const p = fakeProps();
p.node.open = true;
const wrapper = mount(<FolderNameEditor {...p} />);
wrapper.find("i").first().simulate("click");
expect(toggleFolderOpenState).toHaveBeenCalledWith(p.node.id);
});
it("changes folder color", () => {
const p = fakeProps();
const wrapper = shallow(<FolderNameEditor {...p} />);
wrapper.find("ColorPicker").simulate("change", "green");
expect(setFolderColor).toHaveBeenCalledWith(p.node.id, "green");
});
it("opens settings menu", () => {
const p = fakeProps();
const wrapper = shallow<FolderNameEditor>(<FolderNameEditor {...p} />);
expect(wrapper.state().settingsOpen).toBeFalsy();
wrapper.find("i").last().simulate("click");
expect(wrapper.state().settingsOpen).toBeTruthy();
});
it("closes settings menu", () => {
const p = fakeProps();
const wrapper = mount<FolderNameEditor>(<FolderNameEditor {...p} />);
wrapper.setState({ settingsOpen: true });
wrapper.instance().close();
expect(wrapper.state().settingsOpen).toBeFalsy();
});
});
describe("<SequenceDropArea />", () => {
const fakeProps = (): SequenceDropAreaProps => ({
dropAreaVisible: true,
onMoveEnd: jest.fn(),
toggleSequenceMove: jest.fn(),
folderId: 1,
folderName: "my folder",
});
it("shows drop area", () => {
const p = fakeProps();
p.dropAreaVisible = true;
const wrapper = mount(<SequenceDropArea {...p} />);
expect(wrapper.find(".folder-drop-area").hasClass("visible")).toBeTruthy();
expect(wrapper.text().toLowerCase()).toContain("move into my folder");
});
it("hides drop area", () => {
const p = fakeProps();
p.dropAreaVisible = false;
const wrapper = mount(<SequenceDropArea {...p} />);
expect(wrapper.find(".folder-drop-area").hasClass("visible")).toBeFalsy();
});
it("has 'remove from folders' text", () => {
const p = fakeProps();
p.dropAreaVisible = true;
p.folderId = 0;
const wrapper = mount(<SequenceDropArea {...p} />);
expect(wrapper.find(".folder-drop-area").hasClass("visible")).toBeTruthy();
expect(wrapper.text()).not.toContain("my folder");
expect(wrapper.text().toLowerCase()).toContain("move out of folders");
});
it("handles click", () => {
const p = fakeProps();
const wrapper = shallow(<SequenceDropArea {...p} />);
wrapper.find(".folder-drop-area").simulate("click");
expect(p.onMoveEnd).toHaveBeenCalledWith(p.folderId);
});
it("handles drop", () => {
const p = fakeProps();
const wrapper = shallow<SequenceDropArea>(<SequenceDropArea {...p} />);
wrapper.setState({ hovered: true });
expect(wrapper.find(".folder-drop-area").hasClass("hovered")).toBeTruthy();
wrapper.find(".folder-drop-area").simulate("drop");
expect(wrapper.state().hovered).toBeFalsy();
expect(dropSequence).toHaveBeenCalledWith(p.folderId);
expect(p.toggleSequenceMove).toHaveBeenCalled();
});
it("handles drag over", () => {
const p = fakeProps();
const wrapper = shallow(<SequenceDropArea {...p} />);
const e = { preventDefault: jest.fn() };
wrapper.find(".folder-drop-area").simulate("dragOver", e);
expect(e.preventDefault).toHaveBeenCalled();
});
it("handles drag enter", () => {
const p = fakeProps();
const wrapper = shallow<SequenceDropArea>(<SequenceDropArea {...p} />);
wrapper.find(".folder-drop-area").simulate("dragEnter");
expect(wrapper.state().hovered).toBeTruthy();
});
it("handles drag leave", () => {
const p = fakeProps();
const wrapper = shallow<SequenceDropArea>(<SequenceDropArea {...p} />);
wrapper.find(".folder-drop-area").simulate("dragLeave");
expect(wrapper.state().hovered).toBeFalsy();
});
});
describe("<FolderPanelTop />", () => {
const fakeProps = (): FolderPanelTopProps => ({
searchTerm: "",
toggleDirection: true,
toggleAll: jest.fn(),
});
it("changes search term", () => {
const p = fakeProps();
const wrapper = shallow(<FolderPanelTop {...p} />);
wrapper.find("input").simulate("change", {
currentTarget: { value: "new" }
});
expect(updateSearchTerm).toHaveBeenCalledWith("new");
});
it("creates new folder", () => {
const p = fakeProps();
const wrapper = mount(<FolderPanelTop {...p} />);
wrapper.find("button").at(1).simulate("click");
expect(createFolder).toHaveBeenCalledWith(undefined);
});
it("creates new sequence", () => {
const p = fakeProps();
const wrapper = mount(<FolderPanelTop {...p} />);
wrapper.find("button").at(2).simulate("click");
expect(addNewSequenceToFolder).toHaveBeenCalledWith(undefined);
});
});

View File

@ -118,7 +118,8 @@ export const dropSequence = (folder_id: number) =>
const dataXferObj = dispatch(stepGet(key));
const { sequence_id } = dataXferObj.value.args;
const ri = store.getState().resources.index;
const seqUuid = ri.byKindAndId[joinKindAndId("Sequence", sequence_id)];
const seqUuid = dataXferObj.resourceUuid ||
ri.byKindAndId[joinKindAndId("Sequence", sequence_id)];
const sequence = maybeGetSequence(ri, seqUuid);
if (sequence) { sequenceEditMaybeSave(sequence, { folder_id }); }
};

View File

@ -62,7 +62,8 @@ export const FolderListItem = (props: FolderItemProps) => {
body: variableList(props.variableData)
}}
intent="step_splice"
draggerId={NULL_DRAGGER_ID}>
draggerId={NULL_DRAGGER_ID}
resourceUuid={sequence.uuid}>
<li className={`sequence-list-item ${active} ${moveSource}`}
draggable={true}>
<ColorPicker
@ -91,7 +92,7 @@ const ToggleFolderBtn = (props: ToggleFolderBtnProps) => {
const AddFolderBtn = ({ folder, close }: AddFolderBtn) => {
return <button
className="fb-button green"
onClick={() => { close?.(); createFolder(folder || {}); }}>
onClick={() => { close?.(); createFolder(folder); }}>
<div className="fa-stack fa-2x" title={"Create Subfolder"}>
<i className="fa fa-folder fa-stack-2x" />
<i className="fa fa-plus fa-stack-1x" />
@ -129,7 +130,7 @@ export const FolderButtonCluster =
</div>;
};
const FolderNameInput = ({ node }: FolderNameInputProps) =>
export const FolderNameInput = ({ node }: FolderNameInputProps) =>
<div className="folder-name-input">
<BlurableInput value={node.name} onCommit={e =>
setFolderName(node.id, e.currentTarget.value)} />
@ -143,6 +144,7 @@ const FolderNameInput = ({ node }: FolderNameInputProps) =>
export class FolderNameEditor
extends React.Component<FolderNodeProps, FolderNodeState> {
state: FolderNodeState = { settingsOpen: false };
close = () => this.setState({ settingsOpen: false });
render() {
const { node } = this.props;
const settingsOpenClass = this.state.settingsOpen ? "open" : "";
@ -164,8 +166,7 @@ export class FolderNameEditor
<i className={`fa fa-ellipsis-v ${settingsOpenClass}`}
onClick={() =>
this.setState({ settingsOpen: !this.state.settingsOpen })} />
<FolderButtonCluster {...this.props}
close={() => this.setState({ settingsOpen: false })} />
<FolderButtonCluster {...this.props} close={this.close} />
</Popover>
</div>;
}
@ -322,9 +323,7 @@ export const FolderPanelTop = (props: FolderPanelTopProps) =>
<i className="fa fa-search" />
<input
value={props.searchTerm || ""}
onChange={({ currentTarget }) => {
updateSearchTerm(currentTarget.value);
}}
onChange={e => updateSearchTerm(e.currentTarget.value)}
type="text"
placeholder={t("Search sequences")} />
</div>

View File

@ -94,7 +94,8 @@ export interface FolderNodeProps {
sequenceMetas: Record<UUID, VariableNameSet | undefined>;
}
export interface FolderButtonClusterProps extends FolderNodeProps {
export interface FolderButtonClusterProps {
node: FolderUnion;
close(): void;
}

View File

@ -15,6 +15,7 @@ import { fakeResource } from "../../__test_support__/fake_resource";
import { resourceReducer } from "../reducer";
import { findByUuid } from "../reducer_support";
import { EditResourceParams } from "../../api/interfaces";
import { fakeFolder } from "../../__test_support__/fake_state/resources";
describe("resource reducer", () => {
it("marks resources as DIRTY when reducing OVERWRITE_RESOURCE", () => {
@ -114,6 +115,17 @@ describe("resource reducer", () => {
.concat(["Image", "SensorReading"])
.map((kind: ResourceName) => testResourceDestroy(kind));
});
it("toggles folder open state", () => {
const folder = fakeFolder();
folder.body.id = 1;
const startingState = buildResourceIndex([folder]);
delete startingState.index.sequenceFolders.localMetaAttributes[1].open;
const action = { type: Actions.FOLDER_TOGGLE, payload: { id: 1 } };
const newState = resourceReducer(startingState, action);
expect(newState.index.sequenceFolders.localMetaAttributes[1].open)
.toEqual(false);
});
});
describe("findByUuid", () => {

View File

@ -17,7 +17,6 @@ import { resourceReducer } from "../reducer";
import { emptyState } from "../reducer";
import { resourceReady, newTaggedResource } from "../../sync/actions";
import { chain } from "lodash";
// import { Actions } from "../../constants";
const TOOL_ID = 99;
const SLOT_ID = 100;
@ -156,6 +155,7 @@ describe("getSequenceByUUID()", () => {
expect(console.warn).toBeCalled();
});
});
describe("getUserAccountSettings", () => {
it("throws exceptions when user is not loaded", () => {
const boom = () => Selector
@ -164,6 +164,7 @@ describe("getUserAccountSettings", () => {
.toThrow("PROBLEM: Tried to fetch user before it was available.");
});
});
describe("maybeGetSequence", () => {
it("returns undefined", () => {
const i = buildResourceIndex([]);
@ -236,6 +237,13 @@ describe("findRegimenById()", () => {
});
});
describe("findFolderById()", () => {
it("throws error", () => {
const find = () => Selector.findFolderById(fakeIndex, 0);
expect(find).toThrow("Bad folder id: 0");
});
});
describe("maybeFindPlantById()", () => {
it("not found", () => {
const result = Selector.maybeFindPlantById(fakeIndex, 0);

View File

@ -1,68 +1,44 @@
import * as React from "react";
import { AllSteps } from "../all_steps";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { AllSteps, AllStepsProps } from "../all_steps";
import { shallow } from "enzyme";
import { TaggedSequence, SpecialStatus } from "farmbot";
import { TileMoveRelative } from "../step_tiles/tile_move_relative";
import { TileReadPin } from "../step_tiles/tile_read_pin";
import { TileWritePin } from "../step_tiles/tile_write_pin";
import { sanitizeNodes } from "../locals_list/sanitize_nodes";
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { fakeResourceIndex } from "../locals_list/test_helpers";
import { maybeTagStep } from "../../resources/sequence_tagging";
import { DropArea } from "../../draggable/drop_area";
describe("<AllSteps/>", () => {
const TEST_CASE: TaggedSequence = {
"kind": "Sequence",
"specialStatus": SpecialStatus.SAVED,
"body": sanitizeNodes({
"id": 8,
"name": "Goto 0, 0, 0",
"folder_id": undefined,
"color": "gray",
"body": [
{
"kind": "move_relative",
"args": {
"x": 0,
"y": 0,
"z": 0,
"speed": 100
},
},
{
"kind": "read_pin",
"args": {
"pin_number": 0,
"pin_mode": 0,
"label": "---"
},
},
{
"kind": "write_pin",
"args": {
"pin_number": 0,
"pin_value": 0,
"pin_mode": 0
},
}
],
"args": {
"locals": { kind: "scope_declaration", args: {} },
"version": 4,
},
"kind": "sequence"
}).thisSequence,
"uuid": "Sequence.8.52"
};
const fakeProps = (): AllStepsProps => ({
sequence: fakeSequence(),
onDrop: jest.fn(),
dispatch: jest.fn(),
resources: fakeResourceIndex(),
confirmStepDeletion: true,
});
it("uses index as a key", () => {
const el = shallow(<AllSteps
sequence={TEST_CASE}
onDrop={() => { }}
dispatch={jest.fn()}
resources={buildResourceIndex([]).index}
confirmStepDeletion={false} />);
[TileMoveRelative, TileReadPin, TileWritePin]
.map(q => {
expect(el.find(q).length).toEqual(1);
});
it("renders empty sequence", () => {
const wrapper = shallow(<AllSteps {...fakeProps()} />);
expect(wrapper.html()).toEqual("<div class=\"all-steps\"></div>");
});
it("renders steps", () => {
const p = fakeProps();
p.sequence.body.body = [
{ kind: "move_relative", args: { x: 0, y: 0, z: 0, speed: 100 } },
{ kind: "read_pin", args: { pin_number: 0, pin_mode: 0, label: "---" } },
{ kind: "write_pin", args: { pin_number: 0, pin_value: 0, pin_mode: 0 } }
];
p.sequence.body.body.map(step => maybeTagStep(step));
const wrapper = shallow(<AllSteps {...p} />);
["TileMoveRelative", "TileReadPin", "TileWritePin"]
.map(element => expect(wrapper.find(element).length).toEqual(1));
});
it("calls onDrop", () => {
const p = fakeProps();
p.sequence.body.body = [{ kind: "wait", args: { milliseconds: 0 } }];
p.sequence.body.body.map(step => maybeTagStep(step));
const wrapper = shallow(<AllSteps {...p} />);
wrapper.find<DropArea>(DropArea).props().callback?.("fake key");
expect(p.onDrop).toHaveBeenCalledWith(0, "fake key");
});
});

View File

@ -44,7 +44,6 @@ describe("<Sequences/>", () => {
it("renders", () => {
const wrapper = shallow(<Sequences {...fakeProps()} />);
debugger;
expect(wrapper.html()).toContain("Sequences");
expect(wrapper.html()).toContain("Edit Sequence");
expect(wrapper.html()).toContain(ToolTips.SEQUENCE_EDITOR);

View File

@ -10,7 +10,7 @@ import { HardwareFlags, FarmwareInfo } from "./interfaces";
import { ShouldDisplay } from "../devices/interfaces";
import { AddCommandButton } from "./sequence_editor_middle_active";
interface AllStepsProps {
export interface AllStepsProps {
sequence: TaggedSequence;
onDrop(index: number, key: string): void;
dispatch: Function;
@ -25,9 +25,7 @@ interface AllStepsProps {
export class AllSteps extends React.Component<AllStepsProps, {}> {
render() {
const {
sequence, onDrop, dispatch, hardwareFlags, farmwareInfo, shouldDisplay
} = this.props;
const { sequence, dispatch } = this.props;
const items = (sequence.body.body || [])
.map((currentStep: SequenceBodyItem, index) => {
/** HACK: React's diff algorithm (probably?) can't keep track of steps
@ -39,22 +37,22 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
return <div className="sequence-steps"
key={readThatCommentAbove}>
<AddCommandButton dispatch={dispatch} index={index} />
<DropArea callback={(key) => onDrop(index, key)} />
<DropArea callback={key => this.props.onDrop(index, key)} />
<StepDragger
dispatch={dispatch}
step={currentStep}
intent="step_move"
draggerId={index}>
<div>
<div className="sequence-step">
{renderCeleryNode({
currentStep,
index,
dispatch,
currentSequence: sequence,
resources: this.props.resources,
hardwareFlags,
farmwareInfo,
shouldDisplay,
hardwareFlags: this.props.hardwareFlags,
farmwareInfo: this.props.farmwareInfo,
shouldDisplay: this.props.shouldDisplay,
confirmStepDeletion: this.props.confirmStepDeletion,
showPins: this.props.showPins,
expandStepOptions: this.props.expandStepOptions,
@ -64,6 +62,6 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
</div>;
});
return <div> {items} </div>;
return <div className="all-steps">{items}</div>;
}
}

View File

@ -2,13 +2,12 @@ import * as React from "react";
import { connect } from "react-redux";
import { StepButtonCluster } from "./step_button_cluster";
import { SequenceEditorMiddle } from "./sequence_editor_middle";
import { Page, Row, LeftPanel } from "../ui";
import { Page, Row, LeftPanel, CenterPanel, RightPanel } from "../ui";
import { Props } from "./interfaces";
import { mapStateToProps } from "./state_to_props";
import { ToolTips } from "../constants";
import { isTaggedSequence } from "../resources/tagged_resources";
import { setActiveSequenceByName } from "./set_active_sequence_by_name";
import { CenterPanel, RightPanel } from "../ui";
import { t } from "../i18next_wrapper";
import { unselectSequence, closeCommandMenu } from "./actions";
import { isNumber } from "lodash";

View File

@ -0,0 +1,55 @@
import * as React from "react";
import { mount } from "enzyme";
import {
ColorPicker,
ColorPickerProps,
ColorPickerCluster,
ColorPickerClusterProps,
} from "../color_picker";
describe("<ColorPicker />", () => {
const fakeProps = (): ColorPickerProps => ({
current: "green",
onChange: jest.fn(),
});
it("renders saucers", () => {
const wrapper = mount(<ColorPicker {...fakeProps()} />);
expect(wrapper.find(".saucer").length).toEqual(1);
expect(wrapper.find(".green").length).toEqual(1);
});
it("renders icon saucers", () => {
const p = fakeProps();
p.saucerIcon = "fa-check";
const wrapper = mount(<ColorPicker {...p} />);
expect(wrapper.find(".icon-saucer").length).toEqual(1);
expect(wrapper.find(".green").length).toEqual(1);
});
});
describe("<ColorPickerCluster />", () => {
const fakeProps = (): ColorPickerClusterProps => ({
current: "green",
onChange: jest.fn(),
});
it("renders saucers", () => {
const wrapper = mount(<ColorPickerCluster {...fakeProps()} />);
expect(wrapper.find(".saucer").length).toEqual(8);
});
it("renders icon saucers", () => {
const p = fakeProps();
p.saucerIcon = "fa-check";
const wrapper = mount(<ColorPickerCluster {...p} />);
expect(wrapper.find("i").length).toEqual(8);
});
it("changes color", () => {
const p = fakeProps();
const wrapper = mount(<ColorPickerCluster {...p} />);
wrapper.find("div").at(1).simulate("click");
expect(p.onChange).toHaveBeenCalledWith("blue");
});
});

View File

@ -4,14 +4,14 @@ import { Saucer } from "../ui/index";
import { ResourceColor } from "../interfaces";
import { colors } from "../util";
interface PickerProps {
export interface ColorPickerProps {
position?: Position;
current: ResourceColor;
onChange?: (color: ResourceColor) => void;
onChange: (color: ResourceColor) => void;
saucerIcon?: string;
}
interface ColorPickerClusterProps {
export interface ColorPickerClusterProps {
onChange: (color: ResourceColor) => void;
current: ResourceColor;
saucerIcon?: string;
@ -42,10 +42,9 @@ export const ColorPickerCluster = (props: ColorPickerClusterProps) => {
})}
</div>;
};
export class ColorPicker extends React.Component<PickerProps, {}> {
export class ColorPicker extends React.Component<ColorPickerProps, {}> {
public render() {
const cb = this.props.onChange || function () { };
return <Popover className="color-picker"
position={this.props.position || Position.BOTTOM}
popoverClassName="colorpicker-menu gray">
@ -54,7 +53,7 @@ export class ColorPicker extends React.Component<PickerProps, {}> {
this.props.current}`} />
: <Saucer color={this.props.current} />}
<ColorPickerCluster
onChange={cb}
onChange={this.props.onChange}
current={this.props.current}
saucerIcon={this.props.saucerIcon} />
</Popover>;