diff --git a/.github/workflows/karma.yml b/.github/workflows/karma.yml index 5f4e959db5..6d57464ad0 100644 --- a/.github/workflows/karma.yml +++ b/.github/workflows/karma.yml @@ -178,6 +178,11 @@ jobs: - run: DISABLE_STATIC_CONTENT_OPTIMIZATION=1 DISABLE_SYNTHETIC=1 yarn sauce:ci - run: NODE_ENV_FOR_TEST=production yarn sauce:ci - run: NODE_ENV_FOR_TEST=production DISABLE_SYNTHETIC=1 yarn sauce:ci + - run: yarn hydration:sauce:ci:engine-server + - run: ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION=1 yarn hydration:sauce:ci:engine-server + - run: NODE_ENV_FOR_TEST=production yarn hydration:sauce:ci:engine-server + - run: DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 yarn hydration:sauce:ci:engine-server + - run: DISABLE_STATIC_CONTENT_OPTIMIZATION=1 yarn hydration:sauce:ci:engine-server - run: yarn hydration:sauce:ci - run: ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION=1 yarn hydration:sauce:ci - run: NODE_ENV_FOR_TEST=production yarn hydration:sauce:ci diff --git a/packages/@lwc/integration-karma/package.json b/packages/@lwc/integration-karma/package.json index 718d33c084..c0bf0cf73d 100644 --- a/packages/@lwc/integration-karma/package.json +++ b/packages/@lwc/integration-karma/package.json @@ -5,6 +5,10 @@ "scripts": { "start": "KARMA_MODE=watch karma start ./scripts/karma-configs/test/local.js", "test": "karma start ./scripts/karma-configs/test/local.js --single-run --browsers ChromeHeadless", + "hydration:start:engine-server": "ENGINE_SERVER=true KARMA_MODE=watch karma start ./scripts/karma-configs/hydration/local.js", + "hydration:test:engine-server": "ENGINE_SERVER=true karma start ./scripts/karma-configs/hydration/local.js --single-run --browsers ChromeHeadless", + "hydration:sauce:engine-server": "ENGINE_SERVER=true karma start ./scripts/karma-configs/hydration/sauce.js --single-run", + "hydration:sauce:ci:engine-server": "ENGINE_SERVER=true ../../../scripts/ci/retry.sh karma start ./scripts/karma-configs/hydration/sauce.js --single-run", "hydration:start": "KARMA_MODE=watch karma start ./scripts/karma-configs/hydration/local.js", "hydration:test": "karma start ./scripts/karma-configs/hydration/local.js --single-run --browsers ChromeHeadless", "hydration:sauce": "karma start ./scripts/karma-configs/hydration/sauce.js --single-run", diff --git a/packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js b/packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js index 8588df7975..561993edc1 100644 --- a/packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js +++ b/packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js @@ -4,16 +4,29 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ +const ENGINE_SERVER = process.env.ENGINE_SERVER; const path = require('node:path'); const vm = require('node:vm'); const fs = require('node:fs/promises'); const { format } = require('node:util'); const { rollup } = require('rollup'); const lwcRollupPlugin = require('@lwc/rollup-plugin'); -const ssr = require('@lwc/engine-server'); +const ssr = ENGINE_SERVER ? require('@lwc/engine-server') : require('@lwc/ssr-runtime'); const { DISABLE_STATIC_CONTENT_OPTIMIZATION } = require('../shared/options'); const Watcher = require('./Watcher'); +/* + * These are hydration tests that currently fail in ssr-compiler (V2) + * They need to be removed from this list and then fixed. There are only 4 + * so kept here for now. + */ +const EXPECTED_V2_FAILURES = [ + 'mismatches/host-mutation-in-connected-callback/class-mutated-attr-mismatch', + 'mismatches/host-mutation-in-connected-callback/class', + 'directives/lwc-dynamic', + 'inner-outer-html', +]; + const context = { LWC: ssr, moduleOutput: null, @@ -54,13 +67,12 @@ async function exists(path) { } } -let cache; - -async function getCompiledModule(dirName) { +async function getCompiledModule(dirName, compileForSSR) { const bundle = await rollup({ input: path.join(dirName, 'x', COMPONENT_UNDER_TEST, `${COMPONENT_UNDER_TEST}.js`), plugins: [ lwcRollupPlugin({ + targetSSR: !!compileForSSR, modules: [ { dir: dirName, @@ -74,9 +86,8 @@ async function getCompiledModule(dirName) { enableStaticContentOptimization: !DISABLE_STATIC_CONTENT_OPTIMIZATION, }), ], - cache, - external: ['lwc', 'test-utils', '@test/loader'], // @todo: add ssr modules for test-utils and @test/loader + external: ['lwc', '@lwc/ssr-runtime', 'test-utils', '@test/loader'], // @todo: add ssr modules for test-utils and @test/loader onwarn(warning, warn) { // Ignore warnings from our own Rollup plugin @@ -87,13 +98,13 @@ async function getCompiledModule(dirName) { }); const { watchFiles } = bundle; - cache = bundle.cache; const { output } = await bundle.generate({ format: 'iife', name: 'Main', globals: { lwc: 'LWC', + '@lwc/ssr-runtime': 'LWC', 'test-utils': 'TestUtils', }, }); @@ -131,17 +142,23 @@ function throwOnUnexpectedConsoleCalls(runnable) { } /** - * @param {string} moduleCode - * @param {string} testConfig - * @param {string} filename + * This is the function that takes SSR bundle code and test config, constructs a script that will + * run in a separate JS runtime environment with its own global scope. The `context` object + * (defined at the top of this file) is passed in as the global scope for that script. The script + * runs, utilizing the `LWC` object that we've attached to the global scope, it sets a + * new value (the rendered markup) to the globalThis.moduleOutput, which correspond to + * context.moduleOutput in the hydration-test.js module scope. + * + * So, script runs, generates markup, & we get that markup out and return it to Karma for use + * in client-side tests. */ -function getSsrCode(moduleCode, testConfig, filename) { +async function getSsrCode(moduleCode, testConfig, filename) { const script = new vm.Script( ` ${testConfig}; config = config || {}; ${moduleCode}; - moduleOutput = LWC.renderComponent('x-${COMPONENT_UNDER_TEST}-${guid++}', Main, config.props || {});`, + moduleOutput = LWC.renderComponent('x-${COMPONENT_UNDER_TEST}-${guid++}', Main, config.props || {}, 'sync');`, { filename } ); @@ -149,7 +166,6 @@ function getSsrCode(moduleCode, testConfig, filename) { vm.createContext(context); script.runInContext(context); }); - return context.moduleOutput; } @@ -160,7 +176,6 @@ async function getTestModuleCode(input) { }); const { watchFiles } = bundle; - cache = bundle.cache; const { output } = await bundle.generate({ format: 'iife', @@ -194,33 +209,66 @@ function createHCONFIG2JSPreprocessor(config, logger, emitter) { return async (_content, file, done) => { const filePath = file.path; const suiteDir = path.dirname(filePath); + // Wrap all the tests into a describe block with the file stricture name const describeTitle = path.relative(basePath, suiteDir).split(path.sep).join(' '); try { const { code: testCode, watchFiles: testWatchFiles } = await getTestModuleCode(filePath); - const { code: componentDef, watchFiles: componentWatchFiles } = - await getCompiledModule(suiteDir); + // You can add an `.only` file alongside an `index.spec.js` file to make it `fdescribe()` const onlyFileExists = await existsUp(suiteDir, '.only'); + // If V2 failure is expected, skip it. + const v2FailureExpected = + !ENGINE_SERVER && EXPECTED_V2_FAILURES.some((dir) => suiteDir.includes(dir)); + + let describe = onlyFileExists ? 'fdescribe' : 'describe'; + + const { code: componentDefCSR, watchFiles: componentWatchFilesCSR } = + await getCompiledModule(suiteDir, false); + + let ssrOutput; + /* eslint-disable vitest/no-conditional-tests */ + if (ENGINE_SERVER) { + // engine-server uses the same def as the client + watcher.watchSuite(filePath, testWatchFiles.concat(componentWatchFilesCSR)); + ssrOutput = await getSsrCode( + componentDefCSR, + testCode, + path.join(suiteDir, 'ssr.js') + ); + } else if (!v2FailureExpected) { + // ssr-compiler has it's own def + const { code: componentDefSSR, watchFiles: componentWatchFilesSSR } = + await getCompiledModule(suiteDir, true); + watcher.watchSuite( + filePath, + testWatchFiles.concat(componentWatchFilesCSR).concat(componentWatchFilesSSR) + ); + ssrOutput = await getSsrCode( + componentDefSSR.replace(`process.env.NODE_ENV === 'test-karma-lwc'`, 'true'), + testCode, + path.join(suiteDir, 'ssr.js') + ); + } else { + console.log(`Expected failure for ${suiteDir}, skipping`); + describe = 'xdescribe'; + } + /* eslint-enable vitest/no-conditional-tests */ - const ssrOutput = getSsrCode(componentDef, testCode, path.join(suiteDir, 'ssr.js')); - - watcher.watchSuite(filePath, testWatchFiles.concat(componentWatchFiles)); const newContent = format( TEMPLATE, JSON.stringify(ssrOutput), - componentDef, + componentDefCSR, testCode, - onlyFileExists ? 'fdescribe' : 'describe', + describe, JSON.stringify(describeTitle) ); done(null, newContent); } catch (error) { const location = path.relative(basePath, filePath); log.error('Error processing ā€œ%sā€\n\n%s\n', location, error.stack || error.message); - done(error, null); } };