mirror of https://github.com/vector-im/riot-web
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 <erice@element.io> Co-authored-by: Eric Eastwood <erice@element.io>pull/28217/head
parent
d75e2f19c5
commit
f3f14afbbf
|
@ -71,6 +71,7 @@ jobs:
|
||||||
# to run the tests, so use chrome.
|
# to run the tests, so use chrome.
|
||||||
browser: chrome
|
browser: chrome
|
||||||
start: npx serve -p 8080 webapp
|
start: npx serve -p 8080 webapp
|
||||||
|
wait-on: 'http://localhost:8080'
|
||||||
record: true
|
record: true
|
||||||
command-prefix: 'yarn percy exec --'
|
command-prefix: 'yarn percy exec --'
|
||||||
env:
|
env:
|
||||||
|
@ -83,6 +84,8 @@ jobs:
|
||||||
PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser
|
PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser
|
||||||
# pass GitHub token to allow accurately detecting a build vs a re-run build
|
# pass GitHub token to allow accurately detecting a build vs a re-run build
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# make Node's os.tmpdir() return something where we actually have permissions
|
||||||
|
TMPDIR: ${{ runner.temp }}
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|
|
@ -26,5 +26,4 @@ package-lock.json
|
||||||
/cypress/synapselogs
|
/cypress/synapselogs
|
||||||
# These could have files in them but don't currently
|
# These could have files in them but don't currently
|
||||||
# Cypress will still auto-create them though...
|
# Cypress will still auto-create them though...
|
||||||
/cypress/fixtures
|
|
||||||
/cypress/performance
|
/cypress/performance
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -87,8 +87,8 @@ describe("Threads", () => {
|
||||||
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
|
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
|
// Wait for message to send, get its ID and save as @threadId
|
||||||
cy.get(".mx_RoomView_body .mx_EventTile").contains("Hello Mr. Bot")
|
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
|
||||||
.closest(".mx_EventTile[data-scroll-tokens]").invoke("attr", "data-scroll-tokens").as("threadId");
|
.invoke("attr", "data-scroll-tokens").as("threadId");
|
||||||
|
|
||||||
// Bot starts thread
|
// Bot starts thread
|
||||||
cy.get<string>("@threadId").then(threadId => {
|
cy.get<string>("@threadId").then(threadId => {
|
||||||
|
@ -111,7 +111,7 @@ describe("Threads", () => {
|
||||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test");
|
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test");
|
||||||
|
|
||||||
// User reacts to message instead
|
// 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
|
.find('[aria-label="React"]').click({ force: true }); // Cypress has no ability to hover
|
||||||
cy.get(".mx_EmojiPicker").within(() => {
|
cy.get(".mx_EmojiPicker").within(() => {
|
||||||
cy.get('input[type="text"]').type("wave");
|
cy.get('input[type="text"]').type("wave");
|
||||||
|
@ -119,7 +119,7 @@ describe("Threads", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// User redacts their prior response
|
// 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
|
.find('[aria-label="Options"]').click({ force: true }); // Cypress has no ability to hover
|
||||||
cy.get(".mx_IconizedContextMenu").within(() => {
|
cy.get(".mx_IconizedContextMenu").within(() => {
|
||||||
cy.get('[role="menuitem"]').contains("Remove").click();
|
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!");
|
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Great!");
|
||||||
|
|
||||||
// User edits & asserts
|
// 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('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover
|
||||||
cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}");
|
cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}");
|
||||||
});
|
});
|
||||||
|
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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<JQuery> {
|
||||||
|
cy.get(".mx_SpaceButton_new").click();
|
||||||
|
return cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSpacePanelButton(spaceName: string): Chainable<JQuery> {
|
||||||
|
return cy.get(`.mx_SpaceButton[aria-label="${spaceName}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSpaceContextMenu(spaceName: string): Chainable<JQuery> {
|
||||||
|
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<string>("@roomId1"),
|
||||||
|
cy.get<string>("@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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -66,9 +66,6 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
||||||
}
|
}
|
||||||
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-'));
|
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
|
// copy the contents of the template dir, omitting homeserver.yaml as we'll template that
|
||||||
console.log(`Copy ${templateDir} -> ${tempDir}`);
|
console.log(`Copy ${templateDir} -> ${tempDir}`);
|
||||||
await fse.copy(templateDir, tempDir, { filter: f => path.basename(f) !== 'homeserver.yaml' });
|
await fse.copy(templateDir, tempDir, { filter: f => path.basename(f) !== 'homeserver.yaml' });
|
||||||
|
@ -113,6 +110,7 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
|
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
|
||||||
|
|
||||||
const containerName = `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`;
|
const containerName = `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`;
|
||||||
|
const userInfo = os.userInfo();
|
||||||
|
|
||||||
const synapseId = await new Promise<string>((resolve, reject) => {
|
const synapseId = await new Promise<string>((resolve, reject) => {
|
||||||
childProcess.execFile('docker', [
|
childProcess.execFile('docker', [
|
||||||
|
@ -121,6 +119,8 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
"-d",
|
"-d",
|
||||||
"-v", `${synCfg.configDir}:/data`,
|
"-v", `${synCfg.configDir}:/data`,
|
||||||
"-p", `${synCfg.port}:8008/tcp`,
|
"-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",
|
"matrixdotorg/synapse:develop",
|
||||||
"run",
|
"run",
|
||||||
], (err, stdout) => {
|
], (err, stdout) => {
|
||||||
|
@ -129,8 +129,6 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
synapses.set(synapseId, { synapseId, ...synCfg });
|
|
||||||
|
|
||||||
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
|
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
|
||||||
|
|
||||||
// Await Synapse healthcheck
|
// Await Synapse healthcheck
|
||||||
|
@ -150,7 +148,9 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return synapses.get(synapseId);
|
const synapse: SynapseInstance = { synapseId, ...synCfg };
|
||||||
|
synapses.set(synapseId, synapse);
|
||||||
|
return synapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function synapseStop(id: string): Promise<void> {
|
async function synapseStop(id: string): Promise<void> {
|
||||||
|
|
|
@ -35,6 +35,12 @@ declare global {
|
||||||
* @return the ID of the newly created room
|
* @return the ID of the newly created room
|
||||||
*/
|
*/
|
||||||
createRoom(options: ICreateRoomOpts): Chainable<string>;
|
createRoom(options: ICreateRoomOpts): Chainable<string>;
|
||||||
|
/**
|
||||||
|
* 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<string>;
|
||||||
/**
|
/**
|
||||||
* Invites the given user to the given room.
|
* Invites the given user to the given room.
|
||||||
* @param roomId the id of the room to invite to
|
* @param roomId the id of the room to invite to
|
||||||
|
@ -71,6 +77,15 @@ Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string>
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("createSpace", (options: ICreateRoomOpts): Chainable<string> => {
|
||||||
|
return cy.createRoom({
|
||||||
|
...options,
|
||||||
|
creation_content: {
|
||||||
|
"type": "m.space",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Cypress.Commands.add("inviteUser", (roomId: string, userId: string): Chainable<{}> => {
|
Cypress.Commands.add("inviteUser", (roomId: string, userId: string): Chainable<{}> => {
|
||||||
return cy.getClient().then(async (cli: MatrixClient) => {
|
return cy.getClient().then(async (cli: MatrixClient) => {
|
||||||
return cli.invite(roomId, userId);
|
return cli.invite(roomId, userId);
|
||||||
|
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
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<AUTWindow>;
|
||||||
|
/**
|
||||||
|
* Read text from the mocked clipboard.
|
||||||
|
* @return {string} the clipboard text
|
||||||
|
*/
|
||||||
|
getClipboardText(): Chainable<string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string> => {
|
||||||
|
return cy.wrap(copyText);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Needed to make this file a module
|
||||||
|
export { };
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import "@percy/cypress";
|
import "@percy/cypress";
|
||||||
|
import "cypress-real-events";
|
||||||
|
|
||||||
import "./performance";
|
import "./performance";
|
||||||
import "./synapse";
|
import "./synapse";
|
||||||
|
@ -24,3 +25,5 @@ import "./login";
|
||||||
import "./client";
|
import "./client";
|
||||||
import "./settings";
|
import "./settings";
|
||||||
import "./bot";
|
import "./bot";
|
||||||
|
import "./clipboard";
|
||||||
|
import "./util";
|
||||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import "./client"; // XXX: without an (any) import here, types break down
|
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -99,3 +98,6 @@ Cypress.Commands.add("leaveBeta", (name: string): Chainable<JQuery<HTMLElement>>
|
||||||
return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
|
return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Needed to make this file a module
|
||||||
|
export { };
|
||||||
|
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
// @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> = T extends Cypress.Chainable<infer V> ? V : T;
|
||||||
|
|
||||||
|
interface cy {
|
||||||
|
all<T extends Cypress.Chainable[] | []>(
|
||||||
|
commands: T
|
||||||
|
): Cypress.Chainable<{ [P in keyof T]: ChainableValue<T[P]> }>;
|
||||||
|
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 { };
|
|
@ -169,6 +169,7 @@
|
||||||
"blob-polyfill": "^6.0.20211015",
|
"blob-polyfill": "^6.0.20211015",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"cypress": "^9.6.1",
|
"cypress": "^9.6.1",
|
||||||
|
"cypress-real-events": "^1.7.0",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-to-json": "^3.6.2",
|
"enzyme-to-json": "^3.6.2",
|
||||||
"eslint": "8.9.0",
|
"eslint": "8.9.0",
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
import { IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set";
|
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 dis from "../dispatcher/dispatcher";
|
||||||
import { ActionPayload } from "../dispatcher/payloads";
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
|
@ -175,6 +176,21 @@ export interface IRoomTimelineActionPayload extends Pick<ActionPayload, "action"
|
||||||
isLiveUnfilteredRoomTimelineEvent: boolean;
|
isLiveUnfilteredRoomTimelineEvent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef IRoomStateEventsActionPayload
|
||||||
|
* @type {Object}
|
||||||
|
* @property {string} action 'MatrixActions.RoomState.events'.
|
||||||
|
* @property {MatrixEvent} event the state event received
|
||||||
|
* @property {RoomState} state the room state into which the event was applied
|
||||||
|
* @property {MatrixEvent | null} lastStateEvent the previous value for this (event-type, state-key) tuple in room state
|
||||||
|
*/
|
||||||
|
export interface IRoomStateEventsActionPayload extends Pick<ActionPayload, "action"> {
|
||||||
|
action: 'MatrixActions.RoomState.events';
|
||||||
|
event: MatrixEvent;
|
||||||
|
state: RoomState;
|
||||||
|
lastStateEvent: MatrixEvent | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a MatrixActions.Room.timeline action that represents a
|
* Create a MatrixActions.Room.timeline action that represents a
|
||||||
* MatrixClient `Room.timeline` matrix event, emitted when an event
|
* 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
|
* @typedef RoomMembershipAction
|
||||||
* @type {Object}
|
* @type {Object}
|
||||||
|
@ -312,6 +353,7 @@ export default {
|
||||||
addMatrixClientListener(matrixClient, RoomEvent.Timeline, createRoomTimelineAction);
|
addMatrixClientListener(matrixClient, RoomEvent.Timeline, createRoomTimelineAction);
|
||||||
addMatrixClientListener(matrixClient, RoomEvent.MyMembership, createSelfMembershipAction);
|
addMatrixClientListener(matrixClient, RoomEvent.MyMembership, createSelfMembershipAction);
|
||||||
addMatrixClientListener(matrixClient, MatrixEventEvent.Decrypted, createEventDecryptedAction);
|
addMatrixClientListener(matrixClient, MatrixEventEvent.Decrypted, createEventDecryptedAction);
|
||||||
|
addMatrixClientListener(matrixClient, RoomStateEvent.Events, createRoomStateEventsAction);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -131,6 +131,7 @@ import { IConfigOptions } from "../../IConfigOptions";
|
||||||
import { SnakedObject } from "../../utils/SnakedObject";
|
import { SnakedObject } from "../../utils/SnakedObject";
|
||||||
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
|
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
|
||||||
import VideoChannelStore from "../../stores/VideoChannelStore";
|
import VideoChannelStore from "../../stores/VideoChannelStore";
|
||||||
|
import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators";
|
||||||
|
|
||||||
// legacy export
|
// legacy export
|
||||||
export { default as Views } from "../../Views";
|
export { default as Views } from "../../Views";
|
||||||
|
@ -651,6 +652,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
case 'view_user_info':
|
case 'view_user_info':
|
||||||
this.viewUser(payload.userId, payload.subAction);
|
this.viewUser(payload.userId, payload.subAction);
|
||||||
break;
|
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: {
|
case Action.ViewRoom: {
|
||||||
// Takes either a room ID or room alias: if switching to a room the client is already
|
// 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
|
// 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<IProps, IState> {
|
||||||
|
|
||||||
// Store this as the ID of the last room accessed. This is so that we can
|
// 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.
|
// 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
|
// If we are redirecting to a Room Alias and it is for the room we already showing then replace history item
|
||||||
|
|
|
@ -1137,15 +1137,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
if (!this.state.room || this.state.room.roomId !== state.roomId) return;
|
if (!this.state.room || this.state.room.roomId !== state.roomId) return;
|
||||||
|
|
||||||
switch (ev.getType()) {
|
switch (ev.getType()) {
|
||||||
case EventType.RoomCanonicalAlias:
|
|
||||||
// re-view the room so MatrixChat can manage the alias in the URL properly
|
|
||||||
dis.dispatch<ViewRoomPayload>({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: this.state.room.roomId,
|
|
||||||
metricsTrigger: undefined, // room doesn't change
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EventType.RoomTombstone:
|
case EventType.RoomTombstone:
|
||||||
this.setState({ tombstone: this.getRoomTombstone() });
|
this.setState({ tombstone: this.getRoomTombstone() });
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -524,8 +524,13 @@ export const useRoomHierarchy = (space: Room): {
|
||||||
setRooms(hierarchy.rooms);
|
setRooms(hierarchy.rooms);
|
||||||
}, [error, hierarchy]);
|
}, [error, hierarchy]);
|
||||||
|
|
||||||
const loading = hierarchy?.loading ?? true;
|
return {
|
||||||
return { loading, rooms, hierarchy, loadMore, error };
|
loading: hierarchy?.loading ?? true,
|
||||||
|
rooms,
|
||||||
|
hierarchy: hierarchy?.root === space ? hierarchy : undefined,
|
||||||
|
loadMore,
|
||||||
|
error,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const useIntersectionObserver = (callback: () => void) => {
|
const useIntersectionObserver = (callback: () => void) => {
|
||||||
|
|
|
@ -60,7 +60,7 @@ import {
|
||||||
defaultDmsRenderer,
|
defaultDmsRenderer,
|
||||||
defaultRoomsRenderer,
|
defaultRoomsRenderer,
|
||||||
} from "../views/dialogs/AddExistingToSpaceDialog";
|
} from "../views/dialogs/AddExistingToSpaceDialog";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
import Field from "../views/elements/Field";
|
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();
|
ev.preventDefault();
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
setError("");
|
setError("");
|
||||||
|
@ -326,7 +326,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
let onClick = (ev) => {
|
let onClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
onFinished();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,7 +24,6 @@ import { e2eEncryptionScenarios } from './scenarios/e2e-encryption';
|
||||||
import { ElementSession } from "./session";
|
import { ElementSession } from "./session";
|
||||||
import { RestSessionCreator } from "./rest/creator";
|
import { RestSessionCreator } from "./rest/creator";
|
||||||
import { RestMultiSession } from "./rest/multi";
|
import { RestMultiSession } from "./rest/multi";
|
||||||
import { spacesScenarios } from './scenarios/spaces';
|
|
||||||
import { RestSession } from "./rest/session";
|
import { RestSession } from "./rest/session";
|
||||||
import { stickerScenarios } from './scenarios/sticker';
|
import { stickerScenarios } from './scenarios/sticker';
|
||||||
import { userViewScenarios } from "./scenarios/user-view";
|
import { userViewScenarios } from "./scenarios/user-view";
|
||||||
|
@ -56,8 +55,6 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
||||||
console.log("create REST users:");
|
console.log("create REST users:");
|
||||||
const charlies = await createRestUsers(restCreator);
|
const charlies = await createRestUsers(restCreator);
|
||||||
await lazyLoadingScenarios(alice, bob, charlies);
|
await lazyLoadingScenarios(alice, bob, charlies);
|
||||||
// do spaces scenarios last as the rest of the alice/bob tests may get confused by spaces
|
|
||||||
await spacesScenarios(alice, bob);
|
|
||||||
|
|
||||||
// we spawn another session for stickers, partially because it involves injecting
|
// we spawn another session for stickers, partially because it involves injecting
|
||||||
// a custom sticker picker widget for the account, although mostly because for these
|
// a custom sticker picker widget for the account, although mostly because for these
|
||||||
|
|
|
@ -1,33 +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 { createSpace, inviteSpace } from "../usecases/create-space";
|
|
||||||
import { ElementSession } from "../session";
|
|
||||||
|
|
||||||
export async function spacesScenarios(alice: ElementSession, bob: ElementSession): Promise<void> {
|
|
||||||
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<void> {
|
|
||||||
await createSpace(alice, space);
|
|
||||||
await inviteSpace(alice, space, "@bob:localhost");
|
|
||||||
await bob.query(`.mx_SpaceButton[aria-label="${space}"]`); // assert invite received
|
|
||||||
}
|
|
|
@ -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<void> {
|
|
||||||
const spaceCreateButton = await session.query('.mx_SpaceButton_new');
|
|
||||||
await spaceCreateButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createSpace(session: ElementSession, name: string, isPublic = false): Promise<void> {
|
|
||||||
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<void> {
|
|
||||||
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();
|
|
||||||
}
|
|
|
@ -3492,6 +3492,11 @@ csstype@^3.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
||||||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
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:
|
cypress@^9.6.1:
|
||||||
version "9.6.1"
|
version "9.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.6.1.tgz#a7d6b5a53325b3dc4960181f5800a5ade0f085eb"
|
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.6.1.tgz#a7d6b5a53325b3dc4960181f5800a5ade0f085eb"
|
||||||
|
|
Loading…
Reference in New Issue