diff --git a/cypress/e2e/module-system/roomViewLifecycle.spec.ts b/cypress/e2e/module-system/roomViewLifecycle.spec.ts
new file mode 100644
index 00000000000..09194701129
--- /dev/null
+++ b/cypress/e2e/module-system/roomViewLifecycle.spec.ts
@@ -0,0 +1,68 @@
+/*
+Copyright 2023 Nordeck IT + Consulting GmbH.
+
+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 { HomeserverInstance } from "../../plugins/utils/homeserver";
+
+describe("RoomViewLifecycle", () => {
+ let homeserver: HomeserverInstance;
+
+ beforeEach(() => {
+ cy.window().then((win) => {
+ win.localStorage.setItem("mx_local_settings", '{"language":"en"}'); // Ensure the language is set to a consistent value
+ });
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+
+ cy.intercept(
+ { method: "GET", pathname: "/config.json" },
+ { body: { default_server_config: { "m.homeserver": { base_url: data.baseUrl } } } },
+ );
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should show login without the PreviewRoomNotLoggedIn lifecycle", () => {
+ cy.visit(`/#/room/!example:localhost`);
+
+ cy.contains("Join the conversation with an account");
+ cy.contains("Sign Up");
+ cy.contains("Sign In");
+ });
+
+ it("should show login with the PreviewRoomNotLoggedIn lifecycle", () => {
+ // we must reload the page first, otherwise, the module system settings get lost...
+ cy.visit("/");
+ cy.contains("Welcome");
+
+ // we need to set the data to local storage again because of the reload
+ cy.window().then((win) => {
+ win.localStorage.setItem("mx_local_settings", '{"language":"en"}'); // Ensure the language is set to a consistent value
+ });
+ cy.enableModuleSystem();
+ cy.moduleSystemPreviewRoom("!example:localhost");
+
+ cy.visit(`/#/room/!example:localhost`);
+ cy.reload();
+
+ cy.contains("Join the room to participate");
+ cy.contains("Join");
+ });
+});
diff --git a/cypress/e2e/module-system/translations.spec.ts b/cypress/e2e/module-system/translations.spec.ts
new file mode 100644
index 00000000000..ff84b40819e
--- /dev/null
+++ b/cypress/e2e/module-system/translations.spec.ts
@@ -0,0 +1,44 @@
+/*
+Copyright 2023 Nordeck IT + Consulting GmbH.
+
+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 { HomeserverInstance } from "../../plugins/utils/homeserver";
+
+describe("Custom translations module", () => {
+ let homeserver: HomeserverInstance;
+
+ beforeEach(() => {
+ cy.enableModuleSystem();
+
+ cy.window().then((win) => {
+ win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
+ });
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+
+ cy.initTestUser(homeserver, "Tom");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should override the 'Welcome' translation", () => {
+ cy.get("Howdy Tom (Changed by the Module System)");
+ });
+});
diff --git a/cypress/example_module/ExampleModule.ts b/cypress/example_module/ExampleModule.ts
new file mode 100644
index 00000000000..4a2e76c4aa5
--- /dev/null
+++ b/cypress/example_module/ExampleModule.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 Nordeck IT + Consulting GmbH
+ *
+ * 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 { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi";
+import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
+import {
+ RoomPreviewListener,
+ RoomViewLifecycle,
+} from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
+
+export default class ExampleModule extends RuntimeModule {
+ public constructor(moduleApi: ModuleApi) {
+ super(moduleApi);
+
+ // Only apply the module when it is activated so we don't interfere with
+ // other tests that should not be affected by this module.
+ if (window.localStorage.getItem("cypress_module_system_enable") !== "true") {
+ return;
+ }
+
+ this.moduleApi.registerTranslations({
+ "Welcome %(name)s": {
+ en: "Howdy %(name)s (Changed by the Module System)",
+ },
+ });
+
+ this.on(RoomViewLifecycle.PreviewRoomNotLoggedIn, this.onRoomPreview);
+ }
+
+ protected onRoomPreview: RoomPreviewListener = (opts, roomId) => {
+ if (window.localStorage.getItem("cypress_module_system_preview_room_id") === roomId) {
+ opts.canJoin = true;
+ }
+ };
+}
diff --git a/cypress/example_module/build_config.yaml b/cypress/example_module/build_config.yaml
new file mode 100644
index 00000000000..1e1120ff475
--- /dev/null
+++ b/cypress/example_module/build_config.yaml
@@ -0,0 +1,5 @@
+# This file is included by the `yarn start:e2e` task in element-web. It will
+# register the module in this folder as a module in the dev mode so that the
+# Cypress tests can be executed correctly.
+modules:
+ - "file:node_modules/matrix-react-sdk/cypress/example_module"
diff --git a/cypress/example_module/package.json b/cypress/example_module/package.json
new file mode 100644
index 00000000000..c2a5fe1cf34
--- /dev/null
+++ b/cypress/example_module/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "example_module",
+ "version": "1.0.0",
+ "main": "ExampleModule.ts",
+ "dependencies": {
+ "@matrix-org/react-sdk-module-api": "*"
+ }
+}
diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml
index aaad3420b99..a4eaa6191d9 100644
--- a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml
+++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml
@@ -74,3 +74,5 @@ suppress_key_server_warning: true
ui_auth:
session_timeout: "300s"
+
+allow_guest_access: true
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
index 2ff0197ba65..ece8fc5fc63 100644
--- a/cypress/support/e2e.ts
+++ b/cypress/support/e2e.ts
@@ -40,6 +40,7 @@ import "./network";
import "./composer";
import "./proxy";
import "./axe";
+import "./moduleSystem";
installLogsCollector({
// specify the types of logs to collect (and report to the node console at the end of the test)
diff --git a/cypress/support/moduleSystem.ts b/cypress/support/moduleSystem.ts
new file mode 100644
index 00000000000..34b194f4e0b
--- /dev/null
+++ b/cypress/support/moduleSystem.ts
@@ -0,0 +1,50 @@
+/*
+Copyright 2023 Nordeck IT + Consulting GmbH.
+
+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.
+*/
+
+///
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ // Enables the module system for the current session.
+ enableModuleSystem(): void;
+ moduleSystemPreviewRoom(roomId: string): void;
+ }
+ }
+}
+
+beforeEach(() => {
+ cy.window().then((win) => {
+ win.localStorage.removeItem("cypress_module_system_enable");
+ win.localStorage.removeItem("cypress_module_system_preview_room_id");
+ });
+});
+
+Cypress.Commands.add("enableModuleSystem", (): void => {
+ cy.window().then((win) => {
+ win.localStorage.setItem("cypress_module_system_enable", "true");
+ });
+});
+
+Cypress.Commands.add("moduleSystemPreviewRoom", (roomId: string): void => {
+ cy.window().then((win) => {
+ win.localStorage.setItem("cypress_module_system_preview_room_id", roomId);
+ });
+});
+
+// Needed to make this file a module
+export {};