From bac4575bf7122988baa9faca71552f040d9053c1 Mon Sep 17 00:00:00 2001 From: "Rahian S.S." Date: Sun, 21 Jun 2026 18:21:32 -0300 Subject: [PATCH 1/9] test(e2e): add room toolbox layout spec with setup and pinning assertions --- .../tests/e2e/room-toolbox-layout.spec.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 apps/meteor/tests/e2e/room-toolbox-layout.spec.ts diff --git a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts new file mode 100644 index 0000000000000..307d0460d1b22 --- /dev/null +++ b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts @@ -0,0 +1,61 @@ +import { Users } from './fixtures/userStates'; +import { HomeChannel } from './page-objects'; +import { createTargetChannel, deleteChannel, setSettingValueById } from './utils'; +import { setUserPreferences } from './utils/setUserPreferences'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +const LAYOUT_CONFIG = JSON.stringify({ + maxVisibleNormal: 2, + items: [ + { id: 'thread', featured: true, order: 1 }, + { id: 'members-list', featured: false, order: 2 }, + { id: 'discussions', featured: false, order: 3 }, + ], +}); + +test.describe.serial('room toolbox layout', () => { + let poHomeChannel: HomeChannel; + let targetChannel: string; + + test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'Accounts_AllowFeaturePreview', true); + await setSettingValueById(api, 'Room_Toolbox_Layout', LAYOUT_CONFIG); + await setUserPreferences(api, { + featuresPreview: [{ name: 'roomToolboxLayout', value: true }], + }); + targetChannel = await createTargetChannel(api); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Room_Toolbox_Layout', ''); + await setUserPreferences(api, { + featuresPreview: [{ name: 'roomToolboxLayout', value: false }], + }); + await deleteChannel(api, targetChannel); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + await poHomeChannel.gotoChannel(targetChannel); + }); + + test.describe('Custom Layout Ordering and Pinning', () => { + test('featured action (Threads) is visible in the header', async () => { + await expect(poHomeChannel.roomToolbar.btnThreads).toBeVisible(); + }); + + test('visible normal actions (Members, Discussions) are shown in the header', async () => { + await expect(poHomeChannel.roomToolbar.btnMembers).toBeVisible(); + await expect(poHomeChannel.roomToolbar.btnDiscussion).toBeVisible(); + }); + + test('actions beyond maxVisibleNormal (Files) are in the kebab menu', async () => { + await expect(poHomeChannel.roomToolbar.btnFiles).not.toBeVisible(); + + await poHomeChannel.roomToolbar.openMoreOptions(); + await expect(poHomeChannel.roomToolbar.menuItemPinnedMessages).toBeVisible(); + }); + }); +}); From 270270879b822a8ae6b5168d7bdf9694fd719f36 Mon Sep 17 00:00:00 2001 From: "Rahian S.S." Date: Mon, 22 Jun 2026 20:54:34 -0300 Subject: [PATCH 2/9] test(e2e): add mobile viewport and soft fallback test scenarios --- .../tests/e2e/room-toolbox-layout.spec.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts index 307d0460d1b22..bfe4292d792bf 100644 --- a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts +++ b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts @@ -58,4 +58,57 @@ test.describe.serial('room toolbox layout', () => { await expect(poHomeChannel.roomToolbar.menuItemPinnedMessages).toBeVisible(); }); }); + + test.describe('Mobile Viewport', () => { + test.use({ viewport: { width: 640, height: 460 } }); + + test('featured action (Threads) remains visible in the header on narrow viewport', async ({ page }) => { + poHomeChannel = new HomeChannel(page); + await poHomeChannel.gotoChannel(targetChannel); + + await expect(poHomeChannel.roomToolbar.btnThreads).toBeVisible(); + }); + + test('normal actions (Members, Discussions) collapse into Options dropdown on narrow viewport', async ({ page }) => { + poHomeChannel = new HomeChannel(page); + await poHomeChannel.gotoChannel(targetChannel); + + await expect(poHomeChannel.roomToolbar.btnMembers).not.toBeVisible(); + await expect(poHomeChannel.roomToolbar.btnDiscussion).not.toBeVisible(); + + await poHomeChannel.roomToolbar.openMoreOptions(); + await expect(poHomeChannel.roomToolbar.menu.getMenuItem('Members')).toBeVisible(); + await expect(poHomeChannel.roomToolbar.menu.getMenuItem('Discussions')).toBeVisible(); + }); + }); + + test.describe('Soft Fallbacks', () => { + test('feature disabled: toolbar uses legacy behavior without crashing', async ({ api, page }) => { + await setUserPreferences(api, { + featuresPreview: [{ name: 'roomToolboxLayout', value: false }], + }); + + poHomeChannel = new HomeChannel(page); + await poHomeChannel.gotoChannel(targetChannel); + + await expect(poHomeChannel.roomToolbar.btnMoreOptions).toBeVisible(); + await expect(page.getByRole('toolbar', { name: 'Primary Room actions' })).toBeVisible(); + + await setUserPreferences(api, { + featuresPreview: [{ name: 'roomToolboxLayout', value: true }], + }); + }); + + test('malformed JSON config: toolbar falls back gracefully without crashing', async ({ api, page }) => { + await setSettingValueById(api, 'Room_Toolbox_Layout', '{ invalid json }'); + + poHomeChannel = new HomeChannel(page); + await poHomeChannel.gotoChannel(targetChannel); + + await expect(poHomeChannel.roomToolbar.btnMoreOptions).toBeVisible(); + await expect(page.getByRole('toolbar', { name: 'Primary Room actions' })).toBeVisible(); + + await setSettingValueById(api, 'Room_Toolbox_Layout', LAYOUT_CONFIG); + }); + }); }); From 72c9caf46c46d6843a15450ba39751dba4a403c6 Mon Sep 17 00:00:00 2001 From: "Rahian S.S." Date: Tue, 23 Jun 2026 17:32:43 -0300 Subject: [PATCH 3/9] refactor(e2e): use local page object instances in viewport and fallback tests --- .../tests/e2e/room-toolbox-layout.spec.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts index bfe4292d792bf..669c44fa998e6 100644 --- a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts +++ b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts @@ -63,22 +63,22 @@ test.describe.serial('room toolbox layout', () => { test.use({ viewport: { width: 640, height: 460 } }); test('featured action (Threads) remains visible in the header on narrow viewport', async ({ page }) => { - poHomeChannel = new HomeChannel(page); - await poHomeChannel.gotoChannel(targetChannel); + const po = new HomeChannel(page); + await po.gotoChannel(targetChannel); - await expect(poHomeChannel.roomToolbar.btnThreads).toBeVisible(); + await expect(po.roomToolbar.btnThreads).toBeVisible(); }); test('normal actions (Members, Discussions) collapse into Options dropdown on narrow viewport', async ({ page }) => { - poHomeChannel = new HomeChannel(page); - await poHomeChannel.gotoChannel(targetChannel); + const po = new HomeChannel(page); + await po.gotoChannel(targetChannel); - await expect(poHomeChannel.roomToolbar.btnMembers).not.toBeVisible(); - await expect(poHomeChannel.roomToolbar.btnDiscussion).not.toBeVisible(); + await expect(po.roomToolbar.btnMembers).not.toBeVisible(); + await expect(po.roomToolbar.btnDiscussion).not.toBeVisible(); - await poHomeChannel.roomToolbar.openMoreOptions(); - await expect(poHomeChannel.roomToolbar.menu.getMenuItem('Members')).toBeVisible(); - await expect(poHomeChannel.roomToolbar.menu.getMenuItem('Discussions')).toBeVisible(); + await po.roomToolbar.openMoreOptions(); + await expect(po.roomToolbar.menu.getMenuItem('Members')).toBeVisible(); + await expect(po.roomToolbar.menu.getMenuItem('Discussions')).toBeVisible(); }); }); @@ -88,10 +88,10 @@ test.describe.serial('room toolbox layout', () => { featuresPreview: [{ name: 'roomToolboxLayout', value: false }], }); - poHomeChannel = new HomeChannel(page); - await poHomeChannel.gotoChannel(targetChannel); + const po = new HomeChannel(page); + await po.gotoChannel(targetChannel); - await expect(poHomeChannel.roomToolbar.btnMoreOptions).toBeVisible(); + await expect(po.roomToolbar.btnMoreOptions).toBeVisible(); await expect(page.getByRole('toolbar', { name: 'Primary Room actions' })).toBeVisible(); await setUserPreferences(api, { @@ -102,10 +102,10 @@ test.describe.serial('room toolbox layout', () => { test('malformed JSON config: toolbar falls back gracefully without crashing', async ({ api, page }) => { await setSettingValueById(api, 'Room_Toolbox_Layout', '{ invalid json }'); - poHomeChannel = new HomeChannel(page); - await poHomeChannel.gotoChannel(targetChannel); + const po = new HomeChannel(page); + await po.gotoChannel(targetChannel); - await expect(poHomeChannel.roomToolbar.btnMoreOptions).toBeVisible(); + await expect(po.roomToolbar.btnMoreOptions).toBeVisible(); await expect(page.getByRole('toolbar', { name: 'Primary Room actions' })).toBeVisible(); await setSettingValueById(api, 'Room_Toolbox_Layout', LAYOUT_CONFIG); From 29abb95f62b0cb0c40b7d1262ba247977565c7de Mon Sep 17 00:00:00 2001 From: "Rahian S.S." Date: Tue, 23 Jun 2026 19:09:34 -0300 Subject: [PATCH 4/9] test(e2e): add accessibility test for feature preview screen --- apps/meteor/tests/e2e/feature-preview.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index 260da835e3a1a..28d268481f352 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -66,6 +66,14 @@ test.describe.serial('feature preview', () => { await expect(page.getByRole('main').getByRole('button', { name: 'Navigation' })).toBeVisible(); }); + test('should not have any accessibility violations', async ({ page, makeAxeBuilder }) => { + await page.goto('/account/feature-preview'); + await page.waitForSelector('#main-content'); + + const results = await makeAxeBuilder().include('#main-content').analyze(); + expect(results.violations).toEqual([]); + }); + test.describe('Sidepanel', () => { let newChannelModal: CreateNewChannelModal; From f2a73ec6230e764ae0d4bc74d6e01873cad69e30 Mon Sep 17 00:00:00 2001 From: "Rahian S.S." Date: Tue, 23 Jun 2026 20:01:31 -0300 Subject: [PATCH 5/9] fix: address bot review feedback on layout engine and E2E specs --- .../RoomToolbox/hooks/processRoomActions.ts | 4 +- .../hooks/useRoomToolboxActions.ts | 6 ++- .../tests/e2e/room-toolbox-layout.spec.ts | 47 ++++++++----------- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/processRoomActions.ts b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/processRoomActions.ts index da6dade3a221f..d776820a2cb97 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/processRoomActions.ts +++ b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/processRoomActions.ts @@ -24,7 +24,7 @@ export const processRoomActions = (actionsBase: RoomToolboxBaseAction[], config: const appActions = actionsBase.filter((a) => a.type === 'apps'); const nonAppActions = actionsBase.filter((a) => a.type !== 'apps'); - if (!config?.items || config.items.length === 0) { + if (!config) { const hiddenActions: RoomToolboxHiddenSection[] = []; if (appActions.length > 0) { hiddenActions.push({ id: 'apps', items: appActions }); @@ -36,7 +36,7 @@ export const processRoomActions = (actionsBase: RoomToolboxBaseAction[], config: }; } - const itemMap = new Map(config.items.map((item) => [item.id, item])); + const itemMap = new Map((config.items || []).map((item) => [item.id, item])); const [featuredWithOrder, normalWithOrder] = nonAppActions.reduce< [{ action: RoomToolboxBaseAction; order: number }[], { action: RoomToolboxBaseAction; order: number }[]] diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts index af9ef68bf1b60..776d9abba6872 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts +++ b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts @@ -87,7 +87,7 @@ export const useRoomToolboxActions = ({ actions, openTab }: Pick section.items as RoomToolboxActionConfig[])]; + const orderedOverflowActions = [...typedVisible, ...engineSections.flatMap((section) => section.items as RoomToolboxActionConfig[])].filter((item) => !item.disabled); const sectionsMap = new Map(); for (const item of orderedOverflowActions) { @@ -112,7 +112,9 @@ export const useRoomToolboxActions = ({ actions, openTab }: Pick ({ id: section.id, title: section.id === 'apps' ? t('Apps') : '', - items: (section.items as RoomToolboxActionConfig[]).map((item) => actionToMenuItem(item, openTab, t)), + items: (section.items as RoomToolboxActionConfig[]) + .filter((item) => !item.disabled) + .map((item) => actionToMenuItem(item, openTab, t)), })); return { featuredActions: typedFeatured, visibleActions: typedVisible, hiddenActions }; diff --git a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts index 669c44fa998e6..5f90f7d2c9354 100644 --- a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts +++ b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts @@ -55,60 +55,53 @@ test.describe.serial('room toolbox layout', () => { await expect(poHomeChannel.roomToolbar.btnFiles).not.toBeVisible(); await poHomeChannel.roomToolbar.openMoreOptions(); - await expect(poHomeChannel.roomToolbar.menuItemPinnedMessages).toBeVisible(); + await expect(poHomeChannel.roomToolbar.menuItemFiles).toBeVisible(); }); }); test.describe('Mobile Viewport', () => { test.use({ viewport: { width: 640, height: 460 } }); - test('featured action (Threads) remains visible in the header on narrow viewport', async ({ page }) => { - const po = new HomeChannel(page); - await po.gotoChannel(targetChannel); - - await expect(po.roomToolbar.btnThreads).toBeVisible(); + test('featured action (Threads) remains visible in the header on narrow viewport', async () => { + await expect(poHomeChannel.roomToolbar.btnThreads).toBeVisible(); }); - test('normal actions (Members, Discussions) collapse into Options dropdown on narrow viewport', async ({ page }) => { - const po = new HomeChannel(page); - await po.gotoChannel(targetChannel); + test('normal actions (Members, Discussions) collapse into Options dropdown on narrow viewport', async () => { + await expect(poHomeChannel.roomToolbar.btnMembers).not.toBeVisible(); + await expect(poHomeChannel.roomToolbar.btnDiscussion).not.toBeVisible(); - await expect(po.roomToolbar.btnMembers).not.toBeVisible(); - await expect(po.roomToolbar.btnDiscussion).not.toBeVisible(); - - await po.roomToolbar.openMoreOptions(); - await expect(po.roomToolbar.menu.getMenuItem('Members')).toBeVisible(); - await expect(po.roomToolbar.menu.getMenuItem('Discussions')).toBeVisible(); + await poHomeChannel.roomToolbar.openMoreOptions(); + await expect(poHomeChannel.roomToolbar.menu.getMenuItem('Members')).toBeVisible(); + await expect(poHomeChannel.roomToolbar.menu.getMenuItem('Discussions')).toBeVisible(); }); }); test.describe('Soft Fallbacks', () => { + test.afterEach(async ({ api }) => { + await setSettingValueById(api, 'Room_Toolbox_Layout', LAYOUT_CONFIG); + await setUserPreferences(api, { + featuresPreview: [{ name: 'roomToolboxLayout', value: true }], + }); + }); + test('feature disabled: toolbar uses legacy behavior without crashing', async ({ api, page }) => { await setUserPreferences(api, { featuresPreview: [{ name: 'roomToolboxLayout', value: false }], }); - const po = new HomeChannel(page); - await po.gotoChannel(targetChannel); + await poHomeChannel.gotoChannel(targetChannel); - await expect(po.roomToolbar.btnMoreOptions).toBeVisible(); + await expect(poHomeChannel.roomToolbar.btnMoreOptions).toBeVisible(); await expect(page.getByRole('toolbar', { name: 'Primary Room actions' })).toBeVisible(); - - await setUserPreferences(api, { - featuresPreview: [{ name: 'roomToolboxLayout', value: true }], - }); }); test('malformed JSON config: toolbar falls back gracefully without crashing', async ({ api, page }) => { await setSettingValueById(api, 'Room_Toolbox_Layout', '{ invalid json }'); - const po = new HomeChannel(page); - await po.gotoChannel(targetChannel); + await poHomeChannel.gotoChannel(targetChannel); - await expect(po.roomToolbar.btnMoreOptions).toBeVisible(); + await expect(poHomeChannel.roomToolbar.btnMoreOptions).toBeVisible(); await expect(page.getByRole('toolbar', { name: 'Primary Room actions' })).toBeVisible(); - - await setSettingValueById(api, 'Room_Toolbox_Layout', LAYOUT_CONFIG); }); }); }); From 1239e482311a815c9e73b05dd56a2842f358e0e9 Mon Sep 17 00:00:00 2001 From: "Rahian S.S." Date: Tue, 23 Jun 2026 20:20:18 -0300 Subject: [PATCH 6/9] fix: address follow-up review feedback on accessibility scoping and settings cleanup --- .../RoomToolbox/hooks/useRoomToolboxActions.ts | 16 +++++++++------- apps/meteor/tests/e2e/feature-preview.spec.ts | 9 +++++++-- .../meteor/tests/e2e/room-toolbox-layout.spec.ts | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts index 776d9abba6872..046fc3120c725 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts +++ b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts @@ -109,13 +109,15 @@ export const useRoomToolboxActions = ({ actions, openTab }: Pick ({ - id: section.id, - title: section.id === 'apps' ? t('Apps') : '', - items: (section.items as RoomToolboxActionConfig[]) - .filter((item) => !item.disabled) - .map((item) => actionToMenuItem(item, openTab, t)), - })); + const hiddenActions = engineSections + .map((section) => ({ + id: section.id, + title: section.id === 'apps' ? t('Apps') : '', + items: (section.items as RoomToolboxActionConfig[]) + .filter((item) => !item.disabled) + .map((item) => actionToMenuItem(item, openTab, t)), + })) + .filter((section) => section.items.length > 0); return { featuredActions: typedFeatured, visibleActions: typedVisible, hiddenActions }; } diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index 28d268481f352..f2a25fe2f49fa 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -1,4 +1,5 @@ import { faker } from '@faker-js/faker'; +import AxeBuilder from '@axe-core/playwright'; import { Users } from './fixtures/userStates'; import { AdminInfo, HomeChannel, HomeDiscussion, HomeTeam } from './page-objects'; @@ -66,11 +67,15 @@ test.describe.serial('feature preview', () => { await expect(page.getByRole('main').getByRole('button', { name: 'Navigation' })).toBeVisible(); }); - test('should not have any accessibility violations', async ({ page, makeAxeBuilder }) => { + test('should not have any accessibility violations', async ({ page }) => { await page.goto('/account/feature-preview'); await page.waitForSelector('#main-content'); - const results = await makeAxeBuilder().include('#main-content').analyze(); + const results = await new AxeBuilder({ page }) + .include('#main-content') + .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) + .disableRules(['aria-hidden-focus', 'nested-interactive']) + .analyze(); expect(results.violations).toEqual([]); }); diff --git a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts index 5f90f7d2c9354..c013606e044bf 100644 --- a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts +++ b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts @@ -29,6 +29,7 @@ test.describe.serial('room toolbox layout', () => { }); test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Accounts_AllowFeaturePreview', false); await setSettingValueById(api, 'Room_Toolbox_Layout', ''); await setUserPreferences(api, { featuresPreview: [{ name: 'roomToolboxLayout', value: false }], From 02e76bcff200547968dd1298d8ec2cd0e276714f Mon Sep 17 00:00:00 2001 From: "Rahian S.S." Date: Thu, 25 Jun 2026 13:05:21 -0300 Subject: [PATCH 7/9] fix: correct import order in feature-preview spec for lint rule compliance --- apps/meteor/tests/e2e/feature-preview.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index f2a25fe2f49fa..6e5372657668e 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -1,5 +1,5 @@ -import { faker } from '@faker-js/faker'; import AxeBuilder from '@axe-core/playwright'; +import { faker } from '@faker-js/faker'; import { Users } from './fixtures/userStates'; import { AdminInfo, HomeChannel, HomeDiscussion, HomeTeam } from './page-objects'; From 89387f973da112a4f2be61314f05a258a2644d40 Mon Sep 17 00:00:00 2001 From: "Rahian S.S." Date: Thu, 25 Jun 2026 13:11:52 -0300 Subject: [PATCH 8/9] style: fix formatting in useRoomToolboxActions hook for prettier compliance --- .../room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts index 046fc3120c725..61394dfd54574 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts +++ b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts @@ -87,7 +87,10 @@ export const useRoomToolboxActions = ({ actions, openTab }: Pick section.items as RoomToolboxActionConfig[])].filter((item) => !item.disabled); + const orderedOverflowActions = [ + ...typedVisible, + ...engineSections.flatMap((section) => section.items as RoomToolboxActionConfig[]), + ].filter((item) => !item.disabled); const sectionsMap = new Map(); for (const item of orderedOverflowActions) { From f88c8edc20fc0ee3aaab899c144068be1a44ab3b Mon Sep 17 00:00:00 2001 From: "Rahian S.S." Date: Fri, 26 Jun 2026 15:31:22 -0300 Subject: [PATCH 9/9] test(unit): migrate Room Toolbox layout customization tests to RoomHeader unit tests --- .../views/room/Header/RoomHeader.spec.tsx | 190 +++++++++++++++++- apps/meteor/tests/e2e/feature-preview.spec.ts | 13 -- .../tests/e2e/room-toolbox-layout.spec.ts | 108 ---------- 3 files changed, 185 insertions(+), 126 deletions(-) delete mode 100644 apps/meteor/tests/e2e/room-toolbox-layout.spec.ts diff --git a/apps/meteor/client/views/room/Header/RoomHeader.spec.tsx b/apps/meteor/client/views/room/Header/RoomHeader.spec.tsx index 3db80cf37ce76..42fb618869da1 100644 --- a/apps/meteor/client/views/room/Header/RoomHeader.spec.tsx +++ b/apps/meteor/client/views/room/Header/RoomHeader.spec.tsx @@ -1,11 +1,14 @@ 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 } 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 }); +const mockedRoom = createFakeRoom({ prid: undefined, name: 'general', fname: 'General' }); const appRoot = mockAppRoot() .withRoom(mockedRoom) .wrap((children) => {children}) @@ -20,10 +23,37 @@ jest.mock('./ParentRoom', () => ({ default: jest.fn(() =>
ParentRoom
), })); -jest.mock('./RoomToolbox', () => ({ - __esModule: true, - default: jest.fn(() =>
RoomToolbox
), -})); +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 ; + } + return
RoomToolbox
; + }), + }; +}); + +const mockActions: RoomToolboxActionConfig[] = [ + { id: 'thread', icon: 'thread', title: 'Threads' as any, groups: ['channel'] }, + { id: 'members-list', icon: 'members', title: 'Members' as any, groups: ['channel'] }, + { id: 'discussions', icon: 'discussion', title: 'Discussions' as any, groups: ['channel'] }, + { id: 'files', icon: 'clip', title: 'Files' as any, groups: ['channel'] }, + { id: 'pinned-messages', icon: 'pin', title: 'Pinned Messages' as any, groups: ['channel'] }, +]; + +const mockLayoutConfig = JSON.stringify({ + 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', () => { @@ -62,4 +92,154 @@ describe('RoomHeader', () => { 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, + settings = { + Accounts_AllowFeaturePreview: true, + Room_Toolbox_Layout: mockLayoutConfig, + }, + 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 appRootWithSettings = mockAppRoot() + .withSetting('Accounts_AllowFeaturePreview', settings.Accounts_AllowFeaturePreview) + .withSetting('Room_Toolbox_Layout', settings.Room_Toolbox_Layout) + .withUserPreference('featuresPreview', featuresPreview) + .withRoom(room) + .wrap((children) => {children}) + .build(); + + return render( + + + + + , + { wrapper: appRootWithSettings }, + ); + }; + + 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(); + }); + + 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(); + }); + + 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('should fallback to legacy behavior if layout configuration is malformed JSON', () => { + renderWithLayout(testRoom, undefined, { + Accounts_AllowFeaturePreview: true, + Room_Toolbox_Layout: '{ invalid json }', + }); + + 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(); + }); + }); + + describe('Accessibility (a11y)', () => { + it('should not have any accessibility violations', async () => { + const { container } = renderWithLayout(testRoom); + + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); + }); + }); + }); + }); }); diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index 6e5372657668e..260da835e3a1a 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -1,4 +1,3 @@ -import AxeBuilder from '@axe-core/playwright'; import { faker } from '@faker-js/faker'; import { Users } from './fixtures/userStates'; @@ -67,18 +66,6 @@ test.describe.serial('feature preview', () => { await expect(page.getByRole('main').getByRole('button', { name: 'Navigation' })).toBeVisible(); }); - test('should not have any accessibility violations', async ({ page }) => { - await page.goto('/account/feature-preview'); - await page.waitForSelector('#main-content'); - - const results = await new AxeBuilder({ page }) - .include('#main-content') - .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) - .disableRules(['aria-hidden-focus', 'nested-interactive']) - .analyze(); - expect(results.violations).toEqual([]); - }); - test.describe('Sidepanel', () => { let newChannelModal: CreateNewChannelModal; diff --git a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts b/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts deleted file mode 100644 index c013606e044bf..0000000000000 --- a/apps/meteor/tests/e2e/room-toolbox-layout.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Users } from './fixtures/userStates'; -import { HomeChannel } from './page-objects'; -import { createTargetChannel, deleteChannel, setSettingValueById } from './utils'; -import { setUserPreferences } from './utils/setUserPreferences'; -import { test, expect } from './utils/test'; - -test.use({ storageState: Users.admin.state }); - -const LAYOUT_CONFIG = JSON.stringify({ - maxVisibleNormal: 2, - items: [ - { id: 'thread', featured: true, order: 1 }, - { id: 'members-list', featured: false, order: 2 }, - { id: 'discussions', featured: false, order: 3 }, - ], -}); - -test.describe.serial('room toolbox layout', () => { - let poHomeChannel: HomeChannel; - let targetChannel: string; - - test.beforeAll(async ({ api }) => { - await setSettingValueById(api, 'Accounts_AllowFeaturePreview', true); - await setSettingValueById(api, 'Room_Toolbox_Layout', LAYOUT_CONFIG); - await setUserPreferences(api, { - featuresPreview: [{ name: 'roomToolboxLayout', value: true }], - }); - targetChannel = await createTargetChannel(api); - }); - - test.afterAll(async ({ api }) => { - await setSettingValueById(api, 'Accounts_AllowFeaturePreview', false); - await setSettingValueById(api, 'Room_Toolbox_Layout', ''); - await setUserPreferences(api, { - featuresPreview: [{ name: 'roomToolboxLayout', value: false }], - }); - await deleteChannel(api, targetChannel); - }); - - test.beforeEach(async ({ page }) => { - poHomeChannel = new HomeChannel(page); - await poHomeChannel.gotoChannel(targetChannel); - }); - - test.describe('Custom Layout Ordering and Pinning', () => { - test('featured action (Threads) is visible in the header', async () => { - await expect(poHomeChannel.roomToolbar.btnThreads).toBeVisible(); - }); - - test('visible normal actions (Members, Discussions) are shown in the header', async () => { - await expect(poHomeChannel.roomToolbar.btnMembers).toBeVisible(); - await expect(poHomeChannel.roomToolbar.btnDiscussion).toBeVisible(); - }); - - test('actions beyond maxVisibleNormal (Files) are in the kebab menu', async () => { - await expect(poHomeChannel.roomToolbar.btnFiles).not.toBeVisible(); - - await poHomeChannel.roomToolbar.openMoreOptions(); - await expect(poHomeChannel.roomToolbar.menuItemFiles).toBeVisible(); - }); - }); - - test.describe('Mobile Viewport', () => { - test.use({ viewport: { width: 640, height: 460 } }); - - test('featured action (Threads) remains visible in the header on narrow viewport', async () => { - await expect(poHomeChannel.roomToolbar.btnThreads).toBeVisible(); - }); - - test('normal actions (Members, Discussions) collapse into Options dropdown on narrow viewport', async () => { - await expect(poHomeChannel.roomToolbar.btnMembers).not.toBeVisible(); - await expect(poHomeChannel.roomToolbar.btnDiscussion).not.toBeVisible(); - - await poHomeChannel.roomToolbar.openMoreOptions(); - await expect(poHomeChannel.roomToolbar.menu.getMenuItem('Members')).toBeVisible(); - await expect(poHomeChannel.roomToolbar.menu.getMenuItem('Discussions')).toBeVisible(); - }); - }); - - test.describe('Soft Fallbacks', () => { - test.afterEach(async ({ api }) => { - await setSettingValueById(api, 'Room_Toolbox_Layout', LAYOUT_CONFIG); - await setUserPreferences(api, { - featuresPreview: [{ name: 'roomToolboxLayout', value: true }], - }); - }); - - test('feature disabled: toolbar uses legacy behavior without crashing', async ({ api, page }) => { - await setUserPreferences(api, { - featuresPreview: [{ name: 'roomToolboxLayout', value: false }], - }); - - await poHomeChannel.gotoChannel(targetChannel); - - await expect(poHomeChannel.roomToolbar.btnMoreOptions).toBeVisible(); - await expect(page.getByRole('toolbar', { name: 'Primary Room actions' })).toBeVisible(); - }); - - test('malformed JSON config: toolbar falls back gracefully without crashing', async ({ api, page }) => { - await setSettingValueById(api, 'Room_Toolbox_Layout', '{ invalid json }'); - - await poHomeChannel.gotoChannel(targetChannel); - - await expect(poHomeChannel.roomToolbar.btnMoreOptions).toBeVisible(); - await expect(page.getByRole('toolbar', { name: 'Primary Room actions' })).toBeVisible(); - }); - }); -});