Skip to content

Commit

Permalink
fix(jest-transformer): update @salesforce/apex transform (#924)
Browse files Browse the repository at this point in the history
* wip: fix @salesforce/apex import

* wip: refreshApex to resolved promise + tests

* wip: cleanup

* wip: different Object for each Apex import

* wip: move schema specific logic to schema transform

* Update packages/@lwc/jest-transformer/src/test/modules/example/apex/apex.js

Co-Authored-By: trevor-bliss <[email protected]>

* wip: update tests

* Apply suggestions from code review

Co-Authored-By: trevor-bliss <[email protected]>

* wip: remove dead code
  • Loading branch information
Trevor authored Jan 8, 2019
1 parent 6e2817f commit a3d6af5
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 82 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 Apex from 'example/apex';

afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
});

describe('example-apex', () => {
describe('importing @salesforce/apex', () => {
it('returns a Promise that resolves for the default import', () => {
const element = createElement('example-apex', { is: Apex });
document.body.appendChild(element);
const apexCall = element.callDefaultImport();
return apexCall.then((ret) => {
expect(ret).toBe('from test');
})
});

it('returns a Promise that resolves for a second imported Apex method', () => {
const element = createElement('example-apex', { is: Apex });
document.body.appendChild(element);
const apexCall = element.callAnotherDefaultImport();
return apexCall.then((ret) => {
expect(ret).toBe('from test');
})
});

it('returns a Promise that resolves for the refreshApex named import', () => {
const element = createElement('example-apex', { is: Apex });
document.body.appendChild(element);
const refreshApex = element.callRefreshApex();
return refreshApex.then((ret) => {
expect(ret).toBe('from test');
});
});
});
});
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 { createElement } from 'lwc';
import Apex from 'example/apex';
import { getSObjectValue } from '@salesforce/apex';

jest.mock('@salesforce/apex', () => {
return {
getSObjectValue: jest.fn(),
}
}, { virtual: true });

afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
});

describe('example-apex', () => {
it('allows per-test mocking of @salesforce/apex.getSObjectValue', () => {
const element = createElement('example-apex', { is: Apex });
document.body.appendChild(element);
element.callGetSObjectValue();
expect(getSObjectValue).toHaveBeenCalledWith('from test');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!--
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
-->
<template></template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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';

import ApexMethod from '@salesforce/apex/FooClass.FooMethod';
import AnotherApexMethod from '@salesforce/apex/Namespace.BarClass.BarMethod';
import { refreshApex, getSObjectValue } from '@salesforce/apex';

export default class Apex extends LightningElement {
@api
callDefaultImport() {
return ApexMethod().then(() => {
return 'from test';
});
}

@api
callAnotherDefaultImport() {
return AnotherApexMethod().then(() => {
return 'from test';
});
}

@api
callRefreshApex() {
return refreshApex().then(() => {
return 'from test';
});
}

@api
callGetSObjectValue() {
getSObjectValue('from test');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ describe('@salesforce/apex import', () => {
try {
myMethod = require("@salesforce/apex/FooController.fooMethod").default;
} catch (e) {
myMethod = function () {
global.__lwcJestMock_myMethod = global.__lwcJestMock_myMethod || function () {
return Promise.resolve();
};
myMethod = global.__lwcJestMock_myMethod;
}
`);

Expand All @@ -33,21 +35,33 @@ describe('@salesforce/apex import', () => {
try {
myMethod = require("@salesforce/apex/FooController.fooMethod").default;
} catch (e) {
myMethod = function () {
global.__lwcJestMock_myMethod = global.__lwcJestMock_myMethod || function () {
return Promise.resolve();
};
myMethod = global.__lwcJestMock_myMethod;
}
`);

test('throws error if using named import', `
import { Id } from '@salesforce/apex/FooController.fooMethod';
`, undefined, 'Invalid import from @salesforce/apex/FooController.fooMethod');
test('transforms named imports from @salesforce/apex', `
import { refreshApex, getSObjectValue } from '@salesforce/apex';
`, `
let refreshApex;
test('throws error if renamed default imports', `
import { default as label } from '@salesforce/apex/FooController.fooMethod';
`, undefined, 'Invalid import from @salesforce/apex/FooController.fooMethod');
try {
refreshApex = require("@salesforce/apex").refreshApex;
} catch (e) {
refreshApex = function () {
return Promise.resolve();
};
}
let getSObjectValue;
test('throws error if renamed multiple default imports', `
import { default as label, foo } from '@salesforce/apex/FooController.fooMethod';
`, undefined, 'Invalid import from @salesforce/apex/FooController.fooMethod');
try {
getSObjectValue = require("@salesforce/apex").getSObjectValue;
} catch (e) {
getSObjectValue = jest.fn();
}
`);
});
116 changes: 113 additions & 3 deletions packages/@lwc/jest-transformer/src/transforms/apex-scoped-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,126 @@
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
const { resolvedPromiseScopedImportTransform } = require('./utils');
const babelTemplate = require('@babel/template').default;
const { getImportInfo } = require('./utils');

const APEX_IMPORT_IDENTIFIER = '@salesforce/apex';

/*
* Apex imports can be used as @wire ids or called directly. If used as a @wire
* id, it must be the same object in the component under test and the test case
* itself. Due to this requirement, we save the mock to the global object to be
* shared.
*/
const resolvedPromiseTemplate = babelTemplate(`
let RESOURCE_NAME;
try {
RESOURCE_NAME = require(IMPORT_SOURCE).default;
} catch (e) {
global.MOCK_NAME = global.MOCK_NAME || function() { return Promise.resolve(); };
RESOURCE_NAME = global.MOCK_NAME;
}
`);

/**
* Instead of using @babel/template, we manually build the variable declaration
* and try/catch block since we don't know how many named imports we have and
* each one needs it's own try/catch block.
*/
function insertNamedImportReplacement(t, path, resource) {
// jest.fn();
const jestFn = t.callExpression(
t.memberExpression(
t.identifier('jest'),
t.identifier('fn')
),
[]
);

// function() { return Promise.resolve(); };
const resolvedPromise = t.functionExpression(
null,
[],
t.blockStatement([
t.returnStatement(
t.callExpression(
t.memberExpression(
t.identifier('Promise'),
t.identifier('resolve'),
),
[]
)
)
])
);

// we know refreshApex returns a Promise, default to jest.fn() to try to be future proof
const fallbackValue = resource === 'refreshApex' ? resolvedPromise : jestFn;

// `let refreshApex;`
path.insertBefore(
t.variableDeclaration(
'let',
[t.VariableDeclarator(t.identifier(resource))]
)
);

// try/catch block
path.insertBefore(
t.tryStatement(
// `refreshApex = require('@salesforce/apex').refreshApex;`
t.blockStatement([
t.expressionStatement(
t.assignmentExpression(
'=',
t.identifier(resource),
t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral(APEX_IMPORT_IDENTIFIER)]),
t.identifier(resource)
)
)
)
]),
// catch block: `refreshApex = jest.fn()`
t.catchClause(
t.identifier('e'),
t.blockStatement([
t.expressionStatement(
t.assignmentExpression(
'=',
t.identifier(resource),
fallbackValue
))
])
)
)
);
}

module.exports = function ({ types: t }) {
return {
visitor: {
ImportDeclaration(path) {
if (path.get('source.value').node.startsWith(APEX_IMPORT_IDENTIFIER)) {
resolvedPromiseScopedImportTransform(t, path, APEX_IMPORT_IDENTIFIER);
const { importSource, resourceNames } = getImportInfo(path, true);

// if '@salesforce/apex' is the exact source that means we have named imports
// e.g. `import { refreshApex, getSObjectValue } from '@salesforce/apex';`
if (importSource === APEX_IMPORT_IDENTIFIER) {
// add a try/catch block defining the imported resource for each named import
resourceNames.forEach(resource => {
insertNamedImportReplacement(t, path, resource);
});

// remove the original import statement
path.remove();
} else if (importSource.startsWith(APEX_IMPORT_IDENTIFIER)) {
// importing anything after '@salesforce/apex' means they're getting a single Apex method as the default import
// e.g. `import myMethod from '@salesforce/apex/FooController.fooMethod';`
path.replaceWithMultiple(resolvedPromiseTemplate({
RESOURCE_NAME: t.identifier(resourceNames[0]),
IMPORT_SOURCE: t.stringLiteral(importSource),
MOCK_NAME: `__lwcJestMock_${resourceNames[0]}`,
}));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,58 @@
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
const { schemaScopedImportTransform } = require('./utils');
const babelTemplate = require('@babel/template').default;
const { getImportInfo } = require('./utils');

const SCHEMA_IMPORT_IDENTIFIER = '@salesforce/schema';

const schemaObjectTemplate = babelTemplate(`
let RESOURCE_NAME;
try {
RESOURCE_NAME = require(IMPORT_SOURCE).default;
} catch (e) {
RESOURCE_NAME = { objectApiName: OBJECT_API_NAME };
}
`);

const schemaObjectAndFieldTemplate = babelTemplate(`
let RESOURCE_NAME;
try {
RESOURCE_NAME = require(IMPORT_SOURCE).default;
} catch (e) {
RESOURCE_NAME = { objectApiName: OBJECT_API_NAME, fieldApiName: FIELD_API_NAME };
}
`);

function schemaScopedImportTransform(t, path) {
const { importSource, resourceNames } = getImportInfo(path);
const defaultImport = resourceNames[0];

const resourcePath = importSource.substring(SCHEMA_IMPORT_IDENTIFIER.length + 1);
const idx = resourcePath.indexOf('.');

if (idx === -1) {
path.replaceWithMultiple(schemaObjectTemplate({
RESOURCE_NAME: t.identifier(defaultImport),
IMPORT_SOURCE: t.stringLiteral(importSource),
OBJECT_API_NAME: t.stringLiteral(resourcePath),
}));
} else {
path.replaceWithMultiple(schemaObjectAndFieldTemplate({
RESOURCE_NAME: t.identifier(defaultImport),
IMPORT_SOURCE: t.stringLiteral(importSource),
OBJECT_API_NAME: t.stringLiteral(resourcePath.substring(0, idx)),
FIELD_API_NAME: t.stringLiteral(resourcePath.substring(idx + 1)),
}));
}
}

module.exports = function ({ types: t }) {
return {
visitor: {
ImportDeclaration(path) {
if (path.get('source.value').node.startsWith(SCHEMA_IMPORT_IDENTIFIER)) {
schemaScopedImportTransform(t, path, SCHEMA_IMPORT_IDENTIFIER);
schemaScopedImportTransform(t, path);
}
}
}
Expand Down
Loading

0 comments on commit a3d6af5

Please sign in to comment.