/* eslint no-console:0 */
import path from 'path'
import cosmiconfigMock from 'cosmiconfig'
import cpy from 'cpy'
import babel from '@babel/core'
import pluginTester from 'babel-plugin-tester'
import plugin from '../'
const projectRoot = path.join(__dirname, '../../')
jest.mock('cosmiconfig', () => {
const mockSearchSync = jest.fn()
Object.assign(mockSearchSync, {
mockReset() {
return mockSearchSync.mockImplementation(
(filename, configuredCosmiconfig) =>
configuredCosmiconfig.searchSync(filename),
)
},
})
mockSearchSync.mockReset()
const _cosmiconfigMock = (...args) => ({
searchSync(filename) {
return mockSearchSync(
filename,
require.requireActual('cosmiconfig')(...args),
)
},
})
return Object.assign(_cosmiconfigMock, {mockSearchSync})
})
beforeAll(() => {
// copy our mock modules to the node_modules directory
// so we can test how things work when importing a macro
// from the node_modules directory.
return cpy(['**/*.js'], path.join('..', '..', 'node_modules'), {
parents: true,
cwd: path.join(projectRoot, 'other', 'mock-modules'),
})
})
afterEach(() => {
// eslint-disable-next-line
require('babel-plugin-macros-test-fake/macro').innerFn.mockClear()
cosmiconfigMock.mockSearchSync.mockReset()
})
expect.addSnapshotSerializer({
print(val) {
return (
val
.split(projectRoot)
.join('<PROJECT_ROOT>/')
.replace(/\\/g, '/')
// Remove the path of file which thrown an error
.replace(/Error:[^:]*:/, 'Error:')
)
},
test(val) {
return typeof val === 'string'
},
})
pluginTester({
plugin,
snapshot: true,
babelOptions: {
filename: __filename,
parserOpts: {
plugins: ['jsx'],
},
generatorOpts: {quotes: 'double'},
},
tests: [
{
title: 'does nothing to code that does not import macro',
snapshot: false,
code: `
import foo from "./some-file-without-macro";
const bar = require("./some-other-file-without-macro");
`,
},
{
title: 'does nothing but remove macros if it is unused',
snapshot: true,
code: `
import foo from "./fixtures/eval.macro";
const bar = 42;
`,
},
{
title: 'raises an error if macro does not exist',
error: true,
code: `
import foo from './some-macros-that-doesnt-even-need-to-exist.macro'
export default 'something else'
`,
},
{
title: 'works with import',
code: `
import myEval from './fixtures/eval.macro'
const x = myEval\`34 + 45\`
`,
},
{
title: 'works with require',
code: `
const evaler = require('./fixtures/eval.macro')
const x = evaler\`34 + 45\`
`,
},
{
title: 'works with require destructuring',
code: `
const {css, styled} = require('./fixtures/emotion.macro')
const red = css\`
background-color: red;
\`
const Div = styled.div\`
composes: \${red}
color: blue;
\`
`,
},
{
title: 'works with require destructuring and aliasing',
code: `
const {css: CSS, styled: STYLED} = require('./fixtures/emotion.macro')
const red = CSS\`
background-color: red;
\`
const Div = STYLED.div\`
composes: \${red}
color: blue;
\`
`,
},
{
title: 'works with function calls',
code: `
import myEval from './fixtures/eval.macro'
const x = myEval('34 + 45')
`,
},
{
title: 'Works as a JSXElement',
code: `
import MyEval from './fixtures/eval.macro'
const x = <MyEval>34 + 45</MyEval>
`,
},
{
title: 'Supports named imports',
code: `
import {css as CSS, styled as STYLED} from './fixtures/emotion.macro'
const red = CSS\`
background-color: red;
\`
const Div = STYLED.div\`
composes: \${red}
color: blue;
\`
`,
},
{
title: 'supports compiled macros (`__esModule` + `export default`)',
code: `
import {css, styled} from './fixtures/emotion-esm.macro'
const red = css\`
background-color: red;
\`
const Div = styled.div\`
composes: \${red}
color: blue;
\`
`,
},
{
title: 'supports macros from node_modules',
code: `
import fakeMacro from 'babel-plugin-macros-test-fake/macro'
fakeMacro('hi')
`,
teardown() {
try {
// kinda abusing the babel-plugin-tester API here
// to make an extra assertion
// eslint-disable-next-line
const fakeMacro = require('babel-plugin-macros-test-fake/macro')
expect(fakeMacro.innerFn).toHaveBeenCalledTimes(1)
expect(fakeMacro.innerFn).toHaveBeenCalledWith({
references: expect.any(Object),
source: expect.stringContaining(
'babel-plugin-macros-test-fake/macro',
),
state: expect.any(Object),
babel: expect.any(Object),
isBabelMacrosCall: true,
})
expect(fakeMacro.innerFn.mock.calls[0].babel).toBe(babel)
} catch (e) {
console.error(e)
throw e
}
},
},
{
title: 'optionally keep imports (variable assignment)',
code: `
const macro = require('./fixtures/keep-imports.macro')
const red = macro('noop');
`,
},
{
title: 'optionally keep imports (import declaration)',
code: `
import macro from './fixtures/keep-imports.macro'
const red = macro('noop');
`,
},
{
title:
'optionally keep imports in combination with babel-preset-env (#80)',
code: `
import macro from './fixtures/keep-imports.macro'
const red = macro('noop')
`,
babelOptions: {
plugins: [
require.resolve('babel-plugin-transform-es2015-modules-commonjs'),
],
},
},
{
title: 'throws an error if the macro is not properly wrapped',
error: true,
code: `
import unwrapped from './fixtures/non-wrapped.macro'
unwrapped('hey')
`,
},
{
title: 'forwards MacroErrors thrown by the macro',
error: true,
code: `
import errorThrower from './fixtures/macro-error-thrower.macro'
errorThrower('hey')
`,
},
{
title: 'prepends the relative path for errors thrown by the macro',
error: true,
code: `
import errorThrower from './fixtures/error-thrower.macro'
errorThrower('hey')
`,
},
{
title: 'appends the npm URL for errors thrown by node modules',
error: true,
code: `
import errorThrower from 'babel-plugin-macros-test-error-thrower.macro'
errorThrower('hi')
`,
},
{
title:
'appends the npm URL for errors thrown by node modules with a slash',
error: true,
code: `
import errorThrower from 'babel-plugin-macros-test-error-thrower/macro'
errorThrower('hi')
`,
},
{
title: 'macros can set their configName and get their config',
fixture: path.join(__dirname, 'fixtures/config/code.js'),
teardown() {
try {
const babelMacrosConfig = require('./fixtures/config/babel-plugin-macros.config')
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual(
babelMacrosConfig[configurableMacro.configName],
)
configurableMacro.realMacro.mockClear()
} catch (e) {
console.error(e)
throw e
}
},
},
{
title:
'when there is an error reading the config, a helpful message is logged',
error: true,
fixture: path.join(__dirname, 'fixtures/config/code.js'),
setup() {
cosmiconfigMock.mockSearchSync.mockImplementationOnce(() => {
throw new Error('this is a cosmiconfig error')
})
const originalError = console.error
console.error = jest.fn()
return function teardown() {
try {
expect(console.error).toHaveBeenCalledTimes(1)
expect(console.error.mock.calls[0]).toMatchSnapshot()
console.error = originalError
} catch (e) {
console.error(e)
throw e
}
}
},
},
{
title: 'when there is no config to load, then no config is passed',
fixture: path.join(__dirname, 'fixtures/config/code.js'),
setup() {
cosmiconfigMock.mockSearchSync.mockImplementationOnce(() => null)
return function teardown() {
try {
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual(
{},
)
configurableMacro.realMacro.mockClear()
} catch (e) {
console.error(e)
throw e
}
}
},
},
{
title: 'when configuration is specified in plugin options',
pluginOptions: {
configurableMacro: {
someConfig: false,
somePluginConfig: true,
},
},
fixture: path.join(__dirname, 'fixtures/config/code.js'),
teardown() {
try {
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual({
fileConfig: true,
someConfig: true,
somePluginConfig: true,
})
configurableMacro.realMacro.mockClear()
} catch (e) {
console.error(e)
throw e
}
},
},
{
title: 'when configuration is specified in plugin options',
pluginOptions: {
configurableMacro: {
someConfig: false,
somePluginConfig: true,
},
},
fixture: path.join(__dirname, 'fixtures/config/cjs-code.js'),
teardown() {
try {
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual({
fileConfig: true,
someConfig: true,
somePluginConfig: true,
})
configurableMacro.realMacro.mockClear()
} catch (e) {
console.error(e)
throw e
}
},
},
{
title: 'when configuration is specified incorrectly in plugin options',
fixture: path.join(__dirname, 'fixtures/config/code.js'),
pluginOptions: {
configurableMacro: 2,
},
teardown() {
try {
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro).not.toHaveBeenCalledWith(
expect.objectContaining({
config: expect.any,
}),
)
configurableMacro.realMacro.mockClear()
} catch (e) {
console.error(e)
throw e
}
},
},
{
title:
'when plugin options configuration cannot be merged with file configuration',
error: true,
fixture: path.join(__dirname, 'fixtures/primitive-config/code.js'),
pluginOptions: {
configurableMacro: {},
},
},
{
title:
'when a plugin that replaces paths is used, macros still work properly',
fixture: path.join(
__dirname,
'fixtures/path-replace-issue/variable-assignment.js',
),
babelOptions: {
babelrc: true,
},
},
{
title: 'Macros are applied in the order respecting plugins order',
code: `
import Wrap from "./fixtures/jsx-id-prefix.macro";
const bar = Wrap(<div id="d1"><p id="p1"></p></div>);
`,
babelOptions: {
presets: [{plugins: [require('./fixtures/jsx-id-prefix.plugin')]}],
},
},
],
})