Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .drone.star
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ config = {
"suites": [
"shares",
"search",
"runtime",
],
},
"4": {
Expand Down
5 changes: 5 additions & 0 deletions changelog/unreleased/enhancement-add-crash-page.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions packages/web-pkg/src/errors/codes.ts
Original file line number Diff line number Diff line change
@@ -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'
})
1 change: 1 addition & 0 deletions packages/web-pkg/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './types'
export * from './codes'
3 changes: 2 additions & 1 deletion packages/web-runtime/src/composables/layout/useLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export const useLayout = (options?: LayoutOptions) => {
'oidcCallback',
'oidcSilentRedirect',
'resolvePublicLink',
'accessDenied'
'accessDenied',
'crash'
]
if (
!unref(router.currentRoute).name ||
Expand Down
26 changes: 17 additions & 9 deletions packages/web-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 } })
}
},
{
Expand Down
98 changes: 98 additions & 0 deletions packages/web-runtime/src/pages/crash.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<template>
<div id="page-crash" class="container">
<img class="logo" :src="logoImg" :alt="productName" />
<div class="card">
<h1 class="title">
{{
$pgettext(
'Title of the crash page displayed when the application crashes',
'Application crashed'
)
}}
</h1>
<p v-if="errorCode !== ''" class="error-code">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to improve accessibility for this one? I guess some users won't be able to understand the connection between the error and the code. What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, the error is there only for us. I.e. if we get a bug report, we know the code and know right away into which part of the code we should look into. In the future, we might consider adding some documentation where the codes could be listed and users/admins could orient themselves but as since we haven't got any error codes correlating with errors that the users could "fix" themselves I don't see too much value in doing that now.

{{ errorCode }}
</p>
<p>{{ errorMessage }}</p>
</div>
</div>
</template>

<script lang="ts" setup>
import { computed, unref } from 'vue'
import { useThemeStore } from '@ownclouders/web-pkg'
import { useHead } from '../composables/head'
import { storeToRefs } from 'pinia'
import { useRoute } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import { CRASH_CODES } from '@ownclouders/web-pkg/src/errors/codes'

const route = useRoute()
const { $pgettext } = useGettext()

const themeStore = useThemeStore()
const { currentTheme } = storeToRefs(themeStore)

const errorCode = computed(() => route.query.code || '')

const errorMessage = computed(() => {
switch (unref(errorCode)) {
case CRASH_CODES.RUNTIME_BOOTSTRAP_SPACES_LOAD:
return $pgettext(
'An error message displayed on crash page when loading spaces during runtime bootstrap fails due to whatever reason.',
'An error occurred while loading your spaces. Please try to reload the page. If the problem persists, seek help from your Administrator.'
)
default:
return $pgettext(
'Generic error message displayed on crash page when either no error code is provided or the error code is not known.',
'An unknown error occurred. Please try to reload the page. If the problem persists, seek help from your Administrator.'
)
}
})

const productName = computed(() => currentTheme.value.common.name)
const logoImg = computed(() => currentTheme.value.logo.login)

useHead()
</script>

<style lang="scss" scoped>
.container {
align-content: center;
align-items: center;
display: grid;
gap: var(--oc-space-large);
justify-items: center;
min-height: 100dvh;
text-align: center;
}

.logo {
max-height: 12.5rem;
max-width: 12.5rem;
}

.card {
box-sizing: border-box;
background-color: var(--oc-color-background-default);
border-radius: 0.3125rem;
color: var(--oc-color-text-default);
padding: var(--oc-space-medium);
margin-inline: auto;
max-width: min(36rem, 100%);
text-align: center;
width: 100%;
}

.title {
font-size: var(--oc-font-size-large);
font-weight: var(--oc-font-weight-semibold);
margin: 0;
}

.error-code {
margin-top: var(--oc-space-small);
font-size: var(--oc-font-size-xsmall);
color: var(--oc-color-text-muted);
}
</style>
9 changes: 9 additions & 0 deletions packages/web-runtime/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
57 changes: 57 additions & 0 deletions tests/e2e-playwright/specs/runtime/crashPage.spec.ts
Original file line number Diff line number Diff line change
@@ -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 })
})
})
26 changes: 26 additions & 0 deletions tests/e2e-playwright/steps/ui/crashPage.ts
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions tests/e2e-playwright/steps/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './spaces'
export * from './application'
export * from './account'
export * from './appStore'
export * from './crashPage'
23 changes: 23 additions & 0 deletions tests/e2e/support/objects/crash-page/index.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}