Skip to content

Commit e02e9d8

Browse files
authored
test(integration): add missing bits to make more tests pass @W-18763051 (#5411)
* refactor(wtr): move NODE_ENV_FOR_TEST default into options file * chore(wtr): move hooks util to separate file * chore(wtr): move aria utils to separate file * chore(wtr): move constants to separate file * chore(wtr): clean up custom rollup plugin * test(wtr): get hydration tests kinda running * test(wtr): kinda start moving to ESM instead of IIFE * fix(shared): make sanitizeHtmlContent work * chore(ci): run hydration tests in ci * test(wtr): remove unused script * test(wtr): implement missing jasmine assertions * test(wtr): clean up test to make assertions more clear * test(wtr): implement custom console matchers * test(wtr): use simpler console spy * test(wtr): remove outdated, unnecessary spies * test(wtr): move signals util to own file * test(wtr): implement custom error matchers everything passed first try, which is kinda sus * chore: split matchers into separate files * chore: ugh tui go away * chore: remove unused file code is now in helpers/matchers/index.mjs * test(wtr): conditionally import polyfills reduces test failures from 291 to 220
1 parent 82eb255 commit e02e9d8

File tree

13 files changed

+353
-128
lines changed

13 files changed

+353
-128
lines changed

packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/index.spec.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,6 @@ describe('disconnectedCallback for host with slots', () => {
9292
let slotAcceptingChildSpy;
9393
let parent;
9494

95-
beforeAll(() => {
96-
// Ignore the engine logging about passing slot content to a component that does not accept slot
97-
// These should become unnecessary when #869 is fixed
98-
spyOn(console, 'group');
99-
spyOn(console, 'log');
100-
spyOn(console, 'groupEnd');
101-
});
102-
10395
beforeEach(() => {
10496
parentDisconnectSpy = jasmine.createSpy();
10597
slotIgnoringChildSpy = jasmine.createSpy();

packages/@lwc/integration-karma/test/shadow-dom/ignoring-slotted/issue-1090.spec.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,6 @@ import { createElement } from 'lwc';
33
import Parent from 'x/parent';
44

55
describe('Issue #1090', () => {
6-
beforeAll(() => {
7-
// Ignore the engine logging about passing slot content to a component that does not accept slot
8-
// These should become unnecessary when #869 is fixed
9-
spyOn(console, 'group');
10-
spyOn(console, 'log');
11-
spyOn(console, 'groupEnd');
12-
});
13-
146
it('should disconnect slotted content even if it is not allocated into a slot', () => {
157
const elm = createElement('x-parent', { is: Parent });
168
document.body.appendChild(elm);

packages/@lwc/integration-karma/test/template/directive-lwc-dom-manual/index.spec.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,16 @@ describe('dom mutation without the lwc:dom="manual" directive', () => {
1414
const root = createElement('x-without-lwc-dom-manual', { is: withoutLwcDomManual });
1515
document.body.appendChild(root);
1616
const elm = root.shadowRoot.querySelector('div');
17+
const mutate = () => fn(elm);
1718

18-
// eslint-disable-next-line vitest/valid-expect
19-
let expected = expect(() => fn(elm));
19+
const warning = new RegExp(
20+
`\\[LWC warn\\]: The \`${method}\` method is available only on elements that use the \`lwc:dom="manual"\` directive.`
21+
);
2022
if (process.env.NATIVE_SHADOW) {
21-
expected = expected.not; // no error
23+
expect(mutate).not.toLogWarningDev(warning);
24+
} else {
25+
expect(mutate).toLogWarningDev(warning);
2226
}
23-
expected.toLogWarningDev(
24-
new RegExp(
25-
`\\[LWC warn\\]: The \`${method}\` method is available only on elements that use the \`lwc:dom="manual"\` directive.`
26-
)
27-
);
2827
});
2928
}
3029

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { spyOn } from '@vitest/spy';
2+
3+
/**
4+
* A much simplified version of the spies originally used for Karma.
5+
* Should probably be eventually replaced with individual spies.
6+
*/
7+
export function spyConsole() {
8+
const log = spyOn(console, 'log');
9+
const warn = spyOn(console, 'warn');
10+
const error = spyOn(console, 'error');
11+
return {
12+
calls: {
13+
log: log.mock.calls,
14+
warn: warn.mock.calls,
15+
error: error.mock.calls,
16+
},
17+
reset() {
18+
log.mockRestore();
19+
warn.mockRestore();
20+
error.mockRestore();
21+
},
22+
};
23+
}

packages/@lwc/integration-not-karma/helpers/matchers.mjs

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { spyOn } from '@vitest/spy';
2+
3+
function formatConsoleCall(args) {
4+
// Just calling .join suppresses null/undefined, so we stringify separately
5+
return args.map(String).join(' ');
6+
}
7+
8+
function formatAllCalls(argsList) {
9+
return argsList.map((args) => `"${formatConsoleCall(args)}"`).join(', ');
10+
}
11+
12+
function callAndGetLogs(fn, methodName) {
13+
const spy = spyOn(console, methodName).mockImplementation(() => {});
14+
try {
15+
fn();
16+
return spy.mock.calls;
17+
} finally {
18+
spy.mockRestore();
19+
}
20+
}
21+
22+
function consoleMatcherFactory(chai, utils, methodName, expectInProd) {
23+
return function consoleMatcher(expectedMessages) {
24+
const actual = utils.flag(this, 'object');
25+
26+
if (utils.flag(this, 'negate')) {
27+
// If there's a .not in the assertion chain
28+
const callsArgs = callAndGetLogs(actual, methodName);
29+
if (callsArgs.length === 0) {
30+
return;
31+
}
32+
throw new chai.AssertionError(
33+
`Expect no message but received:\n${formatAllCalls(callsArgs)}`
34+
);
35+
}
36+
37+
if (!Array.isArray(expectedMessages)) {
38+
expectedMessages = [expectedMessages];
39+
}
40+
41+
if (typeof actual !== 'function') {
42+
throw new Error('Expected function to throw error.');
43+
} else if (expectedMessages.some((m) => typeof m !== 'string' && !(m instanceof RegExp))) {
44+
throw new Error(
45+
'Expected a string or a RegExp to compare the thrown error against, or an array of such.'
46+
);
47+
}
48+
49+
const callsArgs = callAndGetLogs(actual, methodName);
50+
51+
if (!expectInProd && process.env.NODE_ENV === 'production') {
52+
if (callsArgs.length !== 0) {
53+
throw new chai.AssertionError(
54+
`Expected console.${
55+
methodName
56+
} to never be called in production mode, but it was called ${
57+
callsArgs.length
58+
} time(s) with ${formatAllCalls(callsArgs)}.`
59+
);
60+
}
61+
} else {
62+
if (callsArgs.length === 0) {
63+
// Result: "string", /regex/
64+
const formattedExpected = expectedMessages
65+
.map((msg) => (typeof msg === 'string' ? JSON.stringify(msg) : msg.toString()))
66+
.join(', ');
67+
throw new chai.AssertionError(
68+
`Expected console.${methodName} to be called with [${
69+
formattedExpected
70+
}], but was never called.`
71+
);
72+
} else {
73+
if (callsArgs.length !== expectedMessages.length) {
74+
throw new chai.AssertionError(
75+
`Expected console.${methodName} to be called ${
76+
expectedMessages.length
77+
} time(s), but was called ${callsArgs.length} time(s).`
78+
);
79+
}
80+
for (let i = 0; i < callsArgs.length; i++) {
81+
const callsArg = callsArgs[i];
82+
const expectedMessage = expectedMessages[i];
83+
const actualMessage = formatConsoleCall(callsArg);
84+
85+
const matches =
86+
typeof expectedMessage === 'string'
87+
? actualMessage === expectedMessage
88+
: expectedMessage.test(actualMessage);
89+
if (!matches) {
90+
throw new chai.AssertionError(
91+
`Expected console.${methodName} to be called with "${
92+
expectedMessage
93+
}", but was called with "${actualMessage}".`
94+
);
95+
}
96+
}
97+
}
98+
}
99+
};
100+
}
101+
102+
/**
103+
* Custom console assertions
104+
* @type {Chai.ChaiPlugin}
105+
*/
106+
export const registerConsoleMatchers = (chai, utils) => {
107+
const customMatchers = {
108+
// FIXME: Add descriptions explaining the what/why of these custom matchers
109+
toLogErrorDev: consoleMatcherFactory(chai, utils, 'error'),
110+
toLogError: consoleMatcherFactory(chai, utils, 'error', true),
111+
toLogWarningDev: consoleMatcherFactory(chai, utils, 'warn'),
112+
};
113+
114+
for (const [name, impl] of Object.entries(customMatchers)) {
115+
utils.addMethod(chai.Assertion.prototype, name, impl);
116+
}
117+
};
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Listen for errors thrown directly by the callback
2+
function directErrorListener(callback) {
3+
try {
4+
callback();
5+
} catch (error) {
6+
return error;
7+
}
8+
}
9+
10+
// Listen for errors using window.addEventListener('error')
11+
function windowErrorListener(callback) {
12+
let error;
13+
function onError(event) {
14+
event.preventDefault(); // don't log the error
15+
error = event.error;
16+
}
17+
18+
// Prevent jasmine from handling the global error. There doesn't seem to be another
19+
// way to disable this behavior: https://github.com/jasmine/jasmine/pull/1860
20+
const originalOnError = window.onerror;
21+
window.onerror = null;
22+
window.addEventListener('error', onError);
23+
24+
try {
25+
callback();
26+
} finally {
27+
window.onerror = originalOnError;
28+
window.removeEventListener('error', onError);
29+
}
30+
return error;
31+
}
32+
33+
// For errors we expect to be thrown in the connectedCallback() phase
34+
// of a custom element, there are two possibilities:
35+
// 1) We're using non-native lifecycle callbacks, so the error is thrown synchronously
36+
// 2) We're using native lifecycle callbacks, so the error is thrown asynchronously and can
37+
// only be caught with window.addEventListener('error')
38+
// - Note native lifecycle callbacks are all thrown asynchronously.
39+
function customElementCallbackReactionErrorListener(callback) {
40+
return lwcRuntimeFlags.DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE
41+
? directErrorListener(callback)
42+
: windowErrorListener(callback);
43+
}
44+
45+
function matchError(error, expectedErrorCtor, expectedMessage) {
46+
if ((!error) instanceof expectedErrorCtor) {
47+
return false;
48+
} else if (typeof expectedMessage === 'undefined') {
49+
return true;
50+
} else if (typeof expectedMessage === 'string') {
51+
return error.message === expectedMessage;
52+
} else {
53+
return expectedMessage.test(error.message);
54+
}
55+
}
56+
57+
function throwDescription(thrown) {
58+
return `${thrown.name} with message "${thrown.message}"`;
59+
}
60+
61+
function errorMatcherFactory(chai, utils, errorListener, expectInProd) {
62+
return function toThrowError(expectedErrorCtor, expectedMessage) {
63+
if (typeof expectedMessage === 'undefined') {
64+
if (typeof expectedErrorCtor === 'undefined') {
65+
// 0 arguments provided
66+
expectedMessage = undefined;
67+
expectedErrorCtor = Error;
68+
} else {
69+
// 1 argument provided
70+
expectedMessage = expectedErrorCtor;
71+
expectedErrorCtor = Error;
72+
}
73+
}
74+
75+
const actual = utils.flag(this, 'object');
76+
if (typeof actual !== 'function') {
77+
throw new Error('Expected function to throw error.');
78+
} else if (expectedErrorCtor !== Error && !(expectedErrorCtor.prototype instanceof Error)) {
79+
throw new Error('Expected an error constructor.');
80+
} else if (
81+
typeof expectedMessage !== 'undefined' &&
82+
typeof expectedMessage !== 'string' &&
83+
!(expectedMessage instanceof RegExp)
84+
) {
85+
throw new Error('Expected a string or a RegExp to compare the thrown error against.');
86+
}
87+
88+
const thrown = errorListener(actual);
89+
90+
if (!expectInProd && process.env.NODE_ENV === 'production') {
91+
if (thrown !== undefined) {
92+
throw new chai.AssertionError(
93+
`Expected function not to throw an error in production mode, but it threw ${throwDescription(
94+
thrown
95+
)}.`
96+
);
97+
}
98+
} else if (thrown === undefined) {
99+
throw new chai.AssertionError(
100+
`Expected function to throw an ${
101+
expectedErrorCtor.name
102+
} error in development mode "${
103+
expectedMessage ? 'with message ' + expectedMessage : ''
104+
}".`
105+
);
106+
} else if (!matchError(thrown, expectedErrorCtor, expectedMessage)) {
107+
throw new chai.AssertionError(
108+
`Expected function to throw an ${
109+
expectedErrorCtor.name
110+
} error in development mode "${
111+
expectedMessage ? 'with message ' + expectedMessage : ''
112+
}", but it threw ${throwDescription(thrown)}.`
113+
);
114+
}
115+
};
116+
}
117+
118+
/** @type {Chai.ChaiPlugin} */
119+
export const registerErrorMatchers = (chai, utils) => {
120+
const matchers = {
121+
toThrowErrorDev: errorMatcherFactory(chai, utils, directErrorListener),
122+
toThrowCallbackReactionErrorDev: errorMatcherFactory(
123+
chai,
124+
utils,
125+
customElementCallbackReactionErrorListener
126+
),
127+
toThrowCallbackReactionError: errorMatcherFactory(
128+
chai,
129+
utils,
130+
customElementCallbackReactionErrorListener,
131+
true
132+
),
133+
toThrowCallbackReactionErrorEvenInSyntheticLifecycleMode: errorMatcherFactory(
134+
chai,
135+
utils,
136+
windowErrorListener,
137+
true
138+
),
139+
};
140+
141+
for (const [name, impl] of Object.entries(matchers)) {
142+
utils.addMethod(chai.Assertion.prototype, name, impl);
143+
}
144+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { registerConsoleMatchers } from './console.mjs';
2+
import { registerErrorMatchers } from './errors.mjs';
3+
import { registerJasmineMatchers } from './jasmine.mjs';
4+
5+
export const registerCustomMatchers = (chai, utils) => {
6+
registerConsoleMatchers(chai, utils);
7+
registerErrorMatchers(chai, utils);
8+
registerJasmineMatchers(chai, utils);
9+
};

0 commit comments

Comments
 (0)