diff --git a/e2e/test.ts b/e2e/test.ts index 6025078ab..b36b17de9 100644 --- a/e2e/test.ts +++ b/e2e/test.ts @@ -1,46 +1,136 @@ import 'regenerator-runtime/runtime'; import path from 'path'; import rimraf from 'rimraf'; +import randomString from '../lib/utils/crypto-random-string'; import { Application } from 'spectron'; +const TEST_USERNAME = `sptest-${randomString(16)}@test.localhost.localdomain`; +const TEST_PASSWORD = randomString(22); +console.info( + `Creating user:\n email: ${TEST_USERNAME}\n password: ${TEST_PASSWORD}` +); + +const el = (app: Application, selector: string) => app.client.$(selector); const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +const waitFor = async (app: Application, selector: string, msTimeout = 10000) => + expect(await app.client.waitForExist(selector, msTimeout)).toBe(true); +const waitForEvent = async ( + app: Application, + eventName: string, + msTimeout = 10000 +): Promise => { + const tic = Date.now(); + + return new Promise((resolve, reject) => { + const f = async () => { + const result = await app.client.execute(function() { + var events = window.testEvents; + + if (!events.length) { + return undefined; + } + + window.testEvents = []; + return events; + }); + + const firstOfType = + result.value && + result.value.findIndex( + (event: string | [string, ...any[]]) => + event === eventName || event[0] === eventName + ); + + if (result.value && firstOfType > -1) { + resolve( + 'string' === typeof result.value + ? [] + : result.value[firstOfType].slice(1) + ); + } else if (Date.now() - tic < msTimeout) { + setTimeout(f, 100); + } else { + reject(); + } + }; + + f(); + }); +}; const app: Application = new Application({ path: path.join(__dirname, '../node_modules/.bin/electron'), args: [path.join(__dirname, '..')], }); +let userData = ''; + beforeAll(async () => { await app.start(); - const userData = await app.electron.remote.app.getPath('userData'); + userData = await app.electron.remote.app.getPath('userData'); await app.stop(); - await new Promise(resolve => rimraf(userData, () => resolve())); - await app.start(); }, 10000); -afterAll(async () => app && app.isRunning() && (await app.stop())); - describe('E2E', () => { - test('starts', async () => { + beforeEach(async () => { + await new Promise(resolve => rimraf(userData, () => resolve())); + await app.start(); await app.client.waitUntilWindowLoaded(); + }, 10000); + + afterEach(async () => app && app.isRunning() && (await app.stop())); + + test('starts', async () => { expect(app.isRunning()).toEqual(true); }); - test('logs in', async () => { - await app.client.waitUntilWindowLoaded(); + const usernameField = '#login__field-username'; + const passwordField = '#login__field-password'; + const loginButton = '#login__login-button'; - app.client - .$('#login__field-username') - .setValue(process.env.TEST_USERNAME as string); - app.client - .$('#login__field-password') - .setValue(process.env.TEST_PASSWORD as string); + const loginWith = async (username: string, password: string) => { + await waitFor(app, usernameField); + el(app, usernameField).setValue(username); + await waitFor(app, passwordField); + el(app, passwordField).setValue(password); - await wait(500); + await wait(2000); // try and prevent DDoS protection + await waitFor(app, loginButton); + el(app, loginButton).click(); + }; - app.client.$('#login__login-button').click(); + test('creates an account', async () => { + await waitFor(app, '=Sign up'); + el(app, '=Sign up').click(); - await app.client.waitUntilWindowLoaded(); - await app.client.waitForExist('.note-list', 10000); + await waitFor(app, usernameField); + await loginWith(TEST_USERNAME, TEST_PASSWORD); + + await waitForEvent(app, 'notesLoaded'); + await wait(1000); // @TODO: This delay is necessary but shouldn't be + }, 20000); + + test('login with wrong password fails', async () => { + await loginWith(TEST_USERNAME, `${TEST_PASSWORD}_wrong`); + + await waitFor(app, '[data-error-name="invalid-login"]'); + }, 20000); + + test('login with correct password logs in', async () => { + await loginWith(TEST_USERNAME, TEST_PASSWORD); + + await waitForEvent(app, 'notesLoaded'); + }, 20000); + + test('can create new note by clicking on new note button', async () => { + await loginWith(TEST_USERNAME, TEST_PASSWORD); + await waitForEvent(app, 'notesLoaded'); + await wait(1000); // @TODO: This delay is necessary but shouldn't be + + const newNoteButton = 'button[data-title="New Note"]'; + await waitFor(app, newNoteButton); + el(app, newNoteButton).click(); + + await waitForEvent(app, 'editorNewNote'); }, 20000); }); diff --git a/lib/app.tsx b/lib/app.tsx index 717262a51..71a61b348 100644 --- a/lib/app.tsx +++ b/lib/app.tsx @@ -208,6 +208,8 @@ export const App = connect( ); this.toggleShortcuts(true); + + __TEST__ && window.testEvents.push('booted'); } componentWillUnmount() { @@ -314,6 +316,8 @@ export const App = connect( loadNotes({ noteBucket }); setUnsyncedNoteIds(getUnsyncedNoteIds(noteBucket)); + + __TEST__ && window.testEvents.push('notesLoaded'); }; onNoteRemoved = () => this.onNotesIndex(); diff --git a/lib/auth/index.tsx b/lib/auth/index.tsx index 411236a4c..c519d900c 100644 --- a/lib/auth/index.tsx +++ b/lib/auth/index.tsx @@ -71,12 +71,20 @@ export class Auth extends Component {

{buttonLabel}

{this.props.hasInvalidCredentials && ( -

+

Could not log in with the provided email address and password.

)} {this.props.hasLoginError && ( -

{errorMessage}

+

+ {errorMessage} +

)} {passwordErrorMessage && (

diff --git a/lib/boot.ts b/lib/boot.ts index 2b8d76b4e..21b77d745 100644 --- a/lib/boot.ts +++ b/lib/boot.ts @@ -1,3 +1,7 @@ +if (__TEST__) { + window.testEvents = []; +} + import './utils/ensure-platform-support'; import 'core-js/stable'; import 'regenerator-runtime/runtime'; diff --git a/lib/global.d.ts b/lib/global.d.ts new file mode 100644 index 000000000..5bfd6564d --- /dev/null +++ b/lib/global.d.ts @@ -0,0 +1,9 @@ +declare var __TEST__: boolean; + +interface Window { + _tkq: Array; + testEvents: (string | [string, ...any[]])[]; + wpcom: { + tracks: object; + }; +} diff --git a/lib/icon-button/index.tsx b/lib/icon-button/index.tsx index 3d368ca6d..13c5bb980 100644 --- a/lib/icon-button/index.tsx +++ b/lib/icon-button/index.tsx @@ -8,7 +8,7 @@ export const IconButton = ({ icon, title, ...props }) => ( enterDelay={200} title={title} > - diff --git a/lib/note-content-editor.tsx b/lib/note-content-editor.tsx index 6dd1adbb2..8e26acd00 100644 --- a/lib/note-content-editor.tsx +++ b/lib/note-content-editor.tsx @@ -179,9 +179,17 @@ class NoteContentEditor extends Component { noteId !== prevProps.noteId || content.version !== prevProps.content.version ) { - this.setState({ - editorState: this.createNewEditorState(content.text, searchQuery), - }); + this.setState( + { + editorState: this.createNewEditorState(content.text, searchQuery), + }, + () => + __TEST__ && + window.testEvents.push([ + 'editorNewNote', + plainTextContent(this.state.editorState), + ]) + ); return; } diff --git a/package.json b/package.json index bf56b7564..ae22b6489 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "dev": "make dev", "start": "make start NODE_ENV=development", - "test-e2e": "npx jest --config=./jest.config.e2e.js", + "test-e2e": "npx jest --config=./jest.config.e2e.js --runInBand", "test": "make test", "lint": "make lint", "format": "make format", diff --git a/webpack.config.js b/webpack.config.js index 686e8fd0c..23615380e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -85,6 +85,7 @@ module.exports = () => { chunkFilename: isDevMode ? '[id].css' : '[id].[hash].css', }), new webpack.DefinePlugin({ + __TEST__: JSON.stringify(process.env.NODE_ENV === 'test'), config: JSON.stringify(config), }), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),