Skip to content

Commit

Permalink
fix(testing): support functional components in unit tests (#5722)
Browse files Browse the repository at this point in the history
* fix(testing): support functional components in unit tests

STENCIL-751

* fix tests

* prettier

* fix property

* fix type issues

* use generateRandBundleId to create random number
  • Loading branch information
christian-bromann committed May 6, 2024
1 parent 1b7320b commit 922a972
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/testing/platform/testing-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export async function startAutoApplyChanges(): Promise<void> {
* @param Cstrs the component constructors to register
*/
export const registerComponents = (Cstrs: d.ComponentTestingConstructor[]): void => {
Cstrs.forEach((Cstr) => {
Cstrs.filter((Cstr) => Cstr.COMPILER_META).forEach((Cstr) => {
cstrs.set(Cstr.COMPILER_META.tagName, Cstr);
});
};
Expand Down
21 changes: 18 additions & 3 deletions src/testing/spec-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ import { formatLazyBundleRuntimeMeta } from '@utils';
import { getBuildFeatures } from '../compiler/app-core/app-data';
import { resetBuildConditionals } from './reset-build-conditionals';

/**
* Generates a random number for use in generating a bundle id
* @returns a random number between 100000 and 999999
*/
const generateRandBundleId = () => Math.round(Math.random() * 899999) + 100000;

/**
* Creates a new spec page for unit testing
* @param opts the options to apply to the spec page that influence its configuration and operation
Expand Down Expand Up @@ -85,16 +91,23 @@ export async function newSpecPage(opts: NewSpecPageOptions): Promise<SpecPage> {
};

const lazyBundles: LazyBundlesRuntimeData = opts.components.map((Cstr: ComponentTestingConstructor) => {
/**
* just pass through functional components that don't have styles nor any other metadata
*/
if (Cstr.COMPILER_META == null) {
throw new Error(`Invalid component class: Missing static "COMPILER_META" property.`);
/**
* the bundleId can be arbitrary, but must be unique
*/
const arbitraryBundleId = `fc.${generateRandBundleId()}`;
return formatLazyBundleRuntimeMeta(arbitraryBundleId, []);
}

cmpTags.add(Cstr.COMPILER_META.tagName);
Cstr.isProxied = false;

proxyComponentLifeCycles(Cstr);

const bundleId = `${Cstr.COMPILER_META.tagName}.${Math.round(Math.random() * 899999) + 100000}`;
const bundleId = `${Cstr.COMPILER_META.tagName}.${generateRandBundleId()}`;
const stylesMeta = Cstr.COMPILER_META.styles;
if (Array.isArray(stylesMeta)) {
if (stylesMeta.length > 1) {
Expand All @@ -113,7 +126,9 @@ export async function newSpecPage(opts: NewSpecPageOptions): Promise<SpecPage> {
return lazyBundleRuntimeMeta;
});

const cmpCompilerMeta = opts.components.map((Cstr) => Cstr.COMPILER_META as ComponentCompilerMeta);
const cmpCompilerMeta = opts.components
.filter((Cstr) => Cstr.COMPILER_META != null)
.map((Cstr) => Cstr.COMPILER_META as ComponentCompilerMeta);
const cmpBuild = getBuildFeatures(cmpCompilerMeta);
if (opts.strictBuild) {
Object.assign(BUILD, cmpBuild);
Expand Down
16 changes: 16 additions & 0 deletions src/testing/test/__fixtures__/cmp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Component, h } from '@stencil/core';

@Component({
tag: 'class-component',
shadow: true,
})
export class ClassComponent {
render() {
return (
<div>
<h1>I am a class component</h1>
<slot></slot>
</div>
);
}
}
107 changes: 107 additions & 0 deletions src/testing/test/functional.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Fragment, h } from '@stencil/core';
import { newSpecPage, SpecPage } from '@stencil/core/testing';

import { ClassComponent } from './__fixtures__/cmp';

describe('testing function and class components', () => {
it('can render a single functional component', async () => {
const MyFunctionalComponent = () => <div>Hello World</div>;
const page: SpecPage = await newSpecPage({
components: [MyFunctionalComponent],
template: () => <MyFunctionalComponent></MyFunctionalComponent>,
});
expect(page.root).toEqualHtml(`<div>Hello World</div>`);
});

it('can render a single functional component with props', async () => {
const MyFunctionalComponent = (props: { foo: 'bar' }) => <div>{props.foo}</div>;
const page: SpecPage = await newSpecPage({
components: [MyFunctionalComponent],
template: () => <MyFunctionalComponent foo="bar"></MyFunctionalComponent>,
});
expect(page.root).toEqualHtml(`<div>bar</div>`);
});

it('can render a single functional component with children', async () => {
const MyFunctionalComponent: Fragment = (props: never, children: Fragment) => <div>{children}</div>;
const page: SpecPage = await newSpecPage({
components: [MyFunctionalComponent],
template: () => <MyFunctionalComponent>Hello World</MyFunctionalComponent>,
});
expect(page.root).toEqualHtml(`<div>Hello World</div>`);
});

it('can render a single functional component with children and props', async () => {
const MyFunctionalComponent = (props: { foo: 'bar' }, children: Fragment) => (
<div>
{children} - {props.foo}
</div>
);
const page: SpecPage = await newSpecPage({
components: [MyFunctionalComponent],
template: () => <MyFunctionalComponent foo="bar">Hello World</MyFunctionalComponent>,
});
expect(page.root).toEqualHtml(`<div>Hello World - bar</div>`);
});

it('can render a class component with a functional component', async () => {
const MyFunctionalComponent = (props: never, children: Fragment) => (
<div>I am a functional component - {children}</div>
);
const page: SpecPage = await newSpecPage({
components: [ClassComponent],
template: () => (
<class-component>
<MyFunctionalComponent>Yes I am!</MyFunctionalComponent>
</class-component>
),
});
expect(page.root).toEqualHtml(`<class-component>
<mock:shadow-root>
<div>
<h1>
I am a class component
</h1>
<slot></slot>
</div>
</mock:shadow-root>
<div>
I am a functional component - Yes I am!
</div>
</class-component>
`);
});

it('can render a functional component within a class component', async () => {
const MyFunctionalComponent = (props: never, children: Fragment) => (
<div>
<h1>I am a functional component</h1>
{children}
</div>
);
const page: SpecPage = await newSpecPage({
components: [ClassComponent],
template: () => (
<MyFunctionalComponent>
<class-component>Yes I am!</class-component>
</MyFunctionalComponent>
),
});
expect(page.body).toEqualHtml(`<div>
<h1>
I am a functional component
</h1>
<class-component>
<mock:shadow-root>
<div>
<h1>
I am a class component
</h1>
<slot></slot>
</div>
</mock:shadow-root>
Yes I am!
</class-component>
</div>`);
});
});
11 changes: 11 additions & 0 deletions src/testing/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../testing/tsconfig.internal.json",
"compilerOptions": {
"paths": {
"@stencil/core": ["../../index.ts"],
"@stencil/core/testing": ["../../testing/index.ts"],
"@platform": ["../../client/index.ts"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
}

0 comments on commit 922a972

Please sign in to comment.