From ca33cbc005f12f89b6846280bb910cdba5dfd4bf Mon Sep 17 00:00:00 2001 From: Diana Olarte Date: Wed, 8 Oct 2025 23:01:29 +1100 Subject: [PATCH 1/6] feat: add error boundary --- .env.development | 4 +- package-lock.json | 2 +- package.json | 1 + src/authz-module/index.tsx | 28 +++--- .../libraries-manager/ErrorPage/index.tsx | 93 +++++++++++++++++++ .../libraries-manager/ErrorPage/messages.ts | 56 +++++++++++ .../libraries-manager/context.tsx | 5 +- src/constants.ts | 16 ++++ 8 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 src/authz-module/libraries-manager/ErrorPage/index.tsx create mode 100644 src/authz-module/libraries-manager/ErrorPage/messages.ts diff --git a/.env.development b/.env.development index 9c029de7..b370179f 100644 --- a/.env.development +++ b/.env.development @@ -1,6 +1,7 @@ NODE_ENV='development' PORT=2025 ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' +AUTHORING_BASE_URL= 'http://apps.local.openedx.io:2001' BASE_URL='http://localhost:2025' CREDENTIALS_BASE_URL='http://localhost:18150' CSRF_TOKEN_API_PATH='/csrf/api/v1/token' @@ -15,9 +16,10 @@ LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico MARKETING_SITE_BASE_URL='http://localhost:18000' ORDER_HISTORY_URL='http://localhost:1996/orders' -REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' +REFRESH_ACCESS_TOKEN_ENDPOINT='http://local.openedx.io:8000/login_refresh' SEGMENT_KEY='' SITE_NAME=localhost +STUDIO_BASE_URL='http://studio.local.openedx.io:8001' USER_INFO_COOKIE_NAME='edx-user-info' APP_ID='admin-console' MFE_CONFIG_API_URL='' diff --git a/package-lock.json b/package-lock.json index 32b73312..d364fb68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@tanstack/react-query": "5.89.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-error-boundary": "^4.1.2", "react-router-dom": "^6.0.0" }, "devDependencies": { @@ -23798,7 +23799,6 @@ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.5" }, diff --git a/package.json b/package.json index e0e23aac..50ac43b4 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@tanstack/react-query": "5.89.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-error-boundary": "^4.1.2", "react-router-dom": "^6.0.0" }, "devDependencies": { diff --git a/src/authz-module/index.tsx b/src/authz-module/index.tsx index 3f7d0172..0bb58685 100644 --- a/src/authz-module/index.tsx +++ b/src/authz-module/index.tsx @@ -1,23 +1,29 @@ import { Suspense } from 'react'; import { Routes, Route } from 'react-router-dom'; -import { ErrorBoundary } from '@edx/frontend-platform/react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { QueryErrorResetBoundary } from '@tanstack/react-query'; import LoadingPage from '@src/components/LoadingPage'; import { LibrariesTeamManager, LibrariesUserManager, LibrariesLayout } from './libraries-manager'; +import LibrariesErrorFallback from '@src/authz-module/libraries-manager/ErrorPage'; import { ROUTES } from './constants'; import './index.scss'; const AuthZModule = () => ( - - }> - - }> - } /> - } /> - - - - + + {({ reset }) => ( + + }> + + }> + } /> + } /> + + + + + )} + ); export default AuthZModule; diff --git a/src/authz-module/libraries-manager/ErrorPage/index.tsx b/src/authz-module/libraries-manager/ErrorPage/index.tsx new file mode 100644 index 00000000..bf811ee6 --- /dev/null +++ b/src/authz-module/libraries-manager/ErrorPage/index.tsx @@ -0,0 +1,93 @@ +import { useState } from 'react'; +import { FallbackProps } from 'react-error-boundary'; +import { getConfig } from '@edx/frontend-platform'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button, Container, Hyperlink, Row } from '@openedx/paragon'; +import { CUSTOM_ERRORS, ERROR_STATUS } from '@src/constants'; + +import messages from './messages'; + +const getErrorConfig = ({ errorMessage, errorStatus }) => { + if (errorMessage == CUSTOM_ERRORS.NO_ACCESS || ERROR_STATUS.NO_ACCESS.includes(errorStatus)) { + return ({ + title: messages['error.page.title.noAccess'], + description: messages['error.page.message.noAccess'], + statusCode: errorStatus || ERROR_STATUS.NO_ACCESS[0], + showBackButton: true, + }); + } + if (errorMessage == CUSTOM_ERRORS.NOT_FOUND || ERROR_STATUS.NOT_FOUND.includes(errorStatus)) { + return ({ + title: messages['error.page.title.notFound'], + description: messages['error.page.message.notFound'], + statusCode: errorStatus || ERROR_STATUS.NOT_FOUND[0], + showBackButton: true, + }); + } + if (errorMessage == CUSTOM_ERRORS.SERVER_ERROR || ERROR_STATUS.SERVER_ERROR.includes(errorStatus)) { + return ({ + title: messages['error.page.title.server'], + description: messages['error.page.message.server'], + statusCode: errorStatus || ERROR_STATUS.SERVER_ERROR[0], + showBackButton: true, + showReloadButton: true, + }); + } + return ({ + title: messages['error.page.title.generic'], + description: messages['error.page.message.generic'], + showBackButton: true, + showReloadButton: true, + }); +} + +const LIBRARIES_URL = `${getConfig().AUTHORING_BASE_URL}/authoring/libraries`; + +const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => { + const intl = useIntl(); + const [reloading, setReloading] = useState(false); + + const errorStatus: number = error?.customAttributes?.httpErrorStatus; + const errorMessage: string = error?.message; + const { title, description, statusCode, showBackButton, showReloadButton } = getErrorConfig({ errorMessage, errorStatus }); + + + const handleReload = () => { + if (reloading) { return; } + setReloading(true); + resetErrorBoundary() + } + return ( + +

{statusCode}

+

{intl.formatMessage(title)}

+

{intl.formatMessage(description)}

+ + {showReloadButton && ( + )} + {showBackButton && ( + + )} + + +
+ ) +}; + +const LibrariesErrorFallback = (props: FallbackProps) => { + return ; +} +export default LibrariesErrorFallback; diff --git a/src/authz-module/libraries-manager/ErrorPage/messages.ts b/src/authz-module/libraries-manager/ErrorPage/messages.ts new file mode 100644 index 00000000..9d86ff5b --- /dev/null +++ b/src/authz-module/libraries-manager/ErrorPage/messages.ts @@ -0,0 +1,56 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + 'error.page.title.noAccess': { + id: 'error.page.tile.noAccess', + defaultMessage: 'Access Denied', + description: 'Title error when user does not have access to view', + }, + 'error.page.message.noAccess': { + id: 'error.page.message.noAccess', + defaultMessage: 'You do not have permission to view this page.', + description: 'Error message when user does not have access to view', + }, + 'error.page.title.notFound': { + id: 'error.page.tile.notFound', + defaultMessage: 'Page Not Found', + description: 'Error the resource is not found', + }, + 'error.page.message.notFound': { + id: 'error.page.message.notFound', + defaultMessage: 'The library you are looking for could not be found.', + description: 'Error message when the resource is not found', + }, + 'error.page.title.server': { + id: 'error.page.tile.server', + defaultMessage: 'Something went wrong', + description: 'Title for server error', + }, + 'error.page.message.server': { + id: 'error.page.message.server.error', + defaultMessage: 'We\'re experiencing an internal server problem. Please try again later', + description: 'Server error message for unexpected errors', + }, + 'error.page.title.generic': { + id: 'error.page.tile.generic', + defaultMessage: 'Something went wrong', + description: 'Title for unexpected error', + }, + 'error.page.message.generic': { + id: 'error.page.message.server', + defaultMessage: 'An unexpected error occurred. Please click the button below to refresh the page.', + description: 'Error message for unexpected errors', + }, + 'error.page.action.reload': { + id: 'error.page.action.reload', + defaultMessage: 'Reload Page', + description: 'Label for reload action', + }, + 'error.page.action.back': { + id: 'error.page.action.back', + defaultMessage: 'Back to Libraries', + description: 'Label for return to libraries action', + }, +}); + +export default messages; diff --git a/src/authz-module/libraries-manager/context.tsx b/src/authz-module/libraries-manager/context.tsx index 419744d1..7546fd4e 100644 --- a/src/authz-module/libraries-manager/context.tsx +++ b/src/authz-module/libraries-manager/context.tsx @@ -7,6 +7,7 @@ import { useValidateUserPermissions } from '@src/data/hooks'; import { usePermissionsByRole } from '@src/authz-module/data/hooks'; import { PermissionMetadata, ResourceMetadata, Role } from 'types'; import { libraryPermissions, libraryResourceTypes, libraryRolesMetadata } from './constants'; +import { CUSTOM_ERRORS } from '@src/constants'; const LIBRARY_TEAM_PERMISSIONS = ['view_library_team', 'manage_library_team']; @@ -38,7 +39,7 @@ export const LibraryAuthZProvider: React.FC = ({ children }: // TODO: Implement a custom error view if (!libraryId) { - throw new Error('MissingLibrary'); + throw new Error(CUSTOM_ERRORS.NOT_FOUND); } const permissions = LIBRARY_TEAM_PERMISSIONS.map(action => ({ action, scope: libraryId })); @@ -46,7 +47,7 @@ export const LibraryAuthZProvider: React.FC = ({ children }: const [{ allowed: canViewTeam }, { allowed: canManageTeam }] = userPermissions; if (!canViewTeam && !canManageTeam) { - throw new Error('NoAccess'); + throw new Error(CUSTOM_ERRORS.NO_ACCESS) } const { data: libraryRoles } = usePermissionsByRole(libraryId); diff --git a/src/constants.ts b/src/constants.ts index 0d9b840d..5b47c35d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1 +1,17 @@ export const appId = 'org.openedx.frontend.app.adminConsole'; + +export enum CUSTOM_ERRORS { + NO_ACCESS = 'NO_ACCESS', + NOT_FOUND = 'NOT_FOUND', + SERVER_ERROR = 'SERVER_ERROR', +}; + +type ERROR_STATUS = { + [key in CUSTOM_ERRORS]: number[]; +}; + +export const ERROR_STATUS: ERROR_STATUS = { + [CUSTOM_ERRORS.NO_ACCESS]: [403, 401], + [CUSTOM_ERRORS.NOT_FOUND]: [404], + [CUSTOM_ERRORS.SERVER_ERROR]: [500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511], +}; \ No newline at end of file From a8c493a18d1b14fe8edae291fb14adf6a946a0e6 Mon Sep 17 00:00:00 2001 From: Diana Olarte Date: Thu, 9 Oct 2025 00:08:58 +1100 Subject: [PATCH 2/6] style: fix linter --- src/authz-module/index.tsx | 2 +- .../libraries-manager/ErrorPage/index.tsx | 152 +++++++++--------- .../libraries-manager/context.test.tsx | 5 +- .../libraries-manager/context.tsx | 6 +- src/constants.ts | 18 +-- 5 files changed, 93 insertions(+), 90 deletions(-) diff --git a/src/authz-module/index.tsx b/src/authz-module/index.tsx index 0bb58685..d6e42188 100644 --- a/src/authz-module/index.tsx +++ b/src/authz-module/index.tsx @@ -3,8 +3,8 @@ import { Routes, Route } from 'react-router-dom'; import { ErrorBoundary } from 'react-error-boundary'; import { QueryErrorResetBoundary } from '@tanstack/react-query'; import LoadingPage from '@src/components/LoadingPage'; -import { LibrariesTeamManager, LibrariesUserManager, LibrariesLayout } from './libraries-manager'; import LibrariesErrorFallback from '@src/authz-module/libraries-manager/ErrorPage'; +import { LibrariesTeamManager, LibrariesUserManager, LibrariesLayout } from './libraries-manager'; import { ROUTES } from './constants'; import './index.scss'; diff --git a/src/authz-module/libraries-manager/ErrorPage/index.tsx b/src/authz-module/libraries-manager/ErrorPage/index.tsx index bf811ee6..87a48e11 100644 --- a/src/authz-module/libraries-manager/ErrorPage/index.tsx +++ b/src/authz-module/libraries-manager/ErrorPage/index.tsx @@ -2,92 +2,94 @@ import { useState } from 'react'; import { FallbackProps } from 'react-error-boundary'; import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { Button, Container, Hyperlink, Row } from '@openedx/paragon'; -import { CUSTOM_ERRORS, ERROR_STATUS } from '@src/constants'; +import { + Button, Container, Hyperlink, Row, +} from '@openedx/paragon'; +import { CustomErrors, ERROR_STATUS } from '@src/constants'; import messages from './messages'; const getErrorConfig = ({ errorMessage, errorStatus }) => { - if (errorMessage == CUSTOM_ERRORS.NO_ACCESS || ERROR_STATUS.NO_ACCESS.includes(errorStatus)) { - return ({ - title: messages['error.page.title.noAccess'], - description: messages['error.page.message.noAccess'], - statusCode: errorStatus || ERROR_STATUS.NO_ACCESS[0], - showBackButton: true, - }); - } - if (errorMessage == CUSTOM_ERRORS.NOT_FOUND || ERROR_STATUS.NOT_FOUND.includes(errorStatus)) { - return ({ - title: messages['error.page.title.notFound'], - description: messages['error.page.message.notFound'], - statusCode: errorStatus || ERROR_STATUS.NOT_FOUND[0], - showBackButton: true, - }); - } - if (errorMessage == CUSTOM_ERRORS.SERVER_ERROR || ERROR_STATUS.SERVER_ERROR.includes(errorStatus)) { - return ({ - title: messages['error.page.title.server'], - description: messages['error.page.message.server'], - statusCode: errorStatus || ERROR_STATUS.SERVER_ERROR[0], - showBackButton: true, - showReloadButton: true, - }); - } - return ({ - title: messages['error.page.title.generic'], - description: messages['error.page.message.generic'], - showBackButton: true, - showReloadButton: true, - }); -} + if (errorMessage === CustomErrors.NO_ACCESS || ERROR_STATUS.NO_ACCESS.includes(errorStatus)) { + return ({ + title: messages['error.page.title.noAccess'], + description: messages['error.page.message.noAccess'], + statusCode: errorStatus || ERROR_STATUS.NO_ACCESS[0], + showBackButton: true, + }); + } + if (errorMessage === CustomErrors.NOT_FOUND || ERROR_STATUS.NOT_FOUND.includes(errorStatus)) { + return ({ + title: messages['error.page.title.notFound'], + description: messages['error.page.message.notFound'], + statusCode: errorStatus || ERROR_STATUS.NOT_FOUND[0], + showBackButton: true, + }); + } + if (errorMessage === CustomErrors.SERVER_ERROR || ERROR_STATUS.SERVER_ERROR.includes(errorStatus)) { + return ({ + title: messages['error.page.title.server'], + description: messages['error.page.message.server'], + statusCode: errorStatus || ERROR_STATUS.SERVER_ERROR[0], + showBackButton: true, + showReloadButton: true, + }); + } + return ({ + title: messages['error.page.title.generic'], + description: messages['error.page.message.generic'], + showBackButton: true, + showReloadButton: true, + }); +}; const LIBRARIES_URL = `${getConfig().AUTHORING_BASE_URL}/authoring/libraries`; const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => { - const intl = useIntl(); - const [reloading, setReloading] = useState(false); - - const errorStatus: number = error?.customAttributes?.httpErrorStatus; - const errorMessage: string = error?.message; - const { title, description, statusCode, showBackButton, showReloadButton } = getErrorConfig({ errorMessage, errorStatus }); + const intl = useIntl(); + const [reloading, setReloading] = useState(false); + const errorStatus: number = error?.customAttributes?.httpErrorStatus; + const errorMessage: string = error?.message; + const { + title, description, statusCode, showBackButton, showReloadButton, + } = getErrorConfig({ errorMessage, errorStatus }); - const handleReload = () => { - if (reloading) { return; } - setReloading(true); - resetErrorBoundary() - } - return ( - -

{statusCode}

-

{intl.formatMessage(title)}

-

{intl.formatMessage(description)}

- - {showReloadButton && ( - )} - {showBackButton && ( - - )} + const handleReload = () => { + if (reloading) { return; } + setReloading(true); + resetErrorBoundary(); + }; + return ( + +

{statusCode}

+

{intl.formatMessage(title)}

+

{intl.formatMessage(description)}

+ + {showReloadButton && ( + + )} + {showBackButton && ( + + )} - -
- ) +
+
+ ); }; -const LibrariesErrorFallback = (props: FallbackProps) => { - return ; -} +const LibrariesErrorFallback = (props: FallbackProps) => ; export default LibrariesErrorFallback; diff --git a/src/authz-module/libraries-manager/context.test.tsx b/src/authz-module/libraries-manager/context.test.tsx index 28f039d9..e4b7a676 100644 --- a/src/authz-module/libraries-manager/context.test.tsx +++ b/src/authz-module/libraries-manager/context.test.tsx @@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom'; import { useValidateUserPermissions } from '@src/data/hooks'; import { renderWrapper } from '@src/setupTest'; import { usePermissionsByRole } from '@src/authz-module/data/hooks'; +import { CustomErrors } from '@src/constants'; import { LibraryAuthZProvider, useLibraryAuthZ } from './context'; jest.mock('react-router-dom', () => ({ @@ -112,7 +113,7 @@ describe('LibraryAuthZProvider', () => { , ); - }).toThrow('NoAccess'); + }).toThrow(CustomErrors.NO_ACCESS); }); it('provides context when user can view but not manage team', () => { @@ -141,7 +142,7 @@ describe('LibraryAuthZProvider', () => { , ); - }).toThrow('MissingLibrary'); + }).toThrow(CustomErrors.NOT_FOUND); }); it('throws error when useLibraryAuthZ is used outside provider', () => { diff --git a/src/authz-module/libraries-manager/context.tsx b/src/authz-module/libraries-manager/context.tsx index 7546fd4e..18ac0c1b 100644 --- a/src/authz-module/libraries-manager/context.tsx +++ b/src/authz-module/libraries-manager/context.tsx @@ -6,8 +6,8 @@ import { AppContext } from '@edx/frontend-platform/react'; import { useValidateUserPermissions } from '@src/data/hooks'; import { usePermissionsByRole } from '@src/authz-module/data/hooks'; import { PermissionMetadata, ResourceMetadata, Role } from 'types'; +import { CustomErrors } from '@src/constants'; import { libraryPermissions, libraryResourceTypes, libraryRolesMetadata } from './constants'; -import { CUSTOM_ERRORS } from '@src/constants'; const LIBRARY_TEAM_PERMISSIONS = ['view_library_team', 'manage_library_team']; @@ -39,7 +39,7 @@ export const LibraryAuthZProvider: React.FC = ({ children }: // TODO: Implement a custom error view if (!libraryId) { - throw new Error(CUSTOM_ERRORS.NOT_FOUND); + throw new Error(CustomErrors.NOT_FOUND); } const permissions = LIBRARY_TEAM_PERMISSIONS.map(action => ({ action, scope: libraryId })); @@ -47,7 +47,7 @@ export const LibraryAuthZProvider: React.FC = ({ children }: const [{ allowed: canViewTeam }, { allowed: canManageTeam }] = userPermissions; if (!canViewTeam && !canManageTeam) { - throw new Error(CUSTOM_ERRORS.NO_ACCESS) + throw new Error(CustomErrors.NO_ACCESS); } const { data: libraryRoles } = usePermissionsByRole(libraryId); diff --git a/src/constants.ts b/src/constants.ts index 5b47c35d..9a3fed44 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,17 +1,17 @@ export const appId = 'org.openedx.frontend.app.adminConsole'; -export enum CUSTOM_ERRORS { +export enum CustomErrors { NO_ACCESS = 'NO_ACCESS', NOT_FOUND = 'NOT_FOUND', SERVER_ERROR = 'SERVER_ERROR', -}; +} -type ERROR_STATUS = { - [key in CUSTOM_ERRORS]: number[]; +type ErrorStatusCode = { + [key in CustomErrors]: number[]; }; -export const ERROR_STATUS: ERROR_STATUS = { - [CUSTOM_ERRORS.NO_ACCESS]: [403, 401], - [CUSTOM_ERRORS.NOT_FOUND]: [404], - [CUSTOM_ERRORS.SERVER_ERROR]: [500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511], -}; \ No newline at end of file +export const ERROR_STATUS: ErrorStatusCode = { + [CustomErrors.NO_ACCESS]: [403, 401], + [CustomErrors.NOT_FOUND]: [404], + [CustomErrors.SERVER_ERROR]: [500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511], +}; From 97004987c26e46bde89db4d01b0ca6f63cf72826 Mon Sep 17 00:00:00 2001 From: Diana Olarte Date: Thu, 9 Oct 2025 00:26:44 +1100 Subject: [PATCH 3/6] feat: add test --- .../ErrorPage/index.test.tsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/authz-module/libraries-manager/ErrorPage/index.test.tsx diff --git a/src/authz-module/libraries-manager/ErrorPage/index.test.tsx b/src/authz-module/libraries-manager/ErrorPage/index.test.tsx new file mode 100644 index 00000000..3fb1660b --- /dev/null +++ b/src/authz-module/libraries-manager/ErrorPage/index.test.tsx @@ -0,0 +1,71 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { renderWrapper } from '@src/setupTest'; +import LibrariesErrorFallback from './index'; + +const ThrowError = ({ error }) => { + throw error; + return null +} + +describe('LibrariesErrorFallback', () => { + it('renders Access Denied for 401', () => { + const error = { message: 'NO_ACCESS', customAtributtes: { httpErrorStatus: 401 } }; + renderWrapper( + + + + ); + expect(screen.getByText(/Access Denied/i)).toBeInTheDocument(); + expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument(); + }); + + it('renders Not Found for 404', () => { + const error = { message: 'NOT_FOUND', customAtributtes: { httpErrorStatus: 404 } }; + renderWrapper( + + + + ); + expect(screen.getByText(/Page Not Found/i)).toBeInTheDocument(); + expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument(); + }); + + it('renders Server Error for 500 and shows reload', async () => { + const error = { message: 'SERVER_ERROR', customAtributtes: { httpErrorStatus: 500 } }; + renderWrapper( + + + + ); + expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument(); + expect(screen.getByText(/Reload Page/i)).toBeInTheDocument(); + expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument(); + }); + + it('renders generic error for other error error', () => { + const error = { message: 'SOMETHING_ELSE', customAtributtes: { httpErrorStatus: 418 } }; + renderWrapper( + + + + ); + expect(screen.getByText(/Error/i)).toBeInTheDocument(); + expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument(); + }); + + it('calls reload action if present', async () => { + // Simulate error with a refetch function + const refetch = jest.fn(); + const error = { message: 'SERVER_ERROR', customAtributtes: { httpErrorStatus: 500 }, refetch }; + renderWrapper( + + + + ); + const reloadBtn = screen.getByText(/Reload Page/i); + fireEvent.click(reloadBtn); + // If your ErrorPage uses error.refetch, this will be called + await waitFor(() => expect(refetch).toHaveBeenCalled()); + }); +}); From 57a204460ea5f37065c4e32a27fb2ca55167d4de Mon Sep 17 00:00:00 2001 From: Diana Olarte Date: Mon, 20 Oct 2025 12:12:43 +1100 Subject: [PATCH 4/6] fix: url for authoring redirection --- .env | 1 + .../ErrorPage/index.test.tsx | 32 +++++++++++-------- .../libraries-manager/ErrorPage/index.tsx | 4 +-- src/index.tsx | 9 +++++- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/.env b/.env index fb27c774..7a08dad0 100644 --- a/.env +++ b/.env @@ -1,5 +1,6 @@ NODE_ENV='production' ACCESS_TOKEN_COOKIE_NAME='' +AUTHORING_BASE_URL= '' BASE_URL='' CREDENTIALS_BASE_URL='' CSRF_TOKEN_API_PATH='' diff --git a/src/authz-module/libraries-manager/ErrorPage/index.test.tsx b/src/authz-module/libraries-manager/ErrorPage/index.test.tsx index 3fb1660b..3b76f0a4 100644 --- a/src/authz-module/libraries-manager/ErrorPage/index.test.tsx +++ b/src/authz-module/libraries-manager/ErrorPage/index.test.tsx @@ -1,42 +1,44 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { + screen, fireEvent, waitFor, +} from '@testing-library/react'; import { ErrorBoundary } from 'react-error-boundary'; import { renderWrapper } from '@src/setupTest'; import LibrariesErrorFallback from './index'; -const ThrowError = ({ error }) => { +const ThrowError = ({ error }: { error:Error }) => { throw error; - return null -} + return null; +}; describe('LibrariesErrorFallback', () => { it('renders Access Denied for 401', () => { - const error = { message: 'NO_ACCESS', customAtributtes: { httpErrorStatus: 401 } }; + const error = { name: '', message: 'NO_ACCESS', customAtributtes: { httpErrorStatus: 401 } }; renderWrapper( - + , ); expect(screen.getByText(/Access Denied/i)).toBeInTheDocument(); expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument(); }); it('renders Not Found for 404', () => { - const error = { message: 'NOT_FOUND', customAtributtes: { httpErrorStatus: 404 } }; + const error = { name: '', message: 'NOT_FOUND', customAtributtes: { httpErrorStatus: 404 } }; renderWrapper( - + , ); expect(screen.getByText(/Page Not Found/i)).toBeInTheDocument(); expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument(); }); it('renders Server Error for 500 and shows reload', async () => { - const error = { message: 'SERVER_ERROR', customAtributtes: { httpErrorStatus: 500 } }; + const error = { name: '', message: 'SERVER_ERROR', customAtributtes: { httpErrorStatus: 500 } }; renderWrapper( - + , ); expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument(); expect(screen.getByText(/Reload Page/i)).toBeInTheDocument(); @@ -44,11 +46,11 @@ describe('LibrariesErrorFallback', () => { }); it('renders generic error for other error error', () => { - const error = { message: 'SOMETHING_ELSE', customAtributtes: { httpErrorStatus: 418 } }; + const error = { name: '', message: 'SOMETHING_ELSE', customAtributtes: { httpErrorStatus: 418 } }; renderWrapper( - + , ); expect(screen.getByText(/Error/i)).toBeInTheDocument(); expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument(); @@ -57,11 +59,13 @@ describe('LibrariesErrorFallback', () => { it('calls reload action if present', async () => { // Simulate error with a refetch function const refetch = jest.fn(); - const error = { message: 'SERVER_ERROR', customAtributtes: { httpErrorStatus: 500 }, refetch }; + const error = { + name: '', message: 'SERVER_ERROR', customAtributtes: { httpErrorStatus: 500 }, refetch, + }; renderWrapper( - + , ); const reloadBtn = screen.getByText(/Reload Page/i); fireEvent.click(reloadBtn); diff --git a/src/authz-module/libraries-manager/ErrorPage/index.tsx b/src/authz-module/libraries-manager/ErrorPage/index.tsx index 87a48e11..cded1d4d 100644 --- a/src/authz-module/libraries-manager/ErrorPage/index.tsx +++ b/src/authz-module/libraries-manager/ErrorPage/index.tsx @@ -43,7 +43,7 @@ const getErrorConfig = ({ errorMessage, errorStatus }) => { }); }; -const LIBRARIES_URL = `${getConfig().AUTHORING_BASE_URL}/authoring/libraries`; +const librariesUrl = () => `${getConfig().AUTHORING_BASE_URL}/libraries`; const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => { const intl = useIntl(); @@ -78,7 +78,7 @@ const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => { {showBackButton && (