diff --git a/src/scenario.js b/src/scenario.js index f035e94c35..330db0fef2 100644 --- a/src/scenario.js +++ b/src/scenario.js @@ -15,20 +15,24 @@ limitations under the License. */ +const {acceptDialog} = require('./tests/dialog'); const signup = require('./tests/signup'); const join = require('./tests/join'); const sendMessage = require('./tests/send-message'); +const acceptInvite = require('./tests/accept-invite'); +const invite = require('./tests/invite'); const receiveMessage = require('./tests/receive-message'); const createRoom = require('./tests/create-room'); const changeRoomSettings = require('./tests/room-settings'); const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); +const getE2EDeviceFromSettings = require('./tests/e2e-device'); +const verifyDeviceForUser = require("./tests/verify-device"); module.exports = async function scenario(createSession) { async function createUser(username) { const session = await createSession(username); await signup(session, session.username, 'testtest'); - const noticesName = "Server Notices"; - await acceptServerNoticesInviteAndConsent(session, noticesName); + await acceptServerNoticesInviteAndConsent(session); return session; } @@ -36,22 +40,46 @@ module.exports = async function scenario(createSession) { const bob = await createUser("bob"); await createDirectoryRoomAndTalk(alice, bob); + await createE2ERoomAndTalk(alice, bob); } async function createDirectoryRoomAndTalk(alice, bob) { console.log(" creating a public room and join through directory:"); const room = 'test'; - const message = "hi Alice!"; await createRoom(alice, room); await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); await join(bob, room); - await sendMessage(bob, message); - await receiveMessage(alice, {sender: "bob", body: message}); -} + const bobMessage = "hi Alice!"; + await sendMessage(bob, bobMessage); + await receiveMessage(alice, {sender: "bob", body: bobMessage}); + const aliceMessage = "hi Bob, welcome!" + await sendMessage(alice, aliceMessage); + await receiveMessage(bob, {sender: "alice", body: aliceMessage}); +} async function createE2ERoomAndTalk(alice, bob) { - await createRoom(bob, "secrets"); + console.log(" creating an e2e encrypted room and join through invite:"); + const room = "secrets"; + await createRoom(bob, room); await changeRoomSettings(bob, {encryption: true}); await invite(bob, "@alice:localhost"); - await acceptInvite(alice, "secrets"); -} \ No newline at end of file + await acceptInvite(alice, room); + const bobDevice = await getE2EDeviceFromSettings(bob); + // wait some time for the encryption warning dialog + // to appear after closing the settings + await bob.delay(500); + await acceptDialog(bob, "encryption"); + const aliceDevice = await getE2EDeviceFromSettings(alice); + // wait some time for the encryption warning dialog + // to appear after closing the settings + await alice.delay(500); + await acceptDialog(alice, "encryption"); + await verifyDeviceForUser(bob, "alice", aliceDevice); + await verifyDeviceForUser(alice, "bob", bobDevice); + const aliceMessage = "Guess what I just heard?!" + await sendMessage(alice, aliceMessage); + await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); + const bobMessage = "You've got to tell me!"; + await sendMessage(bob, bobMessage); + await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); +} diff --git a/src/tests/accept-invite.js b/src/tests/accept-invite.js new file mode 100644 index 0000000000..5cdeeb3d84 --- /dev/null +++ b/src/tests/accept-invite.js @@ -0,0 +1,41 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); +const {acceptDialogMaybe} = require('./dialog'); + +module.exports = async function acceptInvite(session, name) { + session.log.step(`accepts "${name}" invite`); + //TODO: brittle selector + const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite', 1000); + const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { + const text = await session.innerText(inviteHandle); + return {inviteHandle, text}; + })); + const inviteHandle = invitesWithText.find(({inviteHandle, text}) => { + return text.trim() === name; + }).inviteHandle; + + await inviteHandle.click(); + + const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child"); + await acceptInvitationLink.click(); + + // accept e2e warning dialog + acceptDialogMaybe(session, "encryption"); + + session.log.done(); +} \ No newline at end of file diff --git a/src/tests/create-room.js b/src/tests/create-room.js index 8f5b5c9e85..0f7f33ddff 100644 --- a/src/tests/create-room.js +++ b/src/tests/create-room.js @@ -17,7 +17,7 @@ limitations under the License. const assert = require('assert'); module.exports = async function createRoom(session, roomName) { - session.log.step(`creates room ${roomName}`); + session.log.step(`creates room "${roomName}"`); //TODO: brittle selector const createRoomButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Create new room"]'); await createRoomButton.click(); diff --git a/src/tests/dialog.js b/src/tests/dialog.js new file mode 100644 index 0000000000..420438b3f9 --- /dev/null +++ b/src/tests/dialog.js @@ -0,0 +1,47 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + + +async function acceptDialog(session, expectedContent) { + const foundDialog = await acceptDialogMaybe(session, expectedContent); + if (!foundDialog) { + throw new Error("could not find a dialog"); + } +} + +async function acceptDialogMaybe(session, expectedContent) { + let dialog = null; + try { + dialog = await session.waitAndQuery(".mx_QuestionDialog", 100); + } catch(err) { + return false; + } + if (expectedContent) { + const contentElement = await dialog.$(".mx_Dialog_content"); + const content = await (await contentElement.getProperty("innerText")).jsonValue(); + assert.ok(content.indexOf(expectedContent) !== -1); + } + const primaryButton = await dialog.$(".mx_Dialog_primary"); + await primaryButton.click(); + return true; +} + +module.exports = { + acceptDialog, + acceptDialogMaybe, +}; \ No newline at end of file diff --git a/src/tests/e2e-device.js b/src/tests/e2e-device.js new file mode 100644 index 0000000000..fd81ac43eb --- /dev/null +++ b/src/tests/e2e-device.js @@ -0,0 +1,31 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + +module.exports = async function getE2EDeviceFromSettings(session) { + session.log.step(`gets e2e device/key from settings`); + const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); + await settingsButton.click(); + const deviceAndKey = await session.waitAndQueryAll(".mx_UserSettings_section.mx_UserSettings_cryptoSection code"); + assert.equal(deviceAndKey.length, 2); + const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue(); + const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue(); + const closeButton = await session.query(".mx_RoomHeader_cancelButton"); + await closeButton.click(); + session.log.done(); + return {id, key}; +} \ No newline at end of file diff --git a/src/tests/invite.js b/src/tests/invite.js new file mode 100644 index 0000000000..37549aa2ca --- /dev/null +++ b/src/tests/invite.js @@ -0,0 +1,30 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + +module.exports = async function invite(session, userId) { + session.log.step(`invites "${userId}" to room`); + await session.delay(200); + const inviteButton = await session.waitAndQuery(".mx_RightPanel_invite"); + await inviteButton.click(); + const inviteTextArea = await session.waitAndQuery(".mx_ChatInviteDialog textarea"); + await inviteTextArea.type(userId); + await inviteTextArea.press("Enter"); + const confirmButton = await session.query(".mx_Dialog_primary"); + await confirmButton.click(); + session.log.done(); +} \ No newline at end of file diff --git a/src/tests/join.js b/src/tests/join.js index 3c76ad2c67..8ab5e80f2d 100644 --- a/src/tests/join.js +++ b/src/tests/join.js @@ -17,7 +17,7 @@ limitations under the License. const assert = require('assert'); module.exports = async function join(session, roomName) { - session.log.step(`joins room ${roomName}`); + session.log.step(`joins room "${roomName}"`); //TODO: brittle selector const directoryButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Room directory"]'); await directoryButton.click(); diff --git a/src/tests/receive-message.js b/src/tests/receive-message.js index c84aefcbfd..9c963c45f4 100644 --- a/src/tests/receive-message.js +++ b/src/tests/receive-message.js @@ -16,26 +16,33 @@ limitations under the License. const assert = require('assert'); - -async function getMessageFromTile(eventTile) { -} - module.exports = async function receiveMessage(session, message) { - session.log.step(`receives message "${message.body}" from ${message.sender} in room`); + session.log.step(`receives message "${message.body}" from ${message.sender}`); // wait for a response to come in that contains the message // crude, but effective await session.page.waitForResponse(async (response) => { + if (response.request().url().indexOf("/sync") === -1) { + return false; + } const body = await response.text(); - return body.indexOf(message.body) !== -1; + if (message.encrypted) { + return body.indexOf(message.sender) !== -1 && + body.indexOf("m.room.encrypted") !== -1; + } else { + return body.indexOf(message.body) !== -1; + } }); // wait a bit for the incoming event to be rendered - await session.delay(100); + await session.delay(300); let lastTile = await session.query(".mx_EventTile_last"); const senderElement = await lastTile.$(".mx_SenderProfile_name"); const bodyElement = await lastTile.$(".mx_EventTile_body"); const sender = await(await senderElement.getProperty("innerText")).jsonValue(); const body = await(await bodyElement.getProperty("innerText")).jsonValue(); - + if (message.encrypted) { + const e2eIcon = await lastTile.$(".mx_EventTile_e2eIcon"); + assert.ok(e2eIcon); + } assert.equal(body, message.body); assert.equal(sender, message.sender); session.log.done(); diff --git a/src/tests/room-settings.js b/src/tests/room-settings.js index 6001d14d34..127afa26dd 100644 --- a/src/tests/room-settings.js +++ b/src/tests/room-settings.js @@ -15,6 +15,19 @@ limitations under the License. */ const assert = require('assert'); +const {acceptDialog} = require('./dialog'); + +async function setCheckboxSetting(session, checkbox, enabled) { + const checked = await session.getElementProperty(checkbox, "checked"); + assert.equal(typeof checked, "boolean"); + if (checked !== enabled) { + await checkbox.click(); + session.log.done(); + return true; + } else { + session.log.done("already set"); + } +} module.exports = async function changeRoomSettings(session, settings) { session.log.startGroup(`changes the room settings`); @@ -31,13 +44,15 @@ module.exports = async function changeRoomSettings(session, settings) { if (typeof settings.directory === "boolean") { session.log.step(`sets directory listing to ${settings.directory}`); - const checked = await session.getElementProperty(isDirectory, "checked"); - assert.equal(typeof checked, "boolean"); - if (checked !== settings.directory) { - await isDirectory.click(); - session.log.done(); - } else { - session.log.done("already set"); + await setCheckboxSetting(session, isDirectory, settings.directory); + } + + if (typeof settings.encryption === "boolean") { + session.log.step(`sets room e2e encryption to ${settings.encryption}`); + const clicked = await setCheckboxSetting(session, e2eEncryptionCheck, settings.encryption); + // if enabling, accept beta warning dialog + if (clicked && settings.encryption) { + await acceptDialog(session, "encryption"); } } @@ -63,5 +78,6 @@ module.exports = async function changeRoomSettings(session, settings) { const saveButton = await session.query(".mx_RoomHeader_wrapper .mx_RoomHeader_textButton"); await saveButton.click(); + session.log.endGroup(); } \ No newline at end of file diff --git a/src/tests/send-message.js b/src/tests/send-message.js index 8a61a15e94..e98d0dad72 100644 --- a/src/tests/send-message.js +++ b/src/tests/send-message.js @@ -18,8 +18,12 @@ const assert = require('assert'); module.exports = async function sendMessage(session, message) { session.log.step(`writes "${message}" in room`); - const composer = await session.waitAndQuery('.mx_MessageComposer'); + // this selector needs to be the element that has contenteditable=true, + // not any if its parents, otherwise it behaves flaky at best. + const composer = await session.waitAndQuery('.mx_MessageComposer_editor'); await composer.type(message); + const text = await session.innerText(composer); + assert.equal(text.trim(), message.trim()); await composer.press("Enter"); session.log.done(); } \ No newline at end of file diff --git a/src/tests/server-notices-consent.js b/src/tests/server-notices-consent.js index def21d04c3..3e07248daa 100644 --- a/src/tests/server-notices-consent.js +++ b/src/tests/server-notices-consent.js @@ -15,26 +15,11 @@ limitations under the License. */ const assert = require('assert'); - -module.exports = async function acceptServerNoticesInviteAndConsent(session, noticesName) { - session.log.step(`accepts "${noticesName}" invite and accepting terms & conditions`); - //TODO: brittle selector - const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite'); - const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { - const text = await session.innerText(inviteHandle); - return {inviteHandle, text}; - })); - const inviteHandle = invitesWithText.find(({inviteHandle, text}) => { - return text.trim() === noticesName; - }).inviteHandle; - - await inviteHandle.click(); - - const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child"); - await acceptInvitationLink.click(); - +const acceptInvite = require("./accept-invite") +module.exports = async function acceptServerNoticesInviteAndConsent(session) { + await acceptInvite(session, "Server Notices"); + session.log.step(`accepts terms & conditions`); const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 1000); - const termsPagePromise = session.waitForNewPage(); await consentLink.click(); const termsPage = await termsPagePromise; diff --git a/src/tests/verify-device.js b/src/tests/verify-device.js new file mode 100644 index 0000000000..0622654876 --- /dev/null +++ b/src/tests/verify-device.js @@ -0,0 +1,42 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const assert = require('assert'); + +module.exports = async function verifyDeviceForUser(session, name, expectedDevice) { + session.log.step(`verifies e2e device for ${name}`); + const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name"); + const membersAndNames = await Promise.all(memberNameElements.map(async (el) => { + return [el, await session.innerText(el)]; + })); + const matchingMember = membersAndNames.filter(([el, text]) => { + return text === name; + }).map(([el]) => el)[0]; + await matchingMember.click(); + const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify"); + await firstVerifyButton.click(); + const dialogCodeFields = await session.waitAndQueryAll(".mx_QuestionDialog code"); + assert.equal(dialogCodeFields.length, 2); + const deviceId = await session.innerText(dialogCodeFields[0]); + const deviceKey = await session.innerText(dialogCodeFields[1]); + assert.equal(expectedDevice.id, deviceId); + assert.equal(expectedDevice.key, deviceKey); + const confirmButton = await session.query(".mx_Dialog_primary"); + await confirmButton.click(); + const closeMemberInfo = await session.query(".mx_MemberInfo_cancel"); + await closeMemberInfo.click(); + session.log.done(); +} \ No newline at end of file diff --git a/start.js b/start.js index 11dbe8d2fa..6c68050c97 100644 --- a/start.js +++ b/start.js @@ -20,12 +20,18 @@ const scenario = require('./src/scenario'); const riotserver = 'http://localhost:5000'; +const noLogs = process.argv.indexOf("--no-logs") !== -1; +const debug = process.argv.indexOf("--debug") !== -1; + async function runTests() { let sessions = []; console.log("running tests ..."); const options = {}; - // options.headless = false; + if (debug) { + // options.slowMo = 10; + options.headless = false; + } if (process.env.CHROME_PATH) { const path = process.env.CHROME_PATH; console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`); @@ -44,20 +50,28 @@ async function runTests() { } catch(err) { failure = true; console.log('failure: ', err); - for(let i = 0; i < sessions.length; ++i) { - const session = sessions[i]; - documentHtml = await session.page.content(); - console.log(`---------------- START OF ${session.username} LOGS ----------------`); - console.log('---------------- console.log output:'); - console.log(session.consoleLogs()); - console.log('---------------- network requests:'); - console.log(session.networkLogs()); - console.log('---------------- document html:'); - console.log(documentHtml); - console.log(`---------------- END OF ${session.username} LOGS ----------------`); + if (!noLogs) { + for(let i = 0; i < sessions.length; ++i) { + const session = sessions[i]; + documentHtml = await session.page.content(); + console.log(`---------------- START OF ${session.username} LOGS ----------------`); + console.log('---------------- console.log output:'); + console.log(session.consoleLogs()); + console.log('---------------- network requests:'); + console.log(session.networkLogs()); + console.log('---------------- document html:'); + console.log(documentHtml); + console.log(`---------------- END OF ${session.username} LOGS ----------------`); + } } } + // wait 5 minutes on failure if not running headless + // to inspect what went wrong + if (failure && options.headless === false) { + await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000)); + } + await Promise.all(sessions.map((session) => session.close())); if (failure) {