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>watcha-feature/make-nextcloud-documents-integration-a-local-widget
parent
d75e2f19c5
commit
f3f14afbbf
|
After Width: | Height: | Size: 14 KiB |
@ -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"); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -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 { }; |
||||
@ -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 { }; |
||||
@ -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(); |
||||
} |
||||
Loading…
Reference in new issue