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

Commit

Permalink
Test CreateNewProjectWizard components (#361)
Browse files Browse the repository at this point in the history
* WIP: Added tests

* WIP: ProjectType undefined not throwing in test

* fixed flow

* added tests

* reverted to children render props

* Merge branch 'master' into test-create-new-project-wiz

* Fixed linting & replaced snapshot with simple smoke tests

* addressed review comments
  • Loading branch information
AWolf81 authored and Haroenv committed Mar 27, 2019
1 parent 5d17d5b commit 9028054
Show file tree
Hide file tree
Showing 25 changed files with 1,847 additions and 65 deletions.
8 changes: 8 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,12 @@ module.exports = {
'flowtype/generic-spacing': 0,
'jest/no-large-snapshots': ['warn', { maxSize: 100 }],
},
overrides: [
{
files: ['*.test.js', '*.spec.js'],
rules: {
'flowtype/require-valid-file-annotation': 0,
},
},
],
};
2 changes: 1 addition & 1 deletion src/components/CreateNewProjectWizard/BuildPane.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const BUILD_STEPS = {
},
};

const BUILD_STEP_KEYS: Array<BuildStep> = Object.keys(BUILD_STEPS);
export const BUILD_STEP_KEYS: Array<BuildStep> = Object.keys(BUILD_STEPS);

type Props = {
projectName: string,
Expand Down
52 changes: 31 additions & 21 deletions src/components/CreateNewProjectWizard/CreateNewProjectWizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,37 @@ import type { Field, Status, Step } from './types';
import type { ProjectType, ProjectInternal, AppSettings } from '../../types';
import type { Dispatch } from '../../actions/types';

const FORM_STEPS: Array<Field> = [
export const FORM_STEPS: Array<Field> = [
'projectName',
'projectType',
'projectIcon',
'projectStarter',
];

export const dialogOptionsFolderExists = {
type: 'warning',
title: 'Project directory exists',
message:
"Looks like there's already a project with that name. Did you mean to import it instead?",
buttons: ['OK'],
};

export const dialogCallbackFolderExists = (
resolve: (result: any) => void,
reject: (error: any) => void
) => (result: number) => {
if (result === 0) {
return reject();
}

resolve();
};

export const dialogStarterNotFoundErrorArgs = (projectStarter: string) => [
`Starter ${projectStarter} not found`,
'Please check your starter url or use the starter selection to pick a starter.',
];

const { dialog } = remote;

type Props = {
Expand Down Expand Up @@ -75,7 +100,7 @@ const initialState = {
settings: null,
};

class CreateNewProjectWizard extends PureComponent<Props, State> {
export class CreateNewProjectWizard extends PureComponent<Props, State> {
state = initialState;
timeoutId: number;

Expand Down Expand Up @@ -129,25 +154,13 @@ class CreateNewProjectWizard extends PureComponent<Props, State> {
};

checkProjectLocationUsage = () => {
return new Promise((resolve, reject) => {
return new Promise<any>((resolve, reject) => {
const projectName = getProjectNameSlug(this.state.projectName);
if (checkIfProjectExists(this.props.projectHomePath, projectName)) {
// show warning that the project folder already exists & stop creation
dialog.showMessageBox(
{
type: 'warning',
title: 'Project directory exists',
message:
"Looks like there's already a project with that name. Did you mean to import it instead?",
buttons: ['OK'],
},
result => {
if (result === 0) {
return reject();
}

resolve();
}
dialogOptionsFolderExists,
dialogCallbackFolderExists(resolve, reject)
);
} else {
resolve();
Expand All @@ -170,10 +183,7 @@ class CreateNewProjectWizard extends PureComponent<Props, State> {

if (!exists) {
// starter not found
dialog.showErrorBox(
`Starter ${projectStarter} not found`,
'Please check your starter url or use the starter selection to pick a starter.'
);
dialog.showErrorBox(...dialogStarterNotFoundErrorArgs(projectStarter));
throw new Error('starter-not-found');
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/CreateNewProjectWizard/ImportExisting.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Props = {
isOnboarding: boolean,
};

const ImportExisting = ({ isOnboarding }: Props) => {
export const ImportExisting = ({ isOnboarding }: Props) => {
if (isOnboarding) {
// When the user is onboarding, there's a much more prominent prompt to
// import existing projects, so we don't need this extra snippet.
Expand Down
6 changes: 4 additions & 2 deletions src/components/CreateNewProjectWizard/ProjectName.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ class ProjectName extends PureComponent<Props, State> {
}
};

setRef = (node: HTMLElement) => (this.node = node);

render() {
const {
name,
Expand All @@ -176,7 +178,7 @@ class ProjectName extends PureComponent<Props, State> {
spacing={15}
>
<TextInput
innerRef={node => (this.node = node)}
innerRef={this.setRef}
type="text"
value={randomizedOverrideName || name}
isFocused={isFocused}
Expand Down Expand Up @@ -219,7 +221,7 @@ class ProjectName extends PureComponent<Props, State> {
}
}

const ErrorMessage = styled.div`
export const ErrorMessage = styled.div`
margin-top: 6px;
color: ${COLORS.pink[700]};
`;
Expand Down
42 changes: 22 additions & 20 deletions src/components/CreateNewProjectWizard/ProjectPath.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,28 @@ type Props = {
changeDefaultProjectPath: Dispatch<typeof changeDefaultProjectPath>,
};

class ProjectPath extends PureComponent<Props> {
export const CLAMP_AT = 29;

export const dialogOptions = {
message: 'Select the directory of Project',
properties: ['openDirectory'],
};

export function dialogCallback(paths: ?[string]) {
// The user might cancel out without selecting a directory.
// In that case, do nothing.
if (!paths) {
return;
}

// Only a single path should be selected
const [firstPath] = paths;
this.props.changeDefaultProjectPath(firstPath);
}

export class ProjectPath extends PureComponent<Props> {
updatePath = () => {
remote.dialog.showOpenDialog(
{
message: 'Select the directory of Project',
properties: ['openDirectory'],
},
(paths: ?[string]) => {
// The user might cancel out without selecting a directory.
// In that case, do nothing.
if (!paths) {
return;
}

// Only a single path should be selected
const [firstPath] = paths;
this.props.changeDefaultProjectPath(firstPath);
}
);
remote.dialog.showOpenDialog(dialogOptions, dialogCallback.bind(this));
};

render() {
Expand All @@ -55,7 +58,6 @@ class ProjectPath extends PureComponent<Props> {

// Using CSS text-overflow is proving challenging, so we'll just crop it
// with JS.
const CLAMP_AT = 29;
let displayedProjectPath = fullProjectPath;
if (displayedProjectPath.length > CLAMP_AT) {
displayedProjectPath = `${displayedProjectPath.slice(0, CLAMP_AT - 1)}…`;
Expand All @@ -81,7 +83,7 @@ const MainText = styled.div`
color: ${COLORS.gray[400]};
`;

const DirectoryButton = styled(TextButton)`
export const DirectoryButton = styled(TextButton)`
font-family: 'Fira Mono';
font-size: 12px;
color: ${COLORS.gray[600]};
Expand Down
2 changes: 1 addition & 1 deletion src/components/CreateNewProjectWizard/SubmitButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const SubmitButtonIconWrapper = styled.div`
margin: auto;
`;

const ChildWrapper = styled.div`
export const ChildWrapper = styled.div`
line-height: 48px;
`;

Expand Down
92 changes: 92 additions & 0 deletions src/components/CreateNewProjectWizard/__tests__/BuildPane.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { shallow } from 'enzyme';

import BuildPane, { BUILD_STEP_KEYS } from '../BuildPane';
import createProject from '../../../services/create-project.service';

jest.mock('../../../services/create-project.service');

describe('BuildPane component', () => {
let wrapper;
let instance;
const newProject = {
projectName: 'New project',
projectType: 'create-react-app',
projectIcon: 'icon',
projectStarter: '',
};

const mockHandleCompleteBuild = jest.fn();

jest.useFakeTimers();

const shallowRenderBuildPane = project =>
shallow(
<BuildPane
{...project}
status="filling-in-form"
handleCompleteBuild={mockHandleCompleteBuild}
/>
);

const testOutputs = ['Installing packages', 'Dependencies installed'];

beforeEach(() => {
wrapper = shallowRenderBuildPane(newProject);
instance = wrapper.instance();

// Mock console errors so they don't output while running the test
jest.spyOn(console, 'error');
console.error.mockImplementation(() => {});
});

afterEach(() => {
console.error.mockRestore();
});

BUILD_STEP_KEYS.forEach(buildStep => {
it(`should render build step ${buildStep}`, () => {
wrapper.setProps({
currentBuildStep: buildStep,
});
expect(wrapper).toMatchSnapshot();
});
});

it('should call createProject', () => {
wrapper.setProps({
status: 'building-project',
});
jest.runAllTimers();
expect(createProject).toHaveBeenCalled();
});

it('should throw error if a project prop is missing', () => {
wrapper = shallowRenderBuildPane();
instance = wrapper.instance();
expect(instance.buildProject).toThrow(/insufficient data/i);
});

it('should handle status updated', () => {
// Note: Instance.handleStatusUpdate is called from createProject service
// with data from stdout we're testing this here with a mock array of output strings
testOutputs.forEach(output => {
instance.handleStatusUpdate(output);
expect(instance.state.currentBuildStep).toMatchSnapshot();
});
});

it('should call handleComplete', () => {
// Note: HandleComplete is also called from createProject service
// so it's OK to call this here directly as it's triggered
// once the service finished the work.
instance.handleComplete(newProject);
jest.runAllTimers();
expect(mockHandleCompleteBuild).toHaveBeenCalledWith(newProject);
});

it('should handle error message', () => {
instance.handleError('npx: installed');
expect(instance.state.currentBuildStep).toEqual(BUILD_STEP_KEYS[1]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { shallow } from 'enzyme';

import BuildStepProgress from '../BuildStepProgress';

describe('BuildStepProgress component', () => {
let wrapper;
let instance;

const mockStep = {
copy: 'Building...',
additionalCopy: 'This may take some time',
};

jest.useFakeTimers();

beforeEach(() => {
wrapper = shallow(<BuildStepProgress step={mockStep} status="upcoming" />);
instance = wrapper.instance();
});

it('should render upcoming', () => {
expect(wrapper).toMatchSnapshot();
});

it('should render in-progress', () => {
wrapper.setProps({ status: 'in-progress' });
expect(wrapper).toMatchSnapshot();
});

it('should render done', () => {
wrapper.setProps({ status: 'done' });
expect(wrapper).toMatchSnapshot();
});

describe('Component logic', () => {
it('should hide additional copy if done', () => {
wrapper.setState({
shouldShowAdditionalCopy: true,
});
wrapper.setProps({
status: 'done',
});
expect(instance.state.shouldShowAdditionalCopy).toBeFalsy();
});

it('should show additonal copy after a delay', () => {
wrapper.setProps({
status: 'in-progress',
});
jest.runAllTimers();
expect(instance.state.shouldShowAdditionalCopy).toBeTruthy();
});

it('should clear timeout on unmount', () => {
window.clearTimeout = jest.fn();
instance.componentWillUnmount();
expect(window.clearTimeout).toHaveBeenCalledWith(instance.timeoutId);
});
});
});
Loading

0 comments on commit 9028054

Please sign in to comment.