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:
David Baker 2022-02-15 11:49:53 +00:00 committed by GitHub
parent 84e15fa148
commit 5fe8442f44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 376 additions and 4 deletions

View file

@ -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"

View file

@ -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 {

View file

@ -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> {

View 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();
}

View file

@ -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);
} }

View 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();
}

View 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();
}

View 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();
}