From 922a97207dbe031d164a9b5e16fac4b004a5b7bf Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 6 May 2024 07:25:46 -0700 Subject: [PATCH] fix(testing): support functional components in unit tests (#5722) * fix(testing): support functional components in unit tests STENCIL-751 * fix tests * prettier * fix property * fix type issues * use generateRandBundleId to create random number --- src/testing/platform/testing-platform.ts | 2 +- src/testing/spec-page.ts | 21 ++++- src/testing/test/__fixtures__/cmp.tsx | 16 ++++ src/testing/test/functional.spec.tsx | 107 +++++++++++++++++++++++ src/testing/test/tsconfig.json | 11 +++ 5 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 src/testing/test/__fixtures__/cmp.tsx create mode 100644 src/testing/test/functional.spec.tsx create mode 100644 src/testing/test/tsconfig.json diff --git a/src/testing/platform/testing-platform.ts b/src/testing/platform/testing-platform.ts index d89ebd9bb7a..772f9209128 100644 --- a/src/testing/platform/testing-platform.ts +++ b/src/testing/platform/testing-platform.ts @@ -105,7 +105,7 @@ export async function startAutoApplyChanges(): Promise { * @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); }); }; diff --git a/src/testing/spec-page.ts b/src/testing/spec-page.ts index a24aaa59330..fe42c30f588 100644 --- a/src/testing/spec-page.ts +++ b/src/testing/spec-page.ts @@ -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 @@ -85,8 +91,15 @@ export async function newSpecPage(opts: NewSpecPageOptions): Promise { }; 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); @@ -94,7 +107,7 @@ export async function newSpecPage(opts: NewSpecPageOptions): Promise { 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) { @@ -113,7 +126,9 @@ export async function newSpecPage(opts: NewSpecPageOptions): Promise { 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); diff --git a/src/testing/test/__fixtures__/cmp.tsx b/src/testing/test/__fixtures__/cmp.tsx new file mode 100644 index 00000000000..cf4d6c34cee --- /dev/null +++ b/src/testing/test/__fixtures__/cmp.tsx @@ -0,0 +1,16 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'class-component', + shadow: true, +}) +export class ClassComponent { + render() { + return ( +
+

I am a class component

+ +
+ ); + } +} diff --git a/src/testing/test/functional.spec.tsx b/src/testing/test/functional.spec.tsx new file mode 100644 index 00000000000..b929dd624df --- /dev/null +++ b/src/testing/test/functional.spec.tsx @@ -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 = () =>
Hello World
; + const page: SpecPage = await newSpecPage({ + components: [MyFunctionalComponent], + template: () => , + }); + expect(page.root).toEqualHtml(`
Hello World
`); + }); + + it('can render a single functional component with props', async () => { + const MyFunctionalComponent = (props: { foo: 'bar' }) =>
{props.foo}
; + const page: SpecPage = await newSpecPage({ + components: [MyFunctionalComponent], + template: () => , + }); + expect(page.root).toEqualHtml(`
bar
`); + }); + + it('can render a single functional component with children', async () => { + const MyFunctionalComponent: Fragment = (props: never, children: Fragment) =>
{children}
; + const page: SpecPage = await newSpecPage({ + components: [MyFunctionalComponent], + template: () => Hello World, + }); + expect(page.root).toEqualHtml(`
Hello World
`); + }); + + it('can render a single functional component with children and props', async () => { + const MyFunctionalComponent = (props: { foo: 'bar' }, children: Fragment) => ( +
+ {children} - {props.foo} +
+ ); + const page: SpecPage = await newSpecPage({ + components: [MyFunctionalComponent], + template: () => Hello World, + }); + expect(page.root).toEqualHtml(`
Hello World - bar
`); + }); + + it('can render a class component with a functional component', async () => { + const MyFunctionalComponent = (props: never, children: Fragment) => ( +
I am a functional component - {children}
+ ); + const page: SpecPage = await newSpecPage({ + components: [ClassComponent], + template: () => ( + + Yes I am! + + ), + }); + expect(page.root).toEqualHtml(` + +
+

+ I am a class component +

+ +
+
+
+ I am a functional component - Yes I am! +
+
+`); + }); + + it('can render a functional component within a class component', async () => { + const MyFunctionalComponent = (props: never, children: Fragment) => ( +
+

I am a functional component

+ {children} +
+ ); + const page: SpecPage = await newSpecPage({ + components: [ClassComponent], + template: () => ( + + Yes I am! + + ), + }); + expect(page.body).toEqualHtml(`
+

+ I am a functional component +

+ + +
+

+ I am a class component +

+ +
+
+ Yes I am! +
+
`); + }); +}); diff --git a/src/testing/test/tsconfig.json b/src/testing/test/tsconfig.json new file mode 100644 index 00000000000..81e0bd09536 --- /dev/null +++ b/src/testing/test/tsconfig.json @@ -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"], +}