diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index b782266288..89ab7724d5 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -142,7 +142,7 @@ jobs: run: | echo "sha=$(cat webapp/sha)" >> $GITHUB_OUTPUT - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # XXX: We're checking out untrusted code in a secure context # We need to be careful to not trust anything this code outputs/may do @@ -163,7 +163,7 @@ jobs: echo "CYPRESS_RUST_CRYPTO=1" >> "$GITHUB_ENV" - name: Run Cypress tests - uses: cypress-io/github-action@fa88e4afe551e64c8827a4b9e379afc63d8f691a + uses: cypress-io/github-action@2558ee6af05072a19de2ce92cb68b38616132726 with: working-directory: matrix-react-sdk # The built-in Electron runner seems to grind to a halt trying to run the tests, so use chrome. diff --git a/.github/workflows/element-web.yaml b/.github/workflows/element-web.yaml index d369641f17..9f8b098147 100644 --- a/.github/workflows/element-web.yaml +++ b/.github/workflows/element-web.yaml @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ${{ inputs.react-sdk-repository || github.repository }} diff --git a/.github/workflows/i18n_check.yml b/.github/workflows/i18n_check.yml index e72f8ca7b6..39b57028f8 100644 --- a/.github/workflows/i18n_check.yml +++ b/.github/workflows/i18n_check.yml @@ -7,12 +7,12 @@ jobs: permissions: pull-requests: read steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: "Get modified files" id: changed_files if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot' && github.event.pull_request.user.login != 't3chguy' - uses: tj-actions/changed-files@1c26215f3fbd51eba03bc199e5cbabdfc3584ce3 # v38 + uses: tj-actions/changed-files@48566bbcc22ceb7c5809ebdd27377309f2c3de8c # v39 with: files: | src/i18n/strings/* diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index 248fb50c9e..aa243d2962 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -41,7 +41,7 @@ jobs: - name: ☁️ Deploy to Netlify id: netlify - uses: nwtgck/actions-netlify@5da65c9f74c7961c5501a3ba329b8d0912f39c03 # v2.0 + uses: nwtgck/actions-netlify@7a92f00dde8c92a5a9e8385ec2919775f7647352 # v2.1 with: publish-dir: webapp deploy-message: "Deploy from GitHub Actions" diff --git a/.github/workflows/notify-element-web.yml b/.github/workflows/notify-element-web.yml index 39a252034c..9f88c61e8a 100644 --- a/.github/workflows/notify-element-web.yml +++ b/.github/workflows/notify-element-web.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'matrix-org/matrix-react-sdk' steps: - name: Notify element-web repo that a new SDK build is on develop - uses: peter-evans/repository-dispatch@26b39ed245ab8f31526069329e112ab2fb224588 # v2 + uses: peter-evans/repository-dispatch@bf47d102fdb849e755b0b0023ea3e81a44b6f570 # v2 with: token: ${{ secrets.ELEMENT_BOT_TOKEN }} repository: vector-im/element-web diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 13893058ac..32c69a8fed 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -20,7 +20,7 @@ jobs: name: "Typescript Syntax Check" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: @@ -61,7 +61,7 @@ jobs: name: "Rethemendex Check" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: ./res/css/rethemendex.sh @@ -71,7 +71,7 @@ jobs: name: "ESLint" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: @@ -88,7 +88,7 @@ jobs: name: "Style Lint" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: @@ -105,7 +105,7 @@ jobs: name: "Analyse Dead Code" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad8fdab8c7..8dc9928e22 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ${{ inputs.matrix-js-sdk-sha && 'matrix-org/matrix-react-sdk' || github.repository }} @@ -93,7 +93,7 @@ jobs: name: Element Web Integration Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: repository: ${{ inputs.matrix-js-sdk-sha && 'matrix-org/matrix-react-sdk' || github.repository }} diff --git a/cypress.config.ts b/cypress.config.ts index bc24763852..c56f0e7097 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -15,9 +15,10 @@ limitations under the License. */ import { defineConfig } from "cypress"; +import * as fs from "node:fs"; export default defineConfig({ - videoUploadOnPasses: false, + video: true, projectId: "ppvnzg", experimentalInteractiveRunEvents: true, experimentalMemoryManagement: true, @@ -25,6 +26,18 @@ export default defineConfig({ chromeWebSecurity: false, e2e: { setupNodeEvents(on, config) { + // Delete videos of passing tests + on("after:spec", (spec, results) => { + if (results && results.video) { + const failures = results.tests.some((test) => + test.attempts.some((attempt) => attempt.state === "failed"), + ); + if (!failures) { + fs.unlinkSync(results.video); + } + } + }); + return require("./cypress/plugins/index.ts").default(on, config); }, baseUrl: "http://localhost:8080", diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index a49804a38d..99dc90f9fa 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -103,6 +103,16 @@ describe("Read receipts", () => { }); } + function jumpTo(room: string, message: string) { + cy.getClient().then((cli) => { + findRoomByName(room).then(async ({ roomId }) => { + const room = cli.getRoom(roomId); + const foundMessage = await getMessage(room, message); + cy.visit(`/#/room/${roomId}/${foundMessage.getId()}`); + }); + }); + } + function openThread(rootMessage: string) { cy.log("Open thread", rootMessage); cy.get(".mx_RoomView_body", { log: false }).within(() => { @@ -246,6 +256,10 @@ describe("Read receipts", () => { })(); } + function many(prefix: string, howMany: number): Array { + return Array.from(Array(howMany).keys()).map((i) => prefix + i.toFixed()); + } + /** * BotActionSpec to send a reaction to an existing event into a room * @param targetMessage - the body of the message to send a reaction to @@ -311,6 +325,16 @@ describe("Read receipts", () => { }); } + /** + * Assert that this room remains read, when it was previously read. + * (In practice, this just waits a short while to allow any unread marker to + * appear, and then asserts that the room is read.) + */ + function assertStillRead(room: string) { + cy.wait(200); + assertRead(room); + } + /** * Assert a given room is marked as unread (via the room list tile) * @param room - the name of the room to check @@ -668,11 +692,39 @@ describe("Read receipts", () => { assertUnread(room2, 1); assertUnreadThread("Msg1"); }); - it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); - it("Creating a new thread based on a reply makes the room unread", () => { + // XXX: fails because we jump to the wrong place in the timeline + it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => { + // Given lots of messages are on the main timeline, and one has a thread off it goTo(room1); - receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1"), threadedOff("Reply1", "Resp1")]); - assertUnread(room2, 3); + receiveMessages(room2, [ + ...many("beforeThread", 30), + "ThreadRoot", + threadedOff("ThreadRoot", "InThread"), + ...many("afterThread", 30), + ]); + assertUnread(room2, 62); // Sanity + + // When I jump to an old message and read the thread + jumpTo(room2, "beforeThread0"); + openThread("ThreadRoot"); + + // Then the thread root is marked as read in the main timeline, + // so there are only 30 left - the ones after the thread root. + assertUnread(room2, 30); + }); + it("Creating a new thread based on a reply makes the room unread", () => { + // Given a message and reply exist and are read + goTo(room1); + receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]); + goTo(room2); + goTo(room1); + assertRead(room2); + + // When I receive a thread message created on the reply + receiveMessages(room2, [threadedOff("Reply1", "Resp1")]); + + // Then the room is unread + assertUnread(room2, 1); }); it("Reading a thread whose root is a reply makes the room read", () => { goTo(room1); @@ -692,15 +744,16 @@ describe("Read receipts", () => { describe("editing messages", () => { describe("in the main timeline", () => { // TODO: this passes but we think this should fail, because we think edits should not cause unreads. - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree it.skip("Editing a message makes a room unread", () => { // Given I am not looking at the room goTo(room1); receiveMessages(room2, ["Msg1"]); assertUnread(room2, 1); - markAsRead(room2); + goTo(room2); assertRead(room2); + goTo(room1); // When an edit appears in the room receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); @@ -708,7 +761,7 @@ describe("Read receipts", () => { // Then it becomes unread assertUnread(room2, 1); }); - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree it.skip("Reading an edit makes the room read", () => { // Given an edit is making the room unread goTo(room1); @@ -730,14 +783,12 @@ describe("Read receipts", () => { goTo(room1); assertRead(room2); }); - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. - it.skip("Marking a room as read after an edit makes it read", () => { - // Given an edit is makng a room unread - goTo(room1); + it("Marking a room as read after an edit makes it read", () => { + // Given an edit is making a room unread + goTo(room2); receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - markAsRead(room2); assertRead(room2); + goTo(room1); receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); assertUnread(room2, 1); @@ -747,7 +798,7 @@ describe("Read receipts", () => { // Then the room becomes read assertRead(room2); }); - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree it.skip("Editing a message after marking as read makes the room unread", () => { // Given the room is marked as read goTo(room1); @@ -762,7 +813,7 @@ describe("Read receipts", () => { // Then the room becomes unread assertUnread(room2, 1); }); - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree it.skip("Editing a reply after reading it makes the room unread", () => { // Given the room is all read goTo(room1); @@ -780,7 +831,7 @@ describe("Read receipts", () => { // Then it becomes unread assertUnread(room2, 1); }); - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree it.skip("Editing a reply after marking as read makes the room unread", () => { // Given a reply is marked as read goTo(room1); @@ -795,14 +846,13 @@ describe("Read receipts", () => { // Then the room becomes unread assertUnread(room2, 1); }); - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree it.skip("A room with an edit is still unread after restart", () => { // Given a message is marked as read - goTo(room1); + goTo(room2); receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - markAsRead(room2); assertRead(room2); + goTo(room1); // When an edit appears in the room receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); @@ -814,13 +864,22 @@ describe("Read receipts", () => { saveAndReload(); assertUnread(room2, 1); }); - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. - it.skip("A room where all edits are read is still read after restart", () => { - // Given an edit made the room unread - goTo(room1); + it("An edited message becomes read if it happens while I am looking", () => { + // Given a message is marked as read + goTo(room2); + receiveMessages(room2, ["Msg1"]); + assertRead(room2); + + // When I see an edit appear in the room I am looking at + receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); + + // Then it becomes read + assertRead(room2); + }); + it("A room where all edits are read is still read after restart", () => { + // Given an edit made the room unread + goTo(room2); receiveMessages(room2, ["Msg1"]); - assertUnread(room2, 1); - markAsRead(room2); assertRead(room2); receiveMessages(room2, [editOf("Msg1", "Msg1 Edit1")]); assertUnread(room2, 1); @@ -838,7 +897,7 @@ describe("Read receipts", () => { }); describe("in threads", () => { - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree it.skip("An edit of a threaded message makes the room unread", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); @@ -852,7 +911,7 @@ describe("Read receipts", () => { receiveMessages(room2, [editOf("Resp1", "Edit1")]); assertUnread(room2, 1); }); - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree it.skip("Reading an edit of a threaded message makes the room read", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); @@ -897,7 +956,7 @@ describe("Read receipts", () => { // Then the room becomes unread assertUnread(room2, 1); // TODO: should this edit make us unread? }); - // XXX: fails because on CI the count is 2 instead of 3. Must be a timing issue. + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree it.skip("A room with an edited threaded message is still unread after restart", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]); @@ -906,8 +965,20 @@ describe("Read receipts", () => { saveAndReload(); assertUnread(room2, 3); }); - // XXX: fails because on CI the count is 2 instead of 3. Must be a timing issue. - it.skip("A room where all threaded edits are read is still read after restart", () => { + it("A room where all threaded edits are read is still read after restart", () => { + goTo(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]); + assertUnread(room2, 2); + openThread("Msg1"); + assertRead(room2); + goTo(room1); // Make sure we are looking at room1 after reload + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree + it.skip("A room where all threaded edits are marked as read is still read after restart", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]); assertUnread(room2, 3); @@ -921,7 +992,7 @@ describe("Read receipts", () => { }); describe("thread roots", () => { - // XXX: fails because on CI we get a dot, but locally we get a count. Must be a timing issue. + // XXX: fails because we see a dot instead of an unread number - probably the server and client disagree it.skip("An edit of a thread root makes the room unread", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); @@ -1042,57 +1113,318 @@ describe("Read receipts", () => { describe("thread roots", () => { it("A reaction to a thread root does not make the room unread", () => { + // Given a read thread root exists + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); + assertUnread(room2, 2); + goTo(room2); + openThread("Msg1"); + assertRead(room2); + + // When someone reacts to it + goTo(room1); + receiveMessages(room2, [reactionTo("Msg1", "🪿")]); + cy.wait(200); + + // Then the room is still read + assertRead(room2); + }); + it("Reading a reaction to a thread root leaves the room read", () => { + // Given a read thread root exists + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); + assertUnread(room2, 2); + goTo(room2); + openThread("Msg1"); + assertRead(room2); + + // And the reaction to it does not make us unread + goTo(room1); + receiveMessages(room2, [reactionTo("Msg1", "🪿")]); + assertRead(room2); + + // When we read the reaction and go away again + goTo(room2); + openThread("Msg1"); + assertRead(room2); + goTo(room1); + cy.wait(200); + + // Then the room is still read + assertRead(room2); + }); + // XXX: fails because the room is still "bold" even though the notification counts all disappear + it.skip("Reacting to a thread root after marking as read makes the room unread but not the thread", () => { + // Given a thread root exists goTo(room1); assertRead(room2); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); assertUnread(room2, 2); - goTo(room2); - openThread("Msg1"); + // And we have marked the room as read + markAsRead(room2); assertRead(room2); - goTo(room1); + // When someone reacts to it receiveMessages(room2, [reactionTo("Msg1", "🪿")]); + cy.wait(200); + // Then the room is still read assertRead(room2); }); - it.skip("Reading a reaction to a thread root makes the room read", () => {}); - it.skip("Marking a room as read after a reaction to a thread root makes it read", () => {}); - it.skip("Reacting to a thread root after marking as read makes the room unread but not the thread", () => {}); }); }); describe("redactions", () => { describe("in the main timeline", () => { it("Redacting the message pointed to by my receipt leaves the room read", () => { + // Given I have read the messages in a room goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + goTo(room2); assertRead(room2); + goTo(room1); + + // When the latest message is redacted + receiveMessages(room2, [redactionOf("Msg2")]); + + // Then the room remains read + assertStillRead(room2); + }); + + it("Reading an unread room after a redaction of the latest message makes it read", () => { + // Given an unread room + goTo(room1); receiveMessages(room2, ["Msg1", "Msg2"]); assertUnread(room2, 2); - // When I read the main timeline + // And the latest message has been redacted + receiveMessages(room2, [redactionOf("Msg2")]); + + // When I read the room + goTo(room2); + assertRead(room2); + goTo(room1); + + // Then it becomes read + assertStillRead(room2); + }); + it("Reading an unread room after a redaction of an older message makes it read", () => { + // Given an unread room with an earlier redaction + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + receiveMessages(room2, [redactionOf("Msg1")]); + + // When I read the room + goTo(room2); + assertRead(room2); + goTo(room1); + + // Then it becomes read + assertStillRead(room2); + }); + it("Marking an unread room as read after a redaction makes it read", () => { + // Given an unread room where latest message is redacted + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + receiveMessages(room2, [redactionOf("Msg2")]); + assertUnread(room2, 1); + + // When I mark it as read + markAsRead(room2); + + // Then it becomes read + assertRead(room2); + }); + it("Sending and redacting a message after marking the room as read makes it read", () => { + // Given a room that is marked as read + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + markAsRead(room2); + assertRead(room2); + + // When a message is sent and then redacted + receiveMessages(room2, ["Msg3"]); + assertUnread(room2, 1); + receiveMessages(room2, [redactionOf("Msg3")]); + + // Then the room is read + assertRead(room2); + }); + it("Redacting a message after marking the room as read leaves it read", () => { + // Given a room that is marked as read + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]); + assertUnread(room2, 3); + markAsRead(room2); + assertRead(room2); + + // When we redact some messages + receiveMessages(room2, [redactionOf("Msg3")]); + receiveMessages(room2, [redactionOf("Msg1")]); + + // Then it is still read + assertStillRead(room2); + }); + it("Redacting one of the unread messages reduces the unread count", () => { + // Given an unread room + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]); + assertUnread(room2, 3); + + // When I redact a non-latest message + receiveMessages(room2, [redactionOf("Msg2")]); + + // Then the unread count goes down + assertUnread(room2, 2); + + // And when I redact the latest message + receiveMessages(room2, [redactionOf("Msg3")]); + + // Then the unread count goes down again + assertUnread(room2, 1); + }); + it("Redacting one of the unread messages reduces the unread count after restart", () => { + // Given unread count was reduced by redacting messages + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2", "Msg3"]); + assertUnread(room2, 3); + receiveMessages(room2, [redactionOf("Msg2")]); + assertUnread(room2, 2); + receiveMessages(room2, [redactionOf("Msg3")]); + assertUnread(room2, 1); + + // When I restart + saveAndReload(); + + // Then the unread count is still reduced + assertUnread(room2, 1); + }); + it("Redacting all unread messages makes the room read", () => { + // Given an unread room + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + // When I redact all the unread messages + receiveMessages(room2, [redactionOf("Msg2")]); + receiveMessages(room2, [redactionOf("Msg1")]); + + // Then the room is back to being read + assertRead(room2); + }); + it("Redacting all unread messages makes the room read after restart", () => { + // Given all unread messages were redacted + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + receiveMessages(room2, [redactionOf("Msg2")]); + receiveMessages(room2, [redactionOf("Msg1")]); + assertRead(room2); + + // When I restart + saveAndReload(); + + // Then the room is still read + assertRead(room2); + }); + // TODO: Doesn't work because the test setup can't (yet) find the ID of a redacted message + it.skip("Reacting to a redacted message leaves the room read", () => { + // Given a redacted message exists + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2"]); + receiveMessages(room2, [redactionOf("Msg2")]); + assertUnread(room2, 1); + + // And the room is read + goTo(room2); + assertRead(room2); + cy.wait(200); + goTo(room1); + + // When I react to the redacted message + // TODO: doesn't work yet because we need to be able to look up + // the ID of Msg2 even though it has now disappeared from the + // timeline. + receiveMessages(room2, [reactionTo("Msg2", "🪿")]); + + // Then the room is still read + assertStillRead(room2); + }); + // TODO: Doesn't work because the test setup can't (yet) find the ID of a redacted message + it.skip("Editing a redacted message leaves the room read", () => { + // Given a redacted message exists + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2"]); + receiveMessages(room2, [redactionOf("Msg2")]); + assertUnread(room2, 1); + + // And the room is read + goTo(room2); + assertRead(room2); + goTo(room1); + + // When I attempt to edit the redacted message + // TODO: doesn't work yet because we need to be able to look up + // the ID of Msg2 even though it has now disappeared from the + // timeline. + receiveMessages(room2, [editOf("Msg2", "Msg2 is BACK")]); + + // Then the room is still read + assertStillRead(room2); + }); + // TODO: Doesn't work because the test setup can't (yet) find the ID of a redacted message + it.skip("A reply to a redacted message makes the room unread", () => { + // Given a message was redacted + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2"]); + receiveMessages(room2, [redactionOf("Msg2")]); + assertUnread(room2, 1); + + // And the room is read + goTo(room2); + assertRead(room2); + goTo(room1); + + // When I receive a reply to the redacted message + // TODO: doesn't work yet because we need to be able to look up + // the ID of Msg2 even though it has now disappeared from the + // timeline. + receiveMessages(room2, [replyTo("Msg2", "Reply to Msg2")]); + + // Then the room is unread + assertUnread(room2, 1); + }); + // TODO: Doesn't work because the test setup can't (yet) find the ID of a redacted message + it.skip("Reading a reply to a redacted message marks the room as read", () => { + // Given someone replied to a redacted message + goTo(room1); + receiveMessages(room2, ["Msg1", "Msg2"]); + receiveMessages(room2, [redactionOf("Msg2")]); + assertUnread(room2, 1); + goTo(room2); + assertRead(room2); + goTo(room1); + // TODO: doesn't work yet because we need to be able to look up + // the ID of Msg2 even though it has now disappeared from the + // timeline. + receiveMessages(room2, [replyTo("Msg2", "Reply to Msg2")]); + assertUnread(room2, 1); + + // When I read the reply goTo(room2); assertRead(room2); + // Then the room is unread goTo(room1); - receiveMessages(room2, [redactionOf("Msg2")]); - assertRead(room2); + assertStillRead(room2); }); - - it.skip("Reading an unread room after a redaction of the latest message makes it read", () => {}); - it.skip("Reading an unread room after a redaction of an older message makes it read", () => {}); - it.skip("Marking an unread room as read after a redaction makes it read", () => {}); - it.skip("Sending and redacting a message after marking the room as read makes it unread", () => {}); - it.skip("?? Redacting a message after marking the room as read makes it unread", () => {}); - it.skip("Reacting to a redacted message leaves the room read", () => {}); - it.skip("Editing a redacted message leaves the room read", () => {}); - - it.skip("?? Reading a reaction to a redacted message marks the room as read", () => {}); - it.skip("?? Reading an edit of a redacted message marks the room as read", () => {}); - it.skip("Reading a reply to a redacted message marks the room as read", () => {}); - - it.skip("A room with an unread redaction is still unread after restart", () => {}); - it.skip("A room with a read redaction is still read after restart", () => {}); }); describe("in threads", () => { diff --git a/cypress/e2e/spotlight/spotlight.spec.ts b/cypress/e2e/spotlight/spotlight.spec.ts index d7ce14eff9..0e8ad33672 100644 --- a/cypress/e2e/spotlight/spotlight.spec.ts +++ b/cypress/e2e/spotlight/spotlight.spec.ts @@ -226,8 +226,8 @@ describe("Spotlight", () => { cy.get(".mx_SpotlightDialog_filter").should("contain", "Public spaces"); cy.spotlightSearch().type("{backspace}"); cy.get(".mx_SpotlightDialog_filter").should("not.exist"); + cy.wait(200); // Again, wait to settle so keypresses arrive correctly - cy.spotlightSearch().type("{downArrow}"); cy.spotlightSearch().type("{downArrow}"); cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true"); cy.spotlightSearch().type("{enter}"); @@ -240,6 +240,7 @@ describe("Spotlight", () => { it("should find joined rooms", () => { cy.openSpotlightDialog() .within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightSearch().clear().type(room1Name); cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", room1Name); @@ -254,6 +255,7 @@ describe("Spotlight", () => { it("should find known public rooms", () => { cy.openSpotlightDialog() .within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.PublicRooms); cy.spotlightSearch().clear().type(room1Name); cy.spotlightResults().should("have.length", 1); @@ -270,6 +272,7 @@ describe("Spotlight", () => { it("should find unknown public rooms", () => { cy.openSpotlightDialog() .within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.PublicRooms); cy.spotlightSearch().clear().type(room2Name); cy.spotlightResults().should("have.length", 1); @@ -287,6 +290,7 @@ describe("Spotlight", () => { it("should find unknown public world readable rooms", () => { cy.openSpotlightDialog() .within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.PublicRooms); cy.spotlightSearch().clear().type(room3Name); cy.spotlightResults().should("have.length", 1); @@ -306,6 +310,7 @@ describe("Spotlight", () => { it.skip("should find unknown public rooms on other homeservers", () => { cy.openSpotlightDialog() .within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.PublicRooms); cy.spotlightSearch().clear().type(room3Name); cy.get("[aria-haspopup=true][role=button]").click(); @@ -318,6 +323,7 @@ describe("Spotlight", () => { }) .then(() => cy.spotlightDialog().within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightResults().should("have.length", 1); cy.spotlightResults().eq(0).should("contain", room3Name); cy.spotlightResults().eq(0).should("contain", room3Id); @@ -328,6 +334,7 @@ describe("Spotlight", () => { it("should find known people", () => { cy.openSpotlightDialog() .within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(bot1Name); cy.spotlightResults().should("have.length", 1); @@ -342,6 +349,7 @@ describe("Spotlight", () => { it("should find unknown people", () => { cy.openSpotlightDialog() .within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(bot2Name); cy.spotlightResults().should("have.length", 1); @@ -359,6 +367,7 @@ describe("Spotlight", () => { // Starting a DM with ByteBot (will be turned into a group dm later) cy.openSpotlightDialog().within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(bot2Name); cy.spotlightResults().should("have.length", 1); @@ -414,6 +423,7 @@ describe("Spotlight", () => { // Test against https://github.com/vector-im/element-web/issues/22851 it("should show each person result only once", () => { cy.openSpotlightDialog().within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.People); // 2 rounds of search to simulate the bug conditions. Specifically, the first search @@ -434,6 +444,7 @@ describe("Spotlight", () => { it("should allow opening group chat dialog", () => { cy.openSpotlightDialog() .within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(bot2Name); cy.wait(3000); // wait for the dialog code to settle @@ -457,6 +468,7 @@ describe("Spotlight", () => { cy.visit("/#/home"); cy.openSpotlightDialog().within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type(bot1Name); cy.wait(3000); // wait for the dialog code to settle @@ -467,6 +479,7 @@ describe("Spotlight", () => { it("should be able to navigate results via keyboard", () => { cy.openSpotlightDialog().within(() => { + cy.wait(500); // Wait for dialog to settle cy.spotlightFilter(Filter.People); cy.spotlightSearch().clear().type("b"); // our debouncing logic only starts the search after a short timeout, @@ -475,6 +488,7 @@ describe("Spotlight", () => { cy.get(".mx_Spinner") .should("not.exist") .then(() => { + cy.wait(500); // Wait to settle again cy.spotlightResults() .should("have.length", 2) .then(() => { diff --git a/docs/cypress.md b/docs/cypress.md index 3ef251e9a9..91ec314bac 100644 --- a/docs/cypress.md +++ b/docs/cypress.md @@ -24,11 +24,15 @@ need to have Docker installed and working in order to run the Cypress tests. There are a few different ways to run the tests yourself. The simplest is to run: ``` +docker pull matrixdotorg/synapse:develop yarn run test:cypress ``` This will run the Cypress tests once, non-interactively. +Note: you don't need to run the `docker pull` command every time, but you should +do it regularly to ensure you are running against an up-to-date Synapse. + You can also run individual tests this way too, as you'd expect: ``` @@ -45,7 +49,7 @@ yarn run test:cypress:open ### Matching the CI environment In our Continuous Integration environment, we run the Cypress tests in the -Chrome browser. +Chrome browser, and with the latest Synapse image from Docker Hub. In some rare cases, tests behave differently between different browsers, so if you see CI failures for the Cypress tests, but those tests work OK on your local @@ -64,6 +68,17 @@ Note that you will need to have Chrome installed on your system to run the tests inside those browsers, whereas the default is to use Electron, which is included within the Cypress dependency. +Another cause of inconsistency between local and CI is the Synapse version. The +first time you run the tests, they automatically fetch the latest Docker image +of Synapse, but this won't update again unless you do it explicitly. To update +the Synapse you are using, run: + +``` +docker pull matrixdotorg/synapse:develop +``` + +and then run the tests as normal. + ### Running with Rust cryptography `matrix-js-sdk` is currently in the diff --git a/package.json b/package.json index 99bc0c0db9..3e4a65d731 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "emojibase-regex": "15.0.0", "escape-html": "^1.0.3", "file-saver": "^2.0.5", - "filesize": "10.0.7", + "filesize": "10.0.12", "focus-visible": "^5.2.0", "gfm.css": "^1.1.2", "glob-to-regexp": "^0.4.1", @@ -106,7 +106,7 @@ "opus-recorder": "^8.0.3", "pako": "^2.0.3", "png-chunks-extract": "^1.0.0", - "posthog-js": "1.73.1", + "posthog-js": "1.77.2", "proposal-temporal": "^0.9.0", "qrcode": "1.5.3", "re-resizable": "^6.9.0", @@ -159,7 +159,7 @@ "@types/fs-extra": "^11.0.0", "@types/geojson": "^7946.0.8", "@types/glob-to-regexp": "^0.4.1", - "@types/jest": "29.5.3", + "@types/jest": "29.5.4", "@types/katex": "^0.16.0", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", @@ -181,23 +181,23 @@ "@typescript-eslint/eslint-plugin": "^5.35.1", "@typescript-eslint/parser": "^5.6.0", "allchange": "^1.1.0", - "axe-core": "4.7.2", + "axe-core": "4.8.0", "babel-jest": "^29.0.0", "blob-polyfill": "^7.0.0", "chokidar": "^3.5.1", - "cypress": "^12.0.0", + "cypress": "^13.0.0", "cypress-axe": "^1.0.0", "cypress-multi-reporters": "^1.6.1", "cypress-real-events": "^1.7.1", "cypress-terminal-report": "^5.3.2", - "eslint": "8.45.0", + "eslint": "8.48.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^9.0.0", - "eslint-plugin-deprecate": "^0.7.0", + "eslint-plugin-deprecate": "0.7.0", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jest": "^27.2.1", "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-matrix-org": "1.2.0", + "eslint-plugin-matrix-org": "1.2.1", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-unicorn": "^48.0.0", diff --git a/res/css/compound/_SuccessDialog.pcss b/res/css/compound/_SuccessDialog.pcss index 61f98a97df..9085cedc11 100644 --- a/res/css/compound/_SuccessDialog.pcss +++ b/res/css/compound/_SuccessDialog.pcss @@ -18,7 +18,7 @@ limitations under the License. text-align: center; .mx_Icon { - mask-border: $spacing-16; + margin-bottom: $spacing-16; } .mx_Dialog_header { diff --git a/res/css/views/avatars/_BaseAvatar.pcss b/res/css/views/avatars/_BaseAvatar.pcss index dd643ebfb5..c8e2360aaf 100644 --- a/res/css/views/avatars/_BaseAvatar.pcss +++ b/res/css/views/avatars/_BaseAvatar.pcss @@ -24,8 +24,9 @@ limitations under the License. } button.mx_BaseAvatar { - /* The user agent stylesheet overrides the font-size in this scenario - And that breaks the alignment, emojis, and all sorts of things + /*