diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index 516aae0eac..0a19706c0b 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -315,6 +315,7 @@ export default class PasswordLogin extends React.PureComponent { case LoginField.Email: classes.error = this.props.loginIncorrect && !this.props.username; return { case LoginField.MatrixId: classes.error = this.props.loginIncorrect && !this.props.username; return { />; return { { loginType } { loginField } Promise, restCreator: RestSessionCreator): Promise { @@ -51,6 +52,15 @@ export async function scenario(createSession: (s: string) => Promise { diff --git a/test/end-to-end-tests/src/scenarios/sticker.ts b/test/end-to-end-tests/src/scenarios/sticker.ts new file mode 100644 index 0000000000..554eb2785f --- /dev/null +++ b/test/end-to-end-tests/src/scenarios/sticker.ts @@ -0,0 +1,143 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +import * as http from "http"; +import { AddressInfo } from "net"; + +import { RestSessionCreator } from "../rest/creator"; +import { ElementSession } from "../session"; +import { login } from "../usecases/login"; +import { selectRoom } from "../usecases/select-room"; +import { sendSticker } from "../usecases/send-sticker"; + +const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker"; +const ROOM_NAME_1 = "Sticker Test"; +const ROOM_NAME_2 = "Sticker Test Two"; +const STICKER_MESSAGE = JSON.stringify({ + action: "m.sticker", + api: "fromWidget", + data: { + name: "teststicker", + description: "Test Sticker", + file: "test.png", + content: { + body: "Test Sticker", + msgtype: "m.sticker", + url: "mxc://somewhere", + }, + }, + requestId: "1", + widgetId: STICKER_PICKER_WIDGET_ID, +}); +const WIDGET_HTML = ` + + + Fake Sticker Picker + + + + + + + +`; + +class WidgetServer { + private server: http.Server = null; + + start() { + this.server = http.createServer(this.onRequest); + this.server.listen(); + } + + stop() { + this.server.close(); + } + + get port(): number { + return (this.server.address()as AddressInfo).port; + } + + onRequest = (req: http.IncomingMessage, res: http.ServerResponse) => { + res.writeHead(200); + res.end(WIDGET_HTML); + }; +} + +export async function stickerScenarios( + username: string, password: string, + session: ElementSession, restCreator: RestSessionCreator, +): Promise { + console.log(" making account to test stickers"); + + const creds = await restCreator.createSession(username, password); + + // we make the room here which also approves the consent stuff + // (besides, we test creating rooms elsewhere: no need to do so again) + await creds.createRoom(ROOM_NAME_1, {}); + await creds.createRoom(ROOM_NAME_2, {}); + + console.log(" injecting fake sticker picker"); + + const widgetServer = new WidgetServer(); + widgetServer.start(); + + const stickerPickerUrl = `http://localhost:${widgetServer.port}/`; + + await creds.put(`/user/${encodeURIComponent(creds.userId())}/account_data/m.widgets`, { + "fake_sticker_picker": { + content: { + type: "m.stickerpicker", + name: "Fake Stickers", + url: stickerPickerUrl, + }, + id: STICKER_PICKER_WIDGET_ID, + }, + }); + + await login(session, username, password, session.hsUrl); + + session.log.startGroup(`can send a sticker`); + await selectRoom(session, ROOM_NAME_1); + await sendSticker(session); + session.log.endGroup(); + + // switch to another room & send another one + session.log.startGroup(`can send a sticker to another room`); + + const navPromise = session.page.waitForNavigation(); + await selectRoom(session, ROOM_NAME_2); + await navPromise; + + await sendSticker(session); + session.log.endGroup(); + + widgetServer.stop(); +} diff --git a/test/end-to-end-tests/src/session.ts b/test/end-to-end-tests/src/session.ts index cce2e657bd..145432f7ab 100644 --- a/test/end-to-end-tests/src/session.ts +++ b/test/end-to-end-tests/src/session.ts @@ -198,6 +198,10 @@ export class ElementSession { this.page.off('request', onRequest); } + public async waitNoSpinner(): Promise { + await this.page.waitForSelector(".mx_Spinner", { hidden: true }); + } + public goto(url: string): Promise { return this.page.goto(url); } diff --git a/test/end-to-end-tests/src/usecases/login.ts b/test/end-to-end-tests/src/usecases/login.ts new file mode 100644 index 0000000000..a7943bb595 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/login.ts @@ -0,0 +1,91 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +import { strict as assert } from 'assert'; + +import { ElementSession } from "../session"; + +export async function login( + session: ElementSession, + username: string, password: string, + homeserver: string, +): Promise { + session.log.startGroup("logs in"); + session.log.step("Navigates to login page"); + + const navPromise = session.page.waitForNavigation(); + await session.goto(session.url('/#/login')); + await navPromise; + session.log.done(); + + // for reasons I still don't fully understand, this seems to be flakey + // such that when it's trying to click on 'mx_ServerPicker_change', + // it ends up clicking instead on the dropdown for username / email / phone. + // Waiting for the serverpicker to appear before proceeding seems to make + // it reliable... + await session.query('.mx_ServerPicker'); + + // wait until no spinners visible + await session.waitNoSpinner(); + + // change the homeserver by clicking the advanced section + if (homeserver) { + session.log.step("Clicks button to change homeserver"); + const changeButton = await session.query('.mx_ServerPicker_change'); + await changeButton.click(); + session.log.done(); + + session.log.step("Enters homeserver"); + const hsInputField = await session.query('.mx_ServerPickerDialog_otherHomeserver'); + await session.replaceInputText(hsInputField, homeserver); + session.log.done(); + + session.log.step("Clicks next"); + const nextButton = await session.query('.mx_ServerPickerDialog_continue'); + // accept homeserver + await nextButton.click(); + session.log.done(); + } + // Delay required because of local race condition on macOS + // Where the form is not query-able despite being present in the DOM + await session.delay(100); + + session.log.step("Fills in login form"); + //fill out form + const usernameField = await session.query("#mx_LoginForm_username"); + const passwordField = await session.query("#mx_LoginForm_password"); + await session.replaceInputText(usernameField, username); + await session.replaceInputText(passwordField, password); + session.log.done(); + + session.log.step("Clicks login"); + const loginButton = await session.query('.mx_Login_submit'); + await loginButton.focus(); + //check no errors + const errorText = await session.tryGetInnertext('.mx_Login_error'); + assert.strictEqual(errorText, null); + //submit form + //await page.screenshot({path: "beforesubmit.png", fullPage: true}); + await loginButton.click(); + session.log.done(); + + const foundHomeUrl = await session.poll(async () => { + const url = session.page.url(); + return url === session.url('/#/home'); + }); + assert(foundHomeUrl); + session.log.endGroup(); +} diff --git a/test/end-to-end-tests/src/usecases/select-room.ts b/test/end-to-end-tests/src/usecases/select-room.ts new file mode 100644 index 0000000000..10a37fe545 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/select-room.ts @@ -0,0 +1,35 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +import { findSublist } from "./create-room"; +import { ElementSession } from "../session"; + +export async function selectRoom(session: ElementSession, name: string): Promise { + session.log.step(`select "${name}" room`); + const inviteSublist = await findSublist(session, "rooms"); + const invitesHandles = await inviteSublist.$$(".mx_RoomTile_name"); + const invitesWithText = await Promise.all(invitesHandles.map(async (roomHandle) => { + const text = await session.innerText(roomHandle); + return { roomHandle, text }; + })); + const roomHandle = invitesWithText.find(({ roomHandle, text }) => { + return text.trim() === name; + }).roomHandle; + + await roomHandle.click(); + + session.log.done(); +} diff --git a/test/end-to-end-tests/src/usecases/send-sticker.ts b/test/end-to-end-tests/src/usecases/send-sticker.ts new file mode 100644 index 0000000000..e599d2bd5e --- /dev/null +++ b/test/end-to-end-tests/src/usecases/send-sticker.ts @@ -0,0 +1,85 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +import { Frame } from "puppeteer"; + +import { ElementSession } from "../session"; + +export async function sendSticker(session: ElementSession): Promise { + session.log.step(`opens composer menu`); + const kebabButton = await session.query('.mx_MessageComposer_buttonMenu'); + await kebabButton.click(); + session.log.done(); + + let stickerFrame: Frame; + + // look to see if the sticker picker is already there (it's persistent, so + // it will only load a new frame the first time we open it) + for (const f of session.page.frames()) { + if ((await f.title()) === "Fake Sticker Picker") { + stickerFrame = f; + } + } + + const stickerFramePromise = new Promise(resolve => { + session.page.once('frameattached', async f => { + await f.waitForNavigation(); + resolve(f); + }); + }); + + session.log.step(`opens sticker picker`); + + const stickerOption = await session.query('#stickersButton'); + await stickerOption.click(); + + if (stickerFrame === undefined) { + stickerFrame = await stickerFramePromise; + } + + if (stickerFrame === undefined) throw new Error("Couldn't find sticker picker frame"); + session.log.done(); + + session.log.step(`clicks sticker button`); + + const sendStickerButton = await stickerFrame.waitForSelector('#sendsticker'); + sendStickerButton.click(); + + // wait for the message to appear sent + await session.query(".mx_EventTile_last:not(.mx_EventTile_sending)"); + + const stickerSrc = await session.page.evaluate(() => { + return document.querySelector( + '.mx_EventTile_last .mx_MStickerBody_wrapper img', + ).getAttribute('src'); + }); + + if (!stickerSrc.split('?')[0].endsWith('/_matrix/media/r0/thumbnail/somewhere')) { + throw new Error("Unexpected image src for sticker: got " + stickerSrc); + } + + const stickerAlt = await session.page.evaluate(() => { + return document.querySelector( + '.mx_EventTile_last .mx_MStickerBody_wrapper img', + ).getAttribute('alt'); + }); + + if (stickerAlt !== "Test Sticker") { + throw new Error("Unexpected image alt for sticker: got " + stickerAlt); + } + + session.log.done(); +}