-
Notifications
You must be signed in to change notification settings - Fork 163
[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
Open
holaontiveros
wants to merge
25
commits into
openedx:master
Choose a base branch
from
WGU-Open-edX:feat/librariesv2-backup-page
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
fb55f69
feat: add backup view for libraries v2
holaontiveros 16c65b4
chore: updated paths and cleanup
holaontiveros f051ec9
chore: cleanup text
holaontiveros 70aefaa
chore: added test
holaontiveros 8347538
chore: fix contracts after rebase
holaontiveros f69e875
chore: more tests to improve coverage
holaontiveros 2c3f5c7
chore: more test for coverage
holaontiveros 33a7d75
chore: more test for coverage
holaontiveros bfd3a5d
chore: fixed lint issues
holaontiveros 541f2b1
chore: update naming for a more semantic one
holaontiveros ec963f0
chore: changed fireEvent to userEvent
holaontiveros 1f51ebf
chore: improved queryKeys
holaontiveros 4b4b93a
chore: lint cleanup
holaontiveros 26ea500
chore: changed tests and time to 1min
holaontiveros 7c4b6f4
chore: even more tests
holaontiveros 15c9c15
chore: split hook for library menu items
holaontiveros ded03af
chore: fixed typo on refactor
holaontiveros dd2674c
chore: improved test to use available mocks
holaontiveros 269d0f7
chore: change from jest.mocks to spyon
holaontiveros 76ae11b
chore: update test based on commets
holaontiveros 821af33
chore: update test to get URL from a better place
holaontiveros d7c627c
chore: added extra getters for new endpoints
holaontiveros b31ad58
Merge branch 'master' into feat/librariesv2-backup-page
holaontiveros 43016d7
chore: update test to prevent issues with useContentLibrary
holaontiveros e02acc4
chore: added comments for clarity
holaontiveros File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
247 changes: 247 additions & 0 deletions
247
src/library-authoring/backup-restore/LibraryBackupPage.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
import { LibraryProvider } from '@src/library-authoring/common/context/LibraryContext'; | ||
import { mockContentLibrary } from '@src/library-authoring/data/api.mocks'; | ||
import { | ||
act, | ||
render as baseRender, | ||
initializeMocks, | ||
screen, | ||
} from '@src/testUtils'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { LibraryBackupStatus } from './data/constants'; | ||
import { LibraryBackupPage } from './LibraryBackupPage'; | ||
import messages from './messages'; | ||
|
||
const render = (libraryId: string = mockContentLibrary.libraryId) => baseRender(<LibraryBackupPage />, { | ||
extraWrapper: ({ children }) => ( | ||
<LibraryProvider libraryId={libraryId}>{children}</LibraryProvider> | ||
), | ||
}); | ||
|
||
// Mocking i18n to prevent having to generate all dynamic translations for this specific test file | ||
// Other tests can still use the real implementation as needed | ||
jest.mock('@edx/frontend-platform/i18n', () => ({ | ||
...jest.requireActual('@edx/frontend-platform/i18n'), | ||
useIntl: () => ({ | ||
formatMessage: (message) => message.defaultMessage, | ||
}), | ||
})); | ||
|
||
const mockLibraryData: | ||
{ data: typeof mockContentLibrary.libraryData | undefined } = { data: mockContentLibrary.libraryData }; | ||
|
||
// TODO: consider using the usual mockContentLibrary.applyMocks pattern after figuring out | ||
// why it doesn't work here as expected | ||
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 = {}; | ||
mockMutationError = null; | ||
mockLibraryData.data = mockContentLibrary.libraryData; | ||
}); | ||
|
||
it('returns NotFoundAlert if no libraryData', () => { | ||
mockLibraryData.data = undefined as any; | ||
render(mockContentLibrary.libraryIdThatNeverLoads); | ||
|
||
expect(screen.getByText(/Not Found/i)).toBeVisible(); | ||
}); | ||
|
||
it('renders the backup page title and initial download button', () => { | ||
render(); | ||
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(); | ||
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(); | ||
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(); | ||
const button = screen.getByRole('button'); | ||
expect(button).toHaveTextContent(messages.downloadReadyButton.defaultMessage); | ||
userEvent.click(button); | ||
expect(downloadSpy).toHaveBeenCalledWith('a'); | ||
downloadSpy.mockRestore(); | ||
}); | ||
|
||
it('shows failed state and error alert', () => { | ||
mockStatusData = { state: LibraryBackupStatus.Failed }; | ||
render(); | ||
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(); | ||
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(); | ||
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', async () => { | ||
jest.useFakeTimers(); | ||
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); | ||
mockMutate.mockImplementation((_arg: any, { onSuccess }: any) => { | ||
onSuccess({ task_id: 'task-123' }); | ||
mockStatusData = { state: LibraryBackupStatus.Pending }; | ||
}); | ||
render(); | ||
const button = screen.getByRole('button'); | ||
expect(button).toBeEnabled(); | ||
await user.click(button); | ||
|
||
// Now in progress | ||
expect(button).toBeDisabled(); | ||
act(() => { | ||
jest.advanceTimersByTime(1 * 60 * 1000); // advance 1 minutes | ||
}); | ||
// After timeout callback, should be enabled again | ||
expect(button).toBeEnabled(); | ||
jest.useRealTimers(); | ||
}); | ||
|
||
it('shows pending message when mutation is in progress but no backup state yet', async () => { | ||
// Mock mutation to trigger onSuccess but don't immediately set backup state | ||
mockMutate.mockImplementation((_arg: any, { onSuccess }: any) => { | ||
onSuccess({ task_id: 'task-123' }); | ||
// Don't set mockStatusData.state immediately to simulate the state | ||
// before the status API has returned any backup state | ||
}); | ||
|
||
render(); | ||
const button = screen.getByRole('button'); | ||
|
||
await userEvent.click(button); | ||
|
||
// This should trigger the specific line: return intl.formatMessage(messages.backupPending); | ||
// when isMutationInProgress is true but !backupState | ||
expect(screen.getByText(messages.backupPending.defaultMessage)).toBeVisible(); | ||
expect(button).toBeDisabled(); | ||
}); | ||
|
||
it('downloads backup immediately when clicking button with already succeeded backup', async () => { | ||
// Set up a scenario where backup is already succeeded with a URL | ||
mockStatusData = { | ||
state: LibraryBackupStatus.Succeeded, | ||
url: '/api/libraries/v2/backup/download/test-backup.tar.gz', | ||
}; | ||
|
||
render(); | ||
|
||
// Spy on handleDownload function call | ||
const createElementSpy = jest.spyOn(document, 'createElement'); | ||
const mockAnchor = { | ||
href: '', | ||
download: '', | ||
click: jest.fn(), | ||
}; | ||
createElementSpy.mockReturnValue(mockAnchor as any); | ||
const appendChildSpy = jest.spyOn(document.body, 'appendChild').mockImplementation(); | ||
const removeChildSpy = jest.spyOn(document.body, 'removeChild').mockImplementation(); | ||
|
||
const button = screen.getByRole('button'); | ||
|
||
// Click the button - this should trigger the early return in handleDownloadBackup | ||
await userEvent.click(button); | ||
|
||
// Verify the download was triggered | ||
expect(createElementSpy).toHaveBeenCalledWith('a'); | ||
expect(mockAnchor.href).toContain('/api/libraries/v2/backup/download/test-backup.tar.gz'); | ||
expect(mockAnchor.download).toContain('backup.tar.gz'); | ||
expect(mockAnchor.click).toHaveBeenCalled(); | ||
expect(appendChildSpy).toHaveBeenCalledWith(mockAnchor); | ||
expect(removeChildSpy).toHaveBeenCalledWith(mockAnchor); | ||
|
||
// Verify mutate was NOT called since backup already exists | ||
expect(mockMutate).not.toHaveBeenCalled(); | ||
|
||
// Clean up spies | ||
createElementSpy.mockRestore(); | ||
appendChildSpy.mockRestore(); | ||
removeChildSpy.mockRestore(); | ||
}); | ||
}); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.