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

Test CreateNewProjectWizard components #361

Merged
merged 8 commits into from
Mar 26, 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
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