Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
b6e5d6e
Merge branch 'master' into wjh/more-wtr
wjhsf Jul 11, 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
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 @@ -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
23 changes: 23 additions & 0 deletions packages/@lwc/integration-not-karma/helpers/console.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { spyOn } from '@vitest/spy';

/**
* A much simplified version of the spies originally used for Karma.
* Should probably be eventually replaced with individual spies.
*/
export function spyConsole() {
const log = spyOn(console, 'log');
const warn = spyOn(console, 'warn');
const error = spyOn(console, 'error');
return {
calls: {
log: log.mock.calls,
warn: warn.mock.calls,
error: error.mock.calls,
},
reset() {
log.mockRestore();
warn.mockRestore();
error.mockRestore();
},
};
}
31 changes: 0 additions & 31 deletions packages/@lwc/integration-not-karma/helpers/matchers.mjs

This file was deleted.

117 changes: 117 additions & 0 deletions packages/@lwc/integration-not-karma/helpers/matchers/console.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { spyOn } from '@vitest/spy';

function formatConsoleCall(args) {
// Just calling .join suppresses null/undefined, so we stringify separately
return args.map(String).join(' ');
}

function formatAllCalls(argsList) {
return argsList.map((args) => `"${formatConsoleCall(args)}"`).join(', ');
}

function callAndGetLogs(fn, methodName) {
const spy = spyOn(console, methodName).mockImplementation(() => {});
try {
fn();
return spy.mock.calls;
} finally {
spy.mockRestore();
}
}

function consoleMatcherFactory(chai, utils, methodName, expectInProd) {
return function consoleMatcher(expectedMessages) {
const actual = utils.flag(this, 'object');

if (utils.flag(this, 'negate')) {
// If there's a .not in the assertion chain
const callsArgs = callAndGetLogs(actual, methodName);
if (callsArgs.length === 0) {
return;
}
throw new chai.AssertionError(
`Expect no message but received:\n${formatAllCalls(callsArgs)}`
);
}

if (!Array.isArray(expectedMessages)) {
expectedMessages = [expectedMessages];
}

if (typeof actual !== 'function') {
throw new Error('Expected function to throw error.');
} else if (expectedMessages.some((m) => typeof m !== 'string' && !(m instanceof RegExp))) {
throw new Error(
'Expected a string or a RegExp to compare the thrown error against, or an array of such.'
);
}

const callsArgs = callAndGetLogs(actual, methodName);

if (!expectInProd && process.env.NODE_ENV === 'production') {
if (callsArgs.length !== 0) {
throw new chai.AssertionError(
`Expected console.${
methodName
} to never be called in production mode, but it was called ${
callsArgs.length
} time(s) with ${formatAllCalls(callsArgs)}.`
);
}
} else {
if (callsArgs.length === 0) {
// Result: "string", /regex/
const formattedExpected = expectedMessages
.map((msg) => (typeof msg === 'string' ? JSON.stringify(msg) : msg.toString()))
.join(', ');
throw new chai.AssertionError(
`Expected console.${methodName} to be called with [${
formattedExpected
}], but was never called.`
);
} else {
if (callsArgs.length !== expectedMessages.length) {
throw new chai.AssertionError(
`Expected console.${methodName} to be called ${
expectedMessages.length
} time(s), but was called ${callsArgs.length} time(s).`
);
}
for (let i = 0; i < callsArgs.length; i++) {
const callsArg = callsArgs[i];
const expectedMessage = expectedMessages[i];
const actualMessage = formatConsoleCall(callsArg);

const matches =
typeof expectedMessage === 'string'
? actualMessage === expectedMessage
: expectedMessage.test(actualMessage);
if (!matches) {
throw new chai.AssertionError(
`Expected console.${methodName} to be called with "${
expectedMessage
}", but was called with "${actualMessage}".`
);
}
}
}
}
};
}

/**
* Custom console assertions
* @type {Chai.ChaiPlugin}
*/
export const registerConsoleMatchers = (chai, utils) => {
const customMatchers = {
// FIXME: Add descriptions explaining the what/why of these custom matchers
toLogErrorDev: consoleMatcherFactory(chai, utils, 'error'),
toLogError: consoleMatcherFactory(chai, utils, 'error', true),
toLogWarningDev: consoleMatcherFactory(chai, utils, 'warn'),
};

for (const [name, impl] of Object.entries(customMatchers)) {
utils.addMethod(chai.Assertion.prototype, name, impl);
}
};
144 changes: 144 additions & 0 deletions packages/@lwc/integration-not-karma/helpers/matchers/errors.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// 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);
}

function matchError(error, expectedErrorCtor, expectedMessage) {
if ((!error) instanceof expectedErrorCtor) {
return false;
} else if (typeof expectedMessage === 'undefined') {
return true;
} else if (typeof expectedMessage === 'string') {
return error.message === expectedMessage;
} else {
return expectedMessage.test(error.message);
}
}

function throwDescription(thrown) {
return `${thrown.name} with message "${thrown.message}"`;
}

function errorMatcherFactory(chai, utils, errorListener, expectInProd) {
return function toThrowError(expectedErrorCtor, expectedMessage) {
if (typeof expectedMessage === 'undefined') {
if (typeof expectedErrorCtor === 'undefined') {
// 0 arguments provided
expectedMessage = undefined;
expectedErrorCtor = Error;
} else {
// 1 argument provided
expectedMessage = expectedErrorCtor;
expectedErrorCtor = Error;
}
}

const actual = utils.flag(this, 'object');
if (typeof actual !== 'function') {
throw new Error('Expected function to throw error.');
} else if (expectedErrorCtor !== Error && !(expectedErrorCtor.prototype instanceof Error)) {
throw new Error('Expected an error constructor.');
} else if (
typeof expectedMessage !== 'undefined' &&
typeof expectedMessage !== 'string' &&
!(expectedMessage instanceof RegExp)
) {
throw new Error('Expected a string or a RegExp to compare the thrown error against.');
}

const thrown = errorListener(actual);

if (!expectInProd && process.env.NODE_ENV === 'production') {
if (thrown !== undefined) {
throw new chai.AssertionError(
`Expected function not to throw an error in production mode, but it threw ${throwDescription(
thrown
)}.`
);
}
} else if (thrown === undefined) {
throw new chai.AssertionError(
`Expected function to throw an ${
expectedErrorCtor.name
} error in development mode "${
expectedMessage ? 'with message ' + expectedMessage : ''
}".`
);
} else if (!matchError(thrown, expectedErrorCtor, expectedMessage)) {
throw new chai.AssertionError(
`Expected function to throw an ${
expectedErrorCtor.name
} error in development mode "${
expectedMessage ? 'with message ' + expectedMessage : ''
}", but it threw ${throwDescription(thrown)}.`
);
}
};
}

/** @type {Chai.ChaiPlugin} */
export const registerErrorMatchers = (chai, utils) => {
const matchers = {
toThrowErrorDev: errorMatcherFactory(chai, utils, directErrorListener),
toThrowCallbackReactionErrorDev: errorMatcherFactory(
chai,
utils,
customElementCallbackReactionErrorListener
),
toThrowCallbackReactionError: errorMatcherFactory(
chai,
utils,
customElementCallbackReactionErrorListener,
true
),
toThrowCallbackReactionErrorEvenInSyntheticLifecycleMode: errorMatcherFactory(
chai,
utils,
windowErrorListener,
true
),
};

for (const [name, impl] of Object.entries(matchers)) {
utils.addMethod(chai.Assertion.prototype, name, impl);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { registerConsoleMatchers } from './console.mjs';
import { registerErrorMatchers } from './errors.mjs';
import { registerJasmineMatchers } from './jasmine.mjs';

export const registerCustomMatchers = (chai, utils) => {
registerConsoleMatchers(chai, utils);
registerErrorMatchers(chai, utils);
registerJasmineMatchers(chai, utils);
};
Loading