diff --git a/cypress.json b/cypress.json
index 009b1f38800..92f7d0f2540 100644
--- a/cypress.json
+++ b/cypress.json
@@ -7,5 +7,6 @@
"retries": {
"runMode": 2,
"openMode": 0
- }
+ },
+ "chromeWebSecurity": false
}
diff --git a/cypress/integration/9-widgets/stickers.spec.ts b/cypress/integration/9-widgets/stickers.spec.ts
new file mode 100644
index 00000000000..0d0fee8776f
--- /dev/null
+++ b/cypress/integration/9-widgets/stickers.spec.ts
@@ -0,0 +1,163 @@
+/*
+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 { SynapseInstance } from "../../plugins/synapsedocker";
+
+const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
+const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
+const STICKER_NAME = "Test Sticker";
+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: STICKER_NAME,
+ file: "test.png",
+ content: {
+ body: STICKER_NAME,
+ msgtype: "m.sticker",
+ url: "mxc://somewhere",
+ },
+ },
+ requestId: "1",
+ widgetId: STICKER_PICKER_WIDGET_ID,
+});
+const WIDGET_HTML = `
+
+
+ Fake Sticker Picker
+
+
+
+
+
+
+
+`;
+
+function openStickerPicker() {
+ cy.get('.mx_MessageComposer_buttonMenu').click();
+ cy.get('#stickersButton').click();
+}
+
+function sendStickerFromPicker() {
+ // Note: Until https://github.com/cypress-io/cypress/issues/136 is fixed we will need
+ // to use `chromeWebSecurity: false` in our cypress config. Not even cy.origin() can
+ // break into the iframe for us :(
+ cy.accessIframe(`iframe[title="${STICKER_PICKER_WIDGET_NAME}"]`).within({}, () => {
+ cy.get("#sendsticker").should('exist').click();
+ });
+
+ // Sticker picker should close itself after sending.
+ cy.get(".mx_AppTileFullWidth#stickers").should('not.exist');
+}
+
+function expectTimelineSticker(roomId: string) {
+ // Make sure it's in the right room
+ cy.get('.mx_EventTile_sticker > a')
+ .should("have.attr", "href")
+ .and("include", `/${roomId}/`);
+
+ // Make sure the image points at the sticker image
+ cy.get(`img[alt="${STICKER_NAME}"]`)
+ .should("have.attr", "src")
+ .and("match", /thumbnail\/somewhere\?/);
+}
+
+describe("Stickers", () => {
+ // We spin up a web server for the sticker picker so that we're not testing to see if
+ // sysadmins can deploy sticker pickers on the same Element domain - we actually want
+ // to make sure that cross-origin postMessage works properly. This makes it difficult
+ // to write the test though, as we have to juggle iframe logistics.
+ //
+ // See sendStickerFromPicker() for more detail on iframe comms.
+
+ let stickerPickerUrl: string;
+ let synapse: SynapseInstance;
+
+ beforeEach(() => {
+ cy.startSynapse("default").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Sally");
+ });
+ cy.serveHtmlFile(WIDGET_HTML).then(url => {
+ stickerPickerUrl = url;
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ cy.stopWebServers();
+ });
+
+ it('should send a sticker to multiple rooms', () => {
+ cy.createRoom({
+ name: ROOM_NAME_1,
+ }).as("roomId1");
+ cy.createRoom({
+ name: ROOM_NAME_2,
+ }).as("roomId2");
+ cy.setAccountData("m.widgets", {
+ [STICKER_PICKER_WIDGET_ID]: {
+ content: {
+ type: "m.stickerpicker",
+ name: STICKER_PICKER_WIDGET_NAME,
+ url: stickerPickerUrl,
+ },
+ id: STICKER_PICKER_WIDGET_ID,
+ },
+ }).as("stickers");
+
+ cy.all([
+ cy.get("@roomId1"),
+ cy.get("@roomId2"),
+ cy.get<{}>("@stickers"), // just want to wait for it to be set up
+ ]).then(([roomId1, roomId2]) => {
+ cy.viewRoomByName(ROOM_NAME_1);
+ cy.url().should("contain", `/#/room/${roomId1}`);
+ openStickerPicker();
+ sendStickerFromPicker();
+ expectTimelineSticker(roomId1);
+
+ // Ensure that when we switch to a different room that the sticker
+ // goes to the right place
+ cy.viewRoomByName(ROOM_NAME_2);
+ cy.url().should("contain", `/#/room/${roomId2}`);
+ openStickerPicker();
+ sendStickerFromPicker();
+ expectTimelineSticker(roomId2);
+ });
+ });
+});
diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts
index eab5441c203..bc62efb03f9 100644
--- a/cypress/plugins/index.ts
+++ b/cypress/plugins/index.ts
@@ -20,6 +20,7 @@ import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import { performance } from "./performance";
import { synapseDocker } from "./synapsedocker";
+import { webserver } from "./webserver";
/**
* @type {Cypress.PluginConfig}
@@ -27,4 +28,5 @@ import { synapseDocker } from "./synapsedocker";
export default function(on: PluginEvents, config: PluginConfigOptions) {
performance(on, config);
synapseDocker(on, config);
+ webserver(on, config);
}
diff --git a/cypress/plugins/webserver.ts b/cypress/plugins/webserver.ts
new file mode 100644
index 00000000000..55a25a313e3
--- /dev/null
+++ b/cypress/plugins/webserver.ts
@@ -0,0 +1,52 @@
+/*
+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 PluginEvents = Cypress.PluginEvents;
+import PluginConfigOptions = Cypress.PluginConfigOptions;
+
+const servers: http.Server[] = [];
+
+function serveHtmlFile(html: string): string {
+ const server = http.createServer((req, res) => {
+ res.writeHead(200, {
+ "Content-Type": "text/html",
+ });
+ res.end(html);
+ });
+ server.listen();
+ servers.push(server);
+
+ return `http://localhost:${(server.address() as AddressInfo).port}/`;
+}
+
+function stopWebServers(): null {
+ for (const server of servers) {
+ server.close();
+ }
+ servers.splice(0, servers.length); // clear
+
+ return null; // tell cypress we did the task successfully (doesn't allow undefined)
+}
+
+export function webserver(on: PluginEvents, config: PluginConfigOptions) {
+ on("task", { serveHtmlFile, stopWebServers });
+ on("after:run", stopWebServers);
+}
diff --git a/cypress/support/client.ts b/cypress/support/client.ts
index 6a6a3932711..db27f4d2b1e 100644
--- a/cypress/support/client.ts
+++ b/cypress/support/client.ts
@@ -47,6 +47,12 @@ declare global {
* @param userId the id of the user to invite
*/
inviteUser(roomId: string, userId: string): Chainable<{}>;
+ /**
+ * Sets account data for the user.
+ * @param type The type of account data.
+ * @param data The data to store.
+ */
+ setAccountData(type: string, data: object): Chainable<{}>;
}
}
}
@@ -91,3 +97,9 @@ Cypress.Commands.add("inviteUser", (roomId: string, userId: string): Chainable<{
return cli.invite(roomId, userId);
});
});
+
+Cypress.Commands.add("setAccountData", (type: string, data: object): Chainable<{}> => {
+ return cy.getClient().then(async (cli: MatrixClient) => {
+ return cli.setAccountData(type, data);
+ });
+});
diff --git a/cypress/support/iframes.ts b/cypress/support/iframes.ts
new file mode 100644
index 00000000000..27bd5e0b8ee
--- /dev/null
+++ b/cypress/support/iframes.ts
@@ -0,0 +1,45 @@
+/*
+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 Chainable = Cypress.Chainable;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Gets you into the `body` of the selectable iframe. Best to call
+ * `within({}, () => { ... })` on the returned Chainable to access
+ * further elements.
+ * @param selector The jquery selector to find the frame with.
+ */
+ accessIframe(selector: string): Chainable>;
+ }
+ }
+}
+
+// Inspired by https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
+Cypress.Commands.add("accessIframe", (selector: string): Chainable> => {
+ return cy.get(selector)
+ .its("0.contentDocument.body").should("not.be.empty")
+ // Cypress loses types in the mess of wrapping, so force cast
+ .then(cy.wrap) as Chainable>;
+});
+
+// Needed to make this file a module
+export { };
diff --git a/cypress/support/index.ts b/cypress/support/index.ts
index 6e3b7d8b9cc..b82b950e994 100644
--- a/cypress/support/index.ts
+++ b/cypress/support/index.ts
@@ -29,3 +29,6 @@ import "./clipboard";
import "./util";
import "./app";
import "./percy";
+import "./webserver";
+import "./views";
+import "./iframes";
diff --git a/cypress/support/views.ts b/cypress/support/views.ts
new file mode 100644
index 00000000000..c7f55b4ac9c
--- /dev/null
+++ b/cypress/support/views.ts
@@ -0,0 +1,40 @@
+/*
+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 Chainable = Cypress.Chainable;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Opens the given room by name. The room must be visible in the
+ * room list.
+ * @param name The room name to find and click on/open.
+ */
+ viewRoomByName(name: string): Chainable>;
+ }
+ }
+}
+
+Cypress.Commands.add("viewRoomByName", (name: string): Chainable> => {
+ return cy.get(`.mx_RoomTile[aria-label="${name}"]`).click();
+});
+
+// Needed to make this file a module
+export { };
diff --git a/cypress/support/webserver.ts b/cypress/support/webserver.ts
new file mode 100644
index 00000000000..a587e1aa8bf
--- /dev/null
+++ b/cypress/support/webserver.ts
@@ -0,0 +1,52 @@
+/*
+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 Chainable = Cypress.Chainable;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Starts a web server which serves the given HTML.
+ * @param html The HTML to serve
+ * @returns The URL at which the HTML can be accessed.
+ */
+ serveHtmlFile(html: string): Chainable;
+
+ /**
+ * Stops all running web servers.
+ */
+ stopWebServers(): Chainable;
+ }
+ }
+}
+
+function serveHtmlFile(html: string): Chainable {
+ return cy.task("serveHtmlFile", html);
+}
+
+function stopWebServers(): Chainable {
+ return cy.task("stopWebServers");
+}
+
+Cypress.Commands.add("serveHtmlFile", serveHtmlFile);
+Cypress.Commands.add("stopWebServers", stopWebServers);
+
+// Needed to make this file a module
+export { };
diff --git a/test/end-to-end-tests/src/scenario.ts b/test/end-to-end-tests/src/scenario.ts
index fffb2042531..31855f29e8b 100644
--- a/test/end-to-end-tests/src/scenario.ts
+++ b/test/end-to-end-tests/src/scenario.ts
@@ -25,7 +25,6 @@ import { ElementSession } from "./session";
import { RestSessionCreator } from "./rest/creator";
import { RestMultiSession } from "./rest/multi";
import { RestSession } from "./rest/session";
-import { stickerScenarios } from './scenarios/sticker';
export async function scenario(createSession: (s: string) => Promise,
restCreator: RestSessionCreator): Promise {
@@ -51,15 +50,6 @@ 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
deleted file mode 100644
index 554eb2785f1..00000000000
--- a/test/end-to-end-tests/src/scenarios/sticker.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
-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();
-}