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