-
Notifications
You must be signed in to change notification settings - Fork 164
[feature] add backup view for libraries v2 #2532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 11 commits
fb55f69
16c65b4
f051ec9
70aefaa
8347538
f69e875
2c3f5c7
33a7d75
bfd3a5d
541f2b1
ec963f0
1f51ebf
4b4b93a
26ea500
7c4b6f4
15c9c15
ded03af
dd2674c
269d0f7
76ae11b
821af33
d7c627c
b31ad58
43016d7
e02acc4
9d2d103
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import { | ||
initializeMocks, | ||
render, | ||
screen, | ||
} from '@src/testUtils'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { act } from '@testing-library/react'; | ||
import { LibraryBackupStatus } from './data/constants'; | ||
import { LibraryBackupPage } from './LibraryBackupPage'; | ||
import messages from './messages'; | ||
|
||
// Mock the hooks/context used by the page so we can render it in isolation. | ||
jest.mock('@src/library-authoring/common/context/LibraryContext', () => ({ | ||
useLibraryContext: () => ({ libraryId: 'lib:TestOrg:test-lib' }), | ||
})); | ||
holaontiveros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
jest.mock('@edx/frontend-platform/i18n', () => ({ | ||
...jest.requireActual('@edx/frontend-platform/i18n'), | ||
useIntl: () => ({ | ||
formatMessage: (message) => message.defaultMessage, | ||
}), | ||
})); | ||
holaontiveros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const mockLibraryData: { data: any } = { data: {} }; | ||
|
||
jest.mock('@src/library-authoring/data/apiHooks', () => ({ | ||
useContentLibrary: () => (mockLibraryData), | ||
})); | ||
|
||
|
||
// Mutable mocks varied per test | ||
const mockMutate = jest.fn(); | ||
let mockStatusData: any = {}; | ||
let mockMutationError: any = null; // allows testing mutation error branch | ||
jest.mock('@src/library-authoring/backup-restore/data/hooks', () => ({ | ||
useCreateLibraryBackup: () => ({ | ||
mutate: mockMutate, | ||
error: mockMutationError, | ||
}), | ||
useGetLibraryBackupStatus: () => ({ | ||
data: mockStatusData, | ||
}), | ||
})); | ||
|
||
describe('<LibraryBackupPage />', () => { | ||
beforeEach(() => { | ||
initializeMocks(); | ||
mockMutate.mockReset(); | ||
mockStatusData = {}; | ||
mockLibraryData.data = { | ||
title: 'My Test Library', | ||
slug: 'test-lib', | ||
org: 'TestOrg', | ||
}; | ||
mockMutationError = null; | ||
}); | ||
|
||
it('returns NotFoundAlert if no libraryData', () => { | ||
mockLibraryData.data = undefined; | ||
|
||
render(<LibraryBackupPage />); | ||
|
||
expect(screen.getByText(/Not Found/i)).toBeVisible(); | ||
}); | ||
|
||
it('renders the backup page title and initial download button', () => { | ||
mockStatusData = {}; | ||
render(<LibraryBackupPage />); | ||
expect(screen.getByText(messages.backupPageTitle.defaultMessage)).toBeVisible(); | ||
const button = screen.getByRole('button', { name: messages.downloadAriaLabel.defaultMessage }); | ||
expect(button).toBeEnabled(); | ||
}); | ||
|
||
it('shows pending state disables button after starting backup', async () => { | ||
mockMutate.mockImplementation((_arg: any, { onSuccess }: any) => { | ||
onSuccess({ task_id: 'task-123' }); | ||
mockStatusData = { state: LibraryBackupStatus.Pending }; | ||
}); | ||
render(<LibraryBackupPage />); | ||
const initialButton = screen.getByRole('button', { name: messages.downloadAriaLabel.defaultMessage }); | ||
expect(initialButton).toBeEnabled(); | ||
await userEvent.click(initialButton); | ||
holaontiveros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const pendingText = await screen.findByText(messages.backupPending.defaultMessage); | ||
const pendingButton = pendingText.closest('button'); | ||
expect(pendingButton).toBeDisabled(); | ||
}); | ||
|
||
it('shows exporting state disables button and changes text', async () => { | ||
mockMutate.mockImplementation((_arg: any, { onSuccess }: any) => { | ||
onSuccess({ task_id: 'task-123' }); | ||
mockStatusData = { state: LibraryBackupStatus.Exporting }; | ||
}); | ||
render(<LibraryBackupPage />); | ||
const initialButton = screen.getByRole('button', { name: messages.downloadAriaLabel.defaultMessage }); | ||
await userEvent.click(initialButton); | ||
const exportingText = await screen.findByText(messages.backupExporting.defaultMessage); | ||
const exportingButton = exportingText.closest('button'); | ||
expect(exportingButton).toBeDisabled(); | ||
}); | ||
|
||
it('shows succeeded state uses ready text and triggers download', () => { | ||
mockStatusData = { state: 'Succeeded', url: '/fake/path.tar.gz' }; | ||
const downloadSpy = jest.spyOn(document, 'createElement'); | ||
render(<LibraryBackupPage />); | ||
const button = screen.getByRole('button'); | ||
expect(button).toHaveTextContent(/Download Library Backup/); | ||
userEvent.click(button); | ||
expect(downloadSpy).toHaveBeenCalledWith('a'); | ||
downloadSpy.mockRestore(); | ||
}); | ||
|
||
it('shows failed state and error alert', () => { | ||
mockStatusData = { state: LibraryBackupStatus.Failed }; | ||
render(<LibraryBackupPage />); | ||
expect(screen.getByText(messages.backupFailedError.defaultMessage)).toBeVisible(); | ||
const button = screen.getByRole('button'); | ||
expect(button).toBeEnabled(); | ||
}); | ||
|
||
it('covers timeout cleanup on unmount', () => { | ||
mockMutate.mockImplementation((_arg: any, { onSuccess }: any) => { | ||
onSuccess({ task_id: 'task-123' }); | ||
mockStatusData = { state: LibraryBackupStatus.Pending }; | ||
}); | ||
const { unmount } = render(<LibraryBackupPage />); | ||
const button = screen.getByRole('button'); | ||
userEvent.click(button); | ||
unmount(); | ||
// No assertion needed, just coverage for cleanup | ||
}); | ||
|
||
it('covers fallback download logic', () => { | ||
mockStatusData = { state: LibraryBackupStatus.Succeeded, url: '/fake/path.tar.gz' }; | ||
// Spy on createElement to force click failure for anchor | ||
const originalCreate = document.createElement.bind(document); | ||
const createSpy = jest.spyOn(document, 'createElement').mockImplementation((tagName: string) => { | ||
const el = originalCreate(tagName); | ||
if (tagName === 'a') { | ||
// Force failure when click is invoked | ||
(el as any).click = () => { throw new Error('fail'); }; | ||
} | ||
return el; | ||
}); | ||
// Stub window.location.href writable | ||
const originalLocation = window.location; | ||
// Use a minimal fake location object | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
delete window.location; | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
window.location = { href: '' }; | ||
Comment on lines
+147
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I usually prefer mocking react-router instead of mocking window.location; it should not required so many lint overrides if you do it that way. But this is fine too. |
||
render(<LibraryBackupPage />); | ||
const button = screen.getByRole('button'); | ||
userEvent.click(button); | ||
expect(window.location.href).toContain('/fake/path.tar.gz'); | ||
// restore | ||
createSpy.mockRestore(); | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
window.location = originalLocation; | ||
}); | ||
|
||
it('executes timeout callback clearing task and re-enabling button after 5 minutes', () => { | ||
jest.useFakeTimers(); | ||
mockMutate.mockImplementation((_arg: any, { onSuccess }: any) => { | ||
onSuccess({ task_id: 'task-123' }); | ||
mockStatusData = { state: LibraryBackupStatus.Pending }; | ||
}); | ||
render(<LibraryBackupPage />); | ||
const button = screen.getByRole('button'); | ||
expect(button).toBeEnabled(); | ||
userEvent.click(button); | ||
// Now in progress | ||
expect(button).toBeDisabled(); | ||
act(() => { | ||
jest.advanceTimersByTime(5 * 60 * 1000); // advance 5 minutes | ||
}); | ||
// After timeout callback, should be enabled again | ||
expect(button).toBeEnabled(); | ||
jest.useRealTimers(); | ||
}); | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.