diff --git a/.gitignore b/.gitignore index 9f07f6ce6dd..842e6a5b45b 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ unused-exports*.txt # wdio test output test/wdio/test-components +test/wdio/test-components-no-external-runtime test/wdio/www-global-script/ test/wdio/www-prerender-script test/wdio/www-invisible-prehydration/ diff --git a/src/runtime/proxy-component.ts b/src/runtime/proxy-component.ts index 2b2fda3b93a..8f7d26a097c 100644 --- a/src/runtime/proxy-component.ts +++ b/src/runtime/proxy-component.ts @@ -29,24 +29,25 @@ export const proxyComponent = ( * @ref https://web.dev/articles/more-capable-form-controls#lifecycle_callbacks */ if (BUILD.formAssociated && cmpMeta.$flags$ & CMP_FLAGS.formAssociated && flags & PROXY_FLAGS.isElementConstructor) { - FORM_ASSOCIATED_CUSTOM_ELEMENT_CALLBACKS.forEach((cbName) => + FORM_ASSOCIATED_CUSTOM_ELEMENT_CALLBACKS.forEach((cbName) => { + const originalFormAssociatedCallback = prototype[cbName]; Object.defineProperty(prototype, cbName, { value(this: d.HostElement, ...args: any[]) { const hostRef = getHostRef(this); - const elm = BUILD.lazyLoad ? hostRef.$hostElement$ : this; - const instance: d.ComponentInterface = BUILD.lazyLoad ? hostRef.$lazyInstance$ : elm; + const instance: d.ComponentInterface = BUILD.lazyLoad ? hostRef.$lazyInstance$ : this; if (!instance) { - hostRef.$onReadyPromise$.then((instance: d.ComponentInterface) => { - const cb = instance[cbName]; - typeof cb === 'function' && cb.call(instance, ...args); + hostRef.$onReadyPromise$.then((asyncInstance: d.ComponentInterface) => { + const cb = asyncInstance[cbName]; + typeof cb === 'function' && cb.call(asyncInstance, ...args); }); } else { - const cb = instance[cbName]; + // Use the method on `instance` if `lazyLoad` is set, otherwise call the original method to avoid an infinite loop. + const cb = BUILD.lazyLoad ? instance[cbName] : originalFormAssociatedCallback; typeof cb === 'function' && cb.call(instance, ...args); } }, - }), - ); + }); + }); } if ((BUILD.member && cmpMeta.$members$) || (BUILD.watchCallback && (cmpMeta.$watchers$ || Cstr.watchers))) { diff --git a/test/wdio/no-external-runtime.stencil.config.ts b/test/wdio/no-external-runtime.stencil.config.ts new file mode 100644 index 00000000000..bb3d18bef56 --- /dev/null +++ b/test/wdio/no-external-runtime.stencil.config.ts @@ -0,0 +1,15 @@ +import type { Config } from '../../internal/index.js'; + +export const config: Config = { + namespace: 'TestNoExternalRuntimeApp', + tsconfig: 'tsconfig-no-external-runtime.json', + outputTargets: [ + { + type: 'dist-custom-elements', + dir: 'test-components-no-external-runtime', + externalRuntime: false, + includeGlobalScripts: false, + }, + ], + srcDir: 'no-external-runtime', +}; diff --git a/test/wdio/no-external-runtime/components.d.ts b/test/wdio/no-external-runtime/components.d.ts new file mode 100644 index 00000000000..ce611509d45 --- /dev/null +++ b/test/wdio/no-external-runtime/components.d.ts @@ -0,0 +1,37 @@ +/* eslint-disable */ +/* tslint:disable */ +/** + * This is an autogenerated file created by the Stencil compiler. + * It contains typing information for all components that exist in this project. + */ +import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; +export namespace Components { + interface CustomElementsFormAssociated { + } +} +declare global { + interface HTMLCustomElementsFormAssociatedElement extends Components.CustomElementsFormAssociated, HTMLStencilElement { + } + var HTMLCustomElementsFormAssociatedElement: { + prototype: HTMLCustomElementsFormAssociatedElement; + new (): HTMLCustomElementsFormAssociatedElement; + }; + interface HTMLElementTagNameMap { + "custom-elements-form-associated": HTMLCustomElementsFormAssociatedElement; + } +} +declare namespace LocalJSX { + interface CustomElementsFormAssociated { + } + interface IntrinsicElements { + "custom-elements-form-associated": CustomElementsFormAssociated; + } +} +export { LocalJSX as JSX }; +declare module "@stencil/core" { + export namespace JSX { + interface IntrinsicElements { + "custom-elements-form-associated": LocalJSX.CustomElementsFormAssociated & JSXBase.HTMLAttributes; + } + } +} diff --git a/test/wdio/no-external-runtime/custom-elements-form-associated/cmp.test.tsx b/test/wdio/no-external-runtime/custom-elements-form-associated/cmp.test.tsx new file mode 100644 index 00000000000..2627985fbd8 --- /dev/null +++ b/test/wdio/no-external-runtime/custom-elements-form-associated/cmp.test.tsx @@ -0,0 +1,67 @@ +import { h } from '@stencil/core'; +import { render } from '@wdio/browser-runner/stencil'; + +import { defineCustomElement } from '../../test-components-no-external-runtime/custom-elements-form-associated.js'; + +describe('custome elements form associated', function () { + beforeEach(() => { + defineCustomElement(); + render({ + template: () => ( +
+ + +
+ ), + }); + }); + + it('should render without errors', async () => { + const elm = $('custom-elements-form-associated'); + await expect(elm).toBePresent(); + }); + + describe('form associated custom element lifecycle callback', () => { + it('should trigger "formAssociated"', async () => { + const formEl = $('form'); + await expect(formEl).toHaveProperty('ariaLabel', 'asdfasdf'); + }); + + it('should trigger "formResetCallback"', async () => { + const resetBtn = $('input[type="reset"]'); + await resetBtn.click(); + + await resetBtn.waitForStable(); + + const formEl = $('form'); + await expect(formEl).toHaveProperty('ariaLabel', 'formResetCallback called'); + }); + + it('should trigger "formDisabledCallback"', async () => { + const elm = document.body.querySelector('custom-elements-form-associated'); + const formEl = $('form'); + + elm.setAttribute('disabled', 'disabled'); + + await formEl.waitForStable(); + await expect(formEl).toHaveProperty('ariaLabel', 'formDisabledCallback called with true'); + + elm.removeAttribute('disabled'); + await formEl.waitForStable(); + await expect(formEl).toHaveProperty('ariaLabel', 'formDisabledCallback called with false'); + }); + }); + + it('should link up to the surrounding form', async () => { + // this shows that the element has, through the `ElementInternals` + // interface, been able to set a value in the surrounding form + await browser.waitUntil( + async () => { + const formEl = document.body.querySelector('form'); + expect(new FormData(formEl).get('test-input')).toBe('my default value'); + return true; + }, + { timeoutMsg: 'form associated value never changed' }, + ); + }); +}); diff --git a/test/wdio/no-external-runtime/custom-elements-form-associated/cmp.tsx b/test/wdio/no-external-runtime/custom-elements-form-associated/cmp.tsx new file mode 100644 index 00000000000..bfce0129014 --- /dev/null +++ b/test/wdio/no-external-runtime/custom-elements-form-associated/cmp.tsx @@ -0,0 +1,25 @@ +import { AttachInternals, Component, h } from '@stencil/core'; + +@Component({ + tag: 'custom-elements-form-associated', + formAssociated: true, + shadow: true, +}) +export class CustomElementsFormAssociated { + @AttachInternals() internals: ElementInternals; + + componentWillLoad() { + this.internals.setFormValue('my default value'); + } + + formAssociatedCallback(form: HTMLCustomElementsFormAssociatedElement) { + form.ariaLabel = 'formAssociated called'; + // this is a regression test for #5106 which ensures that `this` is + // resolved correctly + this.internals.setValidity({}); + } + + render() { + return ; + } +} diff --git a/test/wdio/package.json b/test/wdio/package.json index 60db734854f..0ab4bc68c5d 100644 --- a/test/wdio/package.json +++ b/test/wdio/package.json @@ -3,12 +3,13 @@ "type": "module", "version": "0.0.0", "scripts": { - "build": "run-s build.test-sibling build.main build.global-script build.prerender build.invisible-prehydration", + "build": "run-s build.no-external-runtime build.test-sibling build.main build.global-script build.prerender build.invisible-prehydration", "build.main": "node ../../bin/stencil build --debug --es5", "build.global-script": "node ../../bin/stencil build --debug --es5 --config global-script.stencil.config.ts", "build.test-sibling": "cd test-sibling && npm run build", "build.prerender": "node ../../bin/stencil build --config prerender.stencil.config.ts --prerender --debug && node ./test-prerender/prerender.js && node ./test-prerender/no-script-build.js", "build.invisible-prehydration": "node ../../bin/stencil build --debug --es5 --config invisible-prehydration.stencil.config.ts", + "build.no-external-runtime": "node ../../bin/stencil build --debug --es5 --config no-external-runtime.stencil.config.ts", "test": "run-s build wdio", "wdio": "wdio run ./wdio.conf.ts" }, diff --git a/test/wdio/setup.ts b/test/wdio/setup.ts index fe4500d79b8..9309cb14e4c 100644 --- a/test/wdio/setup.ts +++ b/test/wdio/setup.ts @@ -13,6 +13,7 @@ const testRequiresManualSetup = window.__wdioSpec__.includes('custom-elements-output-tag-class-different') || window.__wdioSpec__.includes('custom-elements-delegates-focus') || window.__wdioSpec__.includes('custom-elements-output') || + window.__wdioSpec__.includes('no-external-runtime') || window.__wdioSpec__.includes('global-script') || window.__wdioSpec__.endsWith('custom-tag-name.test.tsx') || window.__wdioSpec__.endsWith('page-list.test.ts'); diff --git a/test/wdio/tsconfig-no-external-runtime.json b/test/wdio/tsconfig-no-external-runtime.json new file mode 100644 index 00000000000..532f86cd342 --- /dev/null +++ b/test/wdio/tsconfig-no-external-runtime.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig-stencil.json", + "include": ["no-external-runtime"], + "exclude": ["no-external-runtime/**/*.test.tsx"] +} diff --git a/test/wdio/tsconfig-stencil.json b/test/wdio/tsconfig-stencil.json index ebaaf789f82..dfbca06b847 100644 --- a/test/wdio/tsconfig-stencil.json +++ b/test/wdio/tsconfig-stencil.json @@ -38,6 +38,9 @@ "./test-sibling/**/*.tsx", // we also exclude the files in the invisible-prehydration directory "./invisible-prehydration/**/*.tsx", - "./invisible-prehydration/**/*.ts" + "./invisible-prehydration/**/*.ts", + // exclude no-external-runtime because they are built separately with `externalRuntime: false` + "./no-external-runtime/**/*.tsx", + "./no-external-runtime/**/*.ts", ] }