Skip to content
This repository has been archived by the owner on Dec 7, 2021. It is now read-only.

feat: Save partial project progress during project creation #769

Merged
merged 3 commits into from
Apr 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 36 additions & 15 deletions src/react/components/pages/projectSettings/projectForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ describe("Project Form Component", () => {
const appSettings = MockFactory.appSettings();
const connections = MockFactory.createTestConnections();
let wrapper: ReactWrapper<IProjectFormProps, IProjectFormState> = null;
let onSubmitHandler: jest.Mock = null;
let onCancelHandler: jest.Mock = null;
const onSubmitHandler = jest.fn();
const onChangeHandler = jest.fn();
const onCancelHandler = jest.fn();

function createComponent(props: IProjectFormProps) {
return mount(
Expand All @@ -33,13 +34,16 @@ describe("Project Form Component", () => {

describe("Completed project", () => {
beforeEach(() => {
onSubmitHandler = jest.fn();
onCancelHandler = jest.fn();
onChangeHandler.mockClear();
onSubmitHandler.mockClear();
onCancelHandler.mockClear();

wrapper = createComponent({
project,
connections,
appSettings,
onSubmit: onSubmitHandler,
onChange: onChangeHandler,
onCancel: onCancelHandler,
});
});
Expand Down Expand Up @@ -76,10 +80,14 @@ describe("Project Form Component", () => {

const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({

const expectedProject = {
...project,
name: newName,
});
};

expect(onChangeHandler).toBeCalled();
expect(onSubmitHandler).toBeCalledWith(expectedProject);
});

it("starting project should update description upon submission", () => {
Expand All @@ -92,10 +100,14 @@ describe("Project Form Component", () => {

const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({

const expectedProject = {
...project,
description: newDescription,
});
};

expect(onChangeHandler).toBeCalledWith(expect.objectContaining(project));
expect(onSubmitHandler).toBeCalledWith(expectedProject);
});

it("starting project should update source connection ID upon submission", () => {
Expand All @@ -109,11 +121,14 @@ describe("Project Form Component", () => {
expect(wrapper.state().formData.sourceConnection).toEqual(newConnection);
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({

const expectedProject = {
...project,
sourceConnection: connections[1],
});
};

expect(onChangeHandler).toBeCalledWith(expect.objectContaining(project));
expect(onSubmitHandler).toBeCalledWith(expectedProject);
});

it("starting project should update target connection ID upon submission", () => {
Expand All @@ -125,13 +140,17 @@ describe("Project Form Component", () => {
wrapper.find("select#root_targetConnection").simulate("change", { target: { value: newConnection.id } });
expect(wrapper.state().formData.targetConnection).toEqual(newConnection);
wrapper.find("form").simulate("submit");
expect(onSubmitHandler).toBeCalledWith({

const expectedProject = {
...project,
targetConnection: connections[1],
});
};

expect(onChangeHandler).toBeCalledWith(expect.objectContaining(project));
expect(onSubmitHandler).toBeCalledWith(expectedProject);
});

it("starting project should call onChangeHandler on submission", () => {
it("starting project should call onSubmitHandler on submission", () => {
const form = wrapper.find("form");
form.simulate("submit");
expect(onSubmitHandler).toBeCalledWith({
Expand All @@ -155,6 +174,7 @@ describe("Project Form Component", () => {

const form = wrapper.find("form");
form.simulate("submit");
expect(onChangeHandler).toBeCalledWith(expect.objectContaining(project));
expect(onSubmitHandler).toBeCalledWith(
expect.objectContaining({
name: newName,
Expand Down Expand Up @@ -187,6 +207,7 @@ describe("Project Form Component", () => {
appSettings,
connections: newConnections,
onSubmit: onSubmitHandler,
onChange: onChangeHandler,
onCancel: onCancelHandler,
});
// Source Connection should have all connections
Expand All @@ -202,13 +223,12 @@ describe("Project Form Component", () => {

describe("Empty Project", () => {
beforeEach(() => {
onSubmitHandler = jest.fn();
onCancelHandler = jest.fn();
wrapper = createComponent({
project: null,
appSettings,
connections,
onSubmit: onSubmitHandler,
onChange: onChangeHandler,
onCancel: onCancelHandler,
});
});
Expand Down Expand Up @@ -239,6 +259,7 @@ describe("Project Form Component", () => {
appSettings,
connections,
onSubmit: onSubmitHandler,
onChange: onChangeHandler,
onCancel: onCancelHandler,
});
const newTagName = "My new tag";
Expand Down
10 changes: 9 additions & 1 deletion src/react/components/pages/projectSettings/projectForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import Form, { FormValidation, ISubmitEvent } from "react-jsonschema-form";
import Form, { FormValidation, ISubmitEvent, IChangeEvent } from "react-jsonschema-form";
import { ITagsInputProps, TagEditorModal, TagsInput } from "vott-react";
import { addLocValues, strings } from "../../../../common/strings";
import { IConnection, IProject, ITag, IAppSettings } from "../../../../models/applicationState";
Expand Down Expand Up @@ -28,6 +28,7 @@ export interface IProjectFormProps extends React.Props<ProjectForm> {
connections: IConnection[];
appSettings: IAppSettings;
onSubmit: (project: IProject) => void;
onChange?: (project: IProject) => void;
onCancel?: () => void;
}

Expand Down Expand Up @@ -97,6 +98,7 @@ export default class ProjectForm extends React.Component<IProjectFormProps, IPro
schema={this.state.formSchema}
uiSchema={this.state.uiSchema}
formData={this.state.formData}
onChange={this.onFormChange}
onSubmit={this.onFormSubmit}>
<div>
<button className="btn btn-success mr-1" type="submit">{strings.projectSettings.save}</button>
Expand Down Expand Up @@ -184,6 +186,12 @@ export default class ProjectForm extends React.Component<IProjectFormProps, IPro
return errors;
}

private onFormChange = (changeEvent: IChangeEvent<IProject>) => {
if (this.props.onChange) {
this.props.onChange(changeEvent.formData);
}
}

private onFormSubmit(args: ISubmitEvent<IProject>) {
const project: IProject = {
...args.formData,
Expand Down
140 changes: 107 additions & 33 deletions src/react/components/pages/projectSettings/projectSettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
import MockFactory from "../../../../common/mockFactory";
import createReduxStore from "../../../../redux/store/store";
import ProjectSettingsPage, { IProjectSettingsPageProps } from "./projectSettingsPage";
import ProjectSettingsPage, { IProjectSettingsPageProps, IProjectSettingsPageState } from "./projectSettingsPage";

jest.mock("../../../../services/projectService");
import ProjectService from "../../../../services/projectService";
import { IAppSettings } from "../../../../models/applicationState";
import { IAppSettings, IProject } from "../../../../models/applicationState";
import ProjectMetrics from "./projectMetrics";
import ProjectForm, { IProjectFormProps } from "./projectForm";

jest.mock("./projectMetrics", () => () => {
return (
<div className="project-settings-page-metrics">
Dummy Project Metrics
</div>
);
},
return (
<div className="project-settings-page-metrics">
Dummy Project Metrics
</div>
);
},
);

describe("Project settings page", () => {
Expand All @@ -33,12 +34,29 @@ describe("Project settings page", () => {
);
}

const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
};

beforeAll(() => {
Object.defineProperty(global, "_localStorage", {
value: localStorageMock,
writable: false,
});
});

beforeEach(() => {
localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockClear();
localStorageMock.removeItem.mockClear();

projectServiceMock = ProjectService as jest.Mocked<typeof ProjectService>;
projectServiceMock.prototype.load = jest.fn((project) => ({...project}));
projectServiceMock.prototype.load = jest.fn((project) => ({ ...project }));
});

it("Form submission calls save project action", (done) => {
it("Form submission calls save project action", async () => {
const store = createReduxStore(MockFactory.initialState());
const props = MockFactory.projectSettingsProps();
const saveProjectSpy = jest.spyOn(props.projectActions, "saveProject");
Expand All @@ -47,14 +65,12 @@ describe("Project settings page", () => {

const wrapper = createComponent(store, props);
wrapper.find("form").simulate("submit");
await MockFactory.flushUi();

setImmediate(() => {
expect(saveProjectSpy).toBeCalled();
done();
});
expect(saveProjectSpy).toBeCalled();
});

it("Throws an error when a user tries to create a duplicate project", async (done) => {
it("Throws an error when a user tries to create a duplicate project", async () => {
const project = MockFactory.createTestProject("1");
project.id = "25";
const initialStateOverride = {
Expand All @@ -78,18 +94,16 @@ describe("Project settings page", () => {
},
});
wrapper.find("form").simulate("submit");
setImmediate(async () => {
// expect(saveProjectSpy).toBeCalled();
expect(saveProjectSpy.mockRejectedValue).not.toBeNull();
done();
});
await MockFactory.flushUi();

expect(saveProjectSpy.mockRejectedValue).not.toBeNull();
});

it("calls save project when user creates a unique project", (done) => {
it("calls save project when user creates a unique project", async () => {
const initialState = MockFactory.initialState();

// New Project should not have id or security token set by default
const project = {...initialState.recentProjects[0]};
const project = { ...initialState.recentProjects[0] };
project.id = null;
project.name = "Brand New Project";
project.securityToken = "";
Expand All @@ -106,20 +120,20 @@ describe("Project settings page", () => {
const wrapper = createComponent(store, props);
wrapper.find("form").simulate("submit");

setImmediate(() => {
// New security token was created for new project
expect(saveAppSettingsSpy).toBeCalled();
const appSettings = saveAppSettingsSpy.mock.calls[0][0] as IAppSettings;
expect(appSettings.securityTokens.length).toEqual(initialState.appSettings.securityTokens.length + 1);
await MockFactory.flushUi();

// New project was saved with new security token
expect(saveProjectSpy).toBeCalledWith({
...project,
securityToken: `${project.name} Token`,
});
// New security token was created for new project
expect(saveAppSettingsSpy).toBeCalled();
const appSettings = saveAppSettingsSpy.mock.calls[0][0] as IAppSettings;
expect(appSettings.securityTokens.length).toEqual(initialState.appSettings.securityTokens.length + 1);

done();
// New project was saved with new security token
expect(saveProjectSpy).toBeCalledWith({
...project,
securityToken: `${project.name} Token`,
});

expect(localStorage.removeItem).toBeCalledWith("projectForm");
});

it("render ProjectMetrics", () => {
Expand All @@ -146,4 +160,64 @@ describe("Project settings page", () => {
expect(projectMetrics).toHaveLength(0);
});
});

describe("Persisting project form", () => {
let wrapper: ReactWrapper = null;

function initPersistProjectFormTest() {
const store = createReduxStore(MockFactory.initialState());
const props = MockFactory.projectSettingsProps();
props.match.url = "/projects/create";
wrapper = createComponent(store, props);
}

it("Loads partial project from local storage", () => {
const partialProject: IProject = {
...{} as any,
name: "partial project",
description: "partial project description",
tags: [
{ name: "tag-1", color: "#ff0000" },
{ name: "tag-3", color: "#ffff00" },
],
};

localStorageMock.getItem.mockImplementationOnce(() => JSON.stringify(partialProject));

initPersistProjectFormTest();
const projectSettingsPage = wrapper
.find(ProjectSettingsPage)
.childAt(0) as ReactWrapper<IProjectSettingsPageProps, IProjectSettingsPageState>;

expect(localStorage.getItem).toBeCalledWith("projectForm");
expect(projectSettingsPage.state().project).toEqual(partialProject);
});

it("Stores partial project in local storage", () => {
initPersistProjectFormTest();
const partialProject: IProject = {
...{} as any,
name: "partial project",
};

const projectForm = wrapper.find(ProjectForm) as ReactWrapper<IProjectFormProps>;
projectForm.props().onChange(partialProject);

expect(localStorage.setItem).toBeCalledWith("projectForm", JSON.stringify(partialProject));
});

it("Does NOT store empty project in local storage", () => {
initPersistProjectFormTest();
const emptyProject: IProject = {
...{} as any,
sourceConnection: {},
targetConnection: {},
exportFormat: {},
};
const projectForm = wrapper.find(ProjectForm) as ReactWrapper<IProjectFormProps>;
projectForm.props().onChange(emptyProject);

expect(localStorage.setItem).not.toBeCalled();
});
});
});
Loading