Skip to content

Commit

Permalink
test: add production-mode hydration tests (#4338)
Browse files Browse the repository at this point in the history
Fixes #3253
  • Loading branch information
nolanlawson authored Jul 1, 2024
1 parent baa9932 commit b19fe23
Show file tree
Hide file tree
Showing 42 changed files with 267 additions and 205 deletions.
1 change: 1 addition & 0 deletions .github/workflows/karma.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ jobs:
- run: NODE_ENV_FOR_TEST=production DISABLE_SYNTHETIC=1 yarn sauce:ci
- 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

- name: Upload coverage results
uses: actions/upload-artifact@v4
Expand Down
3 changes: 2 additions & 1 deletion packages/@lwc/integration-karma/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"globals": {
"process": true,
"LWC": true,
"spyOnAllFunctions": true
"spyOnAllFunctions": true,
"TestUtils": true
},

"env": {
Expand Down
30 changes: 30 additions & 0 deletions packages/@lwc/integration-karma/helpers/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,34 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) {

const IS_SYNTHETIC_SHADOW_LOADED = !`${ShadowRoot}`.includes('[native code]');

// Designed for hydration tests, this helper asserts certain error/warn console messages were logged
function createExpectConsoleCallsFunc(devOnly) {
return (consoleCalls, methods) => {
for (const [method, matchers] of Object.entries(methods)) {
const calls = consoleCalls[method];
if (devOnly && process.env.NODE_ENV === 'production') {
// assume no console errors/warnings in production
expect(calls).toHaveSize(0);
} else {
expect(calls).toHaveSize(matchers.length);
for (let i = 0; i < matchers.length; i++) {
const matcher = matchers[i];
const call = calls[i][0];
const message = typeof call === 'string' ? call : call.message;
if (typeof matcher === 'string') {
expect(message).toContain(matcher);
} else {
expect(message).toMatch(matcher);
}
}
}
}
};
}

const expectConsoleCalls = createExpectConsoleCallsFunc(false);
const expectConsoleCallsDev = createExpectConsoleCallsFunc(true);

// These values are based on the API versions in @lwc/shared/api-version
const apiFeatures = {
LOWERCASE_SCOPE_TOKENS: process.env.API_VERSION >= 59,
Expand Down Expand Up @@ -604,6 +632,8 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) {
attachReportingControlDispatcher,
detachReportingControlDispatcher,
IS_SYNTHETIC_SHADOW_LOADED,
expectConsoleCalls,
expectConsoleCallsDev,
...apiFeatures,
};
})(LWC, jasmine, beforeAll);
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const { ENABLE_SYNTHETIC_SHADOW_IN_HYDRATION } = require('../../shared/options')
const karmaPluginHydrationTests = require('../../karma-plugins/hydration-tests');
const karmaPluginEnv = require('../../karma-plugins/env');
const karmaPluginTransformFramework = require('../../karma-plugins/transform-framework.js');
const { GREP, COVERAGE } = require('../../shared/options');
const { GREP, COVERAGE, COVERAGE_DIR_FOR_OPTIONS } = require('../../shared/options');
const { createPattern } = require('../utils');

const BASE_DIR = path.resolve(__dirname, '../../../test-hydration');
Expand Down Expand Up @@ -98,7 +98,7 @@ module.exports = (config) => {
config.plugins.push('karma-coverage');

config.coverageReporter = {
dir: path.resolve(COVERAGE_DIR, 'hydration'),
dir: path.resolve(COVERAGE_DIR, 'hydration', COVERAGE_DIR_FOR_OPTIONS),
reporters: [{ type: 'html' }, { type: 'json' }],
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,13 @@ export default {
expect(divs[i].getAttribute('data-foo')).toEqual(expectedAttrValues[i]);
}

expect(consoleCalls.warn).toHaveSize(0);
expect(consoleCalls.error).toHaveSize(3);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <div>: attribute "data-foo" has different values, expected "undefined" but found null'
);
expect(consoleCalls.error[1][0].message).toContain(
'Mismatch hydrating element <div>: attribute "data-foo" has different values, expected "null" but found null'
);
expect(consoleCalls.error[2][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
warn: [],
error: [
'Mismatch hydrating element <div>: attribute "data-foo" has different values, expected "undefined" but found null',
'Mismatch hydrating element <div>: attribute "data-foo" has different values, expected "null" but found null',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ export default {
hydrateComponent(target, Component, {});

const consoleCalls = consoleSpy.calls;
const expectedMessage = '"hydrateComponent" expects an element that is not hydrated.';
expect(consoleCalls.warn).toHaveSize(1);
expect(consoleCalls.warn[0][0]).toContain(expectedMessage);

TestUtils.expectConsoleCalls(consoleCalls, {
warn: ['"hydrateComponent" expects an element that is not hydrated.'],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ export default {
expect(p.getAttribute('data-same')).toBe('same-value');
expect(p.getAttribute('data-another-diff')).toBe('client-val');

expect(consoleCalls.error).toHaveSize(3);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "title" has different values, expected "client-title" but found "ssr-title"'
);
expect(consoleCalls.error[1][0].message).toContain(
'Mismatch hydrating element <p>: attribute "data-another-diff" has different values, expected "client-val" but found "ssr-val"'
);
expect(consoleCalls.error[2][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Mismatch hydrating element <p>: attribute "title" has different values, expected "client-title" but found "ssr-title"',
'Mismatch hydrating element <p>: attribute "data-another-diff" has different values, expected "client-val" but found "ssr-val"',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export default {
expect(div.getAttribute('data-foo')).toBe('client');
expect(div.getAttribute('data-static')).toBe('same-value');

expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <div>: attribute "data-foo" has different values, expected "client" but found "server"'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Mismatch hydrating element <div>: attribute "data-foo" has different values, expected "client" but found "server"',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ export default {
expect(p).not.toBe(snapshots.p);
expect(p.className).not.toBe(snapshots.classes);

expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c2 c3 c4" but found "c1 c2 c3"'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c2 c3 c4" but found "c1 c2 c3"',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ export default {
expect(p).not.toBe(snapshots.p);
expect(p.className).not.toBe(snapshots.className);

expect(consoleCalls.warn).toHaveSize(0);
expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "class" has different values, expected "null" but found null'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
warn: [],
error: [
'Mismatch hydrating element <p>: attribute "class" has different values, expected "null" but found null',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ export default {
expect(p).not.toBe(snapshots.p);
expect(p.className).not.toBe(snapshots.className);

expect(consoleCalls.warn).toHaveSize(0);
expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "class" has different values, expected "" but found "null"'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
warn: [],
error: [
'Mismatch hydrating element <p>: attribute "class" has different values, expected "" but found "null"',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ export default {
expect(p).not.toBe(snapshots.p);
expect(p.className).not.toBe(snapshots.classes);

expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c3 c2 c1" but found "c1 c2 c3"'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c3 c2 c1" but found "c1 c2 c3"',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ export default {

hydrateComponent(target, Component, {});

expect(consoleSpy.calls.error).toHaveSize(2);
expect(consoleSpy.calls.error[0][0].message).toEqual(
'[LWC error]: Mismatch hydrating element <x-child>: attribute "class" has different values, expected "" but found "foo"\n'
);
expect(consoleSpy.calls.error[1][0].message).toEqual(
'[LWC error]: Hydration completed with errors.\n'
);
const consoleCalls = consoleSpy.calls;
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'[LWC error]: Mismatch hydrating element <x-child>: attribute "class" has different values, expected "" but found "foo"\n',
'[LWC error]: Hydration completed with errors.\n',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ export default {
expect(p.className).not.toBe(snapshots.classes);
expect(p.className).toBe('c1 c2 c3');

expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c1 c2 c3" but found "c1 c3"'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c1 c2 c3" but found "c1 c3"',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ export default {
expect(p.className).not.toBe(snapshots.classes);
expect(p.className).toBe('c1 c3');

expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c1 c3" but found "c1 c2 c3"'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c1 c3" but found "c1 c2 c3"',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ export default {
expect(p).not.toBe(snapshots.p);
expect(p.className).toBe('c1 c2 c3');

expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c1 c2 c3" but found "c3 c2 c1"'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c1 c2 c3" but found "c3 c2 c1"',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ export default {
expect(p.className).not.toBe(snapshots.classes);
expect(p.className).toBe('c1 c2 c3');

expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c1 c2 c3" but found "c1 c3"'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c1 c2 c3" but found "c1 c3"',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ export default {
expect(p.className).not.toBe(snapshots.classes);
expect(p.className).toBe('c1 c3');

expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c1 c3" but found "c1 c2 c3"'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Mismatch hydrating element <p>: attribute "class" has different values, expected "c1 c3" but found "c1 c2 c3"',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export default {
expect(comment.nodeType).toBe(Node.COMMENT_NODE);
expect(comment.nodeValue).toBe(snapshots.text.nodeValue);

expect(consoleCalls.error).toHaveSize(2);
expect(consoleCalls.error[0][0].message).toContain(
'Hydration mismatch: incorrect node type received'
);
expect(consoleCalls.error[1][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Hydration mismatch: incorrect node type received',
'Hydration completed with errors.',
],
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ export default {
expect(p).not.toBe(snapshot.p);
expect(p.textContent).toBe('different-content');

expect(consoleCalls.warn).toHaveSize(1);
expect(consoleCalls.warn[0][0].message).toContain(
'Mismatch hydrating element <div>: innerHTML values do not match for element, will recover from the difference'
);
TestUtils.expectConsoleCallsDev(consoleCalls, {
warn: [
'Mismatch hydrating element <div>: innerHTML values do not match for element, will recover from the difference',
],
});

target.content = '<p>another-content</p>';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,13 @@ export default {
expect(p.getAttribute('style')).toBe('background-color: blue;');
expect(p.getAttribute('data-attrs')).toBe('client-attrs');

expect(consoleCalls.error).toHaveSize(4);
expect(consoleCalls.error[0][0].message).toContain(
'Mismatch hydrating element <p>: attribute "data-attrs" has different values, expected "client-attrs" but found "ssr-attrs"'
);
expect(consoleCalls.error[1][0].message).toContain(
'Mismatch hydrating element <p>: attribute "style" has different values, expected "background-color: blue;" but found "background-color: red;"'
);
expect(consoleCalls.error[2][0].message).toContain(
'Mismatch hydrating element <p>: attribute "class" has different values, expected "client-class" but found "ssr-class"'
);
expect(consoleCalls.error[3][0].message).toContain('Hydration completed with errors.');
TestUtils.expectConsoleCallsDev(consoleCalls, {
error: [
'Mismatch hydrating element <p>: attribute "data-attrs" has different values, expected "client-attrs" but found "ssr-attrs"',
'Mismatch hydrating element <p>: attribute "style" has different values, expected "background-color: blue;" but found "background-color: red;"',
'Mismatch hydrating element <p>: attribute "class" has different values, expected "client-class" but found "ssr-class"',
'Hydration completed with errors.',
],
});
},
};
Loading

0 comments on commit b19fe23

Please sign in to comment.