mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 11:47:23 +03:00
Add an end-to-end test for stickers (#7733)
* Add an end-to-end test for stickers * More logs on login * Wait for spinners to go away * Factor out spinner waiting as it seems useful * Move stickers to the end * More waiting * When all else fails... add sleeps * Waiting for the server picker to appear seems to work..? * Typos Co-authored-by: J. Ryan Stinnett <jryans@gmail.com> * remove commented code from registration usecase Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
This commit is contained in:
parent
84e15fa148
commit
5fe8442f44
8 changed files with 376 additions and 4 deletions
|
@ -315,6 +315,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
case LoginField.Email:
|
case LoginField.Email:
|
||||||
classes.error = this.props.loginIncorrect && !this.props.username;
|
classes.error = this.props.loginIncorrect && !this.props.username;
|
||||||
return <EmailField
|
return <EmailField
|
||||||
|
id="mx_LoginForm_email"
|
||||||
className={classNames(classes)}
|
className={classNames(classes)}
|
||||||
name="username" // make it a little easier for browser's remember-password
|
name="username" // make it a little easier for browser's remember-password
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
|
@ -333,6 +334,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
case LoginField.MatrixId:
|
case LoginField.MatrixId:
|
||||||
classes.error = this.props.loginIncorrect && !this.props.username;
|
classes.error = this.props.loginIncorrect && !this.props.username;
|
||||||
return <Field
|
return <Field
|
||||||
|
id="mx_LoginForm_username"
|
||||||
className={classNames(classes)}
|
className={classNames(classes)}
|
||||||
name="username" // make it a little easier for browser's remember-password
|
name="username" // make it a little easier for browser's remember-password
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
|
@ -360,6 +362,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
return <Field
|
return <Field
|
||||||
|
id="mx_LoginForm_phone"
|
||||||
className={classNames(classes)}
|
className={classNames(classes)}
|
||||||
name="phoneNumber"
|
name="phoneNumber"
|
||||||
autoComplete="tel-national"
|
autoComplete="tel-national"
|
||||||
|
@ -447,6 +450,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
{ loginType }
|
{ loginType }
|
||||||
{ loginField }
|
{ loginField }
|
||||||
<Field
|
<Field
|
||||||
|
id="mx_LoginForm_password"
|
||||||
className={pwFieldClass}
|
className={pwFieldClass}
|
||||||
autoComplete="password"
|
autoComplete="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
|
|
@ -22,10 +22,10 @@ import { approveConsent } from './consent';
|
||||||
import { Credentials } from "./creator";
|
import { Credentials } from "./creator";
|
||||||
|
|
||||||
interface RoomOptions {
|
interface RoomOptions {
|
||||||
invite: string;
|
invite?: string;
|
||||||
public: boolean;
|
public?: boolean;
|
||||||
topic: string;
|
topic?: string;
|
||||||
dm: boolean;
|
dm?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RestSession {
|
export class RestSession {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { RestSessionCreator } from "./rest/creator";
|
||||||
import { RestMultiSession } from "./rest/multi";
|
import { RestMultiSession } from "./rest/multi";
|
||||||
import { spacesScenarios } from './scenarios/spaces';
|
import { spacesScenarios } from './scenarios/spaces';
|
||||||
import { RestSession } from "./rest/session";
|
import { RestSession } from "./rest/session";
|
||||||
|
import { stickerScenarios } from './scenarios/sticker';
|
||||||
|
|
||||||
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
|
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
|
||||||
restCreator: RestSessionCreator): Promise<void> {
|
restCreator: RestSessionCreator): Promise<void> {
|
||||||
|
@ -51,6 +52,15 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
||||||
await lazyLoadingScenarios(alice, bob, charlies);
|
await lazyLoadingScenarios(alice, bob, charlies);
|
||||||
// do spaces scenarios last as the rest of the tests may get confused by spaces
|
// do spaces scenarios last as the rest of the tests may get confused by spaces
|
||||||
await spacesScenarios(alice, bob);
|
await spacesScenarios(alice, bob);
|
||||||
|
|
||||||
|
// we spawn another session for stickers, partially because it involves injecting
|
||||||
|
// a custom sticker picker widget for the account, although mostly because for these
|
||||||
|
// tests to scale, they probably need to be split up more, which means running each
|
||||||
|
// scenario with it's own session (and will make it easier to find relevant logs),
|
||||||
|
// so lets move in this direction (although at some point we'll also need to start
|
||||||
|
// closing them as we go rather than leaving them all open until the end).
|
||||||
|
const stickerSession = await createSession("sally");
|
||||||
|
await stickerScenarios("sally", "ilikestickers", stickerSession, restCreator);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createRestUsers(restCreator: RestSessionCreator): Promise<RestMultiSession> {
|
async function createRestUsers(restCreator: RestSessionCreator): Promise<RestMultiSession> {
|
||||||
|
|
143
test/end-to-end-tests/src/scenarios/sticker.ts
Normal file
143
test/end-to-end-tests/src/scenarios/sticker.ts
Normal file
|
@ -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 = `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Fake Sticker Picker</title>
|
||||||
|
<script>
|
||||||
|
window.onmessage = ev => {
|
||||||
|
if (ev.data.action === 'capabilities') {
|
||||||
|
window.parent.postMessage(Object.assign({
|
||||||
|
response: {
|
||||||
|
capabilities: ["m.sticker"]
|
||||||
|
},
|
||||||
|
}, ev.data), '*');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button name="Send" id="sendsticker">Press for sticker</button>
|
||||||
|
<script>
|
||||||
|
document.getElementById('sendsticker').onclick = () => {
|
||||||
|
window.parent.postMessage(${STICKER_MESSAGE}, '*')
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
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();
|
||||||
|
}
|
|
@ -198,6 +198,10 @@ export class ElementSession {
|
||||||
this.page.off('request', onRequest);
|
this.page.off('request', onRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async waitNoSpinner(): Promise<void> {
|
||||||
|
await this.page.waitForSelector(".mx_Spinner", { hidden: true });
|
||||||
|
}
|
||||||
|
|
||||||
public goto(url: string): Promise<puppeteer.HTTPResponse> {
|
public goto(url: string): Promise<puppeteer.HTTPResponse> {
|
||||||
return this.page.goto(url);
|
return this.page.goto(url);
|
||||||
}
|
}
|
||||||
|
|
91
test/end-to-end-tests/src/usecases/login.ts
Normal file
91
test/end-to-end-tests/src/usecases/login.ts
Normal file
|
@ -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<void> {
|
||||||
|
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();
|
||||||
|
}
|
35
test/end-to-end-tests/src/usecases/select-room.ts
Normal file
35
test/end-to-end-tests/src/usecases/select-room.ts
Normal file
|
@ -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<void> {
|
||||||
|
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();
|
||||||
|
}
|
85
test/end-to-end-tests/src/usecases/send-sticker.ts
Normal file
85
test/end-to-end-tests/src/usecases/send-sticker.ts
Normal file
|
@ -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<void> {
|
||||||
|
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<Frame>(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();
|
||||||
|
}
|
Loading…
Reference in a new issue