From 972860ddbbcdf8fa4da64416799e20f623c6296c Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 30 Sep 2025 13:30:44 -0500 Subject: [PATCH 1/2] feat: Connect bulk migration backend with frontend --- .../LegacyLibMigrationPage.test.tsx | 61 +++++++++++++++- .../LegacyLibMigrationPage.tsx | 35 +++++++++- .../data/api.mocks.ts | 61 ++++++++++++++++ .../data/api.test.ts | 34 +++++++++ src/legacy-libraries-migration/data/api.ts | 70 +++++++++++++++++++ .../data/apiHooks.ts | 31 ++++++++ src/legacy-libraries-migration/messages.ts | 15 ++++ .../LibraryAuthoringPage.test.tsx | 35 +++++++++- .../LibraryAuthoringPage.tsx | 58 ++++++++++++--- 9 files changed, 383 insertions(+), 17 deletions(-) create mode 100644 src/legacy-libraries-migration/data/api.mocks.ts create mode 100644 src/legacy-libraries-migration/data/api.test.ts create mode 100644 src/legacy-libraries-migration/data/api.ts create mode 100644 src/legacy-libraries-migration/data/apiHooks.ts diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx index 5a9a4a149e..2c4be3c6bf 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -14,9 +14,11 @@ import { getContentLibraryV2CreateApiUrl } from '@src/library-authoring/create-l import { getStudioHomeApiUrl } from '@src/studio-home/data/api'; import { LegacyLibMigrationPage } from './LegacyLibMigrationPage'; +import { bulkMigrateLegacyLibrariesUrl } from './data/api'; const path = '/libraries-v1/migrate/*'; let axiosMock: MockAdapter; +let mockShowToast; mockGetStudioHomeLibraries.applyMock(); mockGetContentLibraryV2List.applyMock(); @@ -41,7 +43,9 @@ const renderPage = () => ( describe('', () => { beforeEach(() => { - axiosMock = initializeMocks().axiosMock; + const mocks = initializeMocks(); + axiosMock = mocks.axiosMock; + mockShowToast = mocks.mockShowToast; }); it('should render legacy library migration page', async () => { @@ -292,6 +296,7 @@ describe('', () => { it('should confirm migration', async () => { const user = userEvent.setup(); + axiosMock.onPost(bulkMigrateLegacyLibrariesUrl()).reply(200); renderPage(); expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); expect(await screen.findByText('MBA')).toBeInTheDocument(); @@ -334,6 +339,58 @@ describe('', () => { const confirmButton = screen.getByRole('button', { name: /confirm/i }); confirmButton.click(); - // TODO: expect call migrate API + await waitFor(() => { + expect(axiosMock.history.post.length).toBe(1); + }); + expect(axiosMock.history.post[0].data).toBe( + '{"sources":["library-v1:MBA+123","library-v1:UNIX+LG1","library-v1:MBA+1234"],"target":"lib:SampleTaxonomyOrg1:TL1","create_collections":true,"repeat_handling_strategy":"fork"}', + ); + expect(mockShowToast).toHaveBeenCalledWith('3 legacy libraries are being migrated.'); + }); + + it('should show error when confirm migration', async () => { + axiosMock.onPost(bulkMigrateLegacyLibrariesUrl()).reply(400); + renderPage(); + expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + expect(await screen.findByText('MBA')).toBeInTheDocument(); + + const legacyLibrary1 = screen.getByRole('checkbox', { name: 'MBA' }); + const legacyLibrary2 = screen.getByRole('checkbox', { name: /legacy library 1 imported library/i }); + const legacyLibrary3 = screen.getByRole('checkbox', { name: 'MBA 1' }); + + legacyLibrary1.click(); + legacyLibrary2.click(); + legacyLibrary3.click(); + + const nextButton = screen.getByRole('button', { name: /next/i }); + nextButton.click(); + + // Should show alert of SelectDestinationView + expect(await screen.findByText(/any legacy libraries that are used/i)).toBeInTheDocument(); + expect(await screen.findByText('Test Library 1')).toBeInTheDocument(); + const radioButton = screen.getByRole('radio', { name: /test library 1/i }); + radioButton.click(); + + nextButton.click(); + + // Should show alert of ConfirmationView + expect(await screen.findByText(/these 3 legacy libraries will be migrated to/i)).toBeInTheDocument(); + expect(screen.getByText('MBA')).toBeInTheDocument(); + expect(screen.getByText('Legacy library 1')).toBeInTheDocument(); + expect(screen.getByText('MBA 1')).toBeInTheDocument(); + expect(screen.getByText( + /Previously migrated library. Any problem bank links were already moved will be migrated to/i, + )).toBeInTheDocument(); + + const confirmButton = screen.getByRole('button', { name: /confirm/i }); + confirmButton.click(); + + await waitFor(() => { + expect(axiosMock.history.post.length).toBe(1); + }); + expect(axiosMock.history.post[0].data).toBe( + '{"sources":["library-v1:MBA+123","library-v1:UNIX+LG1","library-v1:MBA+1234"],"target":"lib:SampleTaxonomyOrg1:TL1","create_collections":true,"repeat_handling_strategy":"fork"}', + ); + expect(mockShowToast).toHaveBeenCalledWith('Legacy libraries migration failed.'); }); }); diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx index 0d8a7a7a95..168c8673ba 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx @@ -1,4 +1,9 @@ -import { useCallback, useMemo, useState } from 'react'; +import { + useCallback, + useContext, + useMemo, + useState, +} from 'react'; import { Helmet } from 'react-helmet'; import { useNavigate } from 'react-router-dom'; @@ -16,11 +21,13 @@ import Header from '@src/header'; import SubHeader from '@src/generic/sub-header/SubHeader'; import type { ContentLibrary } from '@src/library-authoring/data/api'; import type { LibraryV1Data } from '@src/studio-home/data/api'; +import { ToastContext } from '@src/generic/toast-context'; import { Filter, LibrariesList } from '@src/studio-home/tabs-section/libraries-tab'; import messages from './messages'; import { SelectDestinationView } from './SelectDestinationView'; import { ConfirmationView } from './ConfirmationView'; +import { useUpdateContainerCollections } from './data/apiHooks'; export type MigrationStep = 'select-libraries' | 'select-destination' | 'confirmation-view'; @@ -66,11 +73,33 @@ const ExitModal = ({ export const LegacyLibMigrationPage = () => { const intl = useIntl(); + const navigate = useNavigate(); + const { showToast } = useContext(ToastContext); const [currentStep, setCurrentStep] = useState('select-libraries'); const [isExitModalOpen, openExitModal, closeExitModal] = useToggle(false); const [legacyLibraries, setLegacyLibraries] = useState([]); const [destinationLibrary, setDestination] = useState(); const [confirmationButtonState, setConfirmationButtonState] = useState('default'); + const migrate = useUpdateContainerCollections(); + + const handleMigrate = useCallback(async () => { + if (destinationLibrary) { + try { + const migrationTask = await migrate.mutateAsync({ + sources: legacyLibraries.map((lib) => lib.libraryKey), + target: destinationLibrary.id, + createCollections: true, + repeatHandlingStrategy: 'fork', + }); + showToast(intl.formatMessage(messages.migrationInProgress, { + count: legacyLibraries.length, + })); + navigate(`/library/${destinationLibrary.id}?migration_task=${migrationTask.uuid}`); + } catch (error) { + showToast(intl.formatMessage(messages.migrationFailed)); + } + } + }, [migrate, legacyLibraries, destinationLibrary]); const handleNext = useCallback(() => { switch (currentStep) { @@ -82,13 +111,13 @@ export const LegacyLibMigrationPage = () => { break; case 'confirmation-view': setConfirmationButtonState('pending'); - // TODO Call migration API + handleMigrate(); break; default: /* istanbul ignore next */ break; } - }, [currentStep, setCurrentStep]); + }, [currentStep, setCurrentStep, handleMigrate]); const handleBack = useCallback(() => { switch (currentStep) { diff --git a/src/legacy-libraries-migration/data/api.mocks.ts b/src/legacy-libraries-migration/data/api.mocks.ts new file mode 100644 index 0000000000..a4d2d583ff --- /dev/null +++ b/src/legacy-libraries-migration/data/api.mocks.ts @@ -0,0 +1,61 @@ +import * as api from './api'; + +export async function mockGetMigrationStatus(migrationId: string): Promise { + switch (migrationId) { + case mockGetMigrationStatus.migrationId: + return mockGetMigrationStatus.migrationStatusData; + case mockGetMigrationStatus.migrationIdFailed: + return mockGetMigrationStatus.migrationStatusFailedData; + default: + /* istanbul ignore next */ + throw new Error(`mockGetMigrationStatus: unknown migration ID "${migrationId}"`); + } +} + +mockGetMigrationStatus.migrationId = '1'; +mockGetMigrationStatus.migrationStatusData = { + uuid: mockGetMigrationStatus.migrationId, + state: 'Succeeded', + stateText: 'Succeeded', + completedSteps: 9, + totalSteps: 9, + attempts: 1, + created: '', + modified: '', + artifacts: [], + parameters: [ + { + source: 'legacy-lib-1', + target: 'lib', + compositionLevel: 'component', + repeatHandlingStrategy: 'update', + preserveUrlSlugs: false, + targetCollectionSlug: 'coll-1', + forwardSourceToTarget: true, + }, + ], +} as api.MigrateTaskStatusData; +mockGetMigrationStatus.migrationIdFailed = '2'; +mockGetMigrationStatus.migrationStatusFailedData = { + uuid: mockGetMigrationStatus.migrationId, + state: 'Failed', + stateText: 'Failed', + completedSteps: 9, + totalSteps: 9, + attempts: 1, + created: '', + modified: '', + artifacts: [], + parameters: [ + { + source: 'legacy-lib-1', + target: 'lib', + compositionLevel: 'component', + repeatHandlingStrategy: 'update', + preserveUrlSlugs: false, + targetCollectionSlug: 'coll-1', + forwardSourceToTarget: true, + }, + ], +} as api.MigrateTaskStatusData; +mockGetMigrationStatus.applyMock = () => jest.spyOn(api, 'getMigrationStatus').mockImplementation(mockGetMigrationStatus); diff --git a/src/legacy-libraries-migration/data/api.test.ts b/src/legacy-libraries-migration/data/api.test.ts new file mode 100644 index 0000000000..21afef6d4a --- /dev/null +++ b/src/legacy-libraries-migration/data/api.test.ts @@ -0,0 +1,34 @@ +import { initializeMocks } from '../../testUtils'; +import * as api from './api'; + +let axiosMock; + +describe('legacy libraries migration API', () => { + beforeEach(() => { + ({ axiosMock } = initializeMocks()); + }); + + describe('getMigrationStatus', () => { + it('should get migration status', async () => { + const migrationId = '1'; + const url = api.getMigrationStatusUrl(migrationId); + axiosMock.onGet(url).reply(200); + await api.getMigrationStatus(migrationId); + + expect(axiosMock.history.get[0].url).toEqual(url); + }); + }); + + describe('bulkMigrateLegacyLibraries', () => { + it('should call bulk migrate legacy libraries', async () => { + const url = api.bulkMigrateLegacyLibrariesUrl(); + axiosMock.onPost(url).reply(200); + await api.bulkMigrateLegacyLibraries({ + sources: [], + target: '1', + }); + + expect(axiosMock.history.post[0].url).toEqual(url); + }); + }); +}); diff --git a/src/legacy-libraries-migration/data/api.ts b/src/legacy-libraries-migration/data/api.ts new file mode 100644 index 0000000000..fd711ec79c --- /dev/null +++ b/src/legacy-libraries-migration/data/api.ts @@ -0,0 +1,70 @@ +import { camelCaseObject, getConfig, snakeCaseObject } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; + +/** + * Get the URL to check the migration task status + */ +export const getMigrationStatusUrl = (migrationId: string) => `${getApiBaseUrl()}/api/modulestore_migrator/v1/migrations/${migrationId}/`; + +/** + * Get the URL for bulk migrate legacy libraries + */ +export const bulkMigrateLegacyLibrariesUrl = () => `${getApiBaseUrl()}/api/modulestore_migrator/v1/bulk_migration/`; + +export interface MigrateArtifacts { + source: string; + target: string; + compositionLevel: string; + repeatHandlingStrategy: 'update' | 'skip' | 'fork'; + preserveUrlSlugs: boolean; + targetCollectionSlug: string; + forwardSourceToTarget: boolean; +} + +export interface MigrateTaskStatusData { + state: string; + stateText: string; + completedSteps: number; + totalSteps: number; + attempts: number; + created: string; + modified: string; + artifacts: string[]; + uuid: string; + parameters: MigrateArtifacts[]; +} + +export interface BulkMigrateRequestData { + sources: string[]; + target: string; + targetCollectionSlugList?: string[]; + createCollections?: boolean; + compositionLevel?: string; + repeatHandlingStrategy?: string; + preserveUrlSlugs?: boolean; + forwardSourceToTarget?: boolean; +} + +/** + * Get migration task status + */ +export async function getMigrationStatus( + migrationId: string, +): Promise { + const client = getAuthenticatedHttpClient(); + const { data } = await client.get(getMigrationStatusUrl(migrationId)); + return camelCaseObject(data); +} + +/** + * Bulk migrate legacy libraries + */ +export async function bulkMigrateLegacyLibraries( + requestData: BulkMigrateRequestData, +): Promise { + const client = getAuthenticatedHttpClient(); + const { data } = await client.post(bulkMigrateLegacyLibrariesUrl(), snakeCaseObject(requestData)); + return camelCaseObject(data); +} diff --git a/src/legacy-libraries-migration/data/apiHooks.ts b/src/legacy-libraries-migration/data/apiHooks.ts new file mode 100644 index 0000000000..e8b8ffa6ec --- /dev/null +++ b/src/legacy-libraries-migration/data/apiHooks.ts @@ -0,0 +1,31 @@ +import { skipToken, useMutation, useQuery } from '@tanstack/react-query'; + +import * as api from './api'; + +export const legacyMigrationQueryKeys = { + all: ['contentLibrary'], + /** + * Base key for data specific to a migration task + */ + migrationTask: (migrationId?: string | null) => [...legacyMigrationQueryKeys.all, migrationId], +}; + +/** + * Use this mutation to update container collections + */ +export const useUpdateContainerCollections = () => ( + useMutation({ + mutationFn: async (requestData: api.BulkMigrateRequestData) => api.bulkMigrateLegacyLibraries(requestData), + }) +); + +/** + * Get the migration status + */ +export const useMigrationStatus = (migrationId: string | null) => ( + useQuery({ + queryKey: legacyMigrationQueryKeys.migrationTask(migrationId), + queryFn: migrationId ? () => api.getMigrationStatus(migrationId!) : skipToken, + refetchInterval: 1000, // Refresh every second + }) +); diff --git a/src/legacy-libraries-migration/messages.ts b/src/legacy-libraries-migration/messages.ts index b0d5b4e8b9..2da5b485db 100644 --- a/src/legacy-libraries-migration/messages.ts +++ b/src/legacy-libraries-migration/messages.ts @@ -82,6 +82,21 @@ const messages = defineMessages({ + ' moved will be migrated to {libraryName}', description: 'Alert text when the legacy library is already migrated.', }, + migrationInProgress: { + id: 'legacy-libraries-migration.confirmation-step.toast.migration-in-progress', + defaultMessage: '{count, plural, one {{count} legacy library is} other {{count} legacy libraries are}} being migrated.', + description: 'Toast message that indicates the legacy libraries are being migrated', + }, + migrationFailed: { + id: 'legacy-libraries-migration.confirmation-step.toast.migration-failed', + defaultMessage: 'Legacy libraries migration failed.', + description: 'Toast message that indicates the migration of legacy libraries is failed', + }, + migrationSuccess: { + id: 'legacy-libraries-migration.confirmation-step.toast.migration-success', + defaultMessage: 'The migration of legacy libraries has been completed successfully.', + description: 'Toast message that indicates the migration of legacy libraries is finished', + }, }); export default messages; diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index d84ff75d76..d2cc65d58a 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -10,8 +10,12 @@ import { within, } from '@src/testUtils'; import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; +import { mockGetMigrationStatus } from '@src/legacy-libraries-migration/data/api.mocks'; +import mockEmptyResult from '@src/search-modal/__mocks__/empty-search-result.json'; +import { mockContentSearchConfig } from '@src/search-manager/data/api.mock'; +import { getStudioHomeApiUrl } from '@src/studio-home/data/api'; + import mockResult from './__mocks__/library-search.json'; -import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json'; import { mockContentLibrary, mockGetCollectionMetadata, @@ -19,8 +23,6 @@ import { mockGetLibraryTeam, mockXBlockFields, } from './data/api.mocks'; -import { mockContentSearchConfig } from '../search-manager/data/api.mock'; -import { getStudioHomeApiUrl } from '../studio-home/data/api'; import { LibraryLayout } from '.'; import { getLibraryCollectionsApiUrl, getLibraryContainersApiUrl } from './data/api'; @@ -33,6 +35,7 @@ mockContentSearchConfig.applyMock(); mockContentLibrary.applyMock(); mockGetLibraryTeam.applyMock(); mockXBlockFields.applyMock(); +mockGetMigrationStatus.applyMock(); const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; @@ -1062,4 +1065,30 @@ describe('', () => { 'This page cannot be shown: Libraries v2 are disabled.', ); }); + + it('Should show success in migration legacy libraries', async () => { + render(, { + path, + routerProps: { + initialEntries: [ + `/library/${mockContentLibrary.libraryId}?migration_task=${mockGetMigrationStatus.migrationId}`, + ], + }, + }); + + await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('The migration of legacy libraries has been completed successfully.')); + }); + + it('Should show fail in migration legacy libraries', async () => { + render(, { + path, + routerProps: { + initialEntries: [ + `/library/${mockContentLibrary.libraryId}?migration_task=${mockGetMigrationStatus.migrationIdFailed}`, + ], + }, + }); + + await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('Legacy libraries migration failed.')); + }); }); diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 4769bdb35e..90ee0040eb 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -1,6 +1,7 @@ import { type ReactNode, useCallback, + useContext, useEffect, useState, } from 'react'; @@ -20,13 +21,15 @@ import { Tabs, } from '@openedx/paragon'; import { Add, InfoOutline } from '@openedx/paragon/icons'; -import { Link, useLocation } from 'react-router-dom'; - -import Loading from '../generic/Loading'; -import SubHeader from '../generic/sub-header/SubHeader'; -import Header from '../header'; -import NotFoundAlert from '../generic/NotFoundAlert'; -import { useStudioHome } from '../studio-home/hooks'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; + +import { useMigrationStatus } from '@src/legacy-libraries-migration/data/apiHooks'; +import Loading from '@src/generic/Loading'; +import SubHeader from '@src/generic/sub-header/SubHeader'; +import Header from '@src/header'; +import NotFoundAlert from '@src/generic/NotFoundAlert'; +import { useStudioHome } from '@src/studio-home/hooks'; import { ClearFiltersButton, FilterByBlockType, @@ -35,16 +38,19 @@ import { SearchKeywordsField, SearchSortWidget, TypesFilterData, -} from '../search-manager'; +} from '@src/search-manager'; +import { ToastContext } from '@src/generic/toast-context'; +import migrationMessages from '@src/legacy-libraries-migration/messages'; + import LibraryContent from './LibraryContent'; import { LibrarySidebar } from './library-sidebar'; import { useComponentPickerContext } from './common/context/ComponentPickerContext'; import { useLibraryContext } from './common/context/LibraryContext'; import { SidebarBodyItemId, useSidebarContext } from './common/context/SidebarContext'; import { allLibraryPageTabs, ContentType, useLibraryRoutes } from './routes'; - import messages from './messages'; import LibraryFilterByPublished from './generic/filter-by-published'; +import { libraryQueryPredicate } from './data/apiHooks'; const HeaderActions = () => { const intl = useIntl(); @@ -137,6 +143,16 @@ const LibraryAuthoringPage = ({ }: LibraryAuthoringPageProps) => { const intl = useIntl(); const location = useLocation(); + const navigate = useNavigate(); + const params = new URLSearchParams(location.search); + const { showToast } = useContext(ToastContext); + const queryClient = useQueryClient(); + + // Get migration status every second if applicable + const migrationId = params.get('migration_task'); + const { + data: migrationStatusData, + } = useMigrationStatus(migrationId); const { isLoadingPage: isLoadingStudioHome, @@ -205,6 +221,30 @@ const LibraryAuthoringPage = ({ } }, [navigateTo]); + // Verify the migration task status + if (migrationId) { + let deleteMigrationIdParam = false; + if (migrationStatusData?.state === 'Succeeded') { + showToast(intl.formatMessage(migrationMessages.migrationSuccess)); + queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); + deleteMigrationIdParam = true; + } else if (migrationStatusData?.state === 'Failed') { + showToast(intl.formatMessage(migrationMessages.migrationFailed)); + deleteMigrationIdParam = true; + } else if (migrationStatusData?.state === 'Canceled') { + /* istanbul ignore next */ + deleteMigrationIdParam = true; + } + + if (deleteMigrationIdParam) { + params.delete('migration_task'); + navigate({ + pathname: location.pathname, + search: params.toString(), + }, { replace: true }); + } + } + if (isLoadingLibraryData) { return ; } From 9a9ec02fccf6412ed401415860cabf0059652f85 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 13 Oct 2025 18:54:19 -0500 Subject: [PATCH 2/2] style: fix broken tests --- .../LegacyLibMigrationPage.test.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx index 2c4be3c6bf..db1d24a27b 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -349,11 +349,19 @@ describe('', () => { }); it('should show error when confirm migration', async () => { + const user = userEvent.setup(); axiosMock.onPost(bulkMigrateLegacyLibrariesUrl()).reply(400); renderPage(); expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); expect(await screen.findByText('MBA')).toBeInTheDocument(); + // The filter is 'unmigrated' by default. + // Clear the filter to select all libraries + const filterButton = screen.getByRole('button', { name: /unmigrated/i }); + await user.click(filterButton); + const clearButton = await screen.findByRole('button', { name: /clear filter/i }); + await user.click(clearButton); + const legacyLibrary1 = screen.getByRole('checkbox', { name: 'MBA' }); const legacyLibrary2 = screen.getByRole('checkbox', { name: /legacy library 1 imported library/i }); const legacyLibrary3 = screen.getByRole('checkbox', { name: 'MBA 1' });