diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml
index 360fa2d8bb9..c4bf0ef3be7 100644
--- a/.github/workflows/static_analysis.yaml
+++ b/.github/workflows/static_analysis.yaml
@@ -10,10 +10,11 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
+
env:
- # These must be set for fetchdep.sh to get the right branch
- REPOSITORY: ${{ github.repository }}
+ # fetchdep.sh needs to know our PR number
PR_NUMBER: ${{ github.event.pull_request.number }}
+
jobs:
ts_lint:
name: "Typescript Syntax Check"
@@ -49,12 +50,6 @@ jobs:
permissions:
pull-requests: read
checks: write
- strategy:
- fail-fast: false
- matrix:
- args:
- - "--strict --noImplicitAny"
- - "--noImplicitAny"
steps:
- uses: actions/checkout@v3
with:
@@ -65,7 +60,7 @@ jobs:
- name: Get diff lines
id: diff
- uses: Equip-Collaboration/diff-line-numbers@df70b4b83e05105c15f20dc6cc61f1463411b2a6 # v1.0.0
+ uses: Equip-Collaboration/diff-line-numbers@e752977e2cb4207d671bb9e4dad18c07c1b73d52 # v1.1.0
with:
include: '["\\.tsx?$"]'
@@ -82,7 +77,7 @@ jobs:
use-check: false
check-fail-mode: added
output-behaviour: annotate
- ts-extra-args: ${{ matrix.args }}
+ ts-extra-args: "--strict --noImplicitAny"
files-changed: ${{ steps.files.outputs.files_updated }}
files-added: ${{ steps.files.outputs.files_created }}
files-deleted: ${{ steps.files.outputs.files_deleted }}
diff --git a/.stylelintrc.js b/.stylelintrc.js
index 099a12f09cd..259c626deef 100644
--- a/.stylelintrc.js
+++ b/.stylelintrc.js
@@ -33,6 +33,11 @@ module.exports = {
"import-notation": null,
"value-keyword-case": null,
"declaration-block-no-redundant-longhand-properties": null,
+ "declaration-block-no-duplicate-properties": [
+ true,
+ // useful for fallbacks
+ { ignore: ["consecutive-duplicates-with-different-values"] },
+ ],
"shorthand-property-no-redundant-values": null,
"property-no-vendor-prefix": null,
"value-no-vendor-prefix": null,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 40cf7259ca2..18942f5e65e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,90 @@
+Changes in [3.73.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.73.0) (2023-06-06)
+=====================================================================================================
+
+## ✨ Features
+ * When joining room in sub-space join the parents too ([\#11011](https://github.com/matrix-org/matrix-react-sdk/pull/11011)).
+ * Include thread replies in message previews ([\#10631](https://github.com/matrix-org/matrix-react-sdk/pull/10631)). Fixes vector-im/element-web#23920.
+ * Use semantic headings in space preferences ([\#11021](https://github.com/matrix-org/matrix-react-sdk/pull/11021)). Contributed by @kerryarchibald.
+ * Use semantic headings in user settings - Ignored users ([\#11006](https://github.com/matrix-org/matrix-react-sdk/pull/11006)). Contributed by @kerryarchibald.
+ * Use semantic headings in user settings - profile ([\#10973](https://github.com/matrix-org/matrix-react-sdk/pull/10973)). Fixes vector-im/element-web#25461. Contributed by @kerryarchibald.
+ * Use semantic headings in user settings - account ([\#10972](https://github.com/matrix-org/matrix-react-sdk/pull/10972)). Contributed by @kerryarchibald.
+ * Support `Insert from iPhone or iPad` in Safari ([\#10851](https://github.com/matrix-org/matrix-react-sdk/pull/10851)). Fixes vector-im/element-web#25327. Contributed by @SuperKenVery.
+ * Specify supportedStages for User Interactive Auth ([\#10975](https://github.com/matrix-org/matrix-react-sdk/pull/10975)). Fixes vector-im/element-web#19605.
+ * Pass device id to widgets ([\#10209](https://github.com/matrix-org/matrix-react-sdk/pull/10209)). Contributed by @Fox32.
+ * Use semantic headings in user settings - discovery ([\#10838](https://github.com/matrix-org/matrix-react-sdk/pull/10838)). Contributed by @kerryarchibald.
+ * Use semantic headings in user settings - Notifications ([\#10948](https://github.com/matrix-org/matrix-react-sdk/pull/10948)). Contributed by @kerryarchibald.
+ * Use semantic headings in user settings - spellcheck and language ([\#10959](https://github.com/matrix-org/matrix-react-sdk/pull/10959)). Contributed by @kerryarchibald.
+ * Use semantic headings in user settings Appearance ([\#10827](https://github.com/matrix-org/matrix-react-sdk/pull/10827)). Contributed by @kerryarchibald.
+ * Use semantic heading in user settings Sidebar & Voip ([\#10782](https://github.com/matrix-org/matrix-react-sdk/pull/10782)). Contributed by @kerryarchibald.
+ * Use semantic headings in user settings Security ([\#10774](https://github.com/matrix-org/matrix-react-sdk/pull/10774)). Contributed by @kerryarchibald.
+ * Use semantic headings in user settings - integrations and account deletion ([\#10837](https://github.com/matrix-org/matrix-react-sdk/pull/10837)). Fixes vector-im/element-web#25378. Contributed by @kerryarchibald.
+ * Use semantic headings in user settings Preferences ([\#10794](https://github.com/matrix-org/matrix-react-sdk/pull/10794)). Contributed by @kerryarchibald.
+ * Use semantic headings in user settings Keyboard ([\#10793](https://github.com/matrix-org/matrix-react-sdk/pull/10793)). Contributed by @kerryarchibald.
+ * RTE plain text mentions as pills ([\#10852](https://github.com/matrix-org/matrix-react-sdk/pull/10852)). Contributed by @alunturner.
+ * Use semantic headings in user settings Labs ([\#10773](https://github.com/matrix-org/matrix-react-sdk/pull/10773)). Contributed by @kerryarchibald.
+ * Use semantic list elements for menu lists and tab lists ([\#10902](https://github.com/matrix-org/matrix-react-sdk/pull/10902)). Fixes vector-im/element-web#24928.
+ * Fix aria-required-children axe violation ([\#10900](https://github.com/matrix-org/matrix-react-sdk/pull/10900)). Fixes vector-im/element-web#25342.
+ * Enable pagination for overlay timelines ([\#10757](https://github.com/matrix-org/matrix-react-sdk/pull/10757)). Fixes vector-im/voip-internal#107.
+ * Add tooltip to disabled invite button due to lack of permissions ([\#10869](https://github.com/matrix-org/matrix-react-sdk/pull/10869)). Fixes vector-im/element-web#9824.
+ * Respect configured auth_header_logo_url for default Welcome page ([\#10870](https://github.com/matrix-org/matrix-react-sdk/pull/10870)).
+ * Specify lazy loading for avatars ([\#10866](https://github.com/matrix-org/matrix-react-sdk/pull/10866)). Fixes vector-im/element-web#1983.
+ * Room and user mentions for plain text editor ([\#10665](https://github.com/matrix-org/matrix-react-sdk/pull/10665)). Contributed by @alunturner.
+ * Add audible notifcation on broadcast error ([\#10654](https://github.com/matrix-org/matrix-react-sdk/pull/10654)). Fixes vector-im/element-web#25132.
+ * Fall back from server generated thumbnail to original image ([\#10853](https://github.com/matrix-org/matrix-react-sdk/pull/10853)).
+ * Use semantically correct elements for room sublist context menu ([\#10831](https://github.com/matrix-org/matrix-react-sdk/pull/10831)). Fixes vector-im/customer-retainer#46.
+ * Avoid calling prepareToEncrypt onKeyDown ([\#10828](https://github.com/matrix-org/matrix-react-sdk/pull/10828)).
+ * Allows search to recognize full room links ([\#8275](https://github.com/matrix-org/matrix-react-sdk/pull/8275)). Contributed by @bolu-tife.
+ * "Show rooms with unread messages first" should not be on by default for new users ([\#10820](https://github.com/matrix-org/matrix-react-sdk/pull/10820)). Fixes vector-im/element-web#25304. Contributed by @kerryarchibald.
+ * Fix emitter handler leak in ThreadView ([\#10803](https://github.com/matrix-org/matrix-react-sdk/pull/10803)).
+ * Add better error for email invites without identity server ([\#10739](https://github.com/matrix-org/matrix-react-sdk/pull/10739)). Fixes vector-im/element-web#16893.
+ * Move reaction message previews out of labs ([\#10601](https://github.com/matrix-org/matrix-react-sdk/pull/10601)). Fixes vector-im/element-web#25083.
+ * Sort muted rooms to the bottom of their section of the room list ([\#10592](https://github.com/matrix-org/matrix-react-sdk/pull/10592)). Fixes vector-im/element-web#25131. Contributed by @kerryarchibald.
+ * Use semantic headings in user settings Help & About ([\#10752](https://github.com/matrix-org/matrix-react-sdk/pull/10752)). Contributed by @kerryarchibald.
+ * use ExternalLink components for external links ([\#10758](https://github.com/matrix-org/matrix-react-sdk/pull/10758)). Contributed by @kerryarchibald.
+ * Use semantic headings in space settings ([\#10751](https://github.com/matrix-org/matrix-react-sdk/pull/10751)). Contributed by @kerryarchibald.
+ * Use semantic headings for room settings content ([\#10734](https://github.com/matrix-org/matrix-react-sdk/pull/10734)). Contributed by @kerryarchibald.
+
+## 🐛 Bug Fixes
+ * Use consistent fonts for Japanese text ([\#10980](https://github.com/matrix-org/matrix-react-sdk/pull/10980)). Fixes vector-im/element-web#22333 and vector-im/element-web#23899.
+ * Fix: server picker validates unselected option ([\#11020](https://github.com/matrix-org/matrix-react-sdk/pull/11020)). Fixes vector-im/element-web#25488. Contributed by @kerryarchibald.
+ * Fix room list notification badges going missing in compact layout ([\#11022](https://github.com/matrix-org/matrix-react-sdk/pull/11022)). Fixes vector-im/element-web#25372.
+ * Fix call to `startSingleSignOn` passing enum in place of idpId ([\#10998](https://github.com/matrix-org/matrix-react-sdk/pull/10998)). Fixes vector-im/element-web#24953.
+ * Remove hover effect from user name on a DM creation UI ([\#10887](https://github.com/matrix-org/matrix-react-sdk/pull/10887)). Fixes vector-im/element-web#25305. Contributed by @luixxiul.
+ * Fix layout regression in public space invite dialog ([\#11009](https://github.com/matrix-org/matrix-react-sdk/pull/11009)). Fixes vector-im/element-web#25458.
+ * Fix layout regression in session dropdown ([\#10999](https://github.com/matrix-org/matrix-react-sdk/pull/10999)). Fixes vector-im/element-web#25448.
+ * Fix spacing regression in user settings - roles & permissions ([\#10993](https://github.com/matrix-org/matrix-react-sdk/pull/10993)). Fixes vector-im/element-web#25447 and vector-im/element-web#25451. Contributed by @kerryarchibald.
+ * Fall back to receipt timestamp if we have no event (react-sdk part) ([\#10974](https://github.com/matrix-org/matrix-react-sdk/pull/10974)). Fixes vector-im/element-web#10954. Contributed by @andybalaam.
+ * Fix: Room header 'view your device list' does not link to new session manager ([\#10979](https://github.com/matrix-org/matrix-react-sdk/pull/10979)). Fixes vector-im/element-web#25440. Contributed by @kerryarchibald.
+ * Fix display of devices without encryption support in Settings dialog ([\#10977](https://github.com/matrix-org/matrix-react-sdk/pull/10977)). Fixes vector-im/element-web#25413.
+ * Use aria descriptions instead of labels for TextWithTooltip ([\#10952](https://github.com/matrix-org/matrix-react-sdk/pull/10952)). Fixes vector-im/element-web#25398.
+ * Use grapheme-splitter instead of lodash for saving emoji from being ripped apart ([\#10976](https://github.com/matrix-org/matrix-react-sdk/pull/10976)). Fixes vector-im/element-web#22196.
+ * Fix: content overflow in settings subsection ([\#10960](https://github.com/matrix-org/matrix-react-sdk/pull/10960)). Fixes vector-im/element-web#25416. Contributed by @kerryarchibald.
+ * Make `Privacy Notice` external link on integration manager ToS clickable ([\#10914](https://github.com/matrix-org/matrix-react-sdk/pull/10914)). Fixes vector-im/element-web#25384. Contributed by @luixxiul.
+ * Ensure that open message context menus are updated when the event is sent ([\#10950](https://github.com/matrix-org/matrix-react-sdk/pull/10950)).
+ * Ensure that open sticker picker dialogs are updated when the widget configuration is updated. ([\#10945](https://github.com/matrix-org/matrix-react-sdk/pull/10945)).
+ * Fix big emoji in replies ([\#10932](https://github.com/matrix-org/matrix-react-sdk/pull/10932)). Fixes vector-im/element-web#24798.
+ * Hide empty `MessageActionBar` on message edit history dialog ([\#10447](https://github.com/matrix-org/matrix-react-sdk/pull/10447)). Fixes vector-im/element-web#24903. Contributed by @luixxiul.
+ * Fix roving tab index getting confused after dragging space order ([\#10901](https://github.com/matrix-org/matrix-react-sdk/pull/10901)).
+ * Ignore edits in message previews when they concern messages other than latest ([\#10868](https://github.com/matrix-org/matrix-react-sdk/pull/10868)). Fixes vector-im/element-web#14872.
+ * Send correct receipts when viewing a room ([\#10864](https://github.com/matrix-org/matrix-react-sdk/pull/10864)). Fixes vector-im/element-web#25196.
+ * Fix timeline search bar being overlapped by the right panel ([\#10809](https://github.com/matrix-org/matrix-react-sdk/pull/10809)). Fixes vector-im/element-web#25291. Contributed by @luixxiul.
+ * Fix the state shown for call in rooms ([\#10833](https://github.com/matrix-org/matrix-react-sdk/pull/10833)).
+ * Add string for membership event where both displayname & avatar change ([\#10880](https://github.com/matrix-org/matrix-react-sdk/pull/10880)). Fixes vector-im/element-web#18026.
+ * Fix people space notification badge not updating for new DM invites ([\#10849](https://github.com/matrix-org/matrix-react-sdk/pull/10849)). Fixes vector-im/element-web#23248.
+ * Fix regression in emoji picker order mangling after clearing filter ([\#10854](https://github.com/matrix-org/matrix-react-sdk/pull/10854)). Fixes vector-im/element-web#25323.
+ * Fix: Edit history modal crash ([\#10834](https://github.com/matrix-org/matrix-react-sdk/pull/10834)). Fixes vector-im/element-web#25309. Contributed by @kerryarchibald.
+ * Fix long room address and name not being clipped on room info card and update `_RoomSummaryCard.pcss` ([\#10811](https://github.com/matrix-org/matrix-react-sdk/pull/10811)). Fixes vector-im/element-web#25293. Contributed by @luixxiul.
+ * Treat thumbnail upload failures as complete upload failures ([\#10829](https://github.com/matrix-org/matrix-react-sdk/pull/10829)). Fixes vector-im/element-web#7069.
+ * Update finite automata to match user identifiers as per spec ([\#10798](https://github.com/matrix-org/matrix-react-sdk/pull/10798)). Fixes vector-im/element-web#25246.
+ * Fix icon on empty notification panel ([\#10817](https://github.com/matrix-org/matrix-react-sdk/pull/10817)). Fixes vector-im/element-web#25298 and vector-im/element-web#25302. Contributed by @luixxiul.
+ * Fix: Threads button is highlighted when I create a new room ([\#10819](https://github.com/matrix-org/matrix-react-sdk/pull/10819)). Fixes vector-im/element-web#25284. Contributed by @kerryarchibald.
+ * Fix the top heading of notification panel ([\#10818](https://github.com/matrix-org/matrix-react-sdk/pull/10818)). Fixes vector-im/element-web#25303. Contributed by @luixxiul.
+ * Fix the color of the verified E2EE icon on `RoomSummaryCard` ([\#10812](https://github.com/matrix-org/matrix-react-sdk/pull/10812)). Fixes vector-im/element-web#25295. Contributed by @luixxiul.
+ * Fix: No feedback when waiting for the server on a /delete_devices request with SSO ([\#10795](https://github.com/matrix-org/matrix-react-sdk/pull/10795)). Fixes vector-im/element-web#23096. Contributed by @kerryarchibald.
+ * Fix: reveal images when image previews are disabled ([\#10781](https://github.com/matrix-org/matrix-react-sdk/pull/10781)). Fixes vector-im/element-web#25271. Contributed by @kerryarchibald.
+ * Fix accessibility issues around the room list and space panel ([\#10717](https://github.com/matrix-org/matrix-react-sdk/pull/10717)). Fixes vector-im/element-web#13345.
+ * Ensure tooltip contents is linked via aria to the target element ([\#10729](https://github.com/matrix-org/matrix-react-sdk/pull/10729)). Fixes vector-im/customer-retainer#43.
+
Changes in [3.72.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.72.0) (2023-05-10)
=====================================================================================================
diff --git a/__mocks__/maplibre-gl.js b/__mocks__/maplibre-gl.js
index 77ee0e9a023..6c94f7c77bd 100644
--- a/__mocks__/maplibre-gl.js
+++ b/__mocks__/maplibre-gl.js
@@ -15,7 +15,7 @@ limitations under the License.
*/
const EventEmitter = require("events");
-const { LngLat, NavigationControl, LngLatBounds, AttributionControl } = require("maplibre-gl");
+const { LngLat, NavigationControl, LngLatBounds } = require("maplibre-gl");
class MockMap extends EventEmitter {
addControl = jest.fn();
diff --git a/cypress/e2e/audio-player/audio-player.spec.ts b/cypress/e2e/audio-player/audio-player.spec.ts
index 41f7f1a8568..a59fb64ab46 100644
--- a/cypress/e2e/audio-player/audio-player.spec.ts
+++ b/cypress/e2e/audio-player/audio-player.spec.ts
@@ -29,7 +29,7 @@ describe("Audio player", () => {
".mx_SeekBar, " +
// Exclude various components from the snapshot, for consistency
".mx_JumpToBottomButton, " +
- ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
+ ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
const uploadFile = (file: string) => {
// Upload a file from the message composer
@@ -176,7 +176,7 @@ describe("Audio player", () => {
// Enable high contrast manually
cy.openUserSettings("Appearance")
- .get(".mx_ThemeChoicePanel")
+ .findByTestId("mx_ThemeChoicePanel")
.findByLabelText("Use high contrast")
.click({ force: true }); // force click because the size of the checkbox is zero
@@ -333,30 +333,33 @@ describe("Audio player", () => {
// On a thread
cy.get(".mx_ThreadView").within(() => {
- cy.get(".mx_EventTile_last")
- .within(() => {
- // Assert that the player is correctly rendered on a thread
- cy.get(".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container").within(() => {
- // Assert that the counter is zero before clicking the play button
- cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
-
- // Find and click "Play" button, the wait is to make the test less flaky
- cy.findByRole("button", { name: "Play" }).should("exist");
- cy.wait(500).findByRole("button", { name: "Play" }).click();
-
- // Assert that "Pause" button can be found
- cy.findByRole("button", { name: "Pause" }).should("exist");
-
- // Assert that the timer is reset when the audio file finished playing
- cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
-
- // Assert that "Play" button can be found
- cy.findByRole("button", { name: "Play" }).should("exist").should("not.have.attr", "disabled");
- });
- })
- .realHover()
- .findByRole("button", { name: "Reply" })
- .click(); // Find and click "Reply" button
+ cy.get(".mx_EventTile_last").within(() => {
+ // Assert that the player is correctly rendered on a thread
+ cy.get(".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container").within(() => {
+ // Assert that the counter is zero before clicking the play button
+ cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
+
+ // Find and click "Play" button, the wait is to make the test less flaky
+ cy.findByRole("button", { name: "Play" }).should("exist");
+ cy.wait(500).findByRole("button", { name: "Play" }).click();
+
+ // Assert that "Pause" button can be found
+ cy.findByRole("button", { name: "Pause" }).should("exist");
+
+ // Assert that the timer is reset when the audio file finished playing
+ cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
+
+ // Assert that "Play" button can be found
+ cy.findByRole("button", { name: "Play" }).should("exist").should("not.have.attr", "disabled");
+ });
+ });
+
+ // Find and click "Reply" button
+ //
+ // Calling cy.get(".mx_EventTile_last") again here is a workaround for
+ // https://github.com/matrix-org/matrix-js-sdk/issues/3394: the event tile may have been re-mounted while
+ // the audio was playing.
+ cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Reply" }).click();
cy.get(".mx_MessageComposer--compact").within(() => {
// Assert that the reply preview is rendered on the message composer
diff --git a/cypress/e2e/composer/composer.spec.ts b/cypress/e2e/composer/composer.spec.ts
index 85d1477116c..2b49b5e32e0 100644
--- a/cypress/e2e/composer/composer.spec.ts
+++ b/cypress/e2e/composer/composer.spec.ts
@@ -15,9 +15,11 @@ limitations under the License.
*/
///
+import { EventType } from "matrix-js-sdk/src/@types/event";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { SettingLevel } from "../../../src/settings/SettingLevel";
+import { MatrixClient } from "../../global";
describe("Composer", () => {
let homeserver: HomeserverInstance;
@@ -181,6 +183,83 @@ describe("Composer", () => {
});
});
+ describe("Mentions", () => {
+ // TODO add tests for rich text mode
+
+ describe("Plain text mode", () => {
+ it("autocomplete behaviour tests", () => {
+ // Setup a private room so we have another user to mention
+ const otherUserName = "Bob";
+ let bobClient: MatrixClient;
+ cy.getBot(homeserver, {
+ displayName: otherUserName,
+ }).then((bob) => {
+ bobClient = bob;
+ });
+ // create DM with bob
+ cy.getClient().then(async (cli) => {
+ const bobRoom = await cli.createRoom({ is_direct: true });
+ await cli.invite(bobRoom.room_id, bobClient.getUserId());
+ await cli.setAccountData("m.direct" as EventType, {
+ [bobClient.getUserId()]: [bobRoom.room_id],
+ });
+ });
+
+ cy.viewRoomByName("Bob");
+
+ // Select plain text mode after composer is ready
+ cy.get("div[contenteditable=true]").should("exist");
+ cy.findByRole("button", { name: "Hide formatting" }).click();
+
+ // Typing a single @ does not display the autocomplete menu and contents
+ cy.findByRole("textbox").type("@");
+ cy.findByTestId("autocomplete-wrapper").should("be.empty");
+
+ // Entering the first letter of the other user's name opens the autocomplete...
+ cy.findByRole("textbox").type(otherUserName.slice(0, 1));
+ cy.findByTestId("autocomplete-wrapper")
+ .should("not.be.empty")
+ .within(() => {
+ // ...with the other user name visible, and clicking that username...
+ cy.findByText(otherUserName).should("exist").click();
+ });
+ // ...inserts the username into the composer
+ cy.findByRole("textbox").within(() => {
+ cy.findByText(otherUserName, { exact: false })
+ .should("exist")
+ .should("have.attr", "contenteditable", "false")
+ .should("have.attr", "data-mention-type", "user");
+ });
+
+ // Send the message to clear the composer
+ cy.findByRole("button", { name: "Send message" }).click();
+
+ // Typing an @, then other user's name, then trailing space closes the autocomplete
+ cy.findByRole("textbox").type(`@${otherUserName} `);
+ cy.findByTestId("autocomplete-wrapper").should("be.empty");
+
+ // Send the message to clear the composer
+ cy.findByRole("button", { name: "Send message" }).click();
+
+ // Moving the cursor back to an "incomplete" mention opens the autocomplete
+ cy.findByRole("textbox").type(`initial text @${otherUserName.slice(0, 1)} abc`);
+ cy.findByTestId("autocomplete-wrapper").should("be.empty");
+ // Move the cursor left by 4 to put it to: `@B| abc`, check autocomplete displays
+ cy.findByRole("textbox").type(`${"{leftArrow}".repeat(4)}`);
+ cy.findByTestId("autocomplete-wrapper").should("not.be.empty");
+
+ // Selecting the autocomplete option using Enter inserts it into the composer
+ cy.findByRole("textbox").type(`{Enter}`);
+ cy.findByRole("textbox").within(() => {
+ cy.findByText(otherUserName, { exact: false })
+ .should("exist")
+ .should("have.attr", "contenteditable", "false")
+ .should("have.attr", "data-mention-type", "user");
+ });
+ });
+ });
+ });
+
it("sends a message when you click send or press Enter", () => {
// Type a message
cy.get("div[contenteditable=true]").type("my message 0");
diff --git a/cypress/e2e/create-room/create-room.spec.ts b/cypress/e2e/create-room/create-room.spec.ts
index 7596df087ff..d51e683abf4 100644
--- a/cypress/e2e/create-room/create-room.spec.ts
+++ b/cypress/e2e/create-room/create-room.spec.ts
@@ -64,31 +64,4 @@ describe("Create Room", () => {
cy.findByText(topic);
});
});
-
- it("should create a room with a long room name, which is displayed with ellipsis", () => {
- let roomId: string;
- const LONG_ROOM_NAME =
- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
- "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
- "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " +
- "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
- "officia deserunt mollit anim id est laborum.";
-
- cy.createRoom({ name: LONG_ROOM_NAME }).then((_roomId) => {
- roomId = _roomId;
- cy.visit("/#/room/" + roomId);
- });
-
- // Wait until the room name is set
- cy.get(".mx_RoomHeader_nametext").contains("Lorem ipsum");
-
- // Make sure size of buttons on RoomHeader (except .mx_RoomHeader_name) are specified
- // and the buttons are not compressed
- // TODO: use a same class name
- cy.get(".mx_RoomHeader_button").should("have.css", "height", "32px").should("have.css", "width", "32px");
- cy.get(".mx_HeaderButtons > .mx_RightPanel_headerButton")
- .should("have.css", "height", "32px")
- .should("have.css", "width", "32px");
- cy.get(".mx_RoomHeader").percySnapshotElement("Room header with a long room name");
- });
});
diff --git a/cypress/e2e/crypto/complete-security.spec.ts b/cypress/e2e/crypto/complete-security.spec.ts
index 0838abd4590..b598829b86a 100644
--- a/cypress/e2e/crypto/complete-security.spec.ts
+++ b/cypress/e2e/crypto/complete-security.spec.ts
@@ -16,8 +16,9 @@ limitations under the License.
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { handleVerificationRequest, waitForVerificationRequest } from "./utils";
+import { handleVerificationRequest, logIntoElement, waitForVerificationRequest } from "./utils";
import { CypressBot } from "../../support/bot";
+import { skipIfRustCrypto } from "../../support/util";
describe("Complete security", () => {
let homeserver: HomeserverInstance;
@@ -46,6 +47,8 @@ describe("Complete security", () => {
});
it("should walk through device verification if we have a signed device", () => {
+ skipIfRustCrypto();
+
// create a new user, and have it bootstrap cross-signing
let botClient: CypressBot;
cy.getBot(homeserver, { displayName: "Jeff" })
@@ -66,7 +69,6 @@ describe("Complete security", () => {
// accept the verification request on the "bot" side
cy.wrap(botVerificationRequestPromise).then(async (verificationRequest: VerificationRequest) => {
- await verificationRequest.accept();
await handleVerificationRequest(verificationRequest);
});
@@ -80,22 +82,3 @@ describe("Complete security", () => {
});
});
});
-
-/**
- * Fill in the login form in element with the given creds
- */
-function logIntoElement(homeserverUrl: string, username: string, password: string) {
- cy.visit("/#/login");
-
- // select homeserver
- cy.findByRole("button", { name: "Edit" }).click();
- cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl);
- 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" }).type(username);
- cy.findByPlaceholderText("Password").type(password);
- cy.findByRole("button", { name: "Sign in" }).click();
-}
diff --git a/cypress/e2e/crypto/crypto.spec.ts b/cypress/e2e/crypto/crypto.spec.ts
index 27c9531d44a..17975e88dab 100644
--- a/cypress/e2e/crypto/crypto.spec.ts
+++ b/cypress/e2e/crypto/crypto.spec.ts
@@ -19,7 +19,14 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/
import type { CypressBot } from "../../support/bot";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { UserCredentials } from "../../support/login";
-import { EmojiMapping, handleVerificationRequest, waitForVerificationRequest } from "./utils";
+import {
+ checkDeviceIsCrossSigned,
+ EmojiMapping,
+ handleVerificationRequest,
+ logIntoElement,
+ waitForVerificationRequest,
+} from "./utils";
+import { skipIfRustCrypto } from "../../support/util";
interface CryptoTestContext extends Mocha.Context {
homeserver: HomeserverInstance;
@@ -103,6 +110,27 @@ function autoJoin(client: MatrixClient) {
});
}
+/**
+ * Given a VerificationRequest in a bot client, add cypress commands to:
+ * - wait for the bot to receive a 'verify by emoji' notification
+ * - check that the bot sees the same emoji as the application
+ *
+ * @param botVerificationRequest - a verification request in a bot client
+ */
+function doTwoWaySasVerification(botVerificationRequest: VerificationRequest): void {
+ // on the bot side, wait for the emojis, confirm they match, and return them
+ const emojiPromise = handleVerificationRequest(botVerificationRequest);
+
+ // then, check that our application shows an emoji panel with the same emojis.
+ cy.wrap(emojiPromise).then((emojis: EmojiMapping[]) => {
+ cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
+ emojis.forEach((emoji: EmojiMapping, index: number) => {
+ expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
+ });
+ });
+ });
+}
+
const verify = function (this: CryptoTestContext) {
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
@@ -111,21 +139,9 @@ const verify = function (this: CryptoTestContext) {
cy.findByText("Bob").click();
cy.findByRole("button", { name: "Verify" }).click();
cy.findByRole("button", { name: "Start Verification" }).click();
- cy.wrap(bobsVerificationRequestPromise)
- .then((verificationRequest: VerificationRequest) => {
- verificationRequest.accept();
- return verificationRequest;
- })
- .as("bobsVerificationRequest");
cy.findByRole("button", { name: "Verify by emoji" }).click();
- cy.get("@bobsVerificationRequest").then((request: VerificationRequest) => {
- return cy.wrap(handleVerificationRequest(request)).then((emojis: EmojiMapping[]) => {
- cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
- emojis.forEach((emoji: EmojiMapping, index: number) => {
- expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
- });
- });
- });
+ cy.wrap(bobsVerificationRequestPromise).then((request: VerificationRequest) => {
+ doTwoWaySasVerification(request);
});
cy.findByRole("button", { name: "They match" }).click();
cy.findByText("You've successfully verified Bob!").should("exist");
@@ -143,7 +159,11 @@ describe("Cryptography", function () {
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
aliceCredentials = credentials;
});
- cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob");
+ cy.getBot(homeserver, {
+ displayName: "Bob",
+ autoAcceptInvites: false,
+ userIdPrefix: "bob_",
+ }).as("bob");
});
});
@@ -152,6 +172,7 @@ describe("Cryptography", function () {
});
it("setting up secure key backup should work", () => {
+ skipIfRustCrypto();
cy.openUserSettings("Security & Privacy");
cy.findByRole("button", { name: "Set up Secure Backup" }).click();
cy.get(".mx_Dialog").within(() => {
@@ -175,6 +196,7 @@ describe("Cryptography", function () {
});
it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) {
+ skipIfRustCrypto();
cy.bootstrapCrossSigning(aliceCredentials);
startDMWithBob.call(this);
// send first message
@@ -183,9 +205,20 @@ describe("Cryptography", function () {
bobJoin.call(this);
testMessages.call(this);
verify.call(this);
+
+ // Assert that verified icon is rendered
+ cy.findByRole("button", { name: "Room members" }).click();
+ cy.findByRole("button", { name: "Room information" }).click();
+ cy.get(".mx_RoomSummaryCard_e2ee_verified").should("exist");
+
+ // Take a snapshot of RoomSummaryCard with a verified E2EE icon
+ cy.get(".mx_RightPanel").percySnapshotElement("RoomSummaryCard - with a verified E2EE icon", {
+ widths: [264], // Emulate the UI. The value is based on minWidth specified on MainSplit.tsx
+ });
});
it("should allow verification when there is no existing DM", function (this: CryptoTestContext) {
+ skipIfRustCrypto();
cy.bootstrapCrossSigning(aliceCredentials);
autoJoin(this.bob);
@@ -204,6 +237,7 @@ describe("Cryptography", function () {
});
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
+ skipIfRustCrypto();
cy.bootstrapCrossSigning(aliceCredentials);
// bob has a second, not cross-signed, device
@@ -290,3 +324,67 @@ describe("Cryptography", function () {
});
});
});
+
+describe("Verify own device", () => {
+ let aliceBotClient: CypressBot;
+ let homeserver: HomeserverInstance;
+
+ beforeEach(() => {
+ cy.startHomeserver("default").then((data: HomeserverInstance) => {
+ homeserver = data;
+
+ // Visit the login page of the app, to load the matrix sdk
+ cy.visit("/#/login");
+
+ // wait for the page to load
+ cy.window({ log: false }).should("have.property", "matrixcs");
+
+ // Create a new device for alice
+ cy.getBot(homeserver, { bootstrapCrossSigning: true }).then((bot) => {
+ aliceBotClient = bot;
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ /* Click the "Verify with another device" button, and have the bot client auto-accept it.
+ *
+ * Stores the incoming `VerificationRequest` on the bot client as `@verificationRequest`.
+ */
+ function initiateAliceVerificationRequest() {
+ // alice bot waits for verification request
+ const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient);
+
+ // Click on "Verify with another device"
+ cy.get(".mx_AuthPage").within(() => {
+ cy.findByRole("button", { name: "Verify with another device" }).click();
+ });
+
+ // alice bot responds yes to verification request from alice
+ cy.wrap(promiseVerificationRequest).as("verificationRequest");
+ }
+
+ it("with SAS", function (this: CryptoTestContext) {
+ logIntoElement(homeserver.baseUrl, aliceBotClient.getUserId(), aliceBotClient.__cypress_password);
+
+ // Launch the verification request between alice and the bot
+ initiateAliceVerificationRequest();
+
+ // Handle emoji SAS verification
+ cy.get(".mx_InfoDialog").within(() => {
+ cy.get("@verificationRequest").then((request: VerificationRequest) => {
+ // Handle emoji request and check that emojis are matching
+ doTwoWaySasVerification(request);
+ });
+
+ cy.findByRole("button", { name: "They match" }).click();
+ cy.findByRole("button", { name: "Got it" }).click();
+ });
+
+ // Check that our device is now cross-signed
+ checkDeviceIsCrossSigned();
+ });
+});
diff --git a/cypress/e2e/crypto/decryption-failure.spec.ts b/cypress/e2e/crypto/decryption-failure.spec.ts
index a9ace36c22a..4de2af0e818 100644
--- a/cypress/e2e/crypto/decryption-failure.spec.ts
+++ b/cypress/e2e/crypto/decryption-failure.spec.ts
@@ -15,11 +15,11 @@ limitations under the License.
*/
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
-import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { UserCredentials } from "../../support/login";
-import Chainable = Cypress.Chainable;
+import { handleVerificationRequest } from "./utils";
+import { skipIfRustCrypto } from "../../support/util";
const ROOM_NAME = "Test room";
const TEST_USER = "Alia";
@@ -39,24 +39,6 @@ const waitForVerificationRequest = (cli: MatrixClient): Promise => {
- return cy.wrap(
- new Promise((resolve) => {
- const onShowSas = (event: ISasEvent) => {
- verifier.off("show_sas", onShowSas);
- event.confirm();
- resolve(event.sas.emoji);
- };
-
- const verifier = request.beginKeyVerification("m.sas.v1");
- verifier.on("show_sas", onShowSas);
- verifier.verify();
- }),
- // extra timeout, as this sometimes takes a while
- { timeout: 30_000 },
- );
-};
-
const checkTimelineNarrow = (button = true) => {
cy.viewport(800, 600); // SVGA
cy.get(".mx_LeftPanel_minimized").should("exist"); // Wait until the left panel is minimized
@@ -86,6 +68,7 @@ describe("Decryption Failure Bar", () => {
let roomId: string;
beforeEach(function () {
+ skipIfRustCrypto();
cy.startHomeserver("default").then((hs: HomeserverInstance) => {
homeserver = hs;
cy.initTestUser(homeserver, TEST_USER)
@@ -161,7 +144,11 @@ describe("Decryption Failure Bar", () => {
);
cy.wrap(verificationRequestPromise).then((verificationRequest: VerificationRequest) => {
cy.wrap(verificationRequest.accept());
- handleVerificationRequest(verificationRequest).then((emojis) => {
+ cy.wrap(
+ handleVerificationRequest(verificationRequest),
+ // extra timeout, as this sometimes takes a while
+ { timeout: 30_000 },
+ ).then((emojis: EmojiMapping[]) => {
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
emojis.forEach((emoji: EmojiMapping, index: number) => {
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
diff --git a/cypress/e2e/crypto/utils.ts b/cypress/e2e/crypto/utils.ts
index 6f99a23d0fd..3e91d1e93db 100644
--- a/cypress/e2e/crypto/utils.ts
+++ b/cypress/e2e/crypto/utils.ts
@@ -21,15 +21,16 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/
export type EmojiMapping = [emoji: string, name: string];
/**
- * wait for the given client to receive an incoming verification request
+ * wait for the given client to receive an incoming verification request, and automatically accept it
*
* @param cli - matrix client we expect to receive a request
*/
export function waitForVerificationRequest(cli: MatrixClient): Promise {
return new Promise((resolve) => {
- const onVerificationRequestEvent = (request: VerificationRequest) => {
+ const onVerificationRequestEvent = async (request: VerificationRequest) => {
// @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here
cli.off("crypto.verification.request", onVerificationRequestEvent);
+ await request.accept();
resolve(request);
};
// @ts-ignore
@@ -38,26 +39,83 @@ export function waitForVerificationRequest(cli: MatrixClient): Promise {
return new Promise((resolve) => {
const onShowSas = (event: ISasEvent) => {
+ // @ts-ignore VerifierEvent is a pain to get at here as we don't have a reference to matrixcs;
+ // using the string value here
verifier.off("show_sas", onShowSas);
event.confirm();
- verifier.done();
resolve(event.sas.emoji);
};
const verifier = request.beginKeyVerification("m.sas.v1");
+ // @ts-ignore as above, avoiding reference to VerifierEvent
verifier.on("show_sas", onShowSas);
verifier.verify();
});
}
+
+/**
+ * Check that the user has published cross-signing keys, and that the user's device has been cross-signed.
+ */
+export function checkDeviceIsCrossSigned(): void {
+ let userId: string;
+ let myDeviceId: string;
+ cy.window({ log: false })
+ .then((win) => {
+ // Get the userId and deviceId of the current user
+ const cli = win.mxMatrixClientPeg.get();
+ const accessToken = cli.getAccessToken()!;
+ const homeserverUrl = cli.getHomeserverUrl();
+ myDeviceId = cli.getDeviceId();
+ userId = cli.getUserId();
+ return cy.request({
+ method: "POST",
+ url: `${homeserverUrl}/_matrix/client/v3/keys/query`,
+ headers: { Authorization: `Bearer ${accessToken}` },
+ body: { device_keys: { [userId]: [] } },
+ });
+ })
+ .then((res) => {
+ // there should be three cross-signing keys
+ expect(res.body.master_keys[userId]).to.have.property("keys");
+ expect(res.body.self_signing_keys[userId]).to.have.property("keys");
+ expect(res.body.user_signing_keys[userId]).to.have.property("keys");
+
+ // and the device should be signed by the self-signing key
+ const selfSigningKeyId = Object.keys(res.body.self_signing_keys[userId].keys)[0];
+
+ expect(res.body.device_keys[userId][myDeviceId]).to.exist;
+
+ const myDeviceSignatures = res.body.device_keys[userId][myDeviceId].signatures[userId];
+ expect(myDeviceSignatures[selfSigningKeyId]).to.exist;
+ });
+}
+
+/**
+ * Fill in the login form in element with the given creds
+ */
+export function logIntoElement(homeserverUrl: string, username: string, password: string) {
+ cy.visit("/#/login");
+
+ // select homeserver
+ cy.findByRole("button", { name: "Edit" }).click();
+ cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl);
+ 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" }).type(username);
+ cy.findByPlaceholderText("Password").type(password);
+ cy.findByRole("button", { name: "Sign in" }).click();
+}
diff --git a/cypress/e2e/invite/invite-dialog.spec.ts b/cypress/e2e/invite/invite-dialog.spec.ts
new file mode 100644
index 00000000000..80edfa411d6
--- /dev/null
+++ b/cypress/e2e/invite/invite-dialog.spec.ts
@@ -0,0 +1,184 @@
+/*
+Copyright 2023 Suguru Hirahara
+
+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 type { MatrixClient } from "matrix-js-sdk/src/client";
+import { HomeserverInstance } from "../../plugins/utils/homeserver";
+
+describe("Invite dialog", function () {
+ let homeserver: HomeserverInstance;
+ let bot: MatrixClient;
+ const botName = "BotAlice";
+
+ beforeEach(() => {
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+ cy.initTestUser(homeserver, "Hanako");
+
+ cy.getBot(homeserver, { displayName: botName, autoAcceptInvites: true }).then((_bot) => {
+ bot = _bot;
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should support inviting a user to a room", () => {
+ // Create and view a room
+ cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
+
+ // Assert that the room was configured
+ cy.findByText("Hanako created and configured the room.").should("exist");
+
+ // Open the room info panel
+ cy.findByRole("button", { name: "Room info" }).click();
+
+ cy.get(".mx_RightPanel").within(() => {
+ // Click "People" button on the panel
+ // Regex pattern due to the string of "mx_BaseCard_Button_sublabel"
+ cy.findByRole("button", { name: /People/ }).click();
+ });
+
+ cy.get(".mx_BaseCard_header").within(() => {
+ // Click "Invite to this room" button
+ // Regex pattern due to "mx_MemberList_invite span::before"
+ cy.findByRole("button", { name: /Invite to this room/ }).click();
+ });
+
+ cy.get(".mx_InviteDialog_other").within(() => {
+ cy.get(".mx_Dialog_header .mx_Dialog_title").within(() => {
+ // Assert that the header is rendered
+ cy.findByText("Invite to Test Room").should("exist");
+ });
+
+ // Assert that the bar is rendered
+ cy.get(".mx_InviteDialog_addressBar").should("exist");
+ });
+
+ // TODO: unhide userId
+ const percyCSS = ".mx_InviteDialog_helpText_userId { visibility: hidden !important; }";
+
+ // Take a snapshot of the invite dialog including its wrapper
+ cy.get(".mx_Dialog_wrapper").percySnapshotElement("Invite Dialog - Room (without a user)", { percyCSS });
+
+ cy.get(".mx_InviteDialog_other").within(() => {
+ cy.get(".mx_InviteDialog_identityServer").should("not.exist");
+
+ cy.findByTestId("invite-dialog-input").type(bot.getUserId());
+
+ // Assert that notification about identity servers appears after typing userId
+ cy.get(".mx_InviteDialog_identityServer").should("exist");
+
+ cy.get(".mx_InviteDialog_tile_nameStack").within(() => {
+ cy.get(".mx_InviteDialog_tile_nameStack_userId").within(() => {
+ // Assert that the bot id is rendered properly
+ cy.findByText(bot.getUserId()).should("exist");
+ });
+
+ cy.get(".mx_InviteDialog_tile_nameStack_name").within(() => {
+ cy.findByText(botName).click();
+ });
+ });
+
+ cy.get(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").within(() => {
+ cy.findByText(botName).should("exist");
+ });
+ });
+
+ // Take a snapshot of the invite dialog with a user pill
+ cy.get(".mx_Dialog_wrapper").percySnapshotElement("Invite Dialog - Room (with a user pill)", { percyCSS });
+
+ cy.get(".mx_InviteDialog_other").within(() => {
+ // Invite the bot
+ cy.findByRole("button", { name: "Invite" }).click();
+ });
+
+ // Assert that the invite dialog disappears
+ cy.get(".mx_InviteDialog_other").should("not.exist");
+
+ // Assert that they were invited and joined
+ cy.findByText(`${botName} joined the room`).should("exist");
+ });
+
+ it("should support inviting a user to Direct Messages", () => {
+ cy.get(".mx_RoomList").within(() => {
+ cy.findByRole("button", { name: "Start chat" }).click();
+ });
+
+ cy.get(".mx_InviteDialog_other").within(() => {
+ cy.get(".mx_Dialog_header .mx_Dialog_title").within(() => {
+ // Assert that the header is rendered
+ cy.findByText("Direct Messages").should("exist");
+ });
+
+ // Assert that the bar is rendered
+ cy.get(".mx_InviteDialog_addressBar").should("exist");
+ });
+
+ // TODO: unhide userId and invite link
+ const percyCSS =
+ ".mx_InviteDialog_footer_link, .mx_InviteDialog_helpText_userId { visibility: hidden !important; }";
+
+ // Take a snapshot of the invite dialog including its wrapper
+ cy.get(".mx_Dialog_wrapper").percySnapshotElement("Invite Dialog - Direct Messages (without a user)", {
+ percyCSS,
+ });
+
+ cy.get(".mx_InviteDialog_other").within(() => {
+ cy.findByTestId("invite-dialog-input").type(bot.getUserId());
+
+ cy.get(".mx_InviteDialog_tile_nameStack").within(() => {
+ cy.findByText(bot.getUserId()).should("exist");
+ cy.findByText(botName).click();
+ });
+
+ cy.get(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").within(() => {
+ cy.findByText(botName).should("exist");
+ });
+ });
+
+ // Take a snapshot of the invite dialog with a user pill
+ cy.get(".mx_Dialog_wrapper").percySnapshotElement("Invite Dialog - Direct Messages (with a user pill)", {
+ percyCSS,
+ });
+
+ cy.get(".mx_InviteDialog_other").within(() => {
+ // Open a direct message UI
+ cy.findByRole("button", { name: "Go" }).click();
+ });
+
+ // Assert that the invite dialog disappears
+ cy.get(".mx_InviteDialog_other").should("not.exist");
+
+ // Assert that the hovered user name on invitation UI does not have background color
+ // TODO: implement the test on room-header.spec.ts
+ cy.get(".mx_RoomHeader").within(() => {
+ cy.get(".mx_RoomHeader_name--textonly")
+ .realHover()
+ .should("have.css", "background-color", "rgba(0, 0, 0, 0)");
+ });
+
+ // Send a message to invite the bots
+ cy.getComposer().type("Hello{enter}");
+
+ // Assert that they were invited and joined
+ cy.findByText(`${botName} joined the room`).should("exist");
+
+ // Assert that the message is displayed at the bottom
+ cy.get(".mx_EventTile_last").findByText("Hello").should("exist");
+ });
+});
diff --git a/cypress/e2e/lazy-loading/lazy-loading.spec.ts b/cypress/e2e/lazy-loading/lazy-loading.spec.ts
index 6e53fc33da9..05bed5cf682 100644
--- a/cypress/e2e/lazy-loading/lazy-loading.spec.ts
+++ b/cypress/e2e/lazy-loading/lazy-loading.spec.ts
@@ -116,7 +116,7 @@ describe("Lazy Loading", () => {
}
function openMemberlist(): void {
- cy.get(".mx_HeaderButtons").within(() => {
+ cy.get(".mx_RoomHeader").within(() => {
cy.findByRole("button", { name: "Room info" }).click();
});
diff --git a/cypress/e2e/login/login.spec.ts b/cypress/e2e/login/login.spec.ts
index 7098a4ce9d4..9bc6dd3f1b2 100644
--- a/cypress/e2e/login/login.spec.ts
+++ b/cypress/e2e/login/login.spec.ts
@@ -21,10 +21,6 @@ import { HomeserverInstance } from "../../plugins/utils/homeserver";
describe("Login", () => {
let homeserver: HomeserverInstance;
- beforeEach(() => {
- cy.stubDefaultServer();
- });
-
afterEach(() => {
cy.stopHomeserver(homeserver);
});
@@ -44,17 +40,18 @@ describe("Login", () => {
it("logs in with an existing account and lands on the home screen", () => {
cy.injectAxe();
- cy.findByRole("textbox", { name: "Username", timeout: 15000 }).should("be.visible");
- // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
- //cy.percySnapshot("Login");
- cy.checkA11y();
-
+ // first pick the homeserver, as otherwise the user picker won't be visible
cy.findByRole("button", { name: "Edit" }).click();
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", timeout: 15000 }).should("be.visible");
+ // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
+ //cy.percySnapshot("Login");
+ cy.checkA11y();
+
cy.findByRole("textbox", { name: "Username" }).type(username);
cy.findByPlaceholderText("Password").type(password);
cy.findByRole("button", { name: "Sign in" }).click();
diff --git a/cypress/e2e/permalinks/permalinks.spec.ts b/cypress/e2e/permalinks/permalinks.spec.ts
index 29795631740..2a61df26a09 100644
--- a/cypress/e2e/permalinks/permalinks.spec.ts
+++ b/cypress/e2e/permalinks/permalinks.spec.ts
@@ -126,11 +126,14 @@ describe("permalinks", () => {
getPill(danielle.getSafeUserId());
});
- // clean up before taking the snapshot
- cy.get(".mx_cryptoEvent").invoke("remove");
- cy.get(".mx_NewRoomIntro").invoke("remove");
- cy.get(".mx_GenericEventListSummary").invoke("remove");
-
- cy.get(".mx_RoomView_timeline").percySnapshotElement("Permalink rendering");
+ // Exclude various components from the snapshot, for consistency
+ const percyCSS =
+ ".mx_cryptoEvent, " +
+ ".mx_NewRoomIntro, " +
+ ".mx_MessageTimestamp, " +
+ ".mx_RoomView_myReadMarker, " +
+ ".mx_GenericEventListSummary { visibility: hidden !important; }";
+
+ cy.get(".mx_RoomView_timeline").percySnapshotElement("Permalink rendering", { percyCSS });
});
});
diff --git a/cypress/e2e/polls/polls.spec.ts b/cypress/e2e/polls/polls.spec.ts
index a3850dcdbc1..1a6682a6429 100644
--- a/cypress/e2e/polls/polls.spec.ts
+++ b/cypress/e2e/polls/polls.spec.ts
@@ -22,7 +22,7 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
import Chainable = Cypress.Chainable;
-const hidePercyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
+const hidePercyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
describe("Polls", () => {
let homeserver: HomeserverInstance;
diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts
new file mode 100644
index 00000000000..a36132a408e
--- /dev/null
+++ b/cypress/e2e/read-receipts/read-receipts.spec.ts
@@ -0,0 +1,354 @@
+/*
+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 type { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
+import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
+import type { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
+import { HomeserverInstance } from "../../plugins/utils/homeserver";
+
+describe("Read receipts", () => {
+ const userName = "Mae";
+ const botName = "Other User";
+ const selectedRoomName = "Selected Room";
+ const otherRoomName = "Other Room";
+
+ let homeserver: HomeserverInstance;
+ let otherRoomId: string;
+ let selectedRoomId: string;
+ let bot: MatrixClient | undefined;
+
+ const botSendMessage = (no = 1): Cypress.Chainable => {
+ return cy.botSendMessage(bot, otherRoomId, `Message ${no}`);
+ };
+
+ const botSendThreadMessage = (threadId: string): Cypress.Chainable => {
+ return cy.botSendThreadMessage(bot, otherRoomId, threadId, "Message");
+ };
+
+ const fakeEventFromSent = (eventResponse: ISendEventResponse, threadRootId: string | undefined): MatrixEvent => {
+ return {
+ getRoomId: () => otherRoomId,
+ getId: () => eventResponse.event_id,
+ threadRootId,
+ getTs: () => 1,
+ } as any as MatrixEvent;
+ };
+
+ /**
+ * Send a threaded receipt marking the message referred to in
+ * eventResponse as read. If threadRootEventResponse is supplied, the
+ * receipt will have its event_id as the thread root ID for the receipt.
+ */
+ const sendThreadedReadReceipt = (
+ eventResponse: ISendEventResponse,
+ threadRootEventResponse: ISendEventResponse = undefined,
+ ) => {
+ cy.sendReadReceipt(fakeEventFromSent(eventResponse, threadRootEventResponse?.event_id));
+ };
+
+ /**
+ * Send an unthreaded receipt marking the message referred to in
+ * eventResponse as read.
+ */
+ const sendUnthreadedReadReceipt = (eventResponse: ISendEventResponse) => {
+ cy.sendReadReceipt(fakeEventFromSent(eventResponse, undefined), "m.read" as any as ReceiptType, true);
+ };
+
+ beforeEach(() => {
+ /*
+ * Create 2 rooms:
+ *
+ * - Selected room - this one is clicked in the UI
+ * - Other room - this one contains the bot, which will send events so
+ * we can check its unread state.
+ */
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+ cy.initTestUser(homeserver, userName)
+ .then(() => {
+ cy.createRoom({ name: selectedRoomName }).then((createdRoomId) => {
+ selectedRoomId = createdRoomId;
+ });
+ })
+ .then(() => {
+ cy.createRoom({ name: otherRoomName }).then((createdRoomId) => {
+ otherRoomId = createdRoomId;
+ });
+ })
+ .then(() => {
+ cy.getBot(homeserver, { displayName: botName }).then((botClient) => {
+ bot = botClient;
+ });
+ })
+ .then(() => {
+ // Invite the bot to Other room
+ cy.inviteUser(otherRoomId, bot.getUserId());
+ cy.visit("/#/room/" + otherRoomId);
+ cy.findByText(botName + " joined the room").should("exist");
+
+ // Then go into Selected room
+ cy.visit("/#/room/" + selectedRoomId);
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it(
+ "With sync accumulator, considers main thread and unthreaded receipts #24629",
+ {
+ // When #24629 exists, the test fails the first time but passes later, so we disable retries
+ // to be sure we are going to fail if the bug comes back.
+ // Why does it pass the second time? I wish I knew. (andyb)
+ retries: 0,
+ },
+ () => {
+ // Details are in https://github.com/vector-im/element-web/issues/24629
+ // This proves we've fixed one of the "stuck unreads" issues.
+
+ // Given we sent 3 events on the main thread
+ botSendMessage();
+ botSendMessage().then((main2) => {
+ botSendMessage().then((main3) => {
+ // (So the room starts off unread)
+ cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
+
+ // When we send a threaded receipt for the last event in main
+ // And an unthreaded receipt for an earlier event
+ sendThreadedReadReceipt(main3);
+ sendUnthreadedReadReceipt(main2);
+
+ // (So the room has no unreads)
+ cy.findByLabelText(`${otherRoomName}`).should("exist");
+
+ // And we persuade the app to persist its state to indexeddb by reloading and waiting
+ cy.reload();
+ cy.findByLabelText(`${selectedRoomName}`).should("exist");
+
+ // And we reload again, fetching the persisted state FROM indexeddb
+ cy.reload();
+
+ // Then the room is read, because the persisted state correctly remembers both
+ // receipts. (In #24629, the unthreaded receipt overwrote the main thread one,
+ // meaning that the room still said it had unread messages.)
+ cy.findByLabelText(`${otherRoomName}`).should("exist");
+ cy.findByLabelText(`${otherRoomName} Unread messages.`).should("not.exist");
+ });
+ });
+ },
+ );
+
+ it("Recognises unread messages on main thread after receiving a receipt for earlier ones", () => {
+ // Given we sent 3 events on the main thread
+ botSendMessage();
+ botSendMessage().then((main2) => {
+ botSendMessage().then(() => {
+ // (The room starts off unread)
+ cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
+
+ // When we send a threaded receipt for the second-last event in main
+ sendThreadedReadReceipt(main2);
+
+ // Then the room has only one unread
+ cy.findByLabelText(`${otherRoomName} 1 unread message.`).should("exist");
+ });
+ });
+ });
+
+ it("Considers room read if there is only a main thread and we have a main receipt", () => {
+ // Given we sent 3 events on the main thread
+ botSendMessage();
+ botSendMessage().then(() => {
+ botSendMessage().then((main3) => {
+ // (The room starts off unread)
+ cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
+
+ // When we send a threaded receipt for the last event in main
+ sendThreadedReadReceipt(main3);
+
+ // Then the room has no unreads
+ cy.findByLabelText(`${otherRoomName}`).should("exist");
+ });
+ });
+ });
+
+ it("Recognises unread messages on other thread after receiving a receipt for earlier ones", () => {
+ // Given we sent 3 events on the main thread
+ botSendMessage().then((main1) => {
+ botSendThreadMessage(main1.event_id).then((thread1a) => {
+ botSendThreadMessage(main1.event_id).then((thread1b) => {
+ // 1 unread on the main thread, 2 in the new thread
+ cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
+
+ // When we send receipts for main, and the second-last in the thread
+ sendThreadedReadReceipt(main1);
+ sendThreadedReadReceipt(thread1a, main1);
+
+ // Then the room has only one unread - the one in the thread
+ cy.findByLabelText(`${otherRoomName} 1 unread message.`).should("exist");
+ });
+ });
+ });
+ });
+
+ it("Considers room read if there are receipts for main and other thread", () => {
+ // Given we sent 3 events on the main thread
+ botSendMessage().then((main1) => {
+ botSendThreadMessage(main1.event_id).then((thread1a) => {
+ botSendThreadMessage(main1.event_id).then((thread1b) => {
+ // 1 unread on the main thread, 2 in the new thread
+ cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
+
+ // When we send receipts for main, and the last in the thread
+ sendThreadedReadReceipt(main1);
+ sendThreadedReadReceipt(thread1b, main1);
+
+ // Then the room has no unreads
+ cy.findByLabelText(`${otherRoomName}`).should("exist");
+ });
+ });
+ });
+ });
+
+ it("Recognises unread messages on a thread after receiving a unthreaded receipt for earlier ones", () => {
+ // Given we sent 3 events on the main thread
+ botSendMessage().then((main1) => {
+ botSendThreadMessage(main1.event_id).then((thread1a) => {
+ botSendThreadMessage(main1.event_id).then(() => {
+ // 1 unread on the main thread, 2 in the new thread
+ cy.findByLabelText(`${otherRoomName} 3 unread messages.`).should("exist");
+
+ // When we send an unthreaded receipt for the second-last in the thread
+ sendUnthreadedReadReceipt(thread1a);
+
+ // Then the room has only one unread - the one in the
+ // thread. The one in main is read because the unthreaded
+ // receipt is for a later event.
+ cy.findByLabelText(`${otherRoomName} 1 unread message.`).should("exist");
+ });
+ });
+ });
+ });
+
+ it("Recognises unread messages on main after receiving a unthreaded receipt for a thread message", () => {
+ // Given we sent 3 events on the main thread
+ botSendMessage().then((main1) => {
+ botSendThreadMessage(main1.event_id).then(() => {
+ botSendThreadMessage(main1.event_id).then((thread1b) => {
+ botSendMessage().then(() => {
+ // 2 unreads on the main thread, 2 in the new thread
+ cy.findByLabelText(`${otherRoomName} 4 unread messages.`).should("exist");
+
+ // When we send an unthreaded receipt for the last in the thread
+ sendUnthreadedReadReceipt(thread1b);
+
+ // Then the room has only one unread - the one in the
+ // main thread, because it is later than the unthreaded
+ // receipt.
+ cy.findByLabelText(`${otherRoomName} 1 unread message.`).should("exist");
+ });
+ });
+ });
+ });
+ });
+
+ /**
+ * The idea of this test is to intercept the receipt / read read_markers requests and
+ * assert that the correct ones are sent.
+ * Prose playbook:
+ * - Another user sends enough messages that the timeline becomes scrollable
+ * - The current user looks at the room and jumps directly to the first unread message
+ * - At this point, a receipt for the last message in the room and
+ * a fully read marker for the last visible message are expected to be sent
+ * - Then the user jumps to the end of the timeline
+ * - A fully read marker for the last message in the room is expected to be sent
+ */
+ it("Should send the correct receipts", () => {
+ const uriEncodedOtherRoomId = encodeURIComponent(otherRoomId);
+
+ cy.intercept({
+ method: "POST",
+ url: new RegExp(
+ `http://localhost:\\d+/_matrix/client/r0/rooms/${uriEncodedOtherRoomId}/receipt/m\\.read/.+`,
+ ),
+ }).as("receiptRequest");
+
+ const numberOfMessages = 20;
+ const sendMessagePromises = [];
+
+ for (let i = 1; i <= numberOfMessages; i++) {
+ sendMessagePromises.push(botSendMessage(i));
+ }
+
+ cy.all(sendMessagePromises).then((sendMessageResponses) => {
+ const lastMessageId = sendMessageResponses.at(-1).event_id;
+ const uriEncodedLastMessageId = encodeURIComponent(lastMessageId);
+
+ // wait until all messages have been received
+ cy.findByLabelText(`${otherRoomName} ${sendMessagePromises.length} unread messages.`).should("exist");
+
+ // switch to the room with the messages
+ cy.visit("/#/room/" + otherRoomId);
+
+ cy.wait("@receiptRequest").should((req) => {
+ // assert the read receipt for the last message in the room
+ expect(req.request.url).to.contain(uriEncodedLastMessageId);
+ expect(req.request.body).to.deep.equal({
+ thread_id: "main",
+ });
+ });
+
+ // the following code tests the fully read marker somewhere in the middle of the room
+
+ cy.intercept({
+ method: "POST",
+ url: new RegExp(`http://localhost:\\d+/_matrix/client/r0/rooms/${uriEncodedOtherRoomId}/read_markers`),
+ }).as("readMarkersRequest");
+
+ cy.findByRole("button", { name: "Jump to first unread message." }).click();
+
+ cy.wait("@readMarkersRequest").should((req) => {
+ // since this is not pixel perfect,
+ // the fully read marker should be +/- 1 around the last visible message
+ expect(Array.from(Object.keys(req.request.body))).to.deep.equal(["m.fully_read"]);
+ expect(req.request.body["m.fully_read"]).to.be.oneOf([
+ sendMessageResponses[11].event_id,
+ sendMessageResponses[12].event_id,
+ sendMessageResponses[13].event_id,
+ ]);
+ });
+
+ // the following code tests the fully read marker at the bottom of the room
+
+ cy.intercept({
+ method: "POST",
+ url: new RegExp(`http://localhost:\\d+/_matrix/client/r0/rooms/${uriEncodedOtherRoomId}/read_markers`),
+ }).as("readMarkersRequest");
+
+ cy.findByRole("button", { name: "Scroll to most recent messages" }).click();
+
+ cy.wait("@readMarkersRequest").should((req) => {
+ expect(req.request.body).to.deep.equal({
+ ["m.fully_read"]: sendMessageResponses.at(-1).event_id,
+ });
+ });
+ });
+ });
+});
diff --git a/cypress/e2e/register/register.spec.ts b/cypress/e2e/register/register.spec.ts
index df628d0c0e7..5810915439b 100644
--- a/cypress/e2e/register/register.spec.ts
+++ b/cypress/e2e/register/register.spec.ts
@@ -17,12 +17,12 @@ limitations under the License.
///
import { HomeserverInstance } from "../../plugins/utils/homeserver";
+import { checkDeviceIsCrossSigned } from "../crypto/utils";
describe("Registration", () => {
let homeserver: HomeserverInstance;
beforeEach(() => {
- cy.stubDefaultServer();
cy.visit("/#/register");
cy.startHomeserver("consent").then((data) => {
homeserver = data;
@@ -83,12 +83,20 @@ describe("Registration", () => {
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: "Security & Privacy" }).click();
- cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon").should(
- "have.class",
- "mx_E2EIcon_verified",
- );
+ 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", () => {
diff --git a/cypress/e2e/right-panel/file-panel.spec.ts b/cypress/e2e/right-panel/file-panel.spec.ts
index f2e0e0a013b..b36edfb276f 100644
--- a/cypress/e2e/right-panel/file-panel.spec.ts
+++ b/cypress/e2e/right-panel/file-panel.spec.ts
@@ -70,6 +70,16 @@ describe("FilePanel", () => {
});
describe("render", () => {
+ it("should render empty state", () => {
+ // Wait until the information about the empty state is rendered
+ cy.get(".mx_FilePanel_empty").should("exist");
+
+ // Take a snapshot of RightPanel - fix https://github.com/vector-im/element-web/issues/25332
+ cy.get(".mx_RightPanel").percySnapshotElement("File Panel - empty", {
+ widths: [264], // Emulate the UI. The value is based on minWidth specified on MainSplit.tsx
+ });
+ });
+
it("should list tiles on the panel", () => {
// Upload multiple files
uploadFile("cypress/fixtures/riot.png"); // Image
@@ -164,7 +174,7 @@ describe("FilePanel", () => {
// FIXME: hide mx_SeekBar because flaky - see https://github.com/vector-im/element-web/issues/24897
// Remove this once https://github.com/vector-im/element-web/issues/24898 is fixed.
const percyCSS =
- ".mx_MessageTimestamp, .mx_RoomView_myReadMarker, .mx_SeekBar { visibility: hidden !important; }";
+ ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .mx_SeekBar { visibility: hidden !important; }";
// Take a snapshot of file tiles list on FilePanel
cy.get(".mx_FilePanel .mx_RoomView_MessageList").percySnapshotElement("File tiles list on FilePanel", {
diff --git a/cypress/e2e/right-panel/notification-panel.spec.ts b/cypress/e2e/right-panel/notification-panel.spec.ts
new file mode 100644
index 00000000000..4068285070b
--- /dev/null
+++ b/cypress/e2e/right-panel/notification-panel.spec.ts
@@ -0,0 +1,52 @@
+/*
+Copyright 2023 Suguru Hirahara
+
+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";
+
+const ROOM_NAME = "Test room";
+const NAME = "Alice";
+
+describe("NotificationPanel", () => {
+ let homeserver: HomeserverInstance;
+
+ beforeEach(() => {
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+ cy.initTestUser(homeserver, NAME).then(() => {
+ cy.createRoom({ name: ROOM_NAME });
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should render empty state", () => {
+ cy.viewRoomByName(ROOM_NAME);
+ cy.findByRole("button", { name: "Notifications" }).click();
+
+ // Wait until the information about the empty state is rendered
+ cy.get(".mx_NotificationPanel_empty").should("exist");
+
+ // Take a snapshot of RightPanel
+ cy.get(".mx_RightPanel").percySnapshotElement("Notification Panel - empty", {
+ widths: [264], // Emulate the UI. The value is based on minWidth specified on MainSplit.tsx
+ });
+ });
+});
diff --git a/cypress/e2e/right-panel/right-panel.spec.ts b/cypress/e2e/right-panel/right-panel.spec.ts
index 733eb3c78fa..ec840844639 100644
--- a/cypress/e2e/right-panel/right-panel.spec.ts
+++ b/cypress/e2e/right-panel/right-panel.spec.ts
@@ -20,8 +20,16 @@ import { HomeserverInstance } from "../../plugins/utils/homeserver";
import Chainable = Cypress.Chainable;
const ROOM_NAME = "Test room";
+const ROOM_NAME_LONG =
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
+ "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
+ "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " +
+ "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
+ "officia deserunt mollit anim id est laborum.";
const SPACE_NAME = "Test space";
const NAME = "Alice";
+const ROOM_ADDRESS_LONG =
+ "loremIpsumDolorSitAmetConsecteturAdipisicingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliqua";
const getMemberTileByName = (name: string): Chainable> => {
return cy.get(`.mx_EntityTile, [title="${name}"]`);
@@ -58,6 +66,33 @@ describe("RightPanel", () => {
});
describe("in rooms", () => {
+ it("should handle long room address and long room name", () => {
+ cy.createRoom({ name: ROOM_NAME_LONG });
+ viewRoomSummaryByName(ROOM_NAME_LONG);
+
+ cy.openRoomSettings();
+
+ // Set a local room address
+ cy.contains(".mx_SettingsFieldset", "Local Addresses").within(() => {
+ cy.findByRole("textbox").type(ROOM_ADDRESS_LONG);
+ cy.findByRole("button", { name: "Add" }).click();
+ cy.findByText(`#${ROOM_ADDRESS_LONG}:localhost`)
+ .should("have.class", "mx_EditableItem_item")
+ .should("exist");
+ });
+
+ cy.closeDialog();
+
+ // Close and reopen the right panel to render the room address
+ cy.findByRole("button", { name: "Room info" }).click();
+ cy.get(".mx_RightPanel").should("not.exist");
+ cy.findByRole("button", { name: "Room info" }).click();
+
+ cy.get(".mx_RightPanel").percySnapshotElement("RoomSummaryCard - with a room name and a local address", {
+ widths: [264], // Emulate the UI. The value is based on minWidth specified on MainSplit.tsx
+ });
+ });
+
it("should handle clicking add widgets", () => {
viewRoomSummaryByName(ROOM_NAME);
diff --git a/cypress/e2e/room/room-header.spec.ts b/cypress/e2e/room/room-header.spec.ts
new file mode 100644
index 00000000000..fc20dfbebee
--- /dev/null
+++ b/cypress/e2e/room/room-header.spec.ts
@@ -0,0 +1,292 @@
+/*
+Copyright 2023 Suguru Hirahara
+
+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 { IWidget } from "matrix-widget-api";
+
+import { HomeserverInstance } from "../../plugins/utils/homeserver";
+import { SettingLevel } from "../../../src/settings/SettingLevel";
+
+describe("Room Header", () => {
+ let homeserver: HomeserverInstance;
+
+ beforeEach(() => {
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+ cy.initTestUser(homeserver, "Sakura");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should render default buttons properly", () => {
+ cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
+
+ cy.get(".mx_RoomHeader").within(() => {
+ // Names (aria-label) of every button rendered on mx_RoomHeader by default
+ const expectedButtonNames = [
+ "Room options", // The room name button next to the room avatar, which renders dropdown menu on click
+ "Voice call",
+ "Video call",
+ "Search",
+ "Threads",
+ "Notifications",
+ "Room info",
+ ];
+
+ // Assert they are found and visible
+ for (const name of expectedButtonNames) {
+ cy.findByRole("button", { name }).should("be.visible");
+ }
+
+ // Assert that just those seven buttons exist on mx_RoomHeader by default
+ cy.findAllByRole("button").should("have.length", 7);
+ });
+
+ cy.get(".mx_RoomHeader").percySnapshotElement("Room header");
+ });
+
+ it("should render the pin button for pinned messages card", () => {
+ cy.enableLabsFeature("feature_pinning");
+
+ cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
+
+ cy.getComposer().type("Test message{enter}");
+
+ cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Options" }).click();
+
+ cy.findByRole("menuitem", { name: "Pin" }).should("be.visible").click();
+
+ cy.get(".mx_RoomHeader").within(() => {
+ cy.findByRole("button", { name: "Pinned messages" }).should("be.visible");
+ });
+ });
+
+ it("should render a very long room name without collapsing the buttons", () => {
+ const LONG_ROOM_NAME =
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
+ "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
+ "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " +
+ "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
+ "officia deserunt mollit anim id est laborum.";
+
+ cy.createRoom({ name: LONG_ROOM_NAME }).viewRoomByName(LONG_ROOM_NAME);
+
+ cy.get(".mx_RoomHeader").within(() => {
+ // Wait until the room name is set
+ cy.get(".mx_RoomHeader_nametext").within(() => {
+ cy.findByText(LONG_ROOM_NAME).should("exist");
+ });
+
+ // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed
+ // Note these assertions do not check the size of mx_RoomHeader_name button
+ cy.get(".mx_RoomHeader_button")
+ .should("have.length", 6)
+ .should("be.visible")
+ .should("have.css", "height", "32px")
+ .should("have.css", "width", "32px");
+ });
+
+ cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with a long room name", {
+ widths: [300, 600], // Magic numbers to emulate the narrow RoomHeader on the actual UI
+ });
+ });
+
+ it("should have buttons highlighted by being clicked", () => {
+ cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
+
+ cy.get(".mx_RoomHeader").within(() => {
+ // Check these buttons
+ const buttonsHighlighted = ["Threads", "Notifications", "Room info"];
+
+ for (const name of buttonsHighlighted) {
+ cy.findByRole("button", { name: name })
+ .click() // Highlight the button
+ .then(($btn) => {
+ // Note it is not possible to get CSS values of a pseudo class with "have.css".
+ const color = $btn[0].ownerDocument.defaultView // get window reference from element
+ .getComputedStyle($btn[0], "before") // get the pseudo selector
+ .getPropertyValue("background-color"); // get "background-color" value
+
+ // Assert the value is equal to $accent == hex #0dbd8b == rgba(13, 189, 139)
+ expect(color).to.eq("rgb(13, 189, 139)");
+ });
+ }
+ });
+
+ cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with a highlighted button");
+ });
+
+ describe("with a video room", () => {
+ const createVideoRoom = () => {
+ // Enable video rooms. This command reloads the app
+ cy.setSettingValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
+
+ cy.get(".mx_LeftPanel_roomListContainer", { timeout: 20000 })
+ .findByRole("button", { name: "Add room" })
+ .click();
+
+ cy.findByRole("menuitem", { name: "New video room" }).click();
+
+ cy.findByRole("textbox", { name: "Name" }).type("Test video room");
+
+ cy.findByRole("button", { name: "Create video room" }).click();
+
+ cy.viewRoomByName("Test video room");
+ };
+
+ it("should render buttons for room options, beta pill, invite, chat, and room info", () => {
+ createVideoRoom();
+
+ cy.get(".mx_RoomHeader").within(() => {
+ // Names (aria-label) of the buttons on the video room header
+ const expectedButtonNames = [
+ "Room options",
+ "Video rooms are a beta feature Click for more info", // Beta pill
+ "Invite",
+ "Chat",
+ "Room info",
+ ];
+
+ // Assert they are found and visible
+ for (const name of expectedButtonNames) {
+ cy.findByRole("button", { name }).should("be.visible");
+ }
+
+ // Assert that there is not a button except those buttons
+ cy.findAllByRole("button").should("have.length", 5);
+ });
+
+ cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with a video room");
+ });
+
+ it("should render a working chat button which opens the timeline on a right panel", () => {
+ createVideoRoom();
+
+ cy.get(".mx_RoomHeader").findByRole("button", { name: "Chat" }).click();
+
+ // Assert that the video is rendered
+ cy.get(".mx_CallView video").should("exist");
+
+ cy.get(".mx_RightPanel .mx_TimelineCard")
+ .should("exist")
+ .within(() => {
+ // Assert that GELS is visible
+ cy.findByText("Sakura created and configured the room.").should("exist");
+ });
+ });
+ });
+
+ describe("with a widget", () => {
+ const ROOM_NAME = "Test Room with a widget";
+ const WIDGET_ID = "fake-widget";
+ const WIDGET_HTML = `
+
+
+ Fake Widget
+
+
+ Hello World
+
+
+ `;
+
+ let widgetUrl: string;
+ let roomId: string;
+
+ beforeEach(() => {
+ cy.serveHtmlFile(WIDGET_HTML).then((url) => {
+ widgetUrl = url;
+ });
+
+ cy.createRoom({ name: ROOM_NAME }).then((id) => {
+ roomId = id;
+
+ // setup widget via state event
+ cy.getClient()
+ .then(async (matrixClient) => {
+ const content: IWidget = {
+ id: WIDGET_ID,
+ creatorUserId: "somebody",
+ type: "widget",
+ name: "widget",
+ url: widgetUrl,
+ };
+ await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, WIDGET_ID);
+ })
+ .as("widgetEventSent");
+
+ // set initial layout
+ cy.getClient()
+ .then(async (matrixClient) => {
+ const content = {
+ widgets: {
+ [WIDGET_ID]: {
+ container: "top",
+ index: 1,
+ width: 100,
+ height: 0,
+ },
+ },
+ };
+ await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
+ })
+ .as("layoutEventSent");
+ });
+
+ cy.all([cy.get("@widgetEventSent"), cy.get("@layoutEventSent")]).then(() => {
+ // open the room
+ cy.viewRoomByName(ROOM_NAME);
+ });
+ });
+
+ it("should highlight the apps button", () => {
+ // Assert that AppsDrawer is rendered
+ cy.get(".mx_AppsDrawer").should("exist");
+
+ cy.get(".mx_RoomHeader").within(() => {
+ // Assert that "Hide Widgets" button is rendered and aria-checked is set to true
+ cy.findByRole("button", { name: "Hide Widgets" })
+ .should("exist")
+ .should("have.attr", "aria-checked", "true");
+ });
+
+ cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with apps button (highlighted)");
+ });
+
+ it("should support hiding a widget", () => {
+ cy.get(".mx_AppsDrawer").should("exist");
+
+ cy.get(".mx_RoomHeader").within(() => {
+ // Click the apps button to hide AppsDrawer
+ cy.findByRole("button", { name: "Hide Widgets" }).should("exist").click();
+
+ // Assert that "Show widgets" button is rendered and aria-checked is set to false
+ cy.findByRole("button", { name: "Show Widgets" })
+ .should("exist")
+ .should("have.attr", "aria-checked", "false");
+ });
+
+ // Assert that AppsDrawer is not rendered
+ cy.get(".mx_AppsDrawer").should("not.exist");
+
+ cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with apps button (not highlighted)");
+ });
+ });
+});
diff --git a/cypress/e2e/settings/appearance-user-settings-tab.spec.ts b/cypress/e2e/settings/appearance-user-settings-tab.spec.ts
new file mode 100644
index 00000000000..cb22d26b58b
--- /dev/null
+++ b/cypress/e2e/settings/appearance-user-settings-tab.spec.ts
@@ -0,0 +1,328 @@
+/*
+Copyright 2023 Suguru Hirahara
+
+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 { SettingLevel } from "../../../src/settings/SettingLevel";
+
+describe("Appearance user settings tab", () => {
+ let homeserver: HomeserverInstance;
+
+ beforeEach(() => {
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+ cy.initTestUser(homeserver, "Hanako");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ it("should be rendered properly", () => {
+ cy.openUserSettings("Appearance");
+
+ cy.findByTestId("mx_AppearanceUserSettingsTab").within(() => {
+ cy.get("h2").should("have.text", "Customise your appearance").should("be.visible");
+ });
+
+ cy.findByTestId("mx_AppearanceUserSettingsTab").percySnapshotElement(
+ "User settings tab - Appearance (advanced options collapsed)",
+ {
+ // Emulate TabbedView's actual min and max widths
+ // 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
+ // 796: 1036 (mx_TabbedView_tabsOnLeft actual width) - 240 (mx_TabbedView_tabPanel margin-right)
+ widths: [580, 796],
+ },
+ );
+
+ // Click "Show advanced" link button
+ cy.findByRole("button", { name: "Show advanced" }).click();
+
+ // Assert that "Hide advanced" link button is rendered
+ cy.findByRole("button", { name: "Hide advanced" }).should("exist");
+
+ cy.findByTestId("mx_AppearanceUserSettingsTab").percySnapshotElement(
+ "User settings tab - Appearance (advanced options expanded)",
+ {
+ // Emulate TabbedView's actual min and max widths
+ // 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
+ // 796: 1036 (mx_TabbedView_tabsOnLeft actual width) - 240 (mx_TabbedView_tabPanel margin-right)
+ widths: [580, 796],
+ },
+ );
+ });
+
+ it("should support switching layouts", () => {
+ // Create and view a room first
+ cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
+
+ cy.openUserSettings("Appearance");
+
+ cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
+ // Assert that the layout selected by default is "Modern"
+ cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
+ cy.findByLabelText("Modern").should("exist");
+ });
+ });
+
+ // Assert that the room layout is set to group (modern) layout
+ cy.get(".mx_RoomView_body[data-layout='group']").should("exist");
+
+ cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
+ // Select the first layout
+ cy.get(".mx_LayoutSwitcher_RadioButton").first().click();
+
+ // Assert that the layout selected is "IRC (Experimental)"
+ cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
+ cy.findByLabelText("IRC (Experimental)").should("exist");
+ });
+ });
+
+ // Assert that the room layout is set to IRC layout
+ cy.get(".mx_RoomView_body[data-layout='irc']").should("exist");
+
+ cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
+ // Select the last layout
+ cy.get(".mx_LayoutSwitcher_RadioButton").last().click();
+
+ // Assert that the layout selected is "Message bubbles"
+ cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
+ cy.findByLabelText("Message bubbles").should("exist");
+ });
+ });
+
+ // Assert that the room layout is set to bubble layout
+ cy.get(".mx_RoomView_body[data-layout='bubble']").should("exist");
+ });
+
+ it("should support changing font size by clicking the font slider", () => {
+ cy.openUserSettings("Appearance");
+
+ cy.findByTestId("mx_AppearanceUserSettingsTab").within(() => {
+ cy.get(".mx_FontScalingPanel_fontSlider").within(() => {
+ cy.findByLabelText("Font size").should("exist");
+ });
+
+ cy.get(".mx_FontScalingPanel_fontSlider").within(() => {
+ // Click the left position of the slider
+ cy.get("input").realClick({ position: "left" });
+
+ // Assert that the smallest font size is selected
+ cy.get("input[value='13']").should("exist");
+ cy.get("output .mx_Slider_selection_label").findByText("13");
+ });
+
+ cy.get(".mx_FontScalingPanel_fontSlider").percySnapshotElement("Font size slider - smallest (13)", {
+ widths: [486], // actual size (content-box, including inline padding)
+ });
+
+ cy.get(".mx_FontScalingPanel_fontSlider").within(() => {
+ // Click the right position of the slider
+ cy.get("input").realClick({ position: "right" });
+
+ // Assert that the largest font size is selected
+ cy.get("input[value='18']").should("exist");
+ cy.get("output .mx_Slider_selection_label").findByText("18");
+ });
+
+ cy.get(".mx_FontScalingPanel_fontSlider").percySnapshotElement("Font size slider - largest (18)", {
+ widths: [486],
+ });
+ });
+ });
+
+ it("should disable font size slider when custom font size is used", () => {
+ cy.openUserSettings("Appearance");
+
+ cy.findByTestId("mx_FontScalingPanel").within(() => {
+ cy.findByLabelText("Use custom size").click({ force: true }); // force click as checkbox size is zero
+
+ // Assert that the font slider is disabled
+ cy.get(".mx_FontScalingPanel_fontSlider input[disabled]").should("exist");
+ });
+ });
+
+ it("should support enabling compact group (modern) layout", () => {
+ // Create and view a room first
+ cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
+
+ cy.openUserSettings("Appearance");
+
+ // Click "Show advanced" link button
+ cy.findByRole("button", { name: "Show advanced" }).click();
+
+ // force click as checkbox size is zero
+ cy.findByLabelText("Use a more compact 'Modern' layout").click({ force: true });
+
+ // Assert that the room layout is set to compact group (modern) layout
+ cy.get("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout").should("exist");
+ });
+
+ it("should disable compact group (modern) layout option on IRC layout and bubble layout", () => {
+ const checkDisabled = () => {
+ cy.findByLabelText("Use a more compact 'Modern' layout").should("be.disabled");
+ };
+
+ cy.openUserSettings("Appearance");
+
+ // Click "Show advanced" link button
+ cy.findByRole("button", { name: "Show advanced" }).click();
+
+ // Enable IRC layout
+ cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
+ // Select the first layout
+ cy.get(".mx_LayoutSwitcher_RadioButton").first().click();
+
+ // Assert that the layout selected is "IRC (Experimental)"
+ cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
+ cy.findByLabelText("IRC (Experimental)").should("exist");
+ });
+ });
+
+ checkDisabled();
+
+ // Enable bubble layout
+ cy.get(".mx_LayoutSwitcher_RadioButtons").within(() => {
+ // Select the first layout
+ cy.get(".mx_LayoutSwitcher_RadioButton").last().click();
+
+ // Assert that the layout selected is "IRC (Experimental)"
+ cy.get(".mx_LayoutSwitcher_RadioButton_selected .mx_StyledRadioButton_enabled").within(() => {
+ cy.findByLabelText("Message bubbles").should("exist");
+ });
+ });
+
+ checkDisabled();
+ });
+
+ it("should support enabling system font", () => {
+ cy.openUserSettings("Appearance");
+
+ // Click "Show advanced" link button
+ cy.findByRole("button", { name: "Show advanced" }).click();
+
+ // force click as checkbox size is zero
+ cy.findByLabelText("Use a system font").click({ force: true });
+
+ // Assert that the font-family value was removed
+ cy.get("body").should("have.css", "font-family", '""');
+ });
+
+ describe("Theme Choice Panel", () => {
+ beforeEach(() => {
+ // Disable the default theme for consistency in case ThemeWatcher automatically chooses it
+ cy.setSettingValue("use_system_theme", null, SettingLevel.DEVICE, false);
+ });
+
+ it("should be rendered with the light theme selected", () => {
+ cy.openUserSettings("Appearance")
+ .findByTestId("mx_ThemeChoicePanel")
+ .within(() => {
+ cy.findByTestId("checkbox-use-system-theme").within(() => {
+ cy.findByText("Match system theme").should("be.visible");
+
+ // Assert that 'Match system theme' is not checked
+ // Note that mx_Checkbox_checkmark exists and is hidden by CSS if it is not checked
+ cy.get(".mx_Checkbox_checkmark").should("not.be.visible");
+ });
+
+ cy.findByTestId("theme-choice-panel-selectors").within(() => {
+ cy.get(".mx_ThemeSelector_light").should("exist");
+ cy.get(".mx_ThemeSelector_dark").should("exist");
+
+ // Assert that the light theme is selected
+ cy.get(".mx_ThemeSelector_light.mx_StyledRadioButton_enabled").should("exist");
+
+ // Assert that the buttons for the light and dark theme are not enabled
+ cy.get(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled").should("not.exist");
+ cy.get(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled").should("not.exist");
+ });
+
+ // Assert that the checkbox for the high contrast theme is rendered
+ cy.findByLabelText("Use high contrast").should("exist");
+ });
+ });
+
+ it(
+ "should disable the labels for themes and the checkbox for the high contrast theme if the checkbox for " +
+ "the system theme is clicked",
+ () => {
+ cy.openUserSettings("Appearance")
+ .findByTestId("mx_ThemeChoicePanel")
+ .findByLabelText("Match system theme")
+ .click({ force: true }); // force click because the size of the checkbox is zero
+
+ cy.findByTestId("mx_ThemeChoicePanel").within(() => {
+ // Assert that the labels for the light theme and dark theme are disabled
+ cy.get(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled").should("exist");
+ cy.get(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled").should("exist");
+
+ // Assert that there does not exist a label for an enabled theme
+ cy.get("label.mx_StyledRadioButton_enabled").should("not.exist");
+
+ // Assert that the checkbox and label to enable the the high contrast theme should not exist
+ cy.findByLabelText("Use high contrast").should("not.exist");
+ });
+ },
+ );
+
+ it(
+ "should not render the checkbox and the label for the high contrast theme " +
+ "if the dark theme is selected",
+ () => {
+ cy.openUserSettings("Appearance");
+
+ // Assert that the checkbox and the label to enable the high contrast theme should exist
+ cy.findByLabelText("Use high contrast").should("exist");
+
+ // Enable the dark theme
+ cy.get(".mx_ThemeSelector_dark").click();
+
+ // Assert that the checkbox and the label should not exist
+ cy.findByLabelText("Use high contrast").should("not.exist");
+ },
+ );
+
+ it("should support enabling the high contast theme", () => {
+ cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
+
+ cy.get(".mx_GenericEventListSummary").within(() => {
+ // Assert that $primary-content is applied to GELS summary on the light theme
+ // $primary-content on the light theme = #17191c = rgb(23, 25, 28)
+ cy.get(".mx_TextualEvent.mx_GenericEventListSummary_summary")
+ .should("have.css", "color", "rgb(23, 25, 28)")
+ .should("have.css", "opacity", "0.5");
+ });
+
+ cy.openUserSettings("Appearance")
+ .findByTestId("mx_ThemeChoicePanel")
+ .findByLabelText("Use high contrast")
+ .click({ force: true }); // force click because the size of the checkbox is zero
+
+ cy.closeDialog();
+
+ cy.get(".mx_GenericEventListSummary").within(() => {
+ // Assert that $secondary-content is specified for GELS summary on the high contrast theme
+ // $secondary-content on the high contrast theme = #5e6266 = rgb(94, 98, 102)
+ cy.get(".mx_TextualEvent.mx_GenericEventListSummary_summary")
+ .should("have.css", "color", "rgb(94, 98, 102)")
+ .should("have.css", "opacity", "1");
+ });
+ });
+ });
+});
diff --git a/cypress/e2e/settings/device-management.spec.ts b/cypress/e2e/settings/device-management.spec.ts
index 277fa505fc7..06795b68bef 100644
--- a/cypress/e2e/settings/device-management.spec.ts
+++ b/cypress/e2e/settings/device-management.spec.ts
@@ -24,7 +24,6 @@ describe("Device manager", () => {
let user: UserCredentials | undefined;
beforeEach(() => {
- cy.enableLabsFeature("feature_new_device_manager");
cy.startHomeserver("default").then((data) => {
homeserver = data;
diff --git a/cypress/e2e/settings/general-user-settings-tab.spec.ts b/cypress/e2e/settings/general-user-settings-tab.spec.ts
index 2bdfb1b77d5..2879d6d9301 100644
--- a/cypress/e2e/settings/general-user-settings-tab.spec.ts
+++ b/cypress/e2e/settings/general-user-settings-tab.spec.ts
@@ -43,7 +43,7 @@ describe("General user settings tab", () => {
// Exclude userId from snapshots
const percyCSS = ".mx_ProfileSettings_profile_controls_userId { visibility: hidden !important; }";
- cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab").percySnapshotElement("User settings tab - General", {
+ cy.findByTestId("mx_GeneralUserSettingsTab").percySnapshotElement("User settings tab - General", {
percyCSS,
// Emulate TabbedView's actual min and max widths
// 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
@@ -51,9 +51,9 @@ describe("General user settings tab", () => {
widths: [580, 796],
});
- cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab").within(() => {
+ cy.findByTestId("mx_GeneralUserSettingsTab").within(() => {
// Assert that the top heading is rendered
- cy.findByTestId("general").should("have.text", "General").should("be.visible");
+ cy.findByText("General").should("be.visible");
cy.get(".mx_ProfileSettings_profile")
.scrollIntoView()
@@ -83,44 +83,47 @@ describe("General user settings tab", () => {
});
// Wait until spinners disappear
- cy.get(".mx_GeneralUserSettingsTab_accountSection .mx_Spinner").should("not.exist");
- cy.get(".mx_GeneralUserSettingsTab_discovery .mx_Spinner").should("not.exist");
+ cy.findByTestId("accountSection").within(() => {
+ cy.get(".mx_Spinner").should("not.exist");
+ });
+ cy.findByTestId("discoverySection").within(() => {
+ cy.get(".mx_Spinner").should("not.exist");
+ });
- cy.get(".mx_GeneralUserSettingsTab_accountSection").within(() => {
+ cy.findByTestId("accountSection").within(() => {
// Assert that input areas for changing a password exists
- cy.get("form.mx_GeneralUserSettingsTab_changePassword")
+ cy.get("form.mx_GeneralUserSettingsTab_section--account_changePassword")
.scrollIntoView()
.within(() => {
cy.findByLabelText("Current password").should("be.visible");
cy.findByLabelText("New Password").should("be.visible");
cy.findByLabelText("Confirm password").should("be.visible");
});
+ });
+ // Check email addresses area
+ cy.findByTestId("mx_AccountEmailAddresses")
+ .scrollIntoView()
+ .within(() => {
+ // Assert that an input area for a new email address is rendered
+ cy.findByRole("textbox", { name: "Email Address" }).should("be.visible");
- // Check email addresses area
- cy.get(".mx_EmailAddresses")
- .scrollIntoView()
- .within(() => {
- // Assert that an input area for a new email address is rendered
- cy.findByRole("textbox", { name: "Email Address" }).should("be.visible");
-
- // Assert the add button is visible
- cy.findByRole("button", { name: "Add" }).should("be.visible");
- });
+ // Assert the add button is visible
+ cy.findByRole("button", { name: "Add" }).should("be.visible");
+ });
- // Check phone numbers area
- cy.get(".mx_PhoneNumbers")
- .scrollIntoView()
- .within(() => {
- // Assert that an input area for a new phone number is rendered
- cy.findByRole("textbox", { name: "Phone Number" }).should("be.visible");
+ // Check phone numbers area
+ cy.findByTestId("mx_AccountPhoneNumbers")
+ .scrollIntoView()
+ .within(() => {
+ // Assert that an input area for a new phone number is rendered
+ cy.findByRole("textbox", { name: "Phone Number" }).should("be.visible");
- // Assert that the add button is rendered
- cy.findByRole("button", { name: "Add" }).should("be.visible");
- });
- });
+ // Assert that the add button is rendered
+ cy.findByRole("button", { name: "Add" }).should("be.visible");
+ });
// Check language and region setting dropdown
- cy.get(".mx_GeneralUserSettingsTab_languageInput")
+ cy.get(".mx_GeneralUserSettingsTab_section_languageInput")
.scrollIntoView()
.within(() => {
// Check the default value
@@ -156,16 +159,10 @@ describe("General user settings tab", () => {
// Make sure integration manager's toggle switch is enabled
cy.get(".mx_ToggleSwitch_enabled").should("be.visible");
- // Assert space between "Manage integrations" and the integration server address is set to 4px;
- cy.get(".mx_SetIntegrationManager_heading_manager").should("have.css", "column-gap", "4px");
-
- cy.get(".mx_SetIntegrationManager_heading_manager").within(() => {
- cy.get(".mx_SettingsTab_heading").should("have.text", "Manage integrations");
-
- // Assert the headings' inline end margin values are set to zero in favor of the column-gap declaration
- cy.get(".mx_SettingsTab_heading").should("have.css", "margin-inline-end", "0px");
- cy.get(".mx_SettingsTab_subheading").should("have.css", "margin-inline-end", "0px");
- });
+ cy.get(".mx_SetIntegrationManager_heading_manager").should(
+ "have.text",
+ "Manage integrations(scalar.vector.im)",
+ );
});
// Assert the account deactivation button is displayed
@@ -178,7 +175,7 @@ describe("General user settings tab", () => {
});
it("should support adding and removing a profile picture", () => {
- cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab .mx_ProfileSettings").within(() => {
+ cy.get(".mx_SettingsTab .mx_ProfileSettings").within(() => {
// Upload a picture
cy.get(".mx_ProfileSettings_avatarUpload").selectFile("cypress/fixtures/riot.png", { force: true });
@@ -194,7 +191,7 @@ describe("General user settings tab", () => {
it("should set a country calling code based on default_country_code", () => {
// Check phone numbers area
- cy.get(".mx_PhoneNumbers")
+ cy.findByTestId("mx_AccountPhoneNumbers")
.scrollIntoView()
.within(() => {
// Assert that an input area for a new phone number is rendered
@@ -225,7 +222,7 @@ describe("General user settings tab", () => {
});
it("should support changing a display name", () => {
- cy.get(".mx_SettingsTab.mx_GeneralUserSettingsTab .mx_ProfileSettings").within(() => {
+ cy.get(".mx_SettingsTab .mx_ProfileSettings").within(() => {
// Change the diaplay name to USER_NAME_NEW
cy.findByRole("textbox", { name: "Display Name" }).type(`{selectAll}{del}${USER_NAME_NEW}{enter}`);
});
diff --git a/cypress/e2e/settings/preferences-user-settings-tab.spec.ts b/cypress/e2e/settings/preferences-user-settings-tab.spec.ts
index ad16f0a1c59..61f073e62c7 100644
--- a/cypress/e2e/settings/preferences-user-settings-tab.spec.ts
+++ b/cypress/e2e/settings/preferences-user-settings-tab.spec.ts
@@ -35,19 +35,16 @@ describe("Preferences user settings tab", () => {
it("should be rendered properly", () => {
cy.openUserSettings("Preferences");
- cy.get(".mx_SettingsTab.mx_PreferencesUserSettingsTab").within(() => {
+ cy.findByTestId("mx_PreferencesUserSettingsTab").within(() => {
// Assert that the top heading is rendered
- cy.findByTestId("preferences").should("have.text", "Preferences").should("be.visible");
+ cy.contains("Preferences").should("be.visible");
});
- cy.get(".mx_SettingsTab.mx_PreferencesUserSettingsTab").percySnapshotElement(
- "User settings tab - Preferences",
- {
- // Emulate TabbedView's actual min and max widths
- // 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
- // 796: 1036 (mx_TabbedView_tabsOnLeft actual width) - 240 (mx_TabbedView_tabPanel margin-right)
- widths: [580, 796],
- },
- );
+ cy.findByTestId("mx_PreferencesUserSettingsTab").percySnapshotElement("User settings tab - Preferences", {
+ // Emulate TabbedView's actual min and max widths
+ // 580: '.mx_UserSettingsDialog .mx_TabbedView' min-width
+ // 796: 1036 (mx_TabbedView_tabsOnLeft actual width) - 240 (mx_TabbedView_tabPanel margin-right)
+ widths: [580, 796],
+ });
});
});
diff --git a/cypress/e2e/settings/security-user-settings-tab.spec.ts b/cypress/e2e/settings/security-user-settings-tab.spec.ts
new file mode 100644
index 00000000000..341624dee30
--- /dev/null
+++ b/cypress/e2e/settings/security-user-settings-tab.spec.ts
@@ -0,0 +1,72 @@
+/*
+Copyright 2023 Suguru Hirahara
+
+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("Security user settings tab", () => {
+ let homeserver: HomeserverInstance;
+
+ afterEach(() => {
+ cy.stopHomeserver(homeserver);
+ });
+
+ describe("with posthog enabled", () => {
+ beforeEach(() => {
+ // Enable posthog
+ cy.intercept("/config.json?cachebuster=*", (req) => {
+ req.continue((res) => {
+ res.send(200, {
+ ...res.body,
+ posthog: {
+ project_api_key: "foo",
+ api_host: "bar",
+ },
+ privacy_policy_url: "example.tld", // Set privacy policy URL to enable privacyPolicyLink
+ });
+ });
+ });
+
+ cy.startHomeserver("default").then((data) => {
+ homeserver = data;
+ cy.initTestUser(homeserver, "Hanako");
+ });
+
+ // Hide "Notification" toast on Cypress Cloud
+ cy.contains(".mx_Toast_toast h2", "Notifications")
+ .should("exist")
+ .closest(".mx_Toast_toast")
+ .within(() => {
+ cy.findByRole("button", { name: "Dismiss" }).click();
+ });
+
+ cy.get(".mx_Toast_buttons").within(() => {
+ cy.findByRole("button", { name: "Yes" }).should("exist").click(); // Allow analytics
+ });
+
+ cy.openUserSettings("Security");
+ });
+
+ describe("AnalyticsLearnMoreDialog", () => {
+ it("should be rendered properly", () => {
+ cy.findByRole("button", { name: "Learn more" }).click();
+
+ cy.get(".mx_AnalyticsLearnMoreDialog_wrapper").percySnapshotElement("AnalyticsLearnMoreDialog");
+ });
+ });
+ });
+});
diff --git a/cypress/e2e/spaces/spaces.spec.ts b/cypress/e2e/spaces/spaces.spec.ts
index 9b1fb241d0d..47228e2bcd1 100644
--- a/cypress/e2e/spaces/spaces.spec.ts
+++ b/cypress/e2e/spaces/spaces.spec.ts
@@ -140,6 +140,8 @@ describe("Spaces", () => {
cy.findByPlaceholderText("Support").type("Projects");
cy.findByRole("button", { name: "Continue" }).click();
+ cy.get(".mx_SpaceRoomView").percySnapshotElement("Space - 'Invite your teammates' dialog");
+
cy.get(".mx_SpaceRoomView").within(() => {
cy.get("h1").findByText("Invite your teammates");
cy.findByRole("button", { name: "Skip for now" }).click();
diff --git a/cypress/e2e/spotlight/spotlight.spec.ts b/cypress/e2e/spotlight/spotlight.spec.ts
index 0d4c33926bf..507fc2d75fb 100644
--- a/cypress/e2e/spotlight/spotlight.spec.ts
+++ b/cypress/e2e/spotlight/spotlight.spec.ts
@@ -170,10 +170,9 @@ describe("Spotlight", () => {
)
.then(() =>
cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => {
- cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then((_room1Id) => {
+ cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then(async (_room1Id) => {
room1Id = _room1Id;
- bot1.joinRoom(room1Id);
- cy.visit("/#/room/" + room1Id);
+ await bot1.joinRoom(room1Id);
});
bot2.createRoom({ name: room2Name, visibility: Visibility.Public }).then(
({ room_id: _room2Id }) => {
@@ -199,7 +198,14 @@ describe("Spotlight", () => {
});
}),
)
- .then(() => cy.get(".mx_RoomSublist_skeletonUI").should("not.exist"));
+ .then(() => {
+ cy.visit("/#/room/" + room1Id);
+ cy.get(".mx_RoomSublist_skeletonUI").should("not.exist");
+ });
+ });
+ // wait for the room to have the right name
+ cy.get(".mx_RoomHeader").within(() => {
+ cy.findByText(room1Name);
});
});
@@ -210,8 +216,12 @@ describe("Spotlight", () => {
it("should be able to add and remove filters via keyboard", () => {
cy.openSpotlightDialog().within(() => {
- cy.spotlightSearch().type("{downArrow}");
+ cy.wait(1000); // wait for the dialog to settle, otherwise our keypresses might race with an update
+
+ // initially, publicrooms should be highlighted (because there are no other suggestions)
cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true");
+
+ // hitting enter should enable the publicrooms filter
cy.spotlightSearch().type("{enter}");
cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms");
cy.spotlightSearch().type("{backspace}");
@@ -231,7 +241,6 @@ describe("Spotlight", () => {
cy.openSpotlightDialog()
.within(() => {
cy.spotlightSearch().clear().type(room1Name);
- cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).click();
@@ -247,7 +256,6 @@ describe("Spotlight", () => {
.within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room1Name);
- cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).should("contain", "View");
@@ -264,7 +272,6 @@ describe("Spotlight", () => {
.within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room2Name);
- cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room2Name);
cy.spotlightResults().eq(0).should("contain", "Join");
@@ -282,7 +289,6 @@ describe("Spotlight", () => {
.within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room3Name);
- cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room3Name);
cy.spotlightResults().eq(0).should("contain", "View");
@@ -324,7 +330,6 @@ describe("Spotlight", () => {
.within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot1Name);
- cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot1Name);
cy.spotlightResults().eq(0).click();
@@ -339,7 +344,6 @@ describe("Spotlight", () => {
.within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name);
- cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.spotlightResults().eq(0).click();
@@ -357,7 +361,6 @@ describe("Spotlight", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name);
- cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.spotlightResults().eq(0).click();
diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts
index 465aeb9520c..335c87bc01e 100644
--- a/cypress/e2e/threads/threads.spec.ts
+++ b/cypress/e2e/threads/threads.spec.ts
@@ -69,7 +69,7 @@ describe("Threads", () => {
const MessageTimestampColor = "rgb(172, 172, 172)";
const ThreadViewGroupSpacingStart = "56px"; // --ThreadView_group_spacing-start
// Exclude timestamp and read marker from snapshots
- const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
+ const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
cy.get(".mx_RoomView_body").within(() => {
// User sends message
@@ -296,7 +296,7 @@ describe("Threads", () => {
});
cy.findByRole("button", { name: "Threads" })
- .should("have.class", "mx_RightPanel_headerButton_unread") // User asserts thread list unread indicator
+ .should("have.class", "mx_RoomHeader_button--unread") // User asserts thread list unread indicator
.click(); // User opens thread list
// User asserts thread with correct root & latest events & unread dot
@@ -445,7 +445,7 @@ describe("Threads", () => {
// Exclude timestamp, read marker, and mapboxgl-map from snapshots
const percyCSS =
- ".mx_MessageTimestamp, .mx_RoomView_myReadMarker, .mapboxgl-map { visibility: hidden !important; }";
+ ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .mapboxgl-map { visibility: hidden !important; }";
cy.get(".mx_RoomView_body").within(() => {
// User sends message
diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts
index 06b289d1e2f..8d9d8afbd29 100644
--- a/cypress/e2e/timeline/timeline.spec.ts
+++ b/cypress/e2e/timeline/timeline.spec.ts
@@ -149,7 +149,7 @@ describe("Timeline", () => {
describe("configure room", () => {
// Exclude timestamp and read marker from snapshots
- const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
+ const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
beforeEach(() => {
cy.injectAxe();
@@ -171,7 +171,7 @@ describe("Timeline", () => {
// Check the profile resizer's place
// See: _IRCLayout
// --RoomView_MessageList-padding = 18px (See: _RoomView.pcss)
- // --MessageTimestamp-width = $MessageTimestamp_width = 46px (See: _common.pcss)
+ // --MessageTimestamp-width = 46px (See: _MessageTimestamp.pcss)
// --icon-width = 14px
// --right-padding = 5px
// --name-width = 80px
@@ -207,7 +207,7 @@ describe("Timeline", () => {
cy.get(".mx_GenericEventListSummary[data-layout=irc] .mx_GenericEventListSummary_spacer").should(
"have.css",
"line-height",
- "18px", // $irc-line-height: $font-18px (See: _IRCLayout.pcss)
+ "18px", // var(--irc-line-height): $font-18px (See: _IRCLayout.pcss)
);
cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS on IRC layout", { percyCSS });
@@ -319,7 +319,7 @@ describe("Timeline", () => {
.should("have.css", "inset-inline-start", "0px");
// Exclude timestamp and read marker from snapshot
- const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
+ const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
cy.get(".mx_MainSplit").percySnapshotElement("Event line with inline start margin on IRC layout", {
percyCSS,
});
@@ -371,7 +371,7 @@ describe("Timeline", () => {
// Check inline start spacing of collapsed GELS
// See: _EventTile.pcss
// .mx_GenericEventListSummary[data-layout="irc"] > .mx_EventTile_line
- // = var(--name-width) + var(--icon-width) + $MessageTimestamp_width + 2 * var(--right-padding)
+ // = var(--name-width) + var(--icon-width) + var(--MessageTimestamp-width) + 2 * var(--right-padding)
// = 80 + 14 + 46 + 2 * 5
// = 150px
cy.get(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line").should(
@@ -388,7 +388,7 @@ describe("Timeline", () => {
.should("have.css", "margin-inline-end", "0px");
// --icon-width should be applied
cy.get(".mx_EventTile .mx_EventTile_avatar > .mx_BaseAvatar").should("have.css", "width", "14px");
- // $MessageTimestamp_width should be applied
+ // var(--MessageTimestamp-width) should be applied
cy.get(".mx_EventTile > a").should("have.css", "min-width", "46px");
// Record alignment of collapsed GELS and messages on messagePanel
cy.get(".mx_MainSplit").percySnapshotElement("Collapsed GELS and messages on IRC layout", { percyCSS });
@@ -452,7 +452,7 @@ describe("Timeline", () => {
// Hide because flaky - See https://github.com/vector-im/element-web/issues/24957
".mx_TopUnreadMessagesBar, " +
// Exclude timestamp and read marker from snapshots
- ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
+ ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
sendEvent(roomId);
sendEvent(roomId); // check continuation
@@ -583,7 +583,7 @@ describe("Timeline", () => {
cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click();
// Exclude timestamp and read marker from snapshot
- const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
+ //const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
// should not add inline start padding to a hidden event line on IRC layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
@@ -604,9 +604,10 @@ describe("Timeline", () => {
// calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px
.should("have.css", "padding-inline-start", "84px");
- cy.get(".mx_MainSplit").percySnapshotElement("Hidden event line with padding on modern layout", {
- percyCSS,
- });
+ // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881
+ //cy.get(".mx_MainSplit").percySnapshotElement("Hidden event line with padding on modern layout", {
+ // percyCSS,
+ //});
});
it("should click view source event toggle", () => {
@@ -713,6 +714,12 @@ describe("Timeline", () => {
cy.visit("/#/room/" + roomId);
cy.get(".mx_RoomHeader").findByRole("button", { name: "Search" }).click();
+
+ cy.get(".mx_SearchBar").percySnapshotElement("Search bar on the timeline", {
+ // Emulate narrow timeline
+ widths: [320, 640],
+ });
+
cy.get(".mx_SearchBar_input input").type("Message{enter}");
cy.get(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight").should("exist");
@@ -757,7 +764,7 @@ describe("Timeline", () => {
cy.checkA11y();
// Exclude timestamp and read marker from snapshot
- const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
+ const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", {
percyCSS,
widths: [800, 400],
@@ -903,7 +910,7 @@ describe("Timeline", () => {
cy.get(".mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible");
// Exclude timestamp and read marker from snapshot
- const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
+ const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
// Check the margin value of ReplyChains of EventTile at the bottom on IRC layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
@@ -1014,7 +1021,7 @@ describe("Timeline", () => {
cy.viewport(1600, 1200);
// Exclude timestamp and read marker from snapshots
- //const percyCSS = ".mx_MessageTimestamp, .mx_RoomView_myReadMarker { visibility: hidden !important; }";
+ //const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
// Make sure the strings do not overflow on IRC layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
diff --git a/cypress/e2e/widgets/layout.spec.ts b/cypress/e2e/widgets/layout.spec.ts
index 16bee8d2222..0f18ce85c22 100644
--- a/cypress/e2e/widgets/layout.spec.ts
+++ b/cypress/e2e/widgets/layout.spec.ts
@@ -95,6 +95,10 @@ describe("Widget Layout", () => {
cy.stopWebServers();
});
+ it("should be set properly", () => {
+ cy.get(".mx_AppsDrawer").percySnapshotElement("Widgets drawer on the timeline (AppsDrawer)");
+ });
+
it("manually resize the height of the top container layout", () => {
cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
diff --git a/cypress/fixtures/matrix-org-client-login.json b/cypress/fixtures/matrix-org-client-login.json
deleted file mode 100644
index d7c4fde1e5b..00000000000
--- a/cypress/fixtures/matrix-org-client-login.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
- "flows": [
- {
- "type": "m.login.sso",
- "identity_providers": [
- {
- "id": "oidc-github",
- "name": "GitHub",
- "icon": "mxc://matrix.org/sVesTtrFDTpXRbYfpahuJsKP",
- "brand": "github"
- },
- {
- "id": "oidc-google",
- "name": "Google",
- "icon": "mxc://matrix.org/ZlnaaZNPxtUuQemvgQzlOlkz",
- "brand": "google"
- },
- {
- "id": "oidc-gitlab",
- "name": "GitLab",
- "icon": "mxc://matrix.org/MCVOEmFgVieKFshPxmnejWOq",
- "brand": "gitlab"
- },
- {
- "id": "oidc-facebook",
- "name": "Facebook",
- "icon": "mxc://matrix.org/nsyeLIgzxazZmJadflMAsAWG",
- "brand": "facebook"
- },
- {
- "id": "oidc-apple",
- "name": "Apple",
- "icon": "mxc://matrix.org/QQKNSOdLiMHtJhzeAObmkFiU",
- "brand": "apple"
- }
- ]
- },
- {
- "type": "m.login.token"
- },
- {
- "type": "m.login.password"
- },
- {
- "type": "m.login.application_service"
- }
- ]
-}
diff --git a/cypress/fixtures/matrix-org-client-well-known.json b/cypress/fixtures/matrix-org-client-well-known.json
deleted file mode 100644
index ed726e2421b..00000000000
--- a/cypress/fixtures/matrix-org-client-well-known.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "m.homeserver": {
- "base_url": "https://matrix-client.matrix.org"
- },
- "m.identity_server": {
- "base_url": "https://vector.im"
- }
-}
diff --git a/cypress/fixtures/vector-im-identity-v2.json b/cypress/fixtures/vector-im-identity-v2.json
deleted file mode 100644
index 0967ef424bc..00000000000
--- a/cypress/fixtures/vector-im-identity-v2.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/cypress/plugins/docker/index.ts b/cypress/plugins/docker/index.ts
index 9f755da6742..66bab0b8532 100644
--- a/cypress/plugins/docker/index.ts
+++ b/cypress/plugins/docker/index.ts
@@ -36,12 +36,21 @@ export async function dockerRun(opts: {
const params = opts.params ?? [];
if (params?.includes("-v") && userInfo.uid >= 0) {
- // On *nix we run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult
- params.push("-u", `${userInfo.uid}:${userInfo.gid}`);
-
+ // Run the docker container as our uid:gid to prevent problems with permissions.
if (await isPodman()) {
- // keep the user ID if the docker command is actually podman
- params.push("--userns=keep-id");
+ // Note: this setup is for podman rootless containers.
+
+ // In podman, run as root in the container, which maps to the current
+ // user on the host. This is probably the default since Synapse's
+ // Dockerfile doesn't specify, but we're being explicit here
+ // because it's important for the permissions to work.
+ params.push("-u", "0:0");
+
+ // Tell Synapse not to switch UID
+ params.push("-e", "UID=0");
+ params.push("-e", "GID=0");
+ } else {
+ params.push("-u", `${userInfo.uid}:${userInfo.gid}`);
}
}
diff --git a/cypress/support/axe.ts b/cypress/support/axe.ts
index 4040a983d9e..38a297fe182 100644
--- a/cypress/support/axe.ts
+++ b/cypress/support/axe.ts
@@ -59,6 +59,10 @@ Cypress.Commands.overwrite(
"color-contrast": {
enabled: false,
},
+ // link-in-text-block also complains due to known contrast issues
+ "link-in-text-block": {
+ enabled: false,
+ },
...options.rules,
},
},
@@ -67,3 +71,35 @@ Cypress.Commands.overwrite(
);
},
);
+
+// Load axe-core into the window under test.
+//
+// The injectAxe in cypress-axe attempts to load axe via an `eval`. That conflicts with our CSP
+// which disallows "unsafe-eval". So, replace it with an implementation that loads it via an
+// injected