Skip to content
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
a7bc370
refactor(wtr): move NODE_ENV_FOR_TEST default into options file
wjhsf Jun 23, 2025
85e8b0d
chore(wtr): move hooks util to separate file
wjhsf Jun 23, 2025
4d1615f
chore(wtr): move aria utils to separate file
wjhsf Jun 23, 2025
9d97689
chore(wtr): move constants to separate file
wjhsf Jun 23, 2025
38d8482
chore(wtr): clean up custom rollup plugin
wjhsf Jun 23, 2025
0f960b3
test(wtr): get hydration tests kinda running
wjhsf Jun 24, 2025
30cefce
test(wtr): kinda start moving to ESM instead of IIFE
wjhsf Jun 25, 2025
d292d05
fix(shared): make sanitizeHtmlContent work
wjhsf Jun 25, 2025
abca4bb
chore(ci): run hydration tests in ci
wjhsf Jun 25, 2025
738f32b
Merge branch 'master' into wjh/wtr-hydration
wjhsf Jun 25, 2025
e885a88
test(wtr): remove unused script
wjhsf Jun 25, 2025
d09d5c7
Merge branch 'master' into wjh/wtr-hydration
wjhsf Jul 7, 2025
1c5f80f
test(wtr): implement missing jasmine assertions
wjhsf Jul 8, 2025
ecad806
test(wtr): clean up test to make assertions more clear
wjhsf Jul 8, 2025
1454b2e
test(wtr): implement custom console matchers
wjhsf Jul 8, 2025
aeaec17
test(wtr): use simpler console spy
wjhsf Jul 8, 2025
ebfc163
test(wtr): remove outdated, unnecessary spies
wjhsf Jul 8, 2025
2557b7f
test(wtr): move signals util to own file
wjhsf Jul 8, 2025
ed8ec74
test(wtr): implement custom error matchers
wjhsf Jul 9, 2025
e53f159
chore: split matchers into separate files
wjhsf Jul 9, 2025
c1f5623
chore: ugh tui go away
wjhsf Jul 9, 2025
a7371cc
Merge branch 'master' into wjh/more-wtr
wjhsf Jul 10, 2025
92eddae
chore: remove unused file
wjhsf Jul 10, 2025
0d2d59a
test(wtr): conditionally import polyfills
wjhsf Jul 10, 2025
1551364
test(wtr): use one setup file instead of two
wjhsf Jul 10, 2025
c889881
chore: add comment explaining file
wjhsf Jul 10, 2025
113d86d
chore(wtr): rename file
wjhsf Jul 10, 2025
884655b
chore(wtr): move test configs into directory
wjhsf Jul 10, 2025
901ab22
chore(wtr): extract shared parts of config into base config object
wjhsf Jul 10, 2025
446fcb0
chore(wtr): rename config files
wjhsf Jul 10, 2025
3deb95a
chore(wtr): move WTR plugins from helpers dir to config dir
wjhsf Jul 10, 2025
3951273
chore: move full plugins to plugin files
wjhsf Jul 11, 2025
ad4ca57
test(wtr): not all env vars are boolean
wjhsf Jul 11, 2025
13c259a
test(wtr): make tests async so they clean up properly
wjhsf Jul 11, 2025
9929ace
test(wtr): fix resolution of wire-service
wjhsf Jul 14, 2025
b4dbd54
test(wtr): skip flaky tests
wjhsf Jul 14, 2025
9faeb90
test(wtr): add more jasmine spy adapter methods
wjhsf Jul 14, 2025
da42d85
test(wtr): enable passing tests
wjhsf Jul 14, 2025
0773038
test(wtr): make spy props non-enumerable
wjhsf Jul 14, 2025
b076645
test(wtr): remove unused matcher
wjhsf Jul 14, 2025
8a06494
test(wtr): fix assertion to match error message
wjhsf Jul 14, 2025
1fc08b2
test(wtr): implement jasmine.any
wjhsf Jul 14, 2025
ee78dee
test(wtr): mark test as flaky timeout
wjhsf Jul 15, 2025
66cecd2
update comment
wjhsf Jul 15, 2025
88d1346
update comment
wjhsf Jul 15, 2025
2d9f4a4
test(wtr): move scripts to head instead of body
wjhsf Jul 15, 2025
c11f060
Update packages/@lwc/integration-not-karma/configs/base.mjs
wjhsf Jul 16, 2025
6827b4f
Update packages/@lwc/integration-not-karma/configs/base.mjs
wjhsf Jul 16, 2025
b5e1b60
Merge branch 'master' into wjh/even-more-wtr
wjhsf Jul 16, 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
5 changes: 4 additions & 1 deletion nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@
},
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"cacheDirectory": "./.nx-cache",
"defaultBase": "master"
"defaultBase": "master",
"tui": {
"enabled": false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,6 @@ function testInvalidComponentConstructor(name, ctor) {
});
}

beforeAll(function () {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toEqualWireSettings is never actually used anywhere.

const getNormalizedFunctionAsString = (fn) => fn.toString().replace(/(\s|\n)/g, '');

jasmine.addMatchers({
toEqualWireSettings: function () {
return {
compare: function (actual, expected) {
Object.keys(actual).forEach((currentKey) => {
const normalizedActual = Object.assign({}, actual[currentKey], {
config: getNormalizedFunctionAsString(actual[currentKey].config),
});

const normalizedExpected = Object.assign({}, expected[currentKey], {
config: getNormalizedFunctionAsString(
expected[currentKey].config || function () {}
),
});

expect(normalizedActual).toEqual(normalizedExpected);
});

return {
pass: true,
};
},
};
},
});
});

testInvalidComponentConstructor('null', null);
testInvalidComponentConstructor('undefined', undefined);
testInvalidComponentConstructor('String', 'component');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,6 @@ describe('disconnectedCallback for host with slots', () => {
let slotAcceptingChildSpy;
let parent;

beforeAll(() => {
// Ignore the engine logging about passing slot content to a component that does not accept slot
// These should become unnecessary when #869 is fixed
spyOn(console, 'group');
spyOn(console, 'log');
spyOn(console, 'groupEnd');
});

beforeEach(() => {
parentDisconnectSpy = jasmine.createSpy();
slotIgnoringChildSpy = jasmine.createSpy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const expectLogs = (regexes) => {
const args = logger.calls.allArgs();
expect(args.length).toBe(regexes.length);
for (let i = 0; i < args.length; i++) {
expect(args[i][0]).toMatch(regexes[i]);
expect(args[i][0]).toBeInstanceOf(Error);
expect(args[i][0].message).toMatch(regexes[i]);
Comment on lines +28 to +29
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Karma's toMatch coerces the received value (in this case, an error) to a string, but the setup we have for web-test-runner expects to receive only a string.

}
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ import { createElement } from 'lwc';
import Parent from 'x/parent';

describe('Issue #1090', () => {
beforeAll(() => {
// Ignore the engine logging about passing slot content to a component that does not accept slot
// These should become unnecessary when #869 is fixed
spyOn(console, 'group');
spyOn(console, 'log');
spyOn(console, 'groupEnd');
});

it('should disconnect slotted content even if it is not allocated into a slot', () => {
const elm = createElement('x-parent', { is: Parent });
document.body.appendChild(elm);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@ describe('dom mutation without the lwc:dom="manual" directive', () => {
const root = createElement('x-without-lwc-dom-manual', { is: withoutLwcDomManual });
document.body.appendChild(root);
const elm = root.shadowRoot.querySelector('div');
const mutate = () => fn(elm);

// eslint-disable-next-line vitest/valid-expect
let expected = expect(() => fn(elm));
const warning = new RegExp(
`\\[LWC warn\\]: The \`${method}\` method is available only on elements that use the \`lwc:dom="manual"\` directive.`
);
if (process.env.NATIVE_SHADOW) {
expected = expected.not; // no error
expect(mutate).not.toLogWarningDev(warning);
} else {
expect(mutate).toLogWarningDev(warning);
}
expected.toLogWarningDev(
new RegExp(
`\\[LWC warn\\]: The \`${method}\` method is available only on elements that use the \`lwc:dom="manual"\` directive.`
)
);
});
}

Expand Down
38 changes: 19 additions & 19 deletions packages/@lwc/integration-karma/test/wire/wiring/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function filterCalls(echoAdapterSpy, methodType) {

describe('wiring', () => {
describe('component lifecycle and wire adapter', () => {
it('should call a connect when component is connected', () => {
it('should call a connect when component is connected', async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests in this file use a shared global state. Most of the tests, but not all, have asynchronous behavior. In Karma, the state was properly cleaned for both sync and async tests. With web-test-runner, the state got polluted with the synchronous tests. Converting the sync tests to async functions (even with no inner await/promises) solved the problem, although I don't fully understand why.

Note that I added async to all the functions, not just the sync ones, because that's best practice for anything that returns a promise.

const spy = [];
const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass });
AdapterId.setSpy(spy);
Expand All @@ -30,7 +30,7 @@ describe('wiring', () => {
expect(filterCalls(spy, 'connect').length).toBe(1);
});

it('should call a disconnect when component is disconnected', () => {
it('should call a disconnect when component is disconnected', async () => {
const spy = [];
AdapterId.setSpy(spy);
const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass });
Expand All @@ -40,7 +40,7 @@ describe('wiring', () => {
expect(filterCalls(spy, 'disconnect').length).toBe(1);
});

it('should call a connect and disconnect when component is connected, disconnected twice', () => {
it('should call a connect and disconnect when component is connected, disconnected twice', async () => {
const spy = [];
const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass });
AdapterId.setSpy(spy);
Expand All @@ -59,7 +59,7 @@ describe('wiring', () => {
});

describe('update method on wire adapter', () => {
it('should be called in same tick when component with wire no dynamic params is created', () => {
it('should be called in same tick when component with wire no dynamic params is created', async () => {
const spy = [];
AdapterId.setSpy(spy);
expect(spy.length).toBe(0);
Expand All @@ -83,7 +83,7 @@ describe('wiring', () => {
setFeatureFlagForTest('ENABLE_WIRE_SYNC_EMIT', false);
});

it('should be called synchronously after connect when a component with wire that has dynamic params is created', () => {
it('should be called synchronously after connect when a component with wire that has dynamic params is created', async () => {
const spy = [];
AdapterId.setSpy(spy);
expect(spy.length).toBe(0);
Expand All @@ -96,7 +96,7 @@ describe('wiring', () => {
expect(spy[1].method).toBe('update');
});

it('should call synchronously update only once when the component is created and a wire dynamic param is modified', () => {
it('should call synchronously update only once when the component is created and a wire dynamic param is modified', async () => {
const spy = [];
AdapterId.setSpy(spy);
expect(spy.length).toBe(0);
Expand All @@ -112,7 +112,7 @@ describe('wiring', () => {
});
});

it('should be called next tick when the component with wire that has dynamic params is created', () => {
it('should be called next tick when the component with wire that has dynamic params is created', async () => {
const spy = [];
AdapterId.setSpy(spy);
expect(spy.length).toBe(0);
Expand All @@ -130,7 +130,7 @@ describe('wiring', () => {
});
});

it('should call update only once when the component is created and a wire dynamic param is modified in the same tick', () => {
it('should call update only once when the component is created and a wire dynamic param is modified in the same tick', async () => {
const spy = [];
AdapterId.setSpy(spy);
expect(spy.length).toBe(0);
Expand All @@ -147,7 +147,7 @@ describe('wiring', () => {
});
});

it('should be called only once during multiple renders when the wire config does not change', () => {
it('should be called only once during multiple renders when the wire config does not change', async () => {
const spy = [];
AdapterId.setSpy(spy);
const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass });
Expand All @@ -165,7 +165,7 @@ describe('wiring', () => {
});
});

it('should be called when the wire parameters change its value.', () => {
it('should be called when the wire parameters change its value.', async () => {
const spy = [];
AdapterId.setSpy(spy);
const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass });
Expand All @@ -186,7 +186,7 @@ describe('wiring', () => {
});
});

it('should be called for common parameter when shared among wires', () => {
it('should be called for common parameter when shared among wires', async () => {
const spy = [];
AdapterId.setSpy(spy);
const elm = createElement('x-bc-consumer', { is: BroadcastConsumer });
Expand All @@ -209,7 +209,7 @@ describe('wiring', () => {
});
});

it('should not update when setting parameter with same value', () => {
it('should not update when setting parameter with same value', async () => {
const spy = [];
const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass });
document.body.appendChild(elm);
Expand Down Expand Up @@ -255,7 +255,7 @@ describe('wiring', () => {
expect(dynamicValue.textContent).toBe('modified value');
});

it('should not call update when component is disconnected.', () => {
it('should not call update when component is disconnected.', async () => {
const spy = [];
AdapterId.setSpy(spy);
const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass });
Expand All @@ -272,7 +272,7 @@ describe('wiring', () => {
});
});

it('should call update when component is re-connected.', () => {
it('should call update when component is re-connected.', async () => {
const spy = [];
AdapterId.setSpy(spy);
const elm = createElement('x-echo-adapter-consumer', { is: ComponentClass });
Expand Down Expand Up @@ -302,7 +302,7 @@ describe('wiring', () => {
});

describe('wired fields', () => {
it('should rerender component when adapter pushes data', () => {
it('should rerender component when adapter pushes data', async () => {
BroadcastAdapter.clearInstances();
const elm = createElement('x-bc-consumer', { is: BroadcastConsumer });
document.body.appendChild(elm);
Expand All @@ -322,7 +322,7 @@ describe('wired fields', () => {
});
});

it('should rerender component when wired field is mutated from within the component', () => {
it('should rerender component when wired field is mutated from within the component', async () => {
BroadcastAdapter.clearInstances();
const elm = createElement('x-bc-consumer', { is: BroadcastConsumer });
document.body.appendChild(elm);
Expand All @@ -343,7 +343,7 @@ describe('wired fields', () => {
});

describe('wired methods', () => {
it('should call component method when wired to a method', () => {
it('should call component method when wired to a method', async () => {
BroadcastAdapter.clearInstances();
const elm = createElement('x-bc-consumer', { is: BroadcastConsumer });
document.body.appendChild(elm);
Expand All @@ -355,7 +355,7 @@ describe('wired methods', () => {
});
});

it('should support method override', () => {
it('should support method override', async () => {
const spy = [];
EchoWireAdapter.setSpy(spy);
const elm = createElement('x-inherited-methods', { is: InheritedMethods });
Expand All @@ -376,7 +376,7 @@ describe('wired methods', () => {
});

describe('context aware', () => {
it('should receive the source element tag name when adapter is constructed', () => {
it('should receive the source element tag name when adapter is constructed', async () => {
const spy = [];
ContextLog.setSpy(spy);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { join } from 'node:path';
import { LWC_VERSION } from '@lwc/shared';
import * as options from './helpers/options.mjs';
import wrapHydrationTest from './helpers/hydration-tests.mjs';
import * as options from '../helpers/options.mjs';

const pluck = (obj, keys) => Object.fromEntries(keys.map((k) => [k, Boolean(obj[k])]));
const pluck = (obj, keys) => Object.fromEntries(keys.map((k) => [k, obj[k]]));
const maybeImport = (file, condition) => (condition ? `await import('${file}');` : '');

/** `process.env` to inject into test environment. */
const env = {
Expand All @@ -20,32 +21,19 @@ const env = {
LWC_VERSION,
NODE_ENV: options.NODE_ENV_FOR_TEST,
};

/** @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-hydration/**/*.spec.js',
'!test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/index.spec.js',
],
nodeResolve: true,
rootDir: import.meta.dirname,
rootDir: join(import.meta.dirname, '..'),
plugins: [
{
resolveImport({ source }) {
if (source === 'test-utils') {
return '/helpers/wtr-utils.mjs';
return '/helpers/utils.mjs';
} else if (source === 'wire-service') {
return '@lwc/wire-service';
}
},
async serve(ctx) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serve is moved to a separate plugin file, since it is different for regular tests vs hydration tests.

// Hydration test "index.spec.js" files are actually just config files.
// They don't directly define the tests. Instead, when we request the file,
// we wrap it with some boilerplate. That boilerplate must include the config
// file we originally requested, so the ?original query parameter is used
// to return the file unmodified.
if (ctx.path.endsWith('.spec.js') && !ctx.query.original) {
return await wrapHydrationTest(ctx.path.slice(1)); // remove leading /
// `/__wds-outside-roout__/${depth}/` === '../'.repeat(depth)
return '/__wds-outside-root__/1/wire-service/dist/index.js';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out what I originally had never actually worked. 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this path, is that where modules compiled by wtr ends up?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a magic path that WTR1 uses to serve files that exist outside the web root (the package directory). In our case, it's primarily used by WTR for resolving node_modules in the monorepo root. I think it's mostly intended as an internal detail to make node imports "just work", so this solution is a bit of a hack.

1 technically @web/dev-server, hence wds

}
},
async transform(ctx) {
Expand All @@ -65,9 +53,11 @@ export default {
globalThis.lwcRuntimeFlags = ${JSON.stringify(
pluck(options, ['DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE'])
)};

${maybeImport('@lwc/synthetic-shadow', !options.DISABLE_SYNTHETIC)}
${maybeImport('@lwc/aria-reflection', options.ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL)}
</script>
<script type="module" src="./helpers/setup.mjs"></script>
<script type="module" src="./helpers/wtr-utils.mjs"></script>
<script type="module" src="${testFramework}"></script>
</body>
</html>`,
Expand Down
19 changes: 19 additions & 0 deletions packages/@lwc/integration-not-karma/configs/hydration.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Use native shadow by default in hydration tests; MUST be set before imports
process.env.DISABLE_SYNTHETIC ??= 'true';
Comment on lines +1 to +2
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's probably a more idiomatic way of doing this, but the hack works for now. 🤷

import baseConfig from './base.mjs';
import hydrationTestPlugin from './plugins/serve-hydration.mjs';

/** @type {import("@web/test-runner").TestRunnerConfig} */
export default {
...baseConfig,
files: [
// FIXME: These tests are just symlinks to integration-karma for now so the git diff smaller
'test-hydration/**/*.spec.js',
// FIXME: hits timeout?
'!test-hydration/light-dom/scoped-styles/replace-scoped-styles-with-dynamic-templates/index.spec.js',
// FIXME: This uses ENABLE_SYNTHETIC_SHADOW_IN_MIGRATION to detect status,
// we should just use DISABLE_SYNTHETIC instead
'!test-hydration/synthetic-shadow/index.spec.js',
],
plugins: [...baseConfig.plugins, hydrationTestPlugin],
};
Loading