diff --git a/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx index 08aa89928063b..122a5a52f591e 100644 --- a/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsSection/SettingsSection.tsx @@ -13,13 +13,14 @@ export type SettingsSectionProps = { groupId: string; hasReset?: boolean; sectionName: string; + sectionTitle?: string; currentTab?: string; solo: boolean; help?: ReactNode; children?: ReactNode; }; -function SettingsSection({ groupId, hasReset = true, sectionName, currentTab, solo, help, children }: SettingsSectionProps) { +function SettingsSection({ groupId, hasReset = true, sectionTitle, sectionName, currentTab, solo, help, children }: SettingsSectionProps) { const { t } = useTranslation(); const editableSettings = useEditableSettings( @@ -74,7 +75,7 @@ function SettingsSection({ groupId, hasReset = true, sectionName, currentTab, so {help && ( diff --git a/apps/meteor/client/views/admin/settings/groups/EnterpriseGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/EnterpriseGroupPage.tsx index ede698bab11fc..1827c4e23aec9 100644 --- a/apps/meteor/client/views/admin/settings/groups/EnterpriseGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/EnterpriseGroupPage.tsx @@ -1,11 +1,13 @@ -import { Box } from '@rocket.chat/fuselage'; +import { Accordion, Box } from '@rocket.chat/fuselage'; import { PageScrollableContentWithShadow } from '@rocket.chat/ui-client'; import { useRouter } from '@rocket.chat/ui-contexts'; import { useCallback } from 'react'; import type { MouseEvent } from 'react'; -import { Trans } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; +import { useEditableSettingsGroupSections } from '../../EditableSettingsContext'; import SettingsGroupPage from '../SettingsGroupPage'; +import Section from '../SettingsSection'; type EnterpriseGroupPageProps = { _id: string; @@ -27,9 +29,11 @@ const useRedirectToRouteLink = (onClick: (event: MouseEvent) return { href: '#', onClick: handleClick }; }; -const EnterpriseGroupPage = ({ _id, i18nLabel, onClickBack, ...props }: EnterpriseGroupPageProps) => { +const EnterpriseGroupPage = ({ _id, i18nLabel, hasReset, currentTab, onClickBack, ...props }: EnterpriseGroupPageProps) => { + const { t } = useTranslation(); const { navigate } = useRouter(); const redirectProps = useRedirectToRouteLink(() => navigate('/admin/subscription')); + const sections = useEditableSettingsGroupSections(_id); return ( @@ -39,6 +43,19 @@ const EnterpriseGroupPage = ({ _id, i18nLabel, onClickBack, ...props }: Enterpri i18nKey='Workspace_license_is_now_managed_from_the_subscription_page' components={{ a: }} /> + + {sections.map((sectionName) => ( +
+ ))} + diff --git a/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/LicenseStatus.tsx b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/LicenseStatus.tsx index 05498db381657..124bcba2bcfc5 100644 --- a/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/LicenseStatus.tsx +++ b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/LicenseStatus.tsx @@ -12,7 +12,7 @@ const LicenseStatus = ({ isValidating, isValid, invalidMessage }: LicenseStatusP if (isValidating) { return ( - + ); @@ -20,14 +20,14 @@ const LicenseStatus = ({ isValidating, isValid, invalidMessage }: LicenseStatusP if (isValid) { return ( - + {t('This_license_is_valid_and_ready_to_apply')} ); } return ( - + {invalidMessage} ); diff --git a/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.spec.tsx b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.spec.tsx index d988db1a8bba0..7179d82f230a4 100644 --- a/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.spec.tsx +++ b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.spec.tsx @@ -1,9 +1,12 @@ import type { BehaviorWithContext } from '@rocket.chat/core-typings'; import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { composeStories } from '@storybook/react'; import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { axe } from 'jest-axe'; import ManageLicenseModal from './ManageLicenseModal'; +import * as stories from './ManageLicenseModal.stories'; import createDeferredMockFn from '../../../../../../../../tests/mocks/utils/createDeferredMockFn'; // Long enough to pass isPlausibleLicense (>= 100 chars) so validation actually runs. @@ -20,50 +23,69 @@ const validationFailure = (fails: BehaviorWithContext[]) => () => Promise.reject // getByLabelText matches by label association regardless of visibility, so it reaches the display:none input. const fileInput = () => screen.getByLabelText('Upload_license_file'); -it('should render the title, description and a disabled apply button', () => { - render(, { wrapper: mockAppRoot().build() }); +const enterLicense = async (text: string) => { + await userEvent.click(screen.getByRole('textbox')); + await userEvent.paste(text); +}; - expect(screen.getByRole('heading', { name: 'Manage_license' })).toBeInTheDocument(); - expect(screen.getByText('Manage_license_description')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Apply_license' })).toBeDisabled(); +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); + +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const { baseElement } = render(, { wrapper: mockAppRoot().build() }); + expect(baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(, { wrapper: mockAppRoot().build() }); + + const results = await axe(container); + expect(results).toHaveNoViolations(); }); it('should show a success status for a valid license', async () => { - render(, { + render(, { wrapper: mockAppRoot().withEndpoint('POST', '/v1/licenses.validate', validationSuccess).build(), }); + await enterLicense(LICENSE); + expect(await screen.findByText('Valid_license')).toBeInTheDocument(); }); it('should map a validation failure to a specific message', async () => { - render(, { + render(, { wrapper: mockAppRoot() .withEndpoint('POST', '/v1/licenses.validate', validationFailure(reasons(['invalidate_license', 'period']))) .build(), }); + await enterLicense(LICENSE); + expect(await screen.findByText('License_error_expired')).toBeInTheDocument(); expect(screen.getByText('Invalid_license')).toBeInTheDocument(); }); it('should show a generic message when validation fails for a non-license reason', async () => { - render(, { + render(, { wrapper: mockAppRoot() .withEndpoint('POST', '/v1/licenses.validate', () => Promise.reject(new Error('network down'))) .build(), }); + await enterLicense(LICENSE); + expect(await screen.findByText('License_error_generic')).toBeInTheDocument(); }); it('should show a validating status while the request is in flight', async () => { const { fn, resolve } = createDeferredMockFn(); - render(, { + render(, { wrapper: mockAppRoot().withEndpoint('POST', '/v1/licenses.validate', fn).build(), }); + await enterLicense(LICENSE); + expect(await screen.findByText('Validating_license...')).toBeInTheDocument(); act(() => resolve(null)); @@ -111,6 +133,23 @@ it('should apply a valid license and close the modal', async () => { await waitFor(() => expect(onCancel).toHaveBeenCalled()); }); +it('should keep the apply button disabled after erasing a valid license', async () => { + render(, { + wrapper: mockAppRoot().withEndpoint('POST', '/v1/licenses.validate', validationSuccess).build(), + }); + + await enterLicense(LICENSE); + + expect(await screen.findByText('Valid_license')).toBeInTheDocument(); + + const applyButton = screen.getByRole('button', { name: 'Apply_license' }); + await waitFor(() => expect(applyButton).toBeEnabled()); + + await userEvent.clear(screen.getByRole('textbox')); + + await waitFor(() => expect(applyButton).toBeDisabled()); +}); + it('should ask for confirmation before removing the current license', async () => { render(, { wrapper: mockAppRoot().withEndpoint('POST', '/v1/licenses.validate', validationSuccess).build(), diff --git a/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.stories.tsx b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.stories.tsx new file mode 100644 index 0000000000000..898faf2680e09 --- /dev/null +++ b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.stories.tsx @@ -0,0 +1,75 @@ +import type { BehaviorWithContext } from '@rocket.chat/core-typings'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { Meta, StoryObj } from '@storybook/react'; +import { action } from 'storybook/actions'; +import { screen, userEvent, expect } from 'storybook/test'; + +import ManageLicenseModal from './ManageLicenseModal'; + +const SAMPLE_LICENSE = 'RCLICENSE-'.padEnd(120, 'x'); + +const validationSuccess = () => null; +const validationFailure = (reasons: BehaviorWithContext[]) => () => Promise.reject({ reasons }); + +const enterLicense = async (text: string) => { + await userEvent.click(screen.getByRole('textbox')); + await userEvent.paste(text); +}; + +export default { + component: ManageLicenseModal, + parameters: { + layout: 'fullscreen', + }, + args: { + onCancel: action('onCancel'), + }, + decorators: [ + (Story) => { + const AppRoot = mockAppRoot().withEndpoint('POST', '/v1/licenses.validate', validationSuccess).build(); + + return ( + + + + ); + }, + ], +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + enterpriseLicense: '', + }, +}; + +export const WithCurrentLicense: Story = { + args: { + enterpriseLicense: SAMPLE_LICENSE, + }, +}; + +export const InvalidLicense: Story = { + args: { + enterpriseLicense: '', + }, + decorators: [ + (Story) => { + const AppRoot = mockAppRoot() + .withEndpoint('POST', '/v1/licenses.validate', validationFailure([{ behavior: 'invalidate_license', reason: 'period' }])) + .build(); + + return ( + + + + ); + }, + ], + play: async () => { + await enterLicense(SAMPLE_LICENSE); + await expect(await screen.findByText('Invalid_license')).toBeInTheDocument(); + }, +}; diff --git a/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.tsx b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.tsx index 09ec63acfe183..cca560e2ef866 100644 --- a/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.tsx +++ b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/ManageLicenseModal.tsx @@ -43,15 +43,16 @@ const ManageLicenseModal = ({ enterpriseLicense, onCancel }: ManageLicenseModalP const trimmedLicense = license.trim(); const debouncedLicense = useDebouncedValue(trimmedLicense, 500); - const { data: validation, isPending, isError } = useValidateLicense(debouncedLicense); const isEmpty = trimmedLicense === ''; const isPlausible = isPlausibleLicense(trimmedLicense); - const isValidating = isPlausible && (trimmedLicense !== debouncedLicense || isPending); - const isCurrentLicense = !isEmpty && trimmedLicense === enterpriseLicense.trim(); - const isLicenseValid = !fileError && !isError && validation?.valid === true; + const { data: validation, isPending, isError } = useValidateLicense(debouncedLicense, !isCurrentLicense); + + const isValidating = isPlausible && !isCurrentLicense && (trimmedLicense !== debouncedLicense || isPending); + + const isLicenseValid = isPlausible && !fileError && !isError && validation?.valid === true; const invalidMessage = (() => { if (fileError) { @@ -65,7 +66,7 @@ const ManageLicenseModal = ({ enterpriseLicense, onCancel }: ManageLicenseModalP return t(getLicenseInvalidMessage(validation?.reasons ?? [])); })(); - const showStatus = isPlausible || Boolean(fileError); + const showStatus = !isCurrentLicense && (isPlausible || Boolean(fileError)); const handleApply = async () => { try { diff --git a/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/__snapshots__/ManageLicenseModal.spec.tsx.snap b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/__snapshots__/ManageLicenseModal.spec.tsx.snap new file mode 100644 index 0000000000000..eb7e6761fd2ed --- /dev/null +++ b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/ManageLicenseModal/__snapshots__/ManageLicenseModal.spec.tsx.snap @@ -0,0 +1,451 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`renders Default without crashing 1`] = ` + +
+ +
+
+
+
+

+ Manage_license +

+
+ +
+
+
+
+
+ Manage_license_description +
+ +
+ + + + +
+
+ + +
+
+
+ +
+
+
+ +`; diff --git a/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/PlanCardLicenseDetails.tsx b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/PlanCardLicenseDetails.tsx index c804ec96899cc..b5928fd0fcf2e 100644 --- a/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/PlanCardLicenseDetails.tsx +++ b/apps/meteor/client/views/admin/subscription/components/cards/PlanCard/PlanCardLicenseDetails.tsx @@ -1,28 +1,34 @@ import { IconButton, Divider, Box } from '@rocket.chat/fuselage'; -import { useClipboard } from '@rocket.chat/fuselage-hooks'; import { ActionLink } from '@rocket.chat/layout'; -import { useSetModal, useSetting } from '@rocket.chat/ui-contexts'; +import { usePermission, useSetModal, useSetting } from '@rocket.chat/ui-contexts'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ManageLicenseModal from './ManageLicenseModal'; +import useClipboardWithToast from '../../../../../../hooks/useClipboardWithToast'; import { useServerInfo } from '../../../../../../hooks/useWorkspaceInfo'; const PlanCardLicenseDetails = () => { const { t } = useTranslation(); const setModal = useSetModal(); + const hasPermission = usePermission('edit-privileged-setting'); const siteURL = useSetting('Site_Url', ''); const enterpriseLicense = useSetting('Enterprise_License', ''); const { data: serverInfo } = useServerInfo(); + const hashedSiteURL = serverInfo?.hashedWorkspaceUrl ?? ''; - const { copy: copySiteURL, hasCopied: hasCopiedSiteURL } = useClipboard(siteURL); - const { copy: copyHashed, hasCopied: hasCopiedHashed } = useClipboard(hashedSiteURL); + const { copy: copySiteURL, hasCopied: hasCopiedSiteURL } = useClipboardWithToast(siteURL); + const { copy: copyHashed, hasCopied: hasCopiedHashed } = useClipboardWithToast(hashedSiteURL); const handleOpenModal = useCallback( () => setModal( setModal(null)} />), [enterpriseLicense, setModal], ); + if (!hasPermission) { + return null; + } + return ( <> @@ -32,7 +38,7 @@ const PlanCardLicenseDetails = () => { {hasCopiedSiteURL ? ( ) : ( - copySiteURL()} /> + !hasCopiedSiteURL && copySiteURL()} /> )} @@ -45,26 +51,14 @@ const PlanCardLicenseDetails = () => { {hasCopiedHashed ? ( ) : ( - copyHashed()} /> + !hasCopiedHashed && copyHashed()} /> )} {hashedSiteURL} - - - {t('License_key')} - {enterpriseLicense && } - - {enterpriseLicense ? ( - - {enterpriseLicense} - - ) : ( - {t('Add_license')} - )} - + {t('Manage_license')} ); }; diff --git a/apps/meteor/client/views/admin/subscription/hooks/useValidateLicense.ts b/apps/meteor/client/views/admin/subscription/hooks/useValidateLicense.ts index c11386ebce0b6..6253345e76007 100644 --- a/apps/meteor/client/views/admin/subscription/hooks/useValidateLicense.ts +++ b/apps/meteor/client/views/admin/subscription/hooks/useValidateLicense.ts @@ -11,7 +11,7 @@ export const isPlausibleLicense = (license: string): boolean => license.trim().l const isValidationFailure = (error: unknown): error is { reasons: BehaviorWithContext[] } => typeof error === 'object' && error !== null && Array.isArray((error as { reasons?: unknown }).reasons); -export const useValidateLicense = (license: string) => { +export const useValidateLicense = (license: string, enabled = true) => { const validateLicense = useEndpoint('POST', '/v1/licenses.validate'); const trimmedLicense = license.trim(); @@ -28,7 +28,7 @@ export const useValidateLicense = (license: string) => { throw error; } }, - enabled: isPlausibleLicense(trimmedLicense), + enabled: enabled && isPlausibleLicense(trimmedLicense), staleTime: Infinity, gcTime: Infinity, retry: false, diff --git a/apps/meteor/tests/e2e/channel-management.spec.ts b/apps/meteor/tests/e2e/channel-management.spec.ts index 40d6c757d3183..091144aab0f7f 100644 --- a/apps/meteor/tests/e2e/channel-management.spec.ts +++ b/apps/meteor/tests/e2e/channel-management.spec.ts @@ -261,6 +261,7 @@ test.describe.serial('channel-management', () => { await poHomeChannel.roomToolbar.openMembersTab(); await poHomeChannel.tabs.members.showAllUsers(); await poHomeChannel.tabs.members.ignoreUser('user1'); + await poHomeChannel.toastMessage.waitForDisplay({ type: 'success', message: 'User has been ignored' }); await poHomeChannel.tabs.members.userInfo.openMoreActions(); await expect(poHomeChannel.tabs.members.userInfo.menu.getMenuItem('Unignore')).toBeVisible(); diff --git a/apps/meteor/tests/e2e/message-actions.spec.ts b/apps/meteor/tests/e2e/message-actions.spec.ts index 8f8ce67b849f9..8384ce85c8eb1 100644 --- a/apps/meteor/tests/e2e/message-actions.spec.ts +++ b/apps/meteor/tests/e2e/message-actions.spec.ts @@ -66,8 +66,10 @@ test.describe.serial('message-actions', () => { await expect(poHomeChannel.content.lastUserThreadMessage).toHaveText('this is a reply message'); }); - // close thread before testing because the behavior changes - await page.getByRole('dialog').getByRole('button', { name: 'Close', exact: true }).click(); + await test.step('close thread before testing closed-thread behavior', async () => { + await poHomeChannel.btnContextualbarClose.click(); + await expect(poHomeChannel.btnContextualbarClose).toBeHidden(); + }); await test.step('unfollow thread', async () => { const unFollowButton = poHomeChannel.content.lastUserMessage.getByRole('button', { name: 'Following' }); diff --git a/apps/meteor/tests/e2e/message-mentions.spec.ts b/apps/meteor/tests/e2e/message-mentions.spec.ts index 17ea83daa44a8..af426e44843bd 100644 --- a/apps/meteor/tests/e2e/message-mentions.spec.ts +++ b/apps/meteor/tests/e2e/message-mentions.spec.ts @@ -105,6 +105,7 @@ test.describe.serial('message-mentions', () => { poHomeChannel = new HomeChannel(page); await page.goto('/home'); + await poHomeChannel.waitForHome(); }); test('expect show "all" and "here" options', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-transfers.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-transfers.spec.ts index 0d6ccc35fa0fa..a9a8d060c4072 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-transfers.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-transfers.spec.ts @@ -25,7 +25,7 @@ test.describe('OC - Chat transfers [Monitor role]', () => { let agents: Awaited>[]; let monitors: Awaited>[]; let units: Awaited>[]; - let sessions: { page: Page; poHomeOmnichannel: HomeOmnichannel }[]; + let sessions: { page: Page; poHomeOmnichannel: HomeOmnichannel }[] = []; let poOmnichannel: HomeOmnichannel; @@ -100,11 +100,10 @@ test.describe('OC - Chat transfers [Monitor role]', () => { // Create sessions test.beforeEach(async ({ browser }) => { - sessions = await Promise.all([ - createAuxContext(browser, Users.user1).then(wrapSession), - createAuxContext(browser, Users.user2).then(wrapSession), - createAuxContext(browser, Users.admin).then(wrapSession), - ]); + sessions = []; + for (const user of [Users.user1, Users.user2, Users.admin]) { + sessions.push(await createAuxContext(browser, user).then(wrapSession)); + } }); test.beforeEach(async ({ page }) => { @@ -116,6 +115,7 @@ test.describe('OC - Chat transfers [Monitor role]', () => { // Close sessions test.afterEach(async () => { await Promise.all(sessions.map(({ page }) => page.close())); + sessions = []; }); test.afterAll(async ({ api }) => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts index 336a0cbc355aa..1a88a9c6f007b 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts @@ -88,14 +88,14 @@ test.describe.serial('OC - Livechat', () => { await poLiveChat.page.reload(); await expect(poLiveChat.unreadMessagesBadge(2)).toBeVisible(); }); - }); - test('OC - Livechat - Send message to agent after reload', async () => { - await test.step('expect unread counter to be empty after user sends a message', async () => { + await test.step('expect unread counter to be empty after user sends a message after reload', async () => { await poLiveChat.openAnyLiveChat(); await poLiveChat.onlineAgentMessage.fill('this_a_test_message_from_user'); await poLiveChat.btnSendMessageToOnlineAgent.click(); - expect(await poLiveChat.unreadMessagesBadge(0).all()).toHaveLength(0); + await expect(poLiveChat.txtChatMessage('this_a_test_message_from_user')).toBeVisible(); + await expect(poLiveChat.unreadMessagesBadge(2)).toHaveCount(0); + await expect(poLiveChat.unreadMessagesBadge(1)).toHaveCount(0); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-rooms-forward.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-rooms-forward.spec.ts index 041b06b3469fc..e4e25dc22f559 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-rooms-forward.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-rooms-forward.spec.ts @@ -69,6 +69,7 @@ test.describe('OC - Forwarding to away departments (EE)', () => { // Away Agent window opens with idleTimeLimit of 1, therefore after a second it will turn away ({ page: omnichannelPage } = await createAuxContext(browser, Users.user2, '/', false)); poHomeOmnichannelAwayAgent = new HomeOmnichannel(omnichannelPage); + await poHomeOmnichannelAwayAgent.waitForHome(); await expect(poHomeOmnichannelAwayAgent.navbar.getUserStatusBadge('away')).toBeVisible(); }); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 16a25861a4260..4d8bb8bf8c7b7 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -489,7 +489,6 @@ "Add_email": "Add email", "Add_emoji": "Add emoji", "Add_files_from": "Add files from", - "Add_license": "Add license", "Add_link": "Add link", "Add_manager": "Add manager", "Add_members": "Add Members", @@ -3416,6 +3415,7 @@ "Managers": "Managers", "Managing_assets": "Managing assets", "Managing_integrations": "Managing integrations", + "Manual_license_management_deprecated": "Manual license management (deprecated)", "Manual_Selection": "Manual Selection", "Manually_created_users_briefing": "Manually created users will initially be shown as pending. Once they log in for the first time, they will be shown as active.", "Manufacturing": "Manufacturing",