-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add sample components for ssr unit test (#312)
* tests and snapshot updated * add example component ssr unit test * Sample cmpts * More cleanup * Cleanup * final changes: test and doc updated,setup warn issue fix --------- Co-authored-by: lturanscaia <[email protected]>
- Loading branch information
1 parent
5dc071c
commit 54a7cb6
Showing
38 changed files
with
607 additions
and
223 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# SSR component Unit test setup | ||
|
||
To ensure high-quality rendering for both server and client, separate test suites are necessary: | ||
|
||
- **Server-side tests:** Focus on rendering components to static HTML on the server side. These tests are executed in a Node environment, ensuring that server-side logic works as expected without client-side DOM interactions. | ||
- **Client-side tests:** Validate how components behave post-hydration in a browser-like environment. These tests are executed using JSDOM to simulate browser behavior. | ||
|
||
Combining these tests in the same suite is complex and increases maintenance efforts. By separating them, we ensure better test reliability and coverage. | ||
|
||
## Jest configuration | ||
|
||
Jest is the primary tool used for testing, and both "core" and "off-core" repositories rely on it for server-side and client-side tests. The test configuration differs for each environment. | ||
|
||
### Server-side configuration | ||
|
||
In server-side testing, we focus on generating static HTML on the server. Below is a sample configuration file for server-side testing: | ||
|
||
`jest.ssr-server.config.js` | ||
|
||
```js | ||
module.exports = { | ||
displayName: 'Server-side rendering', | ||
preset: '@lwc/jest-preset/ssr-server', | ||
testMatch: ['**/*.ssr-server.(spec|test).(js|ts)'], | ||
collectCoverageFrom: ['**/*.ssr-server.(spec|test).(js|ts)'], | ||
}; | ||
``` | ||
|
||
### Client-side configuration | ||
|
||
For client-side testing, we validate how the component behaves after the client-side hydration. Below is a sample configuration for client-side-rendering testing. | ||
|
||
`jest.ssr-client.config.js` | ||
|
||
```js | ||
module.exports = { | ||
displayName: 'SSR with hydration', | ||
preset: '@lwc/jest-preset/ssr-for-hydration', | ||
setupFilesAfterEnv: ['./jest.ssr-client.setupAfterEnv.js'], | ||
testMatch: ['**/*.ssr-client.(spec|test).(js|ts)'], | ||
transformIgnorePatterns: ['node_modules/(?!(@webcomponents/.+)/)'], | ||
}; | ||
``` | ||
|
||
At present, hydration errors are tracked by monitoring the console.warn event. | ||
|
||
`jest.ssr-client.setupAfterEnv.js` | ||
|
||
```js | ||
let hydrationMismatchOccurred = false; | ||
let hydrationMismatchMessage = ''; | ||
|
||
beforeEach(() => { | ||
// Reset the flag and message before each test | ||
hydrationMismatchOccurred = false; | ||
hydrationMismatchMessage = ''; | ||
|
||
// Spy on console.warn and intercept warnings | ||
jest.spyOn(console, 'warn').mockImplementation((message) => { | ||
if (message.includes('Hydration mismatch')) { | ||
// Set the flag to indicate a hydration mismatch occurred | ||
hydrationMismatchOccurred = true; | ||
// Store the hydration mismatch message | ||
hydrationMismatchMessage = message; | ||
} else { | ||
// If it's not a hydration mismatch, call the original console.warn | ||
console.warn(message); | ||
} | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
// Restore original console.warn after each test | ||
jest.restoreAllMocks(); | ||
|
||
// Check if a hydration mismatch occurred and fail the test if so | ||
if (hydrationMismatchOccurred) { | ||
throw new Error(`Test failed due to hydration mismatch: ${hydrationMismatchMessage}`); | ||
} | ||
}); | ||
``` | ||
|
||
### Main Jest configuration | ||
|
||
The main Jest configuration file combines both server-side and client-side test setups using the "projects" feature in Jest. | ||
|
||
```js | ||
module.exports = { | ||
projects: ['<rootDir>/jest.ssr-server.config.js', '<rootDir>/jest.ssr-client.config.js'], | ||
}; | ||
``` | ||
|
||
### Writing test | ||
|
||
**Server-side snapshot generation** : | ||
Server-side tests generate static HTML markup as snapshots. These snapshots are critical in verifying the consistency of server-rendered output. | ||
|
||
- Step 1: Run server-side tests to generate initial snapshots. | ||
- Step 2: When component changes occur, rerun side-server tests to identify markup changes. If discrepancies arise, the tests will fail. | ||
- Step 3: Update the snapshots once changes are confirmed valid. | ||
|
||
**Hydration using snapshots** : | ||
Client-side tests utilize server-side snapshots to validate the post-hydration behavior of components: | ||
|
||
- Read server-side-generated snapshots. | ||
- Insert the pre-rendered markup into the DOM. | ||
- Hydrate the component and validate its behavior in the client environment. | ||
|
||
**Snapshot management** : | ||
|
||
- Snapshot hash: Every part of the snapshot associated with a table-driven test case is linked to a unique hash, generated from the component's tag name, properties, and state. This approach ensures the integrity between server-side and client-side tests by enabling precise comparisons during hydration, ensuring that each test case aligns with its expected rendered output. | ||
- Updating snapshots: After modifying a component, update the corresponding snapshot to ensure alignment with changes. | ||
|
||
For more details about how snapshots are generated, updated, and how to use APIs like generateAndSnapshotMarkup and readSnapshotMarkup, refer to [ snapshot utils](../packages/@lwc/jest-ssr-snaphot-utils/README.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
module.exports = { | ||
displayName: 'SSR with hydration', | ||
preset: '@lwc/jest-preset/ssr-for-hydration', | ||
setupFilesAfterEnv: ['./jest.ssr-client.setupAfterEnv.js'], | ||
testMatch: ['**/*.ssr-client.(spec|test).(js|ts)'], | ||
transformIgnorePatterns: ['node_modules/(?!(@webcomponents/.+)/)'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
let hydrationMismatchOccurred = false; | ||
let hydrationMismatchMessage = ''; | ||
|
||
beforeEach(() => { | ||
// Reset the flag and message before each test | ||
hydrationMismatchOccurred = false; | ||
hydrationMismatchMessage = ''; | ||
|
||
// Spy on console.warn and intercept warnings | ||
jest.spyOn(console, 'warn').mockImplementation((message) => { | ||
if (message.includes('Hydration mismatch')) { | ||
// Set the flag to indicate a hydration mismatch occurred | ||
hydrationMismatchOccurred = true; | ||
// Store the hydration mismatch message | ||
hydrationMismatchMessage = message; | ||
} else { | ||
// If it's not a hydration mismatch, call the original console.warn | ||
console.warn(message); | ||
} | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
// Restore original console.warn after each test | ||
jest.restoreAllMocks(); | ||
|
||
// Check if a hydration mismatch occurred and fail the test if so | ||
if (hydrationMismatchOccurred) { | ||
throw new Error(`Test failed due to hydration mismatch: ${hydrationMismatchMessage}`); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module.exports = { | ||
displayName: 'Server-side rendering', | ||
preset: '@lwc/jest-preset/ssr-server', | ||
testMatch: ['**/*.ssr-server.(spec|test).(js|ts)'], | ||
collectCoverageFrom: ['**/*.ssr-server.(spec|test).(js|ts)'], | ||
}; |
5 changes: 5 additions & 0 deletions
5
...ssr/src/modules/x/basic/__tests__/__snapshots__/basic-data-driven.ssr-server.test.js.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`<x-basic> should render on the server (props = {"msg": "Hello, Universe!"}): 34628b2c2f600c2775bdf4131a576fcf6bc07e003cef10d0d523ff923d7af4e2 1`] = `"<x-basic><template shadowrootmode="open"><h1>Basic, Hello, Universe!</h1></template></x-basic>"`; | ||
exports[`<x-basic> should render on the server (props = {"msg": "Hello, world!"}): ebf35c654b2d8178d7dd129e79ae74bd36c7f5bc3c496babc46f60da73dae93b 1`] = `"<x-basic><template shadowrootmode="open"><h1>Basic, Hello, world!</h1></template></x-basic>"`; |
3 changes: 3 additions & 0 deletions
3
example-ssr/src/modules/x/basic/__tests__/__snapshots__/basic.ssr-server.test.js.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`<x-basic> should render on the server: 4da3f8ca30774dda15f0aa526a0101c3229e8df7b695c739f96bcf1bc52d3ddd 1`] = `"<x-basic><template shadowrootmode="open"><h1>Basic, Welcome!</h1></template></x-basic>"`; |
47 changes: 47 additions & 0 deletions
47
example-ssr/src/modules/x/basic/__tests__/basic-data-driven.ssr-client.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { readSnapshotMarkup } from '@lwc/jest-ssr-snapshot-utils'; | ||
import { hydrateShadowRoots } from '@webcomponents/template-shadowroot'; | ||
import { hydrateComponent } from '@lwc/engine-dom'; | ||
import Basic from '../basic'; | ||
import tests from './ssr'; | ||
|
||
describe('<x-basic>', () => { | ||
let wrapper; | ||
beforeEach(() => { | ||
// Create and append the wrapper element before each test | ||
wrapper = document.createElement('div'); | ||
document.body.appendChild(wrapper); | ||
}); | ||
|
||
afterEach(() => { | ||
// Remove the wrapper element after each test | ||
if (wrapper) { | ||
document.body.removeChild(wrapper); | ||
} | ||
}); | ||
|
||
it.each(tests)('should render on the client (props = $props)', async ({ props }) => { | ||
// Retrieve and set the snapshot markup | ||
const markup = readSnapshotMarkup('x-basic', props); | ||
expect(markup).not.toBeNull(); | ||
wrapper.innerHTML = markup; | ||
|
||
// Hydrate shadow roots and component | ||
hydrateShadowRoots(wrapper); | ||
|
||
const componentEl = wrapper.firstElementChild; | ||
expect(componentEl).toBeInstanceOf(HTMLElement); | ||
expect(componentEl).toHaveProperty('shadowRoot'); | ||
|
||
hydrateComponent(componentEl, Basic, props); | ||
|
||
// Query the h1 element inside the shadow root | ||
const shadowRoot = componentEl.shadowRoot; | ||
expect(shadowRoot).not.toBeNull(); | ||
|
||
const h1El = shadowRoot.querySelector('h1'); | ||
expect(h1El).not.toBeNull(); | ||
|
||
// Validate that the message is correctly displayed | ||
expect(h1El.textContent).toContain(props.msg); | ||
}); | ||
}); |
10 changes: 10 additions & 0 deletions
10
example-ssr/src/modules/x/basic/__tests__/basic-data-driven.ssr-server.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import Basic from '../basic'; | ||
import { renderAndHashComponent } from '@lwc/jest-ssr-snapshot-utils'; | ||
import tests from './ssr'; | ||
|
||
describe('<x-basic>', () => { | ||
it.each(tests)('should render on the server (props = $props)', async ({ props }) => { | ||
const { renderedComponent, snapshotHash } = renderAndHashComponent('x-basic', Basic, props); | ||
expect(renderedComponent).toMatchSnapshot(snapshotHash); | ||
}); | ||
}); |
46 changes: 46 additions & 0 deletions
46
example-ssr/src/modules/x/basic/__tests__/basic.ssr-client.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { readSnapshotMarkup } from '@lwc/jest-ssr-snapshot-utils'; | ||
import { hydrateShadowRoots } from '@webcomponents/template-shadowroot'; | ||
import { hydrateComponent } from '@lwc/engine-dom'; | ||
import Basic from '../basic'; | ||
|
||
describe('<x-basic>', () => { | ||
let wrapper; | ||
beforeEach(() => { | ||
// Create and append the wrapper element before each test | ||
wrapper = document.createElement('div'); | ||
document.body.appendChild(wrapper); | ||
}); | ||
|
||
afterEach(() => { | ||
// Remove the wrapper element after each test | ||
if (wrapper) { | ||
document.body.removeChild(wrapper); | ||
} | ||
}); | ||
|
||
test('should hydrate on the client', () => { | ||
// Retrieve and set the snapshot markup | ||
const markup = readSnapshotMarkup('x-basic', { msg: 'Welcome!' }); | ||
expect(markup).not.toBeNull(); | ||
wrapper.innerHTML = markup; | ||
|
||
// Hydrate shadow roots and component | ||
hydrateShadowRoots(wrapper); | ||
|
||
const componentEl = wrapper.firstElementChild; | ||
expect(componentEl).toBeInstanceOf(HTMLElement); | ||
expect(componentEl).toHaveProperty('shadowRoot'); | ||
|
||
hydrateComponent(componentEl, Basic, { msg: 'Welcome!' }); | ||
|
||
// Query the h1 element inside the shadow root | ||
const shadowRoot = componentEl.shadowRoot; | ||
expect(shadowRoot).not.toBeNull(); | ||
|
||
const h1El = shadowRoot.querySelector('h1'); | ||
expect(h1El).not.toBeNull(); | ||
|
||
// Validate that the message is correctly displayed | ||
expect(h1El.textContent).toContain('Welcome!'); | ||
}); | ||
}); |
11 changes: 11 additions & 0 deletions
11
example-ssr/src/modules/x/basic/__tests__/basic.ssr-server.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import Basic from '../basic'; | ||
import { renderAndHashComponent } from '@lwc/jest-ssr-snapshot-utils'; | ||
|
||
describe('<x-basic>', () => { | ||
test('should render on the server', async () => { | ||
const { renderedComponent, snapshotHash } = renderAndHashComponent('x-basic', Basic, { | ||
msg: 'Welcome!', | ||
}); | ||
expect(renderedComponent).toMatchSnapshot(snapshotHash); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export default [ | ||
{ | ||
props: { msg: 'Hello, Universe!' }, | ||
expected: { msg: 'Hello, Universe!' }, | ||
}, | ||
{ | ||
props: { msg: 'Hello, world!' }, | ||
expected: { msg: 'Hello, world!' }, | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<template> | ||
<h1>Basic, {msg}</h1> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { LightningElement, api } from 'lwc'; | ||
|
||
export default class Basic extends LightningElement { | ||
@api msg; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.