diff --git a/.github/workflows/web-test-runner.yml b/.github/workflows/web-test-runner.yml index 3db9f7dd3b..c6b90eff38 100644 --- a/.github/workflows/web-test-runner.yml +++ b/.github/workflows/web-test-runner.yml @@ -1,4 +1,4 @@ -name: Run Web Test Runner integration tests +name: Web Test Runner on: push: @@ -18,8 +18,10 @@ env: NODE_VERSION: '20.19.4' jobs: + # We run tests in parallel to speed up CI, with an arbitrary goal of ~20 minutes per job. + # Test grouping is also somewhat arbitrary. # TODO: upload result artifacts - # TODO: make it saucy 🥫 + integration-tests: name: Integration tests (${{ matrix.shadow_mode }} shadow) strategy: @@ -28,7 +30,7 @@ jobs: runs-on: ubuntu-22.04 env: - SAUCE_TUNNEL_ID: github-action-tunnel-wtr-${{github.run_id}}-group-1 + SAUCE_TUNNEL_ID: gha-${{github.run_id}}-wtr-integration-${{ matrix.shadow_mode }}-1 SHADOW_MODE_OVERRIDE: ${{ matrix.shadow_mode }} defaults: run: @@ -47,30 +49,68 @@ jobs: 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 + - 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 exec -- playwright install chrome firefox webkit --with-deps - run: yarn test + - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 yarn test || true + - run: DISABLE_STATIC_CONTENT_OPTIMIZATION=1 yarn test + - run: ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL=1 yarn test + - run: NODE_ENV_FOR_TEST=production yarn test + + integration-tests-api-versions: + name: Integration tests (${{ matrix.shadow_mode }} shadow) - API versions + strategy: + matrix: + shadow_mode: [native, synthetic] + + runs-on: ubuntu-22.04 + env: + SAUCE_TUNNEL_ID: gha-${{github.run_id}}-wtr-integration-${{ matrix.shadow_mode }}-2 + SHADOW_MODE_OVERRIDE: ${{ matrix.shadow_mode }} + 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 exec -- playwright install chrome firefox webkit --with-deps - run: API_VERSION=58 yarn test - run: API_VERSION=59 yarn test - run: API_VERSION=60 yarn test - run: API_VERSION=61 yarn test - run: API_VERSION=62 yarn test - - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 yarn test || true - - run: DISABLE_STATIC_CONTENT_OPTIMIZATION=1 yarn test - - run: ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL=1 yarn test - - run: NODE_ENV_FOR_TEST=production yarn test + - run: API_VERSION=66 yarn test integration-tests-not-both-modes: # Tests that should run in only synthetic or native shadow, not both name: Integration tests (singleton batch) runs-on: ubuntu-22.04 env: - SAUCE_TUNNEL_ID: github-action-tunnel-wtr-${{github.run_id}}-group-1 + SAUCE_TUNNEL_ID: gha-${{github.run_id}}-wtr-integration-${{ matrix.shadow_mode }}-3 SHADOW_MODE_OVERRIDE: ${{ matrix.shadow_mode }} defaults: run: @@ -89,13 +129,14 @@ jobs: 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 + - 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 exec -- playwright install chrome firefox webkit --with-deps # Synthetic shadow only - run: LEGACY_BROWSERS=1 yarn test || true - run: FORCE_NATIVE_SHADOW_MODE_FOR_TEST=1 yarn test @@ -106,10 +147,11 @@ jobs: - run: SHADOW_MODE_OVERRIDE=native DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER=1 yarn test - run: SHADOW_MODE_OVERRIDE=native DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER=1 DISABLE_STATIC_CONTENT_OPTIMIZATION=1 yarn test - hydration-tests: + hydration-tests-ssr-v2: + name: Hydration tests (SSR v2) runs-on: ubuntu-22.04 env: - SAUCE_TUNNEL_ID: github-action-tunnel-wtr-${{github.run_id}}-group-1 + SAUCE_TUNNEL_ID: gha-${{github.run_id}}-wtr-hydration-1 defaults: run: working-directory: ./packages/@lwc/integration-not-karma @@ -127,21 +169,56 @@ jobs: 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: ENGINE_SERVER=1 yarn test:hydration - - run: ENGINE_SERVER=1 SHADOW_MODE_OVERRIDE=synthetic yarn test:hydration - - run: ENGINE_SERVER=1 NODE_ENV_FOR_TEST=production yarn test:hydration - - run: ENGINE_SERVER=1 DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 yarn test:hydration - - run: ENGINE_SERVER=1 DISABLE_STATIC_CONTENT_OPTIMIZATION=1 yarn test:hydration - - run: ENGINE_SERVER=1 DISABLE_DETACHED_REHYDRATION=1 yarn test:hydration - - run: ENGINE_SERVER=1 DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 DISABLE_DETACHED_REHYDRATION=1 yarn test:hydration + - 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 exec -- playwright install chrome firefox webkit --with-deps + - run: yarn test:hydration + - run: SHADOW_MODE_OVERRIDE=synthetic yarn test:hydration + - run: NODE_ENV_FOR_TEST=production yarn test:hydration + - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 yarn test:hydration + - run: DISABLE_STATIC_CONTENT_OPTIMIZATION=1 yarn test:hydration + + hydration-tests-engine-server: + name: Hydration tests (engine-server) + runs-on: ubuntu-22.04 + env: + SAUCE_TUNNEL_ID: gha-${{github.run_id}}-wtr-hydration-2 + ENGINE_SERVER: 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 exec -- playwright install chrome firefox webkit --with-deps + # NOTE: All tests have ENGINE_SERVER=1 already set - run: yarn test:hydration - run: SHADOW_MODE_OVERRIDE=synthetic yarn test:hydration - run: NODE_ENV_FOR_TEST=production yarn test:hydration - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 yarn test:hydration - run: DISABLE_STATIC_CONTENT_OPTIMIZATION=1 yarn test:hydration + - run: DISABLE_DETACHED_REHYDRATION=1 yarn test:hydration + - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 DISABLE_DETACHED_REHYDRATION=1 yarn test:hydration diff --git a/packages/@lwc/engine-core/src/libs/signal-tracker/index.ts b/packages/@lwc/engine-core/src/libs/signal-tracker/index.ts index 4a6d04745b..bdabbde6da 100644 --- a/packages/@lwc/engine-core/src/libs/signal-tracker/index.ts +++ b/packages/@lwc/engine-core/src/libs/signal-tracker/index.ts @@ -44,6 +44,19 @@ export function unsubscribeFromSignals(target: object) { type CallbackFunction = () => void; +/** + * A normalized string representation of an error, because browsers behave differently + */ +const errorWithStack = (err: unknown): string => { + if (typeof err !== 'object' || err === null) { + return String(err); + } + const stack = 'stack' in err ? String(err.stack) : ''; + const message = 'message' in err ? String(err.message) : ''; + const constructor = err.constructor.name; + return stack.includes(message) ? stack : `${constructor}: ${message}\n${stack}`; +}; + /** * This class is used to keep track of the signals associated to a given object. * It is used to prevent the LWC engine from subscribing duplicate callbacks multiple times @@ -67,9 +80,9 @@ class SignalTracker { } } catch (err: any) { logWarnOnce( - `Attempted to subscribe to an object that has the shape of a signal but received the following error: ${ - err?.stack ?? err - }` + `Attempted to subscribe to an object that has the shape of a signal but received the following error: ${errorWithStack( + err + )}` ); } } @@ -79,9 +92,9 @@ class SignalTracker { this.signalToUnsubscribeMap.forEach((unsubscribe) => unsubscribe()); } catch (err: any) { logWarnOnce( - `Attempted to call a signal's unsubscribe callback but received the following error: ${ - err?.stack ?? err - }` + `Attempted to call a signal's unsubscribe callback but received the following error: ${errorWithStack( + err + )}` ); } } diff --git a/packages/@lwc/integration-not-karma/configs/hydration.js b/packages/@lwc/integration-not-karma/configs/hydration.js index 1b085ad761..38aa6effe9 100644 --- a/packages/@lwc/integration-not-karma/configs/hydration.js +++ b/packages/@lwc/integration-not-karma/configs/hydration.js @@ -1,5 +1,5 @@ import * as options from '../helpers/options.js'; -import createConfig from './base.js'; +import createConfig from './shared/base-config.js'; import hydrationTestPlugin from './plugins/serve-hydration.js'; const SHADOW_MODE = options.SHADOW_MODE_OVERRIDE ?? 'native'; @@ -12,6 +12,6 @@ const baseConfig = createConfig({ /** @type {import("@web/test-runner").TestRunnerConfig} */ export default { ...baseConfig, - files: ['test-hydration/**/*.spec.js'], + files: ['test-hydration/**/*.spec.js', '!test-hydration/synthetic-shadow/index.spec.js'], plugins: [...baseConfig.plugins, hydrationTestPlugin], }; diff --git a/packages/@lwc/integration-not-karma/configs/integration.js b/packages/@lwc/integration-not-karma/configs/integration.js index f7b2b8618a..162e19cbcc 100644 --- a/packages/@lwc/integration-not-karma/configs/integration.js +++ b/packages/@lwc/integration-not-karma/configs/integration.js @@ -1,6 +1,6 @@ import { importMapsPlugin } from '@web/dev-server-import-maps'; import * as options from '../helpers/options.js'; -import createConfig from './base.js'; +import createConfig from './shared/base-config.js'; import testPlugin from './plugins/serve-integration.js'; const SHADOW_MODE = options.SHADOW_MODE_OVERRIDE ?? 'synthetic'; @@ -13,12 +13,7 @@ const baseConfig = createConfig({ /** @type {import("@web/test-runner").TestRunnerConfig} */ export default { ...baseConfig, - files: [ - 'test/**/*.spec.js', - // Make John fix this after his PR is merged - '!test/template-expressions/errors/index.spec.js', - '!test/template-expressions/smoke-test/index.spec.js', - ], + files: ['test/**/*.spec.js', '!test/custom-elements/index.spec.js'], plugins: [ ...baseConfig.plugins, importMapsPlugin({ inject: { importMap: { imports: { lwc: './mocks/lwc.js' } } } }), diff --git a/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.js b/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.js index bdbda892f0..a33054fed3 100644 --- a/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.js +++ b/packages/@lwc/integration-not-karma/configs/plugins/serve-hydration.js @@ -1,20 +1,16 @@ import path from 'node:path'; import vm from 'node:vm'; import { fileURLToPath } from 'node:url'; +import { readFileSync } from 'node:fs'; import { rollup } from 'rollup'; import lwcRollupPlugin from '@lwc/rollup-plugin'; import { DISABLE_STATIC_CONTENT_OPTIMIZATION, ENGINE_SERVER } from '../../helpers/options.js'; -/** LWC SSR module to use when server-side rendering components. */ -const lwcSsr = await (ENGINE_SERVER - ? // Using import('literal') rather than import(variable) so static analysis tools work - import('@lwc/engine-server') - : import('@lwc/ssr-runtime')); -lwcSsr.setHooks({ - sanitizeHtmlContent(content) { - return content; - }, -}); +/** Code for the LWC SSR module. */ +const LWC_SSR = readFileSync( + new URL(import.meta.resolve(ENGINE_SERVER ? '@lwc/engine-server' : '@lwc/ssr-runtime')), + 'utf8' +); const ROOT_DIR = path.join(import.meta.dirname, '../..'); const COMPONENT_NAME = 'x-main'; @@ -69,16 +65,27 @@ async function compileModule(input, targetSSR, format) { */ async function getSsrMarkup(componentEntrypoint, configPath) { const componentIife = await compileModule(componentEntrypoint, !ENGINE_SERVER, 'iife'); - // To minimize the amount of code in the generated script, ideally we'd do `import Component` - // and delegate the bundling to the loader. However, that's complicated to configure and using - // imports with vm.Script/vm.Module is still experimental, so we use an IIFE for simplicity. - // Additionally, we could import LWC, but the framework requires configuration before each test - // (setHooks/setFeatureFlagForTest), so instead we configure it once in the top-level context - // and inject it as a global variable. + // Ideally, we'd be able to do `import Component` and delegate bundling to the loader. We also + // need each import of LWC to be isolated, but by all server-side imports share a global state. + // We could solve this with the right `vm.Script`/`vm.Module` setup, but that's complicated and + // still experimental. Therefore, we just inline everything. const script = new vm.Script( `(async () => { - const {default: config} = await import('./${configPath}'); - ${componentIife /* var Component = ... */} + // node.js / CommonJS setup + const process = { env: ${JSON.stringify(process.env)} }; + const exports = Object.create(null); + const LWC = exports; + + // LWC / test setup + ${LWC_SSR}; + LWC.setHooks({ sanitizeHtmlContent: (v) => v }); + const { default: config } = await import('./${configPath}'); + config.requiredFeatureFlags?.forEach(ff => { + LWC.setFeatureFlagForTest(ff, true); + }); + + // Component code + ${componentIife}; return LWC.renderComponent( '${COMPONENT_NAME}', Component, @@ -88,12 +95,12 @@ async function getSsrMarkup(componentEntrypoint, configPath) { ); })()`, { - filename: `[SSR] ${configPath}`, + filename: `(virtual SSR file for) ${configPath}`, importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, } ); - return await script.runInContext(vm.createContext({ LWC: lwcSsr })); + return await script.runInNewContext(); } /** @@ -101,31 +108,19 @@ async function getSsrMarkup(componentEntrypoint, configPath) { * This function wraps those configs in the test code to be executed. */ async function wrapHydrationTest(configPath) { - const { default: config } = await import(path.join(ROOT_DIR, configPath)); + const suiteDir = path.dirname(configPath); + const componentEntrypoint = path.join(suiteDir, COMPONENT_ENTRYPOINT); + const ssrOutput = await getSsrMarkup(componentEntrypoint, configPath); - try { - config.requiredFeatureFlags?.forEach((featureFlag) => { - lwcSsr.setFeatureFlagForTest(featureFlag, true); - }); - - const suiteDir = path.dirname(configPath); - const componentEntrypoint = path.join(suiteDir, COMPONENT_ENTRYPOINT); - const ssrOutput = await getSsrMarkup(componentEntrypoint, configPath); - - return ` - import * as LWC from 'lwc'; - import { runTest } from '/configs/plugins/test-hydration.js'; - runTest( - '/${configPath}?original=1', - '/${componentEntrypoint}', - ${JSON.stringify(ssrOutput) /* escape quotes */} - ); - `; - } finally { - config.requiredFeatureFlags?.forEach((featureFlag) => { - lwcSsr.setFeatureFlagForTest(featureFlag, false); - }); - } + return ` + import * as LWC from 'lwc'; + import { runTest } from '/configs/plugins/test-hydration.js'; + runTest( + '/${configPath}?original=1', + '/${componentEntrypoint}', + ${JSON.stringify(ssrOutput) /* escape quotes */} + ); + `; } /** @type {import('@web/dev-server-core').Plugin} */ diff --git a/packages/@lwc/integration-not-karma/configs/plugins/serve-integration.js b/packages/@lwc/integration-not-karma/configs/plugins/serve-integration.js index 18890b98f3..8c25cc2e91 100644 --- a/packages/@lwc/integration-not-karma/configs/plugins/serve-integration.js +++ b/packages/@lwc/integration-not-karma/configs/plugins/serve-integration.js @@ -41,30 +41,35 @@ const createRollupPlugin = (input, options) => { const transform = async (ctx) => { const input = ctx.path.slice(1); // strip leading / from URL path to get relative file path - const defaultRollupPlugin = createRollupPlugin(input); + // Override the LWC rollup plugin config on a per-file basis by searching for a comment + // directive /*!WTR {...}*/ and parsing the content as JSON. The spec file acts as a default + // location to update the config for every component file. + let rootConfig = {}; + const configDirective = /(?:\/\*|)/s; + const parseConfig = (src, id) => { + const configStr = src.match(configDirective)?.[1]; + if (!configStr) { + return rootConfig; // default config if no overrides found + } + const config = JSON.parse(configStr); + // id is full file path, input is relative to the package dir + if (id.endsWith(`/${input}`)) { + // this is the test entrypoint + rootConfig = config; + } + return config; + }; + const customLwcRollupPlugin = { ...defaultRollupPlugin, transform(src, id) { - let transform; + const { apiVersion, nativeOnly } = parseConfig(src, id); - // 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); + let transform; if (apiVersion) { - // The original Karma tests only ever had filename-based config for API version 60. - // Filename-based config is a pattern we want to move away from, so this transform - // only works for that version, so that we could simplify the logic here. - if (apiVersion !== '60') { - throw new Error( - 'TODO: fully implement or remove support for filename-based API version' - ); - } - transform = createRollupPlugin(input, { - apiVersion: 60, - }).transform; + transform = createRollupPlugin(input, { apiVersion }).transform; } else if (nativeOnly) { transform = createRollupPlugin(input, { disableSyntheticShadowSupport: true, @@ -72,6 +77,7 @@ const transform = async (ctx) => { } else { transform = defaultRollupPlugin.transform; } + return transform.call(this, src, id); }, }; diff --git a/packages/@lwc/integration-not-karma/configs/base.js b/packages/@lwc/integration-not-karma/configs/shared/base-config.js similarity index 87% rename from packages/@lwc/integration-not-karma/configs/base.js rename to packages/@lwc/integration-not-karma/configs/shared/base-config.js index 9dae0c90c7..e8b546490a 100644 --- a/packages/@lwc/integration-not-karma/configs/base.js +++ b/packages/@lwc/integration-not-karma/configs/shared/base-config.js @@ -1,6 +1,7 @@ import { join } from 'node:path'; import { LWC_VERSION } from '@lwc/shared'; -import { resolvePathOutsideRoot } from '../helpers/utils.js'; +import { resolvePathOutsideRoot } from '../../helpers/utils.js'; +import { getBrowsers } from './browsers.js'; /** * We want to convert from parsed options (true/false) to a `process.env` with only strings. @@ -18,7 +19,7 @@ const envify = (obj) => { const pluck = (obj, keys) => Object.fromEntries(keys.map((k) => [k, obj[k]])); const maybeImport = (file, condition) => (condition ? `await import('${file}');` : ''); -/** @type {() => import("@web/test-runner").TestRunnerConfig} */ +/** @type {(options: typeof import('../../helpers/options.js')) => import("@web/test-runner").TestRunnerConfig} */ export default (options) => { /** `process.env` to inject into test environment. */ const env = envify({ @@ -36,14 +37,18 @@ export default (options) => { NODE_ENV: options.NODE_ENV_FOR_TEST, }); + const browsers = getBrowsers(options); + return { + browsers, + browserLogs: false, // FIXME: Parallelism breaks tests that rely on focus/requestAnimationFrame, because they often // time out before they receive focus. But it also makes the full suite take 3x longer to run... // Potential workaround: https://github.com/modernweb-dev/web/issues/2588 concurrency: 1, - browserLogs: false, + concurrentBrowsers: browsers.length, nodeResolve: true, - rootDir: join(import.meta.dirname, '..'), + rootDir: join(import.meta.dirname, '../..'), plugins: [ { name: 'lwc-base-plugin', @@ -64,7 +69,8 @@ export default (options) => { }, ], testRunnerHtml: (testFramework) => - ` + ` + - `, + + `, }; }; diff --git a/packages/@lwc/integration-not-karma/configs/shared/browsers.js b/packages/@lwc/integration-not-karma/configs/shared/browsers.js new file mode 100644 index 0000000000..3dc2a72192 --- /dev/null +++ b/packages/@lwc/integration-not-karma/configs/shared/browsers.js @@ -0,0 +1,55 @@ +import { playwrightLauncher } from '@web/test-runner-playwright'; +import { createSauceLabsLauncher } from '@web/test-runner-saucelabs'; + +/** @type {(options: typeof import('../../helpers/options.js')) => import("@web/test-runner").BrowserLauncher[]} */ +export function getBrowsers(options) { + if (options.CI) { + if (!options.SAUCE_USERNAME || !options.SAUCE_ACCESS_KEY || !options.SAUCE_TUNNEL_ID) { + throw new Error( + `SAUCE_USERNAME, SAUCE_ACCESS_KEY, and SAUCE_TUNNEL_ID must be configured in CI` + ); + } + const sauceLabsLauncher = createSauceLabsLauncher( + { + user: options.SAUCE_USERNAME, + key: options.SAUCE_ACCESS_KEY, + }, + { + tunnelName: options.SAUCE_TUNNEL_ID, + } + ); + return [ + sauceLabsLauncher({ + browserName: 'chrome', + browserVersion: 'latest', + }), + // sauceLabsLauncher({ + // browserName: 'firefox', + // browserVersion: 'latest', + // }), + // sauceLabsLauncher({ + // browserName: 'safari', + // browserVersion: 'latest', + // }), + // ...(options.LEGACY_BROWSERS + // ? [ + // sauceLabsLauncher({ + // browserName: 'chrome', + // browserVersion: 'latest-2', + // }), + // sauceLabsLauncher({ + // browserName: 'safari', + // browserVersion: 'latest-2', + // platformName: 'Mac 13', // update with new releases + // }), + // ] + // : []), + ]; + } else { + return [ + playwrightLauncher({ product: 'chromium' }), + playwrightLauncher({ product: 'firefox' }), + playwrightLauncher({ product: 'webkit' }), + ]; + } +} diff --git a/packages/@lwc/integration-not-karma/helpers/options.js b/packages/@lwc/integration-not-karma/helpers/options.js index 51a9dd99f8..0c687eb834 100644 --- a/packages/@lwc/integration-not-karma/helpers/options.js +++ b/packages/@lwc/integration-not-karma/helpers/options.js @@ -73,5 +73,5 @@ 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 CI = Boolean(process.env.CI); export const GITHUB_RUN_ID = process.env.GITHUB_RUN_ID; diff --git a/packages/@lwc/integration-not-karma/package.json b/packages/@lwc/integration-not-karma/package.json index 387efde5a2..76a56f6f87 100644 --- a/packages/@lwc/integration-not-karma/package.json +++ b/packages/@lwc/integration-not-karma/package.json @@ -4,6 +4,7 @@ "version": "8.22.6", "type": "module", "scripts": { + "build": "playwright install || true", "start": "web-test-runner --manual", "test": "web-test-runner --config configs/integration.js", "test:hydration": "web-test-runner --config configs/hydration.js" @@ -21,7 +22,10 @@ "@web/dev-server-import-maps": "^0.2.1", "@web/dev-server-rollup": "^0.6.4", "@web/test-runner": "^0.20.2", - "chai": "^6.2.0" + "@web/test-runner-playwright": "^0.11.1", + "@web/test-runner-saucelabs": "^0.13.0", + "chai": "^6.2.0", + "playwright": "^1.56.0" }, "volta": { "extends": "../../../package.json" diff --git a/packages/@lwc/integration-not-karma/test/api/CustomElementConstructor-getter/index.spec.js b/packages/@lwc/integration-not-karma/test/api/CustomElementConstructor-getter/index.spec.js index 6850011af9..4eb8cc217d 100644 --- a/packages/@lwc/integration-not-karma/test/api/CustomElementConstructor-getter/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/api/CustomElementConstructor-getter/index.spec.js @@ -34,7 +34,9 @@ it('CustomElementConstructor cannot be `new`ed before being defined', () => { new UndefinedComponent.CustomElementConstructor(); }; expect(func).toThrowError(TypeError); - expect(func).toThrowError(/(Illegal constructor|does not define a custom element)/); + expect(func).toThrowError( + /(Illegal constructor|does not define a custom element|is not a valid custom element constructor)/ + ); }); it('CustomElementConstructor can be `new`ed after being defined', () => { diff --git a/packages/@lwc/integration-not-karma/test/mixed-api-version/x/useApiVersion60/useApiVersion60.js b/packages/@lwc/integration-not-karma/test/mixed-api-version/x/useApiVersion60/useApiVersion60.js index 9e2ab67e8f..0fd2bb4b23 100644 --- a/packages/@lwc/integration-not-karma/test/mixed-api-version/x/useApiVersion60/useApiVersion60.js +++ b/packages/@lwc/integration-not-karma/test/mixed-api-version/x/useApiVersion60/useApiVersion60.js @@ -1,4 +1,5 @@ +// Per-file compiler config, used in configs/plugins/serve-integration.js +/*!WTR {"apiVersion": 60}*/ import { LightningElement } from 'lwc'; -// By naming this component `useApiVersion60` we tell Karma's LWC Rollup plugin to compile it with API version 60 export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/light/chic.native-only.css b/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/light/chic.native-only.css index 3f7b51fab0..c22f3baea4 100644 --- a/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/light/chic.native-only.css +++ b/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/light/chic.native-only.css @@ -1,3 +1,5 @@ +/* Per-file compiler config, used in configs/plugins/serve-integration.js */ +/*!WTR {"nativeOnly": true}*/ div { --chic: 'native'; } diff --git a/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/light/snazzy.native-only.scoped.css b/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/light/snazzy.native-only.scoped.css index 20e1bf49d9..13c0165120 100644 --- a/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/light/snazzy.native-only.scoped.css +++ b/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/light/snazzy.native-only.scoped.css @@ -1,3 +1,5 @@ +/* Per-file compiler config, used in configs/plugins/serve-integration.js */ +/*!WTR {"nativeOnly": true}*/ div { --snazzy: 'native'; } diff --git a/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/shadow/chic.native-only.css b/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/shadow/chic.native-only.css index 3f7b51fab0..c22f3baea4 100644 --- a/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/shadow/chic.native-only.css +++ b/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/shadow/chic.native-only.css @@ -1,3 +1,5 @@ +/* Per-file compiler config, used in configs/plugins/serve-integration.js */ +/*!WTR {"nativeOnly": true}*/ div { --chic: 'native'; } diff --git a/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/shadow/snazzy.native-only.scoped.css b/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/shadow/snazzy.native-only.scoped.css index 20e1bf49d9..13c0165120 100644 --- a/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/shadow/snazzy.native-only.scoped.css +++ b/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/shadow/snazzy.native-only.scoped.css @@ -1,3 +1,5 @@ +/* Per-file compiler config, used in configs/plugins/serve-integration.js */ +/*!WTR {"nativeOnly": true}*/ div { --snazzy: 'native'; } diff --git a/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/styleLibrary/foo.native-only.css b/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/styleLibrary/foo.native-only.css index f7ae70e9e7..a9700555b7 100644 --- a/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/styleLibrary/foo.native-only.css +++ b/packages/@lwc/integration-not-karma/test/rendering/native-only-css/x/styleLibrary/foo.native-only.css @@ -1,3 +1,5 @@ +/* Per-file compiler config, used in configs/plugins/serve-integration.js */ +/*!WTR {"nativeOnly": true}*/ div { --foo: 'native'; } diff --git a/packages/@lwc/integration-not-karma/test/signal/protocol/index.spec.js b/packages/@lwc/integration-not-karma/test/signal/protocol/index.spec.js index e1b12420a3..ebd82e0e4f 100644 --- a/packages/@lwc/integration-not-karma/test/signal/protocol/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/signal/protocol/index.spec.js @@ -11,7 +11,7 @@ import Throws from 'x/throws'; // Note for testing purposes the signal implementation uses LWC module resolution to simplify things. // In production the signal will come from a 3rd party library. import { Signal } from 'x/signal'; -import { jasmine } from '../../../helpers/jasmine.js'; +import { fn as mockFn } from '@vitest/spy'; import { resetDOM } from '../../../helpers/reset.js'; describe('signal protocol', () => { @@ -190,7 +190,7 @@ describe('signal protocol', () => { it('does not subscribe if the signal shape is incorrect', async () => { const elm = createElement('x-child', { is: Child }); - const subscribe = jasmine.createSpy(); + const subscribe = mockFn(); // Note the signals property is value's' and not value const signal = { values: 'initial value', subscribe }; elm.signal = signal; @@ -202,7 +202,7 @@ describe('signal protocol', () => { it('does not subscribe if the signal is not added as trusted signal', async () => { const elm = createElement('x-child', { is: Child }); - const subscribe = jasmine.createSpy(); + const subscribe = mockFn(); // Note this follows the shape of the signal implementation // but it's not added as a trusted signal (add using lwc.addTrustedSignal) const signal = { diff --git a/packages/@lwc/integration-not-karma/test/template-expressions/errors/index.spec.js b/packages/@lwc/integration-not-karma/test/template-expressions/errors/index.spec.js index 77903150f0..ca2123e3ec 100644 --- a/packages/@lwc/integration-not-karma/test/template-expressions/errors/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/template-expressions/errors/index.spec.js @@ -1,15 +1,20 @@ +// Per-file compiler config, used in configs/plugins/serve-integration.js +/*!WTR {"apiVersion": 66}*/ import { createElement } from 'lwc'; import UndefinedMemberExpressionObjParent from 'x/undefinedMemberExpressionObjParent'; import ThrowDuringCallParent from 'x/throwDuringCallParent'; +import { API_VERSION } from '../../../helpers/options'; -it(`should handle member expression with undefined object`, () => { +const cteEnabled = API_VERSION >= 66; + +it.runIf(cteEnabled)(`should handle member expression with undefined object`, () => { const parent = createElement('x-parent', { is: UndefinedMemberExpressionObjParent }); document.body.appendChild(parent); expect(parent.caughtError).toContain('undefined'); }); -it(`should handle errors thrown during call expression`, () => { +it.runIf(cteEnabled)(`should handle errors thrown during call expression`, () => { const parent = createElement('x-parent', { is: ThrowDuringCallParent }); document.body.appendChild(parent); expect(parent.caughtError).toContain("I'm the Gingerbread man!"); diff --git a/packages/@lwc/integration-not-karma/test/template-expressions/smoke-test/index.spec.js b/packages/@lwc/integration-not-karma/test/template-expressions/smoke-test/index.spec.js index 1c1d44a939..f03bea6cf2 100644 --- a/packages/@lwc/integration-not-karma/test/template-expressions/smoke-test/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/template-expressions/smoke-test/index.spec.js @@ -1,8 +1,13 @@ +// Per-file compiler config, used in configs/plugins/serve-integration.js +/*!WTR {"apiVersion": 66}*/ import { createElement } from 'lwc'; import Test from 'x/test'; +import { API_VERSION } from '../../../helpers/options'; -it(`should support call expressions`, () => { +const cteEnabled = API_VERSION >= 66; + +it.runIf(cteEnabled)(`should support call expressions`, () => { const elm = createElement('x-test', { is: Test }); document.body.appendChild(elm); diff --git a/scripts/bundlesize/bundlesize.config.json b/scripts/bundlesize/bundlesize.config.json index ac12507435..b457893ec8 100644 --- a/scripts/bundlesize/bundlesize.config.json +++ b/scripts/bundlesize/bundlesize.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "packages/@lwc/engine-dom/dist/index.js", - "maxSize": "24.90KB" + "maxSize": "25KB" }, { "path": "packages/@lwc/synthetic-shadow/dist/index.js", diff --git a/yarn.lock b/yarn.lock index c70fd5d4fc..0ef832a1ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2091,9 +2091,11 @@ "@lwc/eslint-plugin-lwc-internal@link:./scripts/eslint-plugin": version "0.0.0" + uid "" "@lwc/test-utils-lwc-internals@link:./scripts/test-utils": version "0.0.0" + uid "" "@napi-rs/wasm-runtime@0.2.4": version "0.2.4" @@ -4026,6 +4028,35 @@ dependencies: "@web/test-runner-core" "^0.13.0" +"@web/test-runner-playwright@^0.11.1": + version "0.11.1" + resolved "https://registry.yarnpkg.com/@web/test-runner-playwright/-/test-runner-playwright-0.11.1.tgz#d993112ae2126eb74c1c5a171d6ea44c2dd24b4e" + integrity sha512-l9tmX0LtBqMaKAApS4WshpB87A/M8sOHZyfCobSGuYqnREgz5rqQpX314yx+4fwHXLLTa5N64mTrawsYkLjliw== + dependencies: + "@web/test-runner-core" "^0.13.0" + "@web/test-runner-coverage-v8" "^0.8.0" + playwright "^1.53.0" + +"@web/test-runner-saucelabs@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@web/test-runner-saucelabs/-/test-runner-saucelabs-0.13.0.tgz#66b50d4cf6ca29163e8c67e0d6a0b5863ac69172" + integrity sha512-RHVDIFVaiOoxydigx1S4wrA20wjjLF9IAxlT7HxYHxO1qx1UIuWLabd6jEGlxluC0xFhuBJLm+zFj8zMEXJNyg== + dependencies: + "@web/test-runner-webdriver" "^0.9.0" + internal-ip "^6.2.0" + nanoid "^3.1.25" + saucelabs "^9.0.0" + webdriver "^9.0.0" + webdriverio "^9.0.0" + +"@web/test-runner-webdriver@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@web/test-runner-webdriver/-/test-runner-webdriver-0.9.0.tgz#18fb3299ddc258a2782028de19f21ee4c27895c5" + integrity sha512-G2io6ph0v/sX0U/DEgh/EUHsLsq8/Gs/uUw8N7rcUaXSdIFCUbxlwzx/qZv2ZKY52q84oYUyeOhViNZ2OqYl6Q== + dependencies: + "@web/test-runner-core" "^0.13.0" + webdriverio "^9.0.0" + "@web/test-runner@^0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.20.2.tgz#3045de8f14eb9f9b8aa51aa7fd9849dd95bb0a93" @@ -7605,6 +7636,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -11279,6 +11315,20 @@ pkg-up@^4.0.0: dependencies: find-up "^6.2.0" +playwright-core@1.56.0: + version "1.56.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.0.tgz#14b40ea436551b0bcefe19c5bfb8d1804c83739c" + integrity sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ== + +playwright@^1.53.0, playwright@^1.56.0: + version "1.56.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.0.tgz#71c533c61da33e95812f8c6fa53960e073548d9a" + integrity sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA== + dependencies: + playwright-core "1.56.0" + optionalDependencies: + fsevents "2.3.2" + portfinder@^1.0.32: version "1.0.37" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.37.tgz#92b754ef89a11801c8efe4b0e5cd845b0064c212" @@ -12132,7 +12182,7 @@ saucelabs@6.2.2: tunnel "0.0.6" yargs "^17.0.1" -saucelabs@^9.0.1: +saucelabs@^9.0.0, saucelabs@^9.0.1: version "9.0.2" resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-9.0.2.tgz#99f6170f3d789fcb0be2f270f7d37a9d7cdf5187" integrity sha512-37QGEOgp9BP1re6S06qpNcBZ0Hw+ZSkZkDepbXHT9VjYoRQwRzUoLtKqE4yyVeK7dzcQXQapmTGF1kp1jO2VDw== @@ -13856,7 +13906,7 @@ webdriver@7.19.5: ky "^0.30.0" lodash.merge "^4.6.1" -webdriver@9.20.0: +webdriver@9.20.0, webdriver@^9.0.0: version "9.20.0" resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-9.20.0.tgz#518fa61abd0b0435509548ef54fb0824f0fa04fd" integrity sha512-Kk+AGV1xWLNHVpzUynQJDULMzbcO3IjXo3s0BzfC30OpGxhpaNmoazMQodhtv0Lp242Mb1VYXD89dCb4oAHc4w== @@ -13906,7 +13956,7 @@ webdriverio@7.19.5: serialize-error "^8.0.0" webdriver "7.19.5" -webdriverio@9.20.0, webdriverio@^9.19.2: +webdriverio@9.20.0, webdriverio@^9.0.0, webdriverio@^9.19.2: version "9.20.0" resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-9.20.0.tgz#46f45268f2c4021a18045a7e7dfd738225514ece" integrity sha512-cqaXfahTzCFaQLlk++feZaze6tAsW8OSdaVRgmOGJRII1z2A4uh4YGHtusTpqOiZAST7OBPqycOwfh01G/Ktbg==