From f3f14afbbf1a619611a44114f2959693602f817a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 26 May 2022 10:19:00 +0100 Subject: [PATCH] Move spaces tests from Puppeteer to Cypress (#8645) * Move spaces tests from Puppeteer to Cypress * Add missing fixture * Tweak synapsedocker to not double error on a docker failure * Fix space hierarchy loading race condition Fixes https://github.com/matrix-org/element-web-rageshakes/issues/10345 * Fix race condition when creating public space with url update code * Try Electron once more due to perms issues around clipboard * Try set browser permissions properly * Try to enable clipboard another way * Try electron again * Try electron again again * Switch to built-in cypress feature for file uploads * Mock clipboard instead * TMPDIR ftw? * uid:gid pls * Clipboard tests can now run on any browser due to mocking * Test Enter as well as button for space creation * Make the test actually work * Update cypress/support/util.ts Co-authored-by: Eric Eastwood Co-authored-by: Eric Eastwood --- .github/workflows/element-build-and-test.yaml | 3 + .gitignore | 1 - cypress/fixtures/riot.png | Bin 0 -> 13818 bytes cypress/integration/5-threads/threads.spec.ts | 10 +- cypress/integration/6-spaces/spaces.spec.ts | 244 ++++++++++++++++++ cypress/plugins/synapsedocker/index.ts | 12 +- cypress/support/client.ts | 15 ++ cypress/support/clipboard.ts | 57 ++++ cypress/support/index.ts | 3 + cypress/support/settings.ts | 4 +- cypress/support/util.ts | 82 ++++++ package.json | 1 + src/actions/MatrixActionCreators.ts | 42 +++ src/components/structures/MatrixChat.tsx | 19 +- src/components/structures/RoomView.tsx | 9 - src/components/structures/SpaceHierarchy.tsx | 9 +- src/components/structures/SpaceRoomView.tsx | 6 +- test/end-to-end-tests/src/scenario.ts | 3 - test/end-to-end-tests/src/scenarios/spaces.ts | 33 --- .../src/usecases/create-space.ts | 82 ------ yarn.lock | 5 + 21 files changed, 492 insertions(+), 148 deletions(-) create mode 100644 cypress/fixtures/riot.png create mode 100644 cypress/integration/6-spaces/spaces.spec.ts create mode 100644 cypress/support/clipboard.ts create mode 100644 cypress/support/util.ts delete mode 100644 test/end-to-end-tests/src/scenarios/spaces.ts delete mode 100644 test/end-to-end-tests/src/usecases/create-space.ts diff --git a/.github/workflows/element-build-and-test.yaml b/.github/workflows/element-build-and-test.yaml index a104709415..9ed06bd8ad 100644 --- a/.github/workflows/element-build-and-test.yaml +++ b/.github/workflows/element-build-and-test.yaml @@ -71,6 +71,7 @@ jobs: # to run the tests, so use chrome. browser: chrome start: npx serve -p 8080 webapp + wait-on: 'http://localhost:8080' record: true command-prefix: 'yarn percy exec --' env: @@ -83,6 +84,8 @@ jobs: PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser # pass GitHub token to allow accurately detecting a build vs a re-run build GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # make Node's os.tmpdir() return something where we actually have permissions + TMPDIR: ${{ runner.temp }} - name: Upload Artifact if: failure() diff --git a/.gitignore b/.gitignore index e360df7767..7d257d7e9a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,4 @@ package-lock.json /cypress/synapselogs # These could have files in them but don't currently # Cypress will still auto-create them though... -/cypress/fixtures /cypress/performance diff --git a/cypress/fixtures/riot.png b/cypress/fixtures/riot.png new file mode 100644 index 0000000000000000000000000000000000000000..ee42954c7826c78416f53af792ee022405d706df GIT binary patch literal 13818 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 T^B6||yQ} { cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}"); // Wait for message to send, get its ID and save as @threadId - cy.get(".mx_RoomView_body .mx_EventTile").contains("Hello Mr. Bot") - .closest(".mx_EventTile[data-scroll-tokens]").invoke("attr", "data-scroll-tokens").as("threadId"); + cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") + .invoke("attr", "data-scroll-tokens").as("threadId"); // Bot starts thread cy.get("@threadId").then(threadId => { @@ -111,7 +111,7 @@ describe("Threads", () => { cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test"); // User reacts to message instead - cy.get(".mx_ThreadView .mx_EventTile").contains("Hello there").closest(".mx_EventTile_line") + cy.get(".mx_ThreadView .mx_EventTile").contains(".mx_EventTile_line", "Hello there") .find('[aria-label="React"]').click({ force: true }); // Cypress has no ability to hover cy.get(".mx_EmojiPicker").within(() => { cy.get('input[type="text"]').type("wave"); @@ -119,7 +119,7 @@ describe("Threads", () => { }); // User redacts their prior response - cy.get(".mx_ThreadView .mx_EventTile").contains("Test").closest(".mx_EventTile_line") + cy.get(".mx_ThreadView .mx_EventTile").contains(".mx_EventTile_line", "Test") .find('[aria-label="Options"]').click({ force: true }); // Cypress has no ability to hover cy.get(".mx_IconizedContextMenu").within(() => { cy.get('[role="menuitem"]').contains("Remove").click(); @@ -166,7 +166,7 @@ describe("Threads", () => { cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Great!"); // User edits & asserts - cy.get(".mx_ThreadView .mx_EventTile_last").contains("Great!").closest(".mx_EventTile_line").within(() => { + cy.get(".mx_ThreadView .mx_EventTile_last").contains(".mx_EventTile_line", "Great!").within(() => { cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}"); }); diff --git a/cypress/integration/6-spaces/spaces.spec.ts b/cypress/integration/6-spaces/spaces.spec.ts new file mode 100644 index 0000000000..e5c03229bf --- /dev/null +++ b/cypress/integration/6-spaces/spaces.spec.ts @@ -0,0 +1,244 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import type { MatrixClient } from "matrix-js-sdk/src/client"; +import type { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests"; +import { SynapseInstance } from "../../plugins/synapsedocker"; +import Chainable = Cypress.Chainable; +import { UserCredentials } from "../../support/login"; + +function openSpaceCreateMenu(): Chainable { + cy.get(".mx_SpaceButton_new").click(); + return cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu"); +} + +function getSpacePanelButton(spaceName: string): Chainable { + return cy.get(`.mx_SpaceButton[aria-label="${spaceName}"]`); +} + +function openSpaceContextMenu(spaceName: string): Chainable { + getSpacePanelButton(spaceName).rightclick(); + return cy.get(".mx_SpacePanel_contextMenu"); +} + +function spaceCreateOptions(spaceName: string): ICreateRoomOpts { + return { + creation_content: { + type: "m.space", + }, + initial_state: [{ + type: "m.room.name", + content: { + name: spaceName, + }, + }], + }; +} + +function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"]["0"] { + return { + type: "m.space.child", + state_key: roomId, + content: { + via: [roomId.split(":")[1]], + }, + }; +} + +describe("Spaces", () => { + let synapse: SynapseInstance; + let user: UserCredentials; + + beforeEach(() => { + cy.startSynapse("default").then(data => { + synapse = data; + + cy.initTestUser(synapse, "Sue").then(_user => { + user = _user; + cy.mockClipboard(); + }); + }); + }); + + afterEach(() => { + cy.stopSynapse(synapse); + }); + + it("should allow user to create public space", () => { + openSpaceCreateMenu().within(() => { + cy.get(".mx_SpaceCreateMenuType_public").click(); + cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]') + .selectFile("cypress/fixtures/riot.png", { force: true }); + cy.get('input[label="Name"]').type("Let's have a Riot"); + cy.get('input[label="Address"]').should("have.value", "lets-have-a-riot"); + cy.get('textarea[label="Description"]').type("This is a space to reminisce Riot.im!"); + cy.get(".mx_AccessibleButton").contains("Create").click(); + }); + + // Create the default General & Random rooms, as well as a custom "Jokes" room + cy.get('input[label="Room name"][value="General"]').should("exist"); + cy.get('input[label="Room name"][value="Random"]').should("exist"); + cy.get('input[placeholder="Support"]').type("Jokes"); + cy.get(".mx_AccessibleButton").contains("Continue").click(); + + // Copy matrix.to link + cy.get(".mx_SpacePublicShare_shareButton").focus().realClick(); + cy.getClipboardText().should("eq", "https://matrix.to/#/#lets-have-a-riot:localhost"); + + // Go to space home + cy.get(".mx_AccessibleButton").contains("Go to my first room").click(); + + // Assert rooms exist in the room list + cy.get(".mx_RoomTile").contains("General").should("exist"); + cy.get(".mx_RoomTile").contains("Random").should("exist"); + cy.get(".mx_RoomTile").contains("Jokes").should("exist"); + }); + + it("should allow user to create private space", () => { + openSpaceCreateMenu().within(() => { + cy.get(".mx_SpaceCreateMenuType_private").click(); + cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]') + .selectFile("cypress/fixtures/riot.png", { force: true }); + cy.get('input[label="Name"]').type("This is not a Riot"); + cy.get('input[label="Address"]').should("not.exist"); + cy.get('textarea[label="Description"]').type("This is a private space of mourning Riot.im..."); + cy.get(".mx_AccessibleButton").contains("Create").click(); + }); + + cy.get(".mx_SpaceRoomView_privateScope_meAndMyTeammatesButton").click(); + + // Create the default General & Random rooms, as well as a custom "Projects" room + cy.get('input[label="Room name"][value="General"]').should("exist"); + cy.get('input[label="Room name"][value="Random"]').should("exist"); + cy.get('input[placeholder="Support"]').type("Projects"); + cy.get(".mx_AccessibleButton").contains("Continue").click(); + + cy.get(".mx_SpaceRoomView").should("contain", "Invite your teammates"); + cy.get(".mx_AccessibleButton").contains("Skip for now").click(); + + // Assert rooms exist in the room list + cy.get(".mx_RoomTile").contains("General").should("exist"); + cy.get(".mx_RoomTile").contains("Random").should("exist"); + cy.get(".mx_RoomTile").contains("Projects").should("exist"); + + // Assert rooms exist in the space explorer + cy.get(".mx_SpaceHierarchy_roomTile").contains("General").should("exist"); + cy.get(".mx_SpaceHierarchy_roomTile").contains("Random").should("exist"); + cy.get(".mx_SpaceHierarchy_roomTile").contains("Projects").should("exist"); + }); + + it("should allow user to create just-me space", () => { + cy.createRoom({ + name: "Sample Room", + }); + + openSpaceCreateMenu().within(() => { + cy.get(".mx_SpaceCreateMenuType_private").click(); + cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]') + .selectFile("cypress/fixtures/riot.png", { force: true }); + cy.get('input[label="Address"]').should("not.exist"); + cy.get('textarea[label="Description"]').type("This is a personal space to mourn Riot.im..."); + cy.get('input[label="Name"]').type("This is my Riot{enter}"); + }); + + cy.get(".mx_SpaceRoomView_privateScope_justMeButton").click(); + + cy.get(".mx_AddExistingToSpace_entry").click(); + cy.get(".mx_AccessibleButton").contains("Add").click(); + + cy.get(".mx_RoomTile").contains("Sample Room").should("exist"); + cy.get(".mx_SpaceHierarchy_roomTile").contains("Sample Room").should("exist"); + }); + + it("should allow user to invite another to a space", () => { + let bot: MatrixClient; + cy.getBot(synapse, "BotBob").then(_bot => { + bot = _bot; + }); + + cy.createSpace({ + visibility: "public" as any, + room_alias_name: "space", + }).as("spaceId"); + + openSpaceContextMenu("#space:localhost").within(() => { + cy.get('.mx_SpacePanel_contextMenu_inviteButton[aria-label="Invite"]').click(); + }); + + cy.get(".mx_SpacePublicShare").within(() => { + // Copy link first + cy.get(".mx_SpacePublicShare_shareButton").focus().realClick(); + cy.getClipboardText().should("eq", "https://matrix.to/#/#space:localhost"); + // Start Matrix invite flow + cy.get(".mx_SpacePublicShare_inviteButton").click(); + }); + + cy.get(".mx_InviteDialog_other").within(() => { + cy.get('input[type="text"]').type(bot.getUserId()); + cy.get(".mx_AccessibleButton").contains("Invite").click(); + }); + + cy.get(".mx_InviteDialog_other").should("not.exist"); + }); + + it("should show space invites at the top of the space panel", () => { + cy.createSpace({ + name: "My Space", + }); + getSpacePanelButton("My Space").should("exist"); + + cy.getBot(synapse, "BotBob").then({ timeout: 10000 }, async bot => { + const { room_id: roomId } = await bot.createRoom(spaceCreateOptions("Space Space")); + await bot.invite(roomId, user.userId); + }); + // Assert that `Space Space` is above `My Space` due to it being an invite + getSpacePanelButton("Space Space").should("exist") + .parent().next().find('.mx_SpaceButton[aria-label="My Space"]').should("exist"); + }); + + it("should include rooms in space home", () => { + cy.createRoom({ + name: "Music", + }).as("roomId1"); + cy.createRoom({ + name: "Gaming", + }).as("roomId2"); + + const spaceName = "Spacey Mc. Space Space"; + cy.all([ + cy.get("@roomId1"), + cy.get("@roomId2"), + ]).then(([roomId1, roomId2]) => { + cy.createSpace({ + name: spaceName, + initial_state: [ + spaceChildInitialState(roomId1), + spaceChildInitialState(roomId2), + ], + }).as("spaceId"); + }); + + cy.get("@spaceId").then(() => { + getSpacePanelButton(spaceName).dblclick(); // Open space home + }); + cy.get(".mx_SpaceRoomView .mx_SpaceHierarchy_list").within(() => { + cy.get(".mx_SpaceHierarchy_roomTile").contains("Music").should("exist"); + cy.get(".mx_SpaceHierarchy_roomTile").contains("Gaming").should("exist"); + }); + }); +}); diff --git a/cypress/plugins/synapsedocker/index.ts b/cypress/plugins/synapsedocker/index.ts index 292c74ee67..7108ade904 100644 --- a/cypress/plugins/synapsedocker/index.ts +++ b/cypress/plugins/synapsedocker/index.ts @@ -66,9 +66,6 @@ async function cfgDirFromTemplate(template: string): Promise { } const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-')); - // change permissions on the temp directory so the docker container can see its contents - await fse.chmod(tempDir, 0o777); - // copy the contents of the template dir, omitting homeserver.yaml as we'll template that console.log(`Copy ${templateDir} -> ${tempDir}`); await fse.copy(templateDir, tempDir, { filter: f => path.basename(f) !== 'homeserver.yaml' }); @@ -113,6 +110,7 @@ async function synapseStart(template: string): Promise { console.log(`Starting synapse with config dir ${synCfg.configDir}...`); const containerName = `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`; + const userInfo = os.userInfo(); const synapseId = await new Promise((resolve, reject) => { childProcess.execFile('docker', [ @@ -121,6 +119,8 @@ async function synapseStart(template: string): Promise { "-d", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`, + // We run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult + "-u", `${userInfo.uid}:${userInfo.gid}`, "matrixdotorg/synapse:develop", "run", ], (err, stdout) => { @@ -129,8 +129,6 @@ async function synapseStart(template: string): Promise { }); }); - synapses.set(synapseId, { synapseId, ...synCfg }); - console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`); // Await Synapse healthcheck @@ -150,7 +148,9 @@ async function synapseStart(template: string): Promise { }); }); - return synapses.get(synapseId); + const synapse: SynapseInstance = { synapseId, ...synCfg }; + synapses.set(synapseId, synapse); + return synapse; } async function synapseStop(id: string): Promise { diff --git a/cypress/support/client.ts b/cypress/support/client.ts index 682f3ee426..6a6a393271 100644 --- a/cypress/support/client.ts +++ b/cypress/support/client.ts @@ -35,6 +35,12 @@ declare global { * @return the ID of the newly created room */ createRoom(options: ICreateRoomOpts): Chainable; + /** + * Create a space with given options. + * @param options the options to apply when creating the space + * @return the ID of the newly created space (room) + */ + createSpace(options: ICreateRoomOpts): Chainable; /** * Invites the given user to the given room. * @param roomId the id of the room to invite to @@ -71,6 +77,15 @@ Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable }); }); +Cypress.Commands.add("createSpace", (options: ICreateRoomOpts): Chainable => { + return cy.createRoom({ + ...options, + creation_content: { + "type": "m.space", + }, + }); +}); + Cypress.Commands.add("inviteUser", (roomId: string, userId: string): Chainable<{}> => { return cy.getClient().then(async (cli: MatrixClient) => { return cli.invite(roomId, userId); diff --git a/cypress/support/clipboard.ts b/cypress/support/clipboard.ts new file mode 100644 index 0000000000..5e80ed8361 --- /dev/null +++ b/cypress/support/clipboard.ts @@ -0,0 +1,57 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import Chainable = Cypress.Chainable; + +// Mock the clipboard, as only Electron gives the app permission to the clipboard API by default +// Virtual clipboard +let copyText: string; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + /** + * Mock the clipboard on the current window, ready for calling `getClipboardText`. + * Irreversible, refresh the window to restore mock. + */ + mockClipboard(): Chainable; + /** + * Read text from the mocked clipboard. + * @return {string} the clipboard text + */ + getClipboardText(): Chainable; + } + } +} + +Cypress.Commands.add("mockClipboard", () => { + cy.window({ log: false }).then(win => { + win.navigator.clipboard.writeText = (text) => { + copyText = text; + return Promise.resolve(); + }; + }); +}); + +Cypress.Commands.add("getClipboardText", (): Chainable => { + return cy.wrap(copyText); +}); + +// Needed to make this file a module +export { }; diff --git a/cypress/support/index.ts b/cypress/support/index.ts index dd8e5cab99..3f40ca198c 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -17,6 +17,7 @@ limitations under the License. /// import "@percy/cypress"; +import "cypress-real-events"; import "./performance"; import "./synapse"; @@ -24,3 +25,5 @@ import "./login"; import "./client"; import "./settings"; import "./bot"; +import "./clipboard"; +import "./util"; diff --git a/cypress/support/settings.ts b/cypress/support/settings.ts index 11f48c2db2..4be44e2711 100644 --- a/cypress/support/settings.ts +++ b/cypress/support/settings.ts @@ -16,7 +16,6 @@ limitations under the License. /// -import "./client"; // XXX: without an (any) import here, types break down import Chainable = Cypress.Chainable; declare global { @@ -99,3 +98,6 @@ Cypress.Commands.add("leaveBeta", (name: string): Chainable> return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click(); }); }); + +// Needed to make this file a module +export { }; diff --git a/cypress/support/util.ts b/cypress/support/util.ts new file mode 100644 index 0000000000..e8f48b4bcc --- /dev/null +++ b/cypress/support/util.ts @@ -0,0 +1,82 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +// @see https://github.com/cypress-io/cypress/issues/915#issuecomment-475862672 +// Modified due to changes to `cy.queue` https://github.com/cypress-io/cypress/pull/17448 +// Note: this DOES NOT run Promises in parallel like `Promise.all` due to the nature +// of Cypress promise-like objects and command queue. This only makes it convenient to use the same +// API but runs the commands sequentially. + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + type ChainableValue = T extends Cypress.Chainable ? V : T; + + interface cy { + all( + commands: T + ): Cypress.Chainable<{ [P in keyof T]: ChainableValue }>; + queue: any; + } + + interface Chainable { + chainerId: string; + } + } +} + +const chainStart = Symbol("chainStart"); + +/** + * @description Returns a single Chainable that resolves when all of the Chainables pass. + * @param {Cypress.Chainable[]} commands - List of Cypress.Chainable to resolve. + * @returns {Cypress.Chainable} Cypress when all Chainables are resolved. + */ +cy.all = function all(commands): Cypress.Chainable { + const chain = cy.wrap(null, { log: false }); + const stopCommand = Cypress._.find(cy.queue.get(), { + attributes: { chainerId: chain.chainerId }, + }); + const startCommand = Cypress._.find(cy.queue.get(), { + attributes: { chainerId: commands[0].chainerId }, + }); + const p = chain.then(() => { + return cy.wrap( + // @see https://lodash.com/docs/4.17.15#lodash + Cypress._(commands) + .map(cmd => { + return cmd[chainStart] + ? cmd[chainStart].attributes + : Cypress._.find(cy.queue.get(), { + attributes: { chainerId: cmd.chainerId }, + }).attributes; + }) + .concat(stopCommand.attributes) + .slice(1) + .map(cmd => { + return cmd.prev.get("subject"); + }) + .value(), + ); + }); + p[chainStart] = startCommand; + return p; +}; + +// Needed to make this file a module +export { }; diff --git a/package.json b/package.json index 6c964a0c64..c49d83bd25 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,7 @@ "blob-polyfill": "^6.0.20211015", "chokidar": "^3.5.1", "cypress": "^9.6.1", + "cypress-real-events": "^1.7.0", "enzyme": "^3.11.0", "enzyme-to-json": "^3.6.2", "eslint": "8.9.0", diff --git a/src/actions/MatrixActionCreators.ts b/src/actions/MatrixActionCreators.ts index b889376336..a307e5b25e 100644 --- a/src/actions/MatrixActionCreators.ts +++ b/src/actions/MatrixActionCreators.ts @@ -18,6 +18,7 @@ import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set"; +import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import dis from "../dispatcher/dispatcher"; import { ActionPayload } from "../dispatcher/payloads"; @@ -175,6 +176,21 @@ export interface IRoomTimelineActionPayload extends Pick { + action: 'MatrixActions.RoomState.events'; + event: MatrixEvent; + state: RoomState; + lastStateEvent: MatrixEvent | null; +} + /** * Create a MatrixActions.Room.timeline action that represents a * MatrixClient `Room.timeline` matrix event, emitted when an event @@ -210,6 +226,31 @@ function createRoomTimelineAction( }; } +/** + * Create a MatrixActions.Room.timeline action that represents a + * MatrixClient `Room.timeline` matrix event, emitted when an event + * is added to or removed from a timeline of a room. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} event the state event received + * @param {RoomState} state the room state into which the event was applied + * @param {MatrixEvent | null} lastStateEvent the previous value for this (event-type, state-key) tuple in room state + * @returns {IRoomStateEventsActionPayload} an action of type `MatrixActions.RoomState.events`. + */ +function createRoomStateEventsAction( + matrixClient: MatrixClient, + event: MatrixEvent, + state: RoomState, + lastStateEvent: MatrixEvent | null, +): IRoomStateEventsActionPayload { + return { + action: 'MatrixActions.RoomState.events', + event, + state, + lastStateEvent, + }; +} + /** * @typedef RoomMembershipAction * @type {Object} @@ -312,6 +353,7 @@ export default { addMatrixClientListener(matrixClient, RoomEvent.Timeline, createRoomTimelineAction); addMatrixClientListener(matrixClient, RoomEvent.MyMembership, createSelfMembershipAction); addMatrixClientListener(matrixClient, MatrixEventEvent.Decrypted, createEventDecryptedAction); + addMatrixClientListener(matrixClient, RoomStateEvent.Events, createRoomStateEventsAction); }, /** diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a41f8000a6..b143fc7448 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -131,6 +131,7 @@ import { IConfigOptions } from "../../IConfigOptions"; import { SnakedObject } from "../../utils/SnakedObject"; import { leaveRoomBehaviour } from "../../utils/leave-behaviour"; import VideoChannelStore from "../../stores/VideoChannelStore"; +import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators"; // legacy export export { default as Views } from "../../Views"; @@ -651,6 +652,20 @@ export default class MatrixChat extends React.PureComponent { case 'view_user_info': this.viewUser(payload.userId, payload.subAction); break; + case "MatrixActions.RoomState.events": { + const event = (payload as IRoomStateEventsActionPayload).event; + if (event.getType() === EventType.RoomCanonicalAlias && + event.getRoomId() === this.state.currentRoomId + ) { + // re-view the current room so we can update alias/id in the URL properly + this.viewRoom({ + action: Action.ViewRoom, + room_id: this.state.currentRoomId, + metricsTrigger: undefined, // room doesn't change + }); + } + break; + } case Action.ViewRoom: { // Takes either a room ID or room alias: if switching to a room the client is already // known to be in (eg. user clicks on a room in the recents panel), supply the ID @@ -891,9 +906,7 @@ export default class MatrixChat extends React.PureComponent { // Store this as the ID of the last room accessed. This is so that we can // persist which room is being stored across refreshes and browser quits. - if (localStorage) { - localStorage.setItem('mx_last_room_id', room.roomId); - } + localStorage?.setItem('mx_last_room_id', room.roomId); } // If we are redirecting to a Room Alias and it is for the room we already showing then replace history item diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 86f581512c..614bbee387 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1137,15 +1137,6 @@ export class RoomView extends React.Component { if (!this.state.room || this.state.room.roomId !== state.roomId) return; switch (ev.getType()) { - case EventType.RoomCanonicalAlias: - // re-view the room so MatrixChat can manage the alias in the URL properly - dis.dispatch({ - action: Action.ViewRoom, - room_id: this.state.room.roomId, - metricsTrigger: undefined, // room doesn't change - }); - break; - case EventType.RoomTombstone: this.setState({ tombstone: this.getRoomTombstone() }); break; diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 0d0dd7ba34..9d7c737c5f 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -524,8 +524,13 @@ export const useRoomHierarchy = (space: Room): { setRooms(hierarchy.rooms); }, [error, hierarchy]); - const loading = hierarchy?.loading ?? true; - return { loading, rooms, hierarchy, loadMore, error }; + return { + loading: hierarchy?.loading ?? true, + rooms, + hierarchy: hierarchy?.root === space ? hierarchy : undefined, + loadMore, + error, + }; }; const useIntersectionObserver = (callback: () => void) => { diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index ff77789802..4f7c801924 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -60,7 +60,7 @@ import { defaultDmsRenderer, defaultRoomsRenderer, } from "../views/dialogs/AddExistingToSpaceDialog"; -import AccessibleButton from "../views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import Field from "../views/elements/Field"; @@ -295,7 +295,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { />; }); - const onNextClick = async (ev) => { + const onNextClick = async (ev: ButtonEvent) => { ev.preventDefault(); if (busy) return; setError(""); @@ -326,7 +326,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { setBusy(false); }; - let onClick = (ev) => { + let onClick = (ev: ButtonEvent) => { ev.preventDefault(); onFinished(); }; diff --git a/test/end-to-end-tests/src/scenario.ts b/test/end-to-end-tests/src/scenario.ts index dc6e1309d7..b0ead39167 100644 --- a/test/end-to-end-tests/src/scenario.ts +++ b/test/end-to-end-tests/src/scenario.ts @@ -24,7 +24,6 @@ import { e2eEncryptionScenarios } from './scenarios/e2e-encryption'; import { ElementSession } from "./session"; import { RestSessionCreator } from "./rest/creator"; import { RestMultiSession } from "./rest/multi"; -import { spacesScenarios } from './scenarios/spaces'; import { RestSession } from "./rest/session"; import { stickerScenarios } from './scenarios/sticker'; import { userViewScenarios } from "./scenarios/user-view"; @@ -56,8 +55,6 @@ export async function scenario(createSession: (s: string) => Promise { - console.log(" creating a space for spaces scenarios:"); - - await alice.delay(1000); // wait for dialogs to close - await setupSpaceUsingAliceAndInviteBob(alice, bob); -} - -const space = "Test Space"; - -async function setupSpaceUsingAliceAndInviteBob(alice: ElementSession, bob: ElementSession): Promise { - await createSpace(alice, space); - await inviteSpace(alice, space, "@bob:localhost"); - await bob.query(`.mx_SpaceButton[aria-label="${space}"]`); // assert invite received -} diff --git a/test/end-to-end-tests/src/usecases/create-space.ts b/test/end-to-end-tests/src/usecases/create-space.ts deleted file mode 100644 index 3fa2730f57..0000000000 --- a/test/end-to-end-tests/src/usecases/create-space.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2021 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 { ElementSession } from "../session"; - -export async function openSpaceCreateMenu(session: ElementSession): Promise { - const spaceCreateButton = await session.query('.mx_SpaceButton_new'); - await spaceCreateButton.click(); -} - -export async function createSpace(session: ElementSession, name: string, isPublic = false): Promise { - session.log.step(`creates space "${name}"`); - - await openSpaceCreateMenu(session); - const className = isPublic ? ".mx_SpaceCreateMenuType_public" : ".mx_SpaceCreateMenuType_private"; - const visibilityButton = await session.query(className); - await visibilityButton.click(); - - const nameInput = await session.query('input[name="spaceName"]'); - await session.replaceInputText(nameInput, name); - - await session.delay(100); - - const createButton = await session.query('.mx_SpaceCreateMenu_wrapper .mx_AccessibleButton_kind_primary'); - await createButton.click(); - - if (!isPublic) { - const justMeButton = await session.query('.mx_SpaceRoomView_privateScope_justMeButton'); - await justMeButton.click(); - const continueButton = await session.query('.mx_AddExistingToSpace_footer .mx_AccessibleButton_kind_primary'); - await continueButton.click(); - } else { - for (let i = 0; i < 2; i++) { - const continueButton = await session.query('.mx_SpaceRoomView_buttons .mx_AccessibleButton_kind_primary'); - await continueButton.click(); - } - } - - session.log.done(); -} - -export async function inviteSpace(session: ElementSession, spaceName: string, userId: string): Promise { - session.log.step(`invites "${userId}" to space "${spaceName}"`); - - const spaceButton = await session.query(`.mx_SpaceButton[aria-label="${spaceName}"]`); - await spaceButton.click({ - button: 'right', - }); - - const inviteButton = await session.query('.mx_SpacePanel_contextMenu_inviteButton[aria-label="Invite"]'); - await inviteButton.click(); - - try { - // You only get this interstitial if it's a public space, so give up after 200ms - // if it hasn't appeared - const button = await session.query('.mx_SpacePublicShare_inviteButton', 200); - await button.click(); - } catch (e) { - // ignore - } - - const inviteTextArea = await session.query(".mx_InviteDialog_editor input"); - await inviteTextArea.type(userId); - const selectUserItem = await session.query(".mx_InviteDialog_roomTile"); - await selectUserItem.click(); - const confirmButton = await session.query(".mx_InviteDialog_goButton"); - await confirmButton.click(); - session.log.done(); -} diff --git a/yarn.lock b/yarn.lock index 9522c92cab..3959d6c847 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3492,6 +3492,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2" integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA== +cypress-real-events@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.0.tgz#ad6a78de33af3af0e6437f5c713e30691c44472c" + integrity sha512-iyXp07j0V9sG3YClVDcvHN2DAQDgr+EjTID82uWDw6OZBlU3pXEBqTMNYqroz3bxlb0k+F74U81aZwzMNaKyew== + cypress@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.6.1.tgz#a7d6b5a53325b3dc4960181f5800a5ade0f085eb"