diff --git a/.github/workflows/web-test-runner.yml b/.github/workflows/web-test-runner.yml new file mode 100644 index 0000000000..5f8de6cf4e --- /dev/null +++ b/.github/workflows/web-test-runner.yml @@ -0,0 +1,59 @@ +name: Run Web Test Runner integration tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}} + SAUCE_ACCESS_KEY: ${{secrets.SAUCE_ACCESS_KEY}} + GITHUB_RUN_ID: ${{github.run_id}} + COVERAGE: '1' + NODE_VERSION: '20.18.1' + +jobs: + # Starting with the basics, just get tests running in CI + # TODO: add env var combos we use for Karma tests + # TODO: upload result artifacts + # TODO: make it saucy 🥫 + run-wtr-tests-group-1: + runs-on: ubuntu-22.04 + env: + SAUCE_TUNNEL_ID: github-action-tunnel-wtr-${{github.run_id}}-group-1 + defaults: + run: + working-directory: ./packages/@lwc/integration-not-karma + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + working-directory: ./ + + # - uses: saucelabs/sauce-connect-action@v3.0.0 + # with: + # username: ${{ secrets.SAUCE_USERNAME }} + # accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} + # tunnelName: ${{ env.SAUCE_TUNNEL_ID }} + # region: us + + - run: yarn test + + run-karma-tests: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: ./packages/@lwc/integration-not-karma + needs: + - run-wtr-tests-group-1 diff --git a/.github/workflows/integration.yml b/.github/workflows/webdriver.yml similarity index 100% rename from .github/workflows/integration.yml rename to .github/workflows/webdriver.yml diff --git a/eslint.config.mjs b/eslint.config.mjs index e737a092dd..f890cfb35a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -218,7 +218,7 @@ export default tseslint.config( }, }, { - files: ['packages/@lwc/integration-karma/**'], + files: ['packages/@lwc/integration-karma/**', 'packages/@lwc/integration-not-karma/**'], languageOptions: { globals: { @@ -355,7 +355,7 @@ export default tseslint.config( }, }, { - files: ['packages/@lwc/integration-karma/**'], + files: ['packages/@lwc/integration-karma/**', 'packages/@lwc/integration-not-karma/**'], languageOptions: { globals: { diff --git a/packages/@lwc/integration-not-karma/LICENSE.md b/packages/@lwc/integration-not-karma/LICENSE.md new file mode 100644 index 0000000000..21c441068e --- /dev/null +++ b/packages/@lwc/integration-not-karma/LICENSE.md @@ -0,0 +1,108 @@ +# LWC core license + +MIT LICENSE + +Copyright (c) 2025, Salesforce, Inc. +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Licenses of bundled dependencies + +## @parse5/tools + +The MIT License (MIT) + +Copyright © 2024 James Garbutt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## entities + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## estree-walker + +Copyright (c) 2015-20 [these people](https://github.com/Rich-Harris/estree-walker/graphs/contributors) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## observable-membrane + +MIT License + +Copyright (c) 2017 Salesforce + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## parse5 + +Copyright (c) 2013-2019 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/@lwc/integration-not-karma/README.md b/packages/@lwc/integration-not-karma/README.md new file mode 100644 index 0000000000..0de50cf62c --- /dev/null +++ b/packages/@lwc/integration-not-karma/README.md @@ -0,0 +1,3 @@ +# @lwc/integration-not-karma + +It's not karma, it's something else! diff --git a/packages/@lwc/integration-not-karma/helpers/lwc.mjs b/packages/@lwc/integration-not-karma/helpers/lwc.mjs new file mode 100644 index 0000000000..5af4331d0b --- /dev/null +++ b/packages/@lwc/integration-not-karma/helpers/lwc.mjs @@ -0,0 +1,97 @@ +import path from 'node:path'; +import { rollup } from 'rollup'; +import lwcRollupPlugin from '@lwc/rollup-plugin'; + +import { + DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER, + API_VERSION, + DISABLE_STATIC_CONTENT_OPTIMIZATION, +} from './options.mjs'; + +// Cache reused between each compilation to speed up the compilation time. +let cache; + +export default async (ctx) => { + const input = ctx.path.slice(1); // strip leading / from URL path to get relative file path + const suiteDir = path.dirname(input); + + // TODO [#3370]: remove experimental template expression flag + const experimentalComplexExpressions = suiteDir.includes('template-expressions'); + + const createRollupPlugin = (options) => { + return lwcRollupPlugin({ + // Sourcemaps don't work with Istanbul coverage + sourcemap: !process.env.COVERAGE, + experimentalDynamicComponent: { + loader: 'test-utils', + strict: true, + }, + enableDynamicComponents: true, + enableLwcOn: true, + experimentalComplexExpressions, + enableStaticContentOptimization: !DISABLE_STATIC_CONTENT_OPTIMIZATION, + disableSyntheticShadowSupport: DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER, + apiVersion: API_VERSION, + modules: [ + { + // Assume `ctx.path` is a component file, e.g. modules/x/foo/foo.js + dir: path.resolve(input, '../../..'), + }, + ], + ...options, + }); + }; + + const defaultRollupPlugin = createRollupPlugin(); + + const customLwcRollupPlugin = { + ...defaultRollupPlugin, + transform(src, id) { + let rollupPluginToUse; + + // Override the LWC Rollup plugin to specify different options based on file name patterns. + // This allows us to alter the API version or other compiler props on a filename-only basis. + const apiVersion = id.match(/useApiVersion(\d+)/)?.[1]; + const nativeOnly = /\.native-only\./.test(id); + if (apiVersion) { + rollupPluginToUse = createRollupPlugin({ + apiVersion: parseInt(apiVersion, 10), + }); + } else if (nativeOnly) { + rollupPluginToUse = createRollupPlugin({ disableSyntheticShadowSupport: true }); + } else { + rollupPluginToUse = defaultRollupPlugin; + } + return rollupPluginToUse.transform.call(this, src, id); + }, + }; + + const bundle = await rollup({ + input, + cache, + plugins: [customLwcRollupPlugin], + + // Rollup should not attempt to resolve the engine and the test utils, Karma takes care of injecting it + // globally in the page before running the tests. + external: ['lwc', 'wire-service', 'test-utils', '@test/loader'], + + onwarn(warning, warn) { + // Ignore warnings from our own Rollup plugin + if (warning.plugin !== 'rollup-plugin-lwc-compiler') { + warn(warning); + } + }, + }); + + cache = bundle.cache; + + const { output } = await bundle.generate({ + format: 'esm', + // FIXME: Does web-test-runner use istanbul? + // Sourcemaps don't work with Istanbul coverage + sourcemap: process.env.COVERAGE ? false : 'inline', + }); + + const { code } = output[0]; + return code; +}; diff --git a/packages/@lwc/integration-not-karma/helpers/matchers.mjs b/packages/@lwc/integration-not-karma/helpers/matchers.mjs new file mode 100644 index 0000000000..d1781cd5ba --- /dev/null +++ b/packages/@lwc/integration-not-karma/helpers/matchers.mjs @@ -0,0 +1,31 @@ +// FIXME: copy implementations from karma tests + +const customMatchers = [ + // LWC custom + 'toLogError', + 'toLogErrorDev', + 'toThrowErrorDev', + 'toLogWarningDev', + 'toThrowCallbackReactionError', + 'toThrowCallbackReactionErrorDev', + 'toThrowCallbackReactionErrorEvenInSyntheticLifecycleMode', + // jasmine compat + 'toHaveSize', + 'toBeFalse', + 'toBeTrue', +]; +export const registerCustomMatchers = (chai, utils) => { + for (const matcher of customMatchers) { + utils.addMethod(chai.Assertion.prototype, matcher, function () { + // FIXME: implement for realsies + const fn = utils.flag(this, 'object'); + if (typeof fn === 'function') { + try { + fn(); + } catch (_) { + // + } + } + }); + } +}; diff --git a/packages/@lwc/integration-not-karma/helpers/options.mjs b/packages/@lwc/integration-not-karma/helpers/options.mjs new file mode 100644 index 0000000000..73a90c2db6 --- /dev/null +++ b/packages/@lwc/integration-not-karma/helpers/options.mjs @@ -0,0 +1,75 @@ +import { HIGHEST_API_VERSION } from '@lwc/shared'; + +// FIXME: Add jsdoc comments to each export explaining what it's used for + +// --- Boolean test flags --- // + +export const LEGACY_BROWSERS = Boolean(process.env.LEGACY_BROWSERS); + +export const DISABLE_SYNTHETIC = Boolean(process.env.DISABLE_SYNTHETIC); + +export const FORCE_NATIVE_SHADOW_MODE_FOR_TEST = Boolean( + process.env.FORCE_NATIVE_SHADOW_MODE_FOR_TEST +); + +export const ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL = Boolean( + process.env.ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL +); + +export const DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER = Boolean( + process.env.DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER +); + +export const DISABLE_STATIC_CONTENT_OPTIMIZATION = Boolean( + process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION +); + +export const ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION = Boolean( + process.env.ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION +); + +export const DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE = Boolean( + process.env.DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE +); + +export const ENGINE_SERVER = Boolean(process.env.ENGINE_SERVER); + +// --- Test config --- // + +export const API_VERSION = process.env.API_VERSION + ? parseInt(process.env.API_VERSION, 10) + : HIGHEST_API_VERSION; + +export const NODE_ENV_FOR_TEST = process.env.NODE_ENV_FOR_TEST; + +export const GREP = process.env.GREP; + +export const NATIVE_SHADOW = DISABLE_SYNTHETIC || FORCE_NATIVE_SHADOW_MODE_FOR_TEST; + +/** Unique directory name that encodes the flags that the tests were executed with. */ +export const COVERAGE_DIR_FOR_OPTIONS = + Object.entries({ + API_VERSION, + DISABLE_STATIC_CONTENT_OPTIMIZATION, + DISABLE_SYNTHETIC, + DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER, + ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL, + ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION, + FORCE_NATIVE_SHADOW_MODE_FOR_TEST, + LEGACY_BROWSERS, + NODE_ENV_FOR_TEST, + DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE, + ENGINE_SERVER, + }) + .filter(([, val]) => val) + .map(([key, val]) => `${key}=${val}`) + .join('/') || 'no-options'; + +// --- CI config --- // + +export const COVERAGE = Boolean(process.env.COVERAGE); +export const SAUCE_USERNAME = process.env.SAUCE_USERNAME; +export const SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY || process.env.SAUCE_KEY; +export const SAUCE_TUNNEL_ID = process.env.SAUCE_TUNNEL_ID; +export const IS_CI = Boolean(process.env.IS_CI); +export const GITHUB_RUN_ID = process.env.GITHUB_RUN_ID; diff --git a/packages/@lwc/integration-not-karma/helpers/setup.mjs b/packages/@lwc/integration-not-karma/helpers/setup.mjs new file mode 100644 index 0000000000..3fdbd96975 --- /dev/null +++ b/packages/@lwc/integration-not-karma/helpers/setup.mjs @@ -0,0 +1,85 @@ +// This import ensures that the global `Mocha` object is present for mutation. +import { JestAsymmetricMatchers, JestChaiExpect, JestExtend } from '@vitest/expect'; +import * as chai from 'chai'; +import * as LWC from 'lwc'; +import { spyOn, fn } from '@vitest/spy'; +import { registerCustomMatchers } from './matchers.mjs'; + +// allows using expect.extend instead of chai.use to extend plugins +chai.use(JestExtend); +// adds all jest matchers to expect +chai.use(JestChaiExpect); +// adds asymmetric matchers like stringContaining, objectContaining +chai.use(JestAsymmetricMatchers); +// add our custom matchers +chai.use(registerCustomMatchers); + +// expose so we don't need to import `expect` in every test file +globalThis.expect = chai.expect; +// Expose globals for karma compat +globalThis.LWC = LWC; +globalThis.spyOn = spyOn; +globalThis.jasmine = { + any: () => { + throw new Error(`TODO: jasmine.any`); + }, + arrayWithExactContents: () => { + throw new Error('TODO: jasmine.arrayWithExactContents'); + }, + createSpy: (name, impl) => { + const spy = fn(impl); + // Bridge for jasmine + spy.calls = { + count() { + return spy.mock.calls.length; + }, + reset() { + spy.mockReset(); + }, + }; + return spy; + }, + objectContaining: expect.objectContaining, +}; + +/** + * `@web/test-runner-mocha`'s autorun.js file inlines its own copy of mocha, and there's no direct + * way to modify the globals before the tests are executed. As a workaround, we predefine setters + * that modify the provided values when they get set by the autorun script. + * @param {string} name Global variable name + * @param {Function} replacer Function that takes the original value and returns a modified one + */ +function hijackGlobal(name, replacer) { + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: true, + set(original) { + Object.defineProperty(globalThis, name, { + configurable: true, + enumerable: true, + writable: true, + value: replacer(original) ?? original, + }); + }, + }); +} + +hijackGlobal('describe', (describe) => { + describe.runIf = (condition) => (condition ? globalThis.describe : globalThis.xdescribe); + describe.skipIf = (condition) => (condition ? globalThis.xdescribe : globalThis.describe); +}); + +hijackGlobal('it', (it) => { + it.runIf = (condition) => (condition ? globalThis.it : globalThis.xit); + it.skipIf = (condition) => (condition ? globalThis.xit : globalThis.it); +}); + +hijackGlobal('before', (before) => { + // Expose as an alias for migration + globalThis.beforeAll = before; +}); + +hijackGlobal('after', (after) => { + // Expose as an alias for migration + globalThis.afterAll = after; +}); diff --git a/packages/@lwc/integration-not-karma/helpers/wtr-utils.mjs b/packages/@lwc/integration-not-karma/helpers/wtr-utils.mjs new file mode 100644 index 0000000000..44cf01e14c --- /dev/null +++ b/packages/@lwc/integration-not-karma/helpers/wtr-utils.mjs @@ -0,0 +1,535 @@ +/* + * Copyright (c) 2024, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +import * as LWC from 'lwc'; + +// TODO [#869]: Replace this custom spy with standard spyOn jasmine spy when logWarning doesn't use console.group +// anymore. On IE11 console.group has a different behavior when the F12 inspector is attached to the page. +function spyConsole() { + const originalConsole = window.console; + const calls = { + log: [], + warn: [], + error: [], + group: [], + groupEnd: [], + }; + + window.console = { + log: function () { + calls.log.push(Array.prototype.slice.call(arguments)); + }, + warn: function () { + calls.warn.push(Array.prototype.slice.call(arguments)); + }, + error: function () { + calls.error.push(Array.prototype.slice.call(arguments)); + }, + group: function () { + calls.group.push(Array.prototype.slice.call(arguments)); + }, + groupEnd: function () { + calls.groupEnd.push(Array.prototype.slice.call(arguments)); + }, + }; + + return { + calls: calls, + reset: function () { + window.console = originalConsole; + }, + }; +} + +// Listen for errors thrown directly by the callback +function directErrorListener(callback) { + try { + callback(); + } catch (error) { + return error; + } +} + +// Listen for errors using window.addEventListener('error') +function windowErrorListener(callback) { + let error; + function onError(event) { + event.preventDefault(); // don't log the error + error = event.error; + } + + // Prevent jasmine from handling the global error. There doesn't seem to be another + // way to disable this behavior: https://github.com/jasmine/jasmine/pull/1860 + const originalOnError = window.onerror; + window.onerror = null; + window.addEventListener('error', onError); + + try { + callback(); + } finally { + window.onerror = originalOnError; + window.removeEventListener('error', onError); + } + return error; +} + +// For errors we expect to be thrown in the connectedCallback() phase +// of a custom element, there are two possibilities: +// 1) We're using non-native lifecycle callbacks, so the error is thrown synchronously +// 2) We're using native lifecycle callbacks, so the error is thrown asynchronously and can +// only be caught with window.addEventListener('error') +// - Note native lifecycle callbacks are all thrown asynchronously. +function customElementCallbackReactionErrorListener(callback) { + return lwcRuntimeFlags.DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE + ? directErrorListener(callback) + : windowErrorListener(callback); +} + +/** + * + * @param dispatcher + * @param runtimeEvents List of runtime events to filter by. If no list is provided, all events will be dispatched. + */ +function attachReportingControlDispatcher(dispatcher, runtimeEvents) { + LWC.__unstable__ReportingControl.attachDispatcher((eventName, payload) => { + if (!runtimeEvents || runtimeEvents.includes(eventName)) { + dispatcher(eventName, payload); + } + }); +} + +function detachReportingControlDispatcher() { + LWC.__unstable__ReportingControl.detachDispatcher(); +} + +function extractDataIds(root) { + const nodes = {}; + + function processElement(elm) { + if (elm.hasAttribute('data-id')) { + nodes[elm.getAttribute('data-id')] = elm; + } + + if (elm.shadowRoot) { + Object.assign(nodes, extractShadowDataIds(elm.shadowRoot)); + } + } + + function acceptNode() { + return NodeFilter.FILTER_ACCEPT; + } + + // Work around Internet Explorer wanting a function instead of an object. IE also *requires* this argument where + // other browsers don't. + const safeFilter = acceptNode; + safeFilter.acceptNode = acceptNode; + + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, safeFilter, false); + + processElement(root); + + let elm; + while ((elm = walker.nextNode())) { + processElement(elm); + } + + return nodes; +} + +function extractShadowDataIds(shadowRoot) { + const nodes = {}; + + // Add the shadow root here even if they don't have [data-id] attributes. This reference is + // subsequently used to add event listeners. + const dataId = shadowRoot.host.getAttribute('data-id'); + if (dataId) { + nodes[dataId + '.shadowRoot'] = shadowRoot; + } + + // We can't use a TreeWalker directly on the ShadowRoot since with synthetic shadow the ShadowRoot is not an + // actual DOM nodes. So we need to iterate over the children manually and run the tree walker on each child. + for (let i = 0; i < shadowRoot.childNodes.length; i++) { + const child = shadowRoot.childNodes[i]; + Object.assign(nodes, extractDataIds(child)); + } + + return nodes; +} + +let register = {}; +function load(id) { + return Promise.resolve(register[id]); +} + +function registerForLoad(name, Ctor) { + register[name] = Ctor; +} +function clearRegister() { + register = {}; +} + +// #986 - childNodes on the host element returns a fake shadow comment node on IE11 for debugging purposes. This method +// filters this node. +function getHostChildNodes(host) { + return Array.prototype.slice.call(host.childNodes).filter(function (n) { + return !(n.nodeType === Node.COMMENT_NODE && n.tagName.startsWith('#shadow-root')); + }); +} + +function isSyntheticShadowRootInstance(sr) { + return Boolean(sr && sr.synthetic); +} + +function isNativeShadowRootInstance(sr) { + return Boolean(sr && !sr.synthetic); +} + +// Providing overridable hooks for tests +let sanitizeHtmlContentHook = function () { + throw new Error('sanitizeHtmlContent hook must be implemented.'); +}; + +LWC.setHooks({ + sanitizeHtmlContent: function (content) { + return sanitizeHtmlContentHook(content); + }, +}); + +function getHooks() { + return { + sanitizeHtmlContent: sanitizeHtmlContentHook, + }; +} + +function setHooks(hooks) { + if (hooks.sanitizeHtmlContent) { + sanitizeHtmlContentHook = hooks.sanitizeHtmlContent; + } +} + +// This mapping should be kept up-to-date with the mapping in @lwc/shared -> aria.ts +const ariaPropertiesMapping = { + ariaAutoComplete: 'aria-autocomplete', + ariaChecked: 'aria-checked', + ariaCurrent: 'aria-current', + ariaDisabled: 'aria-disabled', + ariaExpanded: 'aria-expanded', + ariaHasPopup: 'aria-haspopup', + ariaHidden: 'aria-hidden', + ariaInvalid: 'aria-invalid', + ariaLabel: 'aria-label', + ariaLevel: 'aria-level', + ariaMultiLine: 'aria-multiline', + ariaMultiSelectable: 'aria-multiselectable', + ariaOrientation: 'aria-orientation', + ariaPressed: 'aria-pressed', + ariaReadOnly: 'aria-readonly', + ariaRequired: 'aria-required', + ariaSelected: 'aria-selected', + ariaSort: 'aria-sort', + ariaValueMax: 'aria-valuemax', + ariaValueMin: 'aria-valuemin', + ariaValueNow: 'aria-valuenow', + ariaValueText: 'aria-valuetext', + ariaLive: 'aria-live', + ariaRelevant: 'aria-relevant', + ariaAtomic: 'aria-atomic', + ariaBusy: 'aria-busy', + ariaActiveDescendant: 'aria-activedescendant', + ariaControls: 'aria-controls', + ariaDescribedBy: 'aria-describedby', + ariaFlowTo: 'aria-flowto', + ariaLabelledBy: 'aria-labelledby', + ariaOwns: 'aria-owns', + ariaPosInSet: 'aria-posinset', + ariaSetSize: 'aria-setsize', + ariaColCount: 'aria-colcount', + ariaColSpan: 'aria-colspan', + ariaColIndex: 'aria-colindex', + ariaColIndexText: 'aria-colindextext', + ariaDescription: 'aria-description', + ariaDetails: 'aria-details', + ariaErrorMessage: 'aria-errormessage', + ariaKeyShortcuts: 'aria-keyshortcuts', + ariaModal: 'aria-modal', + ariaPlaceholder: 'aria-placeholder', + ariaRoleDescription: 'aria-roledescription', + ariaRowCount: 'aria-rowcount', + ariaRowIndex: 'aria-rowindex', + ariaRowIndexText: 'aria-rowindextext', + ariaRowSpan: 'aria-rowspan', + ariaBrailleLabel: 'aria-braillelabel', + ariaBrailleRoleDescription: 'aria-brailleroledescription', + role: 'role', +}; + +// See the README for @lwc/aria-reflection +const nonStandardAriaProperties = [ + 'ariaActiveDescendant', + 'ariaControls', + 'ariaDescribedBy', + 'ariaDetails', + 'ariaErrorMessage', + 'ariaFlowTo', + 'ariaLabelledBy', + 'ariaOwns', +]; + +// These properties are not included in the global polyfill, but were added to LightningElement/BridgeElement +// prototypes in https://github.com/salesforce/lwc/pull/3702 +const nonPolyfilledAriaProperties = [ + 'ariaColIndexText', + 'ariaBrailleLabel', + 'ariaBrailleRoleDescription', + 'ariaDescription', + 'ariaRowIndexText', +]; + +const ariaProperties = Object.keys(ariaPropertiesMapping); +const ariaAttributes = Object.values(ariaPropertiesMapping); + +// Keep traversing up the prototype chain until a property descriptor is found +function getPropertyDescriptor(object, prop) { + do { + const descriptor = Object.getOwnPropertyDescriptor(object, prop); + if (descriptor) { + return descriptor; + } + object = Object.getPrototypeOf(object); + } while (object); +} + +const IS_SYNTHETIC_SHADOW_LOADED = !`${ShadowRoot}`.includes('[native code]'); + +// Designed for hydration tests, this helper asserts certain error/warn console messages were logged +function createExpectConsoleCallsFunc(devOnly) { + return (consoleCalls, methods) => { + for (const [method, matchers] of Object.entries(methods)) { + const calls = consoleCalls[method]; + if (devOnly && process.env.NODE_ENV === 'production') { + // assume no console errors/warnings in production + expect(calls).toHaveSize(0); + } else { + expect(calls).toHaveSize(matchers.length); + for (let i = 0; i < matchers.length; i++) { + const matcher = matchers[i]; + const args = calls[i]; + const argsString = args.map((arg) => stringifyArg(arg)).join(' '); + expect(argsString).toMatch(matcher); + } + } + } + }; +} + +// Browsers render nodes differently (class order, etc). +function stringifyArg(arg) { + if (arg instanceof Array) { + return arg.map((v) => stringifyArg(v)); + } else if (arg?.tagName) { + return `<${arg.tagName.toLowerCase()}>`; + } else if (arg?.nodeName) { + return arg.nodeName; + } else if (typeof arg === 'string') { + // Avoids adding newlines in the matchers + return arg.replaceAll('\n', ''); + } else { + return arg; + } +} + +const expectConsoleCalls = createExpectConsoleCallsFunc(false); +const expectConsoleCallsDev = createExpectConsoleCallsFunc(true); + +// Utility to handle unhandled rejections or errors without allowing Jasmine to handle them first. +// Captures both onunhandledrejection and onerror events, since you might want both depending on +// native vs synthetic lifecycle timing differences. +function catchUnhandledRejectionsAndErrors(onUnhandledRejectionOrError) { + let originalOnError; + + const onError = (e) => { + e.preventDefault(); // Avoids logging to the console + onUnhandledRejectionOrError(e); + }; + + const onRejection = (e) => { + // Avoids logging the error to the console, except in Firefox sadly https://bugzilla.mozilla.org/1642147 + e.preventDefault(); + onUnhandledRejectionOrError(e.reason); + }; + + beforeEach(() => { + // Overriding window.onerror disables Jasmine's global error handler, so we can listen for errors + // ourselves. There doesn't seem to be a better way to disable Jasmine's behavior here. + // https://github.com/jasmine/jasmine/pull/1860 + originalOnError = window.onerror; + // Dummy onError because Jasmine tries to call it in case of a rejection: + // https://github.com/jasmine/jasmine/blob/169a2a8/src/core/GlobalErrors.js#L104-L106 + window.onerror = () => {}; + window.addEventListener('error', onError); + window.addEventListener('unhandledrejection', onRejection); + }); + + afterEach(() => { + window.removeEventListener('error', onError); + window.removeEventListener('unhandledrejection', onRejection); + window.onerror = originalOnError; + }); +} + +// Succeeds if the given DOM element is equivalent to the given HTML in terms of nodes and elements. This is +// basically the same as `expect(element.outerHTML).toBe(html)` except that it works despite bugs in synthetic shadow. +function expectEquivalentDOM(element, html) { + const fragment = Document.parseHTMLUnsafe(html); + + // When the fragment is parsed, the string "abc" is considered one text node. Whereas the engine + // may have produced it as three adjacent text nodes: "a", "b", "c". We want to consider these equivalent + // for the purposes of diffing + function concatenateAdjacentTextNodes(nodes) { + const result = []; + for (const node of nodes) { + const lastNode = result[result.length - 1]; + if (node.nodeType === Node.TEXT_NODE && lastNode?.nodeType === Node.TEXT_NODE) { + const newLastNode = (result[result.length - 1] = lastNode.cloneNode(true)); + newLastNode.nodeValue += node.nodeValue; + } else { + result.push(node); + } + } + return result; + } + + function expectEquivalent(a, b) { + if (!a || !b) { + // null/undefined + expect(a).toBe(b); + return; + } + + expect(a.tagName).toBe(b.tagName); + expect(a.nodeType).toBe(b.nodeType); + if (a.nodeType === Node.TEXT_NODE || a.nodeType === Node.COMMENT_NODE) { + expect(a.textContent).toBe(b.textContent); + } + + // attrs + if (a.nodeType === Node.ELEMENT_NODE && b.nodeType === Node.ELEMENT_NODE) { + expect(a.attributes.length).toBe(b.attributes.length); + for (const { name, value } of a.attributes) { + expect(b.getAttribute(name)).toBe(value); + } + } + + // child nodes (recursive) + const aChildNodes = concatenateAdjacentTextNodes(a.childNodes); + const bChildNodes = concatenateAdjacentTextNodes(b.childNodes); + expect(aChildNodes.length).toBe(bChildNodes.length); + for (let i = 0; i < aChildNodes.length; i++) { + expectEquivalent(aChildNodes[i], bChildNodes[i]); + } + + // shadow root (recursive) + expectEquivalent(a.shadowRoot, b.shadowRoot); + } + + expect(fragment.body.childNodes.length).toBe(1); // only supports one top-level element + + expectEquivalent(element, fragment.body.firstChild); +} + +// These values are based on the API versions in @lwc/shared/api-version +const LOWERCASE_SCOPE_TOKENS = process.env.API_VERSION >= 59, + USE_COMMENTS_FOR_FRAGMENT_BOOKENDS = process.env.API_VERSION >= 60, + USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS = process.env.API_VERSION >= 60, + DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION = process.env.API_VERSION >= 60, + ENABLE_ELEMENT_INTERNALS_AND_FACE = process.env.API_VERSION >= 61, + USE_LIGHT_DOM_SLOT_FORWARDING = process.env.API_VERSION >= 61, + ENABLE_THIS_DOT_HOST_ELEMENT = process.env.API_VERSION >= 62, + ENABLE_THIS_DOT_STYLE = process.env.API_VERSION >= 62, + TEMPLATE_CLASS_NAME_OBJECT_BINDING = process.env.API_VERSION >= 62; + +const signalValidator = new WeakSet(); +LWC.setTrustedSignalSet(signalValidator); + +function addTrustedSignal(signal) { + signalValidator.add(signal); +} + +export { + clearRegister, + extractDataIds, + extractShadowDataIds, + getHostChildNodes, + isNativeShadowRootInstance, + isSyntheticShadowRootInstance, + load, + registerForLoad, + getHooks, + setHooks, + spyConsole, + customElementCallbackReactionErrorListener, + ariaPropertiesMapping, + ariaProperties, + ariaAttributes, + nonStandardAriaProperties, + nonPolyfilledAriaProperties, + getPropertyDescriptor, + attachReportingControlDispatcher, + detachReportingControlDispatcher, + IS_SYNTHETIC_SHADOW_LOADED, + expectConsoleCalls, + expectConsoleCallsDev, + catchUnhandledRejectionsAndErrors, + addTrustedSignal, + expectEquivalentDOM, + LOWERCASE_SCOPE_TOKENS, + USE_COMMENTS_FOR_FRAGMENT_BOOKENDS, + USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS, + DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION, + ENABLE_ELEMENT_INTERNALS_AND_FACE, + USE_LIGHT_DOM_SLOT_FORWARDING, + ENABLE_THIS_DOT_HOST_ELEMENT, + ENABLE_THIS_DOT_STYLE, + TEMPLATE_CLASS_NAME_OBJECT_BINDING, +}; + +window.TestUtils = { + clearRegister, + extractDataIds, + extractShadowDataIds, + getHostChildNodes, + isNativeShadowRootInstance, + isSyntheticShadowRootInstance, + load, + registerForLoad, + getHooks, + setHooks, + spyConsole, + customElementCallbackReactionErrorListener, + ariaPropertiesMapping, + ariaProperties, + ariaAttributes, + nonStandardAriaProperties, + nonPolyfilledAriaProperties, + getPropertyDescriptor, + attachReportingControlDispatcher, + detachReportingControlDispatcher, + IS_SYNTHETIC_SHADOW_LOADED, + expectConsoleCalls, + expectConsoleCallsDev, + catchUnhandledRejectionsAndErrors, + addTrustedSignal, + expectEquivalentDOM, + LOWERCASE_SCOPE_TOKENS, + USE_COMMENTS_FOR_FRAGMENT_BOOKENDS, + USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS, + DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION, + ENABLE_ELEMENT_INTERNALS_AND_FACE, + USE_LIGHT_DOM_SLOT_FORWARDING, + ENABLE_THIS_DOT_HOST_ELEMENT, + ENABLE_THIS_DOT_STYLE, + TEMPLATE_CLASS_NAME_OBJECT_BINDING, +}; diff --git a/packages/@lwc/integration-not-karma/package.json b/packages/@lwc/integration-not-karma/package.json new file mode 100644 index 0000000000..c9d5208974 --- /dev/null +++ b/packages/@lwc/integration-not-karma/package.json @@ -0,0 +1,24 @@ +{ + "name": "@lwc/integration-not-karma", + "private": true, + "version": "8.20.0", + "scripts": { + "start": "web-test-runner --manual", + "test": "web-test-runner" + }, + "devDependencies": { + "@lwc/compiler": "8.20.0", + "@lwc/engine-dom": "8.20.0", + "@lwc/engine-server": "8.20.0", + "@lwc/rollup-plugin": "8.20.0", + "@lwc/synthetic-shadow": "8.20.0", + "@types/chai": "^5.2.2", + "@types/jasmine": "^5.1.7", + "@web/dev-server-rollup": "^0.6.4", + "@web/test-runner": "^0.20.1", + "chai": "^5.2.0" + }, + "volta": { + "extends": "../../../package.json" + } +} diff --git a/packages/@lwc/integration-not-karma/test b/packages/@lwc/integration-not-karma/test new file mode 120000 index 0000000000..ecd98dd932 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test @@ -0,0 +1 @@ +../integration-karma/test \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/web-test-runner.config.mjs b/packages/@lwc/integration-not-karma/web-test-runner.config.mjs new file mode 100644 index 0000000000..311746046b --- /dev/null +++ b/packages/@lwc/integration-not-karma/web-test-runner.config.mjs @@ -0,0 +1,166 @@ +import { LWC_VERSION } from '@lwc/shared'; +import customRollup from './helpers/lwc.mjs'; +import * as options from './helpers/options.mjs'; + +const pluck = (obj, keys) => Object.fromEntries(keys.map((k) => [k, Boolean(obj[k])])); + +/** `process.env` to inject into test environment. */ +const env = { + ...pluck(options, [ + 'API_VERSION', + 'DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE', + 'DISABLE_STATIC_CONTENT_OPTIMIZATION', + 'DISABLE_SYNTHETIC', + 'ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL', + 'ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION', + 'ENGINE_SERVER', + 'FORCE_NATIVE_SHADOW_MODE_FOR_TEST', + 'NATIVE_SHADOW', + ]), + LWC_VERSION, + NODE_ENV: options.NODE_ENV_FOR_TEST || 'development', +}; +/** @type {import("@web/test-runner").TestRunnerConfig} */ +export default { + files: [ + // FIXME: These tests are just symlinks to integration-karma for now so the git diff smaller + 'test/**/*.spec.js', + // Failing Karma tests that need to be migrated to WTR + '!test/accessibility/synthetic-cross-root-aria/index.spec.js', + '!test/api/CustomElementConstructor-getter/index.spec.js', + '!test/api/freezeTemplate/index.spec.js', + '!test/api/getComponentDef/index.spec.js', + '!test/api/isNodeFromTemplate/index.spec.js', + '!test/api/readonly/index.spec.js', + '!test/api/registerTemplate/index.spec.js', + '!test/api/sanitizeAttribute/index.spec.js', + '!test/api/sanitizeHtmlContent/index.spec.js', + '!test/component/face-callbacks/index.spec.js', + '!test/component/lifecycle-callbacks/index.spec.js', + '!test/component/LightningElement.addEventListener/index.spec.js', + '!test/component/LightningElement.attachInternals/api/index.spec.js', + '!test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js', + '!test/component/LightningElement.errorCallback/index.spec.js', + '!test/component/LightningElement.hostElement/index.spec.js', + '!test/component/LightningElement.render/index.spec.js', + '!test/component/LightningElement.style/index.spec.js', + '!test/component/LightningElement/index.spec.js', + '!test/component/native-vs-synthetic-lifecycle/index.spec.js', + '!test/custom-elements-registry/index.spec.js', + '!test/events/focus-event-related-target/index.spec.js', + '!test/integrations/locker/index.spec.js', + '!test/light-dom/host-pseudo/index.spec.js', + '!test/light-dom/ids/index.spec.js', + '!test/light-dom/lifecycle/index.spec.js', + '!test/light-dom/multiple-templates/index.spec.js', + '!test/light-dom/scoped-slot/if-block/index.spec.js', + '!test/light-dom/scoped-slot/index.spec.js', + '!test/light-dom/scoped-slot/runtime-checks/index.spec.js', + '!test/light-dom/scoped-styles/index.spec.js', + '!test/light-dom/slot-fowarding/slots/duplicates/index.spec.js', + '!test/light-dom/slot-fowarding/slots/forwarding/index.spec.js', + '!test/light-dom/slot-fowarding/slots/reactivity/index.spec.js', + '!test/light-dom/slotting/index.spec.js', + '!test/light-dom/style-global/index.spec.js', + '!test/light-dom/synthetic-shadow-styles/index.spec.js', + '!test/misc/clean-dom/index.spec.js', + '!test/misc/object-rest-spread/index.spec.js', + '!test/mixed-shadow-mode/composed-path/index.spec.js', + '!test/mixed-shadow-mode/reporting/index.spec.js', + '!test/mixed-shadow-mode/synthetic-behavior/index.spec.js', + '!test/native-shadow/Event-methods/Event.composedPath.spec.js', + '!test/polyfills/document-body-properties/index.spec.js', + '!test/polyfills/document-properties/index.spec.js', + '!test/profiler/mutation-logging/index.spec.js', + '!test/profiler/sanity/profiler.spec.js', + '!test/regression/invalid-key/index.spec.js', + '!test/rendering/callback-invocation-order/index.spec.js', + '!test/rendering/elements-are-not-recycled/index.spec.js', + '!test/rendering/fragment-cache/index.spec.js', + '!test/rendering/iframe/index.spec.js', + '!test/rendering/inner-outer-html/index.spec.js', + '!test/rendering/legacy-scope-tokens/index.spec.js', + '!test/rendering/native-only-css/index.spec.js', + '!test/rendering/programmatic-stylesheets/index.spec.js', + '!test/rendering/sanitize-stylesheet-token/index.spec.js', + '!test/rendering/scoped-styles-with-existing-class/index.spec.js', + '!test/rendering/slot-not-at-top-level/element/light/index.spec.js', + '!test/rendering/slot-not-at-top-level/element/shadow/index.spec.js', + '!test/rendering/slot-not-at-top-level/external/light/index.spec.js', + '!test/rendering/slot-not-at-top-level/external/shadow/index.spec.js', + '!test/rendering/slot-not-at-top-level/ifTrue/shadow/index.spec.js', + '!test/rendering/slot-not-at-top-level/lwcIf/light/index.spec.js', + '!test/rendering/slot-not-at-top-level/lwcIf/shadow/index.spec.js', + '!test/rendering/slotting/index.spec.js', + '!test/rendering/stylesheet-caching/index.spec.js', + '!test/rendering/version-mismatch/index.spec.js', + '!test/shadow-dom/event-in-shadow-tree/propagation.spec.js', + '!test/shadow-dom/Event-properties/Event.target.spec.js', + '!test/shadow-dom/multiple-templates/index.spec.js', + '!test/shadow-dom/MutationObserver/MutationObserver.spec.js', + '!test/shadow-dom/Node-properties/Node.childNodes.spec.js', + '!test/shadow-dom/Node-properties/Node.hasChildNodes.spec.js', + '!test/shadow-dom/Node-properties/Node.textContent.spec.js', + '!test/shadow-dom/ShadowRoot-properties/ShadowRoot.spec.js', + '!test/shadow-dom/ShadowRoot.elementsFromPoint/index.spec.js', + '!test/rendering/slot-not-at-top-level/ifTrue/light/index.spec.js', + '!test/signal/protocol/index.spec.js', + '!test/spread/index.spec.js', + '!test/static-content/index.spec.js', + '!test/swapping/styles/index.spec.js', + '!test/synthetic-shadow/active-element/index.spec.js', + '!test/synthetic-shadow/dom-manual-sharing-nodes/index.spec.js', + '!test/synthetic-shadow/element-api/element-api.spec.js', + '!test/synthetic-shadow/global-styles/index.spec.js', + '!test/synthetic-shadow/host-pseudo/index.spec.js', + '!test/synthetic-shadow/inner-outer-text/inner-outer-text.spec.js', + '!test/synthetic-shadow/scoped-id/multiple-idrefs.spec.js', + '!test/synthetic-shadow/style-svg/index.spec.js', + '!test/template/attribute-class/object-values.spec.js', + '!test/template/directive-for-each/index.spec.js', + '!test/template/directive-if/index.spec.js', + '!test/template/directive-lwc-render-mode/index.spec.js', + '!test/wire/legacy-adapters/index.spec.js', + '!test/wire/reactive-params.spec.js', + '!test/wire/wirecontextevent-legacy/index.spec.js', + ], + nodeResolve: true, + rootDir: import.meta.dirname, + plugins: [ + { + resolveImport({ source }) { + if (source === 'test-utils') { + return '/helpers/wtr-utils.mjs'; + } else if (source === 'wire-service') { + return '@lwc/wire-service'; + } + }, + async serve(ctx) { + if (ctx.path.endsWith('.spec.js')) { + return await customRollup(ctx); + } + }, + async transform(ctx) { + if (ctx.type === 'application/javascript') { + // FIXME: copy/paste Nolan's spiel about why we do this ugly thing + return ctx.body.replace(/process\.env\.NODE_ENV === 'test-karma-lwc'/g, 'true'); + } + }, + }, + ], + testRunnerHtml: (testFramework) => + ` + + + + + + + + `, +}; diff --git a/packages/@lwc/template-compiler/src/parser/expression-complex/html.ts b/packages/@lwc/template-compiler/src/parser/expression-complex/html.ts index 9b4dcb7580..a6d6559b9c 100644 --- a/packages/@lwc/template-compiler/src/parser/expression-complex/html.ts +++ b/packages/@lwc/template-compiler/src/parser/expression-complex/html.ts @@ -279,7 +279,7 @@ class TemplateHtmlParser extends Parser { // that, we create a new text node for the template expression rather than // allowing the concatenation to proceed. _insertCharacters(token: Token.CharacterToken) { - const parentNode = this.openElements.current; + const parentNode = this.openElements.current!; const previousPeer = parentNode.childNodes.at(-1); const html = this.tokenizer.preprocessor.html; if ( diff --git a/vitest.config.mjs b/vitest.config.mjs index 29d3e60d3d..1768ba52d3 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -26,6 +26,7 @@ export default defineConfig({ '**/packages/@lwc/ssr-client-utils/**', // Ignore test packages '**/packages/@lwc/integration-karma/**', + '**/packages/@lwc/integration-not-karma/**', '**/packages/@lwc/integration-tests/**', '**/packages/@lwc/integration-types/**', '**/packages/@lwc/perf-benchmarks-components/**',