Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/components/structures/SpaceRoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,8 @@ const SpaceSetupFirstRooms: React.FC<{
return createRoom(space.client, {
createOpts: {
preset: isPublic ? Preset.PublicChat : Preset.PrivateChat,
name,
},
name,
spinner: false,
encryption: false,
andView: false,
Expand Down Expand Up @@ -423,7 +423,7 @@ const SpaceSetupPublicShare: React.FC<ISpaceSetupPublicShareProps> = ({
<div className="mx_SpaceRoomView_publicShare">
<h1>
{_t("create_space|share_heading", {
name: justCreatedOpts?.createOpts?.name || space.name,
name: justCreatedOpts?.name || space.name,
})}
</h1>
<div className="mx_SpaceRoomView_description">{_t("create_space|share_description")}</div>
Expand All @@ -449,7 +449,7 @@ const SpaceSetupPrivateScope: React.FC<{
<h1>{_t("create_space|private_personal_heading")}</h1>
<div className="mx_SpaceRoomView_description">
{_t("create_space|private_personal_description", {
name: justCreatedOpts?.createOpts?.name || space.name,
name: justCreatedOpts?.name || space.name,
})}
</div>

Expand Down Expand Up @@ -686,7 +686,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
<SpaceSetupFirstRooms
space={this.props.space}
title={_t("create_space|setup_rooms_community_heading", {
spaceName: this.props.justCreatedOpts?.createOpts?.name || this.props.space.name,
spaceName: this.props.justCreatedOpts?.name || this.props.space.name,
})}
description={
<>
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/dialogs/CreateRoomDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
const opts: IOpts = {};
const createOpts: IOpts["createOpts"] = (opts.createOpts = {});
opts.roomType = this.props.type;
createOpts.name = this.state.name;
opts.name = this.state.name;

if (this.state.joinRule === JoinRule.Public) {
createOpts.visibility = Visibility.Public;
Expand All @@ -139,7 +139,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
}

if (this.state.topic) {
createOpts.topic = this.state.topic;
opts.topic = this.state.topic;
}
if (this.state.noFederate) {
createOpts.creation_content = { "m.federate": false };
Expand Down
22 changes: 21 additions & 1 deletion src/createRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,19 @@ import { ElementCallEventType, ElementCallMemberEventType } from "./call-types";

export interface IOpts {
dmUserId?: string;
createOpts?: ICreateRoomOpts;
/**
* The name of the room to be created.
*/
name?: string;
/**
* The topic for the room.
*/
topic?: string;
/**
* Additional options to pass to the room creation API.
* Note: "name", "topic", and "avatar" should be set via their respective properties in IOpts.
*/
createOpts?: Omit<ICreateRoomOpts, "name" | "topic" | "avatar">;
spinner?: boolean;
guestAccess?: boolean;
encryption?: boolean;
Expand Down Expand Up @@ -251,6 +263,14 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
});
}

if (opts.name) {
createOpts.name = opts.name;
}

if (opts.topic) {
createOpts.topic = opts.topic;
}

if (opts.avatar) {
let url = opts.avatar;
if (opts.avatar instanceof File) {
Expand Down
117 changes: 114 additions & 3 deletions test/unit-tests/components/structures/SpaceRoomView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Please see LICENSE files in the repository root for full details.

import React from "react";
import { mocked, type MockedObject } from "jest-mock";
import { type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { render, cleanup, screen, fireEvent } from "jest-matrix-react";
import { type MatrixClient, MatrixEvent, Preset, Room } from "matrix-js-sdk/src/matrix";
import { render, cleanup, screen, fireEvent, waitFor } from "jest-matrix-react";

import { stubClient, mockPlatformPeg, unmockPlatformPeg, withClientContextRenderOptions } from "../../../test-utils";
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
Expand All @@ -17,6 +17,7 @@ import ResizeNotifier from "../../../../src/utils/ResizeNotifier.ts";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks.ts";
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore.ts";
import DMRoomMap from "../../../../src/utils/DMRoomMap.ts";
import { type IOpts } from "../../../../src/createRoom.ts";

describe("SpaceRoomView", () => {
let cli: MockedObject<MatrixClient>;
Expand Down Expand Up @@ -86,7 +87,7 @@ describe("SpaceRoomView", () => {
cleanup();
});

const renderSpaceRoomView = async (): Promise<ReturnType<typeof render>> => {
const renderSpaceRoomView = async (justCreatedOpts?: IOpts): Promise<ReturnType<typeof render>> => {
const resizeNotifier = new ResizeNotifier();
const permalinkCreator = new RoomPermalinkCreator(space);

Expand All @@ -97,6 +98,7 @@ describe("SpaceRoomView", () => {
permalinkCreator={permalinkCreator}
onJoinButtonClicked={jest.fn()}
onRejectButtonClicked={jest.fn()}
justCreatedOpts={justCreatedOpts}
/>,
withClientContextRenderOptions(cli),
);
Expand All @@ -113,5 +115,114 @@ describe("SpaceRoomView", () => {

expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList });
});

it("shows SpaceLandingAddButton context menu when Add button is clicked", async () => {
await renderSpaceRoomView();
await expect(screen.findByText("Welcome to")).resolves.toBeVisible();

const addButton = screen.getByRole("button", { name: /add/i });
fireEvent.click(addButton);

expect(await screen.findByText(/new room/i)).toBeInTheDocument();
expect(screen.getByText(/add existing room/i)).toBeInTheDocument();
});
});

describe("SpaceSetupFirstRooms", () => {
beforeEach(async () => {
await renderSpaceRoomView({
createOpts: { preset: Preset.PublicChat },
});
});

it("renders SpaceSetupFirstRooms with correct title and description", () => {
expect(
screen.getByText("What are some things you want to discuss in !space:example.org?"),
).toBeInTheDocument();
// using regex here since there's a stray <br />
expect(screen.getByText(/let's create a room for each of them/i)).toBeInTheDocument();
expect(
screen.getByText(/you can add more later too, including already existing ones/i),
).toBeInTheDocument();
});

it("renders three input fields with correct placeholders", () => {
expect(screen.getAllByPlaceholderText(/general/i)).toHaveLength(1);
expect(screen.getAllByPlaceholderText(/random/i)).toHaveLength(1);
expect(screen.getAllByPlaceholderText(/support/i)).toHaveLength(1);
});

it("updates input value when typed", () => {
const input = screen.getAllByRole("textbox")[0];
fireEvent.change(input, { target: { value: "My Room" } });
expect(input).toHaveValue("My Room");
});

it("shows 'Skip for now' when all fields are empty, 'Continue' when any field is filled", () => {
// Clear all fields first
screen.getAllByRole("textbox").forEach((input) => fireEvent.change(input, { target: { value: "" } }));

// Should say 'Skip for now'
const button = screen.getByRole("button");
expect(button).toHaveValue("Skip for now");

// Fill a field
fireEvent.change(screen.getAllByRole("textbox")[0], { target: { value: "Room" } });
expect(button).toHaveValue("Continue");
});

it("calls onFinished with no argument when skipping", () => {
const button = screen.getByRole("button");
fireEvent.click(button);
// Since onFinished is handled internally, check that SpaceSetupFirstRooms is no longer rendered
expect(screen.queryByText(/setup_rooms_community_heading/i)).not.toBeInTheDocument();
});

it("calls createRoom for each non-empty field and onFinished with first room id", async () => {
cli.createRoom.mockResolvedValueOnce({ room_id: "room1" }).mockResolvedValueOnce({ room_id: "room2" });

fireEvent.change(screen.getAllByRole("textbox")[0], { target: { value: "Room A" } });
fireEvent.change(screen.getAllByRole("textbox")[1], { target: { value: "Room B" } });
fireEvent.click(screen.getByRole("button"));

await waitFor(() => {
expect(cli.createRoom).toHaveBeenCalledTimes(2);
});
// After finishing, SpaceSetupFirstRooms should not be rendered
expect(screen.queryByText(/setup_rooms_community_heading/i)).not.toBeInTheDocument();
});

it("shows error message if room creation fails", async () => {
// Force failure.
cli.createRoom.mockRejectedValue(new Error("fail"));

// Create a room.
fireEvent.change(screen.getAllByRole("textbox")[0], { target: { value: "Room A" } });
fireEvent.click(screen.getByRole("button"));

await waitFor(() => {
expect(
screen.getByText((content) =>
content.toLowerCase().includes("failed to create initial space rooms"),
),
).toBeInTheDocument();
});
});

it("disables button and shows 'Creating rooms' while busy", async () => {
cli.createRoom.mockImplementation(
() =>
new Promise((resolve) => {
/* intentionally unresolved to mock work by the server */
}),
);

fireEvent.change(screen.getAllByRole("textbox")[0], { target: { value: "Room A" } });
fireEvent.click(screen.getByRole("button"));

const button = screen.getByRole("button");
expect(button).toBeDisabled();
expect(button).toHaveValue("Creating rooms…"); // Note the ellipsis
});
});
});
Loading
Loading