Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions example-ssr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,7 @@ We recommend using the same framework for component testing: **WebdriverIO** in
We strongly recommend running performance tests at the page level. The sample tests are shown at the component level for demonstration purposes only.

For sample tests, please refer to: [Performance Sample Test](https://github.com/salesforce/lwc-test/blob/master/example-ssr/src/modules/x/hello/__performance__/hello.ssr-performance.test.js)

### SSR Compiler Version Support

We now support testing against both SSR Compiler v1 and v2. By default, tests run against SSR v2. However, if you want to test with v1, set the environment variable `LWC_SSR_MODE` to `v1`.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import tests from './test-data';

describe('<x-basic>', () => {
it.each(tests)('should render on the server (props = $props)', async ({ props }) => {
const { renderedComponent, snapshotHash } = renderAndHashComponent('x-basic', Basic, props);
const { renderedComponent, snapshotHash } = await renderAndHashComponent(
'x-basic',
Basic,
props
);
expect(renderedComponent).toMatchSnapshot(snapshotHash);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { renderAndHashComponent } from '@lwc/jest-ssr-snapshot-utils';

describe('<x-hello>', () => {
test('should render on the server', async () => {
const { renderedComponent, snapshotHash } = renderAndHashComponent('x-hello', Greeting);
const { renderedComponent, snapshotHash } = await renderAndHashComponent(
'x-hello',
Greeting
);
expect(renderedComponent).toMatchSnapshot(snapshotHash);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { renderAndHashComponent } from '@lwc/jest-ssr-snapshot-utils';

describe('<x-light-dom-click-me>', () => {
test('should render on the server', async () => {
const { renderedComponent, snapshotHash } = renderAndHashComponent(
const { renderedComponent, snapshotHash } = await renderAndHashComponent(
'x-light-dom-click-me',
LightDomClickMe
);
Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.24.4",
"@lwc/compiler": "8.9.0",
"@lwc/engine-dom": "8.9.0",
"@lwc/engine-server": "8.9.0",
"@lwc/ssr-runtime": "^8.16.0",
"@lwc/compiler": "^8.16.0",
"@lwc/engine-dom": "^8.16.0",
"@lwc/engine-server": "^8.16.0",
"@lwc/module-resolver": "^8.1.0",
"@lwc/synthetic-shadow": "8.9.0",
"@lwc/wire-service": "8.9.0",
"@lwc/synthetic-shadow": "^8.16.0",
"@lwc/wire-service": "^8.16.0",
"@lwc/template-compiler": "^8.16.0",
"@types/jest": "^29.5.12",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/@lwc/jest-preset/ssr/jest-preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module.exports = {
snapshotSerializers: [require.resolve('../src/ssr/html-serializer.js')],
resolver: require.resolve('../src/ssr/resolver.js'),
transform: {
'^.+\\.(js|ts|html|css)$': require.resolve('@lwc/jest-transformer'),
'^.+\\.(js|ts|html|css)$': require.resolve('@lwc/jest-transformer/ssr'),
},
testMatch: ['**/__tests__/**/?(*.)ssr-(spec|test).(js|ts)'],
coveragePathIgnorePatterns: ['.css$', '.html$'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ jest.mock('@lwc/engine-server', () => ({
renderComponent: jest.fn(),
}));

jest.mock('@lwc/ssr-runtime', () => ({
serverSideRenderComponent: jest.fn(),
}));

jest.mock('fs', () => ({
readdirSync: jest.fn(),
readFileSync: jest.fn(),
Expand All @@ -19,28 +23,42 @@ jest.mock('crypto', () => ({
})),
}));

const ssrMode = process.env.LWC_SSR_MODE || 'v2';

describe('Snapshot utilities service', () => {
describe("'renderAndHashComponent' service to genarate markup and hash", () => {
it('should render the component and return the markup and snapshot hash', () => {
it('should render the component and return the markup and snapshot hash', async () => {
const mockMarkup = '<div>some markup</div>';
const mockTagName = 'my-component';
const mockCtor = jest.fn();
const mockProps = { prop: 'value' };
const customTestEnv = { wire: '' };

require('@lwc/engine-server').renderComponent.mockReturnValue(mockMarkup);

const result = renderAndHashComponent(mockTagName, mockCtor, mockProps, customTestEnv);
require('@lwc/ssr-runtime').serverSideRenderComponent.mockReturnValue(mockMarkup);
const result = await renderAndHashComponent(
mockTagName,
mockCtor,
mockProps,
customTestEnv
);

expect(result).toEqual({
renderedComponent: mockMarkup,
snapshotHash: 'mockedHash',
});
expect(require('@lwc/engine-server').renderComponent).toHaveBeenCalledWith(
mockTagName,
mockCtor,
mockProps
);
if (ssrMode === 'v1')
expect(require('@lwc/engine-server').renderComponent).toHaveBeenCalledWith(
mockTagName,
mockCtor,
mockProps
);
else
expect(require('@lwc/ssr-runtime').serverSideRenderComponent).toHaveBeenCalledWith(
mockTagName,
mockCtor,
mockProps
);
expect(createHash).toHaveBeenCalledWith('sha256');
});
});
Expand Down
10 changes: 8 additions & 2 deletions packages/@lwc/jest-ssr-snaphot-utils/src/ssr-snapshot-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { renderComponent: lwcRenderComponent } = require('@lwc/engine-server');
const { createHash } = require('crypto');
const { readdirSync, readFileSync } = require('fs');
const { join, dirname, basename, extname } = require('path');
const { serverSideRenderComponent } = require('@lwc/ssr-runtime');

/**
* Renders the component's markup, captures it in a snapshot that has a unique snapshot hash.
Expand All @@ -12,9 +13,14 @@ const { join, dirname, basename, extname } = require('path');
* @param {Object} [customTestEnv={}] - An object representing the custom test env where the component is being validated.
* @returns {{renderedComponent: string, snapshotHash: string}} - An object containing the rendered markup and the generated snapshot hash.
*/
function renderAndHashComponent(tagName, Ctor, props = {}, customTestEnv = {}) {
const renderedComponent = lwcRenderComponent(tagName, Ctor, props);
async function renderAndHashComponent(tagName, Ctor, props = {}, customTestEnv = {}) {
const snapshotHash = generateSnapshotHash(tagName, props, customTestEnv);
const ssrMode = process.env.LWC_SSR_MODE || 'v2';

const renderedComponent =
ssrMode !== 'v1'
? await serverSideRenderComponent(tagName, Ctor, props)

Choose a reason for hiding this comment

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

We recently turned on style dedupe with serverSideRenderComponent in LWR. Should that be an option here? It will change the SSRed output, but I'm not sure if that matters for the unit tests 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Im not really familiar with style dedupe. @divmain Can you confirm if we need this for unit testing?

Copy link
Contributor

Choose a reason for hiding this comment

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

My inclination is to wait on style-dedupe for now:

  • If we turn it on now, everybody's fixtures will change and their tests will fail.
  • If we tell them "your fixtures may change due to style-dedupe" then component owners may simply accept all changes to their fixtures without thinking critically about those changes.
  • However, that means any actual SSRv2 issues will be less likely to be caught.

We can turn style-dedupe on sometime in the future, once we're past the initial surge of SSRv2-related issues.

: lwcRenderComponent(tagName, Ctor, props);

return { renderedComponent, snapshotHash };
}
Expand Down
4 changes: 4 additions & 0 deletions packages/@lwc/jest-transformer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ Update your `jest` config to point the transformer to this package:
}
}
```

### SSR Transformer

For SSR testing, use the new transformer `@lwc/jest-transformer/ssr`, which compiles components to generate the compiled artifact used for SSR rendering.
5 changes: 5 additions & 0 deletions packages/@lwc/jest-transformer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@
],
"files": [
"/src/index.js",
"/src/ssr.js",
"/src/transforms/*.js"
],
"exports": {
".": "./src/index.js",
"./ssr": "./src/ssr.js"
},
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/plugin-proposal-dynamic-import": "^7.18.6",
Expand Down
156 changes: 86 additions & 70 deletions packages/@lwc/jest-transformer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const { isKnownScopedCssFile } = require('@lwc/jest-shared');
const MagicString = require('magic-string');
const babelCore = require('@babel/core');
const lwcCompiler = require('@lwc/compiler');
const { generateScopeTokens } = require('@lwc/template-compiler');
const jestPreset = require('babel-preset-jest');
const babelCommonJs = require('@babel/plugin-transform-modules-commonjs');
const babelDynamicImport = require('@babel/plugin-proposal-dynamic-import');
Expand Down Expand Up @@ -90,81 +91,94 @@ function transformTypeScript(src, filePath) {
return code;
}

module.exports = {
process(src, filePath) {
if (isTypeScript(filePath)) {
src = transformTypeScript(src, filePath);
function transformLWC(src, filePath, isSSR) {
if (isTypeScript(filePath)) {
src = transformTypeScript(src, filePath);
}

// Set default module name and namespace value for the namespace because it can't be properly guessed from the path
const compilerOptions = {
name: 'test',
namespace: 'x',
outputConfig: {
sourcemap: true,
},
experimentalDynamicComponent: {
strictSpecifier: false,
},
scopedStyles: isKnownScopedCssFile(filePath),
enableDynamicComponents: true,
/**
* Prevent causing tons of warning log lines.
* @see {@link https://github.com/salesforce/lwc/pull/3544}
* @see {@link https://github.com/salesforce/lwc/releases/tag/v2.49.1}
*/
...(semver.lt(compilerVersion, '2.49.1') ? { enableLwcSpread: true } : {}),
};
const ssrMode = process.env.LWC_SSR_MODE || 'v2';
if (isSSR) {
if (ssrMode !== 'v1') {
compilerOptions.targetSSR = true;
compilerOptions.ssrMode = 'sync';
}
}

// Set default module name and namespace value for the namespace because it can't be properly guessed from the path
const { code, map, cssScopeTokens, warnings } = lwcCompiler.transformSync(src, filePath, {
name: 'test',
namespace: 'x',
outputConfig: {
sourcemap: true,
},
experimentalDynamicComponent: {
strictSpecifier: false,
},
scopedStyles: isKnownScopedCssFile(filePath),
enableDynamicComponents: true,
/**
* Prevent causing tons of warning log lines.
* @see {@link https://github.com/salesforce/lwc/pull/3544}
* @see {@link https://github.com/salesforce/lwc/releases/tag/v2.49.1}
*/
...(semver.lt(compilerVersion, '2.49.1') ? { enableLwcSpread: true } : {}),
});
const { code, map, warnings } = lwcCompiler.transformSync(src, filePath, compilerOptions);
const cssScopeTokens = filePath.endsWith('.html')
? generateScopeTokens(filePath, 'x', 'test').cssScopeTokens
: undefined;

// Log compiler warnings, if any
if (warnings && warnings.length > 0) {
warnings.forEach((warning) => {
console.warn(
`\x1b[33m[LWC Warn]\x1b[0m(${filePath}): ${warning?.message ?? warning}`
);
});
}
// if is not .js, we add the .compiled extension in the sourcemap
const filename = path.extname(filePath) === '.js' ? filePath : filePath + '.compiled';
// **Note: .html and .css don't return valid sourcemaps cause they are used for rollup
const config = map && map.version ? { inputSourceMap: map } : {};

let result = babelCore.transform(code, { ...BABEL_CONFIG, ...config, filename });

if (cssScopeTokens) {
// Modify the code so that it calls into @lwc/jest-shared and adds the scope token as a
// known scope token so we can replace it later.
// Note we have to modify the code rather than use @lwc/jest-shared directly because
// the transformer does not run in the same Node process as the serializer.
const magicString = new MagicString(result.code);

// lwc-test may live in a different directory from the component module code, so
// we need to provide an absolute path
const jestSharedPath = require.resolve('@lwc/jest-shared');

magicString.append(
`\nconst { addKnownScopeToken } = require(${JSON.stringify(jestSharedPath)});`
);

for (const scopeToken of cssScopeTokens) {
magicString.append(`\naddKnownScopeToken(${JSON.stringify(scopeToken)});`);
}

const map = magicString.generateMap({
source: filePath,
includeContent: true,
});

const modifiedCode = magicString.toString() + `\n//# sourceMappingURL=${map.toUrl()}\n`;

result = {
...result,
code: modifiedCode,
map,
};
// Log compiler warnings, if any
if (warnings && warnings.length > 0) {
warnings.forEach((warning) => {
console.warn(`\x1b[33m[LWC Warn]\x1b[0m(${filePath}): ${warning?.message ?? warning}`);
});
}
// if is not .js, we add the .compiled extension in the sourcemap
const filename = path.extname(filePath) === '.js' ? filePath : filePath + '.compiled';
// **Note: .html and .css don't return valid sourcemaps cause they are used for rollup
const config = map && map.version ? { inputSourceMap: map } : {};

let result = babelCore.transform(code, { ...BABEL_CONFIG, ...config, filename });

if (cssScopeTokens) {
// Modify the code so that it calls into @lwc/jest-shared and adds the scope token as a
// known scope token so we can replace it later.
// Note we have to modify the code rather than use @lwc/jest-shared directly because
// the transformer does not run in the same Node process as the serializer.
const magicString = new MagicString(result.code);

// lwc-test may live in a different directory from the component module code, so
// we need to provide an absolute path
const jestSharedPath = require.resolve('@lwc/jest-shared');

magicString.append(
`\nconst { addKnownScopeToken } = require(${JSON.stringify(jestSharedPath)});`
);

for (const scopeToken of cssScopeTokens) {
magicString.append(`\naddKnownScopeToken(${JSON.stringify(scopeToken)});`);
}

return result;
const map = magicString.generateMap({
source: filePath,
includeContent: true,
});

const modifiedCode = magicString.toString() + `\n//# sourceMappingURL=${map.toUrl()}\n`;

result = {
...result,
code: modifiedCode,
map,
};
}

return result;
}
module.exports = {
process(src, filePath) {
return transformLWC(src, filePath, false);
},

getCacheKey(sourceText, sourcePath, ...rest) {
Expand All @@ -187,3 +201,5 @@ module.exports = {
.digest('hex');
},
};

module.exports.transformLwc = transformLWC;
14 changes: 14 additions & 0 deletions packages/@lwc/jest-transformer/src/ssr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
const jestTranformer = require('./index.js');

module.exports = {
...jestTranformer,
process(src, filePath) {
return jestTranformer.transformLwc(src, filePath, true);
},
};
9 changes: 4 additions & 5 deletions test/src/modules/ssr/basic/__tests__/basic.ssr-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
import { renderComponent } from 'lwc';
import Basic from 'ssr/basic';

it('renders a basic component and saves formatted snapshot', () => {
const renderedComponent = renderComponent('x-basic', Basic, { msg: 'Hello world' });
it('renders a basic component and saves formatted snapshot', async () => {
const renderedComponent = await renderComponent('x-basic', Basic, { msg: 'Hello world' });
expect(renderedComponent).toMatchSnapshot();
});

it('renders a basic component and saves inline formatted snapshot', () => {
const renderedComponent = renderComponent('x-basic', Basic, { msg: 'Hello world' });

it('renders a basic component and saves inline formatted snapshot', async () => {
const renderedComponent = await renderComponent('x-basic', Basic, { msg: 'Hello world' });
expect(renderedComponent).toMatchInlineSnapshot(`
<x-basic>
<template shadowrootmode="open">
Expand Down
Loading