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
1 change: 1 addition & 0 deletions packages/@lwc/jest-transformer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
],
"files": [
"/src/index.js",
"/src/extract-namespace.js",
"/src/ssr.js",
"/src/transforms/*.js"
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { extractNamespace } = require('../extract-namespace');

describe('extractNamespace', () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  1. Are the filepaths always absolute or do we need tests for relative?
  2. Add tests for modules/x/foo/bar.js and modules/x/foo/bar/baz.js.

it("returns '' when path does not match modules/ or jest-modules/ layout", () => {
expect(extractNamespace('/repo/src/foo.js')).toBe('');
});

it('returns the namespace segment when path is /modules/{namespace}/...', () => {
expect(extractNamespace('/modules/x/foo/bar.js')).toBe('x');
expect(extractNamespace('/modules/x/foo/bar/baz.js')).toBe('x');
expect(extractNamespace('/modules/lightning/spinner/spinner.js')).toBe('lightning');
expect(extractNamespace('/modules/interop/spinner/spinner.js')).toBe('interop');
});

it('returns namespace from modules/{namespace}/...', () => {
expect(extractNamespace('/repo/modules/c/foo/foo.js')).toBe('c');
});

it('returns namespace when path contains __tests__', () => {
expect(extractNamespace('/repo/modules/c/foo/__tests__/foo.test.js')).toBe('c');
});

it('returns namespace from jest-modules/{namespace}/...', () => {
expect(extractNamespace('/repo/jest-modules/interop/bar/bar.js')).toBe('interop');
});

it('normalizes Windows separators before matching', () => {
expect(extractNamespace('C:\\repo\\modules\\lightning\\x\\x.js')).toBe('lightning');
});

it('extracts namespace from absolute paths', () => {
expect(extractNamespace('/var/project/modules/lightning/button/button.js')).toBe(
'lightning'
);
expect(extractNamespace('/repo/nested/jest-modules/interop/cmp/cmp.js')).toBe('interop');
});

it('extracts namespace from relative paths', () => {
expect(extractNamespace('project/modules/smoke/widget/widget.js')).toBe('smoke');
expect(extractNamespace('modules/c/foo/foo.js')).toBe('c');
expect(extractNamespace('./src/jest-modules/interop/x/x.js')).toBe('interop');
expect(extractNamespace('packages/pkg/modules/custom/lib/lib.js')).toBe('custom');
});

it("returns '' when modules segment has no trailing path segment", () => {
expect(extractNamespace('/repo/modules')).toBe('');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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
*/
jest.mock('@lwc/compiler/dist/index.cjs', () => ({
transformSync: jest.fn(() => ({
code: '/* lwc-compiled */',
map: null,
warnings: [],
})),
}));

jest.mock('@babel/core', () => ({
transform: jest.fn(() => ({ code: '/* babel */' })),
}));

jest.mock('@lwc/template-compiler/dist/index.cjs', () => ({
generateScopeTokens: jest.fn(() => ({ cssScopeTokens: undefined })),
}));

const lwcCompiler = require('@lwc/compiler/dist/index.cjs');
const { transformLwc } = require('../index');

describe('transformLwc compiler options (namespace / enablePrivateMethods)', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('sets enablePrivateMethods true when path matches modules/... (lightning)', () => {
transformLwc('export default class {}', '/repo/modules/lightning/foo/foo.js', false);

expect(lwcCompiler.transformSync).toHaveBeenCalledWith(
expect.any(String),
'/repo/modules/lightning/foo/foo.js',
expect.objectContaining({
namespace: 'x',
enablePrivateMethods: true,
})
);
});

it('sets enablePrivateMethods true when path matches modules/... (interop)', () => {
transformLwc('export default class {}', '/repo/modules/interop/bar/bar.js', false);

expect(lwcCompiler.transformSync).toHaveBeenCalledWith(
expect.any(String),
'/repo/modules/interop/bar/bar.js',
expect.objectContaining({
namespace: 'x',
enablePrivateMethods: true,
})
);
});

it('sets enablePrivateMethods false when path matches modules/smoke/...', () => {
transformLwc(
'export default class {}',
'/repo/modules/smoke/privateMethods/privateMethods.js',
false
);

expect(lwcCompiler.transformSync).toHaveBeenCalledWith(
expect.any(String),
'/repo/modules/smoke/privateMethods/privateMethods.js',
expect.objectContaining({
namespace: 'x',
enablePrivateMethods: false,
})
);
});

it('sets enablePrivateMethods false for other modules/... namespaces', () => {
transformLwc('export default class {}', '/repo/modules/c/baz/baz.js', false);

expect(lwcCompiler.transformSync).toHaveBeenCalledWith(
expect.any(String),
'/repo/modules/c/baz/baz.js',
expect.objectContaining({
namespace: 'x',
enablePrivateMethods: false,
})
);
});

it('sets enablePrivateMethods false when path has no modules/ or jest-modules/ segment', () => {
transformLwc('export default class {}', '/repo/src/standalone.js', false);

expect(lwcCompiler.transformSync).toHaveBeenCalledWith(
expect.any(String),
'/repo/src/standalone.js',
expect.objectContaining({
namespace: 'x',
enablePrivateMethods: false,
})
);
});
});
31 changes: 31 additions & 0 deletions packages/@lwc/jest-transformer/src/extract-namespace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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
*/

/**
* Extracts the namespace from file path
* Handles patterns like:
* - modules/{namespace}/{component}/{component}.js
* - modules/{namespace}/{component}/__tests__/...
* - jest-modules/{namespace}/{component}/{component}.js
* Returns '' if the namespace cannot be determined
* @param {string} filePath
* @returns {string}
*/
function extractNamespace(filePath) {
const normalizedPath = filePath.replace(/\\/g, '/');

// Match patterns: modules/{namespace} or jest-modules/{namespace}
const match = normalizedPath.match(/(?:^|\/)(modules|jest-modules)\/([^/]+)\//);

if (match && match[2]) {
return match[2];
}

return '';
}

module.exports = { extractNamespace };
10 changes: 8 additions & 2 deletions packages/@lwc/jest-transformer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const messageChannelScopedImport = require('./transforms/message-channel-scoped-
const accessCheck = require('./transforms/access-check-scoped-import');
const siteScopedImport = require('./transforms/site-scoped-import');
const importMeta = require('./transforms/import-meta');
const { extractNamespace } = require('./extract-namespace');

const BABEL_TS_CONFIG = {
sourceMaps: 'inline',
Expand Down Expand Up @@ -96,7 +97,11 @@ function transformLWC(src, filePath, isSSR) {
src = transformTypeScript(src, filePath);
}

// Set default module name and namespace value for the namespace because it can't be properly guessed from the path
// Extract namespace from the file path
const pathNamespace = extractNamespace(filePath);
const enablePrivateMethods = pathNamespace === 'lightning' || pathNamespace === 'interop';

// Set default module name value for the namespace because it can't be properly guessed from the path
const compilerOptions = {
name: 'test',
namespace: 'x',
Expand All @@ -113,7 +118,7 @@ function transformLWC(src, filePath, isSSR) {
scopedStyles: isKnownScopedCssFile(filePath),
enableDynamicComponents: true,
enableLwcOn: true,
enablePrivateMethods: true,
enablePrivateMethods,
/**
* Prevent causing tons of warning log lines.
* @see {@link https://github.com/salesforce/lwc/pull/3544}
Expand Down Expand Up @@ -209,3 +214,4 @@ module.exports = {
};

module.exports.transformLwc = transformLWC;
module.exports.extractNamespace = extractNamespace;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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
*/
import { createElement } from 'lwc';
import PrivateMethods from '../privateMethods';

describe('lightning private methods', () => {
it('should allow calling private methods from public methods', () => {
const element = createElement('lightning-private-methods', { is: PrivateMethods });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we test both smoke-private-methods and lightning-private-methods, since they'll behave differently?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

sure!

document.body.appendChild(element);

expect(element.publicMethod()).toBe(1);
expect(element.value).toBe(1);
expect(element.getCounter()).toBe(1);
});

it('should maintain state across multiple calls', () => {
const element = createElement('lightning-private-methods', { is: PrivateMethods });
document.body.appendChild(element);

element.publicMethod();
element.publicMethod();
const result = element.publicMethod();

expect(result).toBe(3);
expect(element.getCounter()).toBe(3);
});
});
30 changes: 30 additions & 0 deletions test/src/modules/lightning/privateMethods/privateMethods.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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
*/
import { LightningElement, api } from 'lwc';

export default class PrivateMethods extends LightningElement {
@api value = 0;
counter = 0;

#incrementPrivate() {
this.counter++;
return this.counter;
}

@api
publicMethod() {
// Call private method from public method
const count = this.#incrementPrivate();
this.value = count;
return count;
}

@api
getCounter() {
return this.counter;
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,10 @@
/*
* 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
*/
import { createElement } from 'lwc';
import PrivateMethods from '../privateMethods';

describe('private methods', () => {
it('should allow calling private methods from public methods', () => {
const element = createElement('smoke-private-methods', { is: PrivateMethods });
document.body.appendChild(element);

expect(element.publicMethod()).toBe(1);
expect(element.value).toBe(1);
expect(element.getCounter()).toBe(1);
});

it('should maintain state across multiple calls', () => {
const element = createElement('smoke-private-methods', { is: PrivateMethods });
document.body.appendChild(element);

element.publicMethod();
element.publicMethod();
const result = element.publicMethod();

expect(result).toBe(3);
expect(element.getCounter()).toBe(3);
describe('smoke private methods (smoke-private-methods)', () => {
// A top-level `import '../privateMethods'` runs before tests; Jest transforms that file with
// `enablePrivateMethods: false` for smoke namespace, so compilation throws
it('fails to compile: # private methods need enablePrivateMethods (only lightning/interop paths)', () => {
jest.resetModules();
expect(() => {
require('../privateMethods');
}).toThrow(/LWC1007:[\s\S]*Class private methods are not enabled/);
});
});
7 changes: 1 addition & 6 deletions test/src/modules/smoke/privateMethods/privateMethods.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
/*
* 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
*/
import { LightningElement, api } from 'lwc';

// This component won't fully compile because it's not in lightning or interop namespace (namespace = smoke)
export default class PrivateMethods extends LightningElement {
@api value = 0;
counter = 0;
Expand Down
Loading