Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
099ab86
test(wtr): use vitest spy instead of jasmine
wjhsf Sep 30, 2025
a249b96
test(wtr): add API version 66 coverage to CI
wjhsf Oct 6, 2025
1e50ff1
test(wtr): indent consistently
wjhsf Oct 6, 2025
715cac8
test(wtr): move base config into shared dir
wjhsf Oct 7, 2025
22cfa8d
test(wtr): run tests in multiple browsers
wjhsf Oct 7, 2025
af60a96
test(wtr): use build script to install playwright
wjhsf Oct 7, 2025
41676b5
chore: bundlesize
wjhsf Oct 7, 2025
1b44785
test(wtr): add type for param
wjhsf Oct 7, 2025
2072694
test(wtr): use sauce helper in CI
wjhsf Oct 7, 2025
423c051
test(wtr): add install to playwright setup
wjhsf Oct 7, 2025
7841184
test(wtr): enable saucelabs in all runs
wjhsf Oct 7, 2025
52915ff
test(wtr): kerjigger playwright again
wjhsf Oct 7, 2025
f17448b
test(wtr): rekerjigger playwright again
wjhsf Oct 7, 2025
6412646
test(wtr): make fake SSR file more clear
wjhsf Oct 7, 2025
e4092b4
test(wtr): install playwright deps in WTR CI
wjhsf Oct 7, 2025
5357cd7
test(wtr): and much rekerjiggering there was
wjhsf Oct 7, 2025
3fade97
test(wtr): isolate server-side LWC
wjhsf Oct 9, 2025
9e4ac3e
test(wtr): hack for nucleus
wjhsf Oct 9, 2025
465dd72
test(wtr): re-enable CTE tests @W-19098211 (#5535)
wjhsf Oct 14, 2025
5c81592
chore(ci): split hydration tests into engine-server and ssr-v2
wjhsf Oct 15, 2025
e928017
chore(ci): split integration tests into two groups
wjhsf Oct 15, 2025
1deb775
Merge branch 'master' into wjh/wtr-keep-on-enving
wjhsf Oct 15, 2025
f6ea8de
chore(ci): build first and cache result so parallel jobs are faster
wjhsf Oct 15, 2025
e83ec4e
test(wtr): check correct CI env var
wjhsf Oct 15, 2025
02c8206
test(wtr): use concurrency in CI
wjhsf Oct 15, 2025
8564bec
test(wtr): always run browsers concurrently
wjhsf Oct 15, 2025
e1288ce
chore(ci): give up on caching (for now?)
wjhsf Oct 15, 2025
b1fef23
test(wtr): try difference sauce config
wjhsf Oct 15, 2025
2aa45e1
chore(ci): don't needs: install-and-build
wjhsf Oct 15, 2025
9d59c9a
chore(ci): use distinct tunnel IDs for SauceLabs
wjhsf Oct 16, 2025
3b7657d
Merge branch 'master' into wjh/wtr-keep-on-enving
wjhsf Oct 16, 2025
45d38da
test(wtr): revert concurrency in CI
wjhsf Oct 16, 2025
199412f
test(wtr): disable tests that sauce doesn't like
wjhsf Oct 16, 2025
4672320
test(wtr): only run chrome in saucelabs
wjhsf Oct 17, 2025
41ce390
chore(ci): use env var for runs-on for easier updates
wjhsf Oct 17, 2025
3fc2bc2
chore(ci): use matrix values to ensure unique tunnel IDs
wjhsf Oct 17, 2025
570da42
Revert "chore(ci): use env var for runs-on for easier updates"
wjhsf Oct 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 108 additions & 31 deletions .github/workflows/web-test-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -47,23 +49,61 @@ jobs:
run: yarn install --frozen-lockfile
working-directory: ./

# - uses: saucelabs/[email protected]
# with:
# username: ${{ secrets.SAUCE_USERNAME }}
# accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
# tunnelName: ${{ env.SAUCE_TUNNEL_ID }}
# region: us
- uses: saucelabs/[email protected]
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: github-action-tunnel-wtr-${{github.run_id}}-group-1
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/[email protected]
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
Expand All @@ -89,13 +129,14 @@ jobs:
run: yarn install --frozen-lockfile
working-directory: ./

# - uses: saucelabs/[email protected]
# with:
# username: ${{ secrets.SAUCE_USERNAME }}
# accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
# tunnelName: ${{ env.SAUCE_TUNNEL_ID }}
# region: us
- uses: saucelabs/[email protected]
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
Expand All @@ -106,10 +147,48 @@ 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
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/[email protected]
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: github-action-tunnel-wtr-${{github.run_id}}-group-1
ENGINE_SERVER: 1
defaults:
run:
working-directory: ./packages/@lwc/integration-not-karma
Expand All @@ -127,21 +206,19 @@ jobs:
run: yarn install --frozen-lockfile
working-directory: ./

# - uses: saucelabs/[email protected]
# 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/[email protected]
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
25 changes: 19 additions & 6 deletions packages/@lwc/engine-core/src/libs/signal-tracker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)}`
);
}
}
Expand All @@ -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
)}`
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@lwc/integration-not-karma/configs/hydration.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
9 changes: 2 additions & 7 deletions packages/@lwc/integration-not-karma/configs/integration.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'],
plugins: [
...baseConfig.plugins,
importMapsPlugin({ inject: { importMap: { imports: { lwc: './mocks/lwc.js' } } } }),
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -68,16 +64,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,
Expand All @@ -87,44 +94,32 @@ 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();
}

/**
* Hydration test `index.spec.js` files are actually config files, not spec files.
* 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} */
Expand Down
Loading
Loading