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`] = ` -
- +
+ `; 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 &&