diff --git a/client/components/__test__/FileNode.test.jsx b/client/components/__test__/FileNode.test.jsx
deleted file mode 100644
index b78d6ab2..00000000
--- a/client/components/__test__/FileNode.test.jsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { FileNode } from '../../modules/IDE/components/FileNode';
-
-describe('', () => {
- let component;
- let props = {};
- let input;
- let renameTriggerButton;
- const changeName = (newFileName) => {
- renameTriggerButton.simulate('click');
- input.simulate('change', { target: { value: newFileName } });
- input.simulate('blur');
- };
- const getState = () => component.state();
- const getUpdatedName = () => getState().updatedName;
-
- describe('with valid props, regardless of filetype', () => {
- ['folder', 'file'].forEach((fileType) => {
- beforeEach(() => {
- props = {
- ...props,
- id: '0',
- name: 'test.jsx',
- fileType,
- canEdit: true,
- children: [],
- authenticated: false,
- setSelectedFile: jest.fn(),
- deleteFile: jest.fn(),
- updateFileName: jest.fn(),
- resetSelectedFile: jest.fn(),
- newFile: jest.fn(),
- newFolder: jest.fn(),
- showFolderChildren: jest.fn(),
- hideFolderChildren: jest.fn(),
- openUploadFileModal: jest.fn(),
- setProjectName: jest.fn(),
- };
- component = shallow();
- });
-
- describe('when changing name', () => {
- beforeEach(() => {
- input = component.find('.sidebar__file-item-input');
- renameTriggerButton = component
- .find('.sidebar__file-item-option')
- .first();
- component.setState({ isEditing: true });
- });
-
- describe('to an empty name', () => {
- const newName = '';
- beforeEach(() => changeName(newName));
-
- it('should not save', () => expect(props.updateFileName).not.toHaveBeenCalled());
- it('should reset name', () => expect(getUpdatedName()).toEqual(props.name));
- });
- });
- });
- });
-
- describe('as file with valid props', () => {
- beforeEach(() => {
- props = {
- ...props,
- id: '0',
- name: 'test.jsx',
- fileType: 'file',
- canEdit: true,
- children: [],
- authenticated: false,
- setSelectedFile: jest.fn(),
- deleteFile: jest.fn(),
- updateFileName: jest.fn(),
- resetSelectedFile: jest.fn(),
- newFile: jest.fn(),
- newFolder: jest.fn(),
- showFolderChildren: jest.fn(),
- hideFolderChildren: jest.fn(),
- openUploadFileModal: jest.fn()
- };
- component = shallow();
- });
-
- describe('when changing name', () => {
- beforeEach(() => {
- input = component.find('.sidebar__file-item-input');
- renameTriggerButton = component
- .find('.sidebar__file-item-option')
- .first();
- component.setState({ isEditing: true });
- });
- it('should render', () => expect(component).toBeDefined());
-
- // it('should debug', () => console.log(component.debug()));
-
- describe('to a valid filename', () => {
- const newName = 'newname.jsx';
- beforeEach(() => changeName(newName));
-
- it('should save the name', () => {
- expect(props.updateFileName).toBeCalledWith(props.id, newName);
- });
- });
-
- // Failure Scenarios
-
- describe('to an extensionless filename', () => {
- const newName = 'extensionless';
- beforeEach(() => changeName(newName));
- });
- it('should not save', () => expect(props.setProjectName).not.toHaveBeenCalled());
- it('should reset name', () => expect(getUpdatedName()).toEqual(props.name));
- describe('to different extension', () => {
- const newName = 'name.gif';
- beforeEach(() => changeName(newName));
-
- it('should not save', () => expect(props.setProjectName).not.toHaveBeenCalled());
- it('should reset name', () => expect(getUpdatedName()).toEqual(props.name));
- });
-
- describe('to just an extension', () => {
- const newName = '.jsx';
- beforeEach(() => changeName(newName));
-
- it('should not save', () => expect(props.updateFileName).not.toHaveBeenCalled());
- it('should reset name', () => expect(getUpdatedName()).toEqual(props.name));
- });
- });
- });
-
-
- describe('as folder with valid props', () => {
- beforeEach(() => {
- props = {
- ...props,
- id: '0',
- children: [],
- name: 'filename',
- fileType: 'folder',
- canEdit: true,
- authenticated: false,
- setSelectedFile: jest.fn(),
- deleteFile: jest.fn(),
- updateFileName: jest.fn(),
- resetSelectedFile: jest.fn(),
- newFile: jest.fn(),
- newFolder: jest.fn(),
- showFolderChildren: jest.fn(),
- hideFolderChildren: jest.fn(),
- openUploadFileModal: jest.fn()
- };
- component = shallow();
- });
-
- describe('when changing name', () => {
- beforeEach(() => {
- input = component.find('.sidebar__file-item-input');
- renameTriggerButton = component
- .find('.sidebar__file-item-option')
- .first();
- component.setState({ isEditing: true });
- });
-
- describe('to a foldername', () => {
- const newName = 'newfoldername';
- beforeEach(() => changeName(newName));
-
- it('should save', () => expect(props.updateFileName).toBeCalledWith(props.id, newName));
- it('should update name', () => expect(getUpdatedName()).toEqual(newName));
- });
-
- describe('to a filename', () => {
- const newName = 'filename.jsx';
- beforeEach(() => changeName(newName));
-
- it('should not save', () => expect(props.updateFileName).not.toHaveBeenCalled());
- it('should reset name', () => expect(getUpdatedName()).toEqual(props.name));
- });
- });
- });
-});
diff --git a/client/components/__test__/Nav.test.jsx b/client/components/__test__/Nav.test.jsx
index f9261cfc..8ddb6449 100644
--- a/client/components/__test__/Nav.test.jsx
+++ b/client/components/__test__/Nav.test.jsx
@@ -1,9 +1,9 @@
import React from 'react';
-import { shallow } from 'enzyme';
-import renderer from 'react-test-renderer';
+import { render } from '@testing-library/react';
-import { NavComponent } from './../Nav';
+
+import { NavComponent } from '../Nav';
describe('Nav', () => {
const props = {
@@ -46,17 +46,9 @@ describe('Nav', () => {
id: 'root-file'
}
};
- const getWrapper = () => shallow();
-
- test('it renders main navigation', () => {
- const nav = getWrapper();
- expect(nav.exists('.nav')).toEqual(true);
- });
it('renders correctly', () => {
- const tree = renderer
- .create()
- .toJSON();
- expect(tree).toMatchSnapshot();
+ const { asFragment } = render();
+ expect(asFragment()).toMatchSnapshot();
});
});
diff --git a/client/components/__test__/Toolbar.test.jsx b/client/components/__test__/Toolbar.test.jsx
deleted file mode 100644
index a64f6f2d..00000000
--- a/client/components/__test__/Toolbar.test.jsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { ToolbarComponent } from '../../modules/IDE/components/Toolbar';
-
-
-const initialProps = {
- isPlaying: false,
- preferencesIsVisible: false,
- stopSketch: jest.fn(),
- setProjectName: jest.fn(),
- openPreferences: jest.fn(),
- showEditProjectName: jest.fn(),
- hideEditProjectName: jest.fn(),
- infiniteLoop: false,
- autorefresh: false,
- setAutorefresh: jest.fn(),
- setTextOutput: jest.fn(),
- setGridOutput: jest.fn(),
- startSketch: jest.fn(),
- startAccessibleSketch: jest.fn(),
- saveProject: jest.fn(),
- currentUser: 'me',
- originalProjectName: 'testname',
-
- owner: {
- username: 'me'
- },
- project: {
- name: 'testname',
- isEditingName: false,
- id: 'id',
- },
-};
-
-
-describe('', () => {
- let component;
- let props = initialProps;
- let input;
- let renameTriggerButton;
- const changeName = (newFileName) => {
- component.find('.toolbar__project-name').simulate('click', { preventDefault: jest.fn() });
- input = component.find('.toolbar__project-name-input');
- renameTriggerButton = component.find('.toolbar__edit-name-button');
- renameTriggerButton.simulate('click');
- input.simulate('change', { target: { value: newFileName } });
- input.simulate('blur');
- };
- const setProps = (additionalProps) => {
- props = {
- ...props,
- ...additionalProps,
-
- project: {
- ...props.project,
- ...(additionalProps || {}).project
- },
- };
- };
-
- // Test Cases
-
- describe('with valid props', () => {
- beforeEach(() => {
- setProps();
- component = shallow();
- });
- it('renders', () => expect(component).toBeDefined());
-
- describe('when use owns sketch', () => {
- beforeEach(() => setProps({ currentUser: props.owner.username }));
-
- describe('when changing sketch name', () => {
- beforeEach(() => {
- setProps({
- project: { isEditingName: true, name: 'testname' },
- setProjectName: jest.fn(name => component.setProps({ project: { name } })),
- });
- component = shallow();
- });
-
- describe('to a valid name', () => {
- beforeEach(() => changeName('hello'));
- it('should save', () => expect(props.setProjectName).toBeCalledWith('hello'));
- });
-
-
- describe('to an empty name', () => {
- beforeEach(() => changeName(''));
- it('should set name to empty', () => expect(props.setProjectName).toBeCalledWith(''));
- it(
- 'should detect empty name and revert to original',
- () => expect(props.setProjectName).toHaveBeenLastCalledWith(initialProps.project.name)
- );
- });
- });
- });
-
- describe('when user does not own sketch', () => {
- beforeEach(() => setProps({ currentUser: 'not-the-owner' }));
-
- it('should disable edition', () => expect(component.find('.toolbar__edit-name-button')).toEqual({}));
- });
- });
-});
diff --git a/client/components/__test__/__snapshots__/Nav.test.jsx.snap b/client/components/__test__/__snapshots__/Nav.test.jsx.snap
index 592fc282..e041a4f6 100644
--- a/client/components/__test__/__snapshots__/Nav.test.jsx.snap
+++ b/client/components/__test__/__snapshots__/Nav.test.jsx.snap
@@ -1,346 +1,253 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Nav renders correctly 1`] = `
-
-
-
+
+ About
+
+
+
+
+
+
+
+
`;
diff --git a/client/jest.setup.js b/client/jest.setup.js
index 8fc9b11f..79652c74 100644
--- a/client/jest.setup.js
+++ b/client/jest.setup.js
@@ -1,8 +1,5 @@
-
-// eslint-disable-next-line import/no-extraneous-dependencies
-import { configure } from 'enzyme';
-// eslint-disable-next-line import/no-extraneous-dependencies
-import Adapter from 'enzyme-adapter-react-16';
import '@babel/polyfill';
-configure({ adapter: new Adapter() });
+// See: https://github.com/testing-library/jest-dom
+// eslint-disable-next-line import/no-extraneous-dependencies
+import '@testing-library/jest-dom';
diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx
index bd077874..57238cf8 100644
--- a/client/modules/IDE/components/FileNode.jsx
+++ b/client/modules/IDE/components/FileNode.jsx
@@ -206,12 +206,14 @@ export class FileNode extends React.Component {
}
(
+
+);
diff --git a/client/modules/IDE/components/FileNode.test.jsx b/client/modules/IDE/components/FileNode.test.jsx
new file mode 100644
index 00000000..cc913858
--- /dev/null
+++ b/client/modules/IDE/components/FileNode.test.jsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
+import { FileNode } from './FileNode';
+
+describe('', () => {
+ const changeName = (newFileName) => {
+ const renameButton = screen.getByText(/Rename/i);
+ fireEvent.click(renameButton);
+
+ const input = screen.getByTestId('input');
+ fireEvent.change(input, { target: { value: newFileName } });
+ fireEvent.blur(input);
+ };
+
+ const expectFileNameToBe = async (expectedName) => {
+ const name = screen.getByLabelText(/Name/i);
+ await waitFor(() => within(name).queryByText(expectedName));
+ };
+
+ const renderFileNode = (fileType, extraProps = {}) => {
+ const props = {
+ ...extraProps,
+ id: '0',
+ name: fileType === 'folder' ? 'afolder' : 'test.jsx',
+ fileType,
+ canEdit: true,
+ children: [],
+ authenticated: false,
+ setSelectedFile: jest.fn(),
+ deleteFile: jest.fn(),
+ updateFileName: jest.fn(),
+ resetSelectedFile: jest.fn(),
+ newFile: jest.fn(),
+ newFolder: jest.fn(),
+ showFolderChildren: jest.fn(),
+ hideFolderChildren: jest.fn(),
+ openUploadFileModal: jest.fn(),
+ setProjectName: jest.fn(),
+ };
+
+ render();
+
+ return props;
+ };
+
+ describe('fileType: file', () => {
+ it('cannot change to an empty name', async () => {
+ const props = renderFileNode('file');
+
+ changeName('');
+
+ await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+ await expectFileNameToBe(props.name);
+ });
+
+ it('can change to a valid filename', async () => {
+ const newName = 'newname.jsx';
+ const props = renderFileNode('file');
+
+ changeName(newName);
+
+ await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName));
+ await expectFileNameToBe(newName);
+ });
+
+ it('must have an extension', async () => {
+ const newName = 'newname';
+ const props = renderFileNode('file');
+
+ changeName(newName);
+
+ await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+ await expectFileNameToBe(props.name);
+ });
+
+ it('can change to a different extension', async () => {
+ const newName = 'newname.gif';
+ const props = renderFileNode('file');
+
+ changeName(newName);
+
+ await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+ await expectFileNameToBe(props.name);
+ });
+
+ it('cannot be just an extension', async () => {
+ const newName = '.jsx';
+ const props = renderFileNode('file');
+
+ changeName(newName);
+
+ await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+ await expectFileNameToBe(props.name);
+ });
+ });
+
+ describe('fileType: folder', () => {
+ it('cannot change to an empty name', async () => {
+ const props = renderFileNode('folder');
+
+ changeName('');
+
+ await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+ await expectFileNameToBe(props.name);
+ });
+
+ it('can change to another name', async () => {
+ const newName = 'foldername';
+ const props = renderFileNode('folder');
+
+ changeName(newName);
+
+ await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName));
+ await expectFileNameToBe(newName);
+ });
+
+ it('cannot have a file extension', async () => {
+ const newName = 'foldername.jsx';
+ const props = renderFileNode('folder');
+
+ changeName(newName);
+
+ await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+ await expectFileNameToBe(props.name);
+ });
+ });
+});
diff --git a/client/modules/IDE/components/Toolbar.jsx b/client/modules/IDE/components/Toolbar.jsx
index b3d13364..401a7e76 100644
--- a/client/modules/IDE/components/Toolbar.jsx
+++ b/client/modules/IDE/components/Toolbar.jsx
@@ -17,21 +17,36 @@ class Toolbar extends React.Component {
super(props);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleProjectNameChange = this.handleProjectNameChange.bind(this);
+ this.handleProjectNameSave = this.handleProjectNameSave.bind(this);
+
+ this.state = {
+ projectNameInputValue: props.project.name,
+ };
}
handleKeyPress(event) {
if (event.key === 'Enter') {
this.props.hideEditProjectName();
+ this.projectNameInput.blur();
}
}
handleProjectNameChange(event) {
- this.props.setProjectName(event.target.value);
+ this.setState({ projectNameInputValue: event.target.value });
}
- validateProjectName() {
- if ((this.props.project.name.trim()).length === 0) {
- this.props.setProjectName(this.originalProjectName);
+ handleProjectNameSave() {
+ const newProjectName = this.state.projectNameInputValue.trim();
+ if (newProjectName.length === 0) {
+ this.setState({
+ projectNameInputValue: this.props.project.name,
+ });
+ } else {
+ this.props.setProjectName(newProjectName);
+ this.props.hideEditProjectName();
+ if (this.props.project.id) {
+ this.props.saveProject();
+ }
}
}
@@ -108,7 +123,6 @@ class Toolbar extends React.Component {
className="toolbar__project-name"
onClick={() => {
if (canEditProjectName) {
- this.originalProjectName = this.props.project.name;
this.props.showEditProjectName();
setTimeout(() => this.projectNameInput.focus(), 0);
}
@@ -130,16 +144,11 @@ class Toolbar extends React.Component {
type="text"
maxLength="128"
className="toolbar__project-name-input"
- value={this.props.project.name}
+ aria-label="New sketch name"
+ value={this.state.projectNameInputValue}
onChange={this.handleProjectNameChange}
ref={(element) => { this.projectNameInput = element; }}
- onBlur={() => {
- this.validateProjectName();
- this.props.hideEditProjectName();
- if (this.props.project.id) {
- this.props.saveProject();
- }
- }}
+ onBlur={this.handleProjectNameSave}
onKeyPress={this.handleKeyPress}
/>
{(() => { // eslint-disable-line
diff --git a/client/modules/IDE/components/Toolbar.test.jsx b/client/modules/IDE/components/Toolbar.test.jsx
new file mode 100644
index 00000000..d558fc46
--- /dev/null
+++ b/client/modules/IDE/components/Toolbar.test.jsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import lodash from 'lodash';
+
+
+import { ToolbarComponent } from './Toolbar';
+
+const renderComponent = (extraProps = {}) => {
+ const props = lodash.merge({
+ isPlaying: false,
+ preferencesIsVisible: false,
+ stopSketch: jest.fn(),
+ setProjectName: jest.fn(),
+ openPreferences: jest.fn(),
+ showEditProjectName: jest.fn(),
+ hideEditProjectName: jest.fn(),
+ infiniteLoop: false,
+ autorefresh: false,
+ setAutorefresh: jest.fn(),
+ setTextOutput: jest.fn(),
+ setGridOutput: jest.fn(),
+ startSketch: jest.fn(),
+ startAccessibleSketch: jest.fn(),
+ saveProject: jest.fn(),
+ currentUser: 'me',
+ originalProjectName: 'testname',
+
+ owner: {
+ username: 'me'
+ },
+ project: {
+ name: 'testname',
+ isEditingName: false,
+ id: 'id',
+ },
+ }, extraProps);
+
+ render();
+
+ return props;
+};
+
+describe('', () => {
+ it('sketch owner can switch to sketch name editing mode', async () => {
+ const props = renderComponent();
+ const sketchName = screen.getByLabelText('Edit sketch name');
+
+ fireEvent.click(sketchName);
+
+ await waitFor(() => expect(props.showEditProjectName).toHaveBeenCalled());
+ });
+
+ it('non-owner can\t switch to sketch editing mode', async () => {
+ const props = renderComponent({ currentUser: 'not-me' });
+ const sketchName = screen.getByLabelText('Edit sketch name');
+
+ fireEvent.click(sketchName);
+
+ expect(sketchName).toBeDisabled();
+ await waitFor(() => expect(props.showEditProjectName).not.toHaveBeenCalled());
+ });
+
+ it('sketch owner can change name', async () => {
+ const props = renderComponent({ project: { isEditingName: true } });
+
+ const sketchNameInput = screen.getByLabelText('New sketch name');
+ fireEvent.change(sketchNameInput, { target: { value: 'my new sketch name' } });
+ fireEvent.blur(sketchNameInput);
+
+ await waitFor(() => expect(props.setProjectName).toHaveBeenCalledWith('my new sketch name'));
+ await waitFor(() => expect(props.saveProject).toHaveBeenCalled());
+ });
+
+ it('sketch owner can\'t change to empty name', async () => {
+ const props = renderComponent({ project: { isEditingName: true } });
+
+ const sketchNameInput = screen.getByLabelText('New sketch name');
+ fireEvent.change(sketchNameInput, { target: { value: '' } });
+ fireEvent.blur(sketchNameInput);
+
+ await waitFor(() => expect(props.setProjectName).not.toHaveBeenCalled());
+ await waitFor(() => expect(props.saveProject).not.toHaveBeenCalled());
+ });
+});
diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx
index 9ff8351d..6cc43a58 100644
--- a/client/modules/IDE/pages/IDEView.jsx
+++ b/client/modules/IDE/pages/IDEView.jsx
@@ -215,7 +215,7 @@ class IDEView extends React.Component {
warnIfUnsavedChanges={this.warnIfUnsavedChanges}
cmController={this.cmController}
/>
-
+
{this.props.ide.preferencesIsVisible &&