diff --git a/libs/core/.eslintignore b/libs/core/.eslintignore index 17f207a89..7f6bfd2b3 100644 --- a/libs/core/.eslintignore +++ b/libs/core/.eslintignore @@ -4,3 +4,4 @@ **/*.stories.* **/*.figma.ts stencil.config.ts +src/utils/test/setup.ts diff --git a/libs/core/src/utils/test/setup.ts b/libs/core/src/utils/test/setup.ts new file mode 100644 index 000000000..9b3efe27c --- /dev/null +++ b/libs/core/src/utils/test/setup.ts @@ -0,0 +1,157 @@ +/** + * Global test setup file + * This file is loaded before all tests to provide mocks and polyfills + */ + +/** + * Mock ElementInternals API for testing + * The ElementInternals API is used for form-associated custom elements + * but is not fully supported in Stencil's unit test environment + */ +class MockElementInternals { + // @ts-expect-error - Stored for mock completeness + private _formValue: FormData | string | null = null; + // @ts-expect-error - Stored for mock completeness + private _formState: FormData | string | null = null; + private _validationMessage = ''; + private _validity: ValidityState = { + badInput: false, + customError: false, + patternMismatch: false, + rangeOverflow: false, + rangeUnderflow: false, + stepMismatch: false, + tooLong: false, + tooShort: false, + typeMismatch: false, + valid: true, + valueMissing: false, + }; + + /** + * Sets the form value and state for the associated custom element + */ + setFormValue( + value: File | string | FormData | null, + state?: File | string | FormData | null, + ): void { + this._formValue = value as FormData | string | null; + this._formState = state !== undefined ? (state as FormData | string | null) : value as FormData | string | null; + } + + /** + * Sets the validity state and validation message + */ + setValidity( + flags?: ValidityStateFlags, + message?: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _anchor?: HTMLElement, + ): void { + if (flags) { + this._validity = { ...this._validity, ...flags, valid: !Object.values(flags).some(Boolean) }; + } + this._validationMessage = message || ''; + } + + /** + * Checks if the element will pass validation + */ + checkValidity(): boolean { + return this._validity.valid; + } + + /** + * Checks validity and fires invalid event if not valid + */ + reportValidity(): boolean { + return this._validity.valid; + } + + /** + * Returns the form associated with this element + */ + get form(): HTMLFormElement | null { + return null; + } + + /** + * Returns the validation message + */ + get validationMessage(): string { + return this._validationMessage; + } + + /** + * Returns the validity state + */ + get validity(): ValidityState { + return this._validity; + } + + /** + * Returns whether the element will be validated + */ + get willValidate(): boolean { + return true; + } + + /** + * Returns the labels associated with this element + */ + get labels(): NodeList { + return { + length: 0, + item: () => null, + // eslint-disable-next-line @typescript-eslint/no-empty-function + [Symbol.iterator]: function* () {}, + } as unknown as NodeList; + } +} + +/** + * Mock attachInternals method on HTMLElement + */ +if (typeof HTMLElement !== 'undefined' && typeof HTMLElement.prototype.attachInternals === 'undefined') { + HTMLElement.prototype.attachInternals = function () { + return new MockElementInternals() as unknown as ElementInternals; + }; +} + +/** + * Suppress specific console warnings that are expected in test environment + */ +const originalConsoleError = console.error; +const originalConsoleWarn = console.warn; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +console.error = (...args: any[]) => { + const message = typeof args[0] === 'string' ? args[0] : ''; + + // Filter out the ElementInternals warning since we're mocking it + if (message.includes('Property setFormValue was accessed on ElementInternals')) { + return; + } + + // Filter out SVG icon 404 errors in e2e tests (icons aren't available on test server) + if (message.includes('Failed to load resource') && message.includes('pds-icons/svg')) { + return; + } + + originalConsoleError.apply(console, args); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +console.warn = (...args: any[]) => { + const message = typeof args[0] === 'string' ? args[0] : ''; + + // Filter out SVG icon loading warnings in e2e tests (icons aren't available on test server) + if (message.includes('Failed to load SVG') && message.includes('pds-icons/svg')) { + return; + } + + originalConsoleWarn.apply(console, args); +}; + +export {}; + diff --git a/libs/core/stencil.config.ts b/libs/core/stencil.config.ts index e4c7e21e3..adde4dbf0 100644 --- a/libs/core/stencil.config.ts +++ b/libs/core/stencil.config.ts @@ -11,6 +11,15 @@ export const config: Config = { openBrowser: false, port: 7300, }, + testing: { + setupFilesAfterEnv: ['/src/utils/test/setup.ts'], + coveragePathIgnorePatterns: [ + '/node_modules/', + '/dist/', + '/www/', + 'setup.ts' + ], + }, outputTargets: [ { type: 'dist', diff --git a/libs/core/tsconfig.json b/libs/core/tsconfig.json index 67a50d611..fd861679b 100644 --- a/libs/core/tsconfig.json +++ b/libs/core/tsconfig.json @@ -30,6 +30,7 @@ "**/*.stories.ts", "**/*.stories.tsx", "**/*.figma.ts", - "scripts/custom-elements" + "scripts/custom-elements", + "src/utils/test/setup.ts" ] }