From 53e748164bd486be575d23b48e75609d9f588e15 Mon Sep 17 00:00:00 2001 From: Lukas Hirt Date: Tue, 30 Dec 2025 11:22:56 +0100 Subject: [PATCH] feat: [OCISDEV-541] add crash page We've added a crash page to the application. This page is displayed when the application encounters an error that it cannot recover from. The first such error we are catching is when spaces loading fails. --- .drone.star | 1 + .../unreleased/enhancement-add-crash-page.md | 5 + .../enhancement-catch-spaces-loading-error.md | 5 + packages/web-pkg/src/errors/codes.ts | 6 ++ packages/web-pkg/src/errors/index.ts | 1 + .../src/composables/layout/useLayout.ts | 3 +- packages/web-runtime/src/index.ts | 26 +++-- packages/web-runtime/src/pages/crash.vue | 98 +++++++++++++++++++ packages/web-runtime/src/router/index.ts | 9 ++ .../specs/runtime/crashPage.spec.ts | 57 +++++++++++ tests/e2e-playwright/steps/ui/crashPage.ts | 26 +++++ tests/e2e-playwright/steps/ui/index.ts | 1 + tests/e2e/support/objects/crash-page/index.ts | 23 +++++ 13 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 changelog/unreleased/enhancement-add-crash-page.md create mode 100644 changelog/unreleased/enhancement-catch-spaces-loading-error.md create mode 100644 packages/web-pkg/src/errors/codes.ts create mode 100644 packages/web-runtime/src/pages/crash.vue create mode 100644 tests/e2e-playwright/specs/runtime/crashPage.spec.ts create mode 100644 tests/e2e-playwright/steps/ui/crashPage.ts create mode 100644 tests/e2e/support/objects/crash-page/index.ts diff --git a/.drone.star b/.drone.star index 6a2778bdbc0..33aa19835a1 100644 --- a/.drone.star +++ b/.drone.star @@ -196,6 +196,7 @@ config = { "suites": [ "shares", "search", + "runtime", ], }, "4": { diff --git a/changelog/unreleased/enhancement-add-crash-page.md b/changelog/unreleased/enhancement-add-crash-page.md new file mode 100644 index 00000000000..e9fe58cbcfa --- /dev/null +++ b/changelog/unreleased/enhancement-add-crash-page.md @@ -0,0 +1,5 @@ +Enhancement: Add crash page + +We've added a crash page to the application. This page is displayed when the application encounters an error that it cannot recover from. + +https://github.com/owncloud/web/pull/13426 diff --git a/changelog/unreleased/enhancement-catch-spaces-loading-error.md b/changelog/unreleased/enhancement-catch-spaces-loading-error.md new file mode 100644 index 00000000000..e75f8a2b91c --- /dev/null +++ b/changelog/unreleased/enhancement-catch-spaces-loading-error.md @@ -0,0 +1,5 @@ +Enhancement: Catch spaces loading error + +In case there is any error during spaces loading, the application will now display a crash page instead of showing an infinite loading state. + +https://github.com/owncloud/web/pull/13426 diff --git a/packages/web-pkg/src/errors/codes.ts b/packages/web-pkg/src/errors/codes.ts new file mode 100644 index 00000000000..d1984e78cd0 --- /dev/null +++ b/packages/web-pkg/src/errors/codes.ts @@ -0,0 +1,6 @@ +/** + * Error codes for errors from which the application cannot recover. + */ +export const CRASH_CODES = Object.freeze({ + RUNTIME_BOOTSTRAP_SPACES_LOAD: 'RUNTIME-BOOTSTRAP-040' +}) diff --git a/packages/web-pkg/src/errors/index.ts b/packages/web-pkg/src/errors/index.ts index c9f6f047dc0..dd624b5642c 100644 --- a/packages/web-pkg/src/errors/index.ts +++ b/packages/web-pkg/src/errors/index.ts @@ -1 +1,2 @@ export * from './types' +export * from './codes' diff --git a/packages/web-runtime/src/composables/layout/useLayout.ts b/packages/web-runtime/src/composables/layout/useLayout.ts index 124aab2afc3..6da3f6580eb 100644 --- a/packages/web-runtime/src/composables/layout/useLayout.ts +++ b/packages/web-runtime/src/composables/layout/useLayout.ts @@ -20,7 +20,8 @@ export const useLayout = (options?: LayoutOptions) => { 'oidcCallback', 'oidcSilentRedirect', 'resolvePublicLink', - 'accessDenied' + 'accessDenied', + 'crash' ] if ( !unref(router.currentRoute).name || diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts index 6b1d9549b90..5ab4393c6c6 100644 --- a/packages/web-runtime/src/index.ts +++ b/packages/web-runtime/src/index.ts @@ -46,6 +46,8 @@ import Avatar from './components/Avatar.vue' import focusMixin from './mixins/focusMixin' import { extensionPoints } from './extensionPoints' import { isSilentRedirectRoute } from './helpers/silentRedirect' +import { captureException } from '@sentry/vue' +import { CRASH_CODES } from '@ownclouders/web-pkg/src/errors/codes' export const bootstrapApp = async (configurationPath: string, appsReadyCallback: () => void) => { const isSilentRedirect = isSilentRedirectRoute() @@ -232,15 +234,21 @@ export const bootstrapApp = async (configurationPath: string, appsReadyCallback: sharesStore.setGraphRoles(graphRoleDefinitions) // Load spaces to make them available across the application - await spacesStore.loadSpaces({ graphClient: clientService.graphAuthenticated }) - const personalSpace = spacesStore.spaces.find(isPersonalSpaceResource) - - if (personalSpace) { - spacesStore.updateSpaceField({ - id: personalSpace.id, - field: 'name', - value: app.config.globalProperties.$gettext('Personal') - }) + try { + await spacesStore.loadSpaces({ graphClient: clientService.graphAuthenticated }) + const personalSpace = spacesStore.spaces.find(isPersonalSpaceResource) + + if (personalSpace) { + spacesStore.updateSpaceField({ + id: personalSpace.id, + field: 'name', + value: app.config.globalProperties.$gettext('Personal') + }) + } + } catch (error) { + console.error(error) + captureException(error) + router.push({ name: 'crash', query: { code: CRASH_CODES.RUNTIME_BOOTSTRAP_SPACES_LOAD } }) } }, { diff --git a/packages/web-runtime/src/pages/crash.vue b/packages/web-runtime/src/pages/crash.vue new file mode 100644 index 00000000000..8278d445e84 --- /dev/null +++ b/packages/web-runtime/src/pages/crash.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/packages/web-runtime/src/router/index.ts b/packages/web-runtime/src/router/index.ts index 46c66ef63c1..2dcea5afead 100644 --- a/packages/web-runtime/src/router/index.ts +++ b/packages/web-runtime/src/router/index.ts @@ -6,6 +6,7 @@ import NotFoundPage from '../pages/notFound.vue' import OidcCallbackPage from '../pages/oidcCallback.vue' import ResolvePublicLinkPage from '../pages/resolvePublicLink.vue' import ResolvePrivateLinkPage from '../pages/resolvePrivateLink.vue' +import CrashPage from '../pages/crash.vue' import { setupAuthGuard } from './setupAuthGuard' import { patchRouter } from './patchCleanPath' import { @@ -93,6 +94,14 @@ const routes = [ name: 'notFound', component: NotFoundPage, meta: { title: $gettext('Not found'), authContext: 'hybrid' } + }, + { + path: '/crash', + name: 'crash', + component: CrashPage, + meta: { + authContext: 'hybrid' + } } ] export const router = patchRouter( diff --git a/tests/e2e-playwright/specs/runtime/crashPage.spec.ts b/tests/e2e-playwright/specs/runtime/crashPage.spec.ts new file mode 100644 index 00000000000..0993be82a9c --- /dev/null +++ b/tests/e2e-playwright/specs/runtime/crashPage.spec.ts @@ -0,0 +1,57 @@ +import { test } from '@playwright/test' +import { config } from '../../../e2e/config.js' +import { ActorsEnvironment, UsersEnvironment } from '../../../e2e/support/environment' +import { setAccessAndRefreshToken } from '../../helpers/setAccessAndRefreshToken' +import * as api from '../../steps/api/api' +import * as ui from '../../steps/ui/index' +import { CRASH_CODES } from '../../../../packages/web-pkg/src/errors/codes' + +test.describe('crash page', () => { + let actorsEnvironment: ActorsEnvironment + const usersEnvironment = new UsersEnvironment() + + test.beforeEach(async ({ browser }) => { + actorsEnvironment = new ActorsEnvironment({ + context: { + acceptDownloads: config.acceptDownloads, + reportDir: config.reportDir, + tracingReportDir: config.tracingReportDir, + reportHar: config.reportHar, + reportTracing: config.reportTracing, + reportVideo: config.reportVideo, + failOnUncaughtConsoleError: config.failOnUncaughtConsoleError + }, + browser: browser + }) + + await setAccessAndRefreshToken(usersEnvironment) + await api.userHasBeenCreated({ usersEnvironment, stepUser: 'Admin', userToBeCreated: 'Alice' }) + await ui.logInUser({ usersEnvironment, actorsEnvironment, stepUser: 'Alice' }) + }) + + test.afterEach(async () => { + await api.deleteUser({ usersEnvironment, stepUser: 'Admin', targetUser: 'Alice' }) + }) + + test('when spaces loading fails, the crash page is displayed', async () => { + const { page } = actorsEnvironment.getActor({ key: 'Alice' }) + + await page.route('**/me/drives*', (route) => { + route.abort('failed') + }) + await page.reload() + + await ui.expectCrashPageToBeVisible({ page }) + await ui.logOutUser({ actorsEnvironment, stepUser: 'Alice' }) + }) + + test('the crash page does not have any accessibility violations', async () => { + const { page } = actorsEnvironment.getActor({ key: 'Alice' }) + await ui.openCrashPage({ + page, + errorCode: CRASH_CODES.RUNTIME_BOOTSTRAP_SPACES_LOAD + }) + await ui.expectCrashPageToBeVisible({ page }) + await ui.expectCrashPageHasNoAccessibilityViolations({ page }) + }) +}) diff --git a/tests/e2e-playwright/steps/ui/crashPage.ts b/tests/e2e-playwright/steps/ui/crashPage.ts new file mode 100644 index 00000000000..a6c8336038a --- /dev/null +++ b/tests/e2e-playwright/steps/ui/crashPage.ts @@ -0,0 +1,26 @@ +import { CrashPage } from '../../../e2e/support/objects/crash-page' +import { config } from '../../../e2e/config' +import { Page } from '@playwright/test' + +export function expectCrashPageToBeVisible({ page }: { page: Page }) { + const crashPageObject = new CrashPage({ page }) + return crashPageObject.assertVisibility() +} + +export function openCrashPage({ page, errorCode }: { page: Page; errorCode: string }) { + return page.goto(`${config.baseUrl}/crash?errorCode=${errorCode}`) +} + +export async function expectCrashPageHasNoAccessibilityViolations({ page }: { page: Page }) { + if (config.skipA11y) { + return + } + + const crashPageObject = new CrashPage({ page }) + const a11yViolations = await crashPageObject.getAccessibilityViolations() + + expect( + a11yViolations, + `Found ${a11yViolations.length} severe accessibility violations in crash page` + ).toHaveLength(0) +} diff --git a/tests/e2e-playwright/steps/ui/index.ts b/tests/e2e-playwright/steps/ui/index.ts index bd4d3f6755e..b83957ddc03 100644 --- a/tests/e2e-playwright/steps/ui/index.ts +++ b/tests/e2e-playwright/steps/ui/index.ts @@ -6,3 +6,4 @@ export * from './spaces' export * from './application' export * from './account' export * from './appStore' +export * from './crashPage' diff --git a/tests/e2e/support/objects/crash-page/index.ts b/tests/e2e/support/objects/crash-page/index.ts new file mode 100644 index 00000000000..c1d6168b1bb --- /dev/null +++ b/tests/e2e/support/objects/crash-page/index.ts @@ -0,0 +1,23 @@ +import { expect, Page } from '@playwright/test' +import { objects } from '../..' + +const selectors = { + page: '#page-crash' +} + +export class CrashPage { + #page: Page + + constructor({ page }: { page: Page }) { + this.#page = page + } + + assertVisibility() { + return expect(this.#page.locator(selectors.page).first()).toBeVisible() + } + + getAccessibilityViolations() { + const a11yObject = new objects.a11y.Accessibility({ page: this.#page }) + return a11yObject.getSevereAccessibilityViolations(selectors.page) + } +}