diff --git a/client/modules/IDE/components/EditorAccessibility.unit.test.jsx b/client/modules/IDE/components/EditorAccessibility.unit.test.jsx
index ca6dfd868..9e4e8463b 100644
--- a/client/modules/IDE/components/EditorAccessibility.unit.test.jsx
+++ b/client/modules/IDE/components/EditorAccessibility.unit.test.jsx
@@ -6,7 +6,7 @@ import EditorAccessibility from './EditorAccessibility';
describe('', () => {
it('renders empty message with no lines', () => {
- render();
+ render();
expect(
screen.getByRole('listitem', {
@@ -21,11 +21,12 @@ describe('', () => {
lintMessages={[
{
severity: 'info',
- line: '1',
+ line: 1,
message: 'foo',
- id: '1a2b3c'
+ id: 123
}
]}
+ currentLine={0}
/>
);
diff --git a/client/modules/IDE/components/ErrorModal.unit.test.jsx b/client/modules/IDE/components/ErrorModal.unit.test.jsx
index 541276193..9eab7d674 100644
--- a/client/modules/IDE/components/ErrorModal.unit.test.jsx
+++ b/client/modules/IDE/components/ErrorModal.unit.test.jsx
@@ -8,20 +8,30 @@ jest.mock('../../../i18n');
describe('', () => {
it('renders type forceAuthentication', () => {
- render();
+ render(
+
+ );
expect(screen.getByText('Login')).toBeVisible();
expect(screen.getByText('Sign Up')).toBeVisible();
});
it('renders type staleSession', () => {
- render();
+ render(
+
+ );
expect(screen.getByText('log in')).toBeVisible();
});
it('renders type staleProject', () => {
- render();
+ render(
+
+ );
expect(
screen.getByText(
diff --git a/client/modules/IDE/components/FileNode.unit.test.jsx b/client/modules/IDE/components/FileNode.unit.test.jsx
index 3f6706a2c..8676a817d 100644
--- a/client/modules/IDE/components/FileNode.unit.test.jsx
+++ b/client/modules/IDE/components/FileNode.unit.test.jsx
@@ -83,12 +83,18 @@ describe('', () => {
});
it('can change to a different extension', async () => {
+ const mockConfirm = jest.fn(() => true);
+ window.confirm = mockConfirm;
+
const newName = 'newname.gif';
const props = renderFileNode('file');
changeName(newName);
- await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+ expect(mockConfirm).toHaveBeenCalled();
+ await waitFor(() =>
+ expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
+ );
await expectFileNameToBe(props.name);
});
diff --git a/client/modules/User/components/AccountForm.unit.test.jsx b/client/modules/User/components/AccountForm.unit.test.jsx
new file mode 100644
index 000000000..653dce524
--- /dev/null
+++ b/client/modules/User/components/AccountForm.unit.test.jsx
@@ -0,0 +1,122 @@
+import React from 'react';
+import thunk from 'redux-thunk';
+import configureStore from 'redux-mock-store';
+import {
+ reduxRender,
+ screen,
+ fireEvent,
+ act,
+ waitFor
+} from '../../../test-utils';
+import AccountForm from './AccountForm';
+import { initialTestState } from '../../../testData/testReduxStore';
+import * as actions from '../actions';
+
+const mockStore = configureStore([thunk]);
+const store = mockStore(initialTestState);
+
+jest.mock('../actions', () => ({
+ ...jest.requireActual('../actions'),
+ updateSettings: jest.fn().mockReturnValue(
+ (dispatch) =>
+ new Promise((resolve) => {
+ setTimeout(() => {
+ dispatch({ type: 'UPDATE_SETTINGS', payload: {} });
+ resolve();
+ }, 100);
+ })
+ )
+}));
+
+const subject = () => {
+ reduxRender(, {
+ store
+ });
+};
+
+describe('', () => {
+ it('renders form fields with initial values', () => {
+ subject();
+ const emailElement = screen.getByRole('textbox', {
+ name: /email/i
+ });
+ expect(emailElement).toBeInTheDocument();
+ expect(emailElement).toHaveValue('happydog@example.com');
+
+ const userNameElement = screen.getByRole('textbox', {
+ name: /username/i
+ });
+ expect(userNameElement).toBeInTheDocument();
+ expect(userNameElement).toHaveValue('happydog');
+
+ const currentPasswordElement = screen.getByLabelText(/current password/i);
+ expect(currentPasswordElement).toBeInTheDocument();
+ expect(currentPasswordElement).toHaveValue('');
+
+ const newPasswordElement = screen.getByLabelText(/new password/i);
+ expect(newPasswordElement).toBeInTheDocument();
+ expect(newPasswordElement).toHaveValue('');
+ });
+
+ it('handles form submission and calls updateSettings', async () => {
+ subject();
+
+ const saveAllSettingsButton = screen.getByRole('button', {
+ name: /save all settings/i
+ });
+
+ const currentPasswordElement = screen.getByLabelText(/current password/i);
+ const newPasswordElement = screen.getByLabelText(/new password/i);
+
+ fireEvent.change(currentPasswordElement, {
+ target: {
+ value: 'currentPassword'
+ }
+ });
+
+ fireEvent.change(newPasswordElement, {
+ target: {
+ value: 'newPassword'
+ }
+ });
+
+ await act(async () => {
+ fireEvent.click(saveAllSettingsButton);
+ });
+
+ await waitFor(() => {
+ expect(actions.updateSettings).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('Save all setting button should get disabled while submitting and enable when not submitting', async () => {
+ subject();
+
+ const saveAllSettingsButton = screen.getByRole('button', {
+ name: /save all settings/i
+ });
+
+ const currentPasswordElement = screen.getByLabelText(/current password/i);
+ const newPasswordElement = screen.getByLabelText(/new password/i);
+
+ fireEvent.change(currentPasswordElement, {
+ target: {
+ value: 'currentPassword'
+ }
+ });
+
+ fireEvent.change(newPasswordElement, {
+ target: {
+ value: 'newPassword'
+ }
+ });
+ expect(saveAllSettingsButton).not.toHaveAttribute('disabled');
+
+ await act(async () => {
+ fireEvent.click(saveAllSettingsButton);
+ await waitFor(() => {
+ expect(saveAllSettingsButton).toHaveAttribute('disabled');
+ });
+ });
+ });
+});
diff --git a/client/modules/User/components/LoginForm.unit.test.jsx b/client/modules/User/components/LoginForm.unit.test.jsx
new file mode 100644
index 000000000..28fb9383c
--- /dev/null
+++ b/client/modules/User/components/LoginForm.unit.test.jsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import thunk from 'redux-thunk';
+import configureStore from 'redux-mock-store';
+import LoginForm from './LoginForm';
+import * as actions from '../actions';
+import { initialTestState } from '../../../testData/testReduxStore';
+import { reduxRender, screen, fireEvent, act } from '../../../test-utils';
+
+const mockStore = configureStore([thunk]);
+const store = mockStore(initialTestState);
+
+jest.mock('../actions', () => ({
+ ...jest.requireActual('../actions'),
+ validateAndLoginUser: jest.fn().mockReturnValue(
+ (dispatch) =>
+ new Promise((resolve) => {
+ setTimeout(() => {
+ dispatch({ type: 'AUTH_USER', payload: {} });
+ dispatch({ type: 'SET_PREFERENCES', payload: {} });
+ resolve();
+ }, 100);
+ })
+ )
+}));
+
+const subject = () => {
+ reduxRender(, {
+ store
+ });
+};
+
+describe('', () => {
+ test('Renders form with the correct fields.', () => {
+ subject();
+ const emailTextElement = screen.getByText(/email or username/i);
+ expect(emailTextElement).toBeInTheDocument();
+
+ const emailInputElement = screen.getByRole('textbox', {
+ name: /email or username/i
+ });
+ expect(emailInputElement).toBeInTheDocument();
+
+ const passwordTextElement = screen.getByText(/password/i);
+ expect(passwordTextElement).toBeInTheDocument();
+
+ const passwordInputElement = screen.getByLabelText(/password/i);
+ expect(passwordInputElement).toBeInTheDocument();
+
+ const loginButtonElement = screen.getByRole('button', {
+ name: /log in/i
+ });
+ expect(loginButtonElement).toBeInTheDocument();
+ });
+ test('Validate and login user is called with the correct values.', async () => {
+ subject();
+
+ const emailElement = screen.getByRole('textbox', {
+ name: /email or username/i
+ });
+ fireEvent.change(emailElement, {
+ target: {
+ value: 'correctuser@gmail.com'
+ }
+ });
+
+ const passwordElement = screen.getByLabelText(/password/i);
+
+ fireEvent.change(passwordElement, {
+ target: {
+ value: 'correctpassword'
+ }
+ });
+
+ const loginButton = screen.getByRole('button', {
+ name: /log in/i
+ });
+ await act(async () => {
+ fireEvent.click(loginButton);
+ });
+
+ expect(actions.validateAndLoginUser).toHaveBeenCalledWith({
+ email: 'correctuser@gmail.com',
+ password: 'correctpassword'
+ });
+ });
+});
diff --git a/client/modules/User/components/NewPasswordForm.unit.test.jsx b/client/modules/User/components/NewPasswordForm.unit.test.jsx
new file mode 100644
index 000000000..dbddf3c8c
--- /dev/null
+++ b/client/modules/User/components/NewPasswordForm.unit.test.jsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import thunk from 'redux-thunk';
+import configureStore from 'redux-mock-store';
+import { fireEvent } from '@storybook/testing-library';
+import { reduxRender, screen, act, waitFor } from '../../../test-utils';
+import { initialTestState } from '../../../testData/testReduxStore';
+import NewPasswordForm from './NewPasswordForm';
+
+const mockStore = configureStore([thunk]);
+const store = mockStore(initialTestState);
+
+const mockResetPasswordToken = 'mockResetToken';
+const subject = () => {
+ reduxRender(, {
+ store
+ });
+};
+const mockDispatch = jest.fn();
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useDispatch: () => mockDispatch
+}));
+
+jest.mock('../../../utils/reduxFormUtils', () => ({
+ validateNewPassword: jest.fn()
+}));
+
+jest.mock('../actions', () => ({
+ updatePassword: jest.fn().mockReturnValue(
+ new Promise((resolve) => {
+ resolve();
+ })
+ )
+}));
+
+describe('', () => {
+ beforeEach(() => {
+ mockDispatch.mockClear();
+ jest.clearAllMocks();
+ });
+ test('renders form fields correctly', () => {
+ subject();
+
+ const passwordInputElements = screen.getAllByLabelText(/Password/i);
+ expect(passwordInputElements).toHaveLength(2);
+
+ const passwordInputElement = passwordInputElements[0];
+ expect(passwordInputElement).toBeInTheDocument();
+
+ const confirmPasswordInputElement = passwordInputElements[1];
+ expect(confirmPasswordInputElement).toBeInTheDocument();
+
+ const submitElemement = screen.getByRole('button', {
+ name: /set new password/i
+ });
+ expect(submitElemement).toBeInTheDocument();
+ });
+
+ test('submits form with valid data', async () => {
+ subject();
+ const passwordInputElements = screen.getAllByLabelText(/Password/i);
+
+ const passwordInputElement = passwordInputElements[0];
+ fireEvent.change(passwordInputElement, {
+ target: { value: 'password123' }
+ });
+
+ const confirmPasswordInputElement = passwordInputElements[1];
+ fireEvent.change(confirmPasswordInputElement, {
+ target: { value: 'password123' }
+ });
+
+ const submitElemement = screen.getByRole('button', {
+ name: /set new password/i
+ });
+
+ await act(async () => {
+ fireEvent.click(submitElemement);
+ });
+
+ await waitFor(() => expect(mockDispatch).toHaveBeenCalled());
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index 388e80ad9..de7ae82e8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "p5.js-web-editor",
- "version": "2.14.3",
+ "version": "2.14.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "p5.js-web-editor",
- "version": "2.14.3",
+ "version": "2.14.5",
"license": "LGPL-2.1",
"dependencies": {
"@auth0/s3": "^1.0.0",
diff --git a/package.json b/package.json
index 33f5e2ce2..c9f5edcab 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "p5.js-web-editor",
- "version": "2.14.3",
+ "version": "2.14.5",
"description": "The web editor for p5.js.",
"scripts": {
"clean": "rimraf dist",
diff --git a/server/controllers/project.controller/__test__/createProject.test.js b/server/controllers/project.controller/__test__/createProject.test.js
index 8ab179dd3..8a7ea8f67 100644
--- a/server/controllers/project.controller/__test__/createProject.test.js
+++ b/server/controllers/project.controller/__test__/createProject.test.js
@@ -179,6 +179,10 @@ describe('project.controller', () => {
});
it('fails if user does not have permission', async () => {
+ // We don't want to clog up the jest output with extra
+ // logs, so we turn off console warn for this one test.
+ jest.spyOn(console, 'warn').mockImplementation(() => {});
+
request.user = { _id: 'abc123', username: 'alice' };
request.params = {
username: 'dana'
diff --git a/server/controllers/project.controller/createProject.js b/server/controllers/project.controller/createProject.js
index 75e8ce84d..021fbb21f 100644
--- a/server/controllers/project.controller/createProject.js
+++ b/server/controllers/project.controller/createProject.js
@@ -59,7 +59,7 @@ export async function apiCreateProject(req, res) {
const checkUserHasPermission = () => {
if (req.user.username !== req.params.username) {
- console.log('no permission');
+ console.warn('no permission');
const error = new ProjectValidationError(
`'${req.user.username}' does not have permission to create for '${req.params.username}'`
);
diff --git a/server/previewServer.js b/server/previewServer.js
index 754775645..3c07cd5d5 100644
--- a/server/previewServer.js
+++ b/server/previewServer.js
@@ -19,9 +19,13 @@ const mongoConnectionString = process.env.MONGO_URL;
// Connect to MongoDB
const connectToMongoDB = async () => {
try {
+ mongoose.set('strictQuery', true);
+
await mongoose.connect(mongoConnectionString, {
useNewUrlParser: true,
- useUnifiedTopology: true
+ useUnifiedTopology: true,
+ serverSelectionTimeoutMS: 30000, // 30 seconds timeout
+ socketTimeoutMS: 45000 // 45 seconds timeout
});
} catch (error) {
console.error('Failed to connect to MongoDB: ', error);
@@ -31,7 +35,6 @@ const connectToMongoDB = async () => {
connectToMongoDB();
-mongoose.set('strictQuery', true);
mongoose.connection.on('error', () => {
console.error(
'MongoDB Connection Error. Please make sure that MongoDB is running.'
diff --git a/server/server.js b/server/server.js
index 64affcf50..8b4d19645 100644
--- a/server/server.js
+++ b/server/server.js
@@ -73,6 +73,18 @@ app.options('*', corsMiddleware);
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(bodyParser.json({ limit: '50mb' }));
app.use(cookieParser());
+
+mongoose.set('strictQuery', true);
+
+const clientPromise = mongoose
+ .connect(mongoConnectionString, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true,
+ serverSelectionTimeoutMS: 30000, // 30 seconds timeout
+ socketTimeoutMS: 45000 // 45 seconds timeout
+ })
+ .then((m) => m.connection.getClient());
+
app.use(
session({
resave: true,
@@ -85,7 +97,7 @@ app.use(
secure: false
},
store: new MongoStore({
- mongooseConnection: mongoose.connection,
+ clientPromise,
autoReconnect: true
})
})
@@ -151,29 +163,6 @@ app.use('/', passportRoutes);
// configure passport
require('./config/passport');
-// Connect to MongoDB
-const connectToMongoDB = async () => {
- try {
- await mongoose.connect(mongoConnectionString, {
- useNewUrlParser: true,
- useUnifiedTopology: true
- });
- } catch (error) {
- console.error('Failed to connect to MongoDB: ', error);
- process.exit(1);
- }
-};
-
-connectToMongoDB();
-
-mongoose.set('strictQuery', true);
-mongoose.connection.on('error', () => {
- console.error(
- 'MongoDB Connection Error. Please make sure that MongoDB is running.'
- );
- process.exit(1);
-});
-
app.get('/', (req, res) => {
res.sendFile(renderIndex());
});