Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
|
import { LayoutContext, RoomToolboxContext } from '@rocket.chat/ui-contexts';
|
|
import type { RoomToolboxActionConfig, LayoutContextValue } from '@rocket.chat/ui-contexts';
|
|
import { render, screen, within } from '@testing-library/react';
|
|
import { axe } from 'jest-axe';
|
|
|
|
import RoomHeader from './RoomHeader';
|
|
import FakeRoomProvider from '../../../../tests/mocks/client/FakeRoomProvider';
|
|
import { createFakeRoom } from '../../../../tests/mocks/data';
|
|
|
|
const mockedRoom = createFakeRoom({ prid: undefined, name: 'general', fname: 'General' });
|
|
const appRoot = mockAppRoot()
|
|
.withRoom(mockedRoom)
|
|
.wrap((children) => <FakeRoomProvider roomOverrides={mockedRoom}>{children}</FakeRoomProvider>)
|
|
.build();
|
|
|
|
jest.mock('../../../../client/lib/getURL', () => ({
|
|
getURL: (url: string) => url,
|
|
}));
|
|
|
|
jest.mock('./ParentRoom', () => ({
|
|
__esModule: true,
|
|
default: jest.fn(() => <div>ParentRoom</div>),
|
|
}));
|
|
|
|
const mockUseRealRoomToolbox = { value: false };
|
|
|
|
jest.mock('./RoomToolbox', () => {
|
|
const ActualRoomToolbox = jest.requireActual('./RoomToolbox/RoomToolbox').default;
|
|
return {
|
|
__esModule: true,
|
|
default: jest.fn((props) => {
|
|
if (mockUseRealRoomToolbox.value) {
|
|
return <ActualRoomToolbox {...props} />;
|
|
}
|
|
return <div>RoomToolbox</div>;
|
|
}),
|
|
};
|
|
});
|
|
|
|
const MockTabComponent = () => <div>tab</div>;
|
|
|
|
const mockActions: RoomToolboxActionConfig[] = [
|
|
{ id: 'thread', icon: 'thread', title: 'Threads' as any, groups: ['channel'], tabComponent: MockTabComponent },
|
|
{ id: 'members-list', icon: 'members', title: 'Members' as any, groups: ['channel'], tabComponent: MockTabComponent },
|
|
{ id: 'discussions', icon: 'discussion', title: 'Discussions' as any, groups: ['channel'], tabComponent: MockTabComponent },
|
|
{ id: 'files', icon: 'clip', title: 'Files' as any, groups: ['channel'], tabComponent: MockTabComponent },
|
|
{ id: 'pinned-messages', icon: 'pin', title: 'Pinned Messages' as any, groups: ['channel'], tabComponent: MockTabComponent },
|
|
];
|
|
|
|
const layoutSetting = (...scopes: object[]) => JSON.stringify(scopes);
|
|
|
|
const mockLayoutConfig = {
|
|
maxVisibleNormal: 2,
|
|
items: [
|
|
{ id: 'thread', featured: true, order: 1 },
|
|
{ id: 'members-list', featured: false, order: 2 },
|
|
{ id: 'discussions', featured: false, order: 3 },
|
|
],
|
|
};
|
|
|
|
describe('RoomHeader', () => {
|
|
describe('Toolbox', () => {
|
|
it('should render toolbox by default', async () => {
|
|
render(<RoomHeader room={mockedRoom} slots={{}} />, { wrapper: appRoot });
|
|
expect(screen.getByLabelText('Toolbox_room_actions')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should not render toolbox if roomToolbox is null and no slots are provided', () => {
|
|
render(
|
|
<RoomHeader
|
|
room={mockedRoom}
|
|
slots={{
|
|
toolbox: {
|
|
hidden: true,
|
|
},
|
|
}}
|
|
/>,
|
|
{ wrapper: appRoot },
|
|
);
|
|
expect(screen.queryByLabelText('Toolbox_room_actions')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should render toolbox if slots.toolbox is provided', () => {
|
|
render(<RoomHeader room={mockedRoom} slots={{ toolbox: {} }} />, { wrapper: appRoot });
|
|
expect(screen.getByLabelText('Toolbox_room_actions')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render custom toolbox content from roomToolbox prop', () => {
|
|
render(<RoomHeader room={mockedRoom} slots={{ toolbox: { content: <div>Custom Toolbox</div> } }} />, { wrapper: appRoot });
|
|
expect(screen.getByText('Custom Toolbox')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render custom toolbox content from slots.toolbox.content', () => {
|
|
render(<RoomHeader room={mockedRoom} slots={{ toolbox: { content: <div>Slotted Toolbox</div> } }} />, { wrapper: appRoot });
|
|
expect(screen.getByText('Slotted Toolbox')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Room Toolbox Layout Engine', () => {
|
|
beforeAll(() => {
|
|
mockUseRealRoomToolbox.value = true;
|
|
});
|
|
|
|
afterAll(() => {
|
|
mockUseRealRoomToolbox.value = false;
|
|
});
|
|
|
|
const renderWithLayout = (
|
|
room = mockedRoom,
|
|
layoutContextValue?: Partial<LayoutContextValue>,
|
|
extraSettings?: Record<string, string | boolean>,
|
|
featuresPreview = [{ name: 'roomToolboxLayout', value: true }],
|
|
) => {
|
|
const mockLayoutContextValue: LayoutContextValue = {
|
|
isEmbedded: false,
|
|
showTopNavbarEmbeddedLayout: false,
|
|
isTablet: false,
|
|
isMobile: false,
|
|
roomToolboxExpanded: true,
|
|
navbar: { searchExpanded: false },
|
|
sidebar: {
|
|
overlayed: false,
|
|
setOverlayed: () => undefined,
|
|
isCollapsed: false,
|
|
shouldToggle: false,
|
|
toggle: () => undefined,
|
|
collapse: () => undefined,
|
|
expand: () => undefined,
|
|
close: () => undefined,
|
|
},
|
|
sidePanel: {
|
|
displaySidePanel: true,
|
|
closeSidePanel: () => undefined,
|
|
openSidePanel: () => undefined,
|
|
},
|
|
size: { sidebar: '240px', contextualBar: '380px' },
|
|
contextualBarPosition: 'relative',
|
|
contextualBarExpanded: false,
|
|
hiddenActions: {
|
|
roomToolbox: [],
|
|
messageToolbox: [],
|
|
composerToolbox: [],
|
|
userToolbox: [],
|
|
},
|
|
...layoutContextValue,
|
|
};
|
|
|
|
const mockToolboxValue = {
|
|
actions: mockActions,
|
|
openTab: () => undefined,
|
|
closeTab: () => undefined,
|
|
};
|
|
|
|
const allowFeaturePreview = extraSettings?.Accounts_AllowFeaturePreview ?? true;
|
|
const layoutValue = extraSettings?.Room_Toolbox_Layout ?? layoutSetting({ roomType: [room.t], ...mockLayoutConfig });
|
|
|
|
const appRootWithSettings = mockAppRoot()
|
|
.withSetting('Accounts_AllowFeaturePreview', allowFeaturePreview as boolean)
|
|
.withSetting('Room_Toolbox_Layout', layoutValue as string)
|
|
.withUserPreference('featuresPreview', featuresPreview)
|
|
.withRoom(room)
|
|
.wrap((children) => <FakeRoomProvider roomOverrides={room}>{children}</FakeRoomProvider>)
|
|
.build();
|
|
|
|
return render(
|
|
<LayoutContext.Provider value={mockLayoutContextValue}>
|
|
<RoomToolboxContext.Provider value={mockToolboxValue}>
|
|
<RoomHeader room={room} slots={{}} />
|
|
</RoomToolboxContext.Provider>
|
|
</LayoutContext.Provider>,
|
|
{ wrapper: appRootWithSettings },
|
|
);
|
|
};
|
|
|
|
const getToolbarSequence = () => {
|
|
const toolbar = within(screen.getByRole('toolbar', { name: 'Toolbox_room_actions' }));
|
|
|
|
return [...toolbar.getAllByRole('button'), ...toolbar.queryAllByRole('separator')]
|
|
.sort((a, b) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1))
|
|
.map((element) => element.getAttribute('title') ?? 'divider');
|
|
};
|
|
|
|
const roomScenarios = [
|
|
{ type: 'c', name: 'public-channel', title: 'Public Channel' },
|
|
{ type: 'p', name: 'private-group', title: 'Private Group' },
|
|
{ type: 'd', name: 'direct-message', title: 'Direct Message' },
|
|
] as const;
|
|
|
|
roomScenarios.forEach(({ type, name, title }) => {
|
|
describe(`Room Type: ${title}`, () => {
|
|
const testRoom = createFakeRoom({
|
|
prid: undefined,
|
|
t: type,
|
|
name,
|
|
fname: name,
|
|
});
|
|
|
|
it('should respect custom featured and visible actions layout and send remaining normal actions to options dropdown', () => {
|
|
renderWithLayout(testRoom);
|
|
|
|
expect(screen.getByTitle('Threads')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Members')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Discussions')).toBeInTheDocument();
|
|
expect(screen.queryByTitle('Files')).not.toBeInTheDocument();
|
|
expect(screen.getByTitle('Options')).toBeInTheDocument();
|
|
|
|
expect(getToolbarSequence()).toEqual(['Threads', 'divider', 'Members', 'Discussions', 'Options']);
|
|
});
|
|
|
|
it('should order featured and visible actions by their configured order instead of their declaration order', () => {
|
|
renderWithLayout(testRoom, undefined, {
|
|
Room_Toolbox_Layout: layoutSetting({
|
|
roomType: [type],
|
|
maxVisibleNormal: 2,
|
|
items: [
|
|
{ id: 'thread', featured: true, order: 2 },
|
|
{ id: 'pinned-messages', featured: true, order: 1 },
|
|
{ id: 'members-list', featured: false, order: 3 },
|
|
{ id: 'discussions', featured: false, order: 2 },
|
|
{ id: 'files', featured: false, order: 1 },
|
|
],
|
|
}),
|
|
});
|
|
|
|
expect(getToolbarSequence()).toEqual(['Pinned Messages', 'Threads', 'divider', 'Files', 'Discussions', 'Options']);
|
|
expect(screen.queryByTitle('Members')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should collapse normal actions into kebab menu when roomToolboxExpanded is false (mobile viewport)', () => {
|
|
renderWithLayout(testRoom, { roomToolboxExpanded: false });
|
|
|
|
expect(screen.getByTitle('Threads')).toBeInTheDocument();
|
|
expect(screen.queryByTitle('Members')).not.toBeInTheDocument();
|
|
expect(screen.queryByTitle('Discussions')).not.toBeInTheDocument();
|
|
expect(screen.queryByTitle('Files')).not.toBeInTheDocument();
|
|
expect(screen.getByTitle('Options')).toBeInTheDocument();
|
|
|
|
expect(getToolbarSequence()).toEqual(['Threads', 'divider', 'Options']);
|
|
});
|
|
|
|
describe('Soft Fallbacks', () => {
|
|
it('should fallback to legacy behavior if feature preview is disabled', () => {
|
|
renderWithLayout(testRoom, undefined, undefined, [{ name: 'roomToolboxLayout', value: false }]);
|
|
|
|
expect(screen.getByTitle('Threads')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Members')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Discussions')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Files')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Pinned Messages')).toBeInTheDocument();
|
|
expect(screen.queryByTitle('Options')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it.each([
|
|
['malformed JSON', '{ invalid json }'],
|
|
['a bare scope object instead of an array', JSON.stringify({ roomType: [type], ...mockLayoutConfig })],
|
|
['the legacy layouts wrapper', JSON.stringify({ layouts: [{ roomType: [type], ...mockLayoutConfig }] })],
|
|
['scoped to an unsupported room type', layoutSetting({ roomType: ['l'], ...mockLayoutConfig })],
|
|
[
|
|
'claiming the same room type twice',
|
|
layoutSetting({ roomType: [type], ...mockLayoutConfig }, { roomType: [type], maxVisibleNormal: 1 }),
|
|
],
|
|
])('should fallback to legacy behavior if layout configuration is %s', (_, layoutValue) => {
|
|
renderWithLayout(testRoom, undefined, { Room_Toolbox_Layout: layoutValue });
|
|
|
|
expect(screen.getByTitle('Threads')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Members')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Discussions')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Files')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Pinned Messages')).toBeInTheDocument();
|
|
expect(screen.queryByTitle('Options')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should fallback to legacy behavior if no scope declares this room type', () => {
|
|
const otherType = type === 'd' ? 'c' : 'd';
|
|
renderWithLayout(testRoom, undefined, {
|
|
Room_Toolbox_Layout: layoutSetting({ roomType: [otherType], ...mockLayoutConfig }),
|
|
});
|
|
|
|
expect(screen.getByTitle('Files')).toBeInTheDocument();
|
|
expect(screen.getByTitle('Pinned Messages')).toBeInTheDocument();
|
|
expect(screen.queryByTitle('Options')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Accessibility (a11y)', () => {
|
|
it('should not have any accessibility violations', async () => {
|
|
const { container } = renderWithLayout(testRoom);
|
|
|
|
const results = await axe(container);
|
|
expect(results).toHaveNoViolations();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('a single setting serving every room type', () => {
|
|
const sharedSetting = layoutSetting(
|
|
{
|
|
roomType: ['c', 'p'],
|
|
maxVisibleNormal: 2,
|
|
items: [
|
|
{ id: 'thread', featured: true, order: 1 },
|
|
{ id: 'members-list', featured: false, order: 2 },
|
|
{ id: 'discussions', featured: false, order: 3 },
|
|
],
|
|
},
|
|
{
|
|
roomType: ['d'],
|
|
maxVisibleNormal: 1,
|
|
items: [{ id: 'files', featured: true, order: 1 }],
|
|
},
|
|
);
|
|
|
|
it.each(['c', 'p'] as const)('should apply the channel scope to room type %s', (type) => {
|
|
const room = createFakeRoom({ prid: undefined, t: type, name: type, fname: type });
|
|
renderWithLayout(room, undefined, { Room_Toolbox_Layout: sharedSetting });
|
|
|
|
expect(getToolbarSequence()).toEqual(['Threads', 'divider', 'Members', 'Discussions', 'Options']);
|
|
});
|
|
|
|
it('should apply the direct message scope from the same setting', () => {
|
|
const room = createFakeRoom({ prid: undefined, t: 'd', name: 'dm', fname: 'dm' });
|
|
renderWithLayout(room, undefined, { Room_Toolbox_Layout: sharedSetting });
|
|
|
|
expect(getToolbarSequence()).toEqual(['Files', 'divider', 'Threads', 'Options']);
|
|
});
|
|
});
|
|
});
|
|
});
|