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
20 changes: 20 additions & 0 deletions src/testing/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,18 @@ export async function render<T extends HTMLElement = HTMLElement, I = any>(
setRenderSpyConfig(options.spyOn);
}

// Capture lifecycle errors (e.g. throws in componentWillLoad).
// Stencil's safeCall() catches all lifecycle hook errors and routes them to
// console.error instead of re-throwing.
let lifecycleError: unknown;
const origConsoleError = console.error;
console.error = (err: unknown, ...rest: unknown[]) => {
if (err instanceof Error && lifecycleError === undefined) {
lifecycleError = err;
}
origConsoleError(err, ...rest);
};

if (typeof template === 'string') {
// Handle string template - add as innerHTML
container.innerHTML = template;
Expand Down Expand Up @@ -277,6 +289,14 @@ export async function render<T extends HTMLElement = HTMLElement, I = any>(
await waitForChanges();
}

// Restore console.error now that the lifecycle is done
console.error = origConsoleError;

// Re-throw any lifecycle error that Stencil's safeCall swallowed
if (lifecycleError !== undefined) {
throw lifecycleError;
}

// Clear per-render spy config after component is ready
if (options.spyOn) {
setRenderSpyConfig(null);
Expand Down
40 changes: 40 additions & 0 deletions test-project/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
export namespace Components {
/**
* A component that throws in componentWillLoad when a required prop is missing.
* Used to test that lifecycle hooks are called and errors propagate correctly.
*/
interface LifecycleThrow {
/**
* Required label prop - throws if not provided
*/
"label": string;
}
/**
* A simple button component for testing
*/
Expand Down Expand Up @@ -59,6 +69,16 @@ export interface MyButtonCustomEvent<T> extends CustomEvent<T> {
target: HTMLMyButtonElement;
}
declare global {
/**
* A component that throws in componentWillLoad when a required prop is missing.
* Used to test that lifecycle hooks are called and errors propagate correctly.
*/
interface HTMLLifecycleThrowElement extends Components.LifecycleThrow, HTMLStencilElement {
}
var HTMLLifecycleThrowElement: {
prototype: HTMLLifecycleThrowElement;
new (): HTMLLifecycleThrowElement;
};
interface HTMLMyButtonElementEventMap {
"buttonClick": MouseEvent;
}
Expand Down Expand Up @@ -101,12 +121,23 @@ declare global {
new (): HTMLNonShadowComponentElement;
};
interface HTMLElementTagNameMap {
"lifecycle-throw": HTMLLifecycleThrowElement;
"my-button": HTMLMyButtonElement;
"my-card": HTMLMyCardElement;
"non-shadow-component": HTMLNonShadowComponentElement;
}
}
declare namespace LocalJSX {
/**
* A component that throws in componentWillLoad when a required prop is missing.
* Used to test that lifecycle hooks are called and errors propagate correctly.
*/
interface LifecycleThrow {
/**
* Required label prop - throws if not provided
*/
"label"?: string;
}
/**
* A simple button component for testing
*/
Expand Down Expand Up @@ -159,6 +190,9 @@ declare namespace LocalJSX {
interface NonShadowComponent {
}

interface LifecycleThrowAttributes {
"label": string;
}
interface MyButtonAttributes {
"variant": 'primary' | 'secondary' | 'danger';
"disabled": boolean;
Expand All @@ -171,6 +205,7 @@ declare namespace LocalJSX {
}

interface IntrinsicElements {
"lifecycle-throw": Omit<LifecycleThrow, keyof LifecycleThrowAttributes> & { [K in keyof LifecycleThrow & keyof LifecycleThrowAttributes]?: LifecycleThrow[K] } & { [K in keyof LifecycleThrow & keyof LifecycleThrowAttributes as `attr:${K}`]?: LifecycleThrowAttributes[K] } & { [K in keyof LifecycleThrow & keyof LifecycleThrowAttributes as `prop:${K}`]?: LifecycleThrow[K] };
"my-button": Omit<MyButton, keyof MyButtonAttributes> & { [K in keyof MyButton & keyof MyButtonAttributes]?: MyButton[K] } & { [K in keyof MyButton & keyof MyButtonAttributes as `attr:${K}`]?: MyButtonAttributes[K] } & { [K in keyof MyButton & keyof MyButtonAttributes as `prop:${K}`]?: MyButton[K] };
"my-card": Omit<MyCard, keyof MyCardAttributes> & { [K in keyof MyCard & keyof MyCardAttributes]?: MyCard[K] } & { [K in keyof MyCard & keyof MyCardAttributes as `attr:${K}`]?: MyCardAttributes[K] } & { [K in keyof MyCard & keyof MyCardAttributes as `prop:${K}`]?: MyCard[K] };
"non-shadow-component": NonShadowComponent;
Expand All @@ -180,6 +215,11 @@ export { LocalJSX as JSX };
declare module "@stencil/core" {
export namespace JSX {
interface IntrinsicElements {
/**
* A component that throws in componentWillLoad when a required prop is missing.
* Used to test that lifecycle hooks are called and errors propagate correctly.
*/
"lifecycle-throw": LocalJSX.IntrinsicElements["lifecycle-throw"] & JSXBase.HTMLAttributes<HTMLLifecycleThrowElement>;
/**
* A simple button component for testing
*/
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { render, expect, describe, it, assert, h } from '@stencil/vitest';

describe('lifecycle-throw - componentWillLoad error', () => {
it('should render successfully when the required prop is provided', async () => {
const { root } = await render(<lifecycle-throw label="hello"></lifecycle-throw>);
expect(root).toBeTruthy();
expect(root.shadowRoot?.querySelector('span')?.textContent).toBe('hello');
});

it('should throw when the required prop is missing', async () => {
try {
const page = await render(<lifecycle-throw></lifecycle-throw>);
await page.waitForChanges();
assert.fail('Expected an error');
} catch (error) {
expect(error.message).toEqual('Property [label] required');
}
});
});
26 changes: 26 additions & 0 deletions test-project/src/components/lifecycle-throw/lifecycle-throw.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Component, Prop, h } from '@stencil/core';

/**
* A component that throws in componentWillLoad when a required prop is missing.
* Used to test that lifecycle hooks are called and errors propagate correctly.
*/
@Component({
tag: 'lifecycle-throw',
shadow: true,
})
export class LifecycleThrow {
/**
* Required label prop - throws if not provided
*/
@Prop() label: string;

componentWillLoad() {
if (!this.label) {
throw new Error('Property [label] required');
}
}

render() {
return <span>{this.label}</span>;
}
}
Loading