Skip to content

Commit

Permalink
refactor(errorboundary): miscellaneous improvements
Browse files Browse the repository at this point in the history
add onError callback and error emit, forward params to fallback component
  • Loading branch information
joshuagraber committed Jun 7, 2024
1 parent 6dcb9aa commit 33f40b7
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 28 deletions.
71 changes: 46 additions & 25 deletions src/components/ErrorBoundary/PdapErrorBoundary.vue
Original file line number Diff line number Diff line change
@@ -1,38 +1,59 @@
<template>
<slot v-if="!error$" />
<component
:is="component"
v-if="error"
v-else
class="pdap-flex-container-center h-[full]"
:v-bind="params"
>
<h1>Oops, something went wrong!</h1>
<p class="max-w-full" data-test="error-boundary-message">
If you keep seeing this message, please email
<a href="mailto:[email protected]">[email protected]</a> for assistance.
</p>
</component>
<slot v-else />
</template>
<script>
export default {
props: {
component: {
type: String,
default: 'div',
},
},
data() {
return {
error: false,
};
},
/* TODO: figure out how to cover this lifecycle method in tests */
errorCaptured(error) {
this.interceptError(error);
},
methods: {
interceptError(error) {
this.error = error;
},
},
};

<script setup lang="ts">
import { ComponentPublicInstance, onErrorCaptured, ref } from 'vue';
import { PdapErrorBoundaryProps } from './types';
const props = withDefaults(defineProps<PdapErrorBoundaryProps>(), {
component: 'div',
onError: undefined,
params: undefined,
stopPropagation: false,
});
interface PdapErrorEmitted {
error: Error;
vm: ComponentPublicInstance | null;
info?: string;
}
const emits = defineEmits<{
(event: 'error', { error, vm, info }: PdapErrorEmitted): void;
}>();
const error$ = ref<Error>();
const info$ = ref<string | undefined>('');
function interceptError(
error: Error,
vm: ComponentPublicInstance | null,
info?: string
) {
error$.value = error;
info$.value = info;
props.onError?.(error, vm);
emits('error', { error: error, vm, info });
if (props.stopPropagation) return false;
}
/* Impossible and unwise to try testing this, so we remove from the coverage report */
/* c8 ignore next 3 */
onErrorCaptured((err, vm, info) => {
interceptError(err, vm, info);
});
</script>
25 changes: 24 additions & 1 deletion src/components/ErrorBoundary/error-boundary.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
import { beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import ErrorBoundary from './PdapErrorBoundary.vue';
import { nextTick } from 'vue';

Expand Down Expand Up @@ -28,4 +28,27 @@ describe('ErrorBoundary', () => {
);
expect(wrapper.html()).toMatchSnapshot();
});

it('calls the onError callback when an error occurs', async () => {
const onErrorSpy = vi.fn();

wrapper = mount(ErrorBoundary, {
props: {
onError: onErrorSpy,
},
slots: {
default: '<div>Default Content</div>',
},
});

const testError = new Error('Test Error');
wrapper.vm.interceptError(testError);
await nextTick();

expect(onErrorSpy).toHaveBeenCalledWith({
error: testError,
vm: undefined,
info: undefined,
});
});
});
1 change: 1 addition & 0 deletions src/components/ErrorBoundary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ErrorBoundary } from './PdapErrorBoundary.vue';
11 changes: 11 additions & 0 deletions src/components/ErrorBoundary/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ComponentPublicInstance } from 'vue';

export interface PdapErrorBoundaryProps {
component: string;
onError?: (
error: Error,
target?: ComponentPublicInstance | null | undefined
) => void;
params?: Record<string, string>;
stopPropagation?: boolean;
}
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { Button } from './Button';
export { ErrorBoundary } from './ErrorBoundary';
export { Footer } from './Footer';
export { Form } from './Form';
export { Input } from './Input';
Expand Down
20 changes: 18 additions & 2 deletions src/demo/pages/ComponentDemo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
</template>
</Dropdown>

<h2>Here is a form using the <code>Form</code> component directly</h2>
<h2>Form</h2>
<Form
id="test"
name="test"
Expand All @@ -155,8 +155,19 @@
<Button type="submit">Say hello</Button>
</Form>

<h2>And here is the Quick Search Form component</h2>
<h2>Quick Search Form</h2>
<QuickSearchForm />

<h2>Error Boundary</h2>
<ErrorBoundary class="col-span-full items-start" component="div">
<div class="col-span-full">
<p>
This is the content that will render inside the error boundary if
there is no error
</p>
<Button @click="triggerError">Click here to trigger error</Button>
</div>
</ErrorBoundary>
</main>
</template>

Expand All @@ -166,6 +177,7 @@ import {
Breadcrumbs,
Button,
Dropdown,
ErrorBoundary,
Form,
QuickSearchForm,
} from '../../components';
Expand Down Expand Up @@ -234,6 +246,10 @@ function buttonAlert(msg: string) {
alert(msg);
}
function triggerError() {
throw new Error('Trigger error fallback');
}
function submit(values: Record<'firstName' | 'lastName' | 'iceCream', string>) {
console.debug({ values });
const alertString = `Howdy, ${values.firstName} ${values.lastName}\n${
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import './styles/styles.css';
// Types
export * from './components/Button/types';
export * from './components/Dropdown/types';
export * from './components/ErrorBoundary/types';
export * from './components/Footer/types';
export * from './components/Form/types';
export * from './components/Header/types';
Expand Down

0 comments on commit 33f40b7

Please sign in to comment.