diff --git a/cypress/e2e/register/register.spec.ts b/cypress/e2e/register/register.spec.ts
deleted file mode 100644
index bb91d43611..0000000000
--- a/cypress/e2e/register/register.spec.ts
+++ /dev/null
@@ -1,152 +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 { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { checkDeviceIsCrossSigned } from "../crypto/utils";
-
-describe("Registration", () => {
- let homeserver: HomeserverInstance;
-
- beforeEach(() => {
- cy.visit("/#/register");
- cy.startHomeserver("consent").then((data) => {
- homeserver = data;
- });
- });
-
- afterEach(() => {
- cy.stopHomeserver(homeserver);
- });
-
- it("registers an account and lands on the home screen", () => {
- cy.injectAxe();
-
- cy.findByRole("button", { name: "Edit", timeout: 15000 }).click();
- cy.findByRole("button", { name: "Continue" }).should("be.visible");
- // Only snapshot the server picker otherwise in the background `matrix.org` may or may not be available
- cy.get(".mx_Dialog").percySnapshotElement("Server Picker", { widths: [516] });
- cy.checkA11y(undefined, {
- rules: {
- // Axe is unhappy with the configuration error's contrast here
- "link-in-text-block": {
- enabled: false,
- },
- },
- });
-
- cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl);
- cy.findByRole("button", { name: "Continue" }).click();
- // wait for the dialog to go away
- cy.get(".mx_ServerPickerDialog").should("not.exist");
-
- cy.findByRole("textbox", { name: "Username" }).should("be.visible");
- // Hide the server text as it contains the randomly allocated Homeserver port
- const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }";
- cy.percySnapshot("Registration", { percyCSS });
- cy.checkA11y();
-
- cy.findByRole("textbox", { name: "Username" }).type("alice");
- cy.findByPlaceholderText("Password").type("totally a great password");
- cy.findByPlaceholderText("Confirm password").type("totally a great password");
- cy.findByRole("button", { name: "Register" }).click();
-
- cy.get(".mx_RegistrationEmailPromptDialog").should("be.visible");
- cy.percySnapshot("Registration email prompt", { percyCSS });
- cy.checkA11y();
- cy.get(".mx_RegistrationEmailPromptDialog").within(() => {
- cy.findByRole("button", { name: "Continue" }).click();
- });
-
- cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy").should("be.visible");
- cy.percySnapshot("Registration terms prompt", { percyCSS });
- cy.checkA11y();
-
- cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy").within(() => {
- cy.findByRole("checkbox").click(); // Click the checkbox before privacy policy anchor link
- cy.findByLabelText("Privacy Policy").should("be.visible");
- });
-
- cy.findByRole("button", { name: "Accept" }).click();
-
- cy.get(".mx_UseCaseSelection_skip", { timeout: 30000 }).should("exist");
- cy.percySnapshot("Use-case selection screen");
- cy.checkA11y();
- cy.findByRole("button", { name: "Skip" }).click();
-
- cy.url().should("contain", "/#/home");
-
- /*
- * Cross-signing checks
- */
-
- // check that the device considers itself verified
- cy.findByRole("button", { name: "User menu" }).click();
- cy.findByRole("menuitem", { name: "All settings" }).click();
- cy.findByRole("tab", { name: "Sessions" }).click();
- cy.findByTestId("current-session-section").within(() => {
- cy.findByTestId("device-metadata-isVerified").should("have.text", "Verified");
- });
-
- // check that cross-signing keys have been uploaded.
- checkDeviceIsCrossSigned();
- });
-
- it("should require username to fulfil requirements and be available", () => {
- cy.findByRole("button", { name: "Edit", timeout: 15000 }).click();
- cy.findByRole("button", { name: "Continue" }).should("be.visible");
- cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl);
- cy.findByRole("button", { name: "Continue" }).click();
- // wait for the dialog to go away
- cy.get(".mx_ServerPickerDialog").should("not.exist");
-
- cy.findByRole("textbox", { name: "Username" }).should("be.visible");
-
- cy.intercept("**/_matrix/client/*/register/available?username=_alice", {
- statusCode: 400,
- headers: {
- "Content-Type": "application/json",
- },
- body: {
- errcode: "M_INVALID_USERNAME",
- error: "User ID may not begin with _",
- },
- });
- cy.findByRole("textbox", { name: "Username" }).type("_alice");
- cy.get(".mx_Field_tooltip")
- .should("have.class", "mx_Tooltip_visible")
- .should("contain.text", "Some characters not allowed");
-
- cy.intercept("**/_matrix/client/*/register/available?username=bob", {
- statusCode: 400,
- headers: {
- "Content-Type": "application/json",
- },
- body: {
- errcode: "M_USER_IN_USE",
- error: "The desired username is already taken",
- },
- });
- cy.findByRole("textbox", { name: "Username" }).type("{selectAll}{backspace}bob");
- cy.get(".mx_Field_tooltip")
- .should("have.class", "mx_Tooltip_visible")
- .should("contain.text", "Someone already has that username");
-
- cy.findByRole("textbox", { name: "Username" }).type("{selectAll}{backspace}foobar");
- cy.get(".mx_Field_tooltip").should("not.have.class", "mx_Tooltip_visible");
- });
-});
diff --git a/playwright.config.ts b/playwright.config.ts
index 1d35402299..eea58e0b72 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -39,11 +39,11 @@ export default defineConfig({
projects: [
{
name: "Legacy Crypto",
- use: { crypto: "legacy" },
+ use: { cryptoBackend: "legacy" },
},
{
name: "Rust Crypto",
- use: { crypto: "rust" },
+ use: { cryptoBackend: "rust" },
},
],
snapshotDir: "playwright/snapshots",
diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts
new file mode 100644
index 0000000000..2e8ec34bc2
--- /dev/null
+++ b/playwright/e2e/register/register.spec.ts
@@ -0,0 +1,124 @@
+/*
+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 { test, expect } from "../../element-web-test";
+
+test.describe("Registration", () => {
+ test.use({ startHomeserverOpts: "consent" });
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/#/register");
+ });
+
+ test("registers an account and lands on the home screen", async ({ homeserver, page, checkA11y, crypto }) => {
+ await page.getByRole("button", { name: "Edit", exact: true }).click();
+ await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
+
+ await expect(page.locator(".mx_Dialog")).toHaveScreenshot("server-picker.png");
+ await checkA11y();
+
+ await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
+ await page.getByRole("button", { name: "Continue", exact: true }).click();
+ // wait for the dialog to go away
+ await expect(page.getByRole("dialog")).not.toBeVisible();
+
+ await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible();
+ // Hide the server text as it contains the randomly allocated Homeserver port
+ const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
+ await expect(page).toHaveScreenshot("registration.png", screenshotOptions);
+ await checkA11y();
+
+ await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice");
+ await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password");
+ await page.getByPlaceholder("Confirm password", { exact: true }).fill("totally a great password");
+ await page.getByRole("button", { name: "Register", exact: true }).click();
+
+ const dialog = page.getByRole("dialog");
+ await expect(dialog).toBeVisible();
+ await expect(page).toHaveScreenshot("email-prompt.png", screenshotOptions);
+ await checkA11y();
+ await dialog.getByRole("button", { name: "Continue", exact: true }).click();
+
+ await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible();
+ await expect(page).toHaveScreenshot("terms-prompt.png", screenshotOptions);
+ await checkA11y();
+
+ const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy");
+ await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link
+ await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible();
+
+ await page.getByRole("button", { name: "Accept", exact: true }).click();
+
+ await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible();
+ await expect(page).toHaveScreenshot("use-case-selection.png", screenshotOptions);
+ await checkA11y();
+ await page.getByRole("button", { name: "Skip", exact: true }).click();
+
+ await expect(page).toHaveURL(/\/#\/home$/);
+
+ /*
+ * Cross-signing checks
+ */
+ // check that the device considers itself verified
+ await page.getByRole("button", { name: "User menu", exact: true }).click();
+ await page.getByRole("menuitem", { name: "All settings", exact: true }).click();
+ await page.getByRole("tab", { name: "Sessions", exact: true }).click();
+ await expect(page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified")).toHaveText(
+ "Verified",
+ );
+
+ // check that cross-signing keys have been uploaded.
+ await crypto.assertDeviceIsCrossSigned();
+ });
+
+ test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => {
+ await page.getByRole("button", { name: "Edit", exact: true }).click();
+ await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
+ await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
+ await page.getByRole("button", { name: "Continue", exact: true }).click();
+ // wait for the dialog to go away
+ await expect(page.getByRole("dialog")).not.toBeVisible();
+
+ await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible();
+
+ await page.route("**/_matrix/client/*/register/available?username=_alice", async (route) => {
+ await route.fulfill({
+ status: 400,
+ json: {
+ errcode: "M_INVALID_USERNAME",
+ error: "User ID may not begin with _",
+ },
+ });
+ });
+ await page.getByRole("textbox", { name: "Username", exact: true }).fill("_alice");
+ await expect(page.getByRole("alert").filter({ hasText: "Some characters not allowed" })).toBeVisible();
+
+ await page.route("**/_matrix/client/*/register/available?username=bob", async (route) => {
+ await route.fulfill({
+ status: 400,
+ json: {
+ errcode: "M_USER_IN_USE",
+ error: "The desired username is already taken",
+ },
+ });
+ });
+ await page.getByRole("textbox", { name: "Username", exact: true }).fill("bob");
+ await expect(page.getByRole("alert").filter({ hasText: "Someone already has that username" })).toBeVisible();
+
+ await page.getByRole("textbox", { name: "Username", exact: true }).fill("foobar");
+ await expect(page.getByRole("alert")).not.toBeVisible();
+ });
+});
diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts
index f235efc282..28956919b0 100644
--- a/playwright/element-web-test.ts
+++ b/playwright/element-web-test.ts
@@ -26,6 +26,7 @@ import { Dendrite, Pinecone } from "./plugins/homeserver/dendrite";
import { Instance } from "./plugins/mailhog";
import { ElementAppPage } from "./pages/ElementAppPage";
import { OAuthServer } from "./plugins/oauth_server";
+import { Crypto } from "./pages/crypto";
import { Toasts } from "./pages/toasts";
const CONFIG_JSON: Partial = {
@@ -45,7 +46,7 @@ const CONFIG_JSON: Partial = {
};
export type TestOptions = {
- crypto: "legacy" | "rust";
+ cryptoBackend: "legacy" | "rust";
};
export const test = base.extend<
@@ -62,15 +63,16 @@ export const test = base.extend<
displayName?: string;
app: ElementAppPage;
mailhog?: { api: mailhog.API; instance: Instance };
+ crypto: Crypto;
toasts: Toasts;
}
>({
- crypto: ["legacy", { option: true }],
+ cryptoBackend: ["legacy", { option: true }],
config: CONFIG_JSON,
- page: async ({ context, page, config, crypto }, use) => {
+ page: async ({ context, page, config, cryptoBackend }, use) => {
await context.route(`http://localhost:8080/config.json*`, async (route) => {
const json = { ...CONFIG_JSON, ...config };
- if (crypto === "rust") {
+ if (cryptoBackend === "rust") {
json["features"] = {
...json["features"],
feature_rust_crypto: true,
@@ -163,6 +165,9 @@ export const test = base.extend<
app: async ({ page }, use) => {
await use(new ElementAppPage(page));
},
+ crypto: async ({ page, homeserver, request }, use) => {
+ await use(new Crypto(page, homeserver, request));
+ },
toasts: async ({ page }, use) => {
await use(new Toasts(page));
},
diff --git a/playwright/pages/crypto.ts b/playwright/pages/crypto.ts
new file mode 100644
index 0000000000..183f5629e8
--- /dev/null
+++ b/playwright/pages/crypto.ts
@@ -0,0 +1,57 @@
+/*
+Copyright 2023 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 { APIRequestContext, Page, expect } from "@playwright/test";
+
+import { HomeserverInstance } from "../plugins/homeserver";
+
+export class Crypto {
+ public constructor(
+ private page: Page,
+ private homeserver: HomeserverInstance,
+ private request: APIRequestContext,
+ ) {}
+
+ /**
+ * Check that the user has published cross-signing keys, and that the user's device has been cross-signed.
+ */
+ public async assertDeviceIsCrossSigned(): Promise {
+ const { userId, deviceId, accessToken } = await this.page.evaluate(() => ({
+ userId: window.mxMatrixClientPeg.get().getUserId(),
+ deviceId: window.mxMatrixClientPeg.get().getDeviceId(),
+ accessToken: window.mxMatrixClientPeg.get().getAccessToken(),
+ }));
+
+ const res = await this.request.post(`${this.homeserver.config.baseUrl}/_matrix/client/v3/keys/query`, {
+ headers: { Authorization: `Bearer ${accessToken}` },
+ data: { device_keys: { [userId]: [] } },
+ });
+ const json = await res.json();
+
+ // there should be three cross-signing keys
+ expect(json.master_keys[userId]).toHaveProperty("keys");
+ expect(json.self_signing_keys[userId]).toHaveProperty("keys");
+ expect(json.user_signing_keys[userId]).toHaveProperty("keys");
+
+ // and the device should be signed by the self-signing key
+ const selfSigningKeyId = Object.keys(json.self_signing_keys[userId].keys)[0];
+
+ expect(json.device_keys[userId][deviceId]).toBeDefined();
+
+ const myDeviceSignatures = json.device_keys[userId][deviceId].signatures[userId];
+ expect(myDeviceSignatures[selfSigningKeyId]).toBeDefined();
+ }
+}
diff --git a/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png
new file mode 100644
index 0000000000..55d820a066
Binary files /dev/null and b/playwright/snapshots/register/register.spec.ts/email-prompt-linux.png differ
diff --git a/playwright/snapshots/register/register.spec.ts/registration-linux.png b/playwright/snapshots/register/register.spec.ts/registration-linux.png
new file mode 100644
index 0000000000..fed14e2c8a
Binary files /dev/null and b/playwright/snapshots/register/register.spec.ts/registration-linux.png differ
diff --git a/playwright/snapshots/register/register.spec.ts/server-picker-linux.png b/playwright/snapshots/register/register.spec.ts/server-picker-linux.png
new file mode 100644
index 0000000000..2378aae2a9
Binary files /dev/null and b/playwright/snapshots/register/register.spec.ts/server-picker-linux.png differ
diff --git a/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png
new file mode 100644
index 0000000000..9efa659592
Binary files /dev/null and b/playwright/snapshots/register/register.spec.ts/terms-prompt-linux.png differ
diff --git a/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png b/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png
new file mode 100644
index 0000000000..c7ed8fc864
Binary files /dev/null and b/playwright/snapshots/register/register.spec.ts/use-case-selection-linux.png differ